Multi-Agent Travel Assistant with AI

Orchestrate specialized AI agents to answer complex travel queries locally.

Introduction

30 Second Summary

Planning a trip means juggling weather forecasts, budget estimates, and destination details from half a dozen different sources. What if a team of AI specialists could handle all of that for you at once?

In this project, you will build a multi-agent AI system that answers complex travel questions by decomposing them into subtasks, routing each subtask to a specialized agent, and assembling the results into a single travel briefing.

What You'll Build

A local multi-agent travel assistant powered by CrewAI and Ollama that decomposes travel queries, routes them to specialized agents with real-world API tools, and produces a structured travel briefing.

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

  • 3 specialized AI agents (Weather, Destination Info, Budget) that call free APIs autonomously to gather real travel data.
  • A coordinator agent that synthesizes all responses into a formatted travel briefing with clear sections.
  • A command-line interface for asking natural-language travel questions and receiving structured answers from your local LLM.
  • Secret Mission: Add a Packing Suggestions agent that recommends what to pack based on weather forecast data.

Are there any prerequisites?

You should be comfortable with basic Python (functions, dictionaries, installing packages). Familiarity with APIs is helpful but not required.

Before We Start

Before We Start...

Install Ollama and Pull a Model

Your multi-agent travel assistant needs a brain. That brain is a large language model (LLM) running on your own machine, completely free.

In this step, you will install Ollama, a tool that runs LLMs locally. You will then pull the qwen2.5:7b model, which has strong tool-calling capabilities that your agents need.

In this step, get ready to:
  • Install Ollama on your system.
  • Pull the qwen2.5:7b model.
  • Verify the model works with a test prompt.
Install Ollama

Ollama is a lightweight runtime that lets you download and run LLMs locally. It exposes a local API on port 11434 that your Python code will connect to later.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.

macOS

  • Install Ollama by running this command:
curl -fsSL https://ollama.com/install.sh | sh

What does this command do?

This downloads and runs Ollama's official install script. It detects your system architecture and installs the ollama CLI to your PATH.

Prefer a graphical installer?

You can also download Ollama from ollama.com/download and drag the app into your Applications folder. The app adds the ollama CLI to your PATH on first launch.

Windows

  • Install Ollama by running this command in PowerShell:
irm https://ollama.com/install.ps1 | iex

What does this command do?

This downloads and runs Ollama's official install script for Windows. It installs Ollama in your home directory without needing Administrator rights. The Ollama server starts automatically in the background.

  • Verify Ollama is installed by checking its version:
ollama -v

✔️ I see a version number

Ollama is installed and ready. Move on to the next substep.

ⓧ Command not found

  • On macOS, close and reopen your terminal so it picks up the new PATH entry.
  • On Windows, close PowerShell and reopen it. Ollama's installer adds itself to your PATH, but only new terminal sessions pick this up.
  • If it still fails, re-run the install command from the platform tab above.
Pull the qwen2.5:7b model

The qwen2.5:7b model offers strong tool-calling quality at roughly 5 GB of VRAM. This is the model your agents will use to reason about travel queries and decide which API tools to call.

  • Download the model by running this command:
ollama pull qwen2.5:7b

This download is roughly 4.7 GB. It may take a few minutes depending on your internet speed.

What is happening?

Ollama downloads the model weights to your local machine (~/.ollama by default). Once downloaded, the model runs entirely on your hardware with no internet required.

The pull command is like docker pull for container images. It fetches the model once, and you can run it as many times as you like.

  • Once the download finishes, confirm the model is available by listing your installed models:
ollama ls

You should see qwen2.5:7b in the list with its size displayed.

Download stuck or failing?

  • Check your internet connection and try running the pull command again. Ollama resumes incomplete downloads.
  • Make sure you have at least 5 GB of free disk space on the drive where ~/.ollama lives.

Need help? Why is my Ollama model download failing or stuck?

Verify the model works with a test prompt

Before building anything, you want to confirm the model can generate responses correctly from the command line.

  • Run the model with a simple test prompt:
ollama run qwen2.5:7b "Why is the sky blue?"

You should see a multi-sentence response about light scattering. The model takes a few seconds to load on first run, then generates text word by word.

What does this confirm?

A successful response proves three things: Ollama's server is running on localhost:11434, the model weights loaded into memory, and the model can produce coherent text.

Your Python code will connect to this same server later via http://localhost:11434.

Seeing an error about the model not found?

  • Run ollama ls to double check that qwen2.5:7b appears in your installed models list.
  • If it does not appear, run ollama pull qwen2.5:7b again and wait for the full download to complete.
  • On macOS, make sure the Ollama app is running (look for the llama icon in your menu bar). On Windows, Ollama runs as a background process automatically after install.

Still stuck? Help me fix my Ollama model not loading.

Your local LLM is ready to go. Next up, you will set up the Python project and connect CrewAI to this Ollama instance.

Set Up the Python Project

Your local LLM is ready to go. Now you need a Python project to house the agents, tools, and orchestration logic that will power your travel assistant.

In this step, you'll create a dedicated project folder, set up an isolated Python environment, install the CrewAI framework, and lay out the file structure for your multi-agent system.

In this step, get ready to:
  • Create a virtual environment and install CrewAI.
  • Create the project file structure for agents, tools, and orchestration.
  • Verify that CrewAI can connect to Ollama from Python.
Create the project folder and virtual environment

Every Python project benefits from its own isolated environment. A virtual environment gives your project its own set of packages, separate from anything else on your system.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir travel-assistant
cd travel-assistant
  • Create a virtual environment by running the command for your operating system:

macOS

python3 -m venv venv

Windows

python -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activate command for your operating system. On macOS it starts with source. On Windows it uses the backslash path venv\Scripts\activate.

If you see a permissions error on Windows, try running PowerShell as Administrator and entering Set-ExecutionPolicy RemoteSigned first.

Help me activate my Python virtual environment.

  • Install CrewAI and the requests library by running:
pip install crewai requests

