Skip to content
Ian Cunningham monogram Ian Cunningham AI systems builder

Blog

Pydantic AI and CopilotKit: A Minimal Full-Stack Agent Demo

A practical walkthrough for connecting a Pydantic AI agent to a CopilotKit frontend with FastAPI and AG-UI.

Pydantic AI and CopilotKit: A Minimal Full-Stack Agent Demo
KT

Article summary

Key Takeaways

  1. Pydantic AI and CopilotKit are becoming a compelling pair

    Pydantic AI gives Python developers a clean way to define agents, while CopilotKit gives frontend teams a practical assistant UI layer.

  2. AG-UI gives the backend and frontend a shared protocol

    Instead of inventing your own agent transport, the FastAPI backend and CopilotKit runtime can meet through AG-UI.

  3. A small demo can still teach production-shaped patterns

    The example keeps the agent simple, but separates backend routes, agent code, runtime configuration, and frontend assistant pages.

  4. This foundation can grow into richer agent interfaces

    Once the plumbing is in place, you can add tools, stronger instructions, richer UI components, Pydantic Graph flows, and more realistic workflows.

I’ve been spending more time exploring both Pydantic AI and CopilotKit.

What interests me is that they solve different parts of the same problem.

Pydantic AI gives Python developers a clean, Python-first way to define agents. CopilotKit focuses on something equally important: putting useful agent interfaces in front of users.

AG-UI is the protocol layer that makes this pairing particularly interesting.

This article walks through a minimal full-stack setup for connecting a Pydantic AI agent to a CopilotKit UI.

The stack looks like this:

  • FastAPI for the backend API
  • Pydantic AI for the agent itself
  • AG-UI as the shared protocol layer
  • Next.js for the frontend
  • CopilotKit for the assistant UI

The goal is not to build a sophisticated production assistant yet.

It’s to establish a clean foundation that can be expanded later.

Why this pairing is interesting

Agent logic in Python. Assistant UI in React. AG-UI in the middle.

I like this combination because it keeps the responsibilities easy to reason about. Pydantic AI owns the backend agent behavior, FastAPI exposes the agent over HTTP, AG-UI defines the interaction shape, and CopilotKit gives the browser a ready-made assistant surface.

For a small demo, this may feel unnecessary. For larger systems, it quickly starts paying for itself.

A robot having an epiphany

How the pieces fit together

Before getting into files and commands, it helps to name the request flow.

At a high level, the request flow looks like this:

  • The browser talks to the Next.js app.
  • Next.js exposes a CopilotKit API route.
  • The CopilotKit runtime forwards agent requests to the FastAPI backend.
  • FastAPI exposes a Pydantic AI agent through the AG-UI adapter.
  • The frontend uses a CopilotKit component to interact with the registered agent.

This separation becomes surprisingly valuable later.

It means the agent can stay in Python, the interface can stay in React, and the protocol between them can stay explicit.

That is the architectural boundary this repo is trying to demonstrate.

Code snapshot

The GitHub repository for this project is:

The repository can keep evolving after this article is published, so the exact code for this article is preserved separately:

  • Branch: article/001-pydantic-ai-copilotkit
  • Tag: article-001

To check out the exact version used by this article:

git fetch --all --tags
git checkout article-001

Use the branch if you want to inspect or compare the article version by name:

git checkout article/001-pydantic-ai-copilotkit

The main branch may contain newer work that builds on this foundation.

Prerequisites

  • Python 3.10+
  • uv, unless you prefer another Python package manager
  • Node.js and pnpm
  • An API key for a model provider. This article uses OpenAI, but you can adapt the setup for another provider.

Create your project

All terminal commands below are written for Linux or macOS shells. Windows PowerShell users may need to adjust the commands slightly.

Open a terminal in the directory where you want to create the project and run:

uv init pydantic-ai-copilotkit-demo

Change into the new project:

cd pydantic-ai-copilotkit-demo

Sync the environment:

uv sync

Activate the virtual environment:

source .venv/bin/activate

VS Code

If you’re using VS Code, select the virtual environment you just created:

  1. Press Ctrl+Shift+P.
  2. Select Python: Select Interpreter.
  3. Select the interpreter from this project, usually marked as recommended.

Git ignore

If you plan to commit this project to Git, create a .gitignore before your first commit. At minimum, include:

.env
.venv
__pycache__/

Do this before committing so secrets and generated files do not end up in source control.

Backend setup

Install the minimum Python packages needed for this demo:

uv add python-dotenv fastapi uvicorn "pydantic-ai-slim[ag-ui,openai]"

If you’re not using OpenAI, replace the openai extra with the provider you want to use. See Pydantic AI’s slim install documentation for available extras.

