DeepSeek Function Calling in Action: Building an AI Weather Assistant
Build an AI weather assistant with Function Calling: define tools, handle calls, return results, and generate the final response.
What Problem Does This Tutorial Solve?
You will build a complete AI weather assistant from scratch that can:
- Understand natural language queries from users
- Automatically invoke weather APIs to fetch real-time data
- Generate friendly responses based on the data
This tutorial is your first step toward understanding AI Agents. Function Calling is the cornerstone of AI application development.
What Is Function Calling?
Function Calling enables AI to use external tools. Here is the workflow:
User: "What's the weather like in Beijing today?"
↓
AI determines: I need to call the get_weather function
↓
AI returns function call request: {name: "get_weather", args: {city: "Beijing"}}
↓
Your code executes the function → retrieves weather data
↓
Return the result to the AI
↓
AI: "Beijing is sunny today, 22°C, great for outdoor activities."
Step 1: Define the Tool
import os, json
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
)
# Define the get_weather tool
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Query the weather information for a specified city on the current day",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, in either Chinese or English"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
}]
Step 2: Mock the Weather Query Function
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Mock weather query (use a real API in production)"""
weather_data = {
"Beijing": {"temp": 22, "condition": "Sunny", "humidity": 45},
"Shanghai": {"temp": 28, "condition": "Cloudy", "humidity": 65},
"Tokyo": {"temp": 25, "condition": "Light Rain", "humidity": 70},
"Hangzhou": {"temp": 30, "condition": "Sunny", "humidity": 55},
}
data = weather_data.get(city, {"temp": 20, "condition": "Unknown", "humidity": 50})
if unit == "fahrenheit":
data["temp"] = data["temp"] * 9 / 5 + 32
data["city"] = city
data["unit"] = unit
return data
Step 3: Build the Complete Assistant
def chat_with_weather_assistant(user_query: str) -> str:
messages = [{"role": "user", "content": user_query}]
# First call: AI decides whether a tool is needed
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
tool_choice="auto",
extra_body={"thinking_mode": "thinking"},
)
msg = response.choices[0].message
# If the AI does not need a tool, return the content directly
if not msg.tool_calls:
return msg.content
# Process each tool call
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"Tool invoked: {func_name}")
print(f"Arguments: {func_args}")
# Execute the function
if func_name == "get_weather":
result = get_weather(**func_args)
# Return the execution result to the AI
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
# Second call: AI generates the final response based on tool results
final_response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
)
return final_response.choices[0].message.content
# Test
print(chat_with_weather_assistant("What's the weather like in Beijing today?"))
print(chat_with_weather_assistant("Which city is hotter, Shanghai or Hangzhou?"))
Full Workflow Walkthrough
Input: “Which city is hotter, Shanghai or Hangzhou?”
1st API call:
→ AI returns: need to call get_weather("Shanghai") and get_weather("Hangzhou")
Code executes get_weather → retrieves weather data for both cities
2nd API call (with weather data):
→ AI returns: "Hangzhou (30°C) is 2 degrees hotter than Shanghai (28°C).
Shanghai has higher humidity (65% vs 55%), so it may feel similar.
Neither city has rain today — great for going out."
From Mock to Real: Wiring a Live Weather API
The mock function above is fine for learning, but a real assistant needs live data. Here’s the same get_weather backed by a real HTTP call, with the network failure paths handled explicitly:
import requests
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Fetch live weather. Falls back gracefully on any failure."""
try:
resp = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": 39.9042, # look these up per city in production
"longitude": 116.4074,
"current": "temperature_2m,relative_humidity_2m",
},
timeout=10,
)
resp.raise_for_status()
data = resp.json()["current"]
temp = data["temperature_2m"]
if unit == "fahrenheit":
temp = temp * 9 / 5 + 32
return {
"city": city,
"temp": round(temp, 1),
"humidity": data["relative_humidity_2m"],
"unit": unit,
}
except (requests.RequestException, KeyError) as e:
# Never let a tool crash the whole conversation — return a structured error
return {"city": city, "error": f"weather lookup failed: {e}"}
Handling Tool Execution Failures
When a tool returns an error, feed that error back to the model. The model will then explain the failure to the user in natural language instead of hallucinating an answer:
def dispatch_tool(func_name: str, func_args: dict) -> dict:
"""Route a tool call to the right function, catching everything."""
registry = {
"get_weather": get_weather,
# add more tools here
}
fn = registry.get(func_name)
if fn is None:
return {"error": f"unknown tool: {func_name}"}
try:
return fn(**func_args)
except TypeError as e:
# Model passed wrong/missing arguments
return {"error": f"invalid arguments for {func_name}: {e}"}
except Exception as e:
return {"error": f"{func_name} failed: {e}"}
Also guard the JSON parse — the model occasionally emits malformed arguments:
try:
func_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
func_args = {}
# Return an error result so the model can retry with valid arguments
Parallel Tool Calls
Modern models can request several tools in a single turn. The “Which city is hotter” example already triggers two get_weather calls. The key is to append one tool message per call, matching each tool_call_id, before the second API call:
# msg.tool_calls may contain multiple entries
messages.append(msg) # append the assistant's tool-call message itself
for tool_call in msg.tool_calls:
func_args = json.loads(tool_call.function.arguments)
result = dispatch_tool(tool_call.function.name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # MUST match the specific call
"content": json.dumps(result, ensure_ascii=False),
})
# Now the model has every tool result and can compose the final answer
final = client.chat.completions.create(model="deepseek-v4-pro", messages=messages)
tool_calls message before the tool results. If the tool_call_id in your tool message doesn't match a prior call, the API rejects the request.
Advanced: Defining Multiple Tools
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Query weather information",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "get_news",
"description": "Query the latest news",
"parameters": {
"type": "object",
"properties": {
"topic": {"type": "string"},
"count": {"type": "integer", "default": 3}
},
"required": ["topic"]
}
}
},
{
"type": "function",
"function": {
"name": "send_email",
"description": "Send an email",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string"},
"subject": {"type": "string"},
"body": {"type": "string"}
},
"required": ["to", "subject", "body"]
}
}
}
]
Frequently Asked Questions (FAQ)
Q: When should I use Function Calling?
A: Use it when the AI needs access to real-time data (weather, stocks, news), needs to perform actions (send emails, create files), or needs to invoke your business logic.
Q: How detailed should tool definitions be?
A: The more detailed, the better. The description field for each parameter directly affects whether the AI can invoke the function correctly. Use natural language to describe what each parameter should be.
Q: What happens if a tool call fails or times out?
A: Always return a structured error dictionary (e.g. {"error": "..."}) rather than raising an exception. The model reads that error and explains it to the user in plain language. If you let the exception propagate, your entire request crashes and the user gets nothing. Wrap every tool in a try/except and set a timeout on any network call.
Q: Can the model call multiple tools at once?
A: Yes. msg.tool_calls is a list — the model may request several tools in one turn. Append the assistant’s tool-call message first, then one tool message per call with a matching tool_call_id. Only after every result is appended do you make the second API call to get the final answer.
Next Steps
Tutorial version note: Based on DeepSeek V4 API, verified working as of June 2026.