Build a Weather Assistant with Tool Use

Use a free LLM via OpenRouter to build a chatbot that fetches real weather data.

Introduction

30 Second Summary

Ever asked your phone about the weather and wondered how it knows to check a forecast service instead of just making something up? Behind the scenes, the AI decides it needs real data, fetches it, and turns raw numbers into a friendly answer.

In this project, you will build a command-line Weather Assistant powered by a free AI model. The model uses tool use (also called function calling) to fetch live weather data whenever you ask a question like "What's the weather in Tokyo?"

What You'll Build

A Python script that connects a free LLM (via OpenRouter) to a real weather API, using the tool use pattern that powers modern AI agents.

By the end of this project, you'll have:

  • A tool definition using JSON Schema in the OpenAI function calling format.
  • A weather function that fetches live forecasts from the Open-Meteo API.
  • A complete tool use loop where the AI decides when to call your tool and synthesizes the results into a natural response.
  • Secret Mission: Add a second tool for timezone lookups so your assistant can answer compound questions like "What's the weather and time in London?"

Are there any prerequisites?

You need basic Python knowledge (variables, functions, if-statements). No prior AI or API experience is needed. Everything else is covered step by step.

Before We Start

Before We Start...

Set Up Your Environment

Your Weather Assistant needs three pieces to work: Python to run the code, the OpenAI Python SDK to communicate with AI models, and an OpenRouter API key that gives your code access to free AI models.

In this step, you'll install Python and the SDK, then create a free OpenRouter account to get your API key. Everything here is completely free.

In this step, get ready to:
  • Install Python and verify version 3.9+ is available.
  • Install the OpenAI Python SDK.
  • Create an OpenRouter account and set up your API key.
Check your Python version

Python is the programming language you'll use to write your Weather Assistant. The OpenAI SDK requires Python 3.9 or higher.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

You should see a version number printed in your terminal.

✔️ I see version 3.9 or higher

You're all set. Move on to the next substep below.

ⓧ I see an older version

You need Python 3.9 or higher for this project. Your current version is too old.

  • Go to python.org/downloads in your browser.
  • Download and run the latest Python installer for your operating system.
  • On Windows, make sure to check the Add to PATH option during installation.
  • Close and reopen your terminal, then run the version check again to confirm.

macOS users with older system Python

macOS may ship with an older Python. After installing a newer version from python.org, use python3 (not python) to ensure you are running the newly installed version.

ⓧ Command not found

Python is not installed on your machine yet.

  • Go to python.org/downloads in your browser.
  • Download and run the latest Python installer for your operating system.
  • On Windows, make sure to check the Add to PATH option during installation.
  • Close and reopen your terminal, then run the version check again to confirm.
Install the OpenAI Python SDK

The OpenAI Python SDK is the library your code uses to talk to AI models. You'll point it at OpenRouter instead of OpenAI directly, so it acts as a universal adapter for accessing free models.

  • Install the SDK by running this command in your terminal:

macOS

pip3 install openai

Windows

pip install openai

What does this command do?

The pip install openai command downloads the OpenAI Python SDK from the Python package registry and installs it on your machine. This gives you access to the OpenAI class that you'll use to send requests to AI models through OpenRouter.

  • Confirm the install succeeded by checking that no error messages appeared. You should see output ending with a line like Successfully installed openai-... followed by version information.

Seeing a permission error?

If you see a permissions-related error on macOS, try running pip3 install --user openai instead. On Windows, try opening Command Prompt as Administrator.

If pip is not recognized, make sure Python was added to your PATH during installation. Close and reopen your terminal after installing Python.

Help me troubleshoot this error

Create your OpenRouter account and set up your API key

OpenRouter is a free service that routes your requests to AI models. You need an API key so your Python code can authenticate with their servers.

  • Open openrouter.ai in your browser.
  • Click the sign-up button and create a free account. You can sign up with Google or email.
  • Once signed in, navigate to openrouter.ai/settings/keys to open the API keys page.
  • Click Create Key.
  • Give your key a name (e.g. weather-assistant) and click Create.
  • Copy the key immediately and paste it somewhere safe (a notes app or text file). The full key is only shown once.

Why OpenRouter?

OpenRouter gives you access to free AI models that support tool calling. The openrouter/free model ID automatically picks a free model with the capabilities your request needs. No credits or payment required.

Since OpenRouter's API is OpenAI-compatible, you can use the well-documented OpenAI SDK as your client. If you ever want to switch to a paid model later, you only change one line of code.

Now set your API key as an environment variable so it's accessible to your script.

  • Set your API key as an environment variable by running this command (replace the placeholder with your actual key):

macOS

export OPENROUTER_API_KEY=your-openrouter-api-key-here

Windows

$env:OPENROUTER_API_KEY="your-openrouter-api-key-here"

What is an environment variable?

An environment variable stores a value in your terminal session that programs can read. Storing your API key this way keeps it out of your code files, which is a security best practice.

Keep your key handy. You'll paste it directly into your code in the next step as well, which keeps this project simple.

Key not showing after creation?

OpenRouter only displays the full key once, at the moment you create it. If you missed it, delete the key from the keys page and create a new one.

Help me get my API key set up