I like keeping agents and API routers in separate directories because real projects often grow past a single file.

From the project root, create the backend folders:

mkdir -p backend/agents backend/routers

The structure should now include:

backend/
  agents/
  routers/

Create backend/.env:

OPENAI_API_KEY=<your_openai_api_key_here>

Do not commit this file to GitHub.

Agent creation

Create backend/agents/simple_agent.py:

from pydantic_ai import Agent

MODEL = "openai:gpt-4.1-mini"
INSTRUCTIONS = "Be extroverted!"

agent = Agent(model=MODEL, instructions=INSTRUCTIONS)

if __name__ == "__main__":
    import asyncio

    result = asyncio.run(agent.run("What is your name?"))
    print(result.response)

For a real agent, the instructions will usually be longer. I recommend moving them into a separate file once they become more than a few lines. In production systems, model names and provider settings also belong in configuration rather than directly inside the agent module.

From the backend directory, run:

uv run --env-file .env python agents/simple_agent.py

If your OpenAI API key is valid, you should see a model response in the terminal.

FastAPI AG-UI endpoint

Next, expose the agent through a FastAPI endpoint that speaks AG-UI.

Architecture

The pieces stay intentionally small

FastAPI backend

Hosts health checks and exposes the Pydantic AI agent through a normal HTTP route.

AG-UI protocol

Gives the backend agent and frontend runtime a shared request and response shape.

CopilotKit frontend

Provides the assistant UI and forwards browser requests through the runtime route.

You could put this directly in main.py, but keeping it in a router makes the project easier to extend later.

The split is modest:

  • simple_agent.py owns the Pydantic AI agent.
  • agent_router.py owns the HTTP route that exposes it.
  • main.py owns the FastAPI app and router wiring.

Create backend/routers/agent_router.py:

from fastapi import APIRouter, Request, Response
from pydantic_ai.ui.ag_ui import AGUIAdapter

from agents.simple_agent import agent as simple_agent

router = APIRouter()


@router.get("/")
async def root():
    return {"message": "Hello, I'm the agent API!"}


@router.get("/health")
async def health():
    return {
        "api": "agent",
        "status": "healthy",
    }


@router.post("/simple-agent")
async def simple_agent_endpoint(request: Request) -> Response:
    """AG-UI endpoint for the simple Pydantic AI agent."""

    return await AGUIAdapter.dispatch_request(request, agent=simple_agent)

Create backend/main.py:

from dotenv import load_dotenv

load_dotenv()

from fastapi import FastAPI

from routers.agent_router import router as agent_router

app = FastAPI()
app.include_router(agent_router, prefix="/agent")


@app.get("/")
async def root():
    return {"message": "Hello, I'm the main API!"}


@app.get("/health")
async def health():
    return {
        "api": "main",
        "status": "healthy",
    }

From the backend directory, run:

uv run uvicorn main:app --reload

Check these URLs in your browser:

  • http://127.0.0.1:8000/
  • http://127.0.0.1:8000/health
  • http://127.0.0.1:8000/agent
  • http://127.0.0.1:8000/agent/health

The AG-UI endpoint itself is POST http://127.0.0.1:8000/agent/simple-agent, so you will call it through CopilotKit rather than opening it directly in the browser.

Next.js UI

This section assumes you have pnpm installed.

Open a new terminal in the project root and run:

pnpm create next-app@latest

Use ui as the project name and accept the recommended defaults.

Change into the UI project:

cd ui

Add the CopilotKit and AG-UI dependencies:

pnpm add @copilotkit/react-ui @copilotkit/react-core @copilotkit/runtime @ag-ui/client openai

Run Next.js:

pnpm dev

Navigate to http://localhost:3000.

For a small local demo, hardcoding http://localhost:8000/agent/simple-agent in the API route works. I prefer a tiny configuration layer because the backend URL usually changes between local development, preview deployments, and production.

It is a little more setup now, but it avoids mixing deployment details into the runtime route.

Create ui/.env.example:

COPILOTKIT_AGENT_BASE_URL=http://localhost:8000/agent/
COPILOTKIT_SIMPLE_AGENT_NAME=simple-agent

For local development, copy that to ui/.env.local:

cp .env.example .env.local

These variables do not need the NEXT_PUBLIC_ prefix because they are only read by the server-side API route.

Create ui/lib/copilot-agent-ids.ts:

export const SIMPLE_AGENT_ID = "simpleAgent";

Create ui/lib/copilot-agent-config.ts:

const DEFAULT_AGENT_BASE_URL = "http://localhost:8000/agent/";
const DEFAULT_SIMPLE_AGENT_NAME = "simple-agent";

