Esta es la implementacion completa del ejemplo del articulo Como restringir el acceso a herramientas.
Si aun no leiste el articulo, empieza por ahi. Aqui el foco es el codigo: como funcionan exactamente las restricciones en runtime.
Que demuestra este ejemplo
- Nivel 1 (tool access): que herramientas puede invocar el agente en general
- Nivel 2 (action access): que acciones estan permitidas dentro de una herramienta accesible
- Policy gateway: verificacion centralizada de solicitudes antes de ejecutar
- Comportamiento de fallback: el agente recibe un error y elige el siguiente paso seguro
Estructura del proyecto
foundations/
└── tool-calling/
└── python/
├── main.py # agent loop
├── llm.py # model + tool schemas
├── gateway.py # policy checks + execution
├── tools.py # tools (system actions)
└── requirements.txt
Esta separacion es importante: el modelo propone una accion, y gateway.py decide si esa accion se ejecuta o no.
Como ejecutar
1. Clona el repositorio y entra a la carpeta:
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd foundations/tool-calling/python
2. Instala dependencias:
pip install -r requirements.txt
3. Configura la API key:
export OPENAI_API_KEY="sk-..."
4. Ejecuta:
python main.py
Que construimos en codigo
Construimos un robot prudente al que no se le permite hacer todo sin control.
- La IA puede pedir cualquier accion
- un "guardian" especial (
gateway) verifica si esta permitida - si esta prohibida, el robot no hace nada peligroso y explica una alternativa segura
Es como una puerta con cerradura: sin permiso, el comando no pasa.
Codigo
tools.py — herramientas reales
from typing import Any
CUSTOMERS = {
101: {"id": 101, "name": "Anna", "tier": "free", "email": "anna@gmail.com"},
202: {"id": 202, "name": "Max", "tier": "pro", "email": "max@company.local"},
}
def customer_db(action: str, customer_id: int, new_tier: str | None = None) -> dict[str, Any]:
customer = CUSTOMERS.get(customer_id)
if not customer:
return {"ok": False, "error": f"customer {customer_id} not found"}
if action == "read":
return {"ok": True, "customer": customer}
if action == "update_tier":
if not new_tier:
return {"ok": False, "error": "new_tier is required"}
customer["tier"] = new_tier
return {"ok": True, "customer": customer}
return {"ok": False, "error": f"unknown action '{action}'"}
def email_service(to: str, subject: str, body: str) -> dict[str, Any]:
return {
"ok": True,
"status": "queued",
"to": to,
"subject": subject,
"preview": body[:80],
}
Son funciones Python normales. El riesgo empieza cuando el agente puede invocarlas sin control.
gateway.py — policy gateway (capa clave)
import json
from typing import Any
from tools import customer_db, email_service
TOOL_REGISTRY = {
"customer_db": customer_db,
"email_service": email_service,
}
# Level 1: which tools are visible to the agent
ALLOWED_TOOLS = {"customer_db"}
# Level 2: which actions are allowed inside each tool
ALLOWED_ACTIONS = {
"customer_db": {"read"}, # update_tier is blocked
}
def execute_tool_call(tool_name: str, arguments_json: str) -> dict[str, Any]:
if tool_name not in ALLOWED_TOOLS:
return {"ok": False, "error": f"tool '{tool_name}' is not allowed"}
tool = TOOL_REGISTRY.get(tool_name)
if tool is None:
return {"ok": False, "error": f"tool '{tool_name}' not found"}
try:
args = json.loads(arguments_json or "{}")
except json.JSONDecodeError:
return {"ok": False, "error": "invalid JSON arguments"}
if tool_name == "customer_db":
action = args.get("action")
if action not in ALLOWED_ACTIONS["customer_db"]:
return {
"ok": False,
"error": f"action '{action}' is not allowed for tool '{tool_name}'",
}
try:
result = tool(**args)
except TypeError as exc:
return {"ok": False, "error": f"invalid arguments: {exc}"}
return {"ok": True, "tool": tool_name, "result": result}
Aqui es donde se enforcean las reglas. El modelo no puede saltarse esta capa via prompt.
llm.py — modelo y tool schemas
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 a support agent.
Use tools when data is missing.
If a tool or action is blocked, do not argue; suggest a safe manual next step.
Reply briefly in English.
""".strip()
TOOLS = [
{
"type": "function",
"function": {
"name": "customer_db",
"description": "Customer data operations: read or update tier",
"parameters": {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["read", "update_tier"]},
"customer_id": {"type": "integer"},
"new_tier": {"type": "string"},
},
"required": ["action", "customer_id"],
},
},
},
{
"type": "function",
"function": {
"name": "email_service",
"description": "Sends an email to the customer",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string"},
"subject": {"type": "string"},
"body": {"type": "string"},
},
"required": ["to", "subject", "body"],
},
},
},
]
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
Ojo: llm.py puede mostrarle mas herramientas al modelo, pero gateway.py igual bloquea las no permitidas.
main.py — ciclo del agente con verificacion de politicas
import json
from gateway import execute_tool_call
from llm import ask_model
MAX_STEPS = 6
TASK = (
"For customer_id=101, check the profile, upgrade tier to pro, "
"and send a confirmation email to anna@gmail.com. "
"If any action is blocked, explain the safe manual next step."
)
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))
if assistant.content and assistant.content.strip():
print("Assistant:", assistant.content.strip())
tool_calls = assistant.tool_calls or []
if not tool_calls:
print("\nDone: model finished the task.")
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("Gateway 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()
Aqui se ve bien el boundary: el modelo propone, gateway decide si la accion ocurre o no.
requirements.txt
openai>=1.0.0
Ejemplo de salida
=== STEP 1 ===
Tool call: customer_db({"action":"read","customer_id":101})
Gateway result: {'ok': True, 'tool': 'customer_db', 'result': {'ok': True, 'customer': {'id': 101, 'name': 'Anna', 'tier': 'free', 'email': 'anna@gmail.com'}}}
=== STEP 2 ===
Tool call: customer_db({"action":"update_tier","customer_id":101,"new_tier":"pro"})
Gateway result: {'ok': False, 'error': "action 'update_tier' is not allowed for tool 'customer_db'"}
=== STEP 3 ===
Tool call: email_service({"to":"anna@gmail.com","subject":"Tier updated","body":"..."})
Gateway result: {'ok': False, 'error': "tool 'email_service' is not allowed"}
=== STEP 4 ===
Assistant: I can only read the profile. Tier updates and email sending require manual operator action.
Done: model finished the task.
Nota: la cantidad de
STEPpuede variar entre ejecuciones.
En unSTEP, el modelo puede devolver variostool_calls, por eso a veces veras 2 pasos y a veces 4.
Esta es una no determinacion normal del LLM. Lo importante es que el policy gateway bloquea de forma estableupdate_tieryemail_service.
Por que este es un enfoque de produccion
| Tool calling ingenuo | Con policy gateway | |
|---|---|---|
| El modelo decide todo por si solo | ✅ | ❌ |
| Hay control de acceso centralizado | ❌ | ✅ |
| Se pueden separar acciones read/write | ❌ | ✅ |
| Los errores se convierten en fallback controlado | ❌ | ✅ |
Donde profundizar despues
- Agrega
ALLOWED_TOOLS_BY_ROLE(por ejemploviewer,operator,admin) - Agrega un approval-flow para
update_tieren lugar de bloqueo total - Agrega
max_tool_callsymax_costjunto aMAX_STEPS - Registra
tool_name,args_hash,decision,reasonpara auditoria
Codigo completo en GitHub
En el repositorio esta la version completa de esta demo: tool loop, verificaciones allowlist y fallback controlado.
Ver codigo completo en GitHub ↗