Skip to content

Commit 5390b98

Browse files
authored
Bring in the example from the Pydantic repo so you can see it locally. (#236)
* copy example from pydantic repo * Make work without local pydantic repo * Clean up feedback
1 parent 80b56ea commit 5390b98

File tree

12 files changed

+1474
-0
lines changed

12 files changed

+1474
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
server.egg-info/
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Pydantic AI AG-UI Examples
2+
3+
This directory contains example usage of the AG-UI adapter for Pydantic AI. It provides a FastAPI application that demonstrates how to use the Pydantic AI agent with the AG-UI protocol.
4+
5+
## Features
6+
7+
The examples include implementations for each of the AG-UI dojo features:
8+
- Agentic Chat
9+
- Human in the Loop
10+
- Agentic Generative UI
11+
- Tool Based Generative UI
12+
- Shared State
13+
- Predictive State Updates
14+
15+
## Setup
16+
17+
1. Install dependencies:
18+
```bash
19+
uv sync
20+
```
21+
22+
2. Run the development server:
23+
```bash
24+
uv run dev
25+
```
26+
27+
## Usage
28+
29+
Once the server is running, launch the frontend dojo with:
30+
31+
```bash
32+
cd ../../../
33+
pnpm install
34+
turbo run dev
35+
```
36+
37+
and view it at http://localhost:3000.
38+
39+
By default, the agents can be reached at:
40+
41+
- `http://localhost:9000/agentic_chat` - Agentic Chat
42+
- `http://localhost:9000/agentic_generative_ui` - Agentic Generative UI
43+
- `http://localhost:9000/human_in_the_loop` - Human in the Loop
44+
- `http://localhost:9000/predictive_state_updates` - Predictive State Updates
45+
- `http://localhost:9000/shared_state` - Shared State
46+
- `http://localhost:9000/tool_based_generative_ui` - Tool Based Generative UI
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
tool.uv.package = true
2+
3+
[project]
4+
name = "server"
5+
version = "0.1.0"
6+
description = "Example usage of the AG-UI adapter for Pydantic AI"
7+
license = "MIT"
8+
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
dependencies = [
12+
"fastapi>=0.104.0",
13+
"uvicorn[standard]>=0.24.0",
14+
"pydantic-ai-slim[openai,ag-ui]>=0.1.0",
15+
]
16+
authors = []
17+
18+
[project.scripts]
19+
dev = "server:main"
20+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Example usage of the AG-UI adapter for Pydantic AI.
2+
3+
This provides a FastAPI application that demonstrates how to use the
4+
Pydantic AI agent with the AG-UI protocol. It includes examples for
5+
each of the AG-UI dojo features:
6+
- Agentic Chat
7+
- Human in the Loop
8+
- Agentic Generative UI
9+
- Tool Based Generative UI
10+
- Shared State
11+
- Predictive State Updates
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from fastapi import FastAPI
17+
import uvicorn
18+
19+
20+
from .api import (
21+
agentic_chat_app,
22+
agentic_generative_ui_app,
23+
human_in_the_loop_app,
24+
predictive_state_updates_app,
25+
shared_state_app,
26+
tool_based_generative_ui_app,
27+
)
28+
29+
app = FastAPI(title='Pydantic AI AG-UI server')
30+
app.mount('/agentic_chat', agentic_chat_app, 'Agentic Chat')
31+
app.mount('/agentic_generative_ui', agentic_generative_ui_app, 'Agentic Generative UI')
32+
app.mount('/human_in_the_loop', human_in_the_loop_app, 'Human in the Loop')
33+
app.mount(
34+
'/predictive_state_updates',
35+
predictive_state_updates_app,
36+
'Predictive State Updates',
37+
)
38+
app.mount('/shared_state', shared_state_app, 'Shared State')
39+
app.mount(
40+
'/tool_based_generative_ui',
41+
tool_based_generative_ui_app,
42+
'Tool Based Generative UI',
43+
)
44+
45+
46+
def main():
47+
"""Main function to start the FastAPI server."""
48+
uvicorn.run(app, host="0.0.0.0", port=9000)
49+
50+
if __name__ == "__main__":
51+
main()
52+
53+
__all__ = ["main"]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Example API for a AG-UI compatible Pydantic AI Agent UI."""
2+
3+
from __future__ import annotations
4+
5+
from .agentic_chat import app as agentic_chat_app
6+
from .agentic_generative_ui import app as agentic_generative_ui_app
7+
from .human_in_the_loop import app as human_in_the_loop_app
8+
from .predictive_state_updates import app as predictive_state_updates_app
9+
from .shared_state import app as shared_state_app
10+
from .tool_based_generative_ui import app as tool_based_generative_ui_app
11+
12+
__all__ = [
13+
'agentic_chat_app',
14+
'agentic_generative_ui_app',
15+
'human_in_the_loop_app',
16+
'predictive_state_updates_app',
17+
'shared_state_app',
18+
'tool_based_generative_ui_app',
19+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Agentic Chat feature."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime
6+
from zoneinfo import ZoneInfo
7+
8+
from pydantic_ai import Agent
9+
10+
agent = Agent('openai:gpt-4o-mini')
11+
app = agent.to_ag_ui()
12+
13+
14+
@agent.tool_plain
15+
async def current_time(timezone: str = 'UTC') -> str:
16+
"""Get the current time in ISO format.
17+
18+
Args:
19+
timezone: The timezone to use.
20+
21+
Returns:
22+
The current time in ISO format string.
23+
"""
24+
tz: ZoneInfo = ZoneInfo(timezone)
25+
return datetime.now(tz=tz).isoformat()
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Agentic Generative UI feature."""
2+
3+
from __future__ import annotations
4+
5+
from textwrap import dedent
6+
from typing import Any, Literal
7+
8+
from pydantic import BaseModel, Field
9+
10+
from ag_ui.core import EventType, StateDeltaEvent, StateSnapshotEvent
11+
from pydantic_ai import Agent
12+
13+
StepStatus = Literal['pending', 'completed']
14+
15+
16+
class Step(BaseModel):
17+
"""Represents a step in a plan."""
18+
19+
description: str = Field(description='The description of the step')
20+
status: StepStatus = Field(
21+
default='pending',
22+
description='The status of the step (e.g., pending, completed)',
23+
)
24+
25+
26+
class Plan(BaseModel):
27+
"""Represents a plan with multiple steps."""
28+
29+
steps: list[Step] = Field(default_factory=list, description='The steps in the plan')
30+
31+
32+
class JSONPatchOp(BaseModel):
33+
"""A class representing a JSON Patch operation (RFC 6902)."""
34+
35+
op: Literal['add', 'remove', 'replace', 'move', 'copy', 'test'] = Field(
36+
description='The operation to perform: add, remove, replace, move, copy, or test',
37+
)
38+
path: str = Field(description='JSON Pointer (RFC 6901) to the target location')
39+
value: Any = Field(
40+
default=None,
41+
description='The value to apply (for add, replace operations)',
42+
)
43+
from_: str | None = Field(
44+
default=None,
45+
alias='from',
46+
description='Source path (for move, copy operations)',
47+
)
48+
49+
50+
agent = Agent(
51+
'openai:gpt-4o-mini',
52+
instructions=dedent(
53+
"""
54+
When planning use tools only, without any other messages.
55+
IMPORTANT:
56+
- Use the `create_plan` tool to set the initial state of the steps
57+
- Use the `update_plan_step` tool to update the status of each step
58+
- Do NOT repeat the plan or summarise it in a message
59+
- Do NOT confirm the creation or updates in a message
60+
- Do NOT ask the user for additional information or next steps
61+
62+
Only one plan can be active at a time, so do not call the `create_plan` tool
63+
again until all the steps in current plan are completed.
64+
"""
65+
),
66+
)
67+
68+
69+
@agent.tool_plain
70+
async def create_plan(steps: list[str]) -> StateSnapshotEvent:
71+
"""Create a plan with multiple steps.
72+
73+
Args:
74+
steps: List of step descriptions to create the plan.
75+
76+
Returns:
77+
StateSnapshotEvent containing the initial state of the steps.
78+
"""
79+
plan: Plan = Plan(
80+
steps=[Step(description=step) for step in steps],
81+
)
82+
return StateSnapshotEvent(
83+
type=EventType.STATE_SNAPSHOT,
84+
snapshot=plan.model_dump(),
85+
)
86+
87+
88+
@agent.tool_plain
89+
async def update_plan_step(
90+
index: int, description: str | None = None, status: StepStatus | None = None
91+
) -> StateDeltaEvent:
92+
"""Update the plan with new steps or changes.
93+
94+
Args:
95+
index: The index of the step to update.
96+
description: The new description for the step.
97+
status: The new status for the step.
98+
99+
Returns:
100+
StateDeltaEvent containing the changes made to the plan.
101+
"""
102+
changes: list[JSONPatchOp] = []
103+
if description is not None:
104+
changes.append(
105+
JSONPatchOp(
106+
op='replace', path=f'/steps/{index}/description', value=description
107+
)
108+
)
109+
if status is not None:
110+
changes.append(
111+
JSONPatchOp(op='replace', path=f'/steps/{index}/status', value=status)
112+
)
113+
return StateDeltaEvent(
114+
type=EventType.STATE_DELTA,
115+
delta=changes,
116+
)
117+
118+
119+
app = agent.to_ag_ui()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Human in the Loop Feature.
2+
3+
No special handling is required for this feature.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from textwrap import dedent
9+
10+
from pydantic_ai import Agent
11+
12+
agent = Agent(
13+
'openai:gpt-4o-mini',
14+
instructions=dedent(
15+
"""
16+
When planning tasks use tools only, without any other messages.
17+
IMPORTANT:
18+
- Use the `generate_task_steps` tool to display the suggested steps to the user
19+
- Never repeat the plan, or send a message detailing steps
20+
- If accepted, confirm the creation of the plan and the number of selected (enabled) steps only
21+
- If not accepted, ask the user for more information, DO NOT use the `generate_task_steps` tool again
22+
"""
23+
),
24+
)
25+
26+
app = agent.to_ag_ui()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Predictive State feature."""
2+
3+
from __future__ import annotations
4+
5+
from textwrap import dedent
6+
7+
from pydantic import BaseModel
8+
9+
from ag_ui.core import CustomEvent, EventType
10+
from pydantic_ai import Agent, RunContext
11+
from pydantic_ai.ag_ui import StateDeps
12+
13+
14+
class DocumentState(BaseModel):
15+
"""State for the document being written."""
16+
17+
document: str = ''
18+
19+
20+
agent = Agent('openai:gpt-4o-mini', deps_type=StateDeps[DocumentState])
21+
22+
23+
# Tools which return AG-UI events will be sent to the client as part of the
24+
# event stream, single events and iterables of events are supported.
25+
@agent.tool_plain
26+
async def document_predict_state() -> list[CustomEvent]:
27+
"""Enable document state prediction.
28+
29+
Returns:
30+
CustomEvent containing the event to enable state prediction.
31+
"""
32+
return [
33+
CustomEvent(
34+
type=EventType.CUSTOM,
35+
name='PredictState',
36+
value=[
37+
{
38+
'state_key': 'document',
39+
'tool': 'write_document',
40+
'tool_argument': 'document',
41+
},
42+
],
43+
),
44+
]
45+
46+
47+
@agent.instructions()
48+
async def story_instructions(ctx: RunContext[StateDeps[DocumentState]]) -> str:
49+
"""Provide instructions for writing document if present.
50+
51+
Args:
52+
ctx: The run context containing document state information.
53+
54+
Returns:
55+
Instructions string for the document writing agent.
56+
"""
57+
return dedent(
58+
f"""You are a helpful assistant for writing documents.
59+
60+
Before you start writing, you MUST call the `document_predict_state`
61+
tool to enable state prediction.
62+
63+
To present the document to the user for review, you MUST use the
64+
`write_document` tool.
65+
66+
When you have written the document, DO NOT repeat it as a message.
67+
If accepted briefly summarize the changes you made, 2 sentences
68+
max, otherwise ask the user to clarify what they want to change.
69+
70+
This is the current document:
71+
72+
{ctx.deps.state.document}
73+
"""
74+
)
75+
76+
77+
app = agent.to_ag_ui(deps=StateDeps(DocumentState()))

0 commit comments

Comments
 (0)