What are these packages?

  • crewai is the multi-agent orchestration framework. It provides the Agent, Task, and Crew classes that let you define and coordinate specialized AI agents.
  • requests is a popular HTTP library. You'll use it to call the weather and country APIs from your custom tools.

The installation may take a minute or two as it downloads CrewAI and its dependencies.

Once it finishes, you should see a line that includes Successfully installed followed by a list of package names.

Seeing an install error?

Make sure your virtual environment is active. You should see (venv) in your prompt. If not, re-run the activation command above.

If you see a network timeout, check your internet connection and try running the install command again.

Help me fix my CrewAI installation error.

Create the project file structure

Your travel assistant will be organized into four files, each with a clear responsibility. Creating them now gives you a clean structure to build on.

  • Create the four project files by running the command for your operating system:

macOS

touch tools.py agents.py crew.py main.py

Windows

New-Item -ItemType File -Path "tools.py", "agents.py", "crew.py", "main.py"
  • Confirm the files exist by running:
ls

You should see tools.py, agents.py, crew.py, and main.py listed in the output alongside the venv folder.

What will each file do?

  • tools.py will hold custom tool functions that call external APIs (weather, country info, budget).
  • agents.py will define your specialized AI agents, each with a role, goal, and backstory.
  • crew.py will assemble the agents into a crew and define the tasks they work on.
  • main.py will provide the command-line interface where you type travel questions.

Don't see your files?

Make sure you're inside the travel-assistant folder when you run the create command. Run pwd (macOS) or Get-Location (Windows) to check your current directory.

Help me create my project files in the right location.

Verify CrewAI connects to Ollama

Before writing any project code, confirm that CrewAI can import correctly and that your Ollama connection is configured.

  • Run this verification command in your terminal:
python -c "from crewai import Agent, LLM; llm = LLM(model='ollama/qwen2.5:7b', base_url='http://localhost:11434'); print('CrewAI + Ollama configured successfully')"

You should see the message CrewAI + Ollama configured successfully printed in your terminal.

What does this command do?

This one-liner imports CrewAI's Agent and LLM classes, then creates an LLM object pointed at your local Ollama server. If either the import fails or Ollama is unreachable, you'll see an error instead of the success message.

The ollama/qwen2.5:7b model string tells CrewAI to use Ollama as the provider and qwen2.5:7b as the specific model.

Seeing an import or connection error?

If you see ModuleNotFoundError, your virtual environment may not be active. Check for (venv) in your prompt and re-activate if needed.

If you see a connection error, make sure Ollama is still running. Open a new terminal window and run ollama list to confirm it responds.

Help me fix my CrewAI and Ollama connection issue.

Your project skeleton is in place and CrewAI is talking to Ollama. Next up, you'll build the API tools that your agents will call to fetch real weather, country, and budget data.

Build the API Tools

Your agents need a way to fetch real information from the outside world. Without tools, an agent can only use whatever knowledge is baked into the LLM itself.

Tools are Python functions that agents can call autonomously to get live data. In this step, you'll build three tools that connect to free APIs for weather forecasts, country information, and budget estimation.

In this step, get ready to:
  • Build a weather forecast tool that calls the Open-Meteo API.
  • Build a country info tool that calls the RestCountries API.
  • Build a budget estimation tool with curated cost data.
Create the weather forecast tool

The weather tool will call the Open-Meteo API, a free weather service that requires no API key or signup. To look up coordinates for each city, you'll store a dictionary of pre-mapped cities.

  • Open tools.py in your code editor.
  • Add the imports and city coordinates data by pasting this code:
import requests
from crewai.tools import tool


CITY_COORDINATES = {
    "tokyo": (35.6762, 139.6503),
    "london": (51.5074, -0.1278),
    "paris": (48.8566, 2.3522),
    "new york": (40.7128, -74.0060),
    "frankfurt": (50.1109, 8.6821),
    "reykjavik": (64.1466, -21.9426),
    "bangkok": (13.7563, 100.5018),
    "sydney": (-33.8688, 151.2093),
    "dubai": (25.2048, 55.2708),
    "rome": (41.9028, 12.4964),
    "berlin": (52.5200, 13.4050),
    "singapore": (1.3521, 103.8198),
    "iceland": (64.9631, -19.0208),
}

What does this code do?

The requests library handles HTTP calls to external APIs. The @tool decorator from CrewAI turns a regular Python function into something an agent can call autonomously.

CITY_COORDINATES maps city names to latitude/longitude pairs. The Open-Meteo API needs coordinates (not city names) to return forecasts, so this dictionary acts as a lookup table.

  • Below the CITY_COORDINATES dictionary, add the daily travel cost data:
DAILY_COSTS_USD = {
    "tokyo": {"budget": 80, "mid": 150, "luxury": 350},
    "london": {"budget": 90, "mid": 180, "luxury": 400},
    "paris": {"budget": 85, "mid": 170, "luxury": 380},
    "new york": {"budget": 100, "mid": 200, "luxury": 450},
    "frankfurt": {"budget": 70, "mid": 140, "luxury": 300},
    "reykjavik": {"budget": 120, "mid": 220, "luxury": 500},
    "bangkok": {"budget": 30, "mid": 70, "luxury": 200},
    "sydney": {"budget": 85, "mid": 160, "luxury": 350},
    "dubai": {"budget": 80, "mid": 180, "luxury": 500},
    "rome": {"budget": 70, "mid": 140, "luxury": 320},
    "berlin": {"budget": 60, "mid": 120, "luxury": 280},
    "singapore": {"budget": 60, "mid": 130, "luxury": 350},
    "iceland": {"budget": 120, "mid": 220, "luxury": 500},
}

What is this data for?

