Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/pydantic-ai-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
.venv/
63 changes: 63 additions & 0 deletions examples/pydantic-ai-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Pydantic AI Bot

A Bot written in [Pydantic AI](https://ai.pydantic.dev), served over AG-UI. It sits beside the
[LangGraph](../langgraph-bot) and [Mastra](../mastra-bot) examples and proves the same point in a
third language: OpenBot knows a Bot only as an AG-UI endpoint URL, so a Python agent arrives exactly
the way a TypeScript one does.

The browser and file tools arrive in each run's `tools` from the surface. Pydantic AI exposes them
to the model as external tools whose calls stream back to OpenBot to run through the governed gateway
— so this process drives a real browser it has no direct access to, and the tool loop stays on the
client, the same as the Bot in the box.

## Run it

Requires Python 3.10+. With [uv](https://docs.astral.sh/uv):

```sh
cd examples/pydantic-ai-bot
uv run --env-file ../../.env src/app.py
```

Or with a plain virtualenv:

```sh
cd examples/pydantic-ai-bot
python -m venv .venv && . .venv/bin/activate
pip install -e .
OPENAI_API_KEY=... python src/app.py
```

It listens on `http://localhost:4202/ag-ui` (`PORT` to change) and answers `GET /health`.

| Variable | Default | Meaning |
| ---------------- | --------- | ---------------------------------------------------- |
| `OPENAI_API_KEY` | required | Read by Pydantic AI's OpenAI provider. |
| `BOT_MODEL` | `gpt-4.1` | Model the agent runs. Any tool-calling model. |
| `PORT` | `4202` | Port the AG-UI endpoint listens on. |

`OPENAI_BASE_URL` points the OpenAI provider at a compatible gateway, the same way the rest of the
deployment is configured (see [docs/configuration.md](../../docs/configuration.md)).

## Register it

Give a coworker this endpoint, either from `/agents` in the UI or as a `remote-ag-ui` agent in a
tenant package:

```yaml
agents:
- id: pydantic-analyst
name: Pydantic Analyst
title: Research
role_description: Research on a governed computer, written in Pydantic AI.
type: remote-ag-ui
endpoint: ${PYDANTIC_BOT_AG_UI_URL:-http://localhost:4202/ag-ui}
```

## Notes

- The AG-UI helpers live at `pydantic_ai.ui.ag_ui` in current releases and `pydantic_ai.ag_ui` in
earlier ones; `src/app.py` imports whichever is present. If your `pydantic-ai` predates AG-UI
support, upgrade it.
- Only tool-calling models can drive the computer. A model without tool calling will chat but never
open a page.
13 changes: 13 additions & 0 deletions examples/pydantic-ai-bot/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "openbot-example-pydantic-ai-bot"
version = "0.0.0"
description = "An example OpenBot Bot written in Pydantic AI, served over AG-UI."
requires-python = ">=3.10"
dependencies = [
"pydantic-ai[ag-ui]>=0.4",
"starlette>=0.37",
"uvicorn>=0.30",
]

[tool.uv]
package = false
75 changes: 75 additions & 0 deletions examples/pydantic-ai-bot/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
A Bot written in Pydantic AI.

Like the LangGraph and Mastra examples, this shares no OpenBot-specific code beyond the AG-UI
protocol. The browser and file tools arrive in each run's ``tools`` from the surface, and Pydantic AI
exposes them to the model as external tools whose calls stream back to OpenBot rather than executing
here. So this process drives a governed browser it has no direct access to.

Unlike those two, it is Python. OpenBot knows a Bot only as an AG-UI endpoint URL, so the language
and framework behind that URL are the deployment's business, not the surface's. This is the same
contract as ``agent-bot``, the LangGraph example, and the Mastra example, in a third language.
"""

import os

from pydantic_ai import Agent
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route

try: # pydantic-ai moved the AG-UI helpers under `ui` in later releases.
from pydantic_ai.ui.ag_ui import handle_ag_ui_request
except ImportError: # earlier layout
from pydantic_ai.ag_ui import handle_ag_ui_request

MODEL = os.environ.get("BOT_MODEL", "gpt-4.1")

# A real Pydantic AI agent with its own model client. It defines no tools of its own: the tools it
# may call arrive per run from the surface (see below), so this file never names `computer_navigate`
# and still drives a governed browser.
agent = Agent(
f"openai:{MODEL}",
instructions=(
"You are a Bot running on Pydantic AI inside OpenBot. You have a real web browser available "
"through the tools you are given.\n\n"
# Same guard as the LangGraph and Mastra examples: page contents require a fresh tool result.
"NEVER state what a page contains unless you have just read it with a tool in this "
"conversation. You cannot know a page's contents from memory, and a plausible guess is a "
"wrong answer. If you have not read it, call the tool first, and report exactly what the "
"tool returned."
),
)


async def ag_ui(request: Request) -> Response:
"""One POST carrying a ``RunAgentInput``, a stream of AG-UI events back.

Pydantic AI reads the run input, exposes ``input.tools`` to the model as external tools, runs the
agent, and streams AG-UI events as Server-Sent Events. The tool loop stays on the client, exactly
as it does for the Bot in the box: a tool call is emitted, this run ends, and OpenBot executes it
through the policy gateway before starting the next run with the result. That is why this file can
drive a browser it has no access to.
"""
return await handle_ag_ui_request(agent, request)


async def health(_: Request) -> Response:
return JSONResponse({"status": "ok", "framework": "pydantic-ai"})


app = Starlette(
routes=[
Route("/health", health),
Route("/ag-ui", ag_ui, methods=["POST"]),
],
)


if __name__ == "__main__":
import uvicorn

port = int(os.environ.get("PORT", "4202"))
print(f"pydantic-ai-bot listening on http://localhost:{port}/ag-ui (model {MODEL})")
uvicorn.run(app, host="0.0.0.0", port=port)