Multi-Agent Content Intelligence API

Build a multi-agent FastAPI backend that analyzes web pages with Pydantic AI.

Introduction

30 Second Summary

Every day you read dozens of web pages, but extracting structured takeaways from each one takes real effort. Imagine pointing an AI system at any URL and getting back a clean breakdown of sentiment, key points, and technical depth in seconds.

In this project, you will build a multi-agent Content Intelligence API that accepts a URL and returns structured analysis of the page. You will wire up a coordinator agent that delegates work to specialist agents, then expose everything through a FastAPI backend with both a synchronous JSON endpoint and a streaming real-time endpoint.

What You'll Build

A FastAPI application that uses Pydantic AI to orchestrate multiple AI agents, each with a typed structured output, exposed through REST endpoints including a streaming SSE response.

By the end of this project, you'll have:

  • A running multi-agent system where a coordinator agent delegates to specialist agents using Pydantic AI's tool decorator pattern.
  • Typed Pydantic output models that guarantee structured, type-safe AI responses from your analyst agent.
  • A FastAPI backend with a synchronous JSON endpoint and a streaming SSE endpoint for real-time agent reasoning.
  • Secret Mission: A comparison endpoint that analyzes two URLs side-by-side and produces a structured diff of their content.

Are there any prerequisites?

You should be comfortable with Python and have a basic understanding of FastAPI and LLM API concepts. You will also need an Anthropic API key with credits (around $1-2 worth).

Before We Start

You are about to build a multi-agent system where AI agents delegate work to each other through typed tool functions.

Before you write any agent logic, you need a working Python environment with the right packages installed and your API key configured.

In this step, get ready to:
  • Create a project directory and Python virtual environment.
  • Install Pydantic AI, FastAPI, and supporting dependencies.
  • Set up your Anthropic API key and create the project files.
Create the project directory and virtual environment

A virtual environment keeps your project's packages isolated from the rest of your system. This way, the specific versions you install here won't conflict with anything else on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir content-intelligence-api
cd content-intelligence-api
  • Confirm the folder exists by running:
ls ~/Desktop

You should see content-intelligence-api listed in the output.

  • Create and activate a Python virtual environment by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A Python virtual environment is an isolated space where packages are installed separately from your system Python. Every project gets its own set of dependencies at exactly the versions it needs.

Don't see (venv) in your prompt?

Make sure you ran both commands in sequence. The first creates the environment, the second activates it.

On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned first, then try the activate command again.

Why isn't my virtual environment activating?

Install dependencies and configure the API key

Your project needs four packages. Pydantic AI provides the agent framework. FastAPI handles the web server. httpx fetches web pages. uvicorn runs the FastAPI server.

  • Install all four dependencies by running this command:
pip install pydantic-ai==2.7.0 "fastapi[standard]" httpx uvicorn

What does this command do?

  • pydantic-ai==2.7.0 pins the exact Pydantic AI version so your code matches this project.
  • "fastapi[standard]" installs FastAPI along with its recommended production extras.
  • httpx is an async HTTP client that your agent will use to fetch web pages.
  • uvicorn is a lightning-fast ASGI server that runs your FastAPI app.
  • Verify pydantic-ai installed correctly by running:
python -c "import pydantic_ai; print(pydantic_ai.__version__)"

You should see 2.7.0 printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, re-run the activation command from the previous substep.

Why is my pydantic-ai import failing?

Now you need to provide your Anthropic API key so Pydantic AI can call Claude on your behalf. The key is read from an environment variable at runtime.

  • Set your Anthropic API key as an environment variable by running this command:

macOS

export ANTHROPIC_API_KEY='your-api-key-here'

Windows

set ANTHROPIC_API_KEY=your-api-key-here
  • Replace your-api-key-here with your actual Anthropic API key before running the command.
  • Verify the key is set by running:
python -c "import os; print('Key set:', bool(os.environ.get('ANTHROPIC_API_KEY')))"

You should see Key set: True printed in your terminal.

Why an environment variable?

Storing your API key in an environment variable keeps it out of your source code. Pydantic AI reads the ANTHROPIC_API_KEY variable automatically when you use an Anthropic model.

Create the project file structure

Your project uses three files, each with a clear responsibility. Separating concerns this way keeps the codebase navigable as the agent system grows.

  • Create the three project files by running this command:

macOS

touch main.py agents.py models.py

Windows

New-Item main.py -ItemType File
New-Item agents.py -ItemType File
New-Item models.py -ItemType File

What is each file for?

  • models.py defines the Pydantic output models that structure your AI responses.
  • agents.py holds your agent definitions and tool functions.
  • main.py is the FastAPI application with your API endpoints.
  • Confirm the files exist by running:
ls

You should see agents.py, main.py, models.py, and the venv folder in the output.

Missing a file?

Make sure you are still inside the content-intelligence-api directory. Run pwd (macOS) or cd (Windows) to check your current directory.

My project files aren't showing up when I run ls.

Your environment is ready. Next up, you'll define the Pydantic output models that give your AI agent typed, structured responses.

Define Pydantic Output Models and the Analyst Agent

Your environment is ready and your project files are in place. Now it's time to define what your AI agents will actually produce.

In a multi-agent system, the biggest risk is agents returning unstructured, unpredictable text. Pydantic AI solves this by letting you specify typed Pydantic models as the required output shape. The agent is forced to return data that matches your schema exactly.

In this step, get ready to:
  • Define typed Pydantic models for content analysis output.
  • Create the analyst agent with a structured output type.
  • Test the analyst agent standalone and verify it returns valid structured data.
Create the content analysis model

The ContentAnalysis model defines the exact shape of data your analyst agent must return. Every field has a type and a description that guides the LLM on what to produce.

  • Open models.py in VS Code and add the following code:
from pydantic import BaseModel, Field


class ContentAnalysis(BaseModel):
    # The primary output model for analyzed web content
    title: str = Field(description="The title or main heading of the content")
    summary: str = Field(description="A 2-3 sentence summary of the content")
    key_points: list[str] = Field(description="3-5 key takeaways from the content")
    sentiment: str = Field(description="Overall sentiment: positive, negative, neutral, or mixed")
    technical_depth: str = Field(description="Technical depth rating: beginner, intermediate, or advanced")
    word_count: int = Field(description="Approximate word count of the original content")

What does this code do?

ContentAnalysis inherits from Pydantic's BaseModel, which gives it automatic validation and serialization.

Each Field(description=...) provides the LLM with instructions on what to put in that field. Pydantic AI passes these descriptions to the model as part of the output schema, so the AI knows exactly what format you expect.

Add the request and comparison models

Your API will need models for incoming requests and for the comparison feature you'll build later. Defining all models upfront keeps your codebase clean.

  • Add the following classes below ContentAnalysis in models.py:

class AnalyzeRequest(BaseModel):
    # Request body for the /analyze endpoint
    url: str = Field(description="The URL to analyze")


class CompareRequest(BaseModel):
    # Request body for the /compare endpoint
    url_1: str = Field(description="First URL to compare")
    url_2: str = Field(description="Second URL to compare")


class ContentComparison(BaseModel):
    # Output model for comparing two pieces of content
    similarities: list[str] = Field(description="Key similarities between the two pieces of content")
    differences: list[str] = Field(description="Key differences between the two pieces of content")
    which_is_more_technical: str = Field(description="Which URL has more technical depth and why")
    recommendation: str = Field(description="Which content is better suited for what audience")

What does this code do?

  • AnalyzeRequest and CompareRequest define the shape of incoming HTTP request bodies. FastAPI uses these to validate and parse JSON payloads automatically.
  • ContentComparison is the output model for the Secret Mission comparison endpoint. Defining it now avoids import issues later.
  • Save models.py.

Seeing a red underline in VS Code?

If BaseModel or Field shows a red underline, make sure your Python interpreter is set to the virtual environment you created in Step 1. Click the Python version in the bottom status bar and select the interpreter from your project's venv folder.

Need more help? Help me fix import errors for pydantic in VS Code

✔️ Awesome, I've got everything!

Great. Make sure you've saved models.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete models.py file for comparison:

from pydantic import BaseModel, Field


