Spaces:
Sleeping
Sleeping
| """ | |
| 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") | |
| class ReasoningResult: | |
| action: str # "plan" | "fix" | "continue" | "stop" | "analyze" | "strategy" | |
| steps: List[str] | |
| patch: Optional[str] = None | |
| reason: str = "" | |
| confidence: float = 0.5 | |
| 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 | |
| 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 "" | |