|
| 1 | +""" |
| 2 | +A Bot written in Pydantic AI. |
| 3 | +
|
| 4 | +Like the LangGraph and Mastra examples, this shares no OpenBot-specific code beyond the AG-UI |
| 5 | +protocol. The browser and file tools arrive in each run's ``tools`` from the surface, and Pydantic AI |
| 6 | +exposes them to the model as external tools whose calls stream back to OpenBot rather than executing |
| 7 | +here. So this process drives a governed browser it has no direct access to. |
| 8 | +
|
| 9 | +Unlike those two, it is Python. OpenBot knows a Bot only as an AG-UI endpoint URL, so the language |
| 10 | +and framework behind that URL are the deployment's business, not the surface's. This is the same |
| 11 | +contract as ``agent-bot``, the LangGraph example, and the Mastra example, in a third language. |
| 12 | +""" |
| 13 | + |
| 14 | +import os |
| 15 | + |
| 16 | +from pydantic_ai import Agent |
| 17 | +from pydantic_ai.ui.ag_ui import AGUIAdapter |
| 18 | +from starlette.applications import Starlette |
| 19 | +from starlette.requests import Request |
| 20 | +from starlette.responses import JSONResponse, Response |
| 21 | +from starlette.routing import Route |
| 22 | + |
| 23 | +MODEL = os.environ.get("BOT_MODEL", "gpt-5.5") |
| 24 | + |
| 25 | +# A real Pydantic AI agent with its own model client. It defines no tools of its own: the tools it |
| 26 | +# may call arrive per run from the surface (see below), so this file never names `computer_navigate` |
| 27 | +# and still drives a governed browser. |
| 28 | +agent = Agent( |
| 29 | + f"openai:{MODEL}", |
| 30 | + instructions=( |
| 31 | + "You are a Bot running on Pydantic AI inside OpenBot. You have a real web browser available " |
| 32 | + "through the tools you are given.\n\n" |
| 33 | + # Same guard as the LangGraph and Mastra examples: page contents require a fresh tool result. |
| 34 | + "NEVER state what a page contains unless you have just read it with a tool in this " |
| 35 | + "conversation. You cannot know a page's contents from memory, and a plausible guess is a " |
| 36 | + "wrong answer. If you have not read it, call the tool first, and report exactly what the " |
| 37 | + "tool returned." |
| 38 | + ), |
| 39 | +) |
| 40 | + |
| 41 | + |
| 42 | +async def ag_ui(request: Request) -> Response: |
| 43 | + """One POST carrying a ``RunAgentInput``, a stream of AG-UI events back. |
| 44 | +
|
| 45 | + Guarded by ``REQUIRE_KEY`` when it is set, the same as the LangGraph example: OpenBot sends the |
| 46 | + Bot's key on every run, and an endpoint that never reads the header accepts a run from anyone who |
| 47 | + can reach the port. Optional because a developer running this by hand has no key to send, and |
| 48 | + refusing them would teach nothing. |
| 49 | +
|
| 50 | + ``AGUIAdapter.dispatch_request`` reads the run input, exposes ``input.tools`` to the model as |
| 51 | + external (frontend) tools, runs the agent, and returns a streaming AG-UI Server-Sent-Events |
| 52 | + response. The tool loop stays on the client, exactly as it does for the Bot in the box: a tool |
| 53 | + call is emitted, this run ends, and OpenBot executes it through the policy gateway before starting |
| 54 | + the next run with the result. That is why this file can drive a browser it has no access to. |
| 55 | + """ |
| 56 | + required_key = os.environ.get("REQUIRE_KEY") |
| 57 | + if required_key and request.headers.get("authorization") != required_key: |
| 58 | + print("refused a run: wrong or missing key") |
| 59 | + return JSONResponse({"error": "Unauthorized"}, status_code=401) |
| 60 | + return await AGUIAdapter.dispatch_request(request, agent=agent) |
| 61 | + |
| 62 | + |
| 63 | +async def health(_: Request) -> Response: |
| 64 | + return JSONResponse({"status": "ok", "framework": "pydantic-ai"}) |
| 65 | + |
| 66 | + |
| 67 | +app = Starlette( |
| 68 | + routes=[ |
| 69 | + Route("/health", health), |
| 70 | + Route("/ag-ui", ag_ui, methods=["POST"]), |
| 71 | + ], |
| 72 | +) |
| 73 | + |
| 74 | + |
| 75 | +if __name__ == "__main__": |
| 76 | + import uvicorn |
| 77 | + |
| 78 | + port = int(os.environ.get("PORT", "4600")) |
| 79 | + print(f"pydantic-ai-bot listening on http://localhost:{port}/ag-ui (model {MODEL})") |
| 80 | + uvicorn.run(app, host="0.0.0.0", port=port) |
0 commit comments