Skip to content

Agent Pattern

All AI agents in the pipeline follow a consistent pattern built around call_agent() in src/agents/base.py.

How call_agent() Works

call_agent() is the single entry point for all LLM interactions. It handles:

  1. Prompt loading — fetches the active system prompt from the database (cached with 300s TTL)
  2. API call — sends the request to Anthropic with exponential backoff on 429/5xx errors
  3. Tool use loop — if the model requests tool calls, executes them and continues the conversation
  4. Cost tracking — calculates token costs and records them to the database
  5. Structured logging — logs agent name, duration, token usage, and cost

Parameters

Parameter Type Description
agent_name str Agent identifier (e.g., "factuality_checker")
content_type str Content category for prompt lookup (e.g., "article")
messages list[dict] Conversation history
article_id UUID | None For cost attribution
tools dict[str, Callable] | None Tool name to async handler map
tool_definitions list[dict] | None JSON Schema definitions for Claude's tool protocol
max_tokens int Token budget (default 4096)
max_iterations int Max tool use loop cycles (default 10)

Return Type

@dataclass
class AgentResponse:
    content: str              # Final text response
    tool_results: list[dict]  # Executed tools with inputs/outputs
    usage: dict[str, int]     # {"input_tokens": N, "output_tokens": N}
    cost_usd: float           # Calculated cost
    model: str                # Model used (from prompt)
    duration_ms: int          # Wall-clock milliseconds

Prompt Loading

Prompts are stored in the agent_prompts table and loaded via get_prompt(agent_name, content_type):

  1. Check in-memory TTL cache (300s)
  2. On miss: query DB for the active prompt matching agent_name and content_type
  3. Falls back to content_type='all' if no exact match
  4. Raises PromptNotFoundError if no active prompt exists
  5. Cache can be invalidated via invalidate_prompt_cache(agent_name) (used by dashboard edits)

Tool Dispatch

Agents that need external data (e.g., the factuality checker needs PubMed) use the tool use loop:

# In the agent module
_TOOL_MAP = {"search_pubmed": _wrap_search_pubmed, ...}
_TOOL_DEFINITIONS = [SEARCH_PUBMED_SCHEMA, ...]

# Pass to call_agent
response = await call_agent(
    "factuality_checker", content_type,
    messages, tools=_TOOL_MAP, tool_definitions=_TOOL_DEFINITIONS,
)

Inside call_agent(), the loop runs while the model's stop_reason is "tool_use":

  1. Extract tool call blocks from the response
  2. Execute each tool: result = await tools[tool_name](**block.input)
  3. Sanitize the result via sanitize_external_content()
  4. Append tool results to the conversation
  5. Re-invoke the API with updated messages

Agent Types

Tool-Using Agents

Agents like the Researcher and Factuality Checker use tools to access external data. They:

  1. Define tool wrapper functions that return JSON strings
  2. Build a _TOOL_MAP and _TOOL_DEFINITIONS
  3. Pass these to call_agent() with a higher max_iterations

Generation-Only Agents

Agents like the Writer and Brief Generator produce output without tool calls. They:

  1. Assemble a user message from structured inputs (brief, dossier)
  2. Call call_agent() without tools or tool_definitions
  3. Parse the JSON response into a Pydantic model

I/O Schemas

All agent inputs and outputs are defined as Pydantic v2 models in src/agents/schemas.py:

Agent Output Model Key Fields
Brief Generator ArticleBrief Structure, keywords, outline, AEO spec
Researcher ResearchDossier Sources, evidence summaries
Writer WriterOutput Draft, meta description, SEO title, slug
Factuality Checker FactualityOutput Passed, score, claims checked/verified/flagged, issues
SEO Optimizer SeoOutput Passed, score, deterministic checks, LLM evaluation
Style Checker StyleOutput Passed, score, readability, structure, voice
Synthesis SynthesisOutput Revised draft, change log
Image Generator ImageGeneratorOutput Image bytes, prompt used, revised prompt

Tool-Using + External API Agents

The Image Generator uses Claude to compose a DALL-E prompt from article context, then calls the OpenAI API via a tool to generate the image. It combines both patterns: LLM reasoning (Anthropic) and external API tool use (OpenAI DALL-E 3).

Adding a New Agent

See the Add an Agent how-to guide for step-by-step instructions.