DAILY_COSTS_USD stores estimated daily travel costs for each city across three spending tiers: budget, mid-range, and luxury. The budget estimation tool (which you'll build in substep 3) uses this data instead of a paid API. This teaches the same tool-calling pattern without needing external credentials.

  • Save tools.py.

Now add the actual weather forecast function. This is the first tool your agents will call.

  • Below the DAILY_COSTS_USD dictionary in tools.py, add the weather forecast tool:
@tool("Weather Forecast Tool")
def get_weather_forecast(city: str, days: int = 7) -> str:
    """Get weather forecast for a city. Returns temperature and precipitation data for the specified number of days (max 16)."""
    city_lower = city.lower().strip()
    coords = CITY_COORDINATES.get(city_lower)
    if not coords:
        return f"Error: City '{city}' not found. Available cities: {', '.join(CITY_COORDINATES.keys())}"

    lat, lon = coords
    days = min(max(days, 1), 16)

    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": lat,
        "longitude": lon,
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": days,
    }

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.RequestException as e:
        return f"Error fetching weather data: {e}"

    daily = data.get("daily", {})
    dates = daily.get("time", [])
    max_temps = daily.get("temperature_2m_max", [])
    min_temps = daily.get("temperature_2m_min", [])
    precip = daily.get("precipitation_sum", [])

    forecast_lines = [f"Weather forecast for {city.title()} ({days} days):"]
    for i in range(len(dates)):
        forecast_lines.append(
            f"  {dates[i]}: {min_temps[i]}\u00b0C to {max_temps[i]}\u00b0C, "
            f"precipitation: {precip[i]}mm"
        )

    return "\n".join(forecast_lines)

What does this tool do?

The @tool("Weather Forecast Tool") decorator registers this function so CrewAI agents can discover and call it. The docstring becomes the tool's description that the LLM reads to decide when to use it.

Inside the function: it looks up the city's coordinates, calls the Open-Meteo API with parameters for daily max/min temperatures and precipitation, then formats the response into a readable summary. The timeout=10 prevents the tool from hanging if the API is slow.

  • Save tools.py.
  • Test the weather tool by running this command in your terminal:
python3 -c "from tools import get_weather_forecast; print(get_weather_forecast.run(city='tokyo', days=3))"

You should see a 3-day weather forecast for Tokyo with temperatures and precipitation values.

Seeing an import error or no output?

  • Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • Check that you saved tools.py before running the test command.
  • If you see a network error, confirm your internet connection is working.

Help me debug my weather tool

Create the country info tool

The country info tool calls the RestCountries API to fetch practical travel details like currency, language, and timezone. This API is also free and requires no key.

  • Below the get_weather_forecast function in tools.py, add the country information tool:
@tool("Country Information Tool")
def get_country_info(country: str) -> str:
    """Get country information including capital, currency, languages, timezone, and population."""
    url = f"https://restcountries.com/v3.1/name/{country}"

    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.RequestException as e:
        return f"Error fetching country data: {e}"

    if not data:
        return f"No data found for country: {country}"

    info = data[0]
    name = info.get("name", {}).get("common", country)
    capital = info.get("capital", ["Unknown"])[0]
    population = info.get("population", "Unknown")
    region = info.get("region", "Unknown")
    currencies = info.get("currencies", {})
    currency_info = []
    for code, details in currencies.items():
        currency_info.append(f"{details.get('name', code)} ({code}, {details.get('symbol', '')})")
    languages = info.get("languages", {})
    lang_list = list(languages.values())
    timezones = info.get("timezones", [])

    return (
        f"Country: {name}\n"
        f"Capital: {capital}\n"
        f"Region: {region}\n"
        f"Population: {population:,}\n"
        f"Currency: {', '.join(currency_info)}\n"
        f"Languages: {', '.join(lang_list)}\n"
        f"Timezones: {', '.join(timezones)}"
    )

What does this tool do?

This tool queries https://restcountries.com/v3.1/name/{country} with a country name and extracts practical travel facts from the JSON response. It pulls out the capital city, population, currencies (with symbols), languages spoken, and timezones.

The formatted string output gives the Destination Info Agent everything it needs to brief a traveler on logistics.

  • Save tools.py.
  • Test the country info tool by running:
python3 -c "from tools import get_country_info; print(get_country_info.run(country='japan'))"

You should see Japan's capital, population, currency (Yen), languages, and timezone listed.

Getting a connection error?

  • The RestCountries API is free but occasionally has brief outages. Wait a moment and try again.
  • Double-check that the get_country_info function is placed after get_weather_forecast in the file. Python reads top to bottom.

Help me debug my country info tool

Create the budget estimation tool

The budget tool uses the DAILY_COSTS_USD dictionary you added earlier to calculate trip costs. It doesn't call an external API. Instead it multiplies daily rates by trip length and breaks the total into spending categories.

  • Below the get_country_info function in tools.py, add the budget estimation tool:
@tool("Budget Estimation Tool")
def estimate_budget(destination: str, days: int = 7, travel_style: str = "mid") -> str:
    """Estimate travel budget for a destination. Travel style can be 'budget', 'mid', or 'luxury'. Returns daily and total cost breakdown."""
    dest_lower = destination.lower().strip()
    costs = DAILY_COSTS_USD.get(dest_lower)
    if not costs:
        return f"Error: No budget data for '{destination}'. Available: {', '.join(DAILY_COSTS_USD.keys())}"

    style = travel_style.lower().strip()
    if style not in costs:
        style = "mid"

    daily_cost = costs[style]
    total_cost = daily_cost * days

    breakdown = {
        "budget": {"accommodation": 0.35, "food": 0.30, "transport": 0.15, "activities": 0.15, "misc": 0.05},
        "mid": {"accommodation": 0.40, "food": 0.25, "transport": 0.15, "activities": 0.15, "misc": 0.05},
        "luxury": {"accommodation": 0.45, "food": 0.20, "transport": 0.15, "activities": 0.15, "misc": 0.05},
    }

    categories = breakdown[style]
    lines = [
        f"Budget estimate for {destination.title()} ({days} days, {style} style):",
        f"  Daily cost: ~${daily_cost} USD",
        f"  Total estimated cost: ~${total_cost} USD",
        f"  Breakdown:",
    ]
    for category, pct in categories.items():
        lines.append(f"    {category.title()}: ~${int(total_cost * pct)} USD")

    return "\n".join(lines)

What does this tool do?

The budget tool takes a destination, number of days, and travel style (budget/mid/luxury). It looks up the daily cost from DAILY_COSTS_USD, multiplies by trip length, then splits the total into percentage-based categories like accommodation, food, and transport.