Your environment is ready. Next up, you'll create your Python file and define the weather tool that teaches the AI model how to fetch real forecast data.

Define Your Weather Tool

Your environment is ready and your API key is in place. Now it's time to build the core of your Weather Assistant.

In this step, you'll define a weather tool that the AI model can call whenever it needs real forecast data. This means writing a JSON Schema description of the tool (so the model knows when and how to use it) and a Python function that actually fetches live weather data from the Open-Meteo API.

In this step, get ready to:
  • Create your Python file and connect to OpenRouter.
  • Define a weather tool using the OpenAI function calling format.
  • Write a function that fetches real weather data from the Open-Meteo API.
Create your Python file and connect to OpenRouter

Every tool-using AI app starts with a connection to the model. You'll create a single Python file that holds your entire Weather Assistant and point the OpenAI SDK at OpenRouter so you can use free models.

  • In VS Code, create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save the file immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it weather_assistant.py on your Desktop.
  • Add the following imports and client setup code to the file:
import json
import urllib.request
import urllib.parse
from openai import OpenAI

# Initialize the OpenAI client pointed at OpenRouter
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="your-openrouter-api-key-here",  # Replace with your key, or use an environment variable
)

# Use the free models router - it auto-selects a free model that supports tool calling
MODEL = "openrouter/free"

What does this code do?

  • The import lines bring in Python's built-in modules for working with JSON data and making web requests. No extra installs needed beyond the openai package.
  • The client object is an OpenAI SDK instance, but pointed at OpenRouter's URL instead of OpenAI's. This lets you use OpenRouter's free model router with the same familiar interface.
  • The MODEL variable is set to openrouter/free, which automatically selects a free model that supports tool calling.
  • Replace your-openrouter-api-key-here with the API key you created in the previous step.
  • Save weather_assistant.py.

Not sure where your API key is?

Return to openrouter.ai/settings/keys in your browser to find or regenerate your key. Make sure you paste the full key string between the quotes.

help me find my OpenRouter API key

Define the weather tool in OpenAI function calling format

For an LLM to use a tool, it needs a structured description of what the tool does and what inputs it accepts. This description follows the OpenAI function calling format, which uses JSON Schema to define parameters.

  • Add the tool definition below your MODEL line by pasting this code:
# Define the weather tool in OpenAI function calling format
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather forecast for a given location. Returns temperature, wind speed, and weather conditions. Use this tool when the user asks about weather, temperature, or forecast for any location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city or location to get weather for, e.g. 'London' or 'Tokyo'"
                    },
                    "forecast_days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7). Defaults to 1 for current weather."
                    }
                },
                "required": ["location"]
            }
        }
    }
]

What does this tool definition do?

This tools list tells the model everything it needs to decide when and how to call your weather function.

  • The name field identifies the tool so your code knows which function to run when the model calls it.
  • The description tells the model when this tool is appropriate. A good description helps the model decide whether to call the tool or answer from its own knowledge.
  • The parameters section uses JSON Schema to define the inputs. Only location is required. The model will extract these values from the user's natural language question.
  • Save weather_assistant.py.

You won't see a visible change yet. This definition gets sent to the model in the next step so it knows what tools are available.

Write the weather fetching functions

The tool definition tells the model what inputs to provide. Now you need the actual Python functions that take those inputs, call the Open-Meteo API, and return real weather data.

You'll write two functions: a helper that converts a city name to coordinates, and the main function that fetches the forecast.

  • Add the geocoding helper function below the tools list by pasting this code:
def geocode_location(location):
    """Convert a location name to latitude and longitude using Open-Meteo's geocoding API."""
    params = urllib.parse.urlencode({"name": location, "count": 1, "language": "en", "format": "json"})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode())

    if "results" not in data or len(data["results"]) == 0:
        return None

    result = data["results"][0]
    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", "Unknown")
    }

What does this function do?

Weather APIs need latitude and longitude, but users type city names like "Tokyo" or "London". This helper bridges the gap by calling Open-Meteo's free geocoding endpoint to convert a city name into coordinates.

If the location can't be found, it returns None so the main function can return an error message gracefully.

  • Save weather_assistant.py.
  • The next function will use this helper to fetch the actual forecast.
  • Add the main weather fetching function below geocode_location by pasting this code:
def fetch_weather(location, forecast_days=1):
    """Fetch weather data from the Open-Meteo API."""
    geo = geocode_location(location)
    if geo is None:
        return json.dumps({"error": f"Could not find location: {location}"})

    params = urllib.parse.urlencode({
        "latitude": geo["latitude"],
        "longitude": geo["longitude"],
        "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": forecast_days
    })
    url = f"https://api.open-meteo.com/v1/forecast?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        weather_data = json.loads(response.read().decode())

    result = {
        "location": f"{geo['name']}, {geo['country']}",
        "current": {
            "temperature_celsius": weather_data["current"]["temperature_2m"],
            "humidity_percent": weather_data["current"]["relative_humidity_2m"],
            "wind_speed_kmh": weather_data["current"]["wind_speed_10m"],
            "weather_code": weather_data["current"]["weather_code"]
        },
        "daily_forecast": []
    }

    for i in range(forecast_days):
        result["daily_forecast"].append({
            "date": weather_data["daily"]["time"][i],
            "max_temp_celsius": weather_data["daily"]["temperature_2m_max"][i],
            "min_temp_celsius": weather_data["daily"]["temperature_2m_min"][i],
            "precipitation_mm": weather_data["daily"]["precipitation_sum"][i],
            "weather_code": weather_data["daily"]["weather_code"][i]
        })

    return json.dumps(result)

