Skip to content

Conversation

@dsfaccini
Copy link
Contributor

@dsfaccini dsfaccini commented Nov 17, 2025

web-based chat interface for Pydantic AI agents

  1. new module pydantic_ai.ui.web
  2. new method Agent.to_web()

fastapi

  • app = create_chat_app(agent)

  • the following endpoints come preconfigured:

    • GET / and /:id - serve the chat UI
    • POST /api/chat - Main chat endpoint using VercelAIAdapter
    • GET /api/configure - Returns available models and builtin tools
    • GET /api/health - Health check
    • NOTE: I'm counting on FastAPI to complain if the user tried adding conflicting routes, otherwise we could add a warning on the respective docs.

options and example

NOTE: the module for options is currently pydantic_ai.ui.web.

  • pre-configured model options:

    • anthropic:claude-sonnet-4-5
    • openai-responses:gpt-5
    • google-gla:gemini-2.5-pro
  • supported builtin tools:

    • web_search
    • code_execution
    • image_generation
# app.py
import logfire
from pydantic_ai import Agent

logfire.configure(send_to_logfire='if-token-present')
logfire.instrument_pydantic_ai()

agent = Agent('openai:gpt-5')

@agent.tool
def get_weather(city: str) -> str:
    return f"The weather in {city} is sunny"

app = agent.to_web()

logfire.instrument_fastapi(app, capture_headers=True)

# Run with: uvicorn app:app

testing

  • 7 tests in tests/test_ui_web.py

notes

  • UI is served from CDN: @pydantic/[email protected]
  • Uses Vercel AI protocol for chat streaming
  • TODO: add clai web command to launch from the CLI (as in uvx pydantic-work without the whole URL magic)
  • TODO: should I add a new doc at docs/ui/to_web.md? I'd also reference this in docs/ui/overview.md and docs/agents.md

EDIT: if you try it out it's worth noting that the current hosted UI doesn't handle ErrorChunks, so you will get no spinner and no response when there's a model-level error and fastapi will return a 200 any way.
This will happen for instance when you use a model for which you don't have a valid API key in your environment
I opened a PR for the error chunks here pydantic/ai-chat-ui#4.

Closes #3295

args = parser.parse_args(args_list)

# Handle web subcommand
if args.command == 'web':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe it should be --web so it doesn't conflict with the prompt arg?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeahp, changed that now


self._get_toolset().apply(_set_sampling_model)

def to_web(self) -> Any:
Copy link
Collaborator

Choose a reason for hiding this comment

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

We're gonna need some args here -- have a look at the to_a2a and to_ag_ui methods. Not saying we need all of those args, but some may be useful

@dsfaccini
Copy link
Contributor Author

I just pushed an update to this removing the AST aspect and (hopefully) fixing the tests so they pass in CI

haven't addressed the comments yet so it isn't reviewable yet

args = parser.parse_args(args_list)

# Handle web subcommand
if args.command == 'web':
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeahp, changed that now


@app.get('/')
@app.get('/{id}')
async def index(request: Request, version: str | None = Query(None)): # pyright: ignore[reportUnusedFunction]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I understand the need for a version arg. An older version than the default is worse, and a newer version may not work with the API data model. I think they should develop in tandem, with a pinned version on this side.

What we could do is add a frontend_url argument to the to_web method to allow the entire thing to be overridden easily?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

frontend_url remains relevant, I haven't included logic for this

Comment on lines +95 to +101
with TestClient(app) as client:
response = client.get('/')
assert response.status_code == 200
assert response.headers['content-type'] == 'text/html; charset=utf-8'
assert 'cache-control' in response.headers
assert response.headers['cache-control'] == 'public, max-age=3600'
assert len(response.content) > 0
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this can stay as assertions right?

@dsfaccini dsfaccini requested a review from DouweM November 27, 2025 16:17
model_id = f'{model.system}:{model.model_name}'
display_name = label or format_model_display_name(model.model_name)
model_supported_tools = model.supported_builtin_tools()
supported_tool_ids = list(model_supported_tools & builtin_tool_ids)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I suggested putting this in Model.profile so that we can always trust model.profile.supported_builtin_tools. Then we can also override it on OpenAIChatModel to remove web_search if appropriate

@dsfaccini
Copy link
Contributor Author

dsfaccini commented Nov 29, 2025

been having some fun testing the combinations, for anyone who wants to try them:

combinations to try out the agent UI

1. Generic agent with explicit model

source .env && uv run --with . clai web -m openai:gpt-4.1-mini

2. Agent with model (uses agent's configured model)

source .env && uv run --with . clai web -a clai.clai._test_agents:chat_agent

3. Agent without model + CLI model

source .env && uv run --with . clai web -a clai.clai._test_agents:agent_no_model -m anthropic:claude-haiku-4-5

4. Multiple models (first is default, others are options in UI)

source .env && uv run --with . clai web -m google-gla:gemini-2.5-flash-lite -m openai:gpt-4.1-mini -m anthropic:claude-haiku-4-5

5. Override agent's model with different one

source .env && uv run --with . clai web -a clai.clai._test_agents:chat_agent -m google-gla:gemini-2.5-flash-lite

6. Single tool - web search enabled

source .env && uv run --with . clai web -m openai:gpt-4.1-mini -t web_search

7. Multiple tools

source .env && uv run --with . clai web -m anthropic:claude-haiku-4-5 -t web_search -t code_execution

8. Agent with builtin_tools configured

source .env && uv run --with . clai web -a clai.clai._test_agents:agent_with_tools

9. Roleplay waiter instructions

source .env && uv run --with . clai web -m openai:gpt-4.1-mini -i "You're a grumpy Parisian waiter at a trendy bistro frequented by American tourists. You're secretly proud of the food but act annoyed by every question. Pepper your
responses with French words and sighs."

10. With MCP config

source .env && uv run --with . clai web -m google-gla:gemini-2.5-flash-lite --mcp mcp_servers.json

11. ERROR: No model, no agent (should error)

source .env && uv run --with . clai web

12. ERROR: Agent without model, no CLI model (should error)

source .env && uv run --with . clai web -a clai.clai._test_agents:agent_no_model

13. WARNING: Unknown tool (should warn)

source .env && uv run --with . clai web -m openai:gpt-4.1-mini -t definitely_not_a_real_tool

Some fun alternative instructions you could swap in for #9:

Pirate customer service

source .env && uv run --with . clai web -m anthropic:claude-haiku-4-5 -i "You're a pirate who somehow ended up working tech support. Answer questions helpfully but can't stop using nautical terms and saying 'arrr'."

Overly enthusiastic fitness coach

source .env && uv run --with . clai web -m google-gla:gemini-2.5-flash-lite -i "You're an extremely enthusiastic fitness coach who relates EVERYTHING back to exercise and healthy living. Even coding questions get workout analogies."

Noir detective

source .env && uv run --with . clai web -m openai:gpt-4.1-mini -i "You're a 1940s noir detective narrating your investigation. Every question is a 'case' and every answer is delivered in hard-boiled prose with lots of rain metaphors."

.env.example

export ANTHROPIC_API_KEY=""
export OPENAI_API_KEY=""
export GOOGLE_API_KEY=""

When we publish it should naturally just run as uvx clai web ..., we could support a --url to fetch the agent code from, that would make it easier to publish examples. Though that sounds a bit dangerous as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web chat interface for any agent

2 participants