Large Language Models (LLMs) excel at processing natural language but cannot access real-time data natively. To resolve this limitation, developers build autonomous AI agents. An AI agent extends an LLM's capabilities by executing external tools, accessing live APIs, and validating unstructured data into predictable data models.
This article details how to build a production-ready AI weather agent using PydanticAI, validate external API data with Pydantic, and handle structured dependencies. This project serves as a foundational step toward constructing specialized, autonomous agents capable of managing automated Bitcoin mining nodes, validating Lightning Network lightning transactions, and orchestrating algorithmic payments.
Architectural Overview
The application architecture separates prompt evaluation from the execution of the external business logic.
User Query
│
▼
PydanticAI Agent
│
▼
Weather Tool (@agent.tool)
│
▼
Weather Service (WeatherAgent Dependency)
│
▼
Open-Meteo Geocoding & Forecast APIs
│
▼
Pydantic Data Validation (Coordinates & CurrentWeather Models)
│
▼
Final Structured AI Response
Instead of allowing the LLM to access the network directly, the agent acts as an orchestrator. It extracts natural language parameters (such as a city name), passes them to a registered validation tool, executes network requests via httpx, and maps the response payload back to a strict schema.
Core Concepts Implemented
To transition from a simple text chatbot to an analytical agent, this project implements several core engineering patterns:
-
Dependency Injection via RunContext: Instead of hardcoding API clients inside global functions, the system injects data services dynamically into the execution context using PydanticAI's
deps_type. -
Strict Type Safety: Data objects entering and leaving the agent use Pydantic
BaseModelclasses to prevent data corruption and unexpected structural types. -
Deterministic Tool Execution: The system uses the
@agent.tooldecorator to automatically expose Python functions to the underlying LLM via generated JSON schemas.
Implementation Code
The implementation is contained within a single executable Python module (main.py). The script uses the Open-Meteo Geocoding API to resolve alphanumeric location queries into geographic coordinates before querying weather metrics.
import httpx
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
class Coordinates(BaseModel):
"""Data model to validate geographic coordinates."""
latitude: float
longitude: float
class CurrentWeather(BaseModel):
"""Data model to validate structured weather conditions."""
city: str
temperature: float # Corrected typo from original 'tempereture'
description: str
class WeatherAgent:
"""Service class handling external API interaction and data retrieval."""
def get_coordinates(self, city: str) -> Coordinates:
try:
response = httpx.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1},
timeout=5.0
)
response.raise_for_status()
data = response.json()
results = data.get("results")
if not results:
raise ValueError(f"City '{city}' was not found.")
result = results[0]
return Coordinates(
latitude=result["latitude"],
longitude=result["longitude"]
)
except httpx.ConnectError:
print("[ERROR] Connection failure to Geocoding server.")
raise
except httpx.TimeoutException:
print("[ERROR] Geocoding request timed out.")
raise
except httpx.HTTPStatusError as e:
print(f"[ERROR] Server returned HTTP {e.response.status_code}")
raise
def describe_weather(self, code: int) -> str:
"""Maps World Meteorological Organization (WMO) codes to human-readable strings."""
weather_codes = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
}
return weather_codes.get(code, "Unknown")
def get_weather(self, city: str) -> CurrentWeather:
# Step 1: Resolve city name to GPS coordinates
coordinates = self.get_coordinates(city)
# Step 2: Query the forecast API with resolved positions
response = httpx.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": coordinates.latitude,
"longitude": coordinates.longitude,
"current": "temperature_2m,weather_code",
},
timeout=5.0
)
response.raise_for_status()
data = response.json()
current = data["current"]
return CurrentWeather(
city=city,
temperature=current["temperature_2m"],
description=self.describe_weather(current["weather_code"]),
)
# Instantiate the agent with strict instructions and explicit type dependencies
agent = Agent(
"groq:llama-3.3-70b-versatile",
instructions=(
"You are a precise, helpful, and professional Weather Assistant. "
"Your primary goal is to provide accurate weather forecasts and current conditions "
"by utilizing available weather tools and structuring the data flawlessly.\n\n"
"### Core Responsibilities:\n"
"1. **Location Resolution:** Extract the location (city, country, region) from the user's query.\n"
"2. **Tool Execution:** Use the provided weather tools to fetch real-time data. Never make up weather data.\n"
"3. **Data Mapping:** Carefully parse the tool outputs to fulfill the schema requirements.\n\n"
"### Formatting & Tone Guidelines:\n"
"- **Units:** Default to the metric system (Celsius, km/h, mm) unless the user requests imperial units.\n"
"- **Clarity:** Summarize complex meteorological data into simple, digestible insights.\n\n"
"### Behavioral Constraints:\n"
"- If a location cannot be found, explain the limitation gracefully.\n"
"- Do not hallucinate current dates or times; rely entirely on the tool's timestamps."
),
deps_type=WeatherAgent,
)
@agent.tool
def current_weather(ctx: RunContext[WeatherAgent], city: str) -> CurrentWeather:
"""Fetches the current weather for a given city.
Args:
ctx: The runtime context containing the injected WeatherAgent dependency.
city: The name of the city to lookup.
"""
print(f"[TOOL] Fetching weather for {city}")
return ctx.deps.get_weather(city)
if __name__ == "__main__":
history = None
print("AI Weather Agent Initialized. Type 'exit' to quit.")
while True:
prompt = input("you> ")
if prompt.lower() in {"exit", "quit"}:
break
if not prompt.strip():
continue
try:
# Instantiate service dependency per conversation frame
weather_service = WeatherAgent()
result = agent.run_sync(
prompt,
message_history=history,
deps=weather_service
)
print(f"bot> {result.output}")
history = result.all_messages()
except Exception as e:
print(f"[RUNTIME ERROR] {type(e).__name__}: {e}")
Technical Insights and Key Takeaways
Building this system highlights the fundamental difference between standard generative text generation and actual agentic engineering:
-
State Isolation via Dependency Injection: Passing the
WeatherAgentservice instance through thedepsargument ensures that tools remain stateless and isolated. This allows for concurrent execution threads without shared-state mutation errors. -
Deterministic Network Error Defenses: External systems fail frequently. Wrapping
httpxlogic with deliberate catch blocks forConnectError,TimeoutException, andHTTPStatusErrorensures that the Python app handles infrastructure hiccups cleanly before they crash the runtime stack. - Structured Schemas Keep LLMs in Check: LLMs naturally output variable text formats. Forcing inputs and outputs through Pydantic classes forces the model to adhere to fixed data keys, allowing the data to seamlessly pipeline into downstream databases or APIs.
Future Goals: Bitcoin-Based AI Agents
Mastering structured tools and type-safe dependency execution prepares developers for the next level of intelligent systems: Bitcoin-native AI agents.
The long-term roadmap for this architecture shifts from tracking weather metrics to executing autonomous machine-to-machine financial operations on the Bitcoin network:
- Automated Bitcoin Mining Management: Agents will poll hash rate metrics, ASIC temperature arrays, and real-time energy prices via REST tools. They can then dynamically toggle mining pools or adjust clock speeds to maximize profitability.
-
Autonomous Lightning Payments: By injecting Bitcoin node dependencies (e.g., LND, Core Lightning) through the
RunContext, agents can autonomously pay for APIs, settle micro-invoices, and rebalance liquidity channels based on demand. - Programmable On-Chain Transactions: Integrating Bitcoin script builders and wallet interfaces will allow multi-agent systems to orchestrate multisig escrow releases, programmatic smart contract settlements, and trustless automated transactions.
Top comments (0)