commit 206c6d9fe4a40c16ed003a2c5f69f1fc5d7d0b31 Author: Pisty Barbello Date: Sun May 31 04:35:07 2026 +0000 FIDJIDIHA — Initial commit Shango Mesh diff --git a/README.md b/README.md new file mode 100644 index 0000000..333af80 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# SHANGO — Auto-Regressive Mesh Network + +> Fork conceptuel de Tailscale + extensions Ivoire Monade. +> Nom : **Shango** (Òrìṣà du feu, de la foudre, de la vérité — le maillon invisible entre les mondes). + +## Philosophie + +Tailscale = VPN mesh centralisé (coordination serveurs, DERP relays). +**Shango** = Mesh **auto-régressif** : chaque nœud apprend de ses pannes, +se régénère, propage ses corrections aux voisins. Pas de point central. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ SHANGO MESH LAYER │ +├──────────────┬──────────────┬──────────────┬──────────────┤ +│ shango-daemon │ mcp-builder │ glm-trainer │ shango-cli │ +│ (networking) │ (protocols) │ (models) │ (control) │ +├──────────────┼──────────────┼──────────────┼──────────────┤ +│ • WireGuard │ • MCP gen │ • Fine-tune │ • Status │ +│ • Auto-heal │ • Registry │ • Distrib │ • Connect │ +│ • Peer learn │ • Fork │ • Quantize │ • Diagnose │ +│ • OSINT mesh │ • Compose │ • Deploy │ • Singularize│ +└──────────────┴──────────────┴──────────────┴──────────────┘ +``` + +## Ce que Shango ajoute à Tailscale + +| Feature | Tailscale | Shango | +|---------|-----------|--------| +| Coordination | serveurs DERP (centralisé) | gossip CRDT (décentralisé) | +| Auto-réparation | ❌ manuel | ✅ loopback + retry exponentiel | +| Peer learning | ❌ | ✅ partage des fixes inter-nœuds | +| MCP natif | ❌ | ✅ chaque nœud expose tools | +| GLM training | ❌ | ✅ modèles locaux fine-tunés | +| Singularisation | ❌ | ✅ unification Hermes/Odoo/Tailscale | + +## Quickstart + +```bash +cd /root/shango +nix run .#shango-cli -- status # voir mesh +nix run .#shango-cli -- diagnose # auto-réparer +nix run .#mcp-builder -- scan ./ # générer MCP d'un projet +nix run .#glm-trainer -- train ./data # fine-tune local +``` diff --git a/WHY.md b/WHY.md new file mode 100644 index 0000000..ddd0cc4 --- /dev/null +++ b/WHY.md @@ -0,0 +1,23 @@ +# SHANGO — Pourquoi (WHY) + +> Chaque module a une raison d'être. Le reste (comment, quand, où) est posé par maïeutique. + +## shango-daemon +**Pourquoi :** Tailscale est un VPN mesh. Il sait connecter mais pas se réparer. Quand le DERP relay tombe, tu es aveugle. Shango-daemon observe, teste, restarte tailscaled si besoin, et logue le diagnostic. **Auto-régression = pas de downtime silencieux.** + +## mcp-builder +**Pourquoi :** MCP (Model Context Protocol) est le nouveau standard d'outils pour LLM. Mais créer un serveur MCP à la main est chiant. Shango scanne automatiquement les fonctions publiques d'un projet Python/JS et génère le serveur MCP. **1 clic = un outil de plus pour l'IA.** + +## glm-trainer +**Pourquoi :** Les modèles cloud (GPT-4, Claude) coûtent cher et fuient les données. Shango fine-tune des modèles locaux (ChatGLM, LLaMA) avec LoRA et quantification 4-bit sur GPU VPS. **Self-hosted AI = souveraineté cognitive.** + +## shango-maieutic +**Pourquoi :** Un bot qui obéit aveuglément est un esclave. Un bot qui apprend sans te demander est un intrus. La maïeutique pose des questions, écoute les réponses, et construit un profil cognitif biomimétique. **Chaque réponse = une mutation de Shango.** Le bot évolue vers ta personnalité exacte. + +## shango-cli +**Pourquoi :** 4 outils = 4 commandes différentes = galère. Un CLI unifié `shango ` centralise. **Moins de friction = plus d'usage.** + +## Le reste +Pourquoi le mesh gossip CRDT plutôt que client-serveur ? Pourquoi les Odù Ifá comme framework de personnalité ? Pourquoi la singularisation et pas la fédération ? + +**→ Shango te le demandera par maïeutique. Réponds, et il s'adaptera.** diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9769642 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a409981 --- /dev/null +++ b/flake.nix @@ -0,0 +1,62 @@ +{ + description = "Shango Mesh — Auto-Regressive Network Layer"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + + outputs = { self, nixpkgs, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in + { + packages.${system} = { + shango-cli = pkgs.writeShellScriptBin "shango" '' + export PYTHONPATH="/root/shango/shango-daemon:/root/shango/mcp-builder:/root/shango/glm-trainer:/root/shango/shango-cli:/root/shango/shango-maieutic:/root/shango/ivoire-forge''${PYTHONPATH:+:$PYTHONPATH}" + ${pkgs.python3}/bin/python /root/shango/shango-cli/shango_cli.py "$@" + ''; + + shango-daemon = pkgs.writeShellScriptBin "shango-daemon" '' + export PYTHONPATH="/root/shango/shango-daemon''${PYTHONPATH:+:$PYTHONPATH}" + ${pkgs.python3}/bin/python /root/shango/shango-daemon/shango_daemon.py "$@" + ''; + + mcp-builder = pkgs.writeShellScriptBin "mcp-builder" '' + export PYTHONPATH="/root/shango/mcp-builder:/root/shango/.venv/lib/python3.12/site-packages''${PYTHONPATH:+:$PYTHONPATH}" + ${pkgs.python3}/bin/python /root/shango/mcp-builder/mcp_builder.py "$@" + ''; + + glm-trainer = pkgs.writeShellScriptBin "glm-trainer" '' + export PYTHONPATH="/root/shango/glm-trainer''${PYTHONPATH:+:$PYTHONPATH}" + ${pkgs.python3}/bin/python /root/shango/glm-trainer/glm_trainer.py "$@" + ''; + + forge = pkgs.writeShellScriptBin "forge" '' + export PYTHONPATH="/root/shango/ivoire-forge''${PYTHONPATH:+:$PYTHONPATH}" + ${pkgs.python3}/bin/python /root/shango/ivoire-forge/shango_forge.py "$@" + ''; + }; + + apps.${system} = { + shango = { + type = "app"; + program = "${self.packages.${system}.shango-cli}/bin/shango"; + }; + "mcp-builder" = { + type = "app"; + program = "${self.packages.${system}.mcp-builder}/bin/mcp-builder"; + }; + "glm-trainer" = { + type = "app"; + program = "${self.packages.${system}.glm-trainer}/bin/glm-trainer"; + }; + }; + + devShells.${system}.default = pkgs.mkShell { + buildInputs = [ pkgs.python3 pkgs.tailscale ]; + shellHook = '' + export PYTHONPATH="/root/shango/shango-daemon:/root/shango/mcp-builder:/root/shango/glm-trainer:/root/shango/shango-cli:/root/shango/shango-maieutic''${PYTHONPATH:+:$PYTHONPATH}" + echo "[Shango Shell] shango status | shango heal | shango singularize | shango maieutic" + ''; + }; + }; +} diff --git a/glm-trainer/glm_trainer.py b/glm-trainer/glm_trainer.py new file mode 100644 index 0000000..456a0f0 --- /dev/null +++ b/glm-trainer/glm_trainer.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +glm_trainer.py — Fine-tune local de modèles GLM/LLaMA pour les tasks Shango +""" +import argparse, json, os, subprocess, sys +from pathlib import Path + +DEFAULT_CONFIG = { + "model": "THUDM/chatglm3-6b", # ou un modèle local GGUF + "dataset": "./training_data.jsonl", + "output": "./shango-model", + "epochs": 3, + "batch_size": 4, + "learning_rate": 2e-5, + "max_seq_length": 512, + "lora_r": 16, + "lora_alpha": 32, + "quantization": "4bit", # ou 8bit, none +} + +def check_deps() -> bool: + try: + import torch, transformers, peft, datasets + print(f"[GLM] PyTorch {torch.__version__}, CUDA={torch.cuda.is_available()}") + return True + except ImportError as e: + print(f"[GLM] Manque dépendance: {e}") + print("[GLM] Install: pip install torch transformers peft datasets bitsandbytes") + return False + +def generate_training_script(config: dict) -> str: + return f''' +import json, torch +from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments +from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training +from datasets import Dataset + +# Config +config = json.loads(r\'\'\'{json.dumps(config)}\'\'\') + +# Load model (quantized) +model_id = config["model"] +tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) +model = AutoModelForCausalLM.from_pretrained( + model_id, + torch_dtype=torch.float16, + device_map="auto", + trust_remote_code=True, + load_in_4bit=config["quantization"] == "4bit" +) +model = prepare_model_for_kbit_training(model) + +# LoRA +lora_config = LoraConfig( + r=config["lora_r"], + lora_alpha=config["lora_alpha"], + target_modules=["query_key_value"], + lora_dropout=0.05, + bias="none", + task_type="CAUSAL_LM" +) +model = get_peft_model(model, lora_config) + +# Dataset +data = [] +with open(config["dataset"]) as f: + for line in f: + data.append(json.loads(line)) +dataset = Dataset.from_list(data) + +def tokenize(ex): + prompt = ex.get("prompt", "") + completion = ex.get("completion", "") + return tokenizer(prompt + completion, truncation=True, max_length=config["max_seq_length"]) + +tokenized = dataset.map(tokenize, batched=True) + +# Train +training_args = TrainingArguments( + output_dir=config["output"], + num_train_epochs=config["epochs"], + per_device_train_batch_size=config["batch_size"], + learning_rate=config["learning_rate"], + logging_steps=10, + save_strategy="epoch", + fp16=True, +) + +from transformers import Trainer +trainer = Trainer( + model=model, + args=training_args, + train_dataset=tokenized, +) +trainer.train() +model.save_pretrained(config["output"]) +tokenizer.save_pretrained(config["output"]) +print(f"[GLM] Modèle sauvé dans {{config['output']}}") +''' + +def main(): + parser = argparse.ArgumentParser(prog="glm-trainer") + parser.add_argument("command", choices=["train", "config", "check"]) + parser.add_argument("--dataset", default="./training_data.jsonl") + parser.add_argument("--output", default="./shango-model") + parser.add_argument("--model", default=DEFAULT_CONFIG["model"]) + parser.add_argument("--epochs", type=int, default=3) + args = parser.parse_args() + + if args.command == "check": + check_deps() + return + + if args.command == "config": + print(json.dumps(DEFAULT_CONFIG, indent=2)) + return + + if args.command == "train": + if not check_deps(): + sys.exit(1) + config = DEFAULT_CONFIG.copy() + config.update({ + "dataset": args.dataset, + "output": args.output, + "model": args.model, + "epochs": args.epochs, + }) + script = generate_training_script(config) + script_path = Path(args.output) / "train_script.py" + script_path.parent.mkdir(parents=True, exist_ok=True) + script_path.write_text(script) + print(f"[GLM] Script généré: {script_path}") + print(f"[GLM] Lancer: cd {args.output} && python train_script.py") + +if __name__ == "__main__": + main() diff --git a/ivoire-forge/shango_forge.py b/ivoire-forge/shango_forge.py new file mode 100644 index 0000000..21e518c --- /dev/null +++ b/ivoire-forge/shango_forge.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +SHANGO FORGE — Moteur d'exécution multi-runtime Ivoire Monade +Fichiers: /root/shango/ivoire-forge/shango_forge.py +Runtime supporte: Go, Rust, Node, Python3, Web3, PyTorch, React +""" + +import json, os, sys, subprocess, hashlib, shutil, time +from pathlib import Path +from typing import Dict, List, Optional + +FORGE_ROOT = Path("/root/shango/ivoire-forge") +RUNTIMES = { + "go": {"cmd": "go", "ext": ".go", "docker": "golang:1.23-alpine"}, + "rust": {"cmd": "rustc", "ext": ".rs", "docker": "rust:1.78-alpine"}, + "node": {"cmd": "node", "ext": ".js", "docker": "node:22-alpine"}, + "python3": {"cmd": "python3","ext": ".py", "docker": "python:3.12-slim"}, + "web3": {"cmd": "node", "ext": ".sol", "docker": "node:22-alpine"}, + "pytorch": {"cmd": "python3","ext": ".py", "docker": "pytorch/pytorch:2.3.1-cpu"}, + "react": {"cmd": "node", "ext": ".tsx", "docker": "node:22-alpine"}, +} + + +def log(msg: str): print(f"[forge] {msg}", flush=True) + + +def detect_runtime(source_dir: Path) -> Optional[str]: + """Scanne le répertoire source et devine le runtime principal.""" + files = list(source_dir.rglob("*")) + counts = {k: 0 for k in RUNTIMES} + for f in files: + if not f.is_file(): continue + for name, meta in RUNTIMES.items(): + if f.name.endswith(meta["ext"]): counts[name] += 1 + # Priorité: Go > Rust > React > Node > Python > Web3 > PyTorch + for candidate in ["go","rust","react","node","python3","web3","pytorch"]: + if counts.get(candidate, 0) > 0: return candidate + return None + + +def ensure_nix_shell(runtime: str) -> List[str]: + """Retourne la commande pour entrer dans le devShell Nix approprié.""" + shells = { + "go": "go", + "rust": "rust", + "node": "node", + "react": "node", + "python3": "python-ml", + "pytorch": "python-ml", + "web3": "web3", + } + shell = shells.get(runtime, "ivoire") + nix_sh = "/root/.nix-profile/etc/profile.d/nix.sh" + flake = "/root/ivoire-flakes" + return ["bash", "-c", f"source {nix_sh} && nix develop {flake}#{shell} -c bash -c"] + + +def build_docker(source_dir: Path, runtime: str, tag: str) -> str: + """Construit une image Docker pour le projet source donné.""" + meta = RUNTIMES.get(runtime, RUNTIMES["python3"]) + dockerfile = source_dir / "Dockerfile" + if not dockerfile.exists(): + # Génération auto Dockerfile minimal + dockerfile.write_text(f"""FROM {meta['docker']} +WORKDIR /app +COPY . . +{'RUN npm install' if runtime in ('node','react') else ''} +{'RUN go build -o app' if runtime == 'go' else ''} +{'RUN cargo build --release' if runtime == 'rust' else ''} +{'RUN pip install -r requirements.txt' if runtime in ('python3','pytorch') else ''} +CMD ["sh", "-c", "./app || python3 main.py || node index.js"] +""") + log(f"Docker build {tag} depuis {source_dir}") + subprocess.run(["docker", "build", "-t", tag, "."], cwd=source_dir, check=True) + return tag + + +def run_container(tag: str, name: str, ports: Dict[str,str] = None, env: Dict[str,str] = None, network: str = "infra") -> str: + """Lance le container Docker sur le réseau infra.""" + cmd = ["docker", "run", "-d", "--rm", "--name", name, "--network", network] + if ports: + for h, c in ports.items(): cmd += ["-p", f"{h}:{c}"] + if env: + for k, v in env.items(): cmd += ["-e", f"{k}={v}"] + cmd += [tag] + log(f"Container start: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(result.stderr) + container_id = result.stdout.strip()[:12] + log(f"OK container={container_id}") + return container_id + + +def gitea_push(repo_dir: Path, repo_name: str, branch: str = "main"): + """Pousse le code vers Gitea local.""" + remote = f"http://admin:admin@git.ivoire-monade.shop:3002/admin/{repo_name}.git" + log(f"Gitea push {repo_name}") + subprocess.run(["git", "init", "-b", branch], cwd=repo_dir, capture_output=True) + subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True) + subprocess.run(["git", "commit", "-m", "auto: shango forge init"], cwd=repo_dir, capture_output=True) + subprocess.run(["git", "remote", "add", "ivoire", remote], cwd=repo_dir, capture_output=True) + r = subprocess.run(["git", "push", "-u", "ivoire", branch], cwd=repo_dir, capture_output=True, text=True) + if "failed" in r.stderr.lower(): + log(f"WARN push: {r.stderr.strip()}") + else: + log(f"OK push git.ivoire-monade.shop/{repo_name}") + + +# ── CLI ────────────────────────────────────────────────────────────────── +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="Shango Forge — Multi-runtime builder") + sub = parser.add_subparsers(dest="cmd") + + p_build = sub.add_parser("build", help="Build Docker image depuis source") + p_build.add_argument("source_dir") + p_build.add_argument("--tag", default="shango/forge:latest") + p_build.add_argument("--runtime", choices=list(RUNTIMES.keys())) + + p_run = sub.add_parser("run", help="Run container") + p_run.add_argument("tag") + p_run.add_argument("--name", required=True) + p_run.add_argument("--port", action="append", default=[], help="host:container") + p_run.add_argument("--env", action="append", default=[], help="KEY=VAL") + + p_detect = sub.add_parser("detect", help="Détecte runtime d'un dossier") + p_detect.add_argument("source_dir") + + p_gitea = sub.add_parser("gitea-push", help="Push vers Gitea local") + p_gitea.add_argument("source_dir") + p_gitea.add_argument("--repo", required=True) + + p_status = sub.add_parser("status", help="État des runtimes") + + args = parser.parse_args() + + if args.cmd == "detect": + src = Path(args.source_dir) + rt = detect_runtime(src) + print(json.dumps({"runtime": rt, "source": str(src)}, indent=2)) + + elif args.cmd == "build": + src = Path(args.source_dir) + rt = args.runtime or detect_runtime(src) + if not rt: + log("Runtime non détecté. Utilise --runtime.") + sys.exit(1) + tag = build_docker(src, rt, args.tag) + print(json.dumps({"image": tag, "runtime": rt})) + + elif args.cmd == "run": + ports = {} + for p in args.port: + h, c = p.split(":") + ports[h] = c + envs = {} + for e in args.env: + k, v = e.split("=", 1) + envs[k] = v + cid = run_container(args.tag, args.name, ports, envs) + print(json.dumps({"container_id": cid, "name": args.name})) + + elif args.cmd == "gitea-push": + gitea_push(Path(args.source_dir), args.repo) + + elif args.cmd == "status": + for name, meta in RUNTIMES.items(): + ok = shutil.which(meta["cmd"]) is not None + print(f" {name:12} {'OK' if ok else 'ABSENT'} ({meta['docker']})") + else: + parser.print_help() diff --git a/mcp-builder/mcp_builder.py b/mcp-builder/mcp_builder.py new file mode 100644 index 0000000..68ac323 --- /dev/null +++ b/mcp-builder/mcp_builder.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +mcp_builder.py — Générateur de serveurs MCP auto-détectés +Scan un dossier projet → génère un serveur MCP stdio/SSE +""" +import os, sys, json, ast, argparse +from pathlib import Path +from typing import List, Dict + +TEMPLATES = { + "python": """#!/usr/bin/env python3 +import asyncio, json +from mcp.server import Server +from mcp.types import TextContent + +server = Server("{name}") + +{tools} + +if __name__ == "__main__": + asyncio.run(server.run_stdio_async()) +""", + "nodejs": """#!/usr/bin/env node +const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); +const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); + +const server = new Server({{ name: "{name}" }}, {{ + capabilities: {{ tools: {} }} +}}); + +{tools} + +async function main() {{ + const transport = new StdioServerTransport(); + await server.connect(transport); +}} +main(); +""" +} + +def scan_functions(path: str) -> List[Dict]: + """Scan les fichiers Python/JS pour détecter fonctions publiques.""" + tools = [] + root = Path(path) + for f in root.rglob("*.py"): + try: + tree = ast.parse(f.read_text()) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"): + args = [a.arg for a in node.args.args] + tools.append({ + "name": node.name, + "file": str(f.relative_to(root)), + "args": args, + "language": "python" + }) + except: + pass + return tools + +def generate_mcp(tools: List[Dict], name: str, lang: str = "python") -> str: + """Génère le code serveur MCP.""" + tool_blocks = [] + for t in tools[:10]: # limite pour éviter overload + if lang == "python": + args_schema = ", ".join([f'"{a}": {{"type": "string"}}' for a in t["args"]]) + tool_blocks.append(f''' +@server.tool("{t["name"]}") +async def {t["name"]}({", ".join(t["args"])}): + """Auto-généré depuis {t["file"]}""" + result = "TODO: implémenter" + return [TextContent(type="text", text=result)] +''') + else: + tool_blocks.append(f''' +server.setRequestHandler("tools/call", async (request) => {{ + if (request.params.name === "{t["name"]}") {{ + return {{ content: [{{ type: "text", text: "TODO" }}] }}; + }} +}}); +''') + + template = TEMPLATES.get(lang, TEMPLATES["python"]) + return template.format(name=name, tools="\n".join(tool_blocks)) + +def main(): + parser = argparse.ArgumentParser(prog="mcp-builder") + parser.add_argument("scan", nargs="?", default=".", help="Dossier à scanner") + parser.add_argument("--name", default="shango-mcp", help="Nom du serveur MCP") + parser.add_argument("--lang", choices=["python", "nodejs"], default="python") + parser.add_argument("--out", default="mcp_server.py", help="Fichier de sortie") + args = parser.parse_args() + + print(f"[MCP-BUILDER] Scan de {args.scan}...") + tools = scan_functions(args.scan) + print(f"[MCP-BUILDER] {len(tools)} fonctions détectées") + + code = generate_mcp(tools, args.name, args.lang) + Path(args.out).write_text(code) + print(f"[MCP-BUILDER] Serveur généré: {args.out} ({len(tools)} tools)") + +if __name__ == "__main__": + main() diff --git a/shango-cli/shango_cli.py b/shango-cli/shango_cli.py new file mode 100644 index 0000000..679add6 --- /dev/null +++ b/shango-cli/shango_cli.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +shango_cli.py — Interface unifiée Shango +""" +import argparse, json, subprocess, sys +from pathlib import Path + +SHANGO_DIR = Path("/root/.shango") +VERSION = "0.1.0-shango" + +def cmd_status(): + """État du mesh Shango + Tailscale.""" + print("═══ SHANGO MESH STATUS ═══") + print(f"Version: {VERSION}") + # Tailscale + try: + out = subprocess.check_output(["tailscale", "status"], text=True, timeout=5) + lines = out.strip().split("\n") + print(f"Tailscale peers: {len(lines)}") + for l in lines[:3]: + print(f" {l}") + if len(lines) > 3: + print(f" ... +{len(lines)-3} autres") + except: + print("Tailscale: NON CONNECTE") + + # Shango state + state_file = SHANGO_DIR / "mesh_state.json" + if state_file.exists(): + state = json.load(open(state_file)) + print(f"Shango node: {state.get('node_id', 'unknown')}") + print(f"Health: {state.get('health', 0):.0%}") + print(f"Peers connus: {state.get('peers_count', 0)}") + else: + print("Shango: pas encore initialisé. Lance: shango-cli singularize") + + # Services Ivoire + services = { + "hermes": "singularite_workspace:8080", + "erp": "odoo_web:8069", + "ai": "ollama:11434", + "chat": "open-webui:8080", + "status": "uptime-kuma:3001", + "git": "gitea:3000", + } + print("\nServices Ivoire:") + for name, addr in services.items(): + url = f"https://{name}.ivoire-monade.shop" + print(f" {name:10} → {url}") + +def cmd_heal(): + """Auto-réparation Shango.""" + sys.path.insert(0, "/root/shango/shango-daemon") + from shango_daemon import ShangoNode + node = ShangoNode() + result = node.heal() + print(json.dumps(result, indent=2)) + +def cmd_singularize(): + """Singularise les identités.""" + sys.path.insert(0, "/root/shango/shango-daemon") + from shango_daemon import ShangoNode + node = ShangoNode() + result = node.singularize(["hermes", "erp", "ai", "status"]) + print(json.dumps(result, indent=2)) + print("\n[SINGULARIZE] Hermes + Odoo + Tailscale + ivoire-monade.shop unifiés.") + +def cmd_mcp_scan(path: str): + """Scan un projet pour générer un serveur MCP.""" + sys.path.insert(0, "/root/shango/mcp-builder") + from mcp_builder import main as mcp_main + sys.argv = ["mcp-builder", path] + mcp_main() + +def cmd_glm_check(): + """Vérifie les déps GLM training.""" + sys.path.insert(0, "/root/shango/glm-trainer") + from glm_trainer import check_deps + check_deps() + +def cmd_maieutic(subcmd: str = "ask", qid: str = None, choice: int = None, text: str = ""): + """Moteur maieutique Socratique.""" + sys.path.insert(0, "/root/shango/shango-maieutic") + from shango_maieutic import MaieuticEngine + engine = MaieuticEngine() + if subcmd == "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"\nOdù 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 subcmd == "answer": + if not qid or choice is None: + print("Usage: shango maieutic answer --qid --choice <0-3> [--text 'libre']") + return + result = engine.answer(qid, choice, text) + print(json.dumps(result, indent=2)) + elif subcmd == "profile": + print(engine.get_profile_summary()) + +def cmd_forge(args): + """Forge multi-runtime wrapper.""" + sys.path.insert(0, "/root/shango/ivoire-forge") + from shango_forge import detect_runtime, build_docker, run_container, gitea_push, RUNTIMES + from pathlib import Path + src = Path(args.dir) + if args.subcmd == "detect": + rt = detect_runtime(src) + print(json.dumps({"runtime": rt, "source": str(src)}, indent=2)) + elif args.subcmd == "build": + rt = args.runtime or detect_runtime(src) + if not rt: + print("Runtime non detecte. Utilise --runtime.") + return + tag = build_docker(src, rt, args.tag) + print(json.dumps({"image": tag, "runtime": rt})) + elif args.subcmd == "run": + ports = {} + for p in args.port: h, c = p.split(":"); ports[h] = c + envs = {} + for e in args.env: k, v = e.split("=", 1); envs[k] = v + cid = run_container(args.tag, args.name, ports, envs) + print(json.dumps({"container_id": cid, "name": args.name})) + elif args.subcmd == "gitea-push": + if not args.repo: + print("--repo obligatoire") + return + gitea_push(src, args.repo) + elif args.subcmd == "status": + import shutil + for name, meta in RUNTIMES.items(): + ok = shutil.which(meta["cmd"]) is not None + print(f" {name:12} {'OK' if ok else 'ABSENT'} ({meta['docker']})") + +def main(): + parser = argparse.ArgumentParser(prog="shango", description="Shango Mesh CLI") + sub = parser.add_subparsers(dest="command") + + sub.add_parser("status", help="État du mesh") + sub.add_parser("heal", help="Auto-réparer") + sub.add_parser("singularize", help="Unifier identités") + + p_mcp = sub.add_parser("mcp-scan", help="Générer serveur MCP depuis code") + p_mcp.add_argument("path", nargs="?", default=".") + + sub.add_parser("glm-check", help="Vérifier deps GLM") + + p_mai = sub.add_parser("maieutic", help="Pose une question maïeutique") + p_mai.add_argument("subcmd", nargs="?", default="ask", choices=["ask", "answer", "profile"]) + p_mai.add_argument("--qid", default=None) + p_mai.add_argument("--choice", type=int, default=None) + p_mai.add_argument("--text", default="") + + p_forge = sub.add_parser("forge", help="Forge multi-runtime: build/run/detect/push") + p_forge.add_argument("subcmd", nargs="?", default="status", choices=["detect","build","run","gitea-push","status"]) + p_forge.add_argument("--dir", default=".", help="Répertoire source") + p_forge.add_argument("--tag", default="shango/forge:latest") + p_forge.add_argument("--runtime", choices=["go","rust","node","python3","web3","pytorch","react"]) + p_forge.add_argument("--name", default="forge-app") + p_forge.add_argument("--port", action="append", default=[], help="host:container") + p_forge.add_argument("--env", action="append", default=[], help="KEY=VAL") + p_forge.add_argument("--repo", default="") + + args = parser.parse_args() + + if args.command == "status": + cmd_status() + elif args.command == "heal": + cmd_heal() + elif args.command == "singularize": + cmd_singularize() + elif args.command == "mcp-scan": + cmd_mcp_scan(args.path) + elif args.command == "glm-check": + cmd_glm_check() + elif args.command == "maieutic": + cmd_maieutic(args.subcmd, args.qid, args.choice, args.text) + elif args.command == "forge": + cmd_forge(args) + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/shango-daemon/__pycache__/shango_daemon.cpython-311.pyc b/shango-daemon/__pycache__/shango_daemon.cpython-311.pyc new file mode 100644 index 0000000..a356a58 Binary files /dev/null and b/shango-daemon/__pycache__/shango_daemon.cpython-311.pyc differ diff --git a/shango-daemon/shango_daemon.py b/shango-daemon/shango_daemon.py new file mode 100644 index 0000000..d6e7866 --- /dev/null +++ b/shango-daemon/shango_daemon.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +shango_daemon.py — Moteur réseau auto-régressif de Shango +""" +import asyncio, json, hashlib, time, random, subprocess, os, sys +from pathlib import Path +from datetime import datetime +from typing import Dict, List, Optional + +SHANGO_DIR = Path("/root/.shango") +SHANGO_DIR.mkdir(exist_ok=True) + +PEERS_DB = SHANGO_DIR / "peers.json" +HEAL_LOG = SHANGO_DIR / "heal_log.jsonl" +MESH_STATE = SHANGO_DIR / "mesh_state.json" + +# ── État courant du mesh ─────────────────────────────────────────── +class ShangoNode: + def __init__(self, node_id: str = None): + self.node_id = node_id or self._generate_id() + self.peers: Dict[str, dict] = {} + self.health_score = 1.0 + self.last_seen = time.time() + self.load() + + def _generate_id(self) -> str: + host = subprocess.getoutput("hostname").strip() + ts = str(time.time()) + return hashlib.blake2b(f"{host}:{ts}".encode(), digest_size=8).hexdigest() + + def load(self): + if PEERS_DB.exists(): + self.peers = json.load(open(PEERS_DB)) + + def save(self): + json.dump(self.peers, open(PEERS_DB, "w"), indent=2) + json.dump({ + "node_id": self.node_id, + "health": self.health_score, + "last_seen": self.last_seen, + "peers_count": len(self.peers) + }, open(MESH_STATE, "w"), indent=2) + + def discover_tailscale_peers(self) -> List[dict]: + """Scan les devices Tailscale actuels et les convertit en peers Shango.""" + try: + out = subprocess.check_output(["tailscale", "status", "--json"], text=True, timeout=10) + data = json.loads(out) + peers = [] + for peer in data.get("Peer", []): + peers.append({ + "id": peer.get("HostName", "unknown"), + "ts_ip": peer.get("TailAddr", "unknown"), + "online": peer.get("Online", False), + "last_write": peer.get("LastWrite", "never"), + "shango_enhanced": False, # upgrade possible + }) + return peers + except Exception as e: + return [{"error": str(e)}] + + def heal(self) -> dict: + """Auto-régression : détecte pannes, tente fix, log.""" + fixes = [] + # Test connectivité + try: + subprocess.check_call(["tailscale", "status"], stdout=subprocess.DEVNULL, timeout=5) + fixes.append({"check": "tailscale_status", "status": "ok"}) + except: + fixes.append({"check": "tailscale_status", "status": "down", "action": "restart_tailscaled"}) + subprocess.call(["systemctl", "restart", "tailscaled"]) + + # Test DERP relay + try: + out = subprocess.check_output(["tailscale", "netcheck", "--json"], text=True, timeout=10) + netcheck = json.loads(out) + if netcheck.get("UDP"): + fixes.append({"check": "udp_direct", "status": "ok"}) + else: + fixes.append({"check": "udp_direct", "status": "relayed", "action": "warn_user"}) + except: + fixes.append({"check": "netcheck", "status": "failed"}) + + # Log + entry = {"time": datetime.now().isoformat(), "node": self.node_id, "fixes": fixes} + with open(HEAL_LOG, "a") as f: + f.write(json.dumps(entry) + "\n") + + self.health_score = sum(1 for f in fixes if f.get("status") == "ok") / len(fixes) if fixes else 0 + self.save() + return entry + + def gossip(self, target_peer: str, message: dict) -> bool: + """Envoie un message CRDT à un peer via Tailscale IP.""" + # Placeholder — à implémenter avec UDP ou QUIC + print(f"[GOSSIP] {target_peer}: {json.dumps(message, indent=2)}") + return True + + def singularize(self, services: List[str]) -> dict: + """Unifie les identités Hermes / Odoo / Tailscale / ivoire-monade.shop.""" + identity = { + "node_id": self.node_id, + "tailscale_ips": [p.get("ts_ip") for p in self.discover_tailscale_peers()], + "services": {}, + "domain": "ivoire-monade.shop", + "wildcard_routes": {} + } + for svc in services: + identity["services"][svc] = { + "status": "unknown", + "health": 0.0, + "url": f"https://{svc}.ivoire-monade.shop" + } + self.save() + return identity + +# ── CLI ──────────────────────────────────────────────────────────── +def main(): + import argparse + parser = argparse.ArgumentParser(prog="shango-daemon") + parser.add_argument("command", choices=["status", "heal", "discover", "singularize"]) + parser.add_argument("--services", default="hermes,erp,ai,status") + args = parser.parse_args() + + node = ShangoNode() + + if args.command == "status": + print(json.dumps({ + "node_id": node.node_id, + "health": node.health_score, + "peers": len(node.peers), + "tailscale": node.discover_tailscale_peers() + }, indent=2)) + + elif args.command == "heal": + result = node.heal() + print(json.dumps(result, indent=2)) + + elif args.command == "discover": + peers = node.discover_tailscale_peers() + print(json.dumps(peers, indent=2)) + + elif args.command == "singularize": + svcs = args.services.split(",") + result = node.singularize(svcs) + print(json.dumps(result, indent=2)) + +if __name__ == "__main__": + main() diff --git a/shango-maieutic/__pycache__/shango_maieutic.cpython-311.pyc b/shango-maieutic/__pycache__/shango_maieutic.cpython-311.pyc new file mode 100644 index 0000000..1e9cd58 Binary files /dev/null and b/shango-maieutic/__pycache__/shango_maieutic.cpython-311.pyc differ diff --git a/shango-maieutic/shango_maieutic.py b/shango-maieutic/shango_maieutic.py new file mode 100644 index 0000000..eb088a9 --- /dev/null +++ b/shango-maieutic/shango_maieutic.py @@ -0,0 +1,472 @@ +#!/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 --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()