#!/usr/bin/env python3 """ hermes-auto-improve — Analyse les docs admin/ avec Hermes. Génère des rapports d'amélioration dans la feature branch, ré-ingère dans Qdrant. Usage : python3 auto-improve.py [--all] [--subdir ia] [--dry-run] """ import argparse import json import subprocess import sys from datetime import datetime from pathlib import Path REPO_DIR = Path("/srv/data/lab/Funk-lab") BRANCH = "feature/hermes-auto-improve" ADMIN_DIR = REPO_DIR / "admin" REPORTS_DIR = REPO_DIR / "tools/hermes-auto-improve/reports" STATE_FILE = Path("/var/lib/hermes-auto-improve/state.json") MAX_FILES = 6 # fichiers par run (sans --all) HERMES_CMD = [ "sudo", "-i", "-u", "hermes", "bash", "-c", 'HERMES_HOME=/srv/data/hermes hermes --profile funk-ai -z "{prompt}"' ] def git(args: list, check=False) -> subprocess.CompletedProcess: return subprocess.run( ["git", "-C", str(REPO_DIR)] + args, capture_output=True, text=True, check=check ) def ask_hermes(prompt: str) -> str: safe = prompt.replace("'", "'\\''").replace('"', '\\"') cmd = [ "sudo", "-i", "-u", "hermes", "bash", "-c", f'HERMES_HOME=/srv/data/hermes hermes --profile funk-ai -z "{safe}"' ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=180) return result.stdout.strip() def analyze_file(fpath: Path) -> dict: rel = str(fpath.relative_to(REPO_DIR)) content = fpath.read_text(encoding="utf-8") prompt = f"""Tu es l'agent d'analyse du homelab Funk. Analyse ce fichier de documentation : Fichier : {rel} --- {content[:4000]} --- Réponds en JSON strict (pas de markdown) : {{ "status": "ok" | "à_améliorer", "score": 1-10, "problèmes": ["liste des problèmes concrets"], "améliorations": ["liste des améliorations prioritaires"], "contenu_amélioré": "contenu markdown complet si status=à_améliorer, sinon null" }} Critères d'évaluation : - Informations obsolètes ou incorrectes par rapport à l'état actuel du homelab - Sections manquantes importantes pour un ops - Commandes ou chemins érronés - Documentation incomplète pour reproduire une procédure Si le document est à jour et complet, mettre status=ok et contenu_amélioré=null.""" raw = ask_hermes(prompt) # Extraire le JSON de la réponse Hermes (qui peut contenir du texte autour) try: start = raw.find("{") end = raw.rfind("}") + 1 if start >= 0 and end > start: return json.loads(raw[start:end]) except json.JSONDecodeError: pass return { "status": "erreur", "score": 0, "problèmes": ["Hermes n'a pas retourné de JSON valide"], "améliorations": [], "contenu_amélioré": None, "raw_response": raw[:500] } def load_state() -> dict: if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text()) return {"processed": [], "last_run": None} def save_state(state: dict): STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False)) def main(): parser = argparse.ArgumentParser() parser.add_argument("--all", action="store_true", help="Traiter tous les fichiers") parser.add_argument("--subdir", default=None, help="Sous-dossier admin/ à analyser (ex: ia, k8s)") parser.add_argument("--dry-run", action="store_true", help="Analyser sans commit ni push") parser.add_argument("--reset", action="store_true", help="Réinitialiser l'état (retraiter tous les fichiers)") args = parser.parse_args() # ── Sync branche ──────────────────────────────────────────── git(["fetch", "origin"]) checkout = git(["checkout", BRANCH]) if checkout.returncode != 0: git(["checkout", "-b", BRANCH, f"origin/{BRANCH}"]) git(["pull", "origin", BRANCH]) # ── Sélection des fichiers ─────────────────────────────────── scan_root = ADMIN_DIR / args.subdir if args.subdir else ADMIN_DIR all_files = sorted(scan_root.rglob("*.md")) state = load_state() if args.reset: state["processed"] = [] todo = [f for f in all_files if str(f) not in state["processed"]] if not todo: state["processed"] = [] todo = all_files batch = todo if args.all else todo[:MAX_FILES] # ── Analyse ────────────────────────────────────────────────── run_ts = datetime.now().strftime("%Y-%m-%d_%H-%M") results = [] for fpath in batch: rel = str(fpath.relative_to(REPO_DIR)) print(f" → Analyse : {rel}", flush=True) analysis = analyze_file(fpath) analysis["file"] = rel results.append(analysis) # Appliquer les améliorations si Hermes en a proposé if analysis.get("status") == "à_améliorer" and analysis.get("contenu_amélioré"): if not args.dry_run: fpath.write_text(analysis["contenu_amélioré"], encoding="utf-8") print(f" ✓ Améliorations appliquées (score: {analysis.get('score', '?')}/10)", flush=True) elif analysis.get("status") == "ok": print(f" ✓ OK (score: {analysis.get('score', '?')}/10)", flush=True) else: print(f" ⚠ {analysis.get('status', '?')}", flush=True) state["processed"].append(str(fpath)) save_state(state) # ── Rapport ────────────────────────────────────────────────── REPORTS_DIR.mkdir(parents=True, exist_ok=True) report_file = REPORTS_DIR / f"{run_ts}.json" report_file.write_text( json.dumps({"run": run_ts, "results": results}, indent=2, ensure_ascii=False) ) improved = [r["file"] for r in results if r.get("status") == "à_améliorer" and r.get("contenu_amélioré")] # ── Commit + push ──────────────────────────────────────────── if not args.dry_run: git(["add", str(report_file.relative_to(REPO_DIR))]) for f in improved: git(["add", f]) if git(["diff", "--cached", "--quiet"]).returncode != 0: msg_files = f"\n\nFichiers améliorés :\n" + "\n".join(f" - {f}" for f in improved) if improved else "" git(["commit", "-m", f"docs(auto): analyse Hermes {run_ts}{msg_files}\n\n" f"Co-Authored-By: Hermes "]) git(["push", "origin", BRANCH]) print(f"\n✓ Commit + push → {BRANCH}", flush=True) # ── Ré-ingestion Qdrant ────────────────────────────────── if improved: print("\n⏳ Ré-ingestion Qdrant...", flush=True) ingest = subprocess.run( ["/usr/local/bin/rag-ingest"], capture_output=True, text=True, timeout=300 ) print(f" {ingest.stdout.strip()[-200:] if ingest.stdout else 'OK'}", flush=True) # ── Résumé JSON (lu par n8n) ───────────────────────────────── summary = { "run": run_ts, "branch": BRANCH, "analyzed": len(results), "improved": len(improved), "ok": len([r for r in results if r.get("status") == "ok"]), "errors": len([r for r in results if r.get("status") == "erreur"]), "files_improved": improved, "dry_run": args.dry_run, "details": [ { "file": r["file"], "status": r.get("status"), "score": r.get("score"), "issues": r.get("problèmes", []), "suggestions": r.get("améliorations", []) } for r in results ] } print("\n" + json.dumps(summary, indent=2, ensure_ascii=False)) return 0 if __name__ == "__main__": sys.exit(main())