11from __future__ import annotations as _annotations
22
33import asyncio
4- import os
5- import urllib .parse
64from dataclasses import dataclass
5+ from random import randint
76from typing import Any
87
98import logfire
109from httpx import AsyncClient
11- from pydantic_ai import Agent , ModelRetry , RunContext
10+ from pydantic import BaseModel
11+ from pydantic_ai import Agent , RunContext
1212
13- # 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
14- logfire .configure ()
13+ logfire .configure (service_name = 'pai-weather' )
1514logfire .instrument_pydantic_ai ()
16- logfire .instrument_httpx (capture_all = True )
1715
1816
1917@dataclass
2018class Deps :
2119 client : AsyncClient
22- weather_api_key : str | None
23- geo_api_key : str | None
2420
2521
2622weather_agent = Agent (
27- 'openai:gpt-4o ' ,
23+ 'openai:gpt-4.1-mini ' ,
2824 # 'Be concise, reply with one sentence.' is enough for some models (like openai) to use
2925 # 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- ),
26+ instructions = 'Be concise, reply with one sentence.' ,
3527 deps_type = Deps ,
3628 retries = 2 ,
3729)
3830
3931
32+ class LatLng (BaseModel ):
33+ lat : float
34+ lng : float
35+
36+
4037@weather_agent .tool
41- async def get_lat_lng (ctx : RunContext [Deps ], location_description : str ) -> dict [ str , float ] :
38+ async def get_lat_lng (ctx : RunContext [Deps ], location_description : str ) -> LatLng :
4239 """Get the latitude and longitude of a location.
4340
4441 Args:
4542 ctx: The context.
4643 location_description: A description of a location.
4744 """
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 )
45+ r = await ctx .deps .client .get (
46+ 'https://demo-endpoints.pydantic.workers.dev/latlng' ,
47+ params = {'location' : location_description , 'sleep' : randint (200 , 1200 )},
48+ )
5549 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' )
50+ return LatLng .model_validate_json (r .content )
6351
6452
6553@weather_agent .tool
@@ -71,62 +59,25 @@ async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str
7159 lat: Latitude of the location.
7260 lng: Longitude of the location.
7361 """
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- }
62+ temp_response , descr_response = await asyncio .gather (
63+ ctx .deps .client .get (
64+ 'https://demo-endpoints.pydantic.workers.dev/number' ,
65+ params = {'min' : 10 , 'max' : 30 , 'sleep' : randint (200 , 1200 )},
66+ ),
67+ ctx .deps .client .get (
68+ 'https://demo-endpoints.pydantic.workers.dev/weather' ,
69+ params = {'lat' : lat , 'lng' : lng , 'sleep' : randint (200 , 1200 )},
70+ ),
71+ )
72+ temp_response .raise_for_status ()
73+ descr_response .raise_for_status ()
74+ return {'temperature' : f'{ temp_response .text } °C' , 'description' : descr_response .text }
12075
12176
12277async def main ():
12378 async with AsyncClient () as client :
12479 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 )
80+ deps = Deps (client = client )
13081 result = await weather_agent .run ('What is the weather like in London and in Wiltshire?' , deps = deps )
13182 print ('Response:' , result .output )
13283
0 commit comments