class ContentAnalysis(BaseModel):
    title: str = Field(description="The title or main heading of the content")
    summary: str = Field(description="A 2-3 sentence summary of the content")
    key_points: list[str] = Field(description="3-5 key takeaways from the content")
    sentiment: str = Field(description="Overall sentiment: positive, negative, neutral, or mixed")
    technical_depth: str = Field(description="Technical depth rating: beginner, intermediate, or advanced")
    word_count: int = Field(description="Approximate word count of the original content")


class AnalyzeRequest(BaseModel):
    url: str = Field(description="The URL to analyze")


class CompareRequest(BaseModel):
    url_1: str = Field(description="First URL to compare")
    url_2: str = Field(description="Second URL to compare")


class ContentComparison(BaseModel):
    similarities: list[str] = Field(description="Key similarities between the two pieces of content")
    differences: list[str] = Field(description="Key differences between the two pieces of content")
    which_is_more_technical: str = Field(description="Which URL has more technical depth and why")
    recommendation: str = Field(description="Which content is better suited for what audience")
Create the analyst agent

The analyst agent is a specialist. Its only job is to take raw text and produce a structured ContentAnalysis object. By setting output_type on the agent, Pydantic AI guarantees the LLM's response will match your model's schema.

  • Open agents.py and add the following code:
import httpx
from pydantic_ai import Agent, RunContext

from models import ContentAnalysis


# Specialist agent that produces structured content analysis
analyst_agent = Agent(
    'anthropic:claude-sonnet-4-6',
    output_type=ContentAnalysis,
    instructions=(
        'You are a content analyst. Given text content from a web page, '
        'produce a structured analysis including title, summary, key points, '
        'sentiment, technical depth, and word count. Be precise and concise.'
    ),
)

What does this code do?

  • Agent('anthropic:claude-sonnet-4-6', ...) creates a Pydantic AI agent wired to Claude Sonnet. The agent reads your ANTHROPIC_API_KEY environment variable automatically.
  • output_type=ContentAnalysis tells the agent it must return a valid ContentAnalysis object. The agent will retry if the LLM's response doesn't match the schema.
  • instructions is the system prompt that tells the agent its role and how to behave.
  • The httpx and RunContext imports are included for the tool functions you'll add in the next step.

Why typed outputs instead of plain text?

Without output_type, an agent returns free-form text. You'd have to parse it yourself, handle inconsistent formats, and hope the LLM includes all the fields you need.

With a typed output, Pydantic AI handles all of this. It passes your model's JSON schema to the LLM, validates the response, and retries on failures. Your downstream code can trust that every field exists and has the correct type.

  • Save agents.py.

Seeing an import error for ContentAnalysis?

Make sure you saved models.py first. The from models import ContentAnalysis line requires that file to exist with the class defined.

Help me debug an import error in my Pydantic AI agents file

✔️ Awesome, I've got everything!

Great. Make sure you've saved agents.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete agents.py file for comparison:

import httpx
from pydantic_ai import Agent, RunContext

from models import ContentAnalysis


analyst_agent = Agent(
    'anthropic:claude-sonnet-4-6',
    output_type=ContentAnalysis,
    instructions=(
        'You are a content analyst. Given text content from a web page, '
        'produce a structured analysis including title, summary, key points, '
        'sentiment, technical depth, and word count. Be precise and concise.'
    ),
)
Test the analyst agent standalone

Before wiring the agent into the coordinator, you should verify it works on its own. You'll feed it a small piece of sample text and confirm it returns a valid ContentAnalysis object with all fields populated.

  • Run the following test command in your terminal:
python -c "
import asyncio
from agents import analyst_agent

sample_text = 'Python is a programming language widely used for web development, data science, and automation. It was created by Guido van Rossum and first released in 1991. Python emphasizes code readability and simplicity, making it popular with beginners and experts alike.'

result = asyncio.run(analyst_agent.run(sample_text))
print(type(result.output))
print(result.output.model_dump_json(indent=2))
"

What does this command do?

  • analyst_agent.run(sample_text) sends the text to Claude and waits for a structured response.
  • result.output is the validated ContentAnalysis object. If the LLM's response didn't match the schema, the agent would have retried automatically.
  • model_dump_json(indent=2) pretty-prints the structured output as JSON so you can inspect every field.