If the agent passes an unrecognized style, the tool defaults to "mid" rather than crashing. Defensive fallbacks like this help agents recover gracefully from imprecise queries.

  • Save tools.py.
  • Test the budget tool by running:
python3 -c "from tools import estimate_budget; print(estimate_budget.run(destination='tokyo', days=5, travel_style='budget'))"

You should see a budget breakdown showing a daily cost of ~$80, a total of ~$400, and spending categories like Accommodation and Food.

Seeing a NameError or KeyError?

  • Make sure DAILY_COSTS_USD is defined above the estimate_budget function. Python needs the dictionary to exist before the function can reference it.
  • Check that all dictionary entries use curly braces {} and that each line ends with a comma (except the last entry before the closing brace).

Help me fix my budget estimation tool

Why local data instead of a flight API?

Real flight and hotel APIs (like Amadeus or Duffel) require paid API keys and complex authentication. For a learning project, hardcoded cost estimates teach the same tool-calling pattern without adding external credential complexity. Your agents call this tool the same way they'd call any API-backed tool.

✔️ Awesome, I've got everything!

Great. Double check you've saved tools.py with all three tools and both data dictionaries.

ⓧ I'd like to double check the full code

Here is the complete tools.py file. Compare your code against this:

import requests
from crewai.tools import tool


CITY_COORDINATES = {
    "tokyo": (35.6762, 139.6503),
    "london": (51.5074, -0.1278),
    "paris": (48.8566, 2.3522),
    "new york": (40.7128, -74.0060),
    "frankfurt": (50.1109, 8.6821),
    "reykjavik": (64.1466, -21.9426),
    "bangkok": (13.7563, 100.5018),
    "sydney": (-33.8688, 151.2093),
    "dubai": (25.2048, 55.2708),
    "rome": (41.9028, 12.4964),
    "berlin": (52.5200, 13.4050),
    "singapore": (1.3521, 103.8198),
    "iceland": (64.9631, -19.0208),
}

DAILY_COSTS_USD = {
    "tokyo": {"budget": 80, "mid": 150, "luxury": 350},
    "london": {"budget": 90, "mid": 180, "luxury": 400},
    "paris": {"budget": 85, "mid": 170, "luxury": 380},
    "new york": {"budget": 100, "mid": 200, "luxury": 450},
    "frankfurt": {"budget": 70, "mid": 140, "luxury": 300},
    "reykjavik": {"budget": 120, "mid": 220, "luxury": 500},
    "bangkok": {"budget": 30, "mid": 70, "luxury": 200},
    "sydney": {"budget": 85, "mid": 160, "luxury": 350},
    "dubai": {"budget": 80, "mid": 180, "luxury": 500},
    "rome": {"budget": 70, "mid": 140, "luxury": 320},
    "berlin": {"budget": 60, "mid": 120, "luxury": 280},
    "singapore": {"budget": 60, "mid": 130, "luxury": 350},
    "iceland": {"budget": 120, "mid": 220, "luxury": 500},
}


@tool("Weather Forecast Tool")
def get_weather_forecast(city: str, days: int = 7) -> str:
    """Get weather forecast for a city. Returns temperature and precipitation data for the specified number of days (max 16)."""
    city_lower = city.lower().strip()
    coords = CITY_COORDINATES.get(city_lower)
    if not coords:
        return f"Error: City '{city}' not found. Available cities: {', '.join(CITY_COORDINATES.keys())}"

    lat, lon = coords
    days = min(max(days, 1), 16)

    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": lat,
        "longitude": lon,
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code",
        "timezone": "auto",
        "forecast_days": days,
    }

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.RequestException as e:
        return f"Error fetching weather data: {e}"

    daily = data.get("daily", {})
    dates = daily.get("time", [])
    max_temps = daily.get("temperature_2m_max", [])
    min_temps = daily.get("temperature_2m_min", [])
    precip = daily.get("precipitation_sum", [])

    forecast_lines = [f"Weather forecast for {city.title()} ({days} days):"]
    for i in range(len(dates)):
        forecast_lines.append(
            f"  {dates[i]}: {min_temps[i]}\u00b0C to {max_temps[i]}\u00b0C, "
            f"precipitation: {precip[i]}mm"
        )

    return "\n".join(forecast_lines)


@tool("Country Information Tool")
def get_country_info(country: str) -> str:
    """Get country information including capital, currency, languages, timezone, and population."""
    url = f"https://restcountries.com/v3.1/name/{country}"

    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.RequestException as e:
        return f"Error fetching country data: {e}"

    if not data:
        return f"No data found for country: {country}"

    info = data[0]
    name = info.get("name", {}).get("common", country)
    capital = info.get("capital", ["Unknown"])[0]
    population = info.get("population", "Unknown")
    region = info.get("region", "Unknown")
    currencies = info.get("currencies", {})
    currency_info = []
    for code, details in currencies.items():
        currency_info.append(f"{details.get('name', code)} ({code}, {details.get('symbol', '')})")
    languages = info.get("languages", {})
    lang_list = list(languages.values())
    timezones = info.get("timezones", [])

    return (
        f"Country: {name}\n"
        f"Capital: {capital}\n"
        f"Region: {region}\n"
        f"Population: {population:,}\n"
        f"Currency: {', '.join(currency_info)}\n"
        f"Languages: {', '.join(lang_list)}\n"
        f"Timezones: {', '.join(timezones)}"
    )


