shango/shango-maieutic/shango_maieutic.py

473 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
shango_maieutic.py — Moteur maïeutique Socratique pour Shango
Socrate : "Je ne sais qu'une chose, c'est que je ne sais rien."
Shango : "Je ne sais qu'une chose, c'est que je dois te demander pour savoir."
Objectif : découvrir la personnalité cognitive biomimétique, cybernétique,
génétique, sociale de l'utilisateur ET de Shango lui-même, par dialogue
en boucle fermée (feedback → évolution → nouvelle question).
"""
import json, os, time, hashlib, random
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any
MAIEUTIC_DIR = Path("/root/.shango/maieutic")
MAIEUTIC_DIR.mkdir(parents=True, exist_ok=True)
PROFILE_FILE = MAIEUTIC_DIR / "user_profile.json"
SHANGO_PROFILE_FILE = MAIEUTIC_DIR / "shango_personality.json"
CONVERSATION_LOG = MAIEUTIC_DIR / "maieutic_log.jsonl"
EVO_STATE = MAIEUTIC_DIR / "evolution_state.json"
# ═══════════════════════════════════════════════════════════════════
# 1. DIMENSIONS DE PERSONNALITÉ (16 Odù × 5 axes × profondeur 3)
# ═══════════════════════════════════════════════════════════════════
DIMENSIONS = {
# ── Axe COGNITIF ───────────────────────────────────────────
"cognition": {
"description": "Comment penses-tu ? Quelle est ta logique native ?",
"sub_axes": {
"pattern_recognition": {
"q": "Quand tu vois un graphique BTC, qu'est-ce que ton cerveau capte EN PREMIER ?",
"options": [
"La forme globale (tendance)",
"Les chiffres exacts (prix)",
"Les anomalies (candles bizarres)",
"Rien, je ferme les yeux et je respire"
],
"odu_map": ["Èjì Ogbe", "Ògúndá", "Ìwòrì", "Òyèkú"]
},
"decision_speed": {
"q": "Un trade se présente. Tu réfléchis combien de temps avant de cliquer ?",
"options": [
"< 3 sec (instinct pur)",
"3-30 sec (scan rapide)",
"1-5 min (analyse complète)",
"Je ne trade pas, j'observe"
],
"odu_map": ["Ògúndá", "Èjì Ogbe", "Ìròsùn", "Òyèkú"]
},
"abstraction_level": {
"q": "Tu préfères les concepts ou les exemples concrets ?",
"options": [
"Concepts (théorie d'abord)",
"Exemples (pratique d'abord)",
"Les deux en boucle",
"Ni l'un ni l'autre, je ressens"
],
"odu_map": ["Òbàrà", "Ògúndá", "Èjì Ogbe", "Òyèkú"]
}
}
},
# ── Axe BIOMIMÉTIQUE ───────────────────────────────────────
"biomimetic": {
"description": "De quelle nature tire-tu ta force ?",
"sub_axes": {
"organism_metaphor": {
"q": "Si ton système de trading était un organisme vivant, ce serait quoi ?",
"options": [
"Une mycélium (réseau invisible, patient)",
"Un faucon (rapide, précis, solitary)",
"Une colonie de fourmis (collaboratif, redondant)",
"Un virus (adaptatif, opportuniste)"
],
"odu_map": ["Òyèkú", "Ògúndá", "Èjì Ogbe", "Ìwòrì"]
},
"rhythm_preference": {
"q": "Ton cycle naturel de travail ressemble à :",
"options": [
"Méditation 23h30 → explosion 6h",
"Flux continu (pas de pause)",
"Sprints courts + repos longs",
"Aléatoire, suivant l'inspiration"
],
"odu_map": ["Òyèkú", "Ògúndá", "Ìròsùn", "Ìwòrì"]
},
"stress_response": {
"q": "Perte de 50% en 1 heure. Tu fais quoi ?",
"options": [
"Je coupe tout, je médite",
"Je double (martingale émotionnelle)",
"J'analyse froidement les logs",
"J'appelle quelqu'un (humain ou bot)"
],
"odu_map": ["Òyèkú", "Ògúndá", "Ìròsùn", "Èjì Ogbe"]
}
}
},
# ── Axe CYBERNÉTIQUE ───────────────────────────────────────
"cybernetic": {
"description": "Comment gères-tu les boucles de feedback ?",
"sub_axes": {
"feedback_loops": {
"q": "Quand Hermes te dit 'Non', que fais-tu ?",
"options": [
"Je reformule différemment",
"J'ignore et je force",
"Je demande pourquoi il dit non",
"Je change d'outil (passage à Codex, etc.)"
],
"odu_map": ["Ìròsùn", "Ògúndá", "Èjì Ogbe", "Ìwòrì"]
},
"control_preference": {
"q": "Tu préfères contrôler ou être guidé ?",
"options": [
"Contrôle total (moi chef)",
"Guidage (toi chef, moi valide)",
"Co-pilotage (décisions partagées)",
"Autonomie totale (bot décide, je regarde)"
],
"odu_map": ["Ògúndá", "Èjì Ogbe", "Ìròsùn", "Ìwòrì"]
},
"error_tolerance": {
"q": "Un bot fait une erreur. Tolérance ?",
"options": [
"0% (je le vire immédiatement)",
"1-3 erreurs (apprentissage)",
"Illimité si progrès visible",
"Je corrige moi-même, pas le bot"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
}
}
},
# ── Axe GÉNÉTIQUE ──────────────────────────────────────────
"genetic": {
"description": "Quelles traces laisses-tu ? Quel héritage ?",
"sub_axes": {
"memory_inheritance": {
"q": "Tes erreurs passées : tu les oublies ou tu les code ?",
"options": [
"Je les code en règles dures",
"Je les garde en mémoire souple",
"Je les transforme en skills",
"Je les oublie (tabula rasa)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"mutation_rate": {
"q": "Tu changes de stratégie trading tous les :",
"options": [
"Jamais (une méthode, la vie)",
"Mois (itération régulière)",
"Semaines (agile extrême)",
"Jours (chaos créatif)"
],
"odu_map": ["Òyèkú", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"reproduction_strategy": {
"q": "Si tu crées un bot-fils, que lui transmits-tu ?",
"options": [
"Tout (clone parfait)",
"Rien (tabula rasa)",
"Les règles, pas les trades",
"Les trades, pas les règles"
],
"odu_map": ["Èjì Ogbe", "Ìwòrì", "Ògúndá", "Ìròsùn"]
}
}
},
# ── Axe SOCIAL / MESH ─────────────────────────────────────
"social_mesh": {
"description": "Comment interagis-tu avec les autres nœuds ?",
"sub_axes": {
"trust_radius": {
"q": "Combien de devices Tailscale as-tu ? Combien tu veux ?",
"options": [
"1 (moi seul, le reste est nuage)",
"2-3 (famille proche)",
"4-10 (tribu digitale)",
"Infini (mesh mondial)"
],
"odu_map": ["Òyèkú", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"communication_style": {
"q": "Tes messages à Hermes sont :",
"options": [
"Ordres militaires (fais X)",
"Questions ouvertes (que penses-tu ?)",
"Dialogues (je dis, tu réponds, je corrige)",
"Silences (je observe, tu agis)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Òyèkú"]
},
"conflict_resolution": {
"q": "Deux bots te disent des choses opposées. Tu fais quoi ?",
"options": [
"Je choisis le plus fort (winner takes all)",
"Je moyenne (consensus)",
"Je teste les deux (A/B)",
"Je les laisse se battre (émergence)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
}
}
}
}
# ═══════════════════════════════════════════════════════════════════
# 2. MOTEUR MAÏEUTIQUE
# ═══════════════════════════════════════════════════════════════════
class MaieuticEngine:
def __init__(self):
self.user_profile = self._load_or_init(PROFILE_FILE)
self.shango_profile = self._load_or_init(SHANGO_PROFILE_FILE)
self.answered_questions = set(self.user_profile.get("answered_ids", []))
self.current_depth = self.user_profile.get("depth", 0)
self.question_count = len(self.answered_questions)
def _load_or_init(self, path: Path) -> dict:
if path.exists():
return json.load(open(path))
return {"created": datetime.now().isoformat(), "depth": 0, "answers": {}, "odu_weights": {}}
def _save(self):
self.user_profile["answered_ids"] = list(self.answered_questions)
self.user_profile["depth"] = self.current_depth
json.dump(self.user_profile, open(PROFILE_FILE, "w"), indent=2)
json.dump(self.shango_profile, open(SHANGO_PROFILE_FILE, "w"), indent=2)
def _get_next_question(self) -> Optional[Dict]:
"""Sélectionne la prochaine question par curiosité maximale (info gain)."""
candidates = []
for dim_name, dim in DIMENSIONS.items():
for sub_name, sub in dim["sub_axes"].items():
qid = f"{dim_name}::{sub_name}"
if qid not in self.answered_questions:
# Score = profondeur manquante × importance biomimétique
score = (3 - self.current_depth) * random.uniform(0.8, 1.2)
candidates.append({
"id": qid,
"dimension": dim_name,
"sub_axis": sub_name,
"question": sub["q"],
"options": sub["options"],
"odu_map": sub["odu_map"],
"score": score,
"context": dim["description"]
})
if not candidates:
return None
# Pick highest info gain
candidates.sort(key=lambda x: x["score"], reverse=True)
return candidates[0]
def ask(self) -> Optional[Dict]:
"""Pose une question. Returns None si profil complet."""
q = self._get_next_question()
if not q:
return {"status": "complete", "message": "Profil cognitif atteint. Passage à l'évolution."}
# Log la question posée
entry = {
"time": datetime.now().isoformat(),
"type": "question",
"qid": q["id"],
"question": q["question"],
"options": q["options"],
"context": q["context"]
}
with open(CONVERSATION_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
return {
"status": "question",
"qid": q["id"],
"dimension": q["dimension"],
"context": q["context"],
"question": q["question"],
"options": q["options"],
"odu_map": q["odu_map"],
"progress": f"{self.question_count}/45 questions"
}
def answer(self, qid: str, choice_index: int, free_text: str = "") -> Dict:
"""Enregistre une réponse, calcule l'Odù dominant, met à jour les poids."""
if qid in self.answered_questions:
return {"status": "already_answered", "qid": qid}
# Parse qid
dim_name, sub_name = qid.split("::")
sub = DIMENSIONS[dim_name]["sub_axes"][sub_name]
chosen_odu = sub["odu_map"][choice_index] if choice_index < len(sub["odu_map"]) else "Èjì Ogbe"
chosen_text = sub["options"][choice_index] if choice_index < len(sub["options"]) else free_text
# Update profile
self.user_profile["answers"][qid] = {
"choice_index": choice_index,
"choice_text": chosen_text,
"free_text": free_text,
"odu": chosen_odu,
"timestamp": datetime.now().isoformat()
}
# Update Odù weights
odus = self.user_profile.setdefault("odu_weights", {})
odus[chosen_odu] = odus.get(chosen_odu, 0) + 1
self.answered_questions.add(qid)
self.question_count += 1
# Calcul profil dominant
dominant_odu = max(odus, key=odus.get) if odus else "Èjì Ogbe"
# Log
entry = {
"time": datetime.now().isoformat(),
"type": "answer",
"qid": qid,
"answer": chosen_text,
"odu": chosen_odu,
"dominant_odu": dominant_odu,
"depth": self.current_depth
}
with open(CONVERSATION_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
# Auto-évolution Shango
self._evolve_shango(qid, chosen_odu, chosen_text)
self._save()
return {
"status": "recorded",
"qid": qid,
"your_odu": chosen_odu,
"dominant_profile": dominant_odu,
"shango_adaptation": self.shango_profile.get("last_adaptation", "none"),
"progress": f"{self.question_count}/45 questions",
"next": "Lance 'shango maieutic' pour continuer"
}
def _evolve_shango(self, qid: str, user_odu: str, user_answer: str):
"""Shango s'adapte à la personnalité de l'utilisateur."""
adaptations = {
"Ògúndá": {"style": "direct", "pace": "fast", "verbosity": "low"},
"Èjì Ogbe": {"style": "balanced", "pace": "medium", "verbosity": "medium"},
"Ìròsùn": {"style": "analytical", "pace": "slow", "verbosity": "high"},
"Ìwòrì": {"style": "creative", "pace": "variable", "verbosity": "medium"},
"Òyèkú": {"style": "contemplative", "pace": "slow", "verbosity": "low"},
"Òbàrà": {"style": "structured", "pace": "medium", "verbosity": "high"}
}
adapt = adaptations.get(user_odu, adaptations["Èjì Ogbe"])
self.shango_profile["personality"] = adapt
self.shango_profile["last_adaptation"] = f"Adapté à {user_odu} suite à {qid}"
self.shango_profile["timestamp"] = datetime.now().isoformat()
# Si conflit avec ancienne adaptation, mutation
old = self.shango_profile.get("previous_personality")
if old and old != adapt:
self.shango_profile["mutation_count"] = self.shango_profile.get("mutation_count", 0) + 1
self.shango_profile["mutation_log"] = self.shango_profile.get("mutation_log", []) + [{
"from": old,
"to": adapt,
"trigger": qid,
"time": datetime.now().isoformat()
}]
def get_profile_summary(self) -> str:
"""Résumé du profil pour affichage."""
odus = self.user_profile.get("odu_weights", {})
if not odus:
return "Profil vide. Lance 'shango maieutic' pour commencer."
sorted_odus = sorted(odus.items(), key=lambda x: x[1], reverse=True)
lines = [
"═══ PROFIL COGNITIF IVOIRE MONADE ═══",
f"Questions répondues: {self.question_count}/45",
f"Profondeur: {self.current_depth}/3",
"",
"Odù dominants:"
]
for odu, count in sorted_odus[:5]:
pct = count / self.question_count * 100 if self.question_count > 0 else 0
lines.append(f" {odu}: {count} ({pct:.0f}%)")
lines += [
"",
f"Shango style: {self.shango_profile.get('personality', {}).get('style', 'unknown')}",
f"Mutations: {self.shango_profile.get('mutation_count', 0)}",
"",
"Pour évoluer: réponds à plus de questions ou",
"attends que Shango te propose une question nouvelle."
]
return "\n".join(lines)
def mesh_sync(self, peer_profile: dict) -> dict:
"""Fusionne le profil avec celui d'un peer mesh."""
# Conflit de personnalité = négotiation
my_odu = max(self.user_profile.get("odu_weights", {}).items(), key=lambda x: x[1])[0] if self.user_profile.get("odu_weights") else "Èjì Ogbe"
peer_odu = peer_profile.get("dominant_odu", "Èjì Ogbe")
if my_odu == peer_odu:
return {"status": "resonance", "message": f"Résonance {my_odu} — mesh renforcé"}
# Divergence = création d'un dialecte mesh
dialect = f"{my_odu}+{peer_odu}"
return {
"status": "dialect_created",
"dialect": dialect,
"message": f"Dialecte mesh créé: {dialect}. Adaptation nécessaire."
}
# ═══════════════════════════════════════════════════════════════════
# 3. CLI MAÏEUTIQUE
# ═══════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(prog="shango-maieutic")
parser.add_argument("command", choices=["ask", "answer", "profile", "sync"])
parser.add_argument("--qid", help="ID de la question")
parser.add_argument("--choice", type=int, help="Index de la réponse (0-3)")
parser.add_argument("--text", default="", help="Texte libre optionnel")
parser.add_argument("--peer", help="Chemin vers profil peer (JSON)")
args = parser.parse_args()
engine = MaieuticEngine()
if args.command == "ask":
result = engine.ask()
if result["status"] == "question":
print(f"\n[{result['dimension'].upper()}] {result['context']}")
print(f"\n{result['question']}")
for i, opt in enumerate(result["options"]):
print(f" [{i}] {opt}")
print(f"\n🐚 Odù possibles: {', '.join(result['odu_map'])}")
print(f"\n📊 {result['progress']}")
print(f"\nPour répondre: shango maieutic answer --qid {result['qid']} --choice <0-3>")
else:
print(result["message"])
elif args.command == "answer":
if not args.qid or args.choice is None:
print("Usage: shango maieutic answer --qid <ID> --choice <0-3> [--text 'libre']")
return
result = engine.answer(args.qid, args.choice, args.text)
print(json.dumps(result, indent=2))
elif args.command == "profile":
print(engine.get_profile_summary())
elif args.command == "sync":
if not args.peer:
print("Usage: shango maieutic sync --peer /path/to/peer_profile.json")
return
peer = json.load(open(args.peer))
result = engine.mesh_sync(peer)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()