You should see output showing the type as <class 'models.ContentAnalysis'> followed by a JSON object with title, summary, key_points, sentiment, technical_depth, and word_count fields all populated.

Getting an authentication error?

Make sure your ANTHROPIC_API_KEY environment variable is still set in your current terminal session. If you opened a new terminal window, you'll need to export it again.

Also confirm your API key has credits available. This call costs less than $0.01.

Help me fix an authentication error with my Anthropic API key in Pydantic AI

Your analyst agent is working and producing type-safe structured output. Next, you'll build the coordinator agent that delegates work to this analyst through tool functions.

Build the Coordinator Agent with Tool Delegation

Your analyst agent knows how to produce structured analysis from raw text. Now you need an agent that can orchestrate the full workflow: fetching a web page and delegating the analysis to a specialist.

This is the agent-to-agent delegation pattern. A coordinator agent uses tool-decorated functions to break a complex task into steps. One of those tools internally calls a second agent, creating a chain of responsibility where each agent focuses on what it does best.

In this step, get ready to:
  • Define the coordinator agent and a web-fetching tool.
  • Build agent-to-agent delegation with an analysis tool.
  • Test the full multi-agent pipeline end-to-end.
Define the coordinator agent

The coordinator agent needs to exist before you can decorate functions as its tools. It uses the same claude-sonnet-4-6 model but has no output_type because it returns free-form text summarizing the analysis results.

  • In agents.py, find the closing parenthesis of analyst_agent. Add the following code below it:
coordinator_agent = Agent(
    'anthropic:claude-sonnet-4-6',
    instructions=(
        'You are a content intelligence coordinator. When given a URL to analyze, '
        'first fetch the content using the fetch_content tool, then analyze it '
        'using the analyze_content tool. Report the analysis results to the user.'
    ),
)

What does this code do?

The instructions string tells the LLM exactly which tools to call and in what order. The model reads these instructions and decides to call fetch_content first, then analyze_content second.

Unlike analyst_agent, this agent has no output_type. It defaults to returning a plain string, which is perfect for a coordinator that summarizes results in natural language.

  • Save agents.py.
Create the fetch_content tool

Tools in Pydantic AI are async functions decorated with @agent.tool. The first parameter is always a RunContext object. Any additional parameters become the tool's schema that the LLM sees and fills in.

  • Below the coordinator_agent definition in agents.py, add the fetch tool by pasting this code:
@coordinator_agent.tool
async def fetch_content(ctx: RunContext, url: str) -> str:
    """Fetch and return the text content from a web page URL."""
    async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
        response = await client.get(url)
        response.raise_for_status()
        text = response.text
        # Strip HTML tags for cleaner content
        import re
        clean_text = re.sub(r'<[^>]+>', ' ', text)
        clean_text = re.sub(r'\s+', ' ', clean_text).strip()
        return clean_text[:5000]

What does this code do?

The @coordinator_agent.tool decorator registers this function as a tool the coordinator can call. The LLM sees the function's docstring and parameter names to decide when and how to use it.

Inside the function, httpx fetches the URL asynchronously. The HTML tags are stripped with a regex, and the text is truncated to 5000 characters to stay within token limits for the analyst agent.

Why RunContext as the first parameter?

Pydantic AI requires RunContext as the first parameter in every tool function. It carries runtime state like dependencies and retry info. Even when you don't use it directly (like here), it must be present. The LLM never sees this parameter.

  • Save agents.py.

Seeing a red underline on httpx or RunContext?

Make sure the import lines at the top of agents.py still include import httpx and from pydantic_ai import Agent, RunContext. These were added in the previous step. If they are missing, add them back at the top of the file.

help me fix import errors in my agents.py file

Build agent-to-agent delegation with the analysis tool

This is where the multi-agent pattern comes together. The analyze_content tool internally calls analyst_agent.run(). From the coordinator's perspective, it is just calling a tool. Under the hood, a second agent processes the content and returns typed structured data.

  • Below the fetch_content function in agents.py, add the analysis tool:
@coordinator_agent.tool
async def analyze_content(ctx: RunContext, content: str) -> str:
    """Analyze the provided text content and return structured insights."""
    result = await analyst_agent.run(content)
    analysis = result.output
    return (
        f"Title: {analysis.title}\n"
        f"Summary: {analysis.summary}\n"
        f"Key Points: {', '.join(analysis.key_points)}\n"
        f"Sentiment: {analysis.sentiment}\n"
        f"Technical Depth: {analysis.technical_depth}\n"
        f"Word Count: {analysis.word_count}"
    )

What does this code do?

This tool calls await analyst_agent.run(content) to run the analyst agent on the fetched text. The result.output is a typed ContentAnalysis object with structured fields.

The tool then formats these fields into a readable string for the coordinator to relay back to the user. This is agent-to-agent delegation: one agent's tool wraps another agent's entire reasoning pipeline.

  • Save agents.py.
  • Test the full pipeline end-to-end by running this command in your terminal:
python -c "
import asyncio
from agents import coordinator_agent
result = asyncio.run(coordinator_agent.run('Analyze https://example.com'))
print(result.output)
"

You should see the coordinator's response in your terminal. It contains a summary that references the structured analysis fields (title, summary, key points, sentiment, technical depth, and word count) from the analyst agent.

What just happened?

The coordinator agent read your prompt, decided to call fetch_content with the URL, then passed the result to analyze_content. Inside that second tool, the analyst agent ran a full inference call and returned a typed ContentAnalysis object.

Two agents, two LLM calls, all triggered by a single coordinator_agent.run() call. This is the tool delegation pattern that production AI systems use to decompose complex tasks.

Seeing a timeout or connection error?

The httpx client has a 30-second timeout. If your network is slow or the URL is unreachable, you will see a timeout error. Try the command again or confirm you have an internet connection.

If you see an authentication error mentioning your API key, confirm your ANTHROPIC_API_KEY environment variable is still set in this terminal session.

help me debug errors when running my coordinator agent pipeline

✔️ Awesome, I've got everything!

Great. Double check you have saved agents.py with all changes.

ⓧ I'd like to double check the full code

import httpx
from pydantic_ai import Agent, RunContext

from models import ContentAnalysis


analyst_agent = Agent(
    'anthropic:claude-sonnet-4-6',
    output_type=ContentAnalysis,
    instructions=(
        'You are a content analyst. Given text content from a web page, '
        'produce a structured analysis including title, summary, key points, '
        'sentiment, technical depth, and word count. Be precise and concise.'
    ),
)

coordinator_agent = Agent(
    'anthropic:claude-sonnet-4-6',
    instructions=(
        'You are a content intelligence coordinator. When given a URL to analyze, '
        'first fetch the content using the fetch_content tool, then analyze it '
        'using the analyze_content tool. Report the analysis results to the user.'
    ),
)


@coordinator_agent.tool
async def fetch_content(ctx: RunContext, url: str) -> str:
    """Fetch and return the text content from a web page URL."""
    async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
        response = await client.get(url)
        response.raise_for_status()
        text = response.text
        import re
        clean_text = re.sub(r'<[^>]+>', ' ', text)
        clean_text = re.sub(r'\s+', ' ', clean_text).strip()
        return clean_text[:5000]


@coordinator_agent.tool
async def analyze_content(ctx: RunContext, content: str) -> str:
    """Analyze the provided text content and return structured insights."""
    result = await analyst_agent.run(content)
    analysis = result.output
    return (
        f"Title: {analysis.title}\n"
        f"Summary: {analysis.summary}\n"
        f"Key Points: {', '.join(analysis.key_points)}\n"
        f"Sentiment: {analysis.sentiment}\n"
        f"Technical Depth: {analysis.technical_depth}\n"
        f"Word Count: {analysis.word_count}"
    )

Your multi-agent pipeline is working. The coordinator fetches content and delegates analysis to a specialist. Next up, you will expose this entire system through a FastAPI backend with both synchronous and streaming endpoints.

Expose the Agents via FastAPI with Streaming SSE

Your agents work end-to-end from the command line. The coordinator fetches a page, delegates analysis to the specialist, and returns structured results.

