Weather AI Agent on DeepSeek: OpenAI Agents SDK + Pydantic
A simple but real agent: the DeepSeek model, a live tool with Gismeteo data and a strict Pydantic response contract. Below — full code, a real run and two pitfalls the tutorials stay silent about.
A “simple LLM agent” is not a chat toy but three things: a model you trust to reason, tools that give it facts, and a response contract verified by code rather than hope. In this article we assemble a minimal weather consultant on DeepSeek + OpenAI Agents SDK + Pydantic and run it live.
Stack and installation
OpenAI Agents SDK is the official agent orchestration SDK: function tools, tracing and typed contracts out of the box. DeepSeek plugs into it because the DeepSeek API is OpenAI-compatible. Versions at the time of writing (September 2026): openai-agents 0.22.0, pydantic 2.13.5 — we verified the code on exactly these.
pip install openai-agents pydantic httpx
The DeepSeek key is issued in the platform.deepseek.com console and lives in an environment variable. The official DeepSeek documentation lists the current models as deepseek-v4-flash (fast and cheap) and deepseek-v4-pro (stronger at reasoning); for an agent with simple tools, flash is more than enough.
Plugging DeepSeek in as the agent's model
By default the SDK uses OpenAI's Responses API. For OpenAI-compatible providers the SDK documentation provides a separate class — OpenAIChatCompletionsModel: you pass it a regular OpenAI client with the right base_url.
import os
from openai import AsyncOpenAI
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
client = AsyncOpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com", # OpenAI-compatible endpoint
)
model = OpenAIChatCompletionsModel(model="deepseek-v4-flash", openai_client=client)
The tool: live weather from Gismeteo
The model does not know today's temperature — the tool does. The function_tool decorator turns an async function into a tool: the argument schema is built from annotations, and the docstring becomes the description for the model. A Gismeteo city page returns the current temperature in a custom temperature-value element, so parsing is a single regex; the first pair of values is “now” and “feels like”.
import re
import httpx
from agents import function_tool
CITY_SLUGS = {
"moscow": "weather-moscow-4368",
"saint petersburg": "weather-sankt-peterburg-4079",
"kazan": "weather-kazan-4364",
}
@function_tool
async def get_gismeteo_weather(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, {url})"
Note the returned strings “city not found” and “could not parse”: a tool must answer with text the model understands instead of raising an exception — this way the agent can fix its own mistake, for example by asking to clarify the city.
The response contract — Pydantic
The agent's answer is not free text but a structure: convenient to render in a UI, store in a database and test.
from pydantic import BaseModel, Field, ValidationError
class WeatherReport(BaseModel):
city: str = Field(description="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")
Here comes the main surprise for those who read the SDK docs casually. The output_type parameter with a Pydantic model is sent to the provider on the Chat Completions route as response_format of type json_schema. We verified live: as of September 2026 DeepSeek replies with a 400 error “This response_format type is unavailable now” — only json_object is supported. So the honest working pattern is: ask the model to return JSON as text and validate it with a Pydantic model, with one retry in case of malformed JSON. The contract lives in your code, not in the provider's kindness — which is even more reliable.
def parse_report(text: str) -> WeatherReport:
start, end = text.find("{"), text.rfind("}")
if start == -1 or end <= start:
raise ValueError("No JSON in model output: " + text[:200])
return WeatherReport.model_validate_json(text[start:end + 1])
The agent itself and the run
import asyncio
from agents import Agent, Runner
SCHEMA_HINT = ('Reply with a single JSON object and nothing else: '
'{"city": str, "temperature_c": int, '
'"feels_like_c": int, "advice": str}.')
agent = Agent(
name="weather-consultant",
model=model,
instructions=(
"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. " + SCHEMA_HINT
),
tools=[get_gismeteo_weather],
)
async def main() -> None:
result = await Runner.run(agent, "Is it warm in Moscow now? What should I wear?")
try:
report = parse_report(result.final_output)
except (ValidationError, ValueError) as err:
result = await Runner.run(agent, f"Your previous reply failed validation: {err}. "
f"Return only the JSON per schema.")
report = parse_report(result.final_output)
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 of this code (deepseek-v4-flash, September 4, 2026):
Moscow: 17C, feels like 16C Advice: It is cool outside, definitely not warm. Wear a light jacket, a jumper or a sweater, and waterproof shoes — it may get chilly in the evening.
The agent itself called the tool for the city from the question, plugged in the actual temperatures and gave human advice. The whole run takes a couple of seconds and a fraction of a cent at flash pricing.
What to improve next
- deepseek-v4-pro for complex scenarios: compare “now”, “tonight” and “the weekend” with several tool calls.
- Weather cache for 10–15 minutes: the same city does not hit Gismeteo on every request.
- More cities and slug lookup via the Gismeteo search page instead of a hard-coded dictionary.
- A Telegram bot on top: the same agent with a delivery channel — the architecture does not change.
The key takeaway: a “simple agent” stops being a toy once it has facts (tools) and a contract (Pydantic). Everything else is half an hour of code.
