Files
David Sanchez d19bd0f81c docs(pydantic-ai): port integration docs and demos to Pydantic AI v2
Agent.to_ag_ui(), AGUIApp and the pydantic_ai.ag_ui module were removed in
Pydantic AI v2. The docs installed pydantic-ai unpinned, so anyone following
the quickstart got 2.22.0 and failed first at dependency resolution
(starlette==0.45.3 conflicts with the >=0.46.2 the ag-ui extra needs) and then
at AttributeError.

- 8 doc pages under showcase/shell-docs .../integrations/pydantic-ai serve the
  agent from a Starlette route via AGUIAdapter.dispatch_request
- StateDeps moves from pydantic_ai.ag_ui to pydantic_ai.ui
- stateful snippets build StateDeps per request; dispatch_request writes the
  client's state into deps.state, so a shared instance leaks state between users
- install commands exact-pin pydantic-ai-slim==2.22.0 and ag-ui-protocol==0.1.19
- examples/canvas/pydantic-ai and examples/showcases/pydantic-ai-todos ported
  and pinned, todos relocked
- skills/copilotkit-integrations reference updated to the same shape

showcase/integrations/pydantic-ai is deliberately untouched; it is tracked
separately.
2026-08-04 10:51:17 -05:00

129 lines
3.6 KiB
Python

"""
Tools for the todo agent.
Tools are functions that the AI agent can call to interact with the todo list.
Each tool receives a RunContext which contains:
- deps: Dependencies (in our case, StateDeps wrapping TodoState)
- usage: Token usage information
- messages: Conversation history
Tools that modify state return a StateSnapshotEvent, which tells AG-UI to
update the frontend with the new state.
"""
from __future__ import annotations
from uuid import uuid4
from ag_ui.core import EventType, StateSnapshotEvent
from pydantic_ai import RunContext
from pydantic_ai.ui import StateDeps
from models import TodoItem, TodoState, TodoStatus
def _as_state_snapshot(ctx: RunContext[StateDeps[TodoState]]) -> StateSnapshotEvent:
"""
Helper to create a state snapshot event for AG-UI.
When tools return this, AG-UI knows to update the frontend with the new state.
"""
return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=ctx.deps.state)
def get_todos(ctx: RunContext[StateDeps[TodoState]]) -> list[TodoItem]:
"""
Get the current list of todos. Always use this before making changes or answering questions.
"""
return ctx.deps.state.todos
async def add_todos(
ctx: RunContext[StateDeps[TodoState]],
titles: list[str],
descriptions: list[str | None] | None = None,
statuses: list[TodoStatus] | None = None,
) -> StateSnapshotEvent:
"""
Create new todos and append them to the board.
The LLM can add multiple todos at once by providing lists of titles,
descriptions, and statuses. UUIDs are auto-generated.
"""
descriptions = descriptions or [None] * len(titles)
statuses = statuses or [TodoStatus.TODO] * len(titles)
for title, description, status in zip(titles, descriptions, statuses):
ctx.deps.state.todos.append(
TodoItem(
id=str(uuid4()),
title=title,
description=description,
status=status,
)
)
return _as_state_snapshot(ctx)
async def update_todo(
ctx: RunContext[StateDeps[TodoState]],
id: str,
title: str | None = None,
description: str | None = None,
status: TodoStatus | None = None,
) -> StateSnapshotEvent:
"""
Update a todo in place if it exists.
Only the provided fields are updated - others remain unchanged.
This is commonly used to move todos between columns (changing status).
"""
for todo in ctx.deps.state.todos:
if todo.id == id:
if title is not None:
todo.title = title
if description is not None:
todo.description = description
if status is not None:
todo.status = status
break
return _as_state_snapshot(ctx)
async def delete_todos(
ctx: RunContext[StateDeps[TodoState]], id: list[str]
) -> StateSnapshotEvent:
"""Remove todos matching the provided IDs."""
ctx.deps.state.todos = [todo for todo in ctx.deps.state.todos if todo.id not in id]
return _as_state_snapshot(ctx)
async def set_todos(
ctx: RunContext[StateDeps[TodoState]], todos: list[TodoItem]
) -> StateSnapshotEvent:
"""Replace the entire todo list with the provided items."""
ctx.deps.state.todos = todos
return _as_state_snapshot(ctx)
async def demonstrate_error() -> str:
"""Intentionally raise an error to demonstrate UI handling."""
raise ValueError(
"This is a deliberate error to show error handling! The agent will gracefully recover."
)
# All tools available to the agent
tools = [
get_todos,
add_todos,
update_todo,
delete_todos,
set_todos,
demonstrate_error,
]