Il progetto è pensato per crescere per aggiunte, non per modifiche. Ecco i punti di estensione, dal più frequente al più raro.
Regola generale: se tocchi qualcosa che la documentazione già copre, aggiorna
anche i file in docs/ nello stesso intervento.
1. Aggiungere un agente
Una voce in backend/config/agents.yaml:
agents:
revisore:
label: Revisore
description: >
Rilegge un testo prodotto da un altro agente e ne verifica coerenza,
correttezza dei dati e tono. Da usare prima della consegna finale su
contenuti importanti.
model: claude-sonnet-4
temperature: 0.1
tools: [calculator]
max_iterations: 4
prompt: |
Sei l'agente revisore. Elenca i problemi che trovi in ordine di gravità e
proponi la correzione. Se il testo va bene, dillo in una riga.
ui:
color: "#f97316"
Poi POST /api/graph/reload (o il pulsante "Ricarica configurazione" nella
sezione Workflow). Il nodo compare nel grafo e l'orchestratore inizia a
considerarlo.
La description è il campo più importante. È l'unica cosa che l'orchestratore
legge per decidere se delegare a questo agente. Scrivi quando usarlo, non cosa
sa fare in astratto. Confronta:
| Descrizione debole | Descrizione utile |
|---|---|
| "Agente esperto di testi" | "Rilegge un testo già scritto e verifica coerenza e dati. Da usare prima della consegna su contenuti importanti." |
Se due agenti hanno descrizioni che si sovrappongono, il routing diventa instabile: meglio un agente in meno con un perimetro netto.
Agenti definiti in codice
Quando il prompt o i tool dipendono da qualcosa di dinamico:
# app/agents/personalizzati.py
from app.agents.registry import register_agent
from app.agents.spec import AgentSpec, AgentUI
register_agent(
AgentSpec(
name="report",
label="Reportista",
description="Genera report a partire dai dati aziendali del mese corrente.",
prompt=costruisci_prompt_dal_database(),
model="claude-sonnet-4",
tools=["calculator", "query_vendite"],
ui=AgentUI(color="#22d3ee"),
)
)
Va importato all'avvio (per esempio in app/agents/__init__.py). Gli agenti
registrati da codice hanno la precedenza su quelli omonimi del YAML.
2. Aggiungere un tool
# app/tools/vendite.py
from langchain_core.tools import tool
from app.tools.registry import register_tool
@tool
async def query_vendite(mese: str, prodotto: str | None = None) -> str:
"""Restituisce il fatturato di un mese, opzionalmente filtrato per prodotto.
Args:
mese: mese nel formato AAAA-MM.
prodotto: codice prodotto; se assente considera tutti i prodotti.
"""
righe = await client.fetch(...)
return formatta(righe)
register_tool(query_vendite)
Poi importa il modulo in app/tools/__init__.py e assegna il tool a un agente in
agents.yaml.
Regole pratiche:
- la docstring è il contratto: è ciò che il modello legge per decidere se e come chiamare il tool. Descrivi anche i formati attesi;
- aggiorna sempre la mini docs dei tool: quando aggiungi o modifichi un
tool, aggiorna
docs/TOOLS.mdnello stesso commit; - restituisci stringhe già leggibili, non JSON grezzo;
- non sollevare eccezioni per errori attesi: restituisci un messaggio di errore comprensibile, così l'agente può correggersi da solo;
- dati sensibili dalla config, non dai parametri: se un tool opera su dati di
un utente, leggi
user_iddallaRunnableConfigcome faapp/tools/memory_tools.py, non da un argomento che il modello può inventare.
3. Aggiungere o cambiare un modello
In backend/config/models.yaml:
models:
nova-premier:
model_id: eu.amazon.nova-premier-v1:0
provider: amazon
label: Amazon Nova Premier
supports_tools: true
max_tokens: 8192
temperature: 0.2
pricing:
input_per_1m: 2.50
output_per_1m: 12.50
Verifica che l'id esista nella tua regione:
aws bedrock list-foundation-models --region eu-central-1 \
--query "modelSummaries[].modelId" --output text | tr '\t' '\n' | grep nova
Se ottieni ValidationException: on-demand throughput isn't supported, il
modello richiede un inference profile: aggiungi il prefisso di regione (eu.,
us., apac.).
Per usare un provider diverso da Bedrock, l'unico file da toccare è
app/llm/factory.py: restituisce un BaseChatModel, e a valle nessuno sa quale
provider ci sia dietro.
4. Aggiungere un nodo che non è un agente
Serve per passi deterministici: validazioni, chiamate a sistemi esterni,
arricchimenti. In app/graph/builder.py:
async def valida_output(state: LiaState) -> dict:
ultimo = state["messages"][-1]
if contiene_dati_sensibili(ultimo.text()):
return {"messages": [AIMessage(content="Risposta bloccata dai filtri.")]}
return {}
builder.add_node("validatore", valida_output)
builder.add_edge("respond", "validatore")
builder.add_edge("validatore", ORCHESTRATOR_NODE)
Ricorda di aggiornare app/graph/topology.py se vuoi che il nodo compaia anche
nella vista workflow.
5. Approvazione umana (human-in-the-loop)
Il checkpointer rende possibile sospendere una run e riprenderla in seguito:
from langgraph.types import interrupt
async def conferma_umana(state: LiaState) -> dict:
decisione = interrupt({"domanda": "Confermi l'invio?", "bozza": state["messages"][-1].text()})
return {"messages": [AIMessage(content=f"Decisione: {decisione}")]}
La run si ferma; per riprenderla si invoca il grafo con Command(resume=...) e
lo stesso thread_id.
6. Aggiungere un secondo grafo
build_graph() costruisce il grafo principale. UI agent è già un secondo grafo
(app/graph/ui_graph.py) invocato come subgraph. Per altre pipeline (batch
senza conversazione), crea un modulo accanto; la colonna graph nella tabella
runs esiste già per distinguerli nelle statistiche.
Esempio: workflow sollecito
Grafo lineare indipendente (non multi-agent):
START → load_memory → choose_type → apply_strategy → draft_nudge → END
- codice:
backend/app/graph/sollecito/ - service:
backend/app/services/sollecito.py - API:
POST /api/sollecito,POST /api/sollecito/outcome - memoria esiti: kind
nudge_outcome(cosa ha funzionato / no) - run tracciate con
graph="sollecito"
Esempio: workflow onboarding
Grafo indipendente per l'intervista di configurazione cliente (skill
lia-onboarding-setup). Percorso predeterminato, un nodo LLM per interpretare
ogni risposta:
START → load_setup → interpret_answer → compose_reply → persist_setup → END
- codice:
backend/app/graph/onboarding/(includeSKILL.md) - service:
backend/app/services/onboarding.py - API:
POST /api/onboarding,GET /api/onboarding/setup,GET /api/onboarding/topology - ogni POST è un turno di chat; lo stato vive nel checkpointer sul thread
onboarding:{thread_id} - a conferma finale salva
client_setupeteam_policy(tono, policy solleciti, conversazione, comportamento del team agentico) - UI: sezione Workflow → tab Onboarding
7. Estendere la UI
- nuova metrica: aggiungi la query in
app/observability/analytics.py, esponila inroutes_traces.py, aggiungi il tipo inlib/types.tse il componente incomponents/observability/; - nuovo dettaglio di un nodo: arricchisci
export_topology()e leggilo incomponents/workflow/NodeInspector.tsx; - nuova sezione: crea
app/<nome>/page.tsxe aggiungi la voce incomponents/Nav.tsx.
Errori tipici
| Sintomo | Causa probabile |
|---|---|
| L'orchestratore ignora un agente | descrizione vaga o sovrapposta a un altro |
| L'orchestratore delega all'infinito | prompt poco netto sul quando chiudere; alza o abbassa MAX_SUPERVISOR_TURNS per diagnosticare |
| Un tool non viene mai chiamato | docstring che non dice quando usarlo |
| Costi a zero nella UI | modello assente da models.yaml, oppure model_id diverso da quello reale |
ValidationException da Bedrock | manca il prefisso dell'inference profile nel model_id |
| Nessuna traccia salvata | TRACING_ENABLED=false o DATABASE_URL mancante |