Run the agent and its tools in Python, expose a streaming FastAPI endpoint, and connect it to a Next.js chat interface through AI SDK UI. AI SDK for Python provides the message and stream adapters that connect the runtimes. Python executes the tool calls; React renders the results and assistant text.
This example builds a chat that summarizes numbers. The calculation uses Python's standard library, so you can inspect the result without adding a database or a larger application. Replace that tool with your own Python logic once it all works.
Copy link to headingHow does the request reach Python?
The browser sends its chat messages to /api/chat on the same origin as the Next.js app. During local development, Next.js forwards that request to FastAPI. The backend converts the UI messages into the Python SDK's runtime format and starts the agent loop with a hosted model.
When the model calls summarize_numbers, the agent runs the Python function and returns its result to the model. The response stream carries the structured tool output and the assistant's explanation as separate message parts, so the interface can display calculated values without parsing generated prose.
The AI SDK UI adapter handles the format conversion at the backend boundary. Sending arbitrary text chunks over HTTP would leave the frontend without the message and tool events it expects.
Copy link to heading1. Create the project and install dependencies
You'll need basic familiarity with React and Python, Python 3.12 or later, uv, and Node.js with npm. Use Node.js 24 or later for this setup. AI SDK 7 requires Node.js 22 or later, and AI SDK for Python 0.8.0 requires Python 3.12 or later.
Create the project directory and Python backend folder, then generate an empty Next.js app with create-next-app:
mkdir -p python-agent-nextjs/backendcd python-agent-nextjsnpx [email protected] frontend \ --ts --app --empty --use-npm --no-tailwind --no-linter \ --no-src-dir --no-react-compiler --no-cache-components \ --disable-git --skip-install --yescreate-next-app scaffolds the project for you in the frontend/ directory. The --skip-install option defers dependency installation to the next command so you can install the versions used in this example.
Install the frontend dependencies, then return to the project root:
cd frontendnpm install --save-exact [email protected] [email protected] [email protected] [email protected] @ai-sdk/[email protected]npm install --save-dev typescript@~5.9.3 @types/node@^24 @types/react@^19 @types/react-dom@^19cd ..Add backend/pyproject.toml:
[project]name = "python-agent-nextjs-example"version = "0.1.0"requires-python = ">=3.12"dependencies = ["ai==0.8.0", "fastapi==0.143.0", "uvicorn==0.54.0"]Install the backend dependencies, then return to the project root:
cd backenduv synccd ..At the project root, add .gitignore to exclude local credentials and generated files:
.venv/__pycache__/.pytest_cache/node_modules/.next/.env*!.env.example*.tsbuildinfoCreate an AI Gateway API key and put it in backend/.env, replacing the placeholder:
AI_GATEWAY_API_KEY=replace-with-your-keyThe backend uses this key to authenticate requests to AI Gateway. Keep it out of client-side code and environment variables prefixed with NEXT_PUBLIC_.
The relevant files will have this structure after you complete the remaining steps. create-next-app also creates supporting files that aren't shown:
python-agent-nextjs/ .gitignore vercel.json backend/ .env pyproject.toml uv.lock agent.py main.py frontend/ package.json package-lock.json tsconfig.json next.config.ts app/ layout.tsx page.tsxCopy link to heading2. Define the Python tool and agent
Create backend/agent.py:
import mathimport statistics
import ai
@ai.toolasync def summarize_numbers(values: list[float]) -> dict[str, float | int]: """Calculate the count, mean, and median of the supplied numbers.""" if not 1 <= len(values) <= 1000 or not all(map(math.isfinite, values)): raise ValueError("Provide 1 to 1,000 finite numbers.") return { "count": len(values), "mean": statistics.mean(values), "median": statistics.median(values), }
chat_agent = ai.Agent(tools=[summarize_numbers])SYSTEM_PROMPT = "Use summarize_numbers for numerical summaries. Report its results."The async function takes a typed list and returns a dictionary the interface can display. It checks that the list contains 1 to 1,000 finite values, raising an error before calculating if the list is empty, too long, or contains infinity or NaN.
The @ai.tool decorator exposes the function as an agent tool, and ai.Agent(tools=[summarize_numbers]) runs it when the model requests it. The system prompt tells the model to use the tool for numerical summaries. The function computes the statistics, while the model decides how to describe them.
Mean and median don't require an AI model, and Python isn't the only language that can calculate them. The small tool makes the integration visible. In an existing application, the same boundary can expose Python logic your team already maintains. The other uses for a Python backend in Next.js include analysis pipelines and established business services.
Copy link to heading3. Expose the streamed FastAPI endpoint
Create backend/main.py:
import loggingimport os
import aifrom fastapi import FastAPI, HTTPExceptionfrom fastapi.responses import StreamingResponsefrom pydantic import BaseModel, Field
from agent import SYSTEM_PROMPT, chat_agent
app = FastAPI()logger = logging.getLogger(__name__)
class ChatRequest(BaseModel): messages: list[ai.ui.ai_sdk.UIMessage] = Field(min_length=1, max_length=50)
def get_model(): if not os.environ.get("AI_GATEWAY_API_KEY"): raise HTTPException(status_code=503, detail="Model credentials are missing.") return ai.get_model("openai/gpt-6.1-sol")
@app.post("/api/chat")async def chat(request: ChatRequest): messages, _ = ai.ui.ai_sdk.to_messages(request.messages, tools=chat_agent.tools) model = get_model()
async def stream_response(): try: async with chat_agent.run( model, [ai.system_message(SYSTEM_PROMPT), *messages] ) as stream: async for chunk in ai.ui.ai_sdk.to_sse(stream): yield chunk except Exception: logger.exception("Chat stream failed") yield 'data: {"type":"error","errorText":"The response failed. Try again."}\n\n' yield 'data: [DONE]\n\n' finally: await model.provider.aclose()
return StreamingResponse( stream_response(), headers=ai.ui.ai_sdk.UI_MESSAGE_STREAM_HEADERS )The model identifier openai/gpt-6.1-sol has no colon-separated provider prefix, so ai.get_model routes it through AI Gateway.
ChatRequest accepts the UI message format sent by the frontend. to_messages converts that history for the Python agent and also returns tool approval responses. Keep tools=chat_agent.tools when converting the message history so the adapter knows the tools associated with its results.
The agent loop runs inside the response generator. As it produces events, to_sse encodes them as server-sent events for AI SDK UI. Preserve UI_MESSAGE_STREAM_HEADERS, including x-vercel-ai-ui-message-stream: v1; the client needs the stream protocol as well as the event payloads.
Missing credentials return HTTP 503 before the stream begins. If an exception escapes the agent loop during streaming, the handler logs it on the server and sends a generic error event to the browser. The UI receives an error without a Python traceback or provider details. The finally block closes the provider after the stream ends.
The request model also limits the incoming history to 50 messages. That count doesn't bound request bytes or model tokens, and a single message can contain substantial content. Add separate request-size and usage controls before exposing the endpoint to other users.
Copy link to heading4. Connect the Next.js interface
Keep the generated frontend/app/layout.tsx. Replace the contents of frontend/app/page.tsx with the chat interface:
"use client";
import { useChat } from "@ai-sdk/react";import { DefaultChatTransport, isToolUIPart } from "ai";import { useState } from "react";
export default function Page() { const [input, setInput] = useState(""); const { messages, sendMessage, status, error } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat" }), }); const busy = status === "submitted" || status === "streaming";
return ( <main> <h1>Ask Python about your numbers</h1> {messages.map((message) => ( <section key={message.id} aria-label={message.role}> <strong>{message.role}</strong> {message.parts.map((part, index) => { if (part.type === "text") return <p key={index}>{part.text}</p>; if (isToolUIPart(part) && part.state === "output-available") { return <pre key={index}>{JSON.stringify(part.output, null, 2)}</pre>; } return null; })} </section> ))} {busy && <p role="status">Working...</p>} {error && <p role="alert">The request failed. Check the server and try again.</p>} <form onSubmit={(event) => { event.preventDefault(); const text = input.trim(); if (!text || busy) return; void sendMessage({ text }); setInput(""); }}> <label htmlFor="message">Message</label> <input id="message" value={input} onChange={(event) => setInput(event.target.value)} /> <button type="submit" disabled={busy || !input.trim()}>Send</button> </form> </main> );}useChat manages the message list and request state. DefaultChatTransport posts the UI messages to /api/chat, which is the FastAPI route you created. The component keeps the input value in React state and prevents another submission while a response is pending or streaming.
Each message contains parts. Text parts render as paragraphs, and isToolUIPart identifies tool parts before the component reads their state and output. When a tool part reaches output-available, the component displays its JSON separately from assistant text so you can inspect the Python result.
Tool failures reported as output-error parts don't trigger useChat's request-level error, and this component doesn't render them. If you need a visible tool-failure message, add a branch for that state.
Replace the contents of frontend/next.config.ts to forward API requests during development:
import type { NextConfig } from "next";
const nextConfig: NextConfig = { async rewrites() { return process.env.NODE_ENV === "development" ? [{ source: "/api/:path*", destination: "http://127.0.0.1:8000/api/:path*" }] : []; },};
export default nextConfig;Next.js rewrites can proxy an external destination. Here, the browser requests /api/chat from localhost:3000, and Next.js forwards it to 127.0.0.1:8000/api/chat. You don't need CORS configuration for this same-origin browser path. Restart the Next.js development server after changing the configuration.
The localhost rewrite only runs in development. The deployment configuration in step 6 routes requests between the deployed services.
Copy link to heading5. Run the app and inspect a tool result
Open two terminals at the project root. In the first, start FastAPI:
cd backenduv run --env-file .env uvicorn main:app --reload --port 8000The --env-file .env option loads the local key. The Python application doesn't load that file itself.
In the second terminal, start Next.js:
cd frontendnpm run devOpen localhost:3000 in your browser and enter "Summarize 10, 20, and 30." The expected tool output is:
{ "count": 3, "mean": 20, "median": 20}The assistant should also describe the result in text. Its wording can vary. Check the JSON values to confirm the calculation, then inspect the browser's network panel for the POST to /api/chat. On success, you'll receive stream events rather than a single JSON document containing the entire conversation.
If the request fails, check the backend terminal alongside the browser response. The UI deliberately displays a generic failure message, so it won't tell you whether the underlying cause was credentials or a provider failure.
Copy link to heading6. Configure a combined deployment
Vercel Services builds the frontend and backend independently within one project and uses top-level rewrites to route requests.
Add this vercel.json file at the project root:
{ "services": { "frontend": {"root": "frontend/"}, "backend": {"root": "backend/", "entrypoint": "main:app"} }, "rewrites": [ {"source": "/api/(.*)", "destination": {"service": "backend"}}, {"source": "/(.*)", "destination": {"service": "frontend"}} ]}The API rule must precede the catch-all frontend rule. With this routing configuration, /api/chat reaches the backend with its original path, matching the FastAPI decorator. Other paths go to Next.js.
This configuration exposes /api/chat publicly. Same-origin routing doesn't authenticate callers. Before deploying with real API credits or application data, enforce authentication in FastAPI or a trusted gateway and authorize each data or tool action. Add usage controls at that boundary too; hiding the form from signed-out users won't protect the endpoint.
Configure AI_GATEWAY_API_KEY in the deployment environment without a public prefix. Follow the Services setup and FastAPI deployment guide for project creation and deployment. Review the Python runtime requirements as you add dependencies or longer-running tools.
Copy link to headingDiagnose connection and streaming failures
Copy link to headingWhat does this example leave out?
This app keeps conversation history in browser state and accepts that history from the client. Refreshing the page loses the displayed conversation. For production, store conversations on the server and check ownership before loading or extending a user's history. Treat client-supplied messages as untrusted input.
Streaming keeps the HTTP response active while the agent works. It doesn't save progress or resume an interrupted agent run after a restart. If a task must continue after a disconnect, design persistent execution separately from the chat connection.
The statistics tool exposes one read-only calculation without allowing arbitrary Python execution. Replacing it with a tool that changes records requires authorization, and sensitive writes may also need an explicit approval step. If you add approvals, implement their handling too; this example discards approval responses.
If you want a TypeScript agent to call an existing Python API, the eve Agent with Python template demonstrates that approach. Its agent tools call FastAPI endpoints that the Next.js application also uses. That architecture differs from the Python-owned agent loop here.
Copy link to headingFrequently asked questions
Copy link to headingCan I use AI SDK UI with a Python backend?
Yes. AI SDK for Python includes an adapter that converts UI messages into Python runtime messages and encodes agent events for the UI stream protocol. You can render those responses with AI SDK UI in a Next.js client while Python runs the agent and its tools.
Copy link to headingDoes streaming preserve a conversation after a restart?
No. Streaming delivers events during an active response; it doesn't provide durable conversation storage or resume interrupted execution. This example keeps chat history in browser state, so an application that needs recovery must add server-owned storage and an execution recovery design.
Copy link to headingWhere should model credentials live?
Keep model credentials in the backend environment or a server-side secret store. This example reads AI_GATEWAY_API_KEY in Python, and the browser sends messages to the application endpoint. Never expose the key through a client component or a NEXT_PUBLIC_ environment variable.
Copy link to headingCan I reuse existing Python functions as agent tools?
Yes. Wrap the application operation in an async function decorated with ai.tool, declare its input and output types, and register it with the agent. Keep validation and authorization inside the server-side operation, especially when replacing this read-only calculation with a tool that accesses or changes application data.