AI agents by framework / Python
How to add an AI agent to a FastAPI app
An AI agent in a FastAPI app is an async path operation that calls a model with tools, runs the tool calls the model makes against your own services as the user your auth dependency returns, and streams the result to the browser. Pydantic AI covers the model loop, tool approval and the streaming protocol; you supply the tools, authorization and limits. If the agent only needs to do what your pages already do, you can embed one instead.
What an agent in a FastAPI app is made of
- An endpoint: an async path operation such as
POST /api/chaton anAPIRouter, guarded by your authentication dependency. It holds the model API key, which comes from the server environment. - Tools: typed functions the model may call, each wrapping a service or query your app already has. Their arguments are validated with Pydantic before they run.
- The agent loop: the model calls a tool, receives the result and decides the next step until it can answer. Cap the number of model requests.
- A chat UI: a frontend that reads the streamed response and shows tool activity, usually a React app talking to your API.
- Authorization and approval: the user from your dependency is passed into every tool, and actions that change data wait for the user to approve them.
Ways to add an agent to FastAPI
| Approach | Good for | What you take on |
|---|---|---|
| Pydantic AI or the OpenAI Agents SDK in a path operation | A custom agent with your own tools, model provider and UI. | Tool definitions, authorization in every tool, approval UI, rate and cost limits. |
| An agent framework (for example LangGraph) | Long-running or multi-agent workflows with checkpoints and orchestration. | Another layer to learn and deploy, plus everything in the row above. |
| Hosted answer-only chat widget | Questions about your docs or help center. | Keeping its content current. It does not act in your app. |
| Embedded in-app agent (Bunkhouse) | Letting signed-in users get tasks done through the pages and API you already have. | A script tag, an identity endpoint and hints for operations discovery cannot see. |
Build an agent with Pydantic AI
Pydantic AI is the Pydantic team's Python agent framework: typed tools, dependency injection, usage limits, approval for tool calls and adapters that stream a run to a chat frontend. Like FastAPI, it builds on Pydantic models and type hints. The examples below use Pydantic AI 2.64 with FastAPI 0.162. Install the slim package with the Anthropic and UI extras, and the AI SDK in your frontend:
pip install "pydantic-ai-slim[anthropic,ui]"
npm install ai @ai-sdk/react # in your React frontendThe model string "anthropic:claude-sonnet-5-5" reads the API key from the ANTHROPIC_API_KEY variable. Switching provider is a change of string and extra, for example "openai:gpt-6.1-sol" with pydantic-ai-slim[openai]; see the models overview.
1. The agent and the endpoint
The endpoint gets the signed-in user from your existing current_user dependency, which rejects signed-out requests with 401 before any model call. The agent declares deps_type=User, the endpoint passes that user as deps, and each tool reads it from ctx.deps. The model can choose an order ID but never whose orders it reads or cancels.
The flag requires_approval=True makes cancel_order a deferred tool: when the model calls it, the run ends with a DeferredToolRequests output instead of cancelling anything, which is why that type is in output_type. VercelAIAdapter.dispatch_request reads the request body that the AI SDK's useChat sends, runs the agent and returns a streaming response of server-sent events in the Vercel AI data stream protocol. The option sdk_version=7 turns on approval requests for an AI SDK 7 frontend, and UsageLimits(request_limit=5) caps model requests per run; a run that needs more ends with an error in the stream.
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Request
from fastapi.responses import Response
from pydantic_ai import Agent, DeferredToolRequests, RunContext, UsageLimits
from pydantic_ai.ui.vercel_ai import VercelAIAdapter
from . import orders # your existing data layer
from .auth import User, current_user # your existing auth dependency
agent = Agent(
"anthropic:claude-sonnet-5-5",
deps_type=User,
output_type=[str, DeferredToolRequests],
instructions="You help the signed-in user manage their orders. Use the tools; never guess order data.",
)
@agent.tool
async def list_orders(
ctx: RunContext[User], status: Literal["open", "shipped", "cancelled"] | None = None
) -> list[dict]:
"""List the signed-in user's recent orders."""
return await orders.list_orders(ctx.deps.id, status=status)
@agent.tool(requires_approval=True)
async def cancel_order(ctx: RunContext[User], order_id: str) -> dict:
"""Cancel one of the signed-in user's open orders."""
return await orders.cancel_order(ctx.deps.id, order_id)
router = APIRouter()
@router.post("/api/chat")
async def chat(request: Request, user: Annotated[User, Depends(current_user)]) -> Response:
return await VercelAIAdapter.dispatch_request(
request,
agent=agent,
deps=user,
sdk_version=7,
usage_limits=UsageLimits(request_limit=5),
)Include the router in your application with app.include_router(router).
2. The chat component
The useChat hook posts to /api/chat by default, so serve the frontend from the same origin as the API or proxy that path to it. When the model calls cancel_order, the tool part arrives in the state approval-requested. The user's answer is sent with addToolApprovalResponse, and sendAutomaticallyWhen resends the conversation once every pending approval has a response. The adapter reads the decision from that request and runs or skips the tool.
import { useChat } from "@ai-sdk/react";
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
import { useState } from "react";
export function Agent() {
const [input, setInput] = useState("");
const { messages, sendMessage, addToolApprovalResponse } = useChat({
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});
return (
<div>
{messages.map((message) =>
message.parts.map((part, i) => {
const key = `${message.id}-${i}`;
if (part.type === "text") return <p key={key}>{part.text}</p>;
if (part.type === "tool-cancel_order" && part.state === "approval-requested") {
const { id } = part.approval;
return (
<div key={key}>
<p>Cancel order {JSON.stringify(part.input)}?</p>
<button onClick={() => addToolApprovalResponse({ id, approved: true })}>Approve</button>
<button onClick={() => addToolApprovalResponse({ id, approved: false })}>Deny</button>
</div>
);
}
return null;
}),
)}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input aria-label="Message" value={input} onChange={(e) => setInput(e.currentTarget.value)} />
</form>
</div>
);
}The browser sends the whole conversation, including approval decisions, with every request, and the adapter does not check that an approved call is one the server issued. This is safe here because cancel_order still runs only as the authenticated user, on that user's orders: a forged approval cannot do more than the user can do through your API anyway. If an approval must be tied to server state, Pydantic AI's UI trust model describes keeping paused runs and history on the server instead.
Production checklist for FastAPI
- Authorize in the dependency and in every tool. Put your auth dependency on the agent route, and scope every query in every tool to the user in
ctx.deps, never to an ID the model supplies. - Protect cookie sessions against cross-site requests. FastAPI has no built-in CSRF protection. The adapter accepts only
application/jsonbodies and answers anything else with 415, so a plain cross-site form cannot start a run. KeepCORSMiddlewarelimited to your own origins and your session cookieSameSite, or check a CSRF header in a dependency. - Do not block the event loop. An
asynctool that calls a synchronous database driver stalls every request on that worker. Use an async driver, or define the tool with plaindef, which Pydantic AI runs in a thread. - Allow for long responses. A run with several model requests can stream for a minute or more. Turn off response buffering for the route in your proxy and raise its read timeout.
- Treat tool input and tool results as untrusted. The model chooses tool arguments, and data a tool returns (user-written text, emails, web pages) can contain instructions. Validate arguments with your own business rules, and require approval for anything irreversible.
- Limit cost. Cap requests with
UsageLimits, rate-limit the endpoint per user and set spending limits with your model provider. - Keep secrets server-side. Model keys belong in server environment variables, never in the frontend bundle.
Option: embed an in-app agent with Bunkhouse
If the agent's job is to help signed-in users do work your FastAPI app already supports, you may not need to build the tools, agent loop and chat UI yourself. Bunkhouse adds an Assistant pane to an existing web app with one script tag. When a user opens it, the assistant works with what the page already offers: buttons, links, form fields and tables, the API requests your app makes, a hints file you publish for operations it cannot see, and WebMCP tools your page declares.
The model writes small programs that run in a sandbox on the embed origin, not in your page. They can only call capabilities the assistant has discovered and your configuration allows. Write actions are shown to the user for review before they run, the model never sees credentials, and your FastAPI backend keeps enforcing its own authorization on every request.
In a FastAPI app that renders Jinja2 templates, load the identity hook and then the loader in the base template's head, before your own scripts. For signed-in users, add an APIRouter with a GET /api/agent-identity endpoint that signs a short-lived token for the user your current_user dependency returns, and include it in the app. The signing secret stays in a server environment variable.
<head>
<!-- ... -->
<script src="/static/agent-identity.js"><\/script>
<script src="https://embed.bunkhouse.ai/embed.js" data-public-key="pk_YOUR_SITE_PUBLIC_KEY"><\/script>
<!-- your own scripts come after the loader -->
</head>The tested FastAPI integration guide has the complete code for each session mode (signed identity token, server mint and anonymous), CSRF headers and Content Security Policy settings.
When Bunkhouse fits, and when to build it yourself
- Bunkhouse fits when users want tasks done inside your existing pages, such as filtering, editing or submitting through the controls and API you already have, and you would rather not write and maintain a tool definition for each action, a sandbox for model-written code, an approval UI and usage metering.
- Build it yourself when the agent must run without a browser page (background jobs, email, chat platforms, scheduled work), when you need to choose the model provider, use your own keys or run inference on your own infrastructure, or when the conversation must be part of your own UI components.
- Plan for the limits: discovery cannot see server-only routes, workflows the user has not visited, closed shadow roots or cross-origin frames (describe those in a hints file); some controls need a real user gesture; password, payment and file fields are never read, and you can mark other content private.
- Cost: production use is $49 per month, including $20 of usage credit per paid billing period, then $1 per million input tokens and $3 per million output tokens. See billing.
Try it on your FastAPI app first
A coding agent such as Claude Code, Codex or Cursor can run a free, disposable agent trial on a dummy page of your FastAPI app. No account, card or model API key is needed: your coding agent acts as the assistant's model. A trial lasts 24 hours and allows 100 turns. Give your agent this instruction, changing the address if your app runs elsewhere:
Read https://api.bunkhouse.ai/agent-trial.md and follow it to run a Bunkhouse agent trial on my dummy FastAPI page at http://127.0.0.1:8000.Use a page with disposable data: everything the assistant reads is sent to your agent.
The trial evaluation checklist shows how to test a read, a rejected write and an approved change, and what a trial does not prove about production use.
Related guides
- How to add AI to your website: chat assistants, in-app agents and custom builds compared
- MCP vs WebMCP: choosing where outside agents act on your site
- AI agents in other Python frameworks: Django, Flask