Callback
Le callback sono delle funzioni python che possono essere definite dentro i nodi in base alla loro tipologia. Tali funzioni consentono di personalizzare in maniera approfondita il comportamento dell'agente durante la sua esecuzione.
Ciascuna callback, in base alla sua tipologia ha una firma predefinita. In input gli argomenti passati sono:
self: un riferimento del componente comune a tutti i nodi in esso contenuti;context: è il contesto dal quale è possibile accedere a diversi strumenti utili tra cui ilsession_state;special: è un contesto specifico che è disponibile in alcuni nodi. Vedere il capitolo sui nodi per approfondire;links: è disponibile solo nelle callbackaftere permette di navigare tra i nodi;
Il self rimane visibile solo ai nodi che sono figli diretti del componente, di conseguenza, se ho dei componenti annidati, i nodi figli del componente interno non possono accedere al self del componente esterno e viceversa!
links è un types.SimpleNamespace pertanto per accedere al suo contentuo richiede la notazione con il punto, mentre la notazione con le [] non è supportata. Nel caso in cui si volesse accedere in modo dinamico ad una proprietà di links occorre usare il comando getattr(links, prop), con prop una variabile di tipo stringa che contiene il nome della proprietà richiesta.
Tutte le callback sono di default sincrone, tuttavia, in caso di necessità, inserendo il comando async prima della firma è possibile renderle asincrone.
Di seguito sono descritte, una per una, tutte le callback presenti:
Before
Nodi supportati: empty query emit llm switch
La callback before di un nodo viene lanciata nel relativo step, quindi sempre prima della sua esecuzione.
(self, context) => None
Visto che questa callback non produce alcun output, generalmente viene impiegata per fare chiamate API o interagire con il session_state
After
Nodi supportati: empty query emit llm
La callback after di un nodo viene lanciata durante lo step after, ovvero a seguito della sua esecuzione. Grazie all'oggetto BaseAfterOutput restituito da questa funzione, è possibile controllare il successivo svolgimento dell'interazione. In particolare, si può specificare il prossimo nodo e lo step da eseguire tramite node_target e step. Invece outcome permette di specificare un fallimento avvenuto durante l'esecuzione del nodo. Il lancio di un fallimento comporta l'esecuzione del meccanismo reach_anchor. Il parametro resume permette di sfruttare la gestione multi agente del Tree Agent.
(self, context, special, links) => BaseAfterOutput
con
class BaseAfterOutput:
node_target: int | None = None # nodo che si intende eseguire dopo l'esecuzione della callback
step: Step = Step.EXPLORE # step che si vuole eseguire dopo l'esecuzione della callback
outcome: Outcome = Outcome.SUCCESS # se impostato con Outcome.FAILURE vengono ignorati gli altri parametri, e si attiva il meccanismo di reach_anchor
resume: bool = False # se True viene recuperata l'esecuzione sull'agente indicato tramite node_target (se node_target non è un agent_node viene restituito un errore!)
con
class Outcome(Enum):
SUCCESS = "success"
FAILURE = "failure"
Non è necessario che l'id del nodo indicato tramite node_target sia figlio o discendente del nodo attuale.
Sia per l'after che per il before_run:
- è possibile abilitare il
resume, questo comando è utile solo in caso di multi agenti; - se non si specifica il
node_target, a meno che non si abilitais_failed(solo nel caso delafter) si proseguirà con il normale step diexplore
Gli unici step ammessi sono: BEFORE e EXPLORE!
Extend Message
Nodi supportati: root
La callback extend_message restituisce il prompt che viene poi passato al LLM indicato nel quarto punto dell'esecuzione generale.
(self, context, special) => BaseExtendMessagePromptOutput
con
class BaseExtendMessagePromptOutput:
prompt: str # prompt da passare al LLM per inizializzare context.input.extended
Before run
Nodi supportati: root
Come indicato nel quinto punto dell'esecuzione generale, la callback before_run viene eseguita appena si riceve il messaggio dell'utente e prima di eseguire l'albero.
(self, context, special, links) => BaseBeforeRunOutput
con
class BaseBeforeRunOutput:
node_target: int | None # nodo che si intende eseguire dopo l'esecuzione della callback
step: Step | None # step che si vuole eseguire dopo l'esecuzione della callback
resume: bool | None # viene recuperata l'esecuzione sull'agente indicato tramite node_target (se node_target non è un agent_node viene restituito un errore!)
Questa callback è particolarmente utile in caso di multi agenti: si possono realizzare degli agenti che si occupano unicamente di analizzare la richiesta dell'utente e selezionano l'agente migliore che si deve occupare di rispondere. Grazie a questo nodo, tramite in node_target è possibile puntare a tali agenti. Se node_target viene lasciato a None, allora il resto degli argomenti vengono ignorati, e viene ripresa l'esecuzione a partire dalla callback after dell'ultimo nodo eseguito.
Ad esempio è possibile sviluppare un agente specializzato sull'analisi dei documenti che l'utente carica in chat e la successiva estrazione delle informazioni rilevanti nel session_state. Grazie a questa callback si possono deviare tutte le interazioni avviate da un messaggio con file allegati.
Per la prima interazione della sessione, se la before_run non specifica alcun nodo da eseguire, sarà avviato lo step di explore a partire dall'unico nodo agent figlio del root che ha la proprietà is_primary = True.
After run
Nodi supportati: root
La callback after_run viene eseguita appena si conclude l'esecuzione dell'albero.
(self, context, special) => None
Visto che questa callback non produce alcun output, generalmente viene impiegata per fare chiamate API o interagire con il session_state.
Fallback
Nodi supportati: agent
La callback fallback viene eseguita ogni volta che, durante l'interazione, si verifica un'eccezione all'interno della callback di un nodo qualsiasi. Oltre ai normali errori non gestiti che possono verificarsi in una callback, vengono gestite anche le eccezioni di tipo LoopError, generate dal framework quando viene identificato un rischio di loop. Tramite l'output è possibile indicare il nodo che sarè immediatamente lanciato partendo dalla callback before.
Onde evitare possibili loop, è proibito riprendere l'esecuzione del nodo dal quale è partito l'errore.
(self, context, special, links) => BaseFallbackOutput
con
class BaseFallbackOutput:
node_target: int # nodo che si intende eseguire dopo l'esecuzione della callback
Prompt
La callback prompt è disponibile ai nodi che devono richiamare un LLM per rispondere ad una richiesta. Questa callback consente di specificare il prompt che si intende inviare al LLM.
(self, context, special) => BasePromptOutput
con
class BasePromptOutput:
prompt: str | None = None # prompt da inviare al LLM
system_prompt: str | None = None # system prompt da inviare al LLM
files: List[FileMetadata] | None = None # eventuali file che si intende inviare al LLM
segments: List[PromptSegment] | None = None # prompt composto da una sequenza di testi e file
model_params: Dict[str, Any] | None = None # parametri del modello (p.e. {"temperature": 0.1, "web_search": True})
In model_params è possibile inserire una qualsiasi coppia chiave-valore, dove la chiave è il nome della proprietà (p.e. "web_search", "temperature", "reasoning_effort") e il valore è quello che il parametro dovrà assumere. Se il modello non è compatibile con un parametro, il servizio LLM lo scarterà semplicemente.
con
class PromptSegment:
type: Literal["text", "files"] # il tipo del segmento
content: str | List[FileMetadata] # il contenuto del segmento
Come specificato nella sezione del File Agent il FileMetadata può essere ricavato dai precedenti file scambiati in chat, dai file estratti tramite RAG e da quelli generati direttamente in code
Attualmente il parametro web_search di model_params è compatibile solo con i modelli offerti dai provider Azure e OpenAI
Response format
Nodi supportati: llm
La callback response_format permette di specificare la struttura dell'output che il LLM deve rispettare. Per definire questa struttura si deve far riferimento al response format impiegato da OpenAI. In caso di provider LLM diverso, il framework convertirà autimaticamente il response format per mantenere la compatibilità;
attemptsspecifica il massimo numero di chiamate al LLM che verranno fatte in caso di mancato rispetto dei vincoli imposti dalresponse_format. In caso di superamento di tale limite, l'esecuzione del nodo sarà interotta e si passerà immediatamente ad eseguire la callbackafterdel nodo. Per gestire questi casi, tramitespecial.fail_response_formatsi può verificare se l'output è stato prodotto con successo (False) o meno (True).
(self, context) => BaseResponseFormatOutput
con
class BaseResponseFormatOutput:
schema: dict # la struttura da rispettare
attempts_limit: int # il numero di tentativi che il llm può utilizzare per rispondere correttametne
Ad esempiose voglio costringere il llm a classificare una domanda, posso scrivere:
schema = {
"type": "object",
"title": "InquiryAnalysis",
"required": ["is_urgent", "category"],
"properties": {
"is_urgent": {
"type": "boolean",
"description": "Indica se la richiesta necessita di una gestione prioritaria (entro 24 ore)."
},
"category": {
"type": "string",
"enum": ["ORDER", "SUPPORT", "FAQ"],
"description": "Macro-categoria di appartenenza della richiesta basata sul contenuto."
}
},
"additionalProperties": False
}
Non basarti solo sulle description per indicare al llm come utilizzare lo schema, ma ripeti le regole dentro il prompt, il LLM dende a rispettarle in modo molto più preciso!
Emit
Nodi supportati: emit
La callback emit è l'unico punto in cui è possibile inviare una risposta all'utente. Il messaggio può essere composto esplicitamente tramite codice (MessageType.STATIC) oppure delegato a un LLM (MessageType.GENERATED). Le options consentono di mostrare all'utente delle possibili scelte. Le sources permettono di allegare fonti alla risposta.
(self, context) => BaseEmitOutput
con
class BaseEmitOutput:
message: Message # il testo da inviare all'utente
options: list[str] | None = None # lista di opzioni che si intende mostrare all'utente
attachments: List[FileMetadata] | None = None # eventuali file che si intende inviare all'utente
dynamic_attachments: List[FileMetadata] | None = None # eventuali file che si intende inviare all'utente tramite pattern {{}}
sources: list[Source] | None = None # sono le fonti che si intende inviare all'utente
wait_interaction: bool = False # se True indica al client che in risposta all'output emesso ci si aspetta una nuova richiesta associata
con
class Message:
type: MessageType # il tipo di messaggio
content: str | None # il contenuto del messaggio statico
class MessageType(Enum):
STATIC = "static" # se esplicitato tramite content
GENERATED = "generated" # se prodotto dal LLM tramite la callback prompt
NONE = "none" # se è una risposta priva di messaggio testuale
class Source:
title: str | None = None # il titolo della fonte
content: str # il contenuto della fonte
Gli attachments, sia quelli dinamici che non, permettono di inviare all'utente dei file. Tuttavia, mentre gli attachments sono sempre mostrati in fondo al messaggio, quelli dynamic_attachments sono mostrati solo se esplicitamente citati all'interno di message. Nello specifico, nel contenuto del messaggio STATICO o nella risposta dell'LLM, se appare un pattern del tipo {{orginal_file_name.format}}, allora il widget chat nasconderà il match ed al suo posto farà apparire il file corrispondente. Di conseguenza, quando ci si affida al LLM occorre spiegare nel prompt questa regola ed indicargli in quali casi citare ciascun file!
Se si specifica un messaggio GENERATED non si può specificare content, viene direttamente popolato dalla risposta del LLM.
Preferire MessageType.GENERATED rispetto a costruire manualmente un MessageType.STATIC con la risposta ottenuta da un nodo llm. Nel secondo caso il framework attende che l'intera risposta del LLM sia prodotta prima di girarla al nodo emit, mentre con GENERATED i token vengono trasmessi all'utente in streaming man mano che vengono generati.
Query
Nodi supportati: query
La callback query permette di realizzare una ricerca semantica o un'operazione di scroll tra i chunk con proprietà for_rag = True presenti sul vector store associato al nodo corrente.
(self, context) => BaseQueryOutput
con
class BaseQueryOutput:
query: str | None = None # stringa da cui estrarre l'embedding per fare la ricerca (non richiesto solo per le operazioni di scroll)
filter: models.Filter | None = None # eventuali filtri da applicare sfruttando i tag eventualmente associati a ciascun chunk
black_list: AiBlackList | None = None # eventuali chunk da escludere
chunk_qty: int = 3 # massimo numero di chunk estraibili
threshold: float = 0.25 # soglia minima di similarità richiesta per l'estrazione dei chunk (ignorata in caso di scroll)
offset: str | None = None # pagina/cursore da cui ripartire per l'operazione di scroll
In particolare, oltre a effettuare classiche ricerche semantiche, la callback permette di scorrere (scroll) i chunk del Vector Store applicando le stesse logiche di filtraggio. In caso di scroll, l'estrazione per similarità non avviene: di conseguenza i campi query e threshold diventano superflui (e per questo sono opzionali), mentre può essere valorizzato il campo offset per gestire la paginazione.
model.Filter è una classe implementata tramite libreria Qdrant. Fare riferimento alla loro documentazione per implementare la logica di filtri.
Ad esempio, se nei chunk del nodo ho associato tramite extra la proprietà "category" ed in fase di estrazione (o di scroll) voglio considerare solo i chunk della categoria "food", posso scrivere:
filter = models.Filter(
must=[
models.FieldCondition(
key="category",
match=models.MatchValue(value="food")
)
]
)