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:
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:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
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.
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
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.