Skip to content

Commit 6a04fd1

Browse files
committed
adding weather example
1 parent fcb4057 commit 6a04fd1

File tree

7 files changed

+167
-0
lines changed

7 files changed

+167
-0
lines changed
File renamed without changes.

pai-mcp-sampling/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# MCP Sampling
2+
3+
Demo of using MCP sampling with Pydantic AI as both an MCP Client and an MCP Server.
File renamed without changes.
File renamed without changes.

pai-weather/main.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from __future__ import annotations as _annotations
2+
3+
import asyncio
4+
import os
5+
import urllib.parse
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
import logfire
10+
from httpx import AsyncClient
11+
from pydantic_ai import Agent, ModelRetry, RunContext
12+
13+
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
14+
logfire.configure()
15+
logfire.instrument_pydantic_ai()
16+
logfire.instrument_httpx(capture_all=True)
17+
18+
19+
@dataclass
20+
class Deps:
21+
client: AsyncClient
22+
weather_api_key: str | None
23+
geo_api_key: str | None
24+
25+
26+
weather_agent = Agent(
27+
'openai:gpt-4o',
28+
# 'Be concise, reply with one sentence.' is enough for some models (like openai) to use
29+
# the below tools appropriately, but others like anthropic and gemini require a bit more direction.
30+
instructions=(
31+
'Be concise, reply with one sentence.'
32+
'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, '
33+
'then use the `get_weather` tool to get the weather.'
34+
),
35+
deps_type=Deps,
36+
retries=2,
37+
)
38+
39+
40+
@weather_agent.tool
41+
async def get_lat_lng(ctx: RunContext[Deps], location_description: str) -> dict[str, float]:
42+
"""Get the latitude and longitude of a location.
43+
44+
Args:
45+
ctx: The context.
46+
location_description: A description of a location.
47+
"""
48+
if ctx.deps.geo_api_key is None:
49+
# if no API key is provided, return a dummy response (London)
50+
return {'lat': 51.1, 'lng': -0.1}
51+
52+
params = {'access_token': ctx.deps.geo_api_key}
53+
loc = urllib.parse.quote(location_description)
54+
r = await ctx.deps.client.get(f'https://api.mapbox.com/geocoding/v5/mapbox.places/{loc}.json', params=params)
55+
r.raise_for_status()
56+
data = r.json()
57+
58+
if features := data['features']:
59+
lat, lng = features[0]['center']
60+
return {'lat': lat, 'lng': lng}
61+
else:
62+
raise ModelRetry('Could not find the location')
63+
64+
65+
@weather_agent.tool
66+
async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]:
67+
"""Get the weather at a location.
68+
69+
Args:
70+
ctx: The context.
71+
lat: Latitude of the location.
72+
lng: Longitude of the location.
73+
"""
74+
if ctx.deps.weather_api_key is None:
75+
# if no API key is provided, return a dummy response
76+
return {'temperature': '21 °C', 'description': 'Sunny'}
77+
78+
params = {
79+
'apikey': ctx.deps.weather_api_key,
80+
'location': f'{lat},{lng}',
81+
'units': 'metric',
82+
}
83+
with logfire.span('calling weather API', params=params) as span:
84+
r = await ctx.deps.client.get('https://api.tomorrow.io/v4/weather/realtime', params=params)
85+
r.raise_for_status()
86+
data = r.json()
87+
span.set_attribute('response', data)
88+
89+
values = data['data']['values']
90+
# https://docs.tomorrow.io/reference/data-layers-weather-codes
91+
code_lookup = {
92+
1000: 'Clear, Sunny',
93+
1100: 'Mostly Clear',
94+
1101: 'Partly Cloudy',
95+
1102: 'Mostly Cloudy',
96+
1001: 'Cloudy',
97+
2000: 'Fog',
98+
2100: 'Light Fog',
99+
4000: 'Drizzle',
100+
4001: 'Rain',
101+
4200: 'Light Rain',
102+
4201: 'Heavy Rain',
103+
5000: 'Snow',
104+
5001: 'Flurries',
105+
5100: 'Light Snow',
106+
5101: 'Heavy Snow',
107+
6000: 'Freezing Drizzle',
108+
6001: 'Freezing Rain',
109+
6200: 'Light Freezing Rain',
110+
6201: 'Heavy Freezing Rain',
111+
7000: 'Ice Pellets',
112+
7101: 'Heavy Ice Pellets',
113+
7102: 'Light Ice Pellets',
114+
8000: 'Thunderstorm',
115+
}
116+
return {
117+
'temperature': f'{values["temperatureApparent"]:0.0f}°C',
118+
'description': code_lookup.get(values['weatherCode'], 'Unknown'),
119+
}
120+
121+
122+
async def main():
123+
async with AsyncClient() as client:
124+
logfire.instrument_httpx(client, capture_all=True)
125+
# create a free API key at https://www.tomorrow.io/weather-api/
126+
weather_api_key = os.getenv('WEATHER_API_KEY')
127+
# create a free API key at https://www.mapbox.com/
128+
geo_api_key = os.getenv('GEO_API_KEY')
129+
deps = Deps(client=client, weather_api_key=weather_api_key, geo_api_key=geo_api_key)
130+
result = await weather_agent.run('What is the weather like in London and in Wiltshire?', deps=deps)
131+
print('Response:', result.output)
132+
133+
134+
if __name__ == '__main__':
135+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8+
"devtools>=0.12.2",
89
"logfire[httpx]>=3.21.1",
910
"pydantic-ai>=0.3.4",
1011
]

uv.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)