Skip to content

Commit 681ea69

Browse files
committed
Add pydantic test
1 parent ff02dfd commit 681ea69

File tree

5 files changed

+179
-0
lines changed

5 files changed

+179
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test("[PydanticAI] Backend Tool Rendering displays weather cards", async ({ page }) => {
4+
// Set shorter default timeout for this test
5+
test.setTimeout(30000); // 30 seconds total
6+
7+
await page.goto("/pydantic-ai/feature/backend_tool_rendering");
8+
9+
// Verify suggestion buttons are visible
10+
await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({
11+
timeout: 5000,
12+
});
13+
14+
// Click first suggestion and verify weather card appears
15+
await page.getByRole("button", { name: "Weather in San Francisco" }).click();
16+
17+
// Wait for either test ID or fallback to "Current Weather" text
18+
const weatherCard = page.getByTestId("weather-card");
19+
const currentWeatherText = page.getByText("Current Weather");
20+
21+
// Try test ID first, fallback to text
22+
try {
23+
await expect(weatherCard).toBeVisible({ timeout: 10000 });
24+
} catch (e) {
25+
// Fallback to checking for "Current Weather" text
26+
await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 });
27+
}
28+
29+
// Verify weather content is present (use flexible selectors)
30+
const hasHumidity = await page
31+
.getByText("Humidity")
32+
.isVisible()
33+
.catch(() => false);
34+
const hasWind = await page
35+
.getByText("Wind")
36+
.isVisible()
37+
.catch(() => false);
38+
const hasCityName = await page
39+
.locator("h3")
40+
.filter({ hasText: /San Francisco/i })
41+
.isVisible()
42+
.catch(() => false);
43+
44+
// At least one of these should be true
45+
expect(hasHumidity || hasWind || hasCityName).toBeTruthy();
46+
47+
// Click second suggestion
48+
await page.getByRole("button", { name: "Weather in New York" }).click();
49+
await page.waitForTimeout(2000);
50+
51+
// Verify at least one weather-related element is still visible
52+
const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count();
53+
expect(weatherElements).toBeGreaterThan(0);
54+
});

typescript-sdk/apps/dojo/src/agents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
5252
tool_based_generative_ui: new PydanticAIAgent({
5353
url: `${envVars.pydanticAIUrl}/tool_based_generative_ui/`,
5454
}),
55+
backend_tool_rendering: new PydanticAIAgent({
56+
url: `${envVars.pydanticAIUrl}/backend_tool_rendering`,
57+
}),
5558
};
5659
},
5760
},

