Un esempio di esecuzione automatica di funzioni ("tool") da un modello linguistico locale¶
Luca Mari, settembre 2024
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Non commerciale - Condividi allo stesso modo 4.0 Internazionale.
Obiettivo: comprendere qualche aspetto della logica delle architetture ad agenti e dell'esecuzione automatica di funzioni.
Precompetenze: basi di Python.
Per eseguire questo notebook, supponiamo con VSCode, occorre:
- installare un interprete Python
- scaricare da https://ollama.com e installare Ollama
- scaricare da Ollama un modello capace di operare con strumenti, supporremo
llama3.1:8b
:
ollama pull llama3.1
- scaricare da https://code.visualstudio.com/download e installare VSCode
- eseguire VSCode e attivare le estensioni per Python e Jupyter
- ancora in VSCode:
- creare una cartella di lavoro e renderla la cartella corrente
- copiare nella cartella il file di questa attività: searchingagents.ipynb
- aprire il notebook
searchingagents.ipynb
- creare un ambiente virtuale locale Python (Select Kernel | Python Environments | Create Python Environment | Venv, e scegliere un interprete Python):
- installare i moduli Python richiesti, eseguendo dal terminale:
pip install pyautogen
- eseguire dalla linea di comando:
OLLAMA_MODELS=xxx OLLAMA_HOST=127.0.0.1:1234 ollama serve
dovexxx
è la directory che contiene i modelli Ollama (in Linux potrebbe essere/var/lib/ollama/.ollama/models
)
Importiamo i moduli Python necessari e specifichiamo la configurazione per il modello linguistico che sarà usato (deve essere in grado di operare con strumenti) e l'indirizzo del server su cui sarà in esecuzione in locale (a sua volta, il server deve essere in grado di gestire strumenti: al momento Ollama, ma non LM Studio).
import autogen
from typing_extensions import Annotated
llm_config = {
"config_list": [{ "base_url":"http://localhost:1234/v1",
"model":"llama3.1:8b",
"api_key":"not_used" }],
"timeout": 120,
"cache_seed": None,
}
Definiamo una semplice architettura con un agente di interfaccia ed esecutore (user_proxy
) e un agente che gestisce il modello linguistico (domain_expert
).
user_proxy = autogen.UserProxyAgent(
name="interfaccia con l'utente ed esecutore di codice",
is_termination_msg=(lambda msg: "conclus" in msg["content"].lower()), # a volte potrebbe essere "concluso" o "conclusa"...
human_input_mode="NEVER",
code_execution_config={"use_docker": False},
max_consecutive_auto_reply=5,
)
domain_expert = autogen.AssistantAgent(
name="esperto di dominio",
system_message="Se ti sono richieste informazioni su libri, usa solo la funzione disponibile per la ricerca nel tuo archivio interno, senza mai ricorrere alla tua memoria. Quando hai completato la ricerca, scrivi CONCLUSO.",
llm_config=llm_config,
)
Questa è la funzione Python che dovrebbe essere eseguita quando richiesto, resa disponibile all'agente esperto di dominio
grazie ai decoratori (per semplicità manteniamo il contenuto dell'archivio su cui fare ricerche direttamente all'interno della funzione).
@user_proxy.register_for_execution()
@domain_expert.register_for_llm(description="Cerca informazioni su un libro dal tuo archivio.")
def cerca(
titolo: Annotated[str, "titolo del libro"],
informazione: Annotated[str, "genere di informazione da cercare: editore o prezzo"]
) -> str:
dati = {
"Questo libro ha un titolo inventato": {"editore": "Borgesiana", "prezzo": "13 €"},
"Stiamo davvero scherzando": {"editore": "Cose serie", "prezzo": "15 €"},
"Cose leggere": {"editore": "Siamo ambigui", "prezzo": "12 €"},
}
return dati.get(titolo, "Titolo non trovato...").get(informazione, "Informazione non trovata...")
Verifichiamo che, grazie ai decoratori, l'agente esperto di dominio
sia stato dotato dello schema json con la dichiarazione della funzione (la documentazione dei "tools" per gli "Assistants" è qui:
https://platform.openai.com/docs/guides/function-calling
https://platform.openai.com/docs/api-reference/assistants/modifyAssistant#assistants-modifyassistant-tools).
domain_expert.llm_config["tools"]
[{'type': 'function', 'function': {'description': 'Cerca informazioni su un libro dal tuo archivio.', 'name': 'cerca', 'parameters': {'type': 'object', 'properties': {'titolo': {'type': 'string', 'description': 'titolo del libro'}, 'informazione': {'type': 'string', 'description': 'genere di informazione da cercare: editore o prezzo'}}, 'required': ['titolo', 'informazione']}}}]
Ecco dunque un esempio di uso di questa architettura.
domain_expert.reset()
res = user_proxy.initiate_chat(domain_expert, message="Qual è il prezzo del libro 'Stiamo davvero scherzando'?")
interfaccia con l'utente ed esecutore di codice (to esperto di dominio): Qual è il prezzo del libro 'Stiamo davvero scherzando'? -------------------------------------------------------------------------------- Qual è il prezzo del libro 'Stiamo davvero scherzando'? -------------------------------------------------------------------------------- [autogen.oai.client: 09-23 14:00:52] {349} WARNING - Model llama3.1:8b is not found. The cost will be 0. In your config_list, add field {"price" : [prompt_price_per_1k, completion_token_price_per_1k]} for customized pricing. esperto di dominio (to interfaccia con l'utente ed esecutore di codice): ***** Suggested tool call (call_buufxhap): cerca ***** Arguments: {"informazione":"prezzo","titolo":"Stiamo davvero scherzando"} ****************************************************** -------------------------------------------------------------------------------- >>>>>>>> EXECUTING FUNCTION cerca... interfaccia con l'utente ed esecutore di codice (to esperto di dominio): interfaccia con l'utente ed esecutore di codice (to esperto di dominio): ***** Response from calling tool (call_buufxhap) ***** 15 € ****************************************************** -------------------------------------------------------------------------------- [autogen.oai.client: 09-23 14:00:54] {349} WARNING - Model llama3.1:8b is not found. The cost will be 0. In your config_list, add field {"price" : [prompt_price_per_1k, completion_token_price_per_1k]} for customized pricing. esperto di dominio (to interfaccia con l'utente ed esecutore di codice): CONCLUSO. --------------------------------------------------------------------------------
E questa è la ricostruzione della conversazione tra i due agenti.
from pprint import pprint
pprint(res.chat_history)
[{'content': "Qual è il prezzo del libro 'Stiamo davvero scherzando'?", 'name': "interfaccia con l'utente ed esecutore di codice", 'role': 'assistant'}, {'content': '', 'role': 'assistant', 'tool_calls': [{'function': {'arguments': '{"informazione":"prezzo","titolo":"Stiamo ' 'davvero scherzando"}', 'name': 'cerca'}, 'id': 'call_buufxhap', 'type': 'function'}]}, {'content': '15 €', 'name': "interfaccia con l'utente ed esecutore di codice", 'role': 'tool', 'tool_responses': [{'content': '15 €', 'role': 'tool', 'tool_call_id': 'call_buufxhap'}]}, {'content': 'CONCLUSO.', 'name': 'esperto di dominio', 'role': 'user'}]
È un semplice ma già interessante esempio (modello linguistico di piccole dimensioni, esecuzione locale, in italiano...) di un mix tra un "Sistema 1" (il modello linguistico stesso) e un "Sistema 2" (la funzione Python).