Ceci est une implémentation complète de l'exemple de l'article Comment un agent utilise les outils (Bases).
Si tu n'as pas encore lu l'article, commence par lui. Ici, le focus est uniquement sur le code.
Ce que cet exemple démontre
- Comment LLM décide quand il faut appeler un outil
- Comment le système vérifie si l'outil est autorisé (allowlist)
- Comment exécuter un appel d'outil et renvoyer le résultat au modèle
- Comment l'agent termine la boucle quand les données sont suffisantes
Structure du projet
examples/
└── foundations/
└── tool-calling-basics/
└── python/
├── main.py # agent loop
├── llm.py # model call + tool definitions
├── executor.py # allowlist check + tool execution
├── tools.py # tools (business logic)
└── requirements.txt
Cette séparation est pratique en projet réel : modèle, policy et exécution des outils ne sont pas mélangés dans un seul fichier.
Comment lancer
1. Clone le dépôt et va dans le dossier :
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd examples/foundations/tool-calling-basics/python
2. Installe les dépendances :
pip install -r requirements.txt
3. Définis la clé API :
export OPENAI_API_KEY="sk-..."
4. Lance l'exemple :
python main.py
⚠️ Si tu oublies de définir la clé, l'agent te le dira immédiatement avec une indication sur quoi faire.
Code
tools.py — outils réellement exécutés
from typing import Any
USERS = {
42: {"id": 42, "name": "Anna", "tier": "pro"},
7: {"id": 7, "name": "Max", "tier": "free"},
}
BALANCES = {
42: {"currency": "USD", "value": 128.40},
7: {"currency": "USD", "value": 0.0},
}
def get_user_profile(user_id: int) -> dict[str, Any]:
user = USERS.get(user_id)
if not user:
return {"error": f"user {user_id} not found"}
return {"user": user}
def get_user_balance(user_id: int) -> dict[str, Any]:
balance = BALANCES.get(user_id)
if not balance:
return {"error": f"balance for user {user_id} not found"}
return {"balance": balance}
Le modèle n'a pas d'accès direct aux dictionnaires. Il peut seulement demander à appeler ces fonctions.
executor.py — frontière de sécurité et exécution
import json
from typing import Any
from tools import get_user_balance, get_user_profile
TOOL_REGISTRY = {
"get_user_profile": get_user_profile,
"get_user_balance": get_user_balance,
}
ALLOWED_TOOLS = {"get_user_profile", "get_user_balance"}
def execute_tool_call(tool_name: str, arguments_json: str) -> dict[str, Any]:
if tool_name not in ALLOWED_TOOLS:
return {"error": f"tool '{tool_name}' is not allowed"}
tool = TOOL_REGISTRY.get(tool_name)
if tool is None:
return {"error": f"tool '{tool_name}' not found"}
try:
args = json.loads(arguments_json or "{}")
except json.JSONDecodeError:
return {"error": "invalid JSON arguments"}
try:
result = tool(**args)
except TypeError as exc:
return {"error": f"invalid arguments: {exc}"}
return {"tool": tool_name, "result": result}
Idée clé ici : même si le modèle demande quelque chose d'étrange, le système exécute uniquement ce qui est explicitement autorisé.
llm.py — appel modèle et description des outils disponibles
import os
from openai import OpenAI
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise EnvironmentError(
"OPENAI_API_KEY is not set.\n"
"Run: export OPENAI_API_KEY='sk-...'"
)
client = OpenAI(api_key=api_key)
SYSTEM_PROMPT = """
You are an AI support agent. When you need data, call the available tools.
Once you have enough information, give a short answer.
""".strip()
TOOLS = [
{
"type": "function",
"function": {
"name": "get_user_profile",
"description": "Returns user profile by user_id",
"parameters": {
"type": "object",
"properties": {"user_id": {"type": "integer"}},
"required": ["user_id"],
},
},
},
{
"type": "function",
"function": {
"name": "get_user_balance",
"description": "Returns user balance by user_id",
"parameters": {
"type": "object",
"properties": {"user_id": {"type": "integer"}},
"required": ["user_id"],
},
},
},
]
def ask_model(messages: list[dict]):
completion = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role": "system", "content": SYSTEM_PROMPT}, *messages],
tools=TOOLS,
tool_choice="auto",
)
return completion.choices[0].message
Les outils sont décrits ici en schema. Le modèle voit cette liste et choisit dedans.
main.py — boucle d'agent (model → tool → model)
import json
from executor import execute_tool_call
from llm import ask_model
MAX_STEPS = 6
TASK = "Prepare a short account summary for user_id=42: name, tier, and balance."
def to_assistant_message(message) -> dict:
tool_calls = []
for tc in message.tool_calls or []:
tool_calls.append(
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
)
return {
"role": "assistant",
"content": message.content or "",
"tool_calls": tool_calls,
}
def run():
messages: list[dict] = [{"role": "user", "content": TASK}]
for step in range(1, MAX_STEPS + 1):
print(f"\n=== STEP {step} ===")
assistant = ask_model(messages)
messages.append(to_assistant_message(assistant))
text = assistant.content or ""
if text.strip():
print(f"Assistant: {text.strip()}")
tool_calls = assistant.tool_calls or []
if not tool_calls:
print("\nDone: model finished without a new tool call.")
return
for tc in tool_calls:
print(f"Tool call: {tc.function.name}({tc.function.arguments})")
execution = execute_tool_call(
tool_name=tc.function.name,
arguments_json=tc.function.arguments,
)
print(f"Tool result: {execution}")
messages.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(execution, ensure_ascii=False),
}
)
print("\nStop: MAX_STEPS reached.")
if __name__ == "__main__":
run()
C'est la boucle classique : le modèle demande un outil, le système exécute, le résultat revient au modèle, puis la décision suivante.
requirements.txt
openai>=1.0.0
Exemple de sortie
=== STEP 1 ===
Tool call: get_user_profile({"user_id": 42})
Tool result: {'tool': 'get_user_profile', 'result': {'user': {'id': 42, 'name': 'Anna', 'tier': 'pro'}}}
=== STEP 2 ===
Tool call: get_user_balance({"user_id": 42})
Tool result: {'tool': 'get_user_balance', 'result': {'balance': {'currency': 'USD', 'value': 128.4}}}
=== STEP 3 ===
Assistant: User Anna is a pro tier member with a current balance of 128.4 USD.
Done: model finished without a new tool call.
Pourquoi c'est un agent, et pas un seul model call
| Un model call | Agent avec tools | |
|---|---|---|
| Travaille uniquement avec le texte déjà disponible | ✅ | ❌ |
| Peut demander de nouvelles données via un tool | ❌ | ✅ |
| A une frontière d'exécution explicite (allowlist) | ❌ | ✅ |
| Fait plusieurs étapes vers une réponse finale | ❌ | ✅ |
Ce qu'il faut modifier dans cet exemple
- Bloque
get_user_balancedansALLOWED_TOOLS— que renverra l'agent à l'utilisateur ? - Remplace
user_id=42paruser_id=999— comment l'agent traitera-t-il l'erreur ? - Ajoute un outil
get_user_ordersmais ne l'ajoute pas dansALLOWED_TOOLS— le modèle va-t-il essayer de l'appeler ? - Ajoute une limite du nombre de tool calls séparément de
MAX_STEPS
Code complet sur GitHub
Le dépôt contient la version complète de cette démo : avec ALLOWED_TOOLS, boucle d'étapes et gestion des erreurs.
Si tu veux la lancer rapidement ou parcourir le code ligne par ligne, ouvre l'exemple complet.