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)