@tool("Budget Estimation Tool")
def estimate_budget(destination: str, days: int = 7, travel_style: str = "mid") -> str:
    """Estimate travel budget for a destination. Travel style can be 'budget', 'mid', or 'luxury'. Returns daily and total cost breakdown."""
    dest_lower = destination.lower().strip()
    costs = DAILY_COSTS_USD.get(dest_lower)
    if not costs:
        return f"Error: No budget data for '{destination}'. Available: {', '.join(DAILY_COSTS_USD.keys())}"

    style = travel_style.lower().strip()
    if style not in costs:
        style = "mid"

    daily_cost = costs[style]
    total_cost = daily_cost * days

    breakdown = {
        "budget": {"accommodation": 0.35, "food": 0.30, "transport": 0.15, "activities": 0.15, "misc": 0.05},
        "mid": {"accommodation": 0.40, "food": 0.25, "transport": 0.15, "activities": 0.15, "misc": 0.05},
        "luxury": {"accommodation": 0.45, "food": 0.20, "transport": 0.15, "activities": 0.15, "misc": 0.05},
    }

    categories = breakdown[style]
    lines = [
        f"Budget estimate for {destination.title()} ({days} days, {style} style):",
        f"  Daily cost: ~${daily_cost} USD",
        f"  Total estimated cost: ~${total_cost} USD",
        f"  Breakdown:",
    ]
    for category, pct in categories.items():
        lines.append(f"    {category.title()}: ~${int(total_cost * pct)} USD")

    return "\n".join(lines)

Your three tools are ready. Next up, you'll define the specialized agents that will call these tools to answer travel questions.

Define the Specialized Agents

Your tools are built and tested. Now you need agents to wield them.

In a multi-agent system, each agent has a defined role, a specific goal, and access to the tools it needs. In this step, you'll create four agents in agents.py. Three specialists each get one tool, and a coordinator agent will later combine their outputs into a single travel briefing.

In this step, get ready to:
  • Create a Weather Agent with the weather forecast tool.
  • Create Destination Info and Budget agents with their respective tools.
  • Create a Coordinator Agent that synthesizes all agent outputs.
Connect to Ollama and create the Weather Agent

Before you can define agents, you need to tell CrewAI where your LLM lives. CrewAI uses an LLM class that points to your local Ollama server.

  • Set up the LLM connection and imports by adding this code to agents.py:
from crewai import Agent
from crewai import LLM
from tools import get_weather_forecast, get_country_info, estimate_budget

llm = LLM(
    model="ollama/qwen2.5:7b",
    base_url="http://localhost:11434",
)

What does this code do?

The LLM class tells CrewAI which model to use and where to find it. The model string follows the pattern ollama/model_name, and base_url points to your locally running Ollama server.

The three imports from tools.py bring in the tool functions you built in the previous step. Each agent will receive one of these as its dedicated tool.

You won't see a change yet. The next chunk adds the first agent that uses this LLM connection.

  • Add the Weather Agent below the LLM configuration in agents.py:
weather_agent = Agent(
    role="Weather Forecast Specialist",
    goal="Provide accurate and helpful weather forecasts for travel destinations",
    backstory=(
        "You are an experienced meteorologist who specializes in travel weather. "
        "You analyze forecast data and provide practical summaries that help "
        "travelers plan what to expect and how to prepare."
    ),
    tools=[get_weather_forecast],
    llm=llm,
    verbose=True,
)

What are role, goal, and backstory?

These three parameters shape how the LLM behaves when acting as this agent:

  • role gives the agent a job title. The LLM uses this to understand its expertise.
  • goal tells the agent what outcome to optimize for.
  • backstory provides persona context that guides the agent's tone and decision-making.
  • tools is the list of functions this agent can call autonomously.
  • verbose=True prints the agent's reasoning steps to the console, which helps you debug.
  • Save agents.py.
  • Verify the file loads without errors by running:
python agents.py

You should see no output and no error messages. The script creates the LLM connection and the agent object, then exits silently.

Seeing an error?

  • If you see ModuleNotFoundError: No module named 'crewai', make sure your virtual environment is activated.
  • If you see an import error for tools, check that tools.py is in the same folder as agents.py.
  • If Ollama connection fails, confirm Ollama is still running with ollama list in a separate terminal.

Help me debug this error

Add the destination, budget, and coordinator agents

The remaining agents follow the same pattern. Each specialist gets one tool, and the coordinator agent gets no tools because its job is to synthesize the other agents' outputs.

  • Add the Destination Info Agent and Budget Agent below the weather agent in agents.py:
destination_agent = Agent(
    role="Destination Information Expert",
    goal="Provide practical travel information about countries and cities",
    backstory=(
        "You are a seasoned travel researcher who knows essential facts "
        "about destinations worldwide. You focus on practical details like "
        "currency, language, timezone differences, and cultural tips."
    ),
    tools=[get_country_info],
    llm=llm,
    verbose=True,
)

budget_agent = Agent(
    role="Travel Budget Analyst",
    goal="Estimate realistic travel budgets with clear cost breakdowns",
    backstory=(
        "You are a financial planner specializing in travel budgets. "
        "You provide realistic cost estimates based on destination, "
        "duration, and travel style, helping travelers plan their finances."
    ),
    tools=[estimate_budget],
    llm=llm,
    verbose=True,
)

What do these agents do?

  • destination_agent uses the get_country_info tool to fetch currency, language, and timezone data from RestCountries.
  • budget_agent uses the estimate_budget tool to calculate trip costs based on destination and travel style.

Each agent has one dedicated tool. Keeping tools per agent to a minimum helps local models make reliable tool-calling decisions.

You won't see a change yet. The next chunk adds the final coordinator agent.

  • Add the Coordinator Agent below the budget agent in agents.py:
coordinator_agent = Agent(
    role="Travel Briefing Coordinator",
    goal="Synthesize all travel information into a clear, structured briefing",
    backstory=(
        "You are a senior travel advisor who excels at combining weather, "
        "destination, and budget information into comprehensive travel "
        "briefings. You present information clearly and highlight key "
        "takeaways for the traveler."
    ),
    llm=llm,
    verbose=True,
)

Why does the coordinator have no tools?

The coordinator's job is purely synthesis. It receives the outputs from the other three agents and combines them into a structured travel briefing.

It does not need to call any external API. It relies on the LLM's natural language ability to organize and present information clearly.

  • Save agents.py.
  • Verify all four agents load successfully by running:
python -c "from agents import weather_agent, destination_agent, budget_agent, coordinator_agent; print('All 4 agents loaded successfully')"

You should see All 4 agents loaded successfully printed in your terminal.