function trimSlashes(value: string) {
  return value.replace(/^\/+|\/+$/g, "");
}

function withTrailingSlash(value: string) {
  return value.endsWith("/") ? value : `${value}/`;
}

export function getSimpleAgentUrl() {
  const agentBaseUrl =
    process.env.COPILOTKIT_AGENT_BASE_URL ?? DEFAULT_AGENT_BASE_URL;
  const simpleAgentName =
    process.env.COPILOTKIT_SIMPLE_AGENT_NAME ?? DEFAULT_SIMPLE_AGENT_NAME;

  return new URL(trimSlashes(simpleAgentName), withTrailingSlash(agentBaseUrl))
    .toString();
}

This keeps three related values distinct:

  • COPILOTKIT_AGENT_BASE_URL is the FastAPI router base URL.
  • COPILOTKIT_SIMPLE_AGENT_NAME is the backend route path for this agent.
  • SIMPLE_AGENT_ID is the CopilotKit runtime id used by the frontend.

Keeping SIMPLE_AGENT_ID in its own file lets client components import the id without importing server-side environment configuration.

CopilotKit runtime route

Create ui/app/api/copilotkit/route.ts:

import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
import { NextRequest } from "next/server";
import { getSimpleAgentUrl } from "@/lib/copilot-agent-config";
import { SIMPLE_AGENT_ID } from "@/lib/copilot-agent-ids";

const serviceAdapter = new ExperimentalEmptyAdapter();

const runtime = new CopilotRuntime({
  agents: {
    [SIMPLE_AGENT_ID]: new HttpAgent({ url: getSimpleAgentUrl() }),
  },
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

This route does two important things:

  • It lets the browser talk to CopilotKit’s runtime at /api/copilotkit.
  • It registers the FastAPI AG-UI endpoint as the simpleAgent agent.

From the frontend’s point of view, it only needs to know the CopilotKit runtime URL and the agent id.

Assistants route group

This structure is optional, but it keeps CopilotKit-specific pages and layout code separate from the rest of the app.

Create the route group folder:

mkdir -p "ui/app/(assistants)/simple-agent"

Next.js route groups do not affect the URL path, so a page at ui/app/(assistants)/simple-agent/page.tsx is served at /simple-agent, not /assistants/simple-agent.

Create ui/app/(assistants)/layout.tsx:

import {
  CopilotKitProvider,
} from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";

export default function AssistantsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <CopilotKitProvider runtimeUrl="/api/copilotkit">
      {children}
    </CopilotKitProvider>
  );
}

Create ui/app/(assistants)/simple-agent/page.tsx:

"use client";

import { CopilotSidebar } from "@copilotkit/react-core/v2";
import { SIMPLE_AGENT_ID } from "@/lib/copilot-agent-ids";

export default function SimpleAgentPage() {
  return (
    <div className="flex h-full min-h-0 flex-col gap-4 px-4 pb-6">
      <CopilotSidebar agentId={SIMPLE_AGENT_ID} />
    </div>
  );
}

The agentId must match the key you registered in CopilotRuntime. Importing SIMPLE_AGENT_ID in both places prevents those values from drifting apart.

For this demo, that may seem like a tiny detail.

Once you have multiple agents or multiple assistant screens, it becomes one of those small decisions that keeps the project easier to change.

Try it

With both servers running, navigate to:

http://localhost:3000/simple-agent

You should now have a CopilotKit sidebar connected to the Pydantic AI agent running behind FastAPI.

A robot answering a question

Next steps

From Minimal Demo To Useful Assistant

This is not the end goal. It is the point where the backend, protocol layer, frontend runtime, and assistant UI are all talking to each other.

Once that foundation works, the interesting work can begin: tools, richer UI, better instructions, graph-based workflows, and agent behavior that starts to feel genuinely useful.

Conclusion

This gives you a minimal assistant built with Pydantic AI, FastAPI, AG-UI, and CopilotKit.

It does not do much yet.

That is exactly the point.

The useful part is the foundation:

  • A Python agent running behind FastAPI
  • An AG-UI endpoint that CopilotKit can call
  • A Next.js runtime route that registers the agent
  • A CopilotKit UI component that can interact with it

From here, the next steps are the parts that make the assistant more interesting:

  • adding tools
  • improving instructions
  • building richer CopilotKit UI components
  • experimenting with Pydantic Graph
  • shaping the demo into more production-shaped agent workflows

Work with Ian

Need a workflow, pipeline, or copilot built for a real operational use case?

If this post aligns with what you are building, I can help scope the implementation and turn the concept into a production-ready system.