Tema
Nodo: ai
Una domanda a un modello per ogni record, con la risposta che arricchisce il record. È la stessa meccanica di un lookup — batch, cache, retry, dead-letter — con un tipo tutto suo, perché nel grafo e nel DAG si veda dove decide un modello.
| Metodo | .ai(from, id, config, { deadLetter?, fromPort?, purpose?, recordReport?, writeReport? }) |
|---|---|
| Ingressi | 1 |
| Uscite | ≥1 (fan-out libero) |
| Dead-letter | sì, per record |
| Codice | src/core/ai.ts (AiConfig è un alias di LookupConfig), configurazioni nei connettori |
Quando usarlo
Quando il valore che serve non si può calcolare né interrogare: estrarre campi da un documento, classificare un testo, ricavare un dato da un allegato. Se la risposta sta in un database o in un'API è un lookup; se è una regola scrivibile in codice è una transform, che costa nulla ed è deterministica.
Il tipo è una dichiarazione: il core non verifica che il resolver parli davvero con un modello, come non verifica che un'action scriva davvero. Serve a chi legge il DAG.
La configurazione
Identica a quella del lookup — resolver, key, merge, onNotFound, batchSize, cache, maxCacheEntries, outputSchema, retry, rateLimit — perché la semantica è la stessa: una lettura senza effetti, ripetibile.
Oggi l'unica configurazione pronta è noevaAiExtract (noeva): estrae campi da uno schema Zod, una chiamata per documento, con regole di aggregazione obbligatorie per campo.
Esempio
ts
.ai<Documento, DocumentoEstratto>("testi", "estrai-campi", noevaAiExtract(client, {
modelId,
prompt,
schema: campiSchema, // i campi attesi, validati alla risposta
documenti: (d) => d.fileIds,
regole: { numeroOrdine: "primo-non-vuoto", totale: "somma" },
merge: (campi, d) => ({ ...d, ...campi }),
}), { deadLetter: daRivedere })
.transform("estrai-campi", "contratto", validate("contratto", documentoEstrattoSchema))Errori
Una risposta che non rispetta lo schema è un LookupError, come una riga che non torna da un database: dead-letter del nodo, ramo catch sotto un try-catch, altrimenti fail-fast. Va sempre usato con uno dei due: un documento storto capita, e non deve fermare gli altri.
Timeout e indisponibilità del servizio sono errori transitori: si ritentano con retry, e rateLimit distanzia le chiamate.
Nel DAG e nel report
stepType: "ai" nel piano e kind: "ai" nel report: nell'app il nodo è verde acqua, con l'icona scintilla e il badge «AI». Il descriptor porta il modello e i campi richiesti — mai il prompt né il testo dei documenti. Come nel lookup, outputSchema dà al nodo le colonne del proprio output, e in nodeReports il nodo porta sotto lookup gli stessi contatori della run. Con recordReport il report elenca anche che cosa ha risposto il modello per ogni record: è il modo di rileggere una decisione senza aprire i log.
Dal vivo
Come il lookup, il nodo ai ha un in lavorazione: i record della finestra in attesa del modello. Conta record e non domande, quindi tre documenti con due testi distinti valgono tre anche se le domande sono due. Un numero fermo vuol dire che il modello sta rispondendo lento.
Anti-pattern
.lookup(…, noevaAiExtract(…)): compila (è lo stesso tipo di configurazione), ma nel DAG il modello sparisce dietro un lookup qualunque..ai(…)con un resolver SQL o REST: il core non lo impedisce, ma il grafo direbbe una cosa falsa.- Una chiamata al modello dentro una
transform: è IO nascosto, e per giunta il più costoso. - Fidarsi della risposta senza contratto: lo schema Zod è ciò che rende l'estrazione verificabile.