Now you need to expose this pipeline as a web API so other applications can consume it. You will create two endpoints: one that returns a clean JSON response, and one that streams the agent's reasoning in real time using Server-Sent Events.

In this step, get ready to:
  • Create a POST endpoint that returns structured analysis as JSON.
  • Create a streaming GET endpoint that delivers text tokens via SSE.
  • Run the server and test both endpoints with curl.
Create the POST /analyze endpoint

The /analyze endpoint is the synchronous path. A client sends a URL, your API fetches the page content, runs it through the analyst agent, and returns a typed ContentAnalysis JSON response.

  • Open main.py in VS Code and add the following code:
import asyncio

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

from models import AnalyzeRequest, CompareRequest, ContentAnalysis, ContentComparison
from agents import coordinator_agent, analyst_agent

app = FastAPI(title="Content Intelligence API")


@app.post("/analyze", response_model=ContentAnalysis)
async def analyze_url(request: AnalyzeRequest):
    """Analyze a URL and return structured content analysis."""
    import httpx
    import re

    async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
        response = await client.get(request.url)
        response.raise_for_status()
        text = response.text
        clean_text = re.sub(r'<[^>]+>', ' ', text)
        clean_text = re.sub(r'\s+', ' ', clean_text).strip()[:5000]

    result = await analyst_agent.run(clean_text)
    return result.output

What does this code do?

The imports pull in your models, both agents, and FastAPI's StreamingResponse (used in the next chunk).

  • app = FastAPI(title="Content Intelligence API") creates the application instance.
  • The /analyze endpoint fetches the URL's HTML, strips tags, trims to 5000 characters, and passes the clean text directly to analyst_agent.run().
  • response_model=ContentAnalysis tells FastAPI to validate and serialize the output as your Pydantic model automatically.

Why fetch inside the endpoint instead of using the coordinator?

The coordinator agent returns a free-text summary from its tools. For the JSON endpoint you need the raw ContentAnalysis object, so calling analyst_agent directly gives you the typed output without the coordinator's reformatting.

The streaming endpoint uses the coordinator because there you want its natural language narration delivered token by token.

  • Save main.py.
Create the streaming SSE endpoint

Server-Sent Events let the server push text to the client as soon as it is generated. Instead of waiting for the full analysis to complete, the client sees the agent's reasoning appear word by word.

  • In main.py, add the following code below the analyze_url function:
@app.get("/analyze/stream")
async def analyze_stream(url: str):
    """Stream the coordinator agent's analysis of a URL as Server-Sent Events."""

    async def event_generator():
        async with coordinator_agent.run_stream(
            f"Analyze the content at this URL: {url}"
        ) as stream:
            async for text in stream.stream_text(delta=True):
                yield f"data: {text}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
    )

What does this code do?

  • coordinator_agent.run_stream() starts the agent and returns an async context manager that yields text as it arrives from the model.
  • stream.stream_text(delta=True) emits only new tokens (deltas), not the accumulated full text.
  • Each delta is wrapped in the SSE data: format so the client can parse it as an event stream.
  • When the stream finishes, a final [DONE] event signals completion to the client.
  • Save main.py.

Seeing a red underline on run_stream?

Some editors flag run_stream because it returns a generic type. The code still runs correctly. You can ignore the warning.

If the import from agents is underlined, make sure your virtual environment is active and that pydantic-ai is installed.

Help me debug my streaming endpoint setup.

Run the server and test the endpoints

With both endpoints defined, you can start the server and verify each one returns what you expect.

  • In main.py, add this block at the very bottom of the file:
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

What does this block do?

This lets you run the file directly with python main.py. For development, you will use uvicorn with --reload instead so the server restarts when you save changes.

  • Save main.py.
  • Start the development server by running this command in your terminal:
uvicorn main:app --reload

You should see output indicating the server is running on http://127.0.0.1:8000.

  • Open a second terminal tab (keep the server running in the first one).
  • Test the /analyze endpoint by running:
curl -X POST http://localhost:8000/analyze -H "Content-Type: application/json" -d '{"url": "https://example.com"}'

After a few seconds you should see a JSON object with title, summary, key_points, sentiment, technical_depth, and word_count fields.

  • Test the streaming endpoint by running:
curl -N "http://localhost:8000/analyze/stream?url=https://example.com"

You should see data: lines appearing incrementally as the coordinator agent reasons and calls its tools. The stream ends with data: [DONE].

Getting a connection refused error?

  • Make sure the uvicorn server is still running in your first terminal tab.
  • Check that your ANTHROPIC_API_KEY environment variable is set in the terminal where the server is running.
  • If you see an import error, confirm your virtual environment is active in both terminal tabs.

Help me debug why my FastAPI server won't start or respond to requests.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that both endpoints respond correctly.

ⓧ I'd like to double check the full code

import asyncio

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

from models import AnalyzeRequest, CompareRequest, ContentAnalysis, ContentComparison
from agents import coordinator_agent, analyst_agent

app = FastAPI(title="Content Intelligence API")


@app.post("/analyze", response_model=ContentAnalysis)
async def analyze_url(request: AnalyzeRequest):
    """Analyze a URL and return structured content analysis."""
    import httpx
    import re

    async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
        response = await client.get(request.url)
        response.raise_for_status()
        text = response.text
        clean_text = re.sub(r'<[^>]+>', ' ', text)
        clean_text = re.sub(r'\s+', ' ', clean_text).strip()[:5000]

    result = await analyst_agent.run(clean_text)
    return result.output


@app.get("/analyze/stream")
async def analyze_stream(url: str):
    """Stream the coordinator agent's analysis of a URL as Server-Sent Events."""

    async def event_generator():
        async with coordinator_agent.run_stream(
            f"Analyze the content at this URL: {url}"
        ) as stream:
            async for text in stream.stream_text(delta=True):
                yield f"data: {text}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
    )


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Your multi-agent content intelligence API is live. You have a synchronous endpoint for structured JSON and a streaming endpoint for real-time token delivery. Next up, the Secret Mission challenges you to add a comparison endpoint that analyzes two URLs in parallel.

Secret mission

Add a Comparison Endpoint

Your API can analyze a single URL. But what if a user wants to compare two articles side-by-side? Build a /compare endpoint that runs two analyses in parallel, then delegates to a third agent that produces a structured content diff.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. Your Anthropic API key is only charged per request, not while idle.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Python virtual environment and project files (models.py, agents.py, main.py).
  • Running uvicorn server on localhost:8000.
  • ANTHROPIC_API_KEY environment variable (active in the current terminal session).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your Content Intelligence API.

  • Your uvicorn server will keep running until you close the terminal or press Ctrl+C.
  • The ANTHROPIC_API_KEY environment variable persists only for this terminal session. You will need to re-export it if you open a new terminal.
  • Your API credits are only consumed when you send requests to the endpoints. There is no idle cost.

Pause - I'll come back to this later

Shut down the running server to free up the terminal. Your project files stay on disk for next time.

  • In the terminal where uvicorn is running, press Ctrl+C to stop the server.
  • Your project files remain on your Desktop. When you return, navigate back into the project folder, activate your virtual environment, re-export ANTHROPIC_API_KEY, and start the server again.

Delete - I don't want to use this again

Remove all project files and stop the server. This frees up disk space and leaves no trace of the project on your machine.

  • In the terminal where uvicorn is running, press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder (the one containing main.py, agents.py, and models.py) by running:

macOS

cd ~/Desktop
rm -rf content-intelligence-api

Windows

cd ~\Desktop
Remove-Item -Recurse -Force content-intelligence-api
  • The ANTHROPIC_API_KEY environment variable disappears automatically when you close the terminal. No extra action needed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a production-pattern multi-agent backend that orchestrates AI specialists through typed tool functions and streams real-time results to clients.

You've learned how to:

  • Build a multi-agent system with Pydantic AI where a coordinator agent delegates to specialist agents through typed tool functions.
  • Create structured Pydantic output models that guarantee type-safe AI responses with validated fields.
  • Expose an agent system through a FastAPI backend with both synchronous JSON and streaming SSE endpoints.
  • Secret Mission: Implemented parallel agent execution with asyncio.gather and a comparison agent that synthesizes multiple analysis results into a structured diff.

Ready to quiz yourself?