pythonagentsminimal
Plain Python Agent with Tool Dispatch 2026
Skip LangChain and CrewAI for simple agents. Plain Python with a tool registry and LLM function calling. 50 lines, no framework lock-in.
8 min
Most AI agent projects do not need LangGraph, CrewAI, or any framework. A plain Python loop with explicit tool dispatch handles 80% of use cases: call the LLM with function definitions, check which tool it selected, execute it, append the result, repeat. When it breaks, you see exactly where.
The explicit dispatch pattern
Python
import os, json, requests
from anthropic import Anthropic
client = Anthropic()
SCAVIO_H = {"x-api-key": os.environ["SCAVIO_API_KEY"],
"Content-Type": "application/json"}
def web_search(query: str) -> list:
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers=SCAVIO_H,
json={"query": query, "country_code": "us"},
)
return resp.json().get("organic_results", [])[:5]
TOOLS = {"web_search": web_search}
TOOL_DEFS = [{
"name": "web_search",
"description": "Search the web for current information.",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
}]
def agent(task: str, max_steps: int = 8):
messages = [{"role": "user", "content": task}]
for step in range(max_steps):
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=TOOL_DEFS,
messages=messages,
)
# Check if the model wants to use a tool
tool_use = next(
(b for b in resp.content if b.type == "tool_use"), None)
if not tool_use:
return next(
b.text for b in resp.content if b.type == "text")
# Execute the tool
result = TOOLS[tool_use.name](**tool_use.input)
messages.append({"role": "assistant", "content": resp.content})
messages.append({
"role": "user",
"content": [{"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)}],
})
return "Max steps reached"
print(agent("What are the latest AI search API pricing changes?"))Why this beats a framework for simple agents
- Debugging is a stack trace, not a graph traversal
- Adding a new tool is one function + one schema dict
- No dependency on framework release cycles or breaking changes
- Error handling is explicit: try/except around each tool call
- Testing is straightforward: mock the LLM response, check the tool was called correctly
Adding error handling
Python
import time
def safe_tool_call(name: str, args: dict, retries: int = 2):
for attempt in range(retries + 1):
try:
result = TOOLS[name](**args)
return {"success": True, "data": result}
except requests.exceptions.Timeout:
if attempt < retries:
time.sleep(2 ** attempt)
continue
return {"success": False, "error": "Tool timed out after retries"}
except Exception as e:
return {"success": False, "error": str(e)}When to graduate to LangGraph
Move to LangGraph when you need: parallel tool execution with fan-out/fan-in, persistent state across sessions (checkpointing), branching logic where different tool results trigger different paths, or built-in human-in-the-loop patterns. For a single agent doing sequential tool calls, plain Python is cleaner.