File size: 19,487 Bytes
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
"""
reasoning_core.py β€” MobileMaxAgent Implementation
Cervello di livello massimo: Project Understanding + Strategy Engine + Auto-Debug Loop.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
import asyncio
import json, re
from models.ai_client import AIClient

import logging
_logger = logging.getLogger("agents.reasoning_core")


@dataclass
class ReasoningResult:
    action: str          # "plan" | "fix" | "continue" | "stop" | "analyze" | "strategy"
    steps: List[str]
    patch: Optional[str] = None
    reason: str = ""
    confidence: float = 0.5


@dataclass
class ReasoningState:
    goal: str
    context: str = ""
    last_result: str = ""
    errors: List[str] = field(default_factory=list)
    completed_steps: List[str] = field(default_factory=list)
    loop_count: int = 0
    world_model: Optional[str] = None
    strategy: Optional[str] = None
    project_files: Optional[List[Dict[str, Any]]] = None  # GAP-2: file VFS per deep context reasoning


class ReasoningCore:
    """
    MobileMaxAgent β€” Evoluzione del ReasoningCore.
    Gestisce l'intero ciclo di vita del progetto:
    1. Analyze (Project Understanding)
    2. Strategy (Global Decision Making)
    3. Patch (Multi-file implementation)
    4. Run & Debug (Auto-repair loop)
    """
    MAX_LOOPS      = 15
    MIN_CONFIDENCE = 0.4

    def __init__(self, llm_client: AIClient | None = None, planner=None, critic=None, executor=None):
        self.llm      = llm_client or AIClient()
        self.planner  = planner
        self.critic   = critic
        self.executor = executor

    # ── 1. Project Understanding ────────────────────────────────────────────────
    async def analyze_project(self, repo_context: str) -> str:
        prompt = f"""Analyze full software system.
Return:
- architecture map
- dependencies
- risk zones
- entry points
CONTEXT:
{repo_context}
"""
        # S665: wrap con asyncio.wait_for β€” analyze_project usava await self.llm.chat() senza timeout
        # β†’ hang indefinito se il provider non risponde. Timeout 45s = STREAM_TIMEOUT (ai_client.py).
        try:
            return await asyncio.wait_for(
                self.llm.chat([{"role": "user", "content": prompt}], temperature=0.2),
                timeout=45.0,
            )
        except asyncio.TimeoutError:
            return "[reasoning_core] analyze_project: timeout 45s β€” contesto non disponibile"

    # ── 2. Global Strategy (Devin Core) ─────────────────────────────────────────
    async def develop_strategy(self, state: ReasoningState) -> str:
        prompt = f"""You are an autonomous software engineer.
WORLD MODEL:
{state.world_model}
STATE:
- goal: {state.goal}
- errors: {state.errors}
- completed: {state.completed_steps}
Decide:
- what to change
- why
- impact
- risk level
"""
        # S665: timeout anche per develop_strategy
        try:
            return await asyncio.wait_for(
                self.llm.chat([{"role": "user", "content": prompt}], temperature=0.3),
                timeout=45.0,
            )
        except asyncio.TimeoutError:
            return "[reasoning_core] develop_strategy: timeout 45s β€” strategia non disponibile"

    # ── 3. Error Intelligence ───────────────────────────────────────────────────
    async def analyze_error(self, error: str) -> str:
        prompt = f"""Map error to codebase.
ERROR:
{error}
Return:
- file
- root cause
- fix strategy
"""
        # S665: timeout anche per analyze_error
        try:
            return await asyncio.wait_for(
                self.llm.chat([{"role": "user", "content": prompt}], temperature=0.1),
                timeout=45.0,
            )
        except asyncio.TimeoutError:
            return "[reasoning_core] analyze_error: timeout 45s β€” analisi non disponibile"

    # ── Prompt builder ──────────────────────────────────────────────────────────
    def _build_prompt(self, state: ReasoningState) -> str:
        # S590: errors[-3:]β†’[-5:] β€” piΓΉ errori nel contesto per diagnosi piΓΉ accurata
        # BUG-2: raggruppa errori per tipo + ultimi 5 dettagliati β€” diagnosi piΓΉ accurata
        if state.errors:
            import re as _re_err
            _err_all = state.errors
            _err_grouped: dict[str, int] = {}
            for _e in _err_all:
                _ek = _re_err.match(r'(\w+Error|\w+Exception|[A-Z]\w{3,})', _e)
                _ek_str = _ek.group(1) if _ek else "Error"
                _err_grouped[_ek_str] = _err_grouped.get(_ek_str, 0) + 1
            _err_recent = "\n".join(_err_all[-5:])
            _err_summary = ", ".join(f"{k}Γ—{v}" for k, v in _err_grouped.items()) if len(_err_all) > 5 else ""
            errors_str = _err_recent + (f"\n[Riepilogo tipi: {_err_summary}]" if _err_summary else "")
        else:
            errors_str = "nessuno"
        steps_str  = "\n".join(f"- {s}" for s in state.completed_steps[-5:]) if state.completed_steps else "nessuno"
        
        _base_prompt = f"""Sei MobileMaxAgent, un sistema di ingegneria software autonoma.
