Skip to content

Add a Pydantic AI example Bot - #56

Merged
davidmckayv merged 3 commits into
CopilotKit:mainfrom
andreolf:feat/pydantic-ai-example
Aug 24, 2026
Merged

Add a Pydantic AI example Bot#56
davidmckayv merged 3 commits into
CopilotKit:mainfrom
andreolf:feat/pydantic-ai-example

Conversation

@andreolf

Copy link
Copy Markdown
Contributor

What this changes

A third framework example beside examples/langgraph-bot and examples/mastra-bot, and the first in another language. A real Pydantic AI agent served over AG-UI: the surface's tools arrive per run in RunAgentInput.tools, Pydantic AI exposes them to the model as external tools, and their calls stream back to OpenBot to run through the governed gateway — so the process drives a browser it has no direct access to, the same contract as the Bot in the box and the two existing examples.

Worth doing because the README names Pydantic AI among the frameworks a Bot can be written in, and the strongest evidence that OpenBot is framework- and language-agnostic is a working example in a language that is not TypeScript.

Self-contained: its own pyproject.toml outside the Bun workspaces, so it does not touch the JS build. It imports the AG-UI helper from whichever module path the installed pydantic-ai exposes (pydantic_ai.ui.ag_ui in current releases, pydantic_ai.ag_ui in earlier ones) and answers /health like the other examples.

Where it runs

A customer's own agent process, exactly like langgraph-bot and mastra-bot — reached by OpenBot only as an AG-UI endpoint URL. It holds no OpenBot state; the tool loop stays on the OpenBot client.

  • New state that outlives a request? None. The agent is stateless between runs; conversation history arrives in each RunAgentInput.
  • What happens on the second replica? This is an example Bot, not an OpenBot replica. Multiple instances behind one endpoint are independent, as with the other example Bots.
  • Anything serialised? None.
  • Anything fanned out to a browser? No. It emits AG-UI events on its own response stream; it never touches an OpenBot socket.
  • New listener, port, or schedule? A dev server on PORT (default 4202), run by the developer the same way the other examples are; not part of the deployed ingress.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Yes — this Bot executes no tools; it emits tool calls and OpenBot runs them through the gateway, identical to the existing examples.
  • New refusals and new failures each write a row. N/A — no acting code.
  • Nothing new is trusted from the client that the server can resolve itself. N/A.

Proof

$ python3 -m py_compile examples/pydantic-ai-bot/src/app.py   # compiles

Note for reviewers: I have not yet run this against a live model end-to-end. If you'd prefer the hand-emitted @ag-ui/encoder-style SSE the TS examples use over Pydantic AI's native AG-UI app, say so and I'll switch it.

@andreolf

Copy link
Copy Markdown
Contributor Author

Update: pushed a fix and verified the example end-to-end locally.

The first version imported handle_ag_ui_request (from pydantic_ai.ui.ag_ui, falling back to pydantic_ai.ag_ui). Neither exists in current pydantic-ai — the shipping API is AGUIAdapter, and there is no top-level ag_ui module — so the Bot would have failed at import before serving a request. It now serves with AGUIAdapter.dispatch_request(request, agent=agent).

Verified against pydantic-ai 2.33.0 in a fresh venv (Python 3.14):

  • GET /health{"status":"ok","framework":"pydantic-ai"}
  • POST /ag-ui with a RunAgentInput granting a computer_navigate frontend tool streams valid AG-UI SSE: RUN_STARTED, then the model call. With a placeholder key the stream ends in RUN_ERROR (401) — i.e. input parsing, frontend-tool registration, and AG-UI event emission all work; only the final model auth is unexercised. With a real OPENAI_API_KEY it runs the model and emits TEXT_MESSAGE_* / TOOL_CALL_*.

README note corrected and the dependency pinned to the tested release (pydantic-ai[ag-ui]>=2.33).

One open question for maintainers: happy to keep this on Pydantic AI's native AG-UI adapter, or switch to hand-emitted @ag-ui/encoder-style SSE to match the LangGraph and Mastra examples more closely — whichever you prefer.

@davidmckayv

Copy link
Copy Markdown
Contributor

Keeping this open — it's the only Python example and the direct answer to an enterprise asking whether it can bring its own Python agent, and the earlier import bug is fixed (AGUIAdapter.dispatch_request resolves in pydantic-ai-slim 2.33.0, and the tool loop stays with OpenBot as claimed). One thing must change before it can ship, because customers copy examples as starting points: it binds 0.0.0.0 and accepts any POST while holding a model key, whereas main's agent-bot and agent-langgraph both exit at startup without MANAGED_AGENT_TOKEN and reject /ag-ui calls missing x-openbot-agent-token. Mirror that (about ten lines). Two smaller ones while you're there: it hardcodes the openai: provider where the repo reads BOT_PROVIDER, and defaults BOT_MODEL to gpt-4.1 where the rest uses gpt-5.5. With the auth check in, it's a merge.

andreolf and others added 3 commits August 24, 2026 09:43
A third framework example beside LangGraph and Mastra, and the first in another
language. A real Pydantic AI agent served over AG-UI: the surface's tools arrive
per run and their calls stream back to OpenBot to run through the gateway, so the
process drives a governed browser it has no direct access to, the same contract
as the Bot in the box.

Self-contained (its own pyproject.toml, outside the Bun workspaces), imports the
AG-UI helper from whichever module path the installed pydantic-ai exposes, and
answers /health like the other examples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first version imported handle_ag_ui_request from pydantic_ai.ui.ag_ui with
a fallback to pydantic_ai.ag_ui. Neither exists in current pydantic-ai: the
module exposes AGUIAdapter, and there is no top-level ag_ui module, so the Bot
failed at import before serving a single request.

Serve with AGUIAdapter.dispatch_request(request, agent=agent) instead, verified
against pydantic-ai 2.33.0: the server boots, /health answers, and a RunAgentInput
POST with a granted tool streams RUN_STARTED then the model call (frontend tools
accepted, AG-UI events emitted as SSE). Pin the dependency to the tested release
and correct the README note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…efault

The endpoint read no header, which is the one thing an example must not
teach now that the server sends a Bot's key on every run: it takes a run
from anyone who can reach the port. Guarded by REQUIRE_KEY, the same
optional guard the LangGraph example carries.

4202 sits in the band the in-box agents use, 4200 and 4201. The examples
step by a hundred, 4300 and 4400, and 4500 is the supervisor, so 4600.

gpt-4.1 is not what this deployment ships; gpt-5.5 is the default
everywhere else. Also record that Pydantic AI calls /responses rather
than /v1/chat/completions, which decides whether a given gateway can
serve this at all, and ignore the egg-info that the README's own install
step leaves behind.
@davidmckayv
davidmckayv force-pushed the feat/pydantic-ai-example branch from 4595dd1 to a4be71d Compare August 24, 2026 16:46

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging. The example is right about the contract and I have now driven it, which the PR notes it had not been.

Started LLMock as the model, pointed the example at it, and posted a real RunAgentInput carrying one browser tool the file never names:

  • the surface's tool reached the model as an external tool: modelWasOffered: ["computer_navigate"];
  • it came back as RUN_STARTEDTOOL_CALL_STARTTOOL_CALL_ARGS ({"url":"https://example.com"}) → TOOL_CALL_ENDRUN_FINISHED, and the Python process executed nothing;
  • GET /health answers {"status":"ok","framework":"pydantic-ai"}.

Verified against pydantic-ai 2.33.0. pydantic_ai.ui.ag_ui.AGUIAdapter.dispatch_request exists with the signature this calls, so the single import is correct. The PR body describes a fallback import for older module paths that the code does not have, and should not: an import that silently accepts an older API is worse than one that fails.

Four things fixed in a4be71d:

  • No key check. The endpoint read no header, so it took a run from anyone who could reach the port. That is the one thing an example must not teach now that the server sends a Bot's key on every run. Added the same optional REQUIRE_KEY guard the LangGraph example carries. Driven: 401 with no header, 401 with a wrong one, 200 with the right one.
  • Port. 4202 sits in the band the in-box agents use (4200, 4201). The examples step by a hundred, 4300 and 4400, and 4500 is the supervisor, so 4600.
  • gpt-4.1. The shipped default everywhere else is gpt-5.5.
  • egg-info. The README's own pip install -e . leaves a directory the .gitignore missed.

One thing worth knowing beyond this PR, now recorded in the README: Pydantic AI calls /responses, not /v1/chat/completions. So a gateway serving only chat completions cannot answer this example, and the inverse of the limit the two TypeScript Bots carry, which is that they speak chat completions and so cannot use the models that require Responses.

Rebased onto main, which it was 112 commits behind. format:check and lint clean.

@davidmckayv
davidmckayv merged commit 574b4cd into CopilotKit:main Aug 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants