tool-calling-weather-agent · EN · 2026-10-08

Building a Minimal Weather Agent with Tool Calling on the Aggregator

Learn how to build a minimal weather agent using tool calling on an LLM API aggregator. This tutorial covers declaring a JSON schema for a weather tool, letting the model emit tool calls, executing the tool locally, and feeding results back in a second request. It uses a conceptual approach with placeholder code and emphasizes key steps without specific model details or pricing.

Introduction to Tool Calling

Tool calling (also known as function calling) lets a language model request external actions, such as fetching weather data. The model doesn't execute tools itself; it emits a structured request that your code handles. This pattern is essential for building agents that interact with the real world.

In this tutorial, we'll create a minimal weather agent that:

  • Declares a weather tool using a JSON schema.
  • Sends a user query to the model.
  • Receives a tool call request from the model.
  • Executes the tool locally (e.g., calls a weather API).
  • Sends the tool result back to the model for a final response.

We'll use a generic aggregator API that supports multiple models. The concepts apply regardless of the specific provider.

Prerequisites

  • An API key from the aggregator (top up with USDC on Base, no KYC).
  • Basic knowledge of HTTP requests and JSON.
  • A local environment to run code (Python or Node.js examples below).

Step 1: Define the Weather Tool Schema

First, describe the weather tool in JSON Schema format. This tells the model what the tool does and what parameters it accepts.

{
  "name": "get_weather",
  "description": "Get current weather for a location",
  "parameters": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City and country, e.g., London, UK"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "Temperature unit"
      }
    },
    "required": ["location"]
  }
}

Keep the schema simple. The model uses this to decide when and how to call the tool.

Step 2: Send the Initial Request

Include the tool schema in your API request. The model will either answer directly or emit a tool call.

import requests

API_KEY = "your_api_key"
URL = "https://api.aggregator.com/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

payload = {
    "model": "claude-3-haiku",  # Replace with any supported model
    "messages": [
        {"role": "user", "content": "What's the weather in Paris?"}
    ],
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get current weather for a location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {"type": "string"},
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                    },
                    "required": ["location"]
                }
            }
        }
    ],
    "tool_choice": "auto"
}

response = requests.post(URL, headers=headers, json=payload)
print(response.json())

The response may contain a tool_calls array in the assistant message.

Step 3: Parse the Tool Call

The model returns a message with tool_calls. Extract the function name and arguments.

response_data = response.json()
message = response_data["choices"][0]["message"]

if "tool_calls" in message:
    tool_call = message["tool_calls"][0]
    function_name = tool_call["function"]["name"]
    arguments = json.loads(tool_call["function"]["arguments"])
    print(f"Model wants to call {function_name} with {arguments}")
else:
    print("No tool call. Model responded directly:", message["content"])

Step 4: Execute the Tool Locally

Implement the weather tool. For demonstration, we'll mock a weather API call.

def get_weather(location: str, unit: str = "celsius") -> dict:
    # In production, call a real weather API here.
    # This is a mock response.
    return {
        "location": location,
        "temperature": 22,
        "unit": unit,
        "condition": "Partly cloudy"
    }

# Execute based on the tool call
if function_name == "get_weather":
    result = get_weather(**arguments)
    print("Tool result:", result)

Step 5: Send the Tool Result Back

Append the assistant's tool call message and a new message with the tool result to the conversation. Then send a second request.

messages = [
    {"role": "user", "content": "What's the weather in Paris?"},
    message,  # assistant message with tool_calls
    {
        "role": "tool",
        "tool_call_id": tool_call["id"],
        "content": json.dumps(result)
    }
]

payload["messages"] = messages
response2 = requests.post(URL, headers=headers, json=payload)
final_message = response2.json()["choices"][0]["message"]["content"]
print("Final answer:", final_message)

The model now sees the tool output and generates a natural language response.

Step 6: Putting It All Together

Here's the complete flow in a single script:

import requests, json

API_KEY = "your_api_key"
URL = "https://api.aggregator.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

def get_weather(location, unit="celsius"):
    return {"location": location, "temperature": 22, "unit": unit, "condition": "Partly cloudy"}

# Initial request
payload = {
    "model": "claude-3-haiku",
    "messages": [{"role": "user", "content": "What's the weather in Paris?"}],
    "tools": [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["location"]
            }
        }
    }],
    "tool_choice": "auto"
}

resp = requests.post(URL, headers=headers, json=payload).json()
msg = resp["choices"][0]["message"]

if "tool_calls" in msg:
    tool_call = msg["tool_calls"][0]
    args = json.loads(tool_call["function"]["arguments"])
    result = get_weather(**args)
    
    payload["messages"].append(msg)
    payload["messages"].append({
        "role": "tool",
        "tool_call_id": tool_call["id"],
        "content": json.dumps(result)
    })
    
    resp2 = requests.post(URL, headers=headers, json=payload).json()
    print(resp2["choices"][0]["message"]["content"])
else:
    print(msg["content"])

Best Practices

  • Validate arguments: Always check that the model's arguments match your schema before execution.
  • Handle errors: If the tool fails, return an error message in the tool result so the model can respond appropriately.
  • Iterate: The model can request multiple tool calls in sequence. Loop until no more tool calls are emitted.
  • Keep schemas simple: Overly complex schemas confuse models.

Using the Aggregator

The aggregator provides a unified API for many models. You can switch between Claude, GPT, DeepSeek, Qwen, GLM, Kimi, and others by changing the model field. Billing is straightforward: you pay official price × 1.3, and key contributors are credited official price × 1.1 (premium × 1.2) in USDC. No KYC is required; top up with USDC on Base.

Conclusion

You've built a minimal weather agent using tool calling. The pattern extends to any external API: define the schema, let the model request, execute locally, and return results. With the aggregator, you can experiment across multiple models using a single API key and transparent pricing.