Connesso · memoria volatile

Architettura

Moduli e ciclo di vita

Principi

  1. Il comportamento sta nei dati, non nel flusso di controllo. Un agente è una descrizione (nome, scopo, prompt, modello, tool). Il grafo si costruisce da queste descrizioni. Aggiungere capacità al sistema non richiede di riscrivere l'orchestrazione.
  2. Una sola decisione algoritmica: quando fermarsi. Il resto lo decide l'orchestratore a runtime. Le regole cablate sono limitate a limiti di sicurezza (turni massimi, timeout, fallback su errore di routing) e al passo fisso di classificazione in ingresso.
  3. Indipendenza dalle piattaforme. LangGraph è una libreria, non un servizio. Il tracing è nostro, i dati sono in un Postgres che controlli. LangSmith si può attivare, ma non serve.
  4. Degradazione graziosa. Senza database il sistema resta usabile in memoria volatile; senza prezzi in catalogo i costi sono zero, non un errore.

Mappa dei moduli

                        ┌───────────────────────────┐
   HTTP  ──────────────▶│  app/api  (FastAPI)       │
                        └─────────────┬─────────────┘
                                      │
                        ┌─────────────▼─────────────┐
                        │  app/services/chat.py     │  orchestrazione di un turno
                        └──┬────────┬────────────┬──┘
                           │        │            │
              ┌────────────▼──┐  ┌──▼─────────┐ ┌▼───────────────────┐
              │ app/memory    │  │ app/graph  │ │ app/observability  │
              │ contesto e    │  │ orchestr.  │ │ tracer, costi,     │
              │ persistenza   │  │ e agenti   │ │ analisi            │
              └───────┬───────┘  └──────┬─────┘ └─────────┬──────────┘
                      │                 │                 │
                      │        ┌────────▼────────┐        │
                      │        │ app/agents      │        │
                      │        │ app/tools       │        │
                      │        │ app/llm         │        │
                      │        └────────┬────────┘        │
                      │                 │                 │
                      └────────┬────────┴─────────────────┘
                               ▼
                        app/db  (Postgres / Supabase)
ModuloResponsabilitàNon fa
app/coreconfigurazione, logginglogica applicativa
app/llmcatalogo modelli, client Bedrock, prezziprompt
app/toolsregistro dei tooldecidere chi li usa
app/agentsdefinizioni e compilazione degli agentirouting
app/graphstato, classifier, orchestratore, subgraph, topologiapersistenza
app/memorycheckpoint, thread, memoria a lungo termine, riassuntitracing
app/observabilityspan, token, costi, aggregazioniesecuzione
app/servicesmette insieme i pezzi per un turno di chatdettagli HTTP
app/apitrasporto HTTP e schemilogica

La direzione delle dipendenze è sempre verso il basso: api → services → {graph, memory, observability} → {agents, tools, llm} → core/db. Nessun modulo in basso importa uno in alto.


Ciclo di vita di una richiesta

POST /api/chat con {"message": "...", "thread_id": "..."}.

  1. Threadmemory.threads.ensure_thread crea o recupera la conversazione. Al primo messaggio le assegna un titolo.
  2. Contestoservices.chat.build_context recupera il riassunto del thread e le memorie a lungo termine pertinenti al messaggio. Il risultato è un testo che verrà iniettato nei prompt di tutti gli agenti.
  3. Run e tracer — si apre una riga in runs e si crea un RunTracer, passato a LangGraph come callback.
  4. Esecuzione del grafo:
    • il classifier etichetta intento, urgenza, categoria e tono;
    • l'orchestratore decide se rispondere, quando inviare, goal, sollecito e a chi delegare (relationship, respond, ui_agent) oppure chiude;
    • respond produce la bozza e, se è una trattativa, chiama il sub-agente negotiate;
    • ui_agent è un grafo proprio che può fare interrupt() in attesa di approvazione umana (POST /api/chat/resume);
    • ogni agente torna all'orchestratore finché questo risponde FINISH o si raggiunge MAX_SUPERVISOR_TURNS.
  5. Persistenza — messaggio utente e risposta finiscono in messages, la run viene chiusa con token, costo, durata e agenti coinvolti.
  6. Riassunto — se la conversazione è cresciuta oltre la finestra, il riassunto si aggiorna in background, senza far attendere l'utente.

