Spaces:
Running
Running
File size: 9,760 Bytes
03e5649 | 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 | """unified_loop_delegate.py β DelegateMixin: debug riflessivo, replan, delega in-loop.
Estratto da unified_loop.py per ridurre il file principale.
Contiene:
_reflective_debug(goal, errors): BGAP-GUARD diagnosi breve da errori tool
_budget_replan_check(state, step): BGAP-1 replan probabilistico su budget critico
_DELEGATE_RESEARCH_RE: regex riconoscimento sub-goal tipo ricerca
_run_in_loop_delegate(sub_goal): GAP-1 micro-agente specializzato in-loop
Invariante B1: nessun corpo duplicato con unified_loop.py.
MRO garantisce che DelegateMixin._budget_replan_check sovrascriva HelpersMixin
(DelegateMixin precede HelpersMixin nella lista basi di UnifiedAgentLoop).
"""
from __future__ import annotations
import asyncio
import logging
import re
from typing import Any
from agents.unified_loop_types import StepCallback, UnifiedLoopState, _maybe_await
_logger = logging.getLogger("agente_ai")
class DelegateMixin:
async def _reflective_debug(
self, goal: str = "", errors: Any = None, **kwargs: Any
) -> str:
"""Reflective debug: analizza errori e propone diagnosi in max 2 frasi.
Chiamato dopo tool failures per arricchire state.context con ipotesi fix.
Fail-open: non blocca mai il loop in caso di errore LLM."""
try:
_ctx = f"Goal: {str(goal)[:200]}\nErrori: {'; '.join(str(e)[:300] for e in (errors if isinstance(errors, list) else [errors])[:3])}" # S573: 150β300
_fast = self._get_fast_llm()
_diag = await asyncio.wait_for(
_fast.chat([{"role": "user", "content": f"Diagnosi breve (max 2 frasi):\n{_ctx}"}], max_tokens=300), # S586: 120->180->300
timeout=5.0,
)
return (str(_diag) if _diag else "").strip()[:300]
except Exception:
pass # fail-open
return ""
# ββ BGAP-1: Probabilistic Re-planning Trigger ββββββββββββββββββββββββββββ
async def _budget_replan_check(
self, state: Any, step_count: int, on_step: Any = None
) -> str:
"""BGAP-1: probabilistic re-planning trigger.
Guards: skip se _n_err < 2 OR _budget_ratio < 0.6.
Usa _get_fast_llm() con max_tokens=120. Fail-open."""
_n_err = len(state.errors) if getattr(state, 'errors', None) else 0
if _n_err < 2:
return ''
_budget_ratio = step_count / max(state.max_steps, 1)
if _budget_ratio < 0.6:
return ''
# dedup guard [GAP-1-REPLAN]: skip se giΓ replanned in questo loop
if '[GAP-1-REPLAN]' in (state.context or ''):
return ''
try:
_fast_llm = self._get_fast_llm()
_prompt = (
f'Task ha avuto {_n_err} errori e usato {_budget_ratio:.0%} del budget. '
f'Suggerisci UN approccio alternativo in max 2 frasi. Goal: {state.goal[:500]}' # S597: 200->300->500
)
_hint = await asyncio.wait_for(
_fast_llm.chat([{'role': 'user', 'content': _prompt}], max_tokens=120),
timeout=5.0,
)
return (str(_hint) if _hint else '').strip()[:200]
except Exception:
pass # fail-open totale
return ''
# ββ GAP-1: Delega Dinamica In-Loop βββββββββββββββββββββββββββββββββββββ
_DELEGATE_RESEARCH_RE = re.compile(
r'\b(cerca|research|trova|web|url|leggi|analisi|analizza|documenta|'
r'news|notizie|fetch|scrape|pagina|sito|http)\b',
re.IGNORECASE,
)
async def _run_in_loop_delegate(self, sub_goal: str, timeout: float = 40.0) -> dict:
"""GAP-1: Delega Dinamica In-Loop.
Lancia un micro-agente specializzato per sub_goal DURANTE il loop principale.
Architettura:
- Stesso executor del parent β accesso ai tool reali (write_file, run_python, ...)
- LLM selezionato per ruolo β RESEARCHER, CODER o REASONER in base al goal
- _is_delegate_child = True β blocca ricorsione (max 1 livello di delega)
- max_steps = 4 β micro-agente leggero, non un loop completo
- output troncato a 4000 chars β evita context-window explosion nel parent
"""
# P18: defensive anti-recursion guard at entry point
if getattr(self, '_is_delegate_child', False):
_logger.debug("[delegate] anti-recursion guard triggered at _run_in_loop_delegate entry")
return {"output": "[DELEGATE] Ricorsione bloccata: _is_delegate_child=True.", "steps": [], "goal_met": False}
try:
from models.role_router import RoleRouter as _RR_d, Role as _Role_d
# Seleziona LLM specializzato in base al tipo di sotto-obiettivo
if self._DELEGATE_RESEARCH_RE.search(sub_goal[:300]):
_sub_llm = _RR_d.get_client(_Role_d.RESEARCHER) # Gemini 2.5-flash
elif self._CODE_RE.search(sub_goal[:300]):
_sub_llm = _RR_d.get_client(_Role_d.CODER) # Llama 4 Scout
else:
_sub_llm = _RR_d.get_client(_Role_d.REASONER) # Cerebras 120B
except Exception:
_sub_llm = self.llm # fallback: usa LLM del parent
# Crea loop figlio: stessi executor/planner/memory, LLM specializzato
_sub_loop = UnifiedAgentLoop(
llm_client=_sub_llm,
planner=self.planner,
executor=self.executor,
critic=None, # no critic β micro-agente leggero
memory=self.memory,
verifier=None, # no verifier β massima velocitΓ
)
# Anti-ricorsione: il figlio non puΓ² delegare ulteriormente
_sub_loop._is_delegate_child = True
# Propaga session_id per isolare sandbox backend-exec
_sub_loop._run_task_id = self._run_task_id + "_d"
# GAP-6: condividi dict mutabile _session_files con il parent loop
# Prima: delegate inizializzava _session_files={} -> file scritti non visibili al parent
# Ora: stessa referenza -> parent vede automaticamente tutti i file scritti dal delegate
_sub_loop._session_files = self._session_files
# P17-F1: buffer output parziale via on_step β sopravvive al timeout
_partial_steps: list[dict] = []
async def _capture_partial(step: dict) -> None:
if step.get("output") or step.get("explanation"):
_partial_steps.append(step)
try:
_res = await asyncio.wait_for(
_sub_loop.run(sub_goal, max_steps=4, on_step=_capture_partial),
timeout=timeout,
)
_out = (_res.get("output") or "")[:4000]
_logger.info(
"GAP-1 delegate OK [%s] steps=%d: %s",
_res.get("engine", "?"), len(_res.get("steps", [])), sub_goal[:60],
)
return {
"success": _res.get("success", False),
"output": _out,
"engine": _res.get("engine", "delegate"),
"steps": len(_res.get("steps", [])),
}
except asyncio.TimeoutError:
# P17-F1: esponi stato parziale invece di stringa vuota
# _session_files giΓ condiviso con parent β parent vede file scritti
_partial_files = list(getattr(_sub_loop, "_session_files", {}).keys())
_partial_out = " ".join(
(s.get("output") or s.get("explanation") or "")[:300]
for s in _partial_steps[-3:]
).strip()[:1500]
_logger.warning(
"GAP-1 delegate timeout (%.0fs, %d steps, %d files): %s",
timeout, len(_partial_steps), len(_partial_files), sub_goal[:60],
)
# S-PARTIAL: emetti evento SSE partial_output al frontend PRIMA di restituire
# cosΓ¬ l'utente vede il chip "β output parziale β riprendo" in tempo reale
if on_step:
await _maybe_await(on_step({
"event": "partial_output",
"action": "partial_output",
"visibility": "progress",
"partial": True,
"steps_done": len(_partial_steps),
"partial_files": _partial_files,
"partial_output": _partial_out,
"output": _partial_out,
"explanation": f"Output parziale dopo {timeout:.0f}s β l'agente sta recuperando",
"status": "warning",
}))
return {
"success": False,
"output": _partial_out,
"error": f"delegate timeout ({timeout:.0f}s) β risultato parziale",
"partial": True,
"partial_files": _partial_files,
"steps_done": len(_partial_steps),
}
except Exception as _de:
_logger.warning("GAP-1 delegate error: %s", _de)
return {"success": False, "output": "", "error": str(_de)[:200]}
# Γ’ΒΒΓ’ΒΒ S362: Role routing helpers Γ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒΓ’ΒΒ
# S427: ampliato con verbi IT/EN mancanti + framework/pattern aggiuntivi.
# Stesso set di goal_verifier._CODE_RE + keyword tecnologiche per routing CODER LLM.
|