Skip to content
Learn Agentic AI10 min read0 views

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

Share this article
C

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.