Not seeing the success message?

  • If you see a SyntaxError, check for missing commas between agent parameters or unclosed parentheses.
  • If you see ImportError: cannot import name 'coordinator_agent', make sure the variable name matches exactly (no typos, no capital letters).
  • If the command hangs, Ollama may be unresponsive. Open a new terminal and run ollama list to confirm it's still running.

Help me debug my agents.py import issue

✔️ Awesome, I've got everything!

Great. Double check you've saved agents.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete agents.py file:

from crewai import Agent
from crewai import LLM
from tools import get_weather_forecast, get_country_info, estimate_budget

llm = LLM(
    model="ollama/qwen2.5:7b",
    base_url="http://localhost:11434",
)

weather_agent = Agent(
    role="Weather Forecast Specialist",
    goal="Provide accurate and helpful weather forecasts for travel destinations",
    backstory=(
        "You are an experienced meteorologist who specializes in travel weather. "
        "You analyze forecast data and provide practical summaries that help "
        "travelers plan what to expect and how to prepare."
    ),
    tools=[get_weather_forecast],
    llm=llm,
    verbose=True,
)

destination_agent = Agent(
    role="Destination Information Expert",
    goal="Provide practical travel information about countries and cities",
    backstory=(
        "You are a seasoned travel researcher who knows essential facts "
        "about destinations worldwide. You focus on practical details like "
        "currency, language, timezone differences, and cultural tips."
    ),
    tools=[get_country_info],
    llm=llm,
    verbose=True,
)

budget_agent = Agent(
    role="Travel Budget Analyst",
    goal="Estimate realistic travel budgets with clear cost breakdowns",
    backstory=(
        "You are a financial planner specializing in travel budgets. "
        "You provide realistic cost estimates based on destination, "
        "duration, and travel style, helping travelers plan their finances."
    ),
    tools=[estimate_budget],
    llm=llm,
    verbose=True,
)

coordinator_agent = Agent(
    role="Travel Briefing Coordinator",
    goal="Synthesize all travel information into a clear, structured briefing",
    backstory=(
        "You are a senior travel advisor who excels at combining weather, "
        "destination, and budget information into comprehensive travel "
        "briefings. You present information clearly and highlight key "
        "takeaways for the traveler."
    ),
    llm=llm,
    verbose=True,
)

Your agents are defined and ready to work. Next, you'll build the orchestrator crew that assigns tasks to each agent and chains their outputs together.

Build the Orchestrator Crew

You have four specialized agents ready to go, but they don't know how to work together yet. Right now they're individuals sitting in separate files with no instructions.

In this step, you'll build the orchestration layer that turns those agents into a coordinated team. You'll define tasks that tell each agent exactly what to do, wire up data flow between them, and run the full system end-to-end.

In this step, get ready to:
  • Define tasks with descriptions and expected outputs for each agent.
  • Assemble the crew with sequential processing and task dependencies.
  • Run the full system with a sample travel query.
Define the specialist tasks

Each agent needs a Task that describes what to do with the user's travel query and what output format to produce. You'll define these inside a factory function that accepts the user's query and builds a fresh crew each time.

  • Open crew.py in your editor and add the imports, function signature, and the first task by pasting the following code:
from crewai import Crew, Task, Process
from agents import weather_agent, destination_agent, budget_agent, coordinator_agent


def create_travel_crew(query: str) -> Crew:
    """Create a travel crew configured to answer the given travel query."""

    # Task for the weather agent - fetches forecast data
    weather_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your weather forecast tool to get the forecast for the "
            "destination city. Provide a summary of expected conditions."
        ),
        expected_output=(
            "A weather summary including daily temperatures and precipitation "
            "for the travel dates, plus practical advice."
        ),
        agent=weather_agent,
    )

What does this code do?

The create_travel_crew function is a factory. Every time the user asks a question, it builds a new crew with the query baked into each task's description.

Each Task has two key fields: description (what the agent should do) and expected_output (what format the result should take). These guide the LLM's behavior during execution.

  • Now add the destination and budget tasks directly below the weather_task (still inside the create_travel_crew function):
    # Task for the destination info agent - fetches country details
    destination_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your country information tool to get practical details about "
            "the destination country. Include currency, language, and timezone."
        ),
        expected_output=(
            "Key destination facts including currency, official language, "
            "timezone, and any practical travel tips."
        ),
        agent=destination_agent,
    )

    # Task for the budget agent - estimates trip costs
    budget_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your budget estimation tool to provide a cost estimate. "
            "If no budget style is mentioned, provide the 'budget' (minimum) estimate."
        ),
        expected_output=(
            "A budget breakdown showing daily costs, total estimated cost, "
            "and spending categories."
        ),
        agent=budget_agent,
    )

Why the same pattern for each task?

All three specialist tasks follow the same structure. Each embeds the user's query in its description and tells the agent which tool to use. The agent parameter assigns ownership so CrewAI knows which agent executes which task.

Wire up the coordinator and assemble the crew

The coordinator task is different from the specialists. Instead of calling an API tool, it reads the outputs from the other three tasks and synthesizes them into one briefing.

  • Add the coordination task below the budget_task (still inside the function):
    # Coordination task - synthesizes all agent outputs into one briefing
    coordination_task = Task(
        description=(
            f"Original travel query: '{query}'\n"
            "Compile all the information from the weather, destination, and "
            "budget agents into a single, well-structured travel briefing. "
            "Use clear sections and highlight the most important takeaways."
        ),
        expected_output=(
            "A comprehensive travel briefing in markdown format with sections "
            "for Weather, Destination Info, Budget, and Key Takeaways."
        ),
        agent=coordinator_agent,
        context=[weather_task, destination_task, budget_task],
    )

What is the context parameter?

The context parameter is how you pass data between tasks in CrewAI. By setting context=[weather_task, destination_task, budget_task], the coordination task receives the finished outputs from all three specialist tasks as input.