Analizza lo stato e decidi l'azione successiva.

STATO:
- goal: {state.goal}
- world_model: {'Presente' if state.world_model else 'Mancante'}
- strategy: {'Definita' if state.strategy else 'Da definire'}
- last_result: {state.last_result[:500] if state.last_result else 'vuoto'}  # S592: 300β†’500
- errors: {errors_str}
- loop_count: {state.loop_count}/{self.MAX_LOOPS}

Rispondi SOLO con JSON valido:
{{
  "action": "analyze | strategy | plan | fix | continue | stop",
  "steps": ["prossimo passo tecnico"],
  "patch": "eventuale diff o codice",
  "reason": "perchΓ© questa azione?",
  "confidence": 0.0-1.0
}}

Regole:
1. Se manca world_model -> "analyze"
2. Se manca strategy -> "strategy"
3. Se strategy c'Γ¨ ma serve piano -> "plan"
4. Se ci sono errori -> "fix"
5. Se tutto ok -> "continue" o "stop" se finito.
"""

        # GAP-2: Deep Context β€” inietta skeleton dei file rilevanti per ragionamento multi-file
        _ctx_section = ""
        if state.project_files:
            try:
                from agents.context_manager import rank_files_by_relevance, build_file_skeleton
                _top_paths = set(rank_files_by_relevance(state.goal, state.project_files, k=5))
                _skels = [
                    build_file_skeleton(
                        f.get("path", ""),
                        f.get("content", ""),
                        f.get("language", ""),
                    )
                    for f in state.project_files
                    if f.get("path") in _top_paths
                ]
                if _skels:
                    # P25-B1: ordina i blocchi skeleton per overlap keyword col goal prima di troncare.
                    # Zero LLM, zero latenza β€” stessa logica word-overlap di episodic.py.
                    # Garantisce che i blocchi piΓΉ rilevanti per il goal finiscano PRIMA del taglio.
                    _goal_kw_ctx = set(re.findall(r'\w{4,}', state.goal.lower())) if hasattr(state, 'goal') else set()
                    if _goal_kw_ctx:
                        _skels.sort(
                            key=lambda _s: len(_goal_kw_ctx & set(re.findall(r'\w{4,}', _s.lower()))),
                            reverse=True,
                        )
                    _ctx_raw = "\n".join(_skels)
                    # S780-SMART: Smart Chunking β€” estrae firme funzioni/classi invece di troncare.
                    # BUG-SKEL fix: evita allucinazioni su funzioni mancanti nei file complessi.
                    if len(_ctx_raw) > 6000:
                        import re as _re_sk
                        _sig_lines = _re_sk.findall(
                            r'^(?:(?:async\s+)?def |class |export\s+(?:default\s+)?'
                            r'(?:function|const|class)\s+\w|function\s+\w)[^\n]{0,200}',
                            _ctx_raw, _re_sk.MULTILINE
                        )
                        _ctx_smart = '\n'.join(_sig_lines)
                        if len(_ctx_smart) >= 500:
                            _ctx_raw = (
                                f'[SMART CHUNK β€” {len(_skels)} file β€” solo firme estratte]\n'
                                + _ctx_smart[:10000]
                            )
                        else:
                            _ctx_raw = _ctx_raw[:6000] + '\n… [troncato β€” usa file_search per dettagli]'
                    _ctx_section = "\n\nFILE RILEVANTI (skeleton per ragionamento):\n" + _ctx_raw
            except Exception:
                pass  # non-fatal β€” degradazione graceful senza deep context

        return _base_prompt + _ctx_section

    @staticmethod
    def _extract_json(raw: str) -> str | None:
        """P16-B3: depth-counting bilanciato β€” sostituisce regex greedy r'{[\s\S]+}'
        che su JSON nested (es. patch con oggetti interni) estraeva dal primo { all'ULTIMO }
        producendo JSON malformato β†’ action='continue' per default β†’ agente in loop.
        Pattern identico a safeJsonParse.ts giΓ  in produzione sul frontend."""
        depth = 0
        start = -1
        for i, ch in enumerate(raw):
            if ch == '{':
                if depth == 0:
                    start = i
                depth += 1
            elif ch == '}':
                depth -= 1
                if depth == 0 and start != -1:
                    return raw[start:i + 1]
        return None

    def _parse(self, raw: str) -> ReasoningResult:
        try:
            candidate = self._extract_json(raw)
            data = json.loads(candidate) if candidate else {}
        except Exception:
            return ReasoningResult(action='continue', steps=[], reason='Parsing error fallback', confidence=0.2)

        return ReasoningResult(
            action=data.get("action", "continue"),
            steps=data.get("steps", []),
            patch=data.get("patch"),
            reason=data.get("reason", ""),
            confidence=float(data.get("confidence", 0.5))
        )

    async def decide(self, state: ReasoningState) -> ReasoningResult:
        if state.loop_count >= self.MAX_LOOPS:
            return ReasoningResult(action="stop", steps=[], reason="Max loops reached", confidence=1.0)

        prompt = self._build_prompt(state)
        try:
            # S750-GAP-D: asyncio.wait_for β€” evita hang se LLM provider non risponde
            raw = await asyncio.wait_for(
                self.llm.chat([{"role": "user", "content": prompt}], temperature=0.2),
                timeout=30.0,
            )
            return self._parse(raw)
        except asyncio.TimeoutError:
            return ReasoningResult(action="continue", steps=[], reason="decide(): LLM timeout 30s", confidence=0.3)
        except Exception as e:
            return ReasoningResult(action="continue", steps=[], reason=f"LLM error: {e}", confidence=0.3)

    async def run_loop(self, goal: str, context: str = "", on_step=None,
                       project_files: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
        state = ReasoningState(goal=goal, context=context, project_files=project_files)
        results = []

        while state.loop_count < self.MAX_LOOPS:
            decision = await self.decide(state)
            
            if on_step:
                await on_step({
                    "loop": state.loop_count,
                    "action": decision.action,
                    "reason": decision.reason,
                    "confidence": decision.confidence
                })

            if decision.action == "stop":
                break
            
            elif decision.action == "analyze":
                state.world_model = await self.analyze_project(context or goal)
                results.append({"action": "analyze", "output": "World model built"})
            
            elif decision.action == "strategy":
                state.strategy = await self.develop_strategy(state)
                results.append({"action": "strategy", "output": state.strategy})

            elif decision.action == "plan" and self.planner:
                plan = await self.planner.create_plan(goal, context=state.strategy)
                state.completed_steps.append("Piano creato")
                state.last_result = "Piano generato"
                results.append({"action": "plan", "result": plan})

            elif decision.action == "fix":
                if decision.patch:
                    # Se c'Γ¨ una patch, l'executor la applica
                    if self.executor:
                        res = await self.executor.run_tool("file_editor", {"path": "patch.diff", "content": decision.patch})
                        state.last_result = str(res.get("output", ""))
                    state.errors = []
                    results.append({"action": "fix", "patch": "Applicata"})
                else:
                    error_analysis = await self.analyze_error(str(state.errors))
                    state.last_result = error_analysis
                    results.append({"action": "error_analysis", "output": error_analysis})

            elif decision.action == "continue":
                # S575: direct_response non esiste nel TOOL_REGISTRY β€” usa LLM diretto
                if decision.steps:
                    try:
                        _step_prompt = decision.steps[0]
                        _step_ans = await self.llm.chat(
                            [{"role": "system", "content":
                              "Sei un assistente tecnico. Esegui il passo richiesto in modo conciso."},
                             {"role": "user", "content":
                              f"Goal: {state.goal}\n\nPasso da eseguire: {_step_prompt}"}],
                            temperature=0.2, max_tokens=512,
                        )
                        state.last_result = _step_ans or ""
                        state.completed_steps.append(_step_prompt)
                    except Exception:
                        state.completed_steps.append(decision.steps[0])
                results.append({"action": "continue", "steps": decision.steps})

            # Auto-debug check con Critic
            if self.critic and state.last_result and decision.action != "analyze":
                critique = await self.critic.evaluate(goal, state.last_result)
                if critique.get("needs_retry"):
                    state.errors.extend(critique.get("issues", []))

            state.loop_count += 1

        return {
            "goal": goal,
            "loops": state.loop_count,
            "success": len(state.errors) == 0,
            "results": results,
            "final_state": {
                "has_world_model": state.world_model is not None,
                "has_strategy": state.strategy is not None
            }
        }

    async def run_loop_to_answer(self, goal: str, context: str = "",
                                  on_step=None, max_loops: int = 8,
                                  project_files: Optional[List[Dict[str, Any]]] = None) -> str:
        """S575: Versione di run_loop che ritorna una stringa risposta sintetizzata.

        Usata dal gate in UnifiedAgentLoop quando tok_budget >= 6144 e subtask >= 3.
        Limite max_loops=8 (S701: era 5) β€” piΓΉ iterazioni per task profondi.
        Output: stringa di risultati aggregati da passare come contesto extra al LLM finale.
        Mai solleva eccezioni.
        """
        try:
            # GAP-2: deep context β€” inietta i file VFS nella ReasoningState per rank_files_by_relevance()
            state = ReasoningState(goal=goal, context=context, project_files=project_files)
            parts: List[str] = []
            loop_cap = min(max_loops, self.MAX_LOOPS)

            while state.loop_count < loop_cap:
                try:
                    decision = await self.decide(state)
                except Exception:
                    break

                if on_step:
                    try:
                        import asyncio as _aio
                        coro = on_step({
                            "loop": state.loop_count,
                            "action": f"reasoning:{decision.action}",
                            "reason": decision.reason[:200] if decision.reason else "",  # S578: 120β†’200
                            "confidence": decision.confidence,
                        })
                        if _aio.iscoroutine(coro):
                            await coro
                    except Exception as _exc:
                        _logger.debug("[reasoning_core] silenced %s", type(_exc).__name__)  # noqa: BLE001

                if decision.action == "stop" or decision.confidence < self.MIN_CONFIDENCE:
                    break

                elif decision.action == "analyze":
                    try:
                        state.world_model = await self.analyze_project(context or goal)
                        # S593: 400β†’600 β€” world_model spesso multi-paragrafo
                        parts.append(f"[ANALISI PROGETTO]: {(state.world_model or '')[:600]}")
                    except Exception as _exc:
                        _logger.debug("[reasoning_core] silenced %s", type(_exc).__name__)  # noqa: BLE001

                elif decision.action == "strategy":
                    try:
                        state.strategy = await self.develop_strategy(state)
                        # S593: 400β†’600 β€” strategy spesso multi-step
                        parts.append(f"[STRATEGIA]: {(state.strategy or '')[:600]}")
                    except Exception as _exc:
                        _logger.debug("[reasoning_core] silenced %s", type(_exc).__name__)  # noqa: BLE001

                elif decision.action in ("plan", "continue", "fix"):
                    # Esegui passo diretto via LLM
                    step_desc = (decision.steps[0] if decision.steps
                                 else decision.reason or goal)
                    try:
                        _ans = await self.llm.chat(
                            [{"role": "system", "content":
                              "Sei un assistente tecnico esperto. "
                              "Svolgi il passo richiesto in modo preciso e conciso."},
                             {"role": "user", "content":
                              f"Goal complessivo: {goal}\n\nPasso: {step_desc}"}],
                            temperature=0.2, max_tokens=512,
                        )
                        if _ans and not _ans.startswith("[LLM"):
                            parts.append(f"[PASSO {state.loop_count+1}]: {_ans[:600]}")
                            state.last_result = _ans
                            state.completed_steps.append(step_desc)
                    except Exception as _exc:
                        _logger.debug("[reasoning_core] silenced %s", type(_exc).__name__)  # noqa: BLE001

                state.loop_count += 1

            return "\n\n".join(parts) if parts else ""
        except Exception:
            return ""