Part 4 of “My Journey in Building Agents from Scratch”
Introduction
If you want to scale your AI projects, automating agent creation with templates is a game changer. After building my first few agents, I ran into a familiar developer headache: boilerplate. Creating a new agent meant creating a whole new Python class. While each agent was unique, its core structure was nearly identical to the others. I found myself repeatedly defining the same properties: the list of tools, the lengthy system prompt, the model client object… it was a mountain of copy-paste.
The tipping point came when I was setting up a third agent and realized I was spending more time duplicating code than writing new logic. That’s when I had my “enough is enough” moment. I thought, “Why can’t this be automated from a simple input file?” Why should agent logic be so tightly coupled to the code? Developers should write Python functions for tools, but everything else—prompts, model config, tool selection—should live outside the code. Enter automating agent creation with templates and YAML configs. Suddenly, building agents felt like snapping together LEGO blocks.
- Introduction
- Why Automating Agent Creation with Templates?
- What Goes Into an Agent Template?
- Agent template config example:
- MCP server template config example:
- Local vs. Shared Tools: Integrating the Model Context Protocol (MCP)
- Example `config.yaml` using MCP:
- How I Use Agent Templates in Practice
- The Agent Loader: Bringing It All Together
- From Agent Template to API
- Key Takeaways: Automating Agent Creation with Templates
- Try It Yourself: Automating Agent Creation with Templates
Why Automating Agent Creation with Templates?
Automating agent creation with templates solves the problem of repeated setup. By standardizing agent and MCP server creation, I could:
- Ensure consistency across projects
- Reduce setup time (from hours to minutes)
- Make maintenance easier (no more hunting for tool bugs)
– Share best practices with teammates (even those new to agents)
In Post 3, I described moving from manual tool schemas to Python-first utilities and centralized tools with Model Context Protocol (MCP). Automating agent creation with templates is the next logical step: package all that hard-won knowledge and reuse it everywhere.
What Goes Into an Agent Template?
Automating agent creation with templates means your templates are structured, explicit, and easy to read. For both agents and MCP servers, the YAML config includes:
- name: The name of the agent or server
- description: What it does (for docs, UI, or debugging)
- model_client (for agents): Endpoint, model name, and env key for authentication
- tools: A list of tool definitions, each with:
- file_path: Path to the Python file with tool functions
- functions: List of function names to expose
- prompt (for agents): The system prompt or path to a prompt file
Agent template config example:
name: "MathAgent"
description: "An agent that performs math operations using tools."
model_client:
endpoint: "https://api.openai.com/v1"
model: "gpt-4.1"
env_key: "OPENAI_API_KEY"
tools:
- file_path: "./tools/math.py"
functions: ["add", "sub", "mul"]
prompt:
system_message_file: "./prompts/system.txt"
MCP server template config example:
name: "MathMCPServer"
description: "MCP server exposing math tools."
tools:
- file_path: "./tools/math.py"
functions: ["add", "sub", "mul"]
This parallel structure means you can reuse the same tools and config patterns for both agents and MCP servers. It’s simple, explicit, and keeps everything DRY. I intentionally started with this straightforward structure because it covers the core needs and provides a solid foundation to build on. This is where I began, and it’s proven easy to add more complex features over time.
Local vs. Shared Tools: Integrating the Model Context Protocol (MCP)
This template system is flexible enough to handle both scenarios we’ve discussed: agent-specific tools and shared tools hosted on a Model Context Protocol (MCP) server. The rule of thumb is simple:
- Use local tools (via `file_path`) for functions that are unique to a specific agent.
- Use an MCP endpoint for tools that are shared and maintained centrally for many agents.
To use shared tools, the `tools` section in the `config.yaml` would simply change to point to the MCP server’s URL.
Example `config.yaml` using MCP:
name: "ReportingAgent"
description: "An agent that uses centralized tools to build reports."
model_client:
endpoint: "https://api.openai.com/v1"
model: "gpt-4.1"
env_key: "OPENAI_API_KEY"
tools:
- mcp_server:
url: "http://mcp.internal-service.com/tools"
# Optionally, specify which functions to use from the MCP
functions: ["generate_sales_report", "get_user_analytics"]
prompt:
system_message_file: "./prompts/reporting_prompt.txt"
This hybrid approach means the template accommodates everything from quick, single-purpose agents to complex agents participating in a larger ecosystem.
How I Use Agent Templates in Practice
My journey started with writing agent classes that bundled everything together: prompt, tool calling logic, and model client. For each new use case, I’d create a new class—like `CalculatorAgent` for math tools (see Post 2). But every time requirements changed, I had to touch the Python files. Not scalable.
My mind went to my past experience using `.ini` files to configure applications; I knew separating configuration from code was a powerful concept. So, I started by splitting everything into separate files: prompts, tools, and model client config. But even then, changing the model or tools still meant code changes. That’s when I had the real breakthrough: why not define *everything* in a single, structured YAML file?
Now, automating agent creation with templates means I just pass a different YAML file—no code changes required. The agent class reads the config, loads the model client, prompt, and tools automatically. This made my agents fully generic and dramatically sped up development.
The Agent Loader: Bringing It All Together
The magic of automating agent creation with templates happens in the agent loader. This script reads the YAML config, loads the prompt, imports the tools, and initializes the model client. Suddenly, creating a new agent is as easy as editing a config file and running one command.
Here’s what a typical agent template folder looks like with the loader:
agent-template/
├── config.yaml # All agent config (model, prompt, tools)
├── prompts/
│ └── system.txt # System prompt text
├── tools/
│ └── math.py # Python functions for tools
├── agent_loader.py # Generic agent loader
├── requirements.txt # Dependencies
└── README.md # How to use the template
# agent_loader.py
import yaml
import importlib.util
def load_tools(file_path, tool_names):
spec = importlib.util.spec_from_file_location("tools_module", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
tools = {}
for name in tool_names:
func = getattr(module, name)
tools[name] = func
return tools
def load_agent(config_path):
with open(config_path) as f:
config = yaml.safe_load(f)
with open(config['prompt']['system_message_file']) as pf:
system_message = pf.read()
tool_cfg = config['tools'][0]
# This is a simplified loader; a real one would handle MCP tools, etc.
tools = load_tools(tool_cfg['file_path'], tool_cfg['functions'])
model_client = config['model_client']
return {
'system_message': system_message,
'tools': tools,
'model_client': model_client
}
From Agent Template to API
The template system was a huge internal success. It made our team faster and more consistent. But the real “aha!” moment came when a colleague working on a TypeScript web app needed to integrate one of our agents. They couldn’t use our Python code directly, and rewriting all the tool-calling logic, prompts, and orchestration in TypeScript would have been a massive undertaking.
This is where the true power of automating agent creation with templates became clear. Because our agent’s entire definition—its prompt, tools, and model—is encapsulated in a portable, self-contained folder, we could create a single, generic API server that could load and serve any agent on demand.
Instead of the web developer rebuilding the agent, they could simply call it.
Why FastAPI for Agent APIs?
For the API server, I chose FastAPI. It’s a modern, high-performance Python web framework that’s incredibly easy to use. Its key benefits for this use case are:
- Automatic Docs: It creates interactive API documentation (like Swagger UI) automatically, which is a huge help for consumers of your API.
- Data Validation: It uses Pydantic for data validation, ensuring that requests coming into your API have the correct structure.
- Async Support: It’s built for asynchronous programming, which is perfect for handling I/O-bound operations like waiting for a response from a language model.
The Generic Agent Server: Deploying Agent Templates as APIs
The goal is to create one server that doesn’t need to be changed when you add, remove, or modify agents. The server’s only job is to find an agent’s template folder, use our `agent_loader.py` to load it into memory, and pass on the user’s request.
I created an `api_server.py` file to house this logic. Here is the updated template structure:
agent-project/
├── api_server.py # Our new generic API server
├── agent_loader.py # The loader from before
├── requirements.txt # Now includes fastapi, uvicorn
└── agents/
└── MathAgent/
├── config.yaml
├── prompts/
│ └── system.txt
└── tools/
└── math.py
#api_server.py
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
import os
from agent_loader import load_agent
app = FastAPI()
class ChatRequest(BaseModel):
message: str
# You could add session_id, etc. here
# In a real app, you'd have a more robust agent execution loop.
# This is a simplified example.
def run_agent_once(agent, message):
# Here you would implement the logic to call the model with the message
# and tools, handle tool calls, and return the final response.
print(f"Running agent with message: {message}")
print(f"System Prompt: {agent.get('system_message')}")
return f"Agent executed with message: '{message}'. Tools available: {list(agent['tools'].keys())}"
@app.post("/agents/{agent_name}/chat")
def chat_with_agent(agent_name: str, request: ChatRequest):
# Note: In a real app, validate agent_name to prevent path traversal issues.
config_path = os.path.join("agents", agent_name, "config.yaml")
if not os.path.exists(config_path):
return {"error": f"Agent '{agent_name}' not found."}
try:
agent = load_agent(config_path)
response = run_agent_once(agent, request.message)
return {"response": response}
except Exception as e:
return {"error": f"Failed to load or run agent: {str(e)}"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Running and Using the API
With this server, deploying any agent is now a two-step process:
- Create the agent’s template folder inside the `agents/` directory.
- Run the API server.
To run the server:
uvicorn api_server:app --reload
Now, our TypeScript developer can interact with the `MathAgent` by calling the API:
curl -X POST "http://127.0.0.1:8000/agents/MathAgent/chat" \
-H "Content-Type: application/json" \
-d '{"message": "What is 2 plus 2?"}'
If we create a new `CodeWriterAgent`, we just create the folder and `config.yaml`, and it’s instantly available at `/agents/CodeWriterAgent/chat`. No changes to the API server needed.
Key Takeaways: Automating Agent Creation with Templates
This journey transformed how I build agents. By automating agent creation with templates and combining a template-driven design with a generic API server, we achieved a powerful workflow:
- Templates Make Agent Creation Declarative: Instead of writing Python classes, we declare an agent’s properties in a YAML file. This makes creation faster, less error-prone, and accessible to non-developers.
- Separation of Concerns is Crucial: The agent’s logic (Python tool code), its definition (YAML config), and its instruction (prompt file) are kept separate, making maintenance a breeze.
- An API Makes Agents Reusable Everywhere: Wrapping the agent loader in an API means any application, in any language, can leverage your agents. This breaks down silos and prevents duplicated work.
- The `config.yaml` is the Single Source of Truth: To create, modify, or deploy an agent, you only need to touch its template folder.
Try It Yourself: Automating Agent Creation with Templates
1. Create a New Agent Template: Take the template folder and create a `JokeAgent`. Write a Python tool `get_joke()` that returns a hardcoded joke. Write a prompt and `config.yaml` and test it with the API.
2. Add Configuration to a Tool: Modify the `JokeAgent` tool to take a `category` (e.g., “dad jokes”, “programming jokes”). Add a `configuration` block to your `config.yaml` to specify the default category, and update your `agent_loader.py` to pass this to the tool.
3. Make the API an Agent Tool: Create a *new* agent, the `AgentOrchestrator`, that has a tool called `call_another_agent(agent_name: str, message: str)`. This tool would use `requests` to call your own API server. This is the first step towards multi-agent systems!
Previous: Tools and MCP