typescript-sdk/integrations/pydantic-ai/examples/server/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .api import (
2222
agentic_chat_app,
2323
agentic_generative_ui_app,
24+
backend_tool_rendering_app,
2425
human_in_the_loop_app,
2526
predictive_state_updates_app,
2627
shared_state_app,
@@ -30,6 +31,7 @@
3031
app = FastAPI(title='Pydantic AI AG-UI server')
3132
app.mount('/agentic_chat', agentic_chat_app, 'Agentic Chat')
3233
app.mount('/agentic_generative_ui', agentic_generative_ui_app, 'Agentic Generative UI')
34+
app.mount('/backend_tool_rendering', backend_tool_rendering_app, 'Backend Tool Rendering')
3335
app.mount('/human_in_the_loop', human_in_the_loop_app, 'Human in the Loop')
3436
app.mount(
3537
'/predictive_state_updates',

typescript-sdk/integrations/pydantic-ai/examples/server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .agentic_chat import app as agentic_chat_app
66
from .agentic_generative_ui import app as agentic_generative_ui_app
7+
from .backend_tool_rendering import app as backend_tool_rendering_app
78
from .human_in_the_loop import app as human_in_the_loop_app
89
from .predictive_state_updates import app as predictive_state_updates_app
910
from .shared_state import app as shared_state_app
@@ -12,6 +13,7 @@
1213
__all__ = [
1314
'agentic_chat_app',
1415
'agentic_generative_ui_app',
16+
'backend_tool_rendering_app',
1517
'human_in_the_loop_app',
1618
'predictive_state_updates_app',
1719
'shared_state_app',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Backend Tool Rendering feature."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime
6+
from textwrap import dedent
7+
from zoneinfo import ZoneInfo
8+
9+
import httpx
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+
- Do not call the `generate_task_steps` twice in a row, ever.
20+
- Never repeat the plan, or send a message detailing steps
21+
- If accepted, confirm the creation of the plan and the number of selected (enabled) steps only
22+
- If not accepted, ask the user for more information, DO NOT use the `generate_task_steps` tool again
23+
"""
24+
),
25+
)
26+
app = agent.to_ag_ui()
27+
28+
29+
def get_weather_condition(code: int) -> str:
30+
"""Map weather code to human-readable condition.
31+
32+
Args:
33+
code: WMO weather code.
34+
35+
Returns:
36+
Human-readable weather condition string.
37+
"""
38+
conditions = {
39+
0: "Clear sky",
40+
1: "Mainly clear",
41+
2: "Partly cloudy",
42+
3: "Overcast",
43+
45: "Foggy",
44+
48: "Depositing rime fog",
45+
51: "Light drizzle",
46+
53: "Moderate drizzle",
47+
55: "Dense drizzle",
48+
56: "Light freezing drizzle",
49+
57: "Dense freezing drizzle",
50+
61: "Slight rain",
51+
63: "Moderate rain",
52+
65: "Heavy rain",
53+
66: "Light freezing rain",
54+
67: "Heavy freezing rain",
55+
71: "Slight snow fall",
56+
73: "Moderate snow fall",
57+
75: "Heavy snow fall",
58+
77: "Snow grains",
59+
80: "Slight rain showers",
60+
81: "Moderate rain showers",
61+
82: "Violent rain showers",
62+
85: "Slight snow showers",
63+
86: "Heavy snow showers",
64+
95: "Thunderstorm",
65+
96: "Thunderstorm with slight hail",
66+
99: "Thunderstorm with heavy hail",
67+
}
68+
return conditions.get(code, "Unknown")
69+
70+
71+
@agent.tool_plain
72+
async def get_weather(location: str) -> dict[str, str | float]:
73+
"""Get current weather for a location.
74+
75+
Args:
76+
location: City name.
77+
78+
Returns:
79+
Dictionary with weather information including temperature, feels like,
80+
humidity, wind speed, wind gust, conditions, and location name.
81+
"""
82+
async with httpx.AsyncClient() as client:
83+
# Geocode the location
84+
geocoding_url = (
85+
f"https://geocoding-api.open-meteo.com/v1/search?name={location}&count=1"
86+
)
87+
geocoding_response = await client.get(geocoding_url)
88+
geocoding_data = geocoding_response.json()
89+
90+
if not geocoding_data.get("results"):
91+
raise ValueError(f"Location '{location}' not found")
92+
93+
result = geocoding_data["results"][0]
94+
latitude = result["latitude"]
95+
longitude = result["longitude"]
96+
name = result["name"]
97+
98+
# Get weather data
99+
weather_url = (
100+
f"https://api.open-meteo.com/v1/forecast?"
101+
f"latitude={latitude}&longitude={longitude}"
102+
f"&current=temperature_2m,apparent_temperature,relative_humidity_2m,"
103+
f"wind_speed_10m,wind_gusts_10m,weather_code"
104+
)
105+
weather_response = await client.get(weather_url)
106+
weather_data = weather_response.json()
107+
108+
current = weather_data["current"]
109+
110+
return {
111+
"temperature": current["temperature_2m"],
112+
"feelsLike": current["apparent_temperature"],
113+
"humidity": current["relative_humidity_2m"],
114+
"windSpeed": current["wind_speed_10m"],
115+
"windGust": current["wind_gusts_10m"],
116+
"conditions": get_weather_condition(current["weather_code"]),
117+
"location": name,
118+
}

0 commit comments

Comments
 (0)