Il s'agit de la première partie d'une série en plusieurs parties sur la création d'agents avec l'API Assistant d'OpenAI à l'aide du SDK Python.
De la façon dont j'aime le voir, un agent n'est en réalité qu'un logiciel exploitant un LLM (Large Language Model) et essayant d'imiter le comportement humain. Cela signifie qu’il peut non seulement converser et comprendre le langage, mais également effectuer des actions qui ont un impact sur le monde réel. Ces actions sont généralement appelées outils.
Dans cet article de blog, nous explorerons comment créer un agent à l'aide de l'API Assistant d'OpenAI à l'aide de leur SDK Python. La première partie ne sera que le squelette de l'assistant. Autrement dit, juste la partie conversationnelle.
J'ai choisi de créer une application CLI volontairement pour être indépendante du framework. Nous appellerons délibérément notre implémentation un agent et ferons référence à l'implémentation du SDK OpenAI en tant qu'assistant pour distinguer facilement les deux.
J'utilise les termes outils et fonctions de manière interchangeable lorsqu'il s'agit de fonctions que l'agent peut appeler. La partie 2 couvrira l’appel de fonctions plus en détail.
Pour suivre ce tutoriel, vous aurez besoin des éléments suivants :
Assistant : Un assistant dans l'API Assistants est une entité configurée pour répondre aux messages des utilisateurs. Il utilise des instructions, un modèle choisi et des outils pour interagir avec les fonctions et apporter des réponses.
Thread : Un fil de discussion représente une conversation ou un dialogue dans l'API Assistants. Il est créé pour chaque interaction utilisateur et peut contenir plusieurs messages, servant de conteneur pour la conversation en cours.
Message : Un Message est une unité de communication dans un Thread. Il contient du texte (et potentiellement des fichiers à l'avenir) et est utilisé pour transmettre les requêtes des utilisateurs ou les réponses de l'assistant au sein d'un fil de discussion.
Run : Un Run est une instance de l'Assistant traitant un Thread. Cela implique de lire le fil de discussion, de décider d'appeler ou non des outils et de générer des réponses basées sur l'interprétation des messages du fil de discussion par le modèle.
La première étape consiste à créer un environnement virtuel à l'aide venv
et à l'activer. Cela garantira que nos dépendances sont isolées de l'installation du système Python :
python3 -m venv venv source venv/bin/activate
Installons notre seule dépendance : le package openai
:
pip install openai
Créez un fichier main.py
Remplissons avec une logique d'exécution de base pour notre application CLI :
while True: user_input = input("User: ") if user_input.lower() == 'exit': print("Exiting the assistant...") break print(f"Assistant: You said {user_input}")
Essayez-le en exécutant python3 main.py
:
python3 main.py User: hi Assistant: You said hi
Comme vous pouvez le voir, la CLI accepte un message utilisateur en entrée, et notre assistant génial n'a pas encore de cerveau 🧠, il répète donc simplement le message. Pas encore si intelligent.
Maintenant, le plaisir 😁 (ou les maux de tête 🤕) commence. Je vais fournir toutes les importations nécessaires pour le cours final dès maintenant, afin que vous ne vous demandiez pas d'où viennent les choses puisque j'ai conservé les importations en dehors des exemples de code par souci de concision. Commençons par construire une classe Agent
dans un nouveau fichier agent.py
:
import time import openai from openai.types.beta.threads.run import Run class Agent: def __init__(self, name: str, personality: str): self.name = name self.personality = personality self.client = openai.OpenAI(api_key="sk-*****") self.assistant = self.client.beta.assistants.create( name=self.name, model="gpt-4-turbo-preview" )
Dans le constructeur de classe, nous initialisons le client OpenAI en tant que propriété de classe en transmettant notre clé API OpenAI. Ensuite, nous créons une propriété de classe assistant
qui correspond à notre assistant nouvellement créé. Nous stockons name
et personality
en tant que propriétés de classe pour une utilisation ultérieure.
L'argument name
que nous transmettons à la méthode create sert uniquement à identifier l'assistant dans le tableau de bord OpenAI, et l'IA n'en est pas réellement consciente à ce stade. Il faut en fait transmettre le nom aux instructions
que nous verrons plus tard.
Vous pouvez déjà définir instructions
lors de la création de l'Assistant, mais cela rendra en réalité votre Assistant moins flexible face aux changements dynamiques.
Vous pouvez mettre à jour un assistant en appelant client.beta.assistants.update
, mais il existe un meilleur endroit pour transmettre des valeurs dynamiques que nous verrons lorsque nous arriverons aux exécutions.
Notez que si vous transmettez instructions
ici et encore lors de la création d'une exécution, les instructions
de l'assistant seront écrasées par les instructions
de l'exécution. Ils ne se complètent pas, alors choisissez-en un en fonction de vos besoins : niveau Assistant pour les instructions statiques ou niveau Run pour les instructions dynamiques.
Pour le modèle, j'ai choisi le modèle gpt-4-turbo-preview
afin que nous puissions ajouter des appels de fonctions dans la partie 2 de cette série. Vous pouvez utiliser gpt-3.5-turbo
si vous souhaitez économiser quelques fractions d'un centime tout en vous donnant une migraine de pure frustration lorsque nous implémentons des outils.
GPT 3.5 est très mauvais pour appeler des outils ; les heures que j'ai perdues à y faire face me permettent de le dire. 😝 Je vais en rester là, et j'en reparlerai plus tard.
Après avoir créé un agent, nous devrons démarrer un fil de conversation.
class Agent: # ... (rest of code) def create_thread(self): self.thread = self.client.beta.threads.create()
Et nous voudrons un moyen d'ajouter des messages à ce fil :
class Agent: # ... (rest of code) def add_message(self, message): self.client.beta.threads.messages.create( thread_id=self.thread.id, role="user", content=message )
Notez que pour le moment, il n'est possible d'ajouter des messages qu'avec le rôle user
. Je pense qu'OpenAI prévoit de changer cela dans une prochaine version car cela est assez limitatif.
Maintenant, nous pouvons récupérer le dernier message du fil :
class Agent: # ... (rest of code) def get_last_message(self): return self.client.beta.threads.messages.list( thread_id=self.thread.id ).data[0].content[0].text.value
Ensuite, nous créons une méthode run_agent
de point d’entrée pour tester ce que nous avons jusqu’à présent. Actuellement, la méthode run_agent
renvoie simplement le dernier message du fil de discussion. Il n'effectue pas réellement d'exécution. C'est toujours stupide.
class Agent: # ... (rest of code) def run_agent(self): message = self.get_last_message() return message
De retour dans main.py
, nous créons l'agent et notre premier thread. Nous ajoutons un message au fil. Renvoyez ensuite ce même message à l'utilisateur, mais cette fois, provenant de ce fil de discussion en direct.
from agent import Agent agent = Agent(name="Bilbo Baggins", personality="You are the accomplished and renowned adventurer from The Hobbit. You act like you are a bit of a homebody, but you are always up for an adventure. You worry a bit too much about breakfast.") agent.create_thread() while True: user_input = input("User: ") if user_input.lower() == 'exit': print("Exiting the agent...") break agent.add_message(user_input) answer = agent.run_agent() print(f"Assistant: {answer}")
Lançons-le :
python3 main.py User: hi Assistant: hi
Pas encore très intelligent. Plus proche d'un perroquet 🦜 que d'un hobbit. Dans la section suivante, le vrai plaisir commence.
Lorsque vous créez une exécution, vous devez récupérer périodiquement l'objet Run
pour vérifier l'état de l'exécution. C’est ce qu’on appelle un sondage, et c’est nul. Vous devez effectuer un sondage afin de déterminer ce que votre agent doit faire ensuite. OpenAI prévoit d'ajouter la prise en charge du streaming pour simplifier les choses. En attendant, je vais vous montrer comment configurer les sondages dans la section suivante.
Notez le _
sur les noms de méthodes suivants qui est la norme en Python pour indiquer que la méthode est destinée à un usage interne et ne doit pas être accessible directement par du code externe.
Tout d'abord, créons une méthode d'assistance _create_run
pour créer un Run
et mettons à jour run_agent
pour appeler cette méthode :
class Agent: # ... (rest of code) def get_breakfast_count_from_db(self): return 1 def _create_run(self): count = self.get_breakfast_count_from_db() return self.client.beta.threads.runs.create( thread_id=self.thread.id, assistant_id=self.assistant.id, instructions=f""" Your name is: {self.name} Your personality is: {self.personality} Metadata related to this conversation: {{ "breakfast_count": {count} }} """, ) def run_agent(self): run = self._create_run() # add this line message = self.get_last_message() return message
Remarquez comment nous transmettons thread.id
et assistant.id
pour créer une exécution.
Rappelez-vous comment j'ai dit au début qu'il y avait un meilleur endroit pour transmettre des instructions et des données dynamiques ? Ce serait le paramètre instructions
lors de la création du Run. Dans notre cas, nous pourrions récupérer le count
du petit-déjeuner à partir d’une base de données. Cela vous permettra de transmettre facilement différentes données dynamiques pertinentes à chaque fois que vous souhaitez déclencher une réponse.
Désormais, votre agent est conscient du monde qui évolue autour de lui et peut agir en conséquence. J'aime avoir un objet JSON de métadonnées dans mes instructions qui conserve un contexte dynamique pertinent. Cela me permet de transmettre des données tout en étant moins verbeux et dans un format que le LLM comprend très bien.
Ne lancez pas ceci encore ; cela ne fonctionnera pas car nous n'attendons pas la fin de l'exécution lorsque nous recevons le dernier message, ce sera donc toujours le dernier message utilisateur.
Résolvons ce problème en développant notre mécanisme de sondage. Tout d’abord, nous aurons besoin d’un moyen de récupérer facilement et de manière répétée une exécution, ajoutons donc une méthode _retrieve_run
:
class Agent: # ... (rest of code) def _retrieve_run(self, run: Run): return self.client.beta.threads.runs.retrieve( run_id=run.id, thread_id=self.thread.id)
Remarquez comment nous devons transmettre à la fois run.id
et thread.id
pour trouver une exécution spécifique.
Ajoutez une méthode _poll_run
à notre classe Agent :
class Agent: # ... (rest of code) def _cancel_run(self, run: Run): self.client.beta.threads.runs.cancel( run_id=run.id, thread_id=self.thread.id) def _poll_run(self, run: Run): status = run.status start_time = time.time() while status != "completed": if status == 'failed': raise Exception(f"Run failed with error: {run.last_error}") if status == 'expired': raise Exception("Run expired.") time.sleep(1) run = self._retrieve_run(run) status = run.status elapsed_time = time.time() - start_time if elapsed_time > 120: # 2 minutes self._cancel_run(run) raise Exception("Run took longer than 2 minutes.")
🥵 Ouf, ça fait beaucoup... Déballons ça.
_poll_run
reçoit un objet Run
comme argument et extrait le status
Run actuel. Tous les statuts disponibles peuvent être trouvés dans la documentation OpenAI. Nous n’en utiliserons que quelques-uns qui correspondent à notre objectif actuel.
Nous exécutons maintenant une boucle while pour vérifier un statut terminé tout en gérant quelques scénarios d'erreur. La facturation réelle de l'API Assistant est un peu trouble, donc par mesure de sécurité, j'ai choisi d'annuler mes exécutions après 2 minutes.
Même s'il existe un statut expired
lorsque OpenAI annule l'exécution après 10 minutes. Si une exécution dure plus de 2 minutes, vous avez probablement un problème de toute façon.
Comme je ne veux pas non plus interroger toutes les quelques millisecondes, je limite ma demande en interrogeant uniquement toutes les secondes jusqu'à ce que j'atteigne la barre des 2 minutes et que j'annule mon exécution. Vous pouvez ajuster cela comme bon vous semble.
À chaque itération après le délai, nous récupérons à nouveau le statut Run.
Maintenant, connectons tout cela à notre méthode run_agent
. Vous remarquerez que nous créons d'abord l'exécution avec _create_run
, puis nous interrogeons avec _poll_run
jusqu'à ce que nous obtenions une réponse ou qu'une erreur soit générée, et enfin lorsque l'interrogation est terminée, nous récupérons le dernier message du fil qui proviendra désormais de l'agent.
Nous renvoyons ensuite le message à notre boucle d'exécution, afin qu'il puisse être renvoyé à l'utilisateur.
class Agent: # ... (rest of code) def run_agent(self): run = self._create_run() self._poll_run(run) # add this line message = self.get_last_message() return message
Voilà, maintenant, lorsque vous exécuterez à nouveau votre agent, vous recevrez une réponse de notre sympathique agent :
python3 main.py User: hi Assistant: Hello there! What adventure can we embark on today? Or perhaps, before we set out, we should think about breakfast. Have you had yours yet? I've had mine, of course – can't start the day without a proper breakfast, you know. User: how many breakfasts have you had? Assistant: Ah, well, I've had just 1 breakfast today. But the day is still young, and there's always room for a second, isn't there? What about you? How can I assist you on this fine day?
Dans la partie 2, nous ajouterons la possibilité pour notre agent d'appeler des outils.
Vous pouvez trouver le code complet sur mon GitHub .
Merci pour votre lecture. Heureux d'entendre vos réflexions et vos commentaires dans les commentaires. Suivez-moi sur Linkedin pour plus de contenu comme celui-ci : https://www.linkedin.com/in/jean-marie-dalmasso-1b5473141/