Without context, the coordinator would only see its own description. With it, the coordinator can read and combine everything the other agents discovered.

  • Finally, add the Crew assembly at the bottom of the function to complete crew.py:
    # Assemble all agents and tasks into a sequential crew
    crew = Crew(
        agents=[weather_agent, destination_agent, budget_agent, coordinator_agent],
        tasks=[weather_task, destination_task, budget_task, coordination_task],
        process=Process.sequential,
        verbose=True,
    )

    return crew

What does Process.sequential do?

Setting process=Process.sequential means tasks run one after another in the order you listed them. The weather agent goes first, then destination, then budget, then the coordinator.

Each task's output automatically flows forward. The context parameter on the coordination task explicitly selects which outputs it receives, giving you precise control over the data pipeline.

  • Save crew.py.

Seeing an indentation error?

All the task definitions and the Crew(...) call must be indented inside the create_travel_crew function. Make sure every line after def create_travel_crew is indented by 4 spaces.

help me fix indentation issues in my crew.py file

✔️ Awesome, I've got everything!

Great. Double check you've saved crew.py before moving on.

ⓧ I'd like to double check the full code

from crewai import Crew, Task, Process
from agents import weather_agent, destination_agent, budget_agent, coordinator_agent


def create_travel_crew(query: str) -> Crew:
    """Create a travel crew configured to answer the given travel query."""

    weather_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your weather forecast tool to get the forecast for the "
            "destination city. Provide a summary of expected conditions."
        ),
        expected_output=(
            "A weather summary including daily temperatures and precipitation "
            "for the travel dates, plus practical advice."
        ),
        agent=weather_agent,
    )

    destination_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your country information tool to get practical details about "
            "the destination country. Include currency, language, and timezone."
        ),
        expected_output=(
            "Key destination facts including currency, official language, "
            "timezone, and any practical travel tips."
        ),
        agent=destination_agent,
    )

    budget_task = Task(
        description=(
            f"Based on this travel query: '{query}'\n"
            "Use your budget estimation tool to provide a cost estimate. "
            "If no budget style is mentioned, provide the 'budget' (minimum) estimate."
        ),
        expected_output=(
            "A budget breakdown showing daily costs, total estimated cost, "
            "and spending categories."
        ),
        agent=budget_agent,
    )

    coordination_task = Task(
        description=(
            f"Original travel query: '{query}'\n"
            "Compile all the information from the weather, destination, and "
            "budget agents into a single, well-structured travel briefing. "
            "Use clear sections and highlight the most important takeaways."
        ),
        expected_output=(
            "A comprehensive travel briefing in markdown format with sections "
            "for Weather, Destination Info, Budget, and Key Takeaways."
        ),
        agent=coordinator_agent,
        context=[weather_task, destination_task, budget_task],
    )

    crew = Crew(
        agents=[weather_agent, destination_agent, budget_agent, coordinator_agent],
        tasks=[weather_task, destination_task, budget_task, coordination_task],
        process=Process.sequential,
        verbose=True,
    )

    return crew
Run the full system with a sample query

Time to see all four agents collaborate. You'll run a quick test from the terminal to verify the end-to-end flow works.

  • In your terminal (with your virtual environment still active), run the following test command:
python -c "
from crew import create_travel_crew
crew = create_travel_crew('What is the weather like in Tokyo for 3 days?')
result = crew.kickoff()
print(result.raw)
"

What should I see?

Because verbose=True is set, you'll see detailed logs of each agent thinking, calling its tool, and producing output. The weather agent calls the Open-Meteo API, the destination agent calls RestCountries, and the budget agent looks up Tokyo's cost data.

At the end, the coordinator agent compiles everything into a structured travel briefing. The full process takes 1-3 minutes depending on your machine's speed with the local LLM.

  • Wait for the output to finish. You should see a final travel briefing printed with sections for weather, destination info, and budget.

Seeing a connection error or timeout?

Make sure Ollama is still running. Open a separate terminal and run ollama list to confirm the qwen2.5:7b model is available.

If you see an error about malformed tool arguments, this is a known behavior with local 7B models. Run the test again. The model sometimes needs a second attempt to format tool calls correctly.

help me debug my CrewAI crew not connecting to Ollama or failing during tool calls

Your multi-agent crew is working end-to-end. Next up, you'll wrap it in a proper command-line interface so anyone can ask travel questions interactively.

Add the CLI Interface and Test

Your crew ran successfully in the previous step, proving that all four agents can collaborate on a travel query. Right now, though, the only way to use the system is by editing Python code directly.

In this step, you'll wrap everything in a user-friendly command-line interface. The CLI will accept natural-language travel questions, pass them to your crew, and print a formatted travel briefing back to the terminal.

In this step, get ready to:
  • Build a command-line loop that accepts user travel queries.
  • Format agent outputs into a structured travel briefing.
  • Test the system with weather-only, budget-only, and combined queries.
Build the command-line interface

The main.py file is the entry point that ties everything together. It prints a welcome banner with example queries, then loops continuously waiting for user input.

  • In main.py, add the full CLI code by pasting the following:
from crew import create_travel_crew


def main():
    # Print welcome banner
    print("=" * 60)
    print("  Multi-Agent Travel Assistant")
    print("  Powered by CrewAI + Ollama (local LLM)")
    print("=" * 60)
    print()
    print("Ask me anything about travel! Examples:")
    print("  - I'm traveling to Tokyo next week for 5 days. What's the weather?")
    print("  - What's the minimum budget for 10 days in Iceland?")
    print("  - Tell me about traveling to Thailand for 2 weeks.")
    print()
    print("Type 'quit' to exit.")
    print()

    # Main input loop
    while True:
        query = input("Your travel question: ").strip()
        if query.lower() in ("quit", "exit", "q"):
            print("Safe travels! Goodbye.")
            break
        if not query:
            continue

        print("\n--- Agents are working on your query... ---\n")

        # Create the crew and run the query
        try:
            crew = create_travel_crew(query)
            result = crew.kickoff()
            print("\n" + "=" * 60)
            print("  TRAVEL BRIEFING")
            print("=" * 60)
            print(result.raw)
            print("=" * 60 + "\n")
        except Exception as e:
            print(f"\nError: {e}")
            print("Make sure Ollama is running with the qwen2.5:7b model.\n")


