Spaces:
Sleeping
Sleeping
File size: 15,238 Bytes
8e3a425 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | # 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) |