feat: aggiungi ReAct Agent Loop per trasformare chatbot in agente

- 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
This commit is contained in:
opencode committed 2026-03-24 09:32:00 +01:00
1 parent e5ce70c68e
commit 1c7781a68f
2 files changed
+625

No files matched your search

+227
View File
@@ -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*
+398
View File
@@ -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:<nome_tool>|arg1=val1|arg2=val2
5. Se il goal non può essere raggiunto, rispondi con: FAIL:<reason>
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
)