The Builder Pattern for Agent Configuration: Fluent APIs for Complex Agent Setup
Use the Builder pattern to create fluent, validated, and immutable agent configurations — replacing sprawling constructors with readable step-by-step builder classes.
The Configuration Problem
AI agents often require complex configuration: model selection, temperature, system prompts, tool registrations, memory backends, retry policies, guardrails, and more. Passing all of these as constructor parameters creates unwieldy function signatures. Worse, it makes it easy to forget a required parameter or misconfigure optional ones.
The Builder pattern solves this by providing a step-by-step, fluent API for constructing complex objects. Each method sets one aspect of the configuration and returns the builder itself, enabling method chaining. A final build() call validates everything and produces an immutable configuration object.
The Immutable Agent Configuration
First, define the target configuration as a frozen dataclass — once built, it cannot be modified:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass, field
from typing import Callable, Any
@dataclass(frozen=True)
class ToolDefinition:
name: str
description: str
handler: Callable
parameters_schema: dict
@dataclass(frozen=True)
class AgentConfig:
name: str
model: str
system_prompt: str
temperature: float
max_tokens: int
tools: tuple[ToolDefinition, ...]
memory_backend: str | None
max_retries: int
timeout_seconds: int
guardrails: tuple[str, ...]
def describe(self) -> str:
return (
f"Agent '{self.name}' using {self.model} "
f"with {len(self.tools)} tools, "
f"memory={self.memory_backend or 'none'}"
)
The Builder Class
class AgentConfigBuilder:
def __init__(self):
self._name: str | None = None
self._model: str = "gpt-4o"
self._system_prompt: str = "You are a helpful assistant."
self._temperature: float = 0.7
self._max_tokens: int = 4096
self._tools: list[ToolDefinition] = []
self._memory_backend: str | None = None
self._max_retries: int = 3
self._timeout_seconds: int = 30
self._guardrails: list[str] = []
def with_name(self, name: str) -> "AgentConfigBuilder":
self._name = name
return self
def with_model(self, model: str) -> "AgentConfigBuilder":
allowed = {"gpt-4o", "gpt-4o-mini", "claude-sonnet-4-20250514",
"claude-haiku-35"}
if model not in allowed:
raise ValueError(
f"Unknown model '{model}'. Allowed: {allowed}"
)
self._model = model
return self
def with_system_prompt(self, prompt: str) -> "AgentConfigBuilder":
if len(prompt) > 10000:
raise ValueError("System prompt exceeds 10000 chars")
self._system_prompt = prompt
return self
def with_temperature(self, temp: float) -> "AgentConfigBuilder":
if not 0.0 <= temp <= 2.0:
raise ValueError("Temperature must be between 0.0 and 2.0")
self._temperature = temp
return self
def with_max_tokens(self, tokens: int) -> "AgentConfigBuilder":
self._max_tokens = tokens
return self
def add_tool(self, name: str, description: str,
handler: Callable,
parameters_schema: dict | None = None,
) -> "AgentConfigBuilder":
tool = ToolDefinition(
name=name,
description=description,
handler=handler,
parameters_schema=parameters_schema or {},
)
self._tools.append(tool)
return self
def with_memory(self, backend: str) -> "AgentConfigBuilder":
valid = {"redis", "sqlite", "in_memory", "postgres"}
if backend not in valid:
raise ValueError(f"Unknown memory backend: {backend}")
self._memory_backend = backend
return self
def with_retries(self, count: int) -> "AgentConfigBuilder":
self._max_retries = max(0, count)
return self
def with_timeout(self, seconds: int) -> "AgentConfigBuilder":
self._timeout_seconds = seconds
return self
def add_guardrail(self, rule: str) -> "AgentConfigBuilder":
self._guardrails.append(rule)
return self
def build(self) -> AgentConfig:
# Validation
if not self._name:
raise ValueError("Agent name is required")
if not self._system_prompt.strip():
raise ValueError("System prompt cannot be empty")
# Check for duplicate tool names
tool_names = [t.name for t in self._tools]
if len(tool_names) != len(set(tool_names)):
raise ValueError("Duplicate tool names detected")
return AgentConfig(
name=self._name,
model=self._model,
system_prompt=self._system_prompt,
temperature=self._temperature,
max_tokens=self._max_tokens,
tools=tuple(self._tools),
memory_backend=self._memory_backend,
max_retries=self._max_retries,
timeout_seconds=self._timeout_seconds,
guardrails=tuple(self._guardrails),
)
Fluent API in Action
def search_web(query: str) -> str:
return f"Results for: {query}"
def read_file(path: str) -> str:
return f"Contents of: {path}"
config = (
AgentConfigBuilder()
.with_name("research-assistant")
.with_model("gpt-4o")
.with_system_prompt(
"You are a research assistant that finds and "
"synthesizes information from multiple sources."
)
.with_temperature(0.3)
.with_max_tokens(8192)
.add_tool("search", "Search the web", search_web)
.add_tool("read_file", "Read a local file", read_file)
.with_memory("redis")
.with_retries(3)
.with_timeout(60)
.add_guardrail("Never share personal information")
.add_guardrail("Always cite sources")
.build()
)
print(config.describe())
# Agent 'research-assistant' using gpt-4o with 2 tools, memory=redis
Preset Configurations
Create factory methods for common configurations:
class AgentPresets:
@staticmethod
def fast_and_cheap() -> AgentConfigBuilder:
return (
AgentConfigBuilder()
.with_model("gpt-4o-mini")
.with_temperature(0.5)
.with_max_tokens(2048)
.with_retries(1)
.with_timeout(15)
)
@staticmethod
def high_quality() -> AgentConfigBuilder:
return (
AgentConfigBuilder()
.with_model("gpt-4o")
.with_temperature(0.2)
.with_max_tokens(8192)
.with_retries(3)
.with_timeout(60)
)
# Start from a preset and customize
config = (
AgentPresets.high_quality()
.with_name("legal-reviewer")
.with_system_prompt("You are a legal document reviewer.")
.add_guardrail("Flag any potentially non-compliant clauses")
.build()
)
FAQ
Why use the Builder pattern instead of just passing keyword arguments?
Keyword arguments work for simple configurations but break down when you have validation rules that depend on combinations of parameters, when you want to enforce required fields at build time rather than runtime, or when you need preset configurations that users can extend. The builder gives you all of this with a readable, self-documenting API.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
How do I make the built configuration truly immutable in Python?
Using @dataclass(frozen=True) prevents attribute reassignment after creation. For deeper immutability, use tuples instead of lists for collection fields (as shown with tools and guardrails). This ensures that neither the config object nor its contents can be accidentally modified after construction.
Can I clone and modify an existing configuration?
Add a to_builder() method on AgentConfig that creates a new AgentConfigBuilder pre-populated with the current configuration values. This lets you create variations of existing configs without starting from scratch.
#AgentDesignPatterns #BuilderPattern #Python #Configuration #AgenticAI #LearnAI #AIEngineering
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.