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.