Esistono anche grafi dedicati, invocati da API proprie e tracciati con graph distinto: sollecito (bozza a scalata) e onboarding (intervista di setup cliente).

In streaming (POST /api/chat/stream) i passi sono gli stessi, ma gli eventi start, node, token, final vengono emessi via SSE mentre il grafo lavora.


Lo stato del grafo

Definito in app/graph/state.py. È ciò che il checkpointer salva ad ogni passo.

CampoSignificato
messagesconversazione; il reducer add_messages fonde gli aggiornamenti
contextriassunto e memorie iniettati all'avvio della run
classificationesito del Classifier (intento, urgenza, categoria, tono)
should_respond, send_at, reschedule_atpiano dell'orchestratore
goal, next_nudgeobiettivo del thread e prossimo sollecito
draft_response, negotiation_notesbozza di Respond e brief di Negotiate
relationship_contextbrief prodotto dal Relationship agent
approval_status, approved_textesito del grafo UI
next_agent, instructionesito dell'ultima delega dell'orchestratore
turnnumero di deleghe già effettuate (limite di sicurezza)
route_historytutte le decisioni prese, con la motivazione: è ciò che la UI mostra
agents_usedagenti coinvolti nella run
run_id, thread_id, user_ididentità, propagate a tracing e memoria
stop_reasonfinish, max_turns, no_response, scheduled, awaiting_approval, no_agents

Scelta importante: nello stato condiviso finisce solo la conclusione di ogni agente, non i suoi passaggi intermedi. I ragionamenti e le chiamate ai tool restano nel tracing (dove servono per il debug) ma non gonfiano il contesto degli altri agenti, che pagherebbero token per informazioni che non useranno.


Perché Classifier fisso + stella sull'orchestratore

Il Classifier è un passo deterministico (output strutturato) che gira sempre: è il pattern custom workflow consigliato da LangGraph quando si mescolano step prevedibili e nodi agentici.

Dopo la classificazione, ogni agente è collegato solo all'orchestratore. Le alternative sarebbero:

  • catena fissa (A → B → C): semplice ma rigida, ogni nuovo caso d'uso richiede una nuova catena;
  • handoff diretti fra agenti: più espressivo ma difficile da osservare;
  • stella con orchestratore: un solo punto in cui si decide se rispondere, quando inviare e a chi delegare.

Respond e UI agent sono subgraph (Negotiate è nidificato in Respond; UI agent ha stato e nodi propri, con interrupt() per l'approvazione umana).


Modelli e costi

config/models.yaml è l'unico posto in cui compaiono gli identificativi Bedrock e i prezzi. Il codice usa alias (claude-sonnet-4, nova-lite, ...). Cambiare modello a un agente è una riga di YAML; cambiare regione o passare a un inference profile diverso non tocca il codice.

I costi si calcolano dai token realmente riportati da Bedrock (usage_metadata dei messaggi), non da stime: app/llm/pricing.py.


Sicurezza e limiti

RischioContromisura
Ciclo infinito orchestratore ↔ agenteMAX_SUPERVISOR_TURNS e recursion_limit
Agente bloccato in loop di toolmax_iterations per agente
Run che non terminaRUN_TIMEOUT_SECONDS
Routing verso un agente inesistenteoutput vincolato a un Literal dei nomi reali
Modello che scrive nella memoria di un altro utenteuser_id letto dalla config della run, mai dai parametri del tool
Chiavi esposte al browseril frontend parla solo con il backend; la service key non lascia mai il server
Tabelle leggibili da chiunqueschema liagraph non esposto dall'API REST, RLS attiva senza policy