Introduction
When you check your API analytics, you might notice a new pattern: requests that don't come from a browser, a mobile app, or a traditional script. They come from AI agents—autonomous programs that plan, execute, and iterate. These agents are your newest users, and they interact with your API differently than humans or static clients.
This post explores the challenges of serving agentic clients and provides a concrete implementation of an agent-friendly API endpoint using Python and FastAPI. We'll build a simple task execution API that agents can discover, understand, and use safely.
The Problem
Traditional APIs are designed for deterministic clients. A mobile app knows exactly which endpoints to call, in what order, and with what parameters. Agents, however, operate in a loop: they observe the environment, decide on an action, execute it, and evaluate the result. This loop continues until a termination condition is met.
Agents need:
- Discoverability: They must find available actions without hardcoded knowledge.
- Structured responses: Natural language is ambiguous; agents need machine-readable output.
- Safety: Agents can inadvertently trigger dangerous operations. Your API must guard against misuse.
- Idempotency: Agents may retry actions; operations should be safe to repeat.
If your API isn't designed for these needs, agents will fail, hallucinate endpoints, or worse, cause damage.
Solution
We'll build an API that exposes a set of safe, well-defined actions. Each action has:
- A unique identifier
- A description
- A JSON Schema for input validation
- An execution function
- An idempotency key mechanism
Agents can query a /actions endpoint to discover available actions, then call /execute with the action name and parameters. The API validates input, executes the action, and returns a structured result.
We'll use FastAPI for its automatic OpenAPI generation and Pydantic for validation. The agent loop will be implemented as a separate client script that uses the API.
Implementation
1. Define the Action Registry
We'll create a registry of actions with metadata and handlers.
# actions.py
from typing import Callable, Dict, Any
from pydantic import BaseModel, ValidationError
class Action(BaseModel):
name: str
description: str
input_schema: dict
handler: Callable[[dict], dict]
# Example actions
def add_numbers(params: dict) -> dict:
return {"result": params["a"] + params["b"]}
def reverse_string(params: dict) -> dict:
return {"result": params["text"][::-1]}
ACTIONS: Dict[str, Action] = {
"add_numbers": Action(
name="add_numbers",
description="Add two integers.",
input_schema={
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
},
handler=add_numbers
),
"reverse_string": Action(
name="reverse_string",
description="Reverse a string.",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string"}
},
"required": ["text"]
},
handler=reverse_string
)
}
2. Build the FastAPI App
The API exposes two endpoints: /actions for discovery and /execute for execution. We'll also add idempotency support using a simple in-memory store (not for production).
# main.py
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
from typing import Optional, Dict, Any
from actions import ACTIONS
app = FastAPI(title="Agent-Friendly API")
# In-memory idempotency store (use Redis in production)
IDEMPOTENCY_STORE: Dict[str, dict] = {}
class ExecuteRequest(BaseModel):
action: str
params: dict
idempotency_key: Optional[str] = None
class ExecuteResponse(BaseModel):
success: bool
result: Optional[dict] = None
error: Optional[str] = None
@app.get("/actions")
def list_actions():
"""Return metadata for all available actions."""
return [
{
"name": action.name,
"description": action.description,
"input_schema": action.input_schema
}
for action in ACTIONS.values()
]
@app.post("/execute", response_model=ExecuteResponse)
def execute_action(
request: ExecuteRequest,
idempotency_key: Optional[str] = Header(None)
):
"""Execute a named action with given parameters."""
# Check idempotency
key = request.idempotency_key or idempotency_key
if key and key in IDEMPOTENCY_STORE:
return IDEMPOTENCY_STORE[key]
action = ACTIONS.get(request.action)
if not action:
raise HTTPException(status_code=404, detail="Action not found")
# Validate input against schema (simplified; use jsonschema in production)
try:
# In a real implementation, use jsonschema.validate
required = action.input_schema.get("required", [])
for field in required:
if field not in request.params:
raise ValueError(f"Missing required field: {field}")
except Exception as e:
return ExecuteResponse(success=False, error=str(e))
# Execute handler
try:
result = action.handler(request.params)
response = ExecuteResponse(success=True, result=result)
except Exception as e:
response = ExecuteResponse(success=False, error=str(e))
# Store idempotent response
if key:
IDEMPOTENCY_STORE[key] = response
return response
3. Implement an Agent Loop
The agent loop below uses the API to achieve a goal. It terminates when the goal is met or when a maximum number of steps is reached.
# agent.py
import requests
import json
class Agent:
def __init__(self, base_url: str, max_steps: int = 10):
self.base_url = base_url
self.max_steps = max_steps
def discover_actions(self):
resp = requests.get(f"{self.base_url}/actions")
resp.raise_for_status()
return resp.json()
def execute(self, action: str, params: dict, idempotency_key: str = None):
payload = {"action": action, "params": params}
if idempotency_key:
payload["idempotency_key"] = idempotency_key
resp = requests.post(f"{self.base_url}/execute", json=payload)
resp.raise_for_status()
return resp.json()
def run(self, goal: str):
actions = self.discover_actions()
print(f"Available actions: {[a['name'] for a in actions]}")
# Simple hardcoded plan for demonstration
# In reality, an LLM would generate the plan based on the goal
if goal == "add 2 and 3":
result = self.execute("add_numbers", {"a": 2, "b": 3}, idempotency_key="add-2-3")
print(f"Result: {result}")
return result
elif goal == "reverse hello":
result = self.execute("reverse_string", {"text": "hello"}, idempotency_key="rev-hello")
print(f"Result: {result}")
return result
else:
print("Goal not supported")
return None
if __name__ == "__main__":
agent = Agent("http://localhost:8000")
agent.run("add 2 and 3")
4. Run the System
Start the API:
uvicorn main:app --reload
Run the agent:
python agent.py
The agent discovers actions, executes the appropriate one, and prints the result. The idempotency key ensures that if the agent retries, it gets the same response without re-executing the action.
Key Takeaways
-
Agents need structured discovery: Expose an
/actionsendpoint with JSON Schema for inputs. - Validate and sanitize: Never trust agent input. Use schema validation and reject unknown fields.
- Idempotency is crucial: Agents may retry; idempotency keys prevent duplicate side effects.
- Define termination conditions: In your agent loop, set a maximum number of steps and clear success criteria.
-
Security first: Avoid dynamic code execution. If you must use
evalorexec, isolate it in a sandbox and warn users. Our example uses a fixed registry—no arbitrary code execution.
By designing APIs for machine consumers, you unlock a new class of applications. Agents will thank you—literally, in their logs.
Note: The code above is for demonstration. In production, use a persistent idempotency store, full JSON Schema validation (e.g., jsonschema library), and authentication.
Security Warning: Never expose an endpoint that executes arbitrary code from agents. The eval and exec functions in Python can run malicious code. Always use a predefined set of safe actions, validate inputs strictly, and run in a sandboxed environment if dynamic execution is unavoidable.
Top comments (0)