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.
No description has been provided for this image

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à: computingagents.ipynb
    • aprire il notebook computingagents.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
    dove xxx è 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).

In [3]:
import autogen
import math
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 (chatbot).

In [5]:
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 devi calcolare una radice quadrata, usa la funzione disponibile. Scrivi CONCLUSO quando hai concluso il lavoro.",
    llm_config=llm_config,
)

Questa è la funzione Python che dovrebbe essere eseguita quando richiesto, resa disponibile all'agente esperto di dominio grazie ai decoratori.

In [6]:
@user_proxy.register_for_execution()
@domain_expert.register_for_llm(description="Calcolatore di radici quadrate.")
def square_root_calculator(
        number: Annotated[float, "Numero di cui calcolare la radice quadrata"],
    ) -> str:
    number = float(number)      # a volte potrebbe essere una stringa...
    if number >= 0:
        return f"La radice quadrata di {number} è {math.sqrt(number):.4f}"
    raise ValueError(f"Non sono capace di calcolare la radice quadrata del numero negativo {number}")

Verifichiamo che, grazie ai decoratori, l'agente esperto di dominio sia stato dotato dello schema json con la dichiarazione della funzione.

In [6]:
domain_expert.llm_config["tools"]
Out[6]:
[{'type': 'function',
  'function': {'description': 'Calcolatore di radici quadrate.',
   'name': 'square_root_calculator',
   'parameters': {'type': 'object',
    'properties': {'number': {'type': 'number',
      'description': 'Numero di cui calcolare la radice quadrata'}},
    'required': ['number']}}}]

Ecco dunque un esempio di uso di questa architettura.

In [7]:
domain_expert.reset()
res = user_proxy.initiate_chat(domain_expert, message="Qual è la radice quadrata di 1234.56789?")
interfaccia con l'utente ed esecutore di codice (to esperto di dominio):

Qual è la radice quadrata di 1234.56789?

--------------------------------------------------------------------------------
Qual è la radice quadrata di 1234.56789?

--------------------------------------------------------------------------------
[autogen.oai.client: 09-23 13:23:30] {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_08we5q1i): square_root_calculator *****
Arguments: 
{"number":1234.56789}
***********************************************************************

--------------------------------------------------------------------------------

>>>>>>>> EXECUTING FUNCTION square_root_calculator...
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_08we5q1i) *****
La radice quadrata di 1234.56789 è 35.1364
******************************************************

--------------------------------------------------------------------------------
[autogen.oai.client: 09-23 13:23:33] {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.

In [8]:
from pprint import pprint
pprint(res.chat_history)
[{'content': 'Qual è la radice quadrata di 1234.56789?',
  'name': "interfaccia con l'utente ed esecutore di codice",
  'role': 'assistant'},
 {'content': '',
  'role': 'assistant',
  'tool_calls': [{'function': {'arguments': '{"number":1234.56789}',
                               'name': 'square_root_calculator'},
                  'id': 'call_08we5q1i',
                  'type': 'function'}]},
 {'content': 'La radice quadrata di 1234.56789 è 35.1364',
  'name': "interfaccia con l'utente ed esecutore di codice",
  'role': 'tool',
  'tool_responses': [{'content': 'La radice quadrata di 1234.56789 è 35.1364',
                      'role': 'tool',
                      'tool_call_id': 'call_08we5q1i'}]},
 {'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).