Connesso · memoria volatile

Estendere il sistema

Agenti, tool, grafi

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 deboleDescrizione 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.md nello 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_id dalla RunnableConfig come fa app/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/ (include SKILL.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_setup e team_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 in routes_traces.py, aggiungi il tipo in lib/types.ts e il componente in components/observability/;
  • nuovo dettaglio di un nodo: arricchisci export_topology() e leggilo in components/workflow/NodeInspector.tsx;
  • nuova sezione: crea app/<nome>/page.tsx e aggiungi la voce in components/Nav.tsx.

Errori tipici

SintomoCausa probabile
L'orchestratore ignora un agentedescrizione vaga o sovrapposta a un altro
L'orchestratore delega all'infinitoprompt poco netto sul quando chiudere; alza o abbassa MAX_SUPERVISOR_TURNS per diagnosticare
Un tool non viene mai chiamatodocstring che non dice quando usarlo
Costi a zero nella UImodello assente da models.yaml, oppure model_id diverso da quello reale
ValidationException da Bedrockmanca il prefisso dell'inference profile nel model_id
Nessuna traccia salvataTRACING_ENABLED=false o DATABASE_URL mancante