Terminal / agents /reasoning_core.py
Baida-A's picture
sync: 125 file da Baida98/AI@25b6bd78 (2026-07-10 21:17 UTC)
33f93a9 verified
Raw
History Blame Contribute Delete
19.5 kB
"""
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 ""