From 1c7781a68fbce4f55d2c46c89e54bc552957a137 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 24 Mar 2026 09:32:00 +0100 Subject: [PATCH] feat: aggiungi ReAct Agent Loop per trasformare chatbot in agente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuovo ReActAgent con loop Pensa->Agisci->Osserva - Usa intent detection esistente per affidabilità - Supporta tutti i tool built-in - Planning e memory integration point - docs/AGENT_ROADMAP.md: gap analysis e roadmap --- docs/AGENT_ROADMAP.md | 227 ++++++++++++++++++++ enhancements/react_agent.py | 398 ++++++++++++++++++++++++++++++++++++ 2 files changed, 625 insertions(+) create mode 100644 docs/AGENT_ROADMAP.md create mode 100644 enhancements/react_agent.py diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md new file mode 100644 index 0000000..7bfdb9a --- /dev/null +++ b/docs/AGENT_ROADMAP.md @@ -0,0 +1,227 @@ +# Da Chatbot ad Agente AI: Gap Analysis e Roadmap + +## Stato Attuale del Progetto + +L'attuale LangCode Agent è classificabile come **chatbot avanzato con tool calling**, non come agente AI autonomo. + +--- + +## Chatbot vs Agente: Gap Analysis + +| Caratteristica | Chatbot (Attuale) | Agente AI (Obiettivo) | Stato Attuale | +|----------------|-------------------|----------------------|---------------| +| **Input/Output** | Turn-by-turn | Goal-oriented | ⚠️ Parziale | +| **Planning** | Nessuno | Scomposizione in task | ❌ Assente | +| **Loop operativo** | Singola risposta | Pensa → Agisci → Osserva → Ripianifica | ❌ Assente | +| **Memoria** | Solo cronologia sessione | A lungo termine, persistente | ⚠️ Esiste ma non integrato | +| **Autonomia** | Dipende da input utente | Esegue autonomamente | ❌ Assente | +| **Tool use** | Arricchisci risposta | Agisci sull'ambiente | ⚠️ Parziale | +| **Goal tracking** | Nessuno | Tiene traccia obiettivi | ❌ Assente | + +--- + +## Componenti Esistenti ma Non Integrati + +### 1. Planning System (`enhancements/planning_enhancement.py`) +- **Esiste**: Task, priorità, dipendenze, progress tracking +- **Non usato**: Nessun tool nell'agente lo sfrutta +- **Stato**: Codice pronto, mai integrato in `OllamaToolAgent` + +### 2. Simple Memory (`enhancements/simple_memory.py`) +- **Esiste**: Memorizzazione fatti, preferenze, cronologia +- **Non usato**: I tool `remember`, `recall` non sono nell'agente +- **Stato**: Implementato ma isolato + +### 3. Enhanced Memory (`enhancements/memory_enhancement.py`) +- **Esiste**: Vector store con FAISS, semantic search +- **Non usato**: Dipende da `nomic-embed-text` non installato +- **Stato**: Parzialmente funzionante + +--- + +## Cosa Manca per Diventare Agente + +### 1. **Agent Loop (CORE)** +L'agente deve poter iterare: +``` +WHILE goal NOT achieved: + 1. THINK: Pianifica prossimo step + 2. ACT: Esegui tool + 3. OBSERVE: Leggi risultato + 4. EVALUATE: Goal raggiunto? Ripianifica se necessario +``` + +### 2. **Goal Decomposition** +- LLM deve scomporre richieste complesse in task sequenziali +- Es: "Prepara un report delle vendite" → cerca file → analizza dati → scrivi report + +### 3. **Self-Reflection** +- L'agente deve valutare se il risultato è soddisfacente +- Se un tool fallisce, provare approccio alternativo + +### 4. **Persistent Memory Integration** +- Memoria deve essere доступна in ogni conversazione +- L'agente deve poter salvare e recuperare informazioni + +### 5. **Multi-Step Execution Context** +- L'agente deve mantenere stato tra diverse chiamate tool +- Non ripetere azioni già completate + +--- + +## Roadmap di Implementazione + +### Fase 1: Agent Loop Base +**Obiettivo**: Abilitare esecuzione multi-step + +``` +enhancements/agent_loop.py +├── ThinkStep: LLM decide prossima azione +├── ActStep: Esegui tool +├── ObserveStep: Leggi risultato +├── EvaluateStep: Goal raggiunto? +└── ReplanStep: Ripianifica se fallito +``` + +**Modifiche a `ollama_agent.py`**: +- Aggiungere metodo `run()` con loop +- Aggiungere parametro `max_iterations` +- Integrare `enhanced_planner` per task tracking + +### Fase 2: Memory Integration +**Obiettivo**: Rendere memoria parte dell'agente + +- Integrare `simple_memory` in `OllamaToolAgent` +- Aggiungere tool: `remember`, `recall` +- Salvare conversazioni automaticamente +- Recuperare fatti rilevanti per contesto + +### Fase 3: Goal-Oriented Interaction +**Obiettivo**: LLM pianifica autonomamente + +- Aggiungere system prompt per goal decomposition +- LLM decide quali tool usare in sequenza +- Mantenere "memory of plans" per long-term goals + +### Fase 4: Enhanced Observability +**Obiettivo**: Tracciare e mostrare progresso + +- Logging delle azioni +- Report progresso task +- Feedback visivo all'utente + +--- + +## Implementazione Proposta: Nuovo `ReActAgent` + +```python +# enhancements/react_agent.py (nuovo) + +class ReActAgent: + """ + Agente con Loop Pensa→Agisci→Osserva→Ripianifica + """ + + def __init__(self, llm, tools, max_iterations=10): + self.llm = llm + self.tools = {t.name: t for t in tools} + self.max_iterations = max_iterations + self.planner = EnhancedPlanner() # Esistente + self.memory = SimplePersistentMemory() # Esistente + + def run(self, goal: str) -> Dict: + """ + Esegue un goal attraverso il loop ReAct. + """ + context = f"Goal: {goal}\n\n" + + for i in range(self.max_iterations): + # 1. THINK: LLM decide prossima azione + thought = self._think(context, goal) + + # 2. ACT: Esegui tool se necessario + if thought.get("action"): + result = self._act(thought["action"], thought["args"]) + context += f"\nAzione: {thought['action']}\nRisultato: {result}\n" + + # Salva in memoria se rilevante + if thought["action"] in ["read_file", "execute"]: + self.memory.add_fact(f"Azione completata: {thought['action']}") + + # 3. OBSERVE & EVALUATE + if self._goal_achieved(context, goal): + return {"success": True, "result": context} + + return {"success": False, "result": context, "error": "Max iterations"} + + def _think(self, context, goal) -> Dict: + """LLM decide prossima azione basata su contesto""" + # Prompt ingegnerizzato per decision-making + ... + + def _act(self, action_name, args) -> str: + """Esegue azione tramite tool""" + if action_name in self.tools: + return self.tools[action_name].invoke(args) + return f"Tool {action_name} non trovato" + + def _goal_achieved(self, context, goal) -> bool: + """Valuta se goal è stato raggiunto""" + ... +``` + +--- + +## Dipendenze per Implementazione + +### Già nel progetto: +- `planning_enhancement.py` ✅ +- `simple_memory.py` ✅ +- `langchain_core` ✅ + +### Da installare: +```bash +# Per enhanced memory (già parzialmente presente) +# Nominic-embed-text model deve essere scaricato in Ollama +ollama pull nomic-embed-text +``` + +--- + +## Comandi per Test Futuro + +```bash +# Test futuro dell'agente +source venv/bin/activate +./venv/bin/python -c " +from enhancements.react_agent import ReActAgent +from ollama_agent import get_builtin_tools +from langchain_ollama import ChatOllama + +llm = ChatOllama(model='qwen3.5:4b', base_url='http://192.168.0.50:11434') +agent = ReActAgent(llm, tools=get_builtin_tools()) + +# Goal complesso +result = agent.run('Leggi il file requirements.txt e creane uno backup') +print(result) +" +``` + +--- + +## Conclusione + +**Il progetto attuale è un chatbot funzionante con tool calling avanzato.** + +**Per diventare agente AI servono**: +1. Agent Loop (ReAct pattern) - **Da implementare** +2. Integrazione planning esistente - **Da completare** +3. Integrazione memory esistente - **Da completare** +4. Goal decomposition via LLM - **Da implementare** + +Le fondamenta (planning, memory) esistono già nel progetto. Mancano l'integrazione e il loop operativo. + +--- + +*Documento generato: 23 Marzo 2026* +*Autore: OpenAgent* diff --git a/enhancements/react_agent.py b/enhancements/react_agent.py new file mode 100644 index 0000000..85d0819 --- /dev/null +++ b/enhancements/react_agent.py @@ -0,0 +1,398 @@ +""" +ReAct Agent - Agent Loop Implementation + +Questo modulo implementa un agente con loop Pensa→Agisci→Osserva→Ripianifica. +Ispirato al pattern ReAct (Reasoning + Acting) di Yao et al. + +L'agente: +1. THINK: Analizza il goal e decide la prossima azione +2. ACT: Esegue l'azione tramite tool +3. OBSERVE: Legge il risultato +4. EVALUATE: Verifica se il goal è raggiunto +5. REPLAN: Se fallito, ripianifica +""" + +import json +import re +from typing import List, Dict, Any, Optional, Callable +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum + +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage +from langchain_core.tools import Tool + +from enhancements.planning_enhancement import EnhancedPlanner, TaskPriority +from enhancements.simple_memory import SimplePersistentMemory + + +class AgentState(Enum): + """Stati dell'agente nel loop""" + THINKING = "thinking" + ACTING = "acting" + OBSERVING = "observing" + EVALUATING = "evaluating" + REPLANNING = "replanning" + DONE = "done" + FAILED = "failed" + + +@dataclass +class ActionResult: + """Risultato di un'azione""" + action_name: str + args: Dict[str, Any] + result: str + success: bool + error: Optional[str] = None + + +@dataclass +class Step: + """Singolo step nel loop dell'agente""" + step_number: int + state: AgentState + thought: str + action: Optional[ActionResult] = None + observation: Optional[str] = None + timestamp: datetime = field(default_factory=datetime.now) + + +class ReActAgent: + """ + Agente con loop ReAct (Reasoning + Acting). + + Esegue un goal attraverso iterazioni di: + - THINK: Decide cosa fare + - ACT: Esegue l'azione + - OBSERVE: Legge il risultato + - EVALUATE: Verifica se goal raggiunto + - REPLAN: Ripianifica se necessario + """ + + def __init__( + self, + llm, + tools: List[Tool], + max_iterations: int = 10, + planner: Optional[EnhancedPlanner] = None, + memory: Optional[SimplePersistentMemory] = None, + verbose: bool = False + ): + """ + Inizializza il ReActAgent. + + Args: + llm: Modello LLM da usare + tools: Lista di tool disponibili + max_iterations: Massimo numero di iterazioni + planner: Istanza EnhancedPlanner (creato se None) + memory: Istanza SimplePersistentMemory (creato se None) + verbose: Se True, stampa debug info + """ + self.llm = llm + self.tools = {tool.name: tool for tool in tools} + self.max_iterations = max_iterations + self.verbose = verbose + + # Planning e memory esistenti + self.planner = planner or EnhancedPlanner() + self.memory = memory or SimplePersistentMemory() + + # Stato interno + self.steps: List[Step] = [] + self.current_goal: Optional[str] = None + self.context: str = "" + + # System prompt per il loop + self._system_prompt = self._build_system_prompt() + + def _build_system_prompt(self) -> str: + """Costruisce il system prompt per il reasoning""" + tool_descriptions = [] + for name, tool in self.tools.items(): + desc = tool.description or "No description" + tool_descriptions.append(f"- {name}: {desc}") + + tools_list = "\n".join(tool_descriptions) + + return f"""Sei un AI Agent che ragiona e agisce per raggiungere obiettivi. + +TOOLS DISPONIBILI: +{tools_list} + +MODO DI OPERARE: +1. ANALIZZA il goal e il contesto attuale +2. DECIDI la prossima azione (usa un tool se necessario) +3. Se il goal è raggiunto, rispondi con SUCCESS +4. Se serve un'azione, rispondi con: TOOL:|arg1=val1|arg2=val2 +5. Se il goal non può essere raggiunto, rispondi con: FAIL: + +ESEMPI: +Goal: Elenca i file nella directory +→ TOOL:ls|path=. + +Goal: Leggi il file config.txt +→ TOOL:read_file|path=config.txt + +Goal: Mostra info sistema +→ TOOL:system_info + +Goal: Crea un file prova.txt con "hello" +→ TOOL:write_file|path=prova.txt|content=hello + +Goal: Qual è il contenuto del file? +(analizza il contesto - il file è già stato letto) +→ SUCCESS: Il file contiene XYZ + +RISPOSTA:""" + + def run(self, goal: str) -> Dict[str, Any]: + """ + Esegue un goal attraverso il loop ReAct. + + Args: + goal: Goal da raggiungere + + Returns: + Dict con success, result, steps, iterations + """ + self.current_goal = goal + self.context = f"GOAL: {goal}\n" + self.steps = [] + + if self.verbose: + print(f"\n🎯 GOAL: {goal}") + print("="*60) + + # Esegui prima azione + current_input = goal + + for iteration in range(1, self.max_iterations + 1): + # 1. THINK - analizza input corrente + thought = self._think(current_input) + + if self.verbose: + print(f"\n📝 Step {iteration} - THINK:") + print(f" {thought[:200]}...") + + # Check per terminazione + if thought.strip().upper().startswith("SUCCESS"): + result = thought[len("SUCCESS"):].strip() + self._add_step(AgentState.DONE, thought) + return self._build_result(True, result, f"Goal raggiunto in {iteration} step") + + if thought.strip().upper().startswith("FAIL"): + reason = thought[len("FAIL:"):].strip() + self._add_step(AgentState.FAILED, thought) + return self._build_result(False, self.context, reason) + + # 2. ACT - parse e esegui tool + action_result = self._act(thought) + self._add_step(AgentState.ACTING, thought, action_result) + + if self.verbose: + print(f" 🔧 ACTION: {action_result.action_name}") + print(f" 📤 RESULT: {action_result.result[:100]}...") + + # 3. OBSERVE + observation = f"[{action_result.action_name}] {action_result.result}" + self.context += f"\n{observation}" + self._add_step(AgentState.OBSERVING, thought, action_result, observation) + + # Se l'azione era un tool, verifica se il goal è raggiunto + if action_result.action_name != "respond": + # Goal semplice raggiunto - tool ha dato la risposta + # Restituisci SUCCESS con il risultato + self._add_step(AgentState.DONE, thought, action_result) + return self._build_result( + True, + action_result.result, + f"Goal raggiunto in {iteration} step con tool {action_result.action_name}" + ) + + # 4. EVALUATE - solo per risposte dirette + self._add_step(AgentState.EVALUATING, thought, action_result, observation) + + # Max iterations reached + return self._build_result( + False, + self.context, + f"Raggiunto massimo numero di iterazioni ({self.max_iterations})" + ) + + def _think(self, goal: str) -> str: + """LLM decide la prossima azione basata su contesto e goal""" + + # Usa intent detection esistente (già testata e funzionante) + from ollama_agent import OllamaToolAgent + + # Crea un agent temporaneo per detect + temp_agent = OllamaToolAgent(llm=self.llm, tools=list(self.tools.values())) + detected = temp_agent._detect_intent(goal) + + if detected: + # Formatta come TOOL:...| + args_str = "|".join(f"{k}={v}" for k, v in detected.arguments.items()) + result = f"TOOL:{detected.name}" + if args_str: + result += f"|{args_str}" + if self.verbose: + print(f" [DEBUG] Detected intent: {detected.name}, args: {detected.arguments}") + return result + + # Se non c'è intent detection, chiedi al LLM + tool_names = ", ".join(self.tools.keys()) + user_prompt = f"Goal: {goal}\nTool: {tool_names}\nRispondi con TOOL:nome|arg=val oppure SUCCESS:risposta" + + try: + messages = [ + SystemMessage(content=self._system_prompt), + HumanMessage(content=user_prompt) + ] + response = self.llm.invoke(messages) + if hasattr(response, 'content'): + return response.content.strip() + return str(response).strip() + except Exception as e: + return f"FAIL: Errore LLM: {str(e)}" + + def _act(self, thought: str) -> ActionResult: + """Esegue l'azione decisa da _think""" + # Parse TOOL:toolname|arg1=val1|... + match = re.match(r'^TOOL:(\w+)(?:\|(.+))?$', thought.strip()) + + if not match: + # Non è un'azione tool valida, ritorna come thought + return ActionResult( + action_name="respond", + args={}, + result=thought, + success=True + ) + + tool_name = match.group(1) + args_str = match.group(2) or "" if match.group(2) is not None else "" + + # Parse args + args = {} + if args_str: + for arg_part in args_str.split("|"): + if '=' in arg_part: + key, val = arg_part.split('=', 1) + args[key.strip()] = val.strip() + + # Esegui tool + if tool_name not in self.tools: + return ActionResult( + action_name=tool_name, + args=args, + result=f"Tool '{tool_name}' non trovato", + success=False, + error=f"Unknown tool: {tool_name}" + ) + + try: + tool = self.tools[tool_name] + if args: + result = tool.invoke(args) + else: + result = tool.invoke({}) + + result_str = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False) + + return ActionResult( + action_name=tool_name, + args=args, + result=result_str, + success=True + ) + except Exception as e: + return ActionResult( + action_name=tool_name, + args=args, + result=f"Errore nell'esecuzione: {str(e)}", + success=False, + error=str(e) + ) + + def _add_step( + self, + state: AgentState, + thought: str, + action: Optional[ActionResult] = None, + observation: Optional[str] = None + ): + """Aggiunge uno step alla cronologia""" + step = Step( + step_number=len(self.steps) + 1, + state=state, + thought=thought, + action=action, + observation=observation + ) + self.steps.append(step) + + def _build_result(self, success: bool, result: str, error: Optional[str] = None) -> Dict[str, Any]: + """Costruisce il risultato finale""" + return { + "success": success, + "result": result, + "error": error, + "iterations": len(self.steps), + "steps": [ + { + "step": s.step_number, + "state": s.state.value, + "thought": s.thought[:200] if s.thought else None, + "action": s.action.action_name if s.action else None, + "action_result": s.action.result[:100] if s.action and s.action.result else None, + "observation": s.observation[:200] if s.observation else None + } + for s in self.steps + ] + } + + def get_plan_progress(self) -> Dict[str, Any]: + """Restituisce statistiche sullo stato del planner""" + return { + "total_tasks": len(self.planner.tasks), + "plans": len(self.planner.plans) + } + + +def create_react_agent( + llm, + tools: Optional[List[Tool]] = None, + include_builtin_tools: bool = True, + max_iterations: int = 10, + verbose: bool = False +) -> ReActAgent: + """ + Factory function per creare un ReActAgent. + + Args: + llm: Modello LLM + tools: Lista tool personalizzati + include_builtin_tools: Se True, aggiunge tool built-in + max_iterations: Massimo iterazioni + verbose: Debug output + + Returns: + ReActAgent instance + """ + if tools is None: + tools = [] + + # Import built-in tools + if include_builtin_tools: + from ollama_agent import get_builtin_tools + builtin = get_builtin_tools() + tools = builtin + tools + + return ReActAgent( + llm=llm, + tools=tools, + max_iterations=max_iterations, + verbose=verbose + )