Spaces:
Sleeping
Sleeping
| # 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) |