Spaces:
Running
Running
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 ""
|