Agents

Subpage of Building Agents

SWE Principles for Building Out Agents

An agent is more than a workflow; it is an autonomous system that uses an LLM to decide which tools to use and how to handle unexpected results. In Python, the ecosystem is rapidly maturing, moving from experimental scripts to production-grade frameworks.

Choosing Your Framework

For a seasoned engineer, the choice of framework depends on the required balance between Autonomy and Control.

FrameworkMental ModelWhen to Use
PydanticAIType-Safe FunctionsUse this for Production. It feels like FastAPI for agents. It uses Pydantic for strict validation, making it highly reliable and easy to debug.
CrewAIRole-Based AgentsUse this for Business Logic. If you need an “Accountant Agent” and a “Manager Agent” to collaborate, CrewAI’s role-playing abstraction is powerful.
LangGraphCyclic GraphsUse this for Custom Architectures. If you need to build a bespoke reasoning loop that doesn’t fit the standard ReAct pattern.
AutoGenConversational AgentsUse this for Creative Problem Solving. Ideal for multi-agent brainstorming or coding tasks.

The Tooling Revolution: Model Context Protocol (MCP)

In the past, giving an agent a tool meant writing a custom wrapper for every API. The Model Context Protocol (MCP) changes this. It is a universal standard that allows agents to connect to tools and data sources seamlessly.

Why MCP?

An API allows you to get data. MCP allows you to provide context. * FastMCP: A framework that allows you to turn any Python function into an MCP-compatible tool in minutes. * Motivation: APIs are often too rigid for LLMs. MCP servers can expose not just functions, but also local files, database schemas, and even browser sessions in a way that the LLM can “understand” and navigate.

Production Considerations for Agents

State Management

State is the “memory” of your agent. In production, you must handle: * Persistence: Saving the agent’s state to a database (Postgres/Redis) so it can resume after a crash. * Context Pruning: Agents can “forget” the original goal if the conversation gets too long. You must implement strategies to summarize old turns or “slide” the window of active context.

Failure Handling: The “Retry” isn’t enough

In agentic engineering, a “failure” might be the LLM calling a tool with the wrong arguments. * Self-Correction: Catch the error, pass it back to the LLM as a “Thought,” and let it try again with a different argument. * Fallback Models: If a cheap model (GPT-4o-mini) fails the reasoning task, automatically “escalate” the task to a more capable model (Claude 3.5 Sonnet).

Evaluation & Observability

You cannot “unit test” an agent’s path. You must use Traces. * LangSmith / Langfuse: These tools allow you to visualize the “Trajectory” of the agent. You can see exactly why an agent chose Tool A over Tool B. * Evals: Create a dataset of “Golden Trajectories”—expected sequences of tool calls for specific prompts—and run them against new versions of your agent to check for regressions.

Security: The “Sandboxing” Mandate

Never give an agent direct access to your host shell. 1. Code Interpreters: Use tools like E2B to run agent-generated code in a secure, isolated sandbox. 2. Least Privilege: Scoped API keys are mandatory. If an agent only needs to read a Google Sheet, do not give it access to the entire Google Drive.

Cost / Latency Tradeoffs

Agents are expensive. A single user request might trigger 10 LLM calls. * Parallelism: If the LLM determines it needs to call three tools, use a framework that supports parallel tool execution. * Prefetching: Use “Cheap” models for classification and routing, and “Expensive” models only for the final reasoning or tool argument generation.

Engineering Heuristics for Performance

  1. Prompt as Code: Treat your system prompts like code. Use version control. A one-word change can break an agent’s tool-calling capability.
  2. Explicit Tool Descriptions: The LLM’s “vision” of a tool is its description. Be verbose. Instead of get_data, use fetches_user_billing_history_as_json_for_the_last_30_days.
  3. Always Validate: If a tool returns data, validate it with Pydantic before the agent sees it. Garbage In, Garbage Out still applies to AI.

tep-by-Step Implementation: Pydantic AI + FastMCP

This section provides a concrete roadmap to building a production-ready agent that consumes external tools via MCP.

Precise Setup Steps

  1. Environment: Create a virtual environment and install dependencies: bash pip install pydantic-ai[mcp] mcp python-dotenv
  2. Build the Tool Server (FastMCP): Create a separate file server.py to host your tools. This isolates the “Execution Layer” from the “Intelligence Layer.”
  3. Define the Agent: Create agent_app.py, initialize the Agent with Pydantic validation for structured outputs.
  4. Connect via Stdio: Use MCPServerStdio to allow the agent to launch and communicate with the tool server.
  5. Inject Dependencies: Use Pydantic AI’s RunContext to pass stateful objects (like database connections) into tools.

Code Implementation

Part A: The MCP Tool Server (server.py)

This follows the Least Privilege principle by only exposing specific functions.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("InventoryManager")

@mcp.tool()
async def check_stock(item_id: str) -> int:
    """Check the current stock level for a specific item ID."""
    # Engineering Note: This is where you connect to your real DB
    inventory_db = {"A101": 50, "B202": 0}
    return inventory_db.get(item_id, 0)

if __name__ == "__main__":
    mcp.run()

Part B: The Pydantic AI Agent (agent_app.py)

This demonstrates State Management, Type Safety, and Observability.

import asyncio
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
from pydantic_ai.mcp import MCPServerStdio
from dotenv import load_dotenv

load_dotenv()

# 1. Structured Output Schema (Validation)
class PurchaseRecommendation(BaseModel):
    item_id: str
    should_buy: bool
    reasoning: str

# 2. Define the Agent with Intelligence and Toolsets
# The 'toolsets' parameter is where the MCP integration happens
mcp_server = MCPServerStdio("python", args=["server.py"])

agent = Agent(
    'openai:gpt-4o',
    result_type=PurchaseRecommendation,
    toolsets=[mcp_server],
    system_prompt="Analyze inventory levels and recommend if we should restock."
)

# 3. Execution Loop with Lifecycle Management
async def main():
    # Use 'async with' to handle the subprocess lifecycle of the MCP server
    async with mcp_server:
        result = await agent.run("Should we restock item A101?")

        # Accessing validated data
        print(f"Recommendation for{result.data.item_id}:")
        print(f"Should Buy:{result.data.should_buy}")
        print(f"Reason:{result.data.reasoning}")

if __name__ == "__main__":
    asyncio.run(main())

Factoring in Production Concepts

  • Stateful Dependencies: Use RunContext in Pydantic AI to pass a User object or a DatabaseSession to your local tools. This ensures the agent’s tools are grounded in the current State.
  • Tool Validation: By using Pydantic models for result_type, the agent is forced to “Reason” toward a structured schema. If it fails, Pydantic AI’s internal Failure Handling will automatically retry the prompt with the validation error.
  • Security: The FastMCP server runs in a separate process. You can further secure this by running the server in a Docker Sandbox while the agent remains in your primary application environment.
  • Cost/Latency: Notice that check_stock is a fast, local function. By using MCP, the agent only calls this tool when its “Intelligence” (GPT-4o) determines it is necessary, saving expensive tokens on unnecessary lookups.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.