What does this function do?

This is the function the model will ultimately trigger when it decides it needs weather data.

  • It calls geocode_location to convert the city name to coordinates.
  • It requests current conditions (temperature, humidity, wind) and daily forecast data from the Open-Meteo forecast endpoint.
  • It packages everything into a clean JSON string that the model can read and summarize for the user.
  • Save weather_assistant.py.
  • Test that your functions work by opening a terminal in VS Code with Ctrl+` and running:
python3 -c "from weather_assistant import fetch_weather; print(fetch_weather('Tokyo'))"

You should see a JSON string containing Tokyo's current temperature, humidity, wind speed, and a daily forecast.

Seeing a ModuleNotFoundError or file not found?

  • Make sure your terminal is in the same folder as weather_assistant.py. Run ls to check.
  • If you saved the file on your Desktop, run cd ~/Desktop first.
  • On Windows, use python instead of python3 if python3 is not recognized.

help me debug why my weather_assistant.py import is failing

Why use urllib instead of requests?

The urllib module is part of Python's standard library, so you don't need to install anything extra. This keeps the project's dependencies minimal. The only non-standard package you need is openai.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete weather_assistant.py file so far:

import json
import urllib.request
import urllib.parse
from openai import OpenAI

# Initialize the OpenAI client pointed at OpenRouter
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="your-openrouter-api-key-here",  # Replace with your key, or use an environment variable
)

# Use the free models router - it auto-selects a free model that supports tool calling
MODEL = "openrouter/free"

# Define the weather tool in OpenAI function calling format
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather forecast for a given location. Returns temperature, wind speed, and weather conditions. Use this tool when the user asks about weather, temperature, or forecast for any location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city or location to get weather for, e.g. 'London' or 'Tokyo'"
                    },
                    "forecast_days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7). Defaults to 1 for current weather."
                    }
                },
                "required": ["location"]
            }
        }
    }
]


def geocode_location(location):
    """Convert a location name to latitude and longitude using Open-Meteo's geocoding API."""
    params = urllib.parse.urlencode({"name": location, "count": 1, "language": "en", "format": "json"})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode())

    if "results" not in data or len(data["results"]) == 0:
        return None

    result = data["results"][0]
    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", "Unknown")
    }


def fetch_weather(location, forecast_days=1):
    """Fetch weather data from the Open-Meteo API."""
    geo = geocode_location(location)
    if geo is None:
        return json.dumps({"error": f"Could not find location: {location}"})

    params = urllib.parse.urlencode({
        "latitude": geo["latitude"],
        "longitude": geo["longitude"],
        "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": forecast_days
    })
    url = f"https://api.open-meteo.com/v1/forecast?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        weather_data = json.loads(response.read().decode())

    result = {
        "location": f"{geo['name']}, {geo['country']}",
        "current": {
            "temperature_celsius": weather_data["current"]["temperature_2m"],
            "humidity_percent": weather_data["current"]["relative_humidity_2m"],
            "wind_speed_kmh": weather_data["current"]["wind_speed_10m"],
            "weather_code": weather_data["current"]["weather_code"]
        },
        "daily_forecast": []
    }

    for i in range(forecast_days):
        result["daily_forecast"].append({
            "date": weather_data["daily"]["time"][i],
            "max_temp_celsius": weather_data["daily"]["temperature_2m_max"][i],
            "min_temp_celsius": weather_data["daily"]["temperature_2m_min"][i],
            "precipitation_mm": weather_data["daily"]["precipitation_sum"][i],
            "weather_code": weather_data["daily"]["weather_code"][i]
        })

    return json.dumps(result)

Your weather tool is fully defined and tested. Next up, you'll build the tool use loop that lets the AI model call this function automatically when a user asks about the weather.

Build the Tool Use Loop

You now have a tool definition that tells the model what your weather function can do, and the actual function that fetches real data.

The missing piece is the loop that connects them. When a user asks a weather question, the model needs to decide to call your tool, you need to execute it, and then send the results back so the model can write a friendly answer. This is the core pattern behind every AI agent that interacts with the real world.

In this step, get ready to:
  • Write a dispatcher function that routes tool calls to the correct function.
  • Build the chat function that sends messages to the model and handles tool use in a loop.
Write the tool call dispatcher

When the model decides to use a tool, it sends back the tool name and arguments as a JSON string. You need a function that receives this information and routes it to the correct Python function.

  • In weather_assistant.py, scroll to the bottom of the file (below the fetch_weather function).
  • Add the tool call dispatcher by pasting this code:
def process_tool_call(tool_name, tool_arguments):
    """Execute the appropriate tool based on the tool name."""
    if tool_name == "get_weather":
        args = json.loads(tool_arguments)
        return fetch_weather(
            location=args["location"],
            forecast_days=args.get("forecast_days", 1)
        )
    return json.dumps({"error": f"Unknown tool: {tool_name}"})

What does this code do?

This function acts as a router. The model sends back tool calls as JSON strings, so this function parses the arguments and calls the matching Python function.

  • json.loads(tool_arguments) parses the JSON string the model returns into a Python dictionary.
  • The fallback return json.dumps({"error": ...}) handles unexpected tool names gracefully.
  • Save weather_assistant.py.
  • You won't see a change yet. The next function will use this dispatcher to complete the loop.

Seeing a syntax error?

  • Make sure there are two blank lines between the end of fetch_weather and the start of process_tool_call. Python expects blank lines between top-level functions.
  • Check that your indentation uses spaces, not tabs. Mixing them causes invisible errors.

Why is my process_tool_call function showing an error?

Send messages to the model and handle tool calls

The tool use loop follows a specific pattern. You send a message to the model. If the model wants to use a tool, it responds with a tool_calls field instead of a text answer. You execute the tool, send the result back, and ask the model again. This continues until the model is satisfied and returns a normal text response.

  • Below process_tool_call, add the main chat function by pasting this code:
def chat(user_message, conversation_history):
    """Send a message to the model and handle tool use."""
    conversation_history.append({"role": "user", "content": user_message})

    # Initial API call
    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
            *conversation_history
        ]
    )

    message = response.choices[0].message

    # Tool use loop: keep going until the model is done
    while message.tool_calls:
        # Process each tool call
        tool_call = message.tool_calls[0]
        tool_name = tool_call.function.name
        tool_arguments = tool_call.function.arguments

        print(f"  [Calling tool: {tool_name} with args: {tool_arguments}]")

        # Execute the tool
        tool_result = process_tool_call(tool_name, tool_arguments)

        # Add assistant's response and tool result to conversation
        conversation_history.append({
            "role": "assistant",
            "content": message.content,
            "tool_calls": [
                {
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": tool_name,
                        "arguments": tool_arguments
                    }
                }
            ]
        })
        conversation_history.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": tool_result
        })

        # Call the API again with the tool result
        response = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            tools=tools,
            messages=[
                {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
                *conversation_history
            ]
        )
        message = response.choices[0].message

    # Extract the final text response
    assistant_message = message.content or ""
    conversation_history.append({"role": "assistant", "content": assistant_message})
    return assistant_message

What does this code do?

This function implements the complete tool use loop in three phases:

  • Phase 1 (Initial call): Appends the user message to the conversation history and sends everything to the model along with the tools list. The tools parameter must be included in every request so the router can validate the schema.
  • Phase 2 (Tool use while loop): If the model responds with tool_calls, the loop extracts the tool name and arguments, calls process_tool_call, appends the result as a tool role message, and calls the API again.
  • Phase 3 (Final response): When the model has all the information it needs, it stops requesting tools. The loop exits, and the function returns the model's natural language answer.

Why a while loop instead of a single check?

A model might need to call a tool more than once. For example, if a user asks "Compare the weather in Tokyo and London," the model could make two sequential tool calls. The while loop keeps processing tool calls until the model is done.

  • Save weather_assistant.py.
  • You won't see output yet. In the next step, you will add an interactive chat loop that calls this function and prints the results.

Seeing an indentation error on save?

  • Make sure the entire while message.tool_calls: block is indented one level inside the chat function (4 spaces).
  • Check that conversation_history.append calls inside the loop are indented 8 spaces (two levels in from the function definition).
  • If you see a red underline on client or tools, confirm they are defined earlier in the same file (not inside another function).

Help me fix my chat function in weather_assistant.py

✔️ Awesome, I've got everything!

Great. Double check you have saved weather_assistant.py with both new functions.

ⓧ I'd like to double check the full code

Here is the complete weather_assistant.py file so far. Compare it with yours:

import json
import urllib.request
import urllib.parse
from openai import OpenAI

# Initialize the OpenAI client pointed at OpenRouter
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="your-openrouter-api-key-here",  # Replace with your key, or use an environment variable
)

# Use the free models router - it auto-selects a free model that supports tool calling
MODEL = "openrouter/free"

# Define the weather tool in OpenAI function calling format
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather forecast for a given location. Returns temperature, wind speed, and weather conditions. Use this tool when the user asks about weather, temperature, or forecast for any location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city or location to get weather for, e.g. 'London' or 'Tokyo'"
                    },
                    "forecast_days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7). Defaults to 1 for current weather."
                    }
                },
                "required": ["location"]
            }
        }
    }
]


def geocode_location(location):
    """Convert a location name to latitude and longitude using Open-Meteo's geocoding API."""
    params = urllib.parse.urlencode({"name": location, "count": 1, "language": "en", "format": "json"})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode())

    if "results" not in data or len(data["results"]) == 0:
        return None

    result = data["results"][0]
    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", "Unknown")
    }


def fetch_weather(location, forecast_days=1):
    """Fetch weather data from the Open-Meteo API."""
    geo = geocode_location(location)
    if geo is None:
        return json.dumps({"error": f"Could not find location: {location}"})

    params = urllib.parse.urlencode({
        "latitude": geo["latitude"],
        "longitude": geo["longitude"],
        "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": forecast_days
    })
    url = f"https://api.open-meteo.com/v1/forecast?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        weather_data = json.loads(response.read().decode())

    result = {
        "location": f"{geo['name']}, {geo['country']}",
        "current": {
            "temperature_celsius": weather_data["current"]["temperature_2m"],
            "humidity_percent": weather_data["current"]["relative_humidity_2m"],
            "wind_speed_kmh": weather_data["current"]["wind_speed_10m"],
            "weather_code": weather_data["current"]["weather_code"]
        },
        "daily_forecast": []
    }

    for i in range(forecast_days):
        result["daily_forecast"].append({
            "date": weather_data["daily"]["time"][i],
            "max_temp_celsius": weather_data["daily"]["temperature_2m_max"][i],
            "min_temp_celsius": weather_data["daily"]["temperature_2m_min"][i],
            "precipitation_mm": weather_data["daily"]["precipitation_sum"][i],
            "weather_code": weather_data["daily"]["weather_code"][i]
        })

    return json.dumps(result)


def process_tool_call(tool_name, tool_arguments):
    """Execute the appropriate tool based on the tool name."""
    if tool_name == "get_weather":
        args = json.loads(tool_arguments)
        return fetch_weather(
            location=args["location"],
            forecast_days=args.get("forecast_days", 1)
        )
    return json.dumps({"error": f"Unknown tool: {tool_name}"})


def chat(user_message, conversation_history):
    """Send a message to the model and handle tool use."""
    conversation_history.append({"role": "user", "content": user_message})

    # Initial API call
    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
            *conversation_history
        ]
    )

    message = response.choices[0].message

    # Tool use loop: keep going until the model is done
    while message.tool_calls:
        # Process each tool call
        tool_call = message.tool_calls[0]
        tool_name = tool_call.function.name
        tool_arguments = tool_call.function.arguments

        print(f"  [Calling tool: {tool_name} with args: {tool_arguments}]")

        # Execute the tool
        tool_result = process_tool_call(tool_name, tool_arguments)

        # Add assistant's response and tool result to conversation
        conversation_history.append({
            "role": "assistant",
            "content": message.content,
            "tool_calls": [
                {
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": tool_name,
                        "arguments": tool_arguments
                    }
                }
            ]
        })
        conversation_history.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": tool_result
        })

        # Call the API again with the tool result
        response = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            tools=tools,
            messages=[
                {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
                *conversation_history
            ]
        )
        message = response.choices[0].message

    # Extract the final text response
    assistant_message = message.content or ""
    conversation_history.append({"role": "assistant", "content": assistant_message})
    return assistant_message

Your tool use loop is ready. Next up, you will wrap it in an interactive chat so you can actually talk to your weather assistant and see it call tools in real time.

Create the Interactive Chat

You have a complete tool use loop that can send messages, detect tool calls, execute them, and return the model's final response. The missing piece is a way for you to actually talk to it.

In this step, you'll add an interactive chat loop that reads your questions from the terminal, feeds them through the chat function, and prints the assistant's responses. You'll also maintain conversation history so the model remembers earlier messages.

In this step, get ready to:
  • Add a main function with an interactive input loop.
  • Maintain multi-turn conversation history.
  • Test the assistant with real weather questions.
Add the main function and input loop

The main() function is where everything comes together. It creates an empty conversation history list, then runs a loop that continuously reads your input and passes it to the chat function you built in the previous step.

  • In weather_assistant.py, scroll to the very bottom of the file (after the chat function) and add the following code:
def main():
    """Main function to run the interactive chat."""
    print("Weather Assistant (powered by free models via OpenRouter)")
    print("Ask me about the weather anywhere in the world!")
    print("Type 'quit' to exit.\n")

    conversation_history = []

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit", "q"):
            print("Goodbye!")
            break
        if not user_input:
            continue

        try:
            response = chat(user_input, conversation_history)
            print(f"\nAssistant: {response}\n")
        except Exception as e:
            print(f"\nError: {e}\n")


if __name__ == "__main__":
    main()

What does this code do?

  • conversation_history = [] creates an empty list that grows with each message. Because you pass this same list to chat() every time, the model can remember what you talked about earlier.
  • while True keeps the loop running until you type quit, exit, or q.
  • The try/except block catches network errors or API failures so the script does not crash mid-conversation.
  • if __name__ == "__main__" ensures main() only runs when you execute the file directly, not when it is imported by another script.
  • Save weather_assistant.py.

Why does conversation history matter?

Without history, each message would be treated as a brand-new conversation. The model would have no idea what "there" means if you asked "What about tomorrow there?" after asking about Paris.

By passing the growing conversation_history list on every call, the model sees the full thread and can resolve follow-up questions naturally.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on to the test.

ⓧ I'd like to double check the full code

Here is the complete weather_assistant.py file at this point:

import json
import urllib.request
import urllib.parse
from openai import OpenAI

# Initialize the OpenAI client pointed at OpenRouter
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="your-openrouter-api-key-here",  # Replace with your key, or use an environment variable
)

# Use the free models router - it auto-selects a free model that supports tool calling
MODEL = "openrouter/free"

# Define the weather tool in OpenAI function calling format
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather forecast for a given location. Returns temperature, wind speed, and weather conditions. Use this tool when the user asks about weather, temperature, or forecast for any location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city or location to get weather for, e.g. 'London' or 'Tokyo'"
                    },
                    "forecast_days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7). Defaults to 1 for current weather."
                    }
                },
                "required": ["location"]
            }
        }
    }
]


def geocode_location(location):
    """Convert a location name to latitude and longitude using Open-Meteo's geocoding API."""
    params = urllib.parse.urlencode({"name": location, "count": 1, "language": "en", "format": "json"})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode())

    if "results" not in data or len(data["results"]) == 0:
        return None

    result = data["results"][0]
    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", "Unknown")
    }


def fetch_weather(location, forecast_days=1):
    """Fetch weather data from the Open-Meteo API."""
    geo = geocode_location(location)
    if geo is None:
        return json.dumps({"error": f"Could not find location: {location}"})

    params = urllib.parse.urlencode({
        "latitude": geo["latitude"],
        "longitude": geo["longitude"],
        "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": forecast_days
    })
    url = f"https://api.open-meteo.com/v1/forecast?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        weather_data = json.loads(response.read().decode())

    result = {
        "location": f"{geo['name']}, {geo['country']}",
        "current": {
            "temperature_celsius": weather_data["current"]["temperature_2m"],
            "humidity_percent": weather_data["current"]["relative_humidity_2m"],
            "wind_speed_kmh": weather_data["current"]["wind_speed_10m"],
            "weather_code": weather_data["current"]["weather_code"]
        },
        "daily_forecast": []
    }

    for i in range(forecast_days):
        result["daily_forecast"].append({
            "date": weather_data["daily"]["time"][i],
            "max_temp_celsius": weather_data["daily"]["temperature_2m_max"][i],
            "min_temp_celsius": weather_data["daily"]["temperature_2m_min"][i],
            "precipitation_mm": weather_data["daily"]["precipitation_sum"][i],
            "weather_code": weather_data["daily"]["weather_code"][i]
        })

    return json.dumps(result)


def process_tool_call(tool_name, tool_arguments):
    """Execute the appropriate tool based on the tool name."""
    if tool_name == "get_weather":
        args = json.loads(tool_arguments)
        return fetch_weather(
            location=args["location"],
            forecast_days=args.get("forecast_days", 1)
        )
    return json.dumps({"error": f"Unknown tool: {tool_name}"})


def chat(user_message, conversation_history):
    """Send a message to the model and handle tool use."""
    conversation_history.append({"role": "user", "content": user_message})

    # Initial API call
    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
            *conversation_history
        ]
    )

    message = response.choices[0].message

    # Tool use loop: keep going until the model is done
    while message.tool_calls:
        # Process each tool call
        tool_call = message.tool_calls[0]
        tool_name = tool_call.function.name
        tool_arguments = tool_call.function.arguments

        print(f"  [Calling tool: {tool_name} with args: {tool_arguments}]")

        # Execute the tool
        tool_result = process_tool_call(tool_name, tool_arguments)

        # Add assistant's response and tool result to conversation
        conversation_history.append({
            "role": "assistant",
            "content": message.content,
            "tool_calls": [
                {
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": tool_name,
                        "arguments": tool_arguments
                    }
                }
            ]
        })
        conversation_history.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": tool_result
        })

        # Call the API again with the tool result
        response = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            tools=tools,
            messages=[
                {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
                *conversation_history
            ]
        )
        message = response.choices[0].message

    # Extract the final text response
    assistant_message = message.content or ""
    conversation_history.append({"role": "assistant", "content": assistant_message})
    return assistant_message


def main():
    """Main function to run the interactive chat."""
    print("Weather Assistant (powered by free models via OpenRouter)")
    print("Ask me about the weather anywhere in the world!")
    print("Type 'quit' to exit.\n")

    conversation_history = []

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit", "q"):
            print("Goodbye!")
            break
        if not user_input:
            continue

        try:
            response = chat(user_input, conversation_history)
            print(f"\nAssistant: {response}\n")
        except Exception as e:
            print(f"\nError: {e}\n")


if __name__ == "__main__":
    main()
Run and test your Weather Assistant

Your script is complete. Time to run it and have a conversation with your assistant.

  • Run your Weather Assistant by executing this command in your terminal:
python weather_assistant.py

You should see the welcome message appear, followed by a You: prompt waiting for your input.

  • Try asking a weather question. Type the following and press Enter:
What's the weather in Paris?

You should see a line like [Calling tool: get_weather with args: ...] printed, followed by a friendly response with real temperature and weather data for Paris.

  • Now test multi-turn context. Without exiting, type:
What about next week?

The model remembers you were asking about Paris and fetches a multi-day forecast. This is conversation history in action.

  • When you are done, type quit to exit.

Seeing an error when you run the script?

  • If you see an authentication error, double-check that your API key in the file matches the key from your OpenRouter dashboard.
  • If you see a ModuleNotFoundError for openai, make sure you installed it with pip install openai and that you are running the script with the same Python that has the package installed.
  • If the model responds but never calls the tool, free model availability can vary. Wait a moment and try again.

Need more help? Help me debug my Weather Assistant script

Your Weather Assistant is working. Next up, you'll put it through tougher tests and add error handling for edge cases like invalid locations and network failures.

Test and Refine

Your Weather Assistant is fully built. Now it's time to push it to its limits and see how the model behaves in different scenarios.

Testing is where you develop an intuition for how tool use actually works. You'll see the model decide on its own when it needs weather data and when it can answer directly from its training.

In this step, get ready to:
  • Test edge cases like multiple cities and invalid locations.
  • Observe the model deciding when to use the tool vs. answering directly.
  • Confirm error handling for network failures and bad inputs.
Test with multiple cities and follow-up questions

Because your assistant maintains conversation history, the model can resolve follow-up questions like "What about next week?" by remembering the city from your previous message.

  • Run your Weather Assistant by running this command:
python weather_assistant.py
  • At the You: prompt, type a weather question for a specific city:
What's the weather in Paris?

You should see the [Calling tool: get_weather with args: ...] message appear, followed by a friendly weather summary from the assistant.

  • Now try a follow-up question without repeating the city name:
What about next week?

The model should call the tool again with the same city (Paris) but with a higher forecast_days value. This shows the conversation history is working.

  • Ask about a different city to test switching context:
What about Tokyo?

The model should recognize you're asking about weather in a new location and call the tool with Tokyo as the location.

Why does follow-up context work?

Your conversation_history list grows with each exchange. When the model receives the full history, it can infer that "What about next week?" refers to Paris because that's the city from the previous turn.

This is why you pass *conversation_history in every API call. Without it, each message would be treated as a brand new conversation.

Getting unexpected responses?

Free models via OpenRouter may vary in quality. If the model doesn't call the tool when it should, try rephrasing your question more explicitly (e.g., "Tell me the current weather forecast for Paris, France").

Free models may also have higher latency during peak usage. If responses are slow, wait a moment and try again.

Help me debug my Weather Assistant's tool calling behavior.

Observe tool use decisions with non-weather questions

The model doesn't blindly call your tool every time. It evaluates whether the user's question actually requires weather data.

  • Try asking a non-weather question:
What is the capital of France?

This time you should NOT see the [Calling tool: ...] message. The model answers directly from its training data because it doesn't need weather information.

  • Try a few more questions to see the decision boundary:
Should I bring an umbrella to London tomorrow?

The model should call the tool here because answering the umbrella question requires real precipitation data.

How does the model decide?

The model reads your tool's description field to understand when to use it. Your description says "Use this tool when the user asks about weather, temperature, or forecast for any location." The model matches that description against the user's intent.

This is why writing clear, specific tool descriptions matters. A vague description leads to unreliable tool calling behavior.

Verify error handling for edge cases

Real users will type unexpected things. Your assistant needs to handle invalid locations gracefully without crashing.

  • Try asking about a nonsense location:
What's the weather in Xyzzyville?

The model calls the tool, but geocode_location returns None for an unrecognized city. Your fetch_weather function returns a JSON error message, and the model relays that to you in a friendly way.

What about network failures?

Your main function already wraps the chat call in a try/except block. If the Open-Meteo API or OpenRouter is unreachable, the exception is caught and printed as an Error: message instead of crashing the entire script.

This means your assistant stays running even when something goes wrong. The learner can ask another question immediately after an error.

  • Type quit to exit the assistant when you're done testing.

Seeing a connection error immediately?

Make sure you replaced your-openrouter-api-key-here in your code with your actual OpenRouter API key. Also check that your internet connection is active.

Help me fix a connection error in my Weather Assistant.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

import json
import urllib.request
import urllib.parse
from openai import OpenAI

# Initialize the OpenAI client pointed at OpenRouter
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="your-openrouter-api-key-here",  # Replace with your key, or use an environment variable
)

# Use the free models router - it auto-selects a free model that supports tool calling
MODEL = "openrouter/free"

# Define the weather tool in OpenAI function calling format
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather forecast for a given location. Returns temperature, wind speed, and weather conditions. Use this tool when the user asks about weather, temperature, or forecast for any location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city or location to get weather for, e.g. 'London' or 'Tokyo'"
                    },
                    "forecast_days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7). Defaults to 1 for current weather."
                    }
                },
                "required": ["location"]
            }
        }
    }
]


def geocode_location(location):
    """Convert a location name to latitude and longitude using Open-Meteo's geocoding API."""
    params = urllib.parse.urlencode({"name": location, "count": 1, "language": "en", "format": "json"})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode())

    if "results" not in data or len(data["results"]) == 0:
        return None

    result = data["results"][0]
    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", "Unknown")
    }


def fetch_weather(location, forecast_days=1):
    """Fetch weather data from the Open-Meteo API."""
    geo = geocode_location(location)
    if geo is None:
        return json.dumps({"error": f"Could not find location: {location}"})

    params = urllib.parse.urlencode({
        "latitude": geo["latitude"],
        "longitude": geo["longitude"],
        "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": forecast_days
    })
    url = f"https://api.open-meteo.com/v1/forecast?{params}"

    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
        weather_data = json.loads(response.read().decode())

    result = {
        "location": f"{geo['name']}, {geo['country']}",
        "current": {
            "temperature_celsius": weather_data["current"]["temperature_2m"],
            "humidity_percent": weather_data["current"]["relative_humidity_2m"],
            "wind_speed_kmh": weather_data["current"]["wind_speed_10m"],
            "weather_code": weather_data["current"]["weather_code"]
        },
        "daily_forecast": []
    }

    for i in range(forecast_days):
        result["daily_forecast"].append({
            "date": weather_data["daily"]["time"][i],
            "max_temp_celsius": weather_data["daily"]["temperature_2m_max"][i],
            "min_temp_celsius": weather_data["daily"]["temperature_2m_min"][i],
            "precipitation_mm": weather_data["daily"]["precipitation_sum"][i],
            "weather_code": weather_data["daily"]["weather_code"][i]
        })

    return json.dumps(result)


def process_tool_call(tool_name, tool_arguments):
    """Execute the appropriate tool based on the tool name."""
    if tool_name == "get_weather":
        args = json.loads(tool_arguments)
        return fetch_weather(
            location=args["location"],
            forecast_days=args.get("forecast_days", 1)
        )
    return json.dumps({"error": f"Unknown tool: {tool_name}"})


def chat(user_message, conversation_history):
    """Send a message to the model and handle tool use."""
    conversation_history.append({"role": "user", "content": user_message})

    # Initial API call
    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
            *conversation_history
        ]
    )

    message = response.choices[0].message

    # Tool use loop: keep going until the model is done
    while message.tool_calls:
        # Process each tool call
        tool_call = message.tool_calls[0]
        tool_name = tool_call.function.name
        tool_arguments = tool_call.function.arguments

        print(f"  [Calling tool: {tool_name} with args: {tool_arguments}]")

        # Execute the tool
        tool_result = process_tool_call(tool_name, tool_arguments)

        # Add assistant's response and tool result to conversation
        conversation_history.append({
            "role": "assistant",
            "content": message.content,
            "tool_calls": [
                {
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": tool_name,
                        "arguments": tool_arguments
                    }
                }
            ]
        })
        conversation_history.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": tool_result
        })

        # Call the API again with the tool result
        response = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            tools=tools,
            messages=[
                {"role": "system", "content": "You are a helpful weather assistant. When users ask about weather, use the get_weather tool to fetch real data. Provide friendly, concise responses with the key weather information."},
                *conversation_history
            ]
        )
        message = response.choices[0].message

    # Extract the final text response
    assistant_message = message.content or ""
    conversation_history.append({"role": "assistant", "content": assistant_message})
    return assistant_message


def main():
    """Main function to run the interactive chat."""
    print("Weather Assistant (powered by free models via OpenRouter)")
    print("Ask me about the weather anywhere in the world!")
    print("Type 'quit' to exit.\n")

    conversation_history = []

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit", "q"):
            print("Goodbye!")
            break
        if not user_input:
            continue

        try:
            response = chat(user_input, conversation_history)
            print(f"\nAssistant: {response}\n")
        except Exception as e:
            print(f"\nError: {e}\n")


if __name__ == "__main__":
    main()

Your Weather Assistant is complete and battle-tested. You've seen the core loop in action: the model reads a question, decides it needs data, calls your tool, and weaves the result into a natural response. Next up, a Secret Mission that adds a second tool and shows how the model orchestrates multiple tool calls in a single conversation.

Secret mission

Add a Second Tool for Timezone Lookups

Your weather assistant can only answer one type of question right now. What if someone asks "What time is it in Tokyo and what's the weather like?" Time to teach your assistant a new trick by adding a second tool.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses free APIs, so there are no ongoing costs. Decide whether to keep your resources available for future experimentation or delete them entirely.

Resources you used:

  • Project folder with weather_assistant.py on your Desktop.
  • The openai Python package installed via pip.
  • Your OpenRouter API key at openrouter.ai/settings/keys.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your Weather Assistant or plan to add more tools.

  • Your project folder stays on your Desktop, ready to run anytime.
  • Your OpenRouter API key remains active for future projects.
  • The openai package stays installed in your Python environment.

Pause - I'll come back to this later

Since there are no running processes or paid services, pausing is simple. Just revoke your API key so it cannot be used if exposed.

  • Go to openrouter.ai/settings/keys in your browser.
  • Delete your API key. You can create a new one when you return.
  • Your project folder and installed packages stay intact for later.

Delete - I don't want to use this again

Remove all project resources from your machine and revoke your API key.

  • Delete your API key at openrouter.ai/settings/keys by clicking the trash icon next to it.
  • Uninstall the OpenAI package by running:
pip uninstall openai
  • Delete the project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf weather-assistant

On Windows, run:

cd ~\Desktop
rmdir /s /q weather-assistant

Nice Work!

Nice Work!

You just built an AI-powered Weather Assistant that uses tool use (function calling) to fetch real weather data from the Open-Meteo API. All for $0 using free models via OpenRouter.

You've learned how to:

  • Define tools using JSON Schema in the OpenAI function calling format, giving an LLM structured access to your own functions.
  • Implement the tool use loop, the core pattern behind every AI agent that interacts with the real world.
  • Connect a free LLM to a real external API (Open-Meteo) for live weather forecasts, and build an interactive multi-turn chatbot that maintains conversation history.
  • Secret Mission: Add a second tool and watch the model orchestrate multiple tool calls to answer compound questions.

Ready to quiz yourself?