Esta es una implementación completa del ejemplo del artículo Cómo un agente usa herramientas (Bases).
Si todavía no leíste el artículo, empieza por ahí. Aquí el foco está solo en el código.
Qué demuestra este ejemplo
- Cómo LLM decide cuándo hay que llamar una herramienta
- Cómo el sistema verifica si la herramienta está permitida (allowlist)
- Cómo ejecutar una invocación de herramienta y devolver el resultado al modelo
- Cómo el agente termina el bucle cuando los datos son suficientes
Estructura del proyecto
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
Esta separación es práctica en un proyecto real: modelo, policy y ejecución de herramientas no están mezclados en un solo archivo.
Cómo ejecutar
1. Clona el repositorio y entra en la carpeta:
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd examples/foundations/tool-calling-basics/python
2. Instala dependencias:
pip install -r requirements.txt
3. Define la API key:
export OPENAI_API_KEY="sk-..."
4. Ejecuta el ejemplo:
python main.py
⚠️ Si te olvidas de definir la clave, el agente te lo dirá al instante con una pista de qué hacer.
Código
tools.py — herramientas que realmente se ejecutan
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}
El modelo no tiene acceso directo a los diccionarios. Solo puede pedir llamar estas funciones.
executor.py — límite de seguridad y ejecución
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}
La idea principal aquí: aunque el modelo pida algo raro, el sistema ejecuta solo lo que está explícitamente permitido.
llm.py — llamada al modelo y descripción de herramientas 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
Las herramientas aquí están descritas como schema. El modelo ve esta lista y elige de ahí.
main.py — bucle de agente (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()
Este es el bucle clásico: el modelo pide herramienta, el sistema ejecuta, el resultado vuelve al modelo, luego viene la siguiente decisión.
requirements.txt
openai>=1.0.0
Ejemplo de salida
=== 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.
Por qué esto es un agente y no un solo model call
| Un model call | Agente con tools | |
|---|---|---|
| Trabaja solo con texto ya disponible | ✅ | ❌ |
| Puede pedir datos nuevos vía tool | ❌ | ✅ |
| Tiene un límite explícito de ejecución (allowlist) | ❌ | ✅ |
| Hace varios pasos hasta la respuesta final | ❌ | ✅ |
Qué cambiar en este ejemplo
- Bloquea
get_user_balanceenALLOWED_TOOLS— ¿qué devolverá el agente al usuario? - Cambia
user_id=42poruser_id=999— ¿cómo manejará el agente el error? - Agrega una herramienta
get_user_orderspero no la agregues enALLOWED_TOOLS— ¿intentará el modelo llamarla? - Agrega un límite para la cantidad de tool calls separado de
MAX_STEPS
Código completo en GitHub
En el repositorio está la versión completa de esta demo: con ALLOWED_TOOLS, bucle de pasos y manejo de errores.
Si quieres ejecutarlo rápido o revisar el código línea por línea, abre el ejemplo completo.