mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 11:04:43 +02:00
feat(hermes-auto-improve): ajouter hermes-doc-rewrite pour réécriture doc basée sur état cluster
Script hermes-doc-rewrite : inspecte l'état réel du cluster (read-only) et demande à Hermes de réécrire chaque fichier admin/ avec des infos exactes. Commit local dans hermes/daily-work, pas de push automatique. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
994a2f1e54
commit
e62832fd71
1 changed files with 365 additions and 0 deletions
365
tools/hermes-auto-improve/doc-rewrite.py
Normal file
365
tools/hermes-auto-improve/doc-rewrite.py
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
hermes-doc-rewrite — Vérifie l'état réel du cluster (LECTURE SEULE)
|
||||
et réécrit les docs admin/ dans hermes/daily-work.
|
||||
Commit local uniquement, pas de push.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
REPO_DIR = Path("/srv/data/lab/Funk-lab")
|
||||
BRANCH = "hermes/daily-work"
|
||||
ADMIN_DIR = REPO_DIR / "admin"
|
||||
STATE_FILE = Path("/var/lib/hermes-auto-improve/rewrite-state.json")
|
||||
MAX_FILES = 3
|
||||
|
||||
# ── Commandes read-only par fichier (clé = substring du chemin relatif) ──────
|
||||
CONTEXT_MAP = {
|
||||
"ia/litellm": [
|
||||
"systemctl status litellm --no-pager -n 30",
|
||||
"cat /etc/litellm/config.yaml",
|
||||
],
|
||||
"ia/hermes.md": [
|
||||
"systemctl status hermes --no-pager -n 20",
|
||||
"cat /srv/data/hermes/config.yaml 2>/dev/null | head -60",
|
||||
"ls /opt/hermes/ 2>/dev/null",
|
||||
],
|
||||
"ia/hermes-souls": [
|
||||
"ls -la ~hermes/ 2>/dev/null",
|
||||
"ls /srv/data/hermes/ 2>/dev/null",
|
||||
"cat /srv/data/hermes/config.yaml 2>/dev/null | grep -A5 'profile' | head -30",
|
||||
],
|
||||
"ia/hermes-voice": [
|
||||
"systemctl status hermes-voice 2>/dev/null --no-pager -n 10 || echo 'service hermes-voice absent'",
|
||||
"ls /opt/hermes/ 2>/dev/null",
|
||||
],
|
||||
"ia/llama_server": [
|
||||
"systemctl cat llama-server 2>/dev/null | head -40",
|
||||
"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 gpu-01 'systemctl status llama-server --no-pager -n 15; systemctl status llama-server-cpu llama-server-cpu2 --no-pager -n 5 2>/dev/null' 2>/dev/null || echo 'gpu-01 inaccessible'",
|
||||
],
|
||||
"ia/rag": [
|
||||
"systemctl status qdrant --no-pager -n 15",
|
||||
"ls /srv/data/rag/docs/ 2>/dev/null | wc -l",
|
||||
"/usr/local/bin/rag-query 'hermes configuration' 2>&1 | head -20",
|
||||
],
|
||||
"ia/rocm": [
|
||||
"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 gpu-01 'uname -r; ls /opt/rocm/bin/ 2>/dev/null | head -5; rocm-smi 2>/dev/null | head -15' 2>/dev/null || echo 'gpu-01 inaccessible'",
|
||||
],
|
||||
"ia/alertmanager-webhook": [
|
||||
"systemctl status alertmanager-webhook --no-pager -n 15",
|
||||
"cat /etc/alertmanager-webhook/*.yaml 2>/dev/null | head -50",
|
||||
],
|
||||
"infra/dnsmasq": [
|
||||
"systemctl status dnsmasq --no-pager -n 10",
|
||||
"cat /etc/dnsmasq.d/funk.conf 2>/dev/null || ls /etc/dnsmasq.d/",
|
||||
"dig @127.0.0.1 compute-01.lab.local +short 2>/dev/null",
|
||||
"dig @127.0.0.1 openwebui.lab.local +short 2>/dev/null",
|
||||
],
|
||||
"infra/nfs": [
|
||||
"systemctl status nfs-server --no-pager -n 10",
|
||||
"exportfs -v 2>/dev/null",
|
||||
"showmount -e localhost 2>/dev/null",
|
||||
],
|
||||
"infra/reseau": [
|
||||
"ip addr show | grep -E 'inet |^[0-9]+'",
|
||||
"ip route show",
|
||||
"nft list ruleset 2>/dev/null | head -80",
|
||||
],
|
||||
"infra/ssh": [
|
||||
"sshd -T 2>/dev/null | grep -E 'permitrootlogin|passwordauth|pubkeyauth|^port '",
|
||||
"cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | head -30",
|
||||
],
|
||||
"infra/email": [
|
||||
"systemctl status postfix --no-pager -n 10",
|
||||
"postconf -n 2>/dev/null | grep -vi 'pass'",
|
||||
],
|
||||
"install/storage-01": [
|
||||
"hostnamectl",
|
||||
"df -h",
|
||||
"free -h",
|
||||
"lsblk -o NAME,SIZE,TYPE,MOUNTPOINT",
|
||||
"systemctl list-units --state=failed --no-pager 2>/dev/null | head -10",
|
||||
],
|
||||
"install/gpu-01": [
|
||||
"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 gpu-01 'hostnamectl; df -h; free -h; uname -r; lsblk -o NAME,SIZE,TYPE,MOUNTPOINT' 2>/dev/null || echo 'gpu-01 inaccessible'",
|
||||
"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 gpu-01 'systemctl list-units --state=failed --no-pager 2>/dev/null' 2>/dev/null",
|
||||
],
|
||||
"install/kubernetes": [
|
||||
"kubectl get nodes -o wide 2>/dev/null",
|
||||
"kubectl version 2>/dev/null | head -4",
|
||||
"kubectl get pods -A --field-selector=status.phase!=Running 2>/dev/null | head -20",
|
||||
],
|
||||
"k8s/argocd": [
|
||||
"kubectl get applications -n argocd -o wide 2>/dev/null",
|
||||
"kubectl get pods -n argocd 2>/dev/null",
|
||||
],
|
||||
"k8s/monitoring": [
|
||||
"kubectl get pods -n monitoring 2>/dev/null",
|
||||
"kubectl get svc -n monitoring 2>/dev/null",
|
||||
"kubectl get ingress -n monitoring 2>/dev/null",
|
||||
],
|
||||
"k8s/n8n": [
|
||||
"kubectl get all -n ai -l app=n8n 2>/dev/null",
|
||||
"kubectl get ingress -n ai 2>/dev/null",
|
||||
],
|
||||
"k8s/open-webui": [
|
||||
"kubectl get all -n ai 2>/dev/null",
|
||||
"kubectl get ingress -n ai 2>/dev/null",
|
||||
],
|
||||
"k8s/talos": [
|
||||
"kubectl get nodes 2>/dev/null",
|
||||
"talosctl version 2>/dev/null | head -5",
|
||||
],
|
||||
"k8s/k9s": [
|
||||
"k9s version 2>/dev/null || echo 'k9s: check depuis storage-01'",
|
||||
"kubectl config get-contexts 2>/dev/null",
|
||||
],
|
||||
"ops/ansible": [
|
||||
"ansible --version 2>/dev/null | head -3",
|
||||
"ls /srv/data/lab/Funk-lab/ansible/roles/",
|
||||
"ls /srv/data/lab/Funk-lab/ansible/playbooks/",
|
||||
],
|
||||
"ops/cluster": [
|
||||
"kubectl get nodes -o wide 2>/dev/null",
|
||||
"systemctl list-units --state=failed --no-pager 2>/dev/null | head -10",
|
||||
"df -h /srv/data 2>/dev/null",
|
||||
],
|
||||
"ops/systeme": [
|
||||
"uname -r",
|
||||
"hostnamectl",
|
||||
"uptime",
|
||||
"df -h",
|
||||
"free -h",
|
||||
"systemctl list-units --state=failed --no-pager 2>/dev/null | head -10",
|
||||
],
|
||||
"README": [
|
||||
"ls /srv/data/lab/Funk-lab/admin/",
|
||||
"systemctl is-active litellm hermes dnsmasq nfs-server qdrant postfix 2>/dev/null",
|
||||
],
|
||||
}
|
||||
|
||||
DEFAULT_COMMANDS = [
|
||||
"date",
|
||||
"df -h /srv/data 2>/dev/null",
|
||||
"systemctl list-units --state=failed --no-pager 2>/dev/null | head -5",
|
||||
]
|
||||
|
||||
|
||||
# ── Git ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
def git(args):
|
||||
return subprocess.run(
|
||||
["git", "-C", str(REPO_DIR)] + args,
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
|
||||
def git_sync():
|
||||
git(["config", "user.email", "hermes@funk.lab"])
|
||||
git(["config", "user.name", "Hermes Bot"])
|
||||
git(["fetch", "origin"])
|
||||
r = git(["checkout", BRANCH])
|
||||
if r.returncode != 0:
|
||||
git(["checkout", "-b", BRANCH, f"origin/{BRANCH}"])
|
||||
git(["pull", "--rebase", "origin", BRANCH])
|
||||
|
||||
|
||||
# ── Cluster inspection (read-only) ───────────────────────────────────────────
|
||||
|
||||
def run_cmd(cmd):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, timeout=30
|
||||
)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:2000] if out else "(aucune sortie)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "(timeout)"
|
||||
except Exception as e:
|
||||
return f"(erreur: {e})"
|
||||
|
||||
|
||||
def get_context_cmds(rel_path):
|
||||
for key, cmds in CONTEXT_MAP.items():
|
||||
if key in rel_path:
|
||||
return cmds
|
||||
return DEFAULT_COMMANDS
|
||||
|
||||
|
||||
def collect_cluster_state(cmds):
|
||||
parts = []
|
||||
for cmd in cmds:
|
||||
out = run_cmd(cmd)
|
||||
parts.append(f"$ {cmd}\n{out}")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
# ── Hermes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def ask_hermes(prompt):
|
||||
safe = prompt.replace("'", "'\\''")
|
||||
cmd = [
|
||||
"sudo", "-i", "-u", "hermes", "bash", "-c",
|
||||
f"HERMES_HOME=/srv/data/hermes hermes --profile funk-ai -z '{safe}'"
|
||||
]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def rewrite_doc(fpath):
|
||||
rel = str(fpath.relative_to(REPO_DIR))
|
||||
content = fpath.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
cmds = get_context_cmds(rel)
|
||||
print(f" Collecte état cluster ({len(cmds)} cmd)...", flush=True)
|
||||
cluster_state = collect_cluster_state(cmds)
|
||||
|
||||
prompt = f"""Tu es l'agent de documentation du homelab Funk.
|
||||
MISSION : réécrire ce fichier de documentation en te basant sur l'état RÉEL du système.
|
||||
CONTRAINTE ABSOLUE : lecture seule — tu documentes ce que tu vois, tu ne modifies rien sur le cluster.
|
||||
|
||||
Fichier : {rel}
|
||||
--- CONTENU ACTUEL ---
|
||||
{content[:3000]}
|
||||
--- FIN CONTENU ---
|
||||
|
||||
--- ÉTAT RÉEL (commandes read-only exécutées sur le cluster) ---
|
||||
{cluster_state[:3000]}
|
||||
--- FIN ÉTAT ---
|
||||
|
||||
RÈGLES :
|
||||
1. Réécris le document en Markdown en corrigeant les informations inexactes ou obsolètes
|
||||
2. Conserve la structure et le style du document original
|
||||
3. Base-toi UNIQUEMENT sur ce que tu vois dans l'état réel ci-dessus — n'invente rien
|
||||
4. Si une section est correcte, garde-la telle quelle
|
||||
5. Ajoute les informations manquantes que tu observes dans l'état réel
|
||||
6. Ne mentionne pas cette réécriture dans le contenu du document
|
||||
|
||||
Réponds UNIQUEMENT avec le contenu Markdown du fichier réécrit, sans JSON, sans introduction."""
|
||||
|
||||
print(f" Hermes réécrit le document...", flush=True)
|
||||
result = ask_hermes(prompt)
|
||||
|
||||
if not result or len(result) < 80:
|
||||
return None, f"réponse trop courte ({len(result)} chars)"
|
||||
|
||||
stripped = result.lstrip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
return None, "réponse JSON inattendue, ignorée"
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def load_state():
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
return {"rewritten": [], "errors": []}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--all", action="store_true", help="Traiter tous les fichiers en une passe")
|
||||
parser.add_argument("--reset", action="store_true", help="Réinitialiser l'état de progression")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Afficher le plan sans modifier les fichiers")
|
||||
parser.add_argument("--subdir", default=None, help="Sous-dossier admin/ (ex: ia, k8s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"hermes-doc-rewrite — {datetime.now().strftime('%Y-%m-%d %H:%M')}", flush=True)
|
||||
print(f"Branche cible : {BRANCH} (pas de push)", flush=True)
|
||||
|
||||
if not args.dry_run:
|
||||
git_sync()
|
||||
|
||||
state = load_state()
|
||||
|
||||
if args.reset:
|
||||
state = {"rewritten": [], "errors": []}
|
||||
save_state(state)
|
||||
print("État réinitialisé.\n")
|
||||
|
||||
scan_root = ADMIN_DIR / args.subdir if args.subdir else ADMIN_DIR
|
||||
all_md = [
|
||||
f for f in sorted(scan_root.rglob("*.md"))
|
||||
if "hermes/builtin" not in str(f)
|
||||
]
|
||||
|
||||
if args.all:
|
||||
batch = all_md
|
||||
else:
|
||||
todo = [f for f in all_md if str(f) not in state["rewritten"]]
|
||||
if not todo:
|
||||
state["rewritten"] = []
|
||||
save_state(state)
|
||||
todo = all_md
|
||||
print("Rotation : tous les fichiers retraités depuis le début.")
|
||||
batch = todo[:MAX_FILES]
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d_%H-%M")
|
||||
print(f"\n{len(batch)} fichiers à réécrire\n{'─'*50}", flush=True)
|
||||
|
||||
changed = []
|
||||
errors = []
|
||||
|
||||
for fpath in batch:
|
||||
rel = str(fpath.relative_to(REPO_DIR))
|
||||
print(f"\n→ {rel}", flush=True)
|
||||
|
||||
if args.dry_run:
|
||||
cmds = get_context_cmds(rel)
|
||||
print(f" [dry-run] {len(cmds)} commandes prévues :")
|
||||
for c in cmds:
|
||||
print(f" {c}")
|
||||
continue
|
||||
|
||||
new_content, err = rewrite_doc(fpath)
|
||||
|
||||
if err:
|
||||
print(f" ✗ {err}", flush=True)
|
||||
errors.append(rel)
|
||||
continue
|
||||
|
||||
fpath.write_text(new_content, encoding="utf-8")
|
||||
state["rewritten"].append(str(fpath))
|
||||
changed.append(rel)
|
||||
print(f" ✓ Réécrit ({len(new_content)} chars)", flush=True)
|
||||
|
||||
git(["add", rel])
|
||||
if git(["diff", "--cached", "--quiet"]).returncode != 0:
|
||||
git(["commit", "-m",
|
||||
f"docs(hermes): révision {Path(rel).name} — état cluster {now}\n\n"
|
||||
f"Basé sur inspection read-only du cluster.\n\n"
|
||||
"Co-Authored-By: Hermes <hermes@funk.lab>"])
|
||||
print(f" ✓ Commit local — pas de push", flush=True)
|
||||
else:
|
||||
print(f" — Aucun changement détecté par git", flush=True)
|
||||
|
||||
if not args.dry_run:
|
||||
save_state(state)
|
||||
|
||||
print(f"\n{'─'*50}")
|
||||
print(f"Réécritures réussies : {len(changed)}/{len(batch)}")
|
||||
if errors:
|
||||
print(f"Erreurs ({len(errors)}) : {', '.join(errors)}")
|
||||
if changed:
|
||||
print("Fichiers mis à jour :")
|
||||
for f in changed:
|
||||
print(f" • {f}")
|
||||
print(f"\nBranche : {BRANCH} (local, pas de push)")
|
||||
print("Pour pusher : git -C /srv/data/lab/Funk-lab push origin hermes/daily-work")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue