Skip to content

Commit 1d873e7

Browse files
dsfaccinipetyosiclaude
authored
Use pydantic-ai's new to_web method (#8)
Co-authored-by: Petyo Ivanov <underlog@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d15592d commit 1d873e7

File tree

7 files changed

+1237
-1235
lines changed

7 files changed

+1237
-1235
lines changed

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ Example React frontend for Pydantic AI Chat using [Vercel AI Elements](https://v
55
## Dev
66

77
```sh
8-
npm install
9-
npm run dev
10-
11-
# stop your logfire platform, to avoid port 8000 conflicts
12-
13-
cd agent && uv run uvicorn chatbot.server:app
8+
pnpm install
9+
pnpm run dev:server # start the python example backend
10+
pnpm run dev
1411
```

agent/chatbot/agent.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@
22

33
import pydantic_ai
44

5-
import logfire
6-
from chatbot.data import get_docs_dir, get_markdown, get_table_of_contents
5+
from chatbot.data import Repo, get_docs_dir, get_markdown, get_table_of_contents
76
from chatbot.db import open_populated_table
87

9-
from chatbot.data import Repo
10-
11-
logfire.configure(send_to_logfire='if-token-present', console=False)
12-
logfire.instrument_pydantic_ai()
13-
148

159
agent = pydantic_ai.Agent(
16-
'anthropic:claude-sonnet-4-0',
1710
instructions="Help the user answer questions about two products ('repos'): Pydantic AI (pydantic-ai), an open source agent framework library, and Pydantic Logfire (logfire), an observability platform. Start by using the `search_docs` tool to search the relevant documentation and answer the question based on the search results. It uses a hybrid of semantic and keyword search, so writing either keywords or sentences may work. It's not searching google. Each search result starts with a path to a .md file. The file `foo/bar.md` corresponds to the URL `https://ai.pydantic.dev/foo/bar/` for Pydantic AI, `https://logfire.pydantic.dev/docs/foo/bar/` for Logfire. Include the URLs in your answer. The search results may not return complete files, or may not return the files you need. If they don't have what you need, you can use the `get_docs_file` tool. You probably only need to search once or twice, definitely not more than 3 times. The user doesn't see the search results, you need to actually return a summary of the info. To see the files that exist for the `get_docs_file` tool, along with a preview of the sections within, use the `get_table_of_contents` tool.",
1811
)
1912

agent/chatbot/server.py

Lines changed: 13 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,28 @@
11
from __future__ import annotations as _annotations
22

3-
from typing import Literal
4-
5-
import fastapi
6-
import httpx
73
import logfire
8-
from fastapi import Request, Response
9-
from fastapi.responses import HTMLResponse
10-
from pydantic import BaseModel
11-
from pydantic.alias_generators import to_camel
124
from pydantic_ai.builtin_tools import (
13-
AbstractBuiltinTool,
145
CodeExecutionTool,
156
ImageGenerationTool,
167
WebSearchTool,
178
)
18-
from pydantic_ai.ui.vercel_ai import VercelAIAdapter
199

2010
from .agent import agent
2111

2212
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
2313
logfire.configure(send_to_logfire='if-token-present')
2414
logfire.instrument_pydantic_ai()
2515

26-
app = fastapi.FastAPI()
27-
logfire.instrument_fastapi(app)
28-
29-
30-
@app.options('/api/chat')
31-
def options_chat():
32-
pass
33-
34-
35-
AIModelID = Literal[
36-
'anthropic:claude-sonnet-4-5',
37-
'openai-responses:gpt-5',
38-
'google-gla:gemini-2.5-pro',
39-
]
40-
BuiltinToolID = Literal['web_search', 'image_generation', 'code_execution']
41-
42-
43-
class AIModel(BaseModel):
44-
id: AIModelID
45-
name: str
46-
builtin_tools: list[BuiltinToolID]
47-
48-
49-
class BuiltinTool(BaseModel):
50-
id: BuiltinToolID
51-
name: str
52-
53-
54-
BUILTIN_TOOL_DEFS: list[BuiltinTool] = [
55-
BuiltinTool(id='web_search', name='Web Search'),
56-
BuiltinTool(id='code_execution', name='Code Execution'),
57-
BuiltinTool(id='image_generation', name='Image Generation'),
58-
]
59-
60-
BUILTIN_TOOLS: dict[BuiltinToolID, AbstractBuiltinTool] = {
61-
'web_search': WebSearchTool(),
62-
'code_execution': CodeExecutionTool(),
63-
'image_generation': ImageGenerationTool(),
64-
}
65-
66-
AI_MODELS: list[AIModel] = [
67-
AIModel(
68-
id='anthropic:claude-sonnet-4-5',
69-
name='Claude Sonnet 4.5',
70-
builtin_tools=[
71-
'web_search',
72-
'code_execution',
73-
],
74-
),
75-
AIModel(
76-
id='openai-responses:gpt-5',
77-
name='GPT 5',
78-
builtin_tools=[
79-
'web_search',
80-
'code_execution',
81-
'image_generation',
82-
],
83-
),
84-
AIModel(
85-
id='google-gla:gemini-2.5-pro',
86-
name='Gemini 2.5 Pro',
87-
builtin_tools=[
88-
'web_search',
89-
'code_execution',
90-
],
91-
),
92-
]
93-
94-
95-
class ConfigureFrontend(BaseModel, alias_generator=to_camel, populate_by_name=True):
96-
models: list[AIModel]
97-
builtin_tools: list[BuiltinTool]
98-
99-
100-
@app.get('/api/configure')
101-
async def configure_frontend() -> ConfigureFrontend:
102-
return ConfigureFrontend(
103-
models=AI_MODELS,
104-
builtin_tools=BUILTIN_TOOL_DEFS,
105-
)
106-
107-
108-
class ChatRequestExtra(BaseModel, extra='ignore', alias_generator=to_camel):
109-
model: AIModelID | None = None
110-
builtin_tools: list[BuiltinToolID] = []
111-
112-
113-
@app.post('/api/chat')
114-
async def post_chat(request: Request) -> Response:
115-
run_input = VercelAIAdapter.build_run_input(await request.body())
116-
extra_data = ChatRequestExtra.model_validate(run_input.__pydantic_extra__)
117-
return await VercelAIAdapter.dispatch_request(
118-
request,
119-
agent=agent,
120-
model=extra_data.model,
121-
builtin_tools=[BUILTIN_TOOLS[tool_id] for tool_id in extra_data.builtin_tools],
122-
)
123-
124-
125-
@app.get('/')
126-
@app.get('/{id}')
127-
async def index(request: Request):
128-
async with httpx.AsyncClient() as client:
129-
response = await client.get(
130-
'https://cdn.jsdelivr.net/npm/@pydantic/ai-chat-ui@0.0.2/dist/index.html'
131-
)
132-
return HTMLResponse(content=response.content, status_code=response.status_code)
16+
app = agent.to_web(
17+
models={
18+
'Claude Sonnet 4.5': 'anthropic:claude-sonnet-4-5',
19+
'GPT 5': 'openai-responses:gpt-5',
20+
'Gemini 2.5 Pro': 'google-gla:gemini-2.5-pro',
21+
},
22+
builtin_tools=[
23+
WebSearchTool(),
24+
CodeExecutionTool(),
25+
ImageGenerationTool(),
26+
],
27+
)
28+
logfire.instrument_starlette(app)

agent/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ dependencies = [
77
"fastapi>=0.117.1",
88
"lancedb>=0.25.0",
99
"langchain-text-splitters>=0.3.11",
10-
"logfire[fastapi]>=4.9.0",
10+
"logfire[fastapi,starlette]>=4.9.0",
1111
"markdown2>=2.5.4",
1212
"pip>=25.2",
13-
"pydantic-ai-slim[anthropic,openai,google,cli]>=1.14.0",
13+
"pydantic-ai-slim[anthropic,cli,google,openai]>=1.14.0",
1414
"pyright>=1.1.405",
1515
"python-frontmatter>=1.1.0",
1616
"sentence-transformers>=5.1.1",

0 commit comments

Comments
 (0)