From 206c6d9fe4a40c16ed003a2c5f69f1fc5d7d0b31 Mon Sep 17 00:00:00 2001 From: Pisty Barbello Date: Sun, 31 May 2026 04:35:07 +0000 Subject: [PATCH] =?UTF-8?q?FIDJIDIHA=20=E2=80=94=20Initial=20commit=20Shan?= =?UTF-8?q?go=20Mesh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 47 ++ WHY.md | 23 + flake.lock | 27 + flake.nix | 62 +++ glm-trainer/glm_trainer.py | 136 +++++ ivoire-forge/shango_forge.py | 172 +++++++ mcp-builder/mcp_builder.py | 103 ++++ shango-cli/shango_cli.py | 190 +++++++ .../__pycache__/shango_daemon.cpython-311.pyc | Bin 0 -> 10208 bytes shango-daemon/shango_daemon.py | 149 ++++++ .../shango_maieutic.cpython-311.pyc | Bin 0 -> 21459 bytes shango-maieutic/shango_maieutic.py | 472 ++++++++++++++++++ 12 files changed, 1381 insertions(+) create mode 100644 README.md create mode 100644 WHY.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 glm-trainer/glm_trainer.py create mode 100644 ivoire-forge/shango_forge.py create mode 100644 mcp-builder/mcp_builder.py create mode 100644 shango-cli/shango_cli.py create mode 100644 shango-daemon/__pycache__/shango_daemon.cpython-311.pyc create mode 100644 shango-daemon/shango_daemon.py create mode 100644 shango-maieutic/__pycache__/shango_maieutic.cpython-311.pyc create mode 100644 shango-maieutic/shango_maieutic.py 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 0000000000000000000000000000000000000000..a356a5865100846792f60c6c9826a1a32759362f GIT binary patch literal 10208 zcmcIKYit`wdb8YJa`_UewMKZff zKUgYJul~Ucbg&aRu$+?-Hn_{lh!52j1?nIDk*kpd3beqkuz-jK1Q;y}H2)Mh2L$e) zelyG0>Jb=+b~OBEcJ`ace)G-u8vWMob|6U9S9@kZtV8HO@k1do`O33K3ZZ+5N0W%B zcuSO;q)2I*w2+dXq)Ew4GNfcDSyEajt)#R~+DOSwa!}Gy`Y8&;y5}k;E9N|t9tv5I;NY!a!rWiM&q$MA!JDltJe8Nc?Gb0jrI6s|?I|ZhlT}c2 zkXkj=oTOG`L8lS#x`lZ6mll`&$3Gl6CG}LXN~^p=iq8C&uG) zU#~Io1nwi;0F+?-YRZui1X1dpmEy5f#f%V&2BYz5@)%8d<^*XbD9JEBX$coU3M$t8 z!?O%j?jZreW+T{W4{s6ZDGN{EVqu#ZQfF^5u+>%|mX)^wv_U&3*nP$E0G|UK2ejLv zZil*)cL+4^6ztIEnzF(MFq3ZH4gKg+KZ4J|HZYSFJT;C2Ufp5PX)MkQW4bK{MKHpL zi<<^h;yMk#XR842p){4KPNOW;w9{}7mka9 z&R_=7?a>hIx+DlOor%Une88eJk`SFD9VPrVe*g0W2Dcc@(q|Ms2D9d$O027M!C)j7 zk%Pfhb&2l1g~nstj`g-fYi)S3oP)J*Tg&xq^vKb1P zXbZpa^drVBezIH%w^TE)9{7bF)47?rB*#K?!gH$sTigpIfRm3*3z!)qDIs9j9a8d2 zLX3w&+UWLaL5?ToL{ipSNfsjsorXg6LPy;;6Ov}4kt@3GN;Gs;c=?KM6=GpHWV(Gu zxM7SLphdj#I)kUxEwUuxo$>qqVhxm{8$hbQoLkCRxDj4TGXNIQhR3IQ+Ouz{o<7af zx9&Nz<~gEzj%l7_8D_)n{j}z;@8dmp_o(h>&E1@_Y-I0agN7NZBGQ63JI?}PkxM%@|2Mu3xZZ?lwJh@WOr8IKJn2B)!C#un>G<;KSa*t zZw~*C`@7@6KCb#+)_gB#h8Ew*z~3ef?+Ma&I$}m6N0NcrcRP}4G+R7`rh`_-+;D6G zr=)jz%OmjoLpull&{9T6FjrbnAV5Mm`^XVJ|eo| zC{owI#ozmo(yfsgFT~^kV@PeQA+ANd8r?}mcrYAK0)j(4fZG^8IhP=tDt5!O7{CCF z7SV$N=|@LFvOC@y9zPQtpBS7N7JJ}T+2(_^4_y;XyN?^%G1dnFVD~xKqWw7`s1TG~{hqt&TwSA9*O>#6-LkQ#Wl3I6-9NE>V$;r6 zJ2nx3KRJ-g`>A8;lUlSIx@3PI)U?P42 z1}Bk`CdL5iU1Wgr!V~(M#RZ%r4`A z^SP)jEyiQ$5H|WdgIu-DyhQ`C8m_I5=V9#EQfU@yVgd$NX9EuA5 zP*_e1QOPgJ1R)%cT@yq(BKw7ypB#t00o)5_u3av4)2-M%O-j$LJv~?ePJD}b<{{WM zvT#GzZP@GqT?WQ7n7KN41~k$bC_>$qj9rbzug7!_uV9epMcsNX7LCM&5#2rtR?<(! zh%D%AOn_AcSltrgbyk*wpbV{Xf~Px(F?F5bc?>ftD8#@>4-0@Rh+STiqH;fW+o?`|yphK&*Dr58VP*Ly^+t3&6||k@K%Jm>xPM zNLZkOUWG(dtKPszz}$ls2a5y>656HO%LM*zNLZ{$;A4Rp*kmf~of}n6T2(;ldNZFd zTvXn>qEv;|s<2iS&Yaq)uD{#A6#n?c-4mIiTuohOIM>khIrkZtty`&58@jayIN>&T zH34^+oVWI#>l0V5vE}pqpY6}~uiR7{U)364%{A}+{L*Kava_oJwRu2m9>}${eLnNq zO!mfVt=e)-YdMx{d+E3Pzu3R>+GCsA_PW;gdakp3(`jv~+C%`pL)Oab?=gVK0;oVW zzRc(k8=eNNojOY(SKIU;oZWkWYI#bj*{4*$q`0=`A0T1H)(NH50z~=w$-^g)p?^8% zIMqx)p(;-`&`+u;TyJ0jnv4gmR*1w}1$YmERH1g0TG-W{wv?L!B~`dxl2PENWbw$` z5)YmgZ(Fu(J;$-5?dV5)aXiS5WqNBrlkHrThah8}Uu=<@6uOLJv~jw(d}UECLO&PC zW;b8)$YWO8U>RdmFrP{aIlbjeKrYie+q%5hymryID~&35q0x`^sN$=4qED>#N9kI_ z*Y30%B}&QII4*oO398@+vk{ps^BD@bTXDcxDN5)x#n&y<^NhTwge^Cf>U_Of@*hF- zY?{rMN%In}X;-k(Pg0sO)0%HcLA$vPJX9b4GtWh#n6n)nL=tn|V(<#)8;bCz7?9iZ z6F|W?-U2Qk+CmHS9P+*r{#GrH!!?IROCjtAID3?e&{E@2H;!F`QTTBUgAM)i?73@ z?k$O9LtW{X_*Fn+iCQ52z`n!ks0!jeAm-(g0O!G-dIttEu&kgaA*h3OpmbA`g}JaC z)ouAsx}!J`?uFN+b1^|SW*LCT3GpxRP{;8b`t70fx+BRaf_y}TkHf=6LpMQr*{_Gh zSTHGp4&yPVbqaK(fQKYLAcKP{Itw2!NIDgIPC3R*G7!rQOvh)V0N#Xsh8PQar*)o! z(+Mrx6TgLzc*+Q;{rw)^auMC5z$WqYV3UBYj7cp148XV8r5OYN<~)=@N=xrgbZROe z!ywx5xqSdTmNydO@l;!x`oo5H??5yTR)=)Fw}^8Y#*vN!_;2`IKuYT<-=F)(b6?AAhu>5VznP@~lrq62#;b^J`#`Os^BsoHhiPDkhK4VWy*)ZA{v{yj z3~m$0ph`>u@f@z zgp`~U@!Abv;1#IlZ3poxZXL&f2LN_SOcrm7BGk884-BS^$vMCcm{+7j@L>z+skbV_ zZZfQWh}v+~X|BE5*H$^D?bUC(w38FcyFtbEp6Yr}bG-+ZoU8Ko=trYU`;o6}6yFGB z)pc5PomNQxVbfxPQOc0&l<{fBHv?I9MKo7LaYbN`#dAv2Nh7PSQ=03PQpi|GTRFUv zyp{5n(na#bCFKgQR0(R8pj8QArGTfB%C>*};oT22XL6qU+v$(eOT((CP4l#EBHDe0 z${Qxx^kZ3R9)hfThc)l8Lh@6?YJrzGm5HFz{2pZ08`8WX#T$Y(Sg6u>WcwdCD87@B zw=ecjR@B(G+;qQXxn!=2>X;f<({dY+jzx1!Ee@m%Nmlen}5fZyIh3j&j znv6^80_M5(+8{!IKnDj8(SJBLc=%K|deYrE#L?eW92jBgKeEh--S$Tp1yFZ_=mUG0 z*sQt@qR&!jS_qe0Jvg$8pPyX5}=X+E3zdC(xeEjVBcl;?!zhC?b ztWrD(U;-pIn-C*#`VQc>zXp(xlZhYVh6MnoIGJvpjzj1_k*X|{T}BgD2huSB3kcZC zUZGT+Svmj!^6zRMHLjk9a93bN4UA}k5u;h<&T8CQg*%(`RA*!&t8@EZpq^0{3*K$XtWLM8@CbxD;YTmC^A@b&->IFac?KJtIGKRktPcjG(~bmL=dw4g=!bvB(r`9QHFKIK~o@;a0_eMi5~i z{eAv(d^`>XcKjsbdiMDvu+yT@1NSi@UVwX&c%s*&(y&s;CdD%>TCrIV3a$Rd723y^ z#Rga%C7y-uVgf+GBEAoWP9;n#%P|mzU|u*rmpGp4D%*D{5}Qs&Lm~ug%T!kpcN&J0 z+5mvwYQAXX)yk#N=fY<~_VoSOa;(@)_C8g${euIn5r}dZZ3u97CGi$srInA*g(5NC zb3GE}!y%CmieTFal6VT<;>k%$aWk@ z0a08Gae9r3x%~d!1QmBJEHQvo@^+nNR7utCOsz2VkbVgOd|E4NXo8>>A&mt61^2QBf>;1D+$K1a5D8`N6$E5XJ#B2yH8$t! z`?KvzeZP@6ZA<{(1IV1YGpV`!*=p6bPjT(rbRfICbd3QTDC04Vh5fLsfCpcp;MjaH zs5^qexj3JU;<_ssd_NhA8gE2Dpo+B^V3&zRc?q#Zd|bhFgj+v@f;ft&GgspAsQ9-S z^EQBo$e0h?!6J4z2*3eM>3;w$6#q6YEL#KL$kt?OQfvhkG&@c$g+XmXxdU`qScnD} z0cT@z1fT$_ylm%6$BO)@YZF0rE9huuYwz%z2#T%XI0H*+2kNpls|+a~!#d$z3(HmAfNUXt`J|YNN=BsdX$MCgMbZ4OvX8{0K`NGRpNVAHQEKrF zIK=E1)FbOwCkc$>oqO1L?vZ^5Y*^d&WR{BHV&iYgr!DV5-j<@{yd!^ueW~32D-ZY0 z79;X$dbT(!)G_%;lIsgKgrL3XoSGdtaOTV0{!$O~>Uife!@C}Bxp6m-WL$`vzM>-k z6|cf9`2J&CGS31IbJ<#U`Px5c9fkh~!0RZRp|qSW>tJ7C9qgY$4{x3x6F{Y+v!C^r z)={eQm1gSBgKpmWKPb{${@3FFl8WS>q-u;fQYp7Y{^cQm`3#qq3|vu8>o(AjbD;RQ^gv z#EQzea3S7((UueOgyMY+uy}n=bqA_+4lLJ1NR$Mh5TZTPBO z!k>jlfXg3Hp%RWzVjT*}2pTh3wTqY`ufb3H1|%6=*E7`%=kQ<7UAyk?SaWx*^eOHR z)!nbT`xi!XjANatUt{W*?8<>l`OMU-%w>(atT2~L?_Emokb>U~Ys|314CkDc>&~V% zXOrq|(VQ&{L;vNh{l&oIz{1c|oAXxs&W$x&t72>2;M~9D7iY_^nKo)0mTHz8KMvjv z!j03q@93KEsOoz~^SuHWPL24&34jAt*T5x{rxq@mJhgDiL~8areBD$BC?-&W92JXA zOLc4ZR>j_$tE|1}`-EI6XRP_@m+Dq1rJ_r%=+Xdk2Q==0Qpiv3Zs^>&W^Y&Q?KyAF zy0>S|+oO8>G;bd`a>n|XYP0Vv6#=y(paJB%G_Ff2WMhElHTz47{Uxx=7w6YKZEK#k z>><_Dsd+jx3@mBAa_?H@-fV|j*{M}_zR z;_u-RI~vlv0Kvhfs7=N~f$qn^il{+KL9hS+&Ux?yh|Ml@~Ws+L#+yGed?@Ta;eUa1uo~TRT}oI&iznz*DH+&RQG`e z$EGzzQE-K^GyIOUe_#Ig4BbS5%~b>gh;e07YqU?HeL03(82X!$TO$kPZ<9t$BQz8G F|1a(u@B{z= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1e9cd586232a97bf45dd7a837baeecefd23c48e7 GIT binary patch literal 21459 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 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()