atypique-api / core /hermes_loop.py
RDS777's picture
Backend v3.0 complet
8e3a425
Raw
History Blame Contribute Delete
15.2 kB
# core/hermes_loop.py
"""
Hermès Loop v3 – avec Diffusion, Exploration (Arbre d’Hypothèses) et parsing JSON robuste.
"""
import logging
import json
import re
import time
import asyncio
from typing import Optional, Dict, List, Any
from core.hypothesis_tree import HypothesisTree, BranchStatus
from core.branch_executor import BranchExecutor
from core.error_tree import ErrorTree
logger = logging.getLogger("vortex.hermes")
class HermesLoop:
"""
Cycle Hermès en 7 étapes classiques + exploration d’hypothèses.
"""
def __init__(self, llm, memory, error_tree: ErrorTree, semantic_router,
vector_memory=None, diffusion_passes: int = 2):
self.llm = llm
self.memory = memory
self.error_tree = error_tree
self.semantic_router = semantic_router
self.vector_memory = vector_memory
self.diffusion_passes = diffusion_passes
# ─── DIFFUSION (auto‑correction) ────────────────────────────
async def _diffuse(self, text: str, task: str, passes: int = None) -> str:
"""Relit et corrige un texte en plusieurs passes."""
if passes is None:
passes = self.diffusion_passes
current = text
for p in range(1, passes + 1):
prompt = f"""
Tu es un relecteur expert. Voici une réponse à la tâche : "{task}".
RÉPONSE ACTUELLE (passe {p}/{passes}) :
{current}
INSTRUCTIONS :
1. Corrige les erreurs de syntaxe ou de logique.
2. Améliore la clarté et la documentation.
3. Ajoute des tests ou des vérifications si nécessaire.
4. Si parfaite, recopie‑la à l'identique.
RENDS UNIQUEMENT LA VERSION RÉVISÉE.
"""
try:
resp = await self.llm.call(
agent="Hermès-Diffusion",
system="Tu es un relecteur méticuleux.",
user=prompt
)
new_text = resp.content if hasattr(resp, 'content') else str(resp)
if new_text and new_text != current and len(new_text) > 20:
current = new_text
logger.info("[Diffusion] Passe %d : amélioration appliquée.", p)
else:
logger.info("[Diffusion] Passe %d : aucune amélioration, arrêt.", p)
break
except Exception as e:
logger.warning(f"[Diffusion] Échec passe {p} : {e}")
break
return current
# ─── EXPLORATION D’HYPOTHÈSES (Arbre) ─────────────────────
async def explore_hypotheses(self, task: str, n_hypotheses: int = 3) -> Dict:
"""
Génère plusieurs hypothèses, les exécute en parallèle, et retourne la meilleure.
"""
logger.info(f"[Hermès] Exploration d'hypothèses pour: {task[:80]}...")
# 1. Générer les hypothèses via le LLM
hypotheses = await self._generate_hypotheses(task, n_hypotheses)
# 2. Construire l'arbre
tree = HypothesisTree(task, max_branches=n_hypotheses)
for hyp in hypotheses:
description = hyp.get("description", "Aucune description")
code = hyp.get("code", "")
tree.add_branch(description, code=code)
# 3. Exécuter chaque branche en parallèle
executor = BranchExecutor(timeout=30)
tasks = []
for branch in tree.branches.values():
tasks.append(executor.execute_branch(branch, tree))
results = await asyncio.gather(*tasks)
# 4. Sélectionner la meilleure branche
best = tree.get_best_branch()
if best:
logger.info(f"[Hermès] Meilleure hypothèse: {best.id} (score={best.score:.2f})")
# Capitaliser les échecs dans Harbour
for branch in tree.branches.values():
if branch.status == BranchStatus.FAILED and branch.error:
self.error_tree.add_error(branch.error, branch.hypothesis)
# Indexation vectorielle du résultat
if self.vector_memory:
try:
self.vector_memory.add_episode(
f"Exploration: {task}\nMeilleure hypothèse: {best.hypothesis}\nRésultat: {best.result}",
metadata={"source": "explore", "score": best.score}
)
except Exception as e:
logger.warning(f"[Hermès] Indexation échouée: {e}")
return {
"task": task,
"method": "Hermès/Exploration",
"best_branch": best.id,
"best_score": round(best.score, 3),
"best_result": best.result,
"summary": tree.get_summary(),
"branches": [
{"id": b.id, "hypothesis": b.hypothesis[:100], "score": b.score,
"status": b.status.value}
for b in tree.branches.values()
]
}
else:
return {"task": task, "error": "Aucune branche réussie"}
async def _generate_hypotheses(self, task: str, n: int) -> List[Dict]:
"""
Demande au LLM de générer n approches différentes, avec parsing JSON robuste.
"""
prompt = f"""
Tâche : {task}
Génère précisément {n} approches différentes pour résoudre ce problème.
Pour chaque approche, donne une **description concise** et un **code Python** (si applicable).
Format de sortie : UNIQUEMENT un JSON valide, sans texte avant ou après.
Le JSON doit être une liste de {n} dicts contenant "description" et "code".
Exemple :
[
{{"description": "Approche avec heaps", "code": "import heapq\\n..."}},
{{"description": "Approche avec deque triée", "code": "..."}}
]
"""
try:
resp = await self.llm.call(
agent="Hermès-Explorateur",
system="Tu es un explorateur de solutions. Génère des approches variées et originales. Réponds uniquement en JSON valide.",
user=prompt
)
content = resp.content if hasattr(resp, 'content') else str(resp)
# Nettoyer les backticks et autres
content = content.strip()
content = re.sub(r'^```(?:json)?\s*', '', content)
content = re.sub(r'\s*```$', '', content)
# Essayer de parser le JSON
try:
data = json.loads(content)
if isinstance(data, list) and len(data) >= n:
return data[:n]
elif isinstance(data, list):
# Si moins de n, compléter avec des descriptions génériques
missing = n - len(data)
for i in range(missing):
data.append({"description": f"Approche supplémentaire {i+1}", "code": ""})
return data
except json.JSONDecodeError:
# Fallback : extraire les éléments séparés par des numéros ou des tirets
lines = content.split('\n')
hypotheses = []
current = {"description": "", "code": ""}
for line in lines:
if re.match(r'^\s*(\d+\.|\-)\s*', line):
if current["description"]:
hypotheses.append(current)
current = {"description": "", "code": ""}
current["description"] = re.sub(r'^\s*(\d+\.|\-)\s*', '', line).strip()
elif current["description"] and line.strip():
if "```" in line:
if current["code"]:
current["code"] += "\n" + line
else:
current["code"] = line
else:
current["description"] += " " + line.strip()
if current["description"]:
hypotheses.append(current)
# Si on n'a pas assez, on ajoute des placeholders
while len(hypotheses) < n:
hypotheses.append({"description": f"Approche alternative {len(hypotheses)+1}", "code": ""})
return hypotheses[:n]
except Exception as e:
logger.error(f"[Hermès] Échec génération hypothèses: {e}")
# Fallback : générer n hypothèses génériques
return [{"description": f"Approche {i+1} (générique)", "code": ""} for i in range(n)]
# ─── CYCLE HERMÈS CLASSIQUE (7 étapes) ────────────────────
async def run(self, task: str) -> Dict:
"""Cycle complet en 7 étapes, avec diffusion sur l’étape Act."""
t0 = time.time()
logger.info(f"[Hermès] Démarrage : {task[:80]}...")
# Contexte RAG
vector_context = ""
if self.vector_memory and hasattr(self.vector_memory, "search_context_block"):
try:
vector_context = self.vector_memory.search_context_block(task, n=3, max_chars=800)
if vector_context:
logger.info("[Hermès] Contexte vectoriel récupéré (%d caractères)", len(vector_context))
except Exception as e:
logger.warning(f"[Hermès] Échec RAG : {e}")
steps = []
# 1. ANCRER
anchor = await self._anchor(task, vector_context)
steps.append({"step": "anchor", "action": "Cadrer le problème", "result": anchor[:300], "score": 0.9})
# 2. RAISONNER
reasoning = await self._reason(task, anchor, vector_context)
steps.append({"step": "reason", "action": "Explorer les approches", "result": reasoning[:300], "score": 0.85})
# 3. AGIR (avec DIFFUSION)
raw_solution = await self._act(task, reasoning, vector_context)
diffused_solution = await self._diffuse(raw_solution, task, passes=self.diffusion_passes)
steps.append({"step": "act", "action": "Produire la solution", "result": diffused_solution[:300], "score": 0.85})
# 4. OBSERVER
observation = await self._observe(task, diffused_solution)
steps.append({"step": "observe", "action": "Capturer le résultat", "result": observation[:300], "score": 0.825})
# 5. RÉÉVALUER
reeval = await self._reevaluate(task, diffused_solution, observation)
steps.append({"step": "reeval", "action": "Comparer à l'attendu", "result": reeval[:300], "score": 0.7})
# 6. VÉRIFIER (avec diffusion sur les tests)
raw_tests = await self._verify(task, diffused_solution)
diffused_tests = await self._diffuse(raw_tests, f"Tests pour : {task}", passes=1)
steps.append({"step": "verify", "action": "Valider formellement", "result": diffused_tests[:300], "score": 0.6})
# 7. NARRER
narration = await self._narrate(task, diffused_solution, diffused_tests, steps)
steps.append({"step": "narrate", "action": "Documenter et livrer", "result": narration[:300], "score": 0.9})
overall_score = sum(s["score"] for s in steps) / len(steps)
elapsed = time.time() - t0
logger.info(f"[Hermès] Terminé en {elapsed:.1f}s, score={overall_score:.3f}")
# Indexation automatique
if self.vector_memory and hasattr(self.vector_memory, "add_hermes_result"):
try:
self.vector_memory.add_hermes_result({
"task": task,
"narration": narration,
"deliverable": diffused_solution,
"overall_score": overall_score
})
except Exception as e:
logger.warning(f"[Hermès] Indexation échouée : {e}")
return {
"task": task,
"method": "Hermès/Fable",
"elapsed_s": round(elapsed, 2),
"overall_score": round(overall_score, 3),
"steps": steps,
"deliverable": diffused_solution,
"narration": narration,
"used_vector_context": bool(vector_context)
}
# ─── SOUS‑ÉTAPES (appels async) ────────────────────────────
async def _anchor(self, task: str, vector_context: str) -> str:
prompt = f"Tâche : {task}\nContexte RAG : {vector_context[:600]}\nDéfinis le problème, les contraintes et les critères de succès."
resp = await self.llm.call(agent="Hermès", system="Tu es un chef de projet.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _reason(self, task: str, anchor: str, vector_context: str) -> str:
prompt = f"Tâche : {task}\nCadre : {anchor}\nContexte : {vector_context[:400]}\nExplore 3 approches possibles, explique avantages/inconvénients."
resp = await self.llm.call(agent="Hermès", system="Tu es un architecte technique.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _act(self, task: str, reasoning: str, vector_context: str) -> str:
prompt = f"Tâche : {task}\nRaisonnement : {reasoning}\nContexte : {vector_context[:400]}\nProduis une solution complète en Python avec code, docstrings et exemples."
resp = await self.llm.call(agent="Hermès", system="Tu es un développeur senior.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _observe(self, task: str, solution: str) -> str:
prompt = f"Tâche : {task}\nSolution : {solution}\nExtrais les métriques, logs et résultats observables."
resp = await self.llm.call(agent="Hermès", system="Tu es un analyste.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _reevaluate(self, task: str, solution: str, observation: str) -> str:
prompt = f"Tâche : {task}\nSolution : {solution}\nObservation : {observation}\nÉcarts par rapport à l'attendu ? Que faudrait‑il améliorer ?"
resp = await self.llm.call(agent="Hermès", system="Tu es un expert en qualité.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _verify(self, task: str, solution: str) -> str:
prompt = f"Tâche : {task}\nSolution : {solution}\nGénère des tests unitaires (unittest ou pytest) pour valider cette solution."
resp = await self.llm.call(agent="Hermès", system="Tu es un ingénieur de test.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)
async def _narrate(self, task: str, solution: str, tests: str, steps: List) -> str:
prompt = f"Tâche : {task}\nSolution finale : {solution[:500]}\nTests : {tests[:300]}\nRésumé des étapes : {json.dumps(steps, default=str)[:400]}\nRédige un compte‑rendu structuré avec recommandations."
resp = await self.llm.call(agent="Hermès", system="Tu es un rédacteur technique.", user=prompt)
return resp.content if hasattr(resp, 'content') else str(resp)