if __name__ == "__main__":
    main()

What does this code do?

  • from crew import create_travel_crew pulls in the crew factory function you built in the previous step.
  • The banner print() statements give users example queries so they know what kinds of questions the system handles.
  • while True keeps the program running until the user types quit, exit, or q.
  • result.raw contains the final coordinator agent's text output, which gets printed inside a formatted briefing box.
  • The try/except block catches errors gracefully and reminds the user to check that Ollama is running.
  • Save main.py.

Seeing a syntax error on save?

  • Check that the if __name__ == "__main__": line uses two underscores on each side of name and main.
  • Make sure no extra indentation crept in when pasting. The from crew import create_travel_crew line should have zero indentation.
  • Help me fix a syntax error in main.py

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py before moving on to testing.

ⓧ I'd like to double check the full code

from crew import create_travel_crew


def main():
    print("=" * 60)
    print("  Multi-Agent Travel Assistant")
    print("  Powered by CrewAI + Ollama (local LLM)")
    print("=" * 60)
    print()
    print("Ask me anything about travel! Examples:")
    print("  - I'm traveling to Tokyo next week for 5 days. What's the weather?")
    print("  - What's the minimum budget for 10 days in Iceland?")
    print("  - Tell me about traveling to Thailand for 2 weeks.")
    print()
    print("Type 'quit' to exit.")
    print()

    while True:
        query = input("Your travel question: ").strip()
        if query.lower() in ("quit", "exit", "q"):
            print("Safe travels! Goodbye.")
            break
        if not query:
            continue

        print("\n--- Agents are working on your query... ---\n")

        try:
            crew = create_travel_crew(query)
            result = crew.kickoff()
            print("\n" + "=" * 60)
            print("  TRAVEL BRIEFING")
            print("=" * 60)
            print(result.raw)
            print("=" * 60 + "\n")
        except Exception as e:
            print(f"\nError: {e}")
            print("Make sure Ollama is running with the qwen2.5:7b model.\n")


if __name__ == "__main__":
    main()
Test with different query types

Your travel assistant handles three categories of questions: weather-focused, budget-focused, and combined queries that activate all agents. Testing each type confirms that the correct agents activate and return useful data.

  • Run the travel assistant by entering this command in your terminal:
python main.py

You should see the welcome banner with example queries and a Your travel question: prompt waiting for input.

  • Test a weather-only query by typing:
I'm traveling to Tokyo next week for 5 days. What's the weather?

What should I see?

You'll see verbose output from each agent as it works. The Weather Agent calls the Open-Meteo API and summarizes the forecast. The Destination and Budget agents also contribute because the coordinator synthesizes everything.

After 1-3 minutes (local models take time), a TRAVEL BRIEFING section appears with the final formatted output. The exact content varies with each run since the LLM generates it.

  • Once the first briefing finishes, test a budget-only query:
What's the minimum budget for 10 days in Iceland?
  • Finally, test a combined query that exercises all agents together:
Tell me about traveling to Thailand for 2 weeks.
  • When you're done testing, type quit to exit the program.

Responses take a while

Local 7B models process tokens much slower than cloud APIs. Each query takes 1-3 minutes depending on your hardware. The verbose output keeps you informed about what each agent is doing while you wait.

Seeing an error instead of a briefing?

  • If you see a connection error, make sure Ollama is still running. Open a new terminal and run ollama list to confirm.
  • If the agent produces garbled output or fails to call a tool, try running the same query again. Local models occasionally produce malformed tool calls on the first attempt.
  • If you see ModuleNotFoundError, make sure your virtual environment is activated and you installed crewai inside it.
  • Help me debug my travel assistant CLI errors

Secret mission

Add a Packing Suggestions Agent

Your travel assistant gives great weather, destination, and budget info. But what about the eternal question: what do I actually pack? Add a 4th specialized agent that reads the weather forecast and recommends a practical packing list.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop containing tools.py, agents.py, crew.py, and main.py.
  • Python virtual environment (inside the project folder).
  • Ollama with the qwen2.5:7b model (~5GB of disk space).

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with different travel queries and agents.

  • Ollama will continue running in the background, serving the local LLM.
  • Your project files stay on your Desktop, ready for you to run main.py again any time.
  • The qwen2.5:7b model stays downloaded (~5GB) so you don't have to re-pull it later.

Pause - I'll come back to this later

Quit Ollama to free up memory. Your project files and downloaded model stay on disk so you can resume later without re-downloading anything.

  • On macOS, click the Ollama icon in the menu bar and select Quit Ollama.
  • On Windows, right-click the Ollama icon in the system tray and select Quit.
  • Your project folder and virtual environment remain on your Desktop untouched.
  • When you're ready to come back, launch Ollama again and run main.py.

Delete - I don't want to use this again

Remove all project resources and free up disk space. This cannot be undone.

  • Delete the downloaded model by running this command in your terminal:
ollama rm qwen2.5:7b
  • Delete your project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf travel-assistant
  • On Windows, run:
cd ~\Desktop
rmdir /s /q travel-assistant
  • To uninstall Ollama entirely on macOS, refer to the Ollama macOS uninstall guide. The documented steps remove the app, CLI binary, and all cached data.
  • To uninstall Ollama entirely on Windows, open Settings, go to Add or remove programs, find Ollama, and click Uninstall.

Nice Work!

Nice Work!

Nice work! You just built a fully local multi-agent AI system that decomposes complex travel queries, routes them to specialized agents, and produces structured travel briefings.

You've learned how to:

  • Built 3 specialized AI agents (Weather, Destination Info, Budget) plus a Coordinator Agent that synthesizes their outputs into a single travel briefing.
  • Integrated free, real-world APIs as agent tools (Open-Meteo for weather forecasts, RestCountries for destination data) that agents call autonomously.
  • Orchestrated multi-agent collaboration with CrewAI's sequential crew system, passing context between tasks.
  • Added a Packing Suggestions Agent that uses inter-agent data passing to recommend items based on weather forecast output.

Ready to quiz yourself?