Tipologie di nodi
I nodi sono i mattoni che compongono il grafo del Tree Agent.
Ogni volta che si compila con successo un grafo, questo viene convertito in un file pronto per l'esecuzione. Durante questo processo, alcuni nodi, detti virtuali, vengono eliminati e le informazioni in essi contenute trasferite nel file finale. I nodi virtuali sono quelli di tipo: root, component, head, tail.
Nessun nodo può mai essere figlio di più nodi, e tutti i nodi, ad eccezione di quelli virtuali root, head e tail devono sempre avere un genitore!
Per offrire la massima flessibilità, esistono diverse tipologie di nodi, ognuno dotato di un differente set di callback e di proprietà, e con una funzione di esecuzione distinta. Indipendentemente dalla loro tipologia, tutti i nodi non virtuali condividono questo set di proprietà e callback:
name(str): è il nome del nodo;instruction(str | None): è una stringa richiamabile in fase di creazione del prompt;context(str | None): è una stringa richiamabile in fase di creazione del prompt;before(Callable | None): la callbackbefore;after(Callable | None): la callbackafter;is_blocking(bool): èTruese il nodo è bloccante (fase 1 dello step explore),Falsealtrimenti (a livello grafico si ha l'icona 🔒 attiva quando la proprietà èTrue);is_anchor(bool): èTruese il nodo è un anchor (fase 2 dello step explore explore),Falsealtrimenti (a livello grafico si ha l'icona ⚓ attiva quando la proprietà èTrue);similarity_threshold(float | None): se specificato sovrascrive la similarità minima che si deve raggiungere in fase di explore per scendere su un nodo figlio. In caso di unquerynode specifica invece la similarità minima che si deve raggiungere per estrarre i chunk dal vector store.
Seppure le proprietà sopra indicate siano comuni, alcune di queste potrebbero avere dei valori predefiniti per alcune tipologie di nodi, e pertanto apparire nascoste a livello di interfaccia grafica.
Root node
Questo nodo virtuale viene aggiunto automaticamente durante la creazione di un nuovo albero, e non può: nè essere duplicato nè essere cancellato. Il root node è utile per specificare alcune proprietà associate all'intero albero che, come tali, non appartengono ad alcun nodo specifico.
Nell'interfaccia, tramite il nodo root è possibile specificare:
init(Callable | None): funzione di inizializzazione delselfdisponibile a tutti i nodi definiti allo stesso livello dellaroot, quindi al di fuori di ogni componente;before_run(Callable | None): callbackbefore_run;after_run(Callable | None): callbackafter_run;default_models: dove è possibile definire il modello LLM da impiegare di default per ciascuna tipologia di nodo;default_similarity_threshold: dove è possibile definire la similarità minima da raggiungere per scendere in un nodo figlio durante lo stepexplore;memory_agent_props: le proprietà richieste dal Memory Agent.
Tutte le proprietà di default possono essere sovrascritte nodo per nodo tramite apposite proprietà
L'argomento special della callback extend message contiene:
class SimpleNamespace:
topics: str | None # i topics eventualmente inseriti tramite interfaccia grafica
conversation_goal: str | None # il conversation goal eventualmente inserito tramite interfaccia grafica
L'argomento special della callback after_run contiene il messaggio di risposta prodotto dal Tree Agent accessibile con special.agent_output
Il root può avere solo nodi figli di tipo agent
Anche se i nodi di tipo llm sono gli unici a poter richiamare liberamente un LLM per rispondere all'utente, i nodi di tipo open e close li utilizzano per fare la traduzione quando la callback translate è definita
Agent node
Tramite i nodi di tipo agent è possibile definire più agenti distinti.
L'agent node deve avere come genitore il root node e avere sempre e solo un nodo figlio.
Il nodo agent ha le seguenti proprietà extra:
is_primary(bool): seTrueallora in caso di primo messaggio, in mancanza di una callbackbefore_runche specifica un nodo da eseguire, lo step diexploresarà avviato dall'agente composto dai nodi appartenenti al sottoalbero di questo nodo;role(str) è il ruolo dell'agente. Questa informazione di default sarà accessibile tramite gli argomenti delle callbackpromptdi tutti i nodi appartenenti all'agente;fallback(Callable) funzione di fallback associata.
Sempre e solo un nodo agent ha la proprietà is_primary = True. A livello grafico è possibile riconoscerlo perchè ha l'icona ☆ attiva
LLM node
Il nodo di tipo llm è utile quando si vuole richiamare un LLM per interagire direttamente con l'utente, oppure per fare dei ragionamenti guidati.
Il nodo llm ha le seguenti proprietà extra:
model(str | None): se specificato, sovrascrive il modello LLM utilizzato di default;prompt(Callable): è la callbackprompt;is_mute(bool): seTrueallora l'output del nodo NON sarà inviato all'utente, ma sarà comunque accessibile nella callbackafterdel nodo stesso tramite l'argomentospecial.llm_response;response_format(dict): se abilitato il dizionario dovrà contenere la forma che si intende imporre alla risposta del LLM. 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).
L'argomento special delle callback after e prompt nel nodo di tipo llm contiene le seguenti informazioni:
class SpecialLlmNodeContext:
prompt_obj: dict | None # l'insieme di informazioni utili per la composizione del prompt
llm_response: str | None # la risposta prodotta dal LLM
fail_response_format: bool | None # diventa True se è stato impostato un response_format ed il LLM ha fallito la sua applicazione per attempts volte
con prompt_obj:
{
"role": str # il ruolo definito dentro l'agent node a capo del nodo attuale
"context": List[str] # una lista che contiene, concatenati uno dopo l'altro, i contesti definiti nella catena di nodi che parte dal nodo agente e finisce nel nodo corrente
"instructions": str # le istruzioni eventualmente inserite nel nodo attuale
}
Il prompt_obj è disponibile solo quando la callback è prompt. La llm_response è disponibile solo quando la callback è after e il nodo ha la proprietà is_mute = True, in caso contrario è None. Quando si vuole accedere alla risposta di un LLM non muto, visto che la riposta è stata restituita all'utente, è suffciente accedere all'ultimo messaggio dello storico della chat attraverso il Memory Agent.
La fail_response_format è disponibile solo quando la callback è after e il nodo ha specificato un response_format, in caso contrario è None.
Switch node
Il nodo di tipo switch è una versione più specifica e ottimizzata del llm node da utilizzare in quei casi in cui si ha bisogno di eseguire un LLM per individuare il corretto nodo da eseguire.
Il nodo switch ha le seguenti proprietà extra:
model(str | None): se specificato, sovrascrive il modello LLM utilizzato di default;alternatives: permette di indicare quali nodi eseguire in base alle descrizioni associate;
is_mute(bool): impostato automaticamente aTrue, quindi l'output del nodo NON sarà mai inviato all'utente, ma sarà accessibile nella callbackafterdel nodo stesso tramite l'argomentospecial.llm_response.
In particolare le alternatives permettono di specificare i possibili nodi da eseguire in base a delle condizioni indicate via testo. Ogni alternatives richiede le seguenti informazioni:
keyword: una parola chiave che descrive la condizione che attiva l'alternativa;node_target: il nodo da raggiungere quando la condizione si verifica secondo la descrizione fornita;step: lo step da settare quando si verifica la condizione;description: condizione che il LLM deve verificare per attivare o meno quell'alternativa;
Seguendo alternatives il framework imposta automaticamente il response_format, il prompt e la callback after.
Il sistema verifica che la risposta fornita dall'LLM rispetti il response_format indicato. Nel caso in cui:
- il LLM seleziona la condizione associata a
FALLBACK; - oppure il
response_formatviene ignorato per 2 volte consecutive; - oppure il LLM seleziona 0 o più di 1 opzione;
il sistema attiverà comunque l'alternativa associata a FALLBACK. Quindi è sconsigliato mettere delle alternative che si sovrappongono, poichè in quel caso si rischia di finire in FALLBACK.
Più alternatives vengono aggiunte maggiore è il rischio che il LLM fallisca nel riconoscimento di quella corretta. In caso di più di 5-6 alternative, considerare di annidarle su più livelli con 2 o più nodi in serie.
Open node
Il nodo di tipo open serve quando si vuole avere il massimo controllo sull'output da inviare, infatti questo nodo delega la produzione dell'output da produrre alla callback question.
Un nodo di tipo open ha le seguenti proprietà extra:
question(Callable) callbackquestion;translate(Callable) callbacktranslate;
L'argomento special della callback translate e after nel nodo di tipo open contiene le seguenti informazioni:
class SpecialOpenNodeContext:
question: str # la question indicata tramite l'apposita callback
files: List[FileMetadata] | None # i file inviati all'utente
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
Close node
Il nodo di tipo close serve quando si vuole fornire all'utente una serie di opzioni di risposta alternative. Come nel nodo open, la produzione dell'output è delegata alla callback question, ma a differenza di questo, a seguto della risposta è possibile specificare delle opzioni alternative tramite la callback options. Le opzioni appariranno sulla chat come pulsanti cliccabili.
Un nodo di tipo close ha le seguenti proprietà extra:
question(Callable): callback diquestion;options(Callable): callbackoptions;translate(Callable): callbacktranslate;
L'argomento special delle callback translate e after nel nodo di tipo close contiene le seguenti informazioni:
class SpecialCloseNodeContext:
question: str # la question indicata tramite callback question
original_options: List[str] # le opzioni specificate tramite callback options
options: List[str] # le opzioni specificate tramite callback options o eventualmente tradotte tramite callback translate
selected_option: str | None # l'opzione selezionata dall'utente (sempre nella lingua originale)
Il campo selected_option è popolato solo dentro la callback after
L'opzione selezionata viene automaticamente individuata facendo un controllo tra stringhe non rigido (se c'è un piccolo errore di battitura comunque viene individuata la risposta più simile). selected_option, se definito è sempre un elemento di original_options, quindi riporta la voce selezionata dall'utente in lingua originale.
Visto che comunque l'utente può evitare di cliccare un'opzione e scrivere liberamente, nel caso in cui nessuna opzione sia individuata, la proprietà selected_option sarà None
Query node
Il nodo di tipo query è l'unico che permette di immagazzinare e cercare tra i chunk definiti al suo interno. Questo nodo, infatti, oltre ai chunk con proprietà for_rag = False che vengono utilizzati durante lo step explore per individuare il prossimo nodo da eseguire, può contenere i chunk con proprietà for_rag = True.
Un nodo di tipo query ha le seguenti proprietà extra:
query(Callable) callbackquery;
L'argomento special della callback after nel nodo di tipo query contiene le seguenti informazioni:
class SpecialQueryNodeContext:
chunks: List[Chunk] # i chunk estratti dal vector store
assets: List[FileMetadata] # tutti i file presenti dentro i chunk estratti
con Chunk descritto in seguito nella sezione dedicata ai chunk e FileMetadata descritto in seguito nella sezione dedicata al File Agent:
Durante lo step explore vengono considerati sia i chunk con proprietà for_rag = True che quelli con for_rag = False
Empty node
Il nodo di tipo empty è un nodo privo di proprietà specifiche. Lo scopo di questo nodo è quello di consentire l'esecuzione delle sue callback before e after senza che venga lanciata alcun altra funzione.
Di seguito sono riportati i nodi relativi alla logica a componenti:
Component node
Il nodo virtuale di tipo component rappresenta un componente dall'esterno. Come per i nodi non virtuali, anche il nodo componente deve sempre avere un genitore.
Un nodo di tipo component ha le seguenti proprietà:
name(str): il nome del nodo;icon(str): l'icona che si vuole associare al componente recuperata da Lucid;
Per cambiare l'icona del nodo è sufficiente scegliere nel catalogo di lucid e copiare il nome in kebab-case
Head node
Il nodo virtuale di tipo head rappresenta sempre il primo nodo dentro un componente, di conseguenza NON ha mai alcun nodo genitore.
Un nodo di tipo head ha le seguenti proprietà:
init(Callable | None): funzione di inizializzazione delselfdisponibile a tutti i nodi definiti allo stesso livello dellahead, quindi all'interno del componente;
Se il componente contiene altri componenti, i nodi in essi contenuti avranno un loro self distinto ed isolato.
Tail node
Il nodo virtuale di tipo tail può essere considerato come l'ultimo nodo del componente. Questo è l'unico nodo che può essere sia collegato che non; infatti la conseguenza di collegare questo nodo è quello di permettere all'esterno di associare dei nodi figli al rispettivo nodo componente. In tal caso, i nodi figli all'esterno saranno da considerarsi come collegati direttamente al genitore del nodo tail.