Skip to main content

Command Palette

Search for a command to run...

Simple AF AI Agent

Updated
5 min read

The best way to learn something is by doing it. Instead of diving deep into concepts from get go, let's jump straight into building an agent and work backward to understand each component.

Objective

We will build a weather agent. Sure, it may not be the most creative project (and it might feel a bit painful for those enduring gloomy European weather), but it is a great way to learn how to build an AI agent. And, who knows? You might reuse it for summer forecasts ☀️

Weather agent will take a location as input and provide the current weather details.

Show Time

Let’s start by creating an agent using Pydantic AI

from pydantic_ai import Agent
from httpx import AsyncClient
from dataclasses import dataclass

@dataclass
class Deps:
    client: AsyncClient
    weather_api_key: str | None
    geo_api_key: str | None

weather_agent = from pydantic_ai import Agent(
    'openai:gpt-4o',
    system_prompt=(
        'Be concise, reply with one sentence.'
        'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, '
        'then use the `get_weather` tool to get the weather.'
    ),
    retries=2,
    deps_type=Deps,
)

In the above code, we instantiated the Agent with several parameters

  • openai:gpt-4o This is the name of the LLM model that you want to use for the agent. Pydantic support many other models.

  • system_prompt Prompt set by the developer to instruct the LLM what it needs to do. As you can see in the above prompt, I am instructing the LLM to reply concisely and use some tools. We will dive deeper into tools shortly.

  • retries Is a functionality offered by Pydantic to retry if there is an error during generating the response. It is similar to how HTTP libraries provide a retry parameter mechanism.

  • deps_type Specifies the dependency type used by the agent. We will discuss later in the blog about this

Tools: Our keys to the outside world

LLMs are trained from the data available on the internet. This has two limitations:

  1. Data Staleness: When we utilize LLMs in the present, they are trained on the data from the past. World doesn’t stop and generates millions of terabytes of information every hour which LLM isn’t aware of

  2. Limited Scope: LLMs can only use data that was accessible during their training. For instance, OpenAI models no longer have access to X.com data.

oh Nein, what to do now? Don’t you worry fam, we got some Tools! Tools creates a bridge between LLM and outside world which allows agents to be much more accurate and reliable.

In coding terms, tools are essentially functions that the LLM can call to retrieve additional context or data, helping it generate more accurate and reliable responses.

Let’s create some tools:

@weather_agent.tool
async def get_lat_lng(
    ctx: RunContext[Deps], location_description: str
) -> dict[str, float]:
    params = {
        'q': location_description,
        'api_key': ctx.deps.geo_api_key,
    }
    with logfire.span('calling geocode API', params=params) as span:
        r = await ctx.deps.client.get('https://geocode.maps.co/search', params=params)
        r.raise_for_status()
        data = r.json()
        span.set_attribute('response', data)

    if data:
        return {'lat': data[0]['lat'], 'lng': data[0]['lon']}
    else:
        raise ModelRetry('Could not find the location')

@weather_agent.tool
async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]:
    params = {
        'apikey': ctx.deps.weather_api_key,
        'location': f'{lat},{lng}',
        'units': 'metric',
    }
    with logfire.span('calling weather API', params=params) as span:
        r = await ctx.deps.client.get(
            'https://api.tomorrow.io/v4/weather/realtime', params=params
        )
        r.raise_for_status()
        data = r.json()
        span.set_attribute('response', data)

    values = data['data']['values']
    # https://docs.tomorrow.io/reference/data-layers-weather-codes
    code_lookup = {
        1000: 'Clear, Sunny',
        1100: 'Mostly Clear',
        1101: 'Partly Cloudy',
        1102: 'Mostly Cloudy',
        1001: 'Cloudy',
        2000: 'Fog',
        2100: 'Light Fog',
        4000: 'Drizzle',
        4001: 'Rain',
        4200: 'Light Rain',
        4201: 'Heavy Rain',
        5000: 'Snow',
        5001: 'Flurries',
        5100: 'Light Snow',
        5101: 'Heavy Snow',
        6000: 'Freezing Drizzle',
        6001: 'Freezing Rain',
        6200: 'Light Freezing Rain',
        6201: 'Heavy Freezing Rain',
        7000: 'Ice Pellets',
        7101: 'Heavy Ice Pellets',
        7102: 'Light Ice Pellets',
        8000: 'Thunderstorm',
    }
    return {
        'temperature': f'{values["temperatureApparent"]:0.0f}°C',
        'description': code_lookup.get(values['weatherCode'], 'Unknown'),
    }

In the code above, we defined two tools:

  1. get_lat_lng: Retrieves the latitude and longitude of a given location using a geocoding API.

  2. get_weather: Fetches real-time weather data for a given latitude and longitude using a weather API.

We passed the decorator @weather_agent.tool to both functions which provides the weather_agent aware of the tools it can call. Unlike some frameworks that require passing tools as a list during agent creation, Pydantic AI simplifies this with decorators.

Dependencies in Pydantic AI

PydanticAI uses a dependency injection system to provide data and services to your agent's tools. As you can see in the above code snippet, the first parameter in both function is ctx which has a property deps which is basically Pydantic AI way of injecting dependencies that is available to tools during run time.

Let’s Run the Agent

async def main():
    async with AsyncClient() as client:
        # Get location from user input
        location = input("Enter a location: ")
        # create a free API key at https://www.tomorrow.io/weather-api/
        weather_api_key = os.getenv('WEATHER_API_KEY')
        # create a free API key at https://geocode.maps.co/
        geo_api_key = os.getenv('GEO_API_KEY')
        deps = Deps(
            client=client, weather_api_key=weather_api_key, geo_api_key=geo_api_key
        )
        result = await weather_agent.run(
            f'What is the weather like in {location}?', deps=deps
        )
        print('Response:', result.data)


if __name__ == "__main__":
    asyncio.run(main())

This script:

  1. Prompts the user to enter a location.

  2. Fetches the weather and geocoding API keys from environment variables.

  3. Passes dependencies to the agent.

  4. Invokes the agent to get the weather data for the specified location.

The code for the simple agent is available here.