Weather AI Agent on DeepSeek and Pydantic AI: Same Agent, Different SDK
The same task as in the previous article, but on the framework by the Pydantic team: a native DeepSeek provider out of the box and a response contract the framework validates itself. We compare it with OpenAI Agents SDK on a live run.
In the previous article we built a weather consultant on OpenAI Agents SDK and ran into a pitfall: DeepSeek does not accept response_format of type json_schema, so the response contract had to be validated by hand. Today we build exactly the same agent — the same tool with Gismeteo, the same contract — but on the framework by the Pydantic team, and see how much code disappears.
Stack and installation
Pydantic AI is an agent framework from the team behind Pydantic itself: typing and validation are first-class citizens here. The version at the time of writing (September 2026) is 2.32.0 — the code in this article was verified on exactly that one.
pip install pydantic-ai
The key, as before, is issued in the platform.deepseek.com console and placed into the DEEPSEEK_API_KEY environment variable — the provider picks it up automatically.
The model and the contract: one line for the provider
No AsyncOpenAI client with a base_url: Pydantic AI has a native provider for DeepSeek, and the model is set with a “provider:model” string. The contract is the same Pydantic model as in the previous article, passed via the output_type parameter. The key difference: the framework itself parses and validates the JSON from the model's reply, retrying on error — no manual text parsing needed.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class WeatherReport(BaseModel):
city: str = Field(description="The city the report is about")
temperature_c: int = Field(description="Current temperature, °C")
feels_like_c: int = Field(description="Feels-like temperature, °C")
advice: str = Field(description="Practical advice: what to wear and take")
agent = Agent(
"deepseek:deepseek-v4-flash", # native provider: the key is taken from DEEPSEEK_API_KEY
output_type=WeatherReport, # response contract; the framework validates it
system_prompt=(
"You are a weather consultant. Detect the city from the question, "
"call get_gismeteo_weather and write a short practical report. "
"Use exactly the temperatures returned by the tool. No inventing."
),
)
Version pitfalls: the framework moves fast
To be honest about the first minutes of porting: in the current version 2.32 the parameter is called output_type, not result_type as in the older tutorials, and the key is not passed to the agent constructor — it is taken from the environment or via an explicit provider. Both times we got a TypeError and fixed the code; if you follow older examples, do not be surprised. The upside of the framework is that errors are loud and instant: the contract does not silently degrade into garbage.
The tool: the same live weather from Gismeteo
The tool is registered with the @agent.tool decorator; the first argument is the run context, followed by regular annotated parameters. The parsing logic is exactly the same as in the previous article.
import re
import httpx
from pydantic_ai import RunContext
CITY_SLUGS = {
"moscow": "weather-moscow-4368",
"saint petersburg": "weather-sankt-peterburg-4079",
"kazan": "weather-kazan-4364",
}
@agent.tool
async def get_gismeteo_weather(ctx: RunContext[None], city: str) -> str:
"""Current temperature and feels-like from the city page on Gismeteo."""
slug = CITY_SLUGS.get(city.strip().lower())
if slug is None:
return "City not found. Available: " + ", ".join(sorted(CITY_SLUGS))
url = f"https://www.gismeteo.ru/{slug}/"
async with httpx.AsyncClient(timeout=15, headers={"User-Agent": "Mozilla/5.0"}) as http:
page = (await http.get(url)).text
temps = re.findall(r'<temperature-value[^>]*value="(-?\d+)"[^>]*from-unit="c"', page)
if len(temps) < 2:
return "Could not parse weather from the Gismeteo page."
return f"{city}: now {temps[0]}°C, feels like {temps[1]}°C (Gismeteo data, {url})"
The run
import asyncio
async def main() -> None:
result = await agent.run("Is it warm in Moscow now? What should I wear for a walk?")
report = result.output # an already validated Pydantic model
print(f"{report.city}: {report.temperature_c}°C, feels like {report.feels_like_c}°C")
print("Advice:", report.advice)
asyncio.run(main())
A real run (deepseek-v4-flash, September 4, 2026):
Moscow: 17°C, feels like 16°C Advice: It is cool outside but not cold — dress in layers. A light jacket or a denim jacket, a sweater or a long-sleeve tee, comfortable sneakers will do. Take a windbreaker or a light scarf in case it gets windy.
Same city, same temperatures from Gismeteo, the same advice in spirit — but there is not a single line of manual JSON parsing in the code. Compare with the previous article: there the contract rested on our own parse function with a retry; here it is the framework's responsibility.
Which SDK to choose
- OpenAI Agents SDK is the pick if you are in the OpenAI ecosystem and need tracing and handoff escalations between agents. On DeepSeek mind the pitfall from the previous article: structured output via output_type does not work there, so the contract has to be closed manually.
- Pydantic AI is the pick if you want less code and output validation out of the box: a native “provider:model” provider, a one-line contract, retries on validation errors. The price is a young framework with fast renames.
We ran both implementations live on the same task: the weather numbers matched and the contract was fulfilled in both cases. The choice is a matter of the team's taste, not of capability.
What to improve next
- retries — a parameter of the agent constructor: how many times to retry on a response validation error.
- deps_type — typed dependencies (config, clients) that the tools receive through the same context.
- Streaming and Logfire — streaming replies and observability from the same development team.
- Weather cache and more cities — exactly as in the previous article: the architecture allows it without changes.
The takeaway of the two-article series: a simple agent is a model, facts and a contract. Either framework will do; what matters is that the contract is checked by code and the data comes from a tool.
