Tutorial: build a Flight Booking Crew
Build a Crew that finds the best roundtrip flights on the given dates.
Flight booking automation presents significant challenges, primarily due to the scarcity of public APIs. Consequently, the process of searching for flights often requires simulating human-like interactions with web interfaces.
Fortunately, the combination of CrewAI and Browserbase only requires a few hundred lines of code to automate this complex task.
By following this tutorial, you’ll learn how to build a CrewAI program that searches for a roundtrip flight from a simple human input:
> python3 main.py "Sofia to Berlin one-way on 26th May"
> Here are our top 5 picks from Sofia to Berlin on 2nd July 2024:
1. **Ryanair**
- Departure: 21:35
- Arrival: 22:50
- Duration: 2 hours 15 minutes
- Layovers: Direct
- Price: $18
- Booking: [Ryanair](https://www.skyscanner.net/transport/flights/sof/ber/240702/0/config/16440-2407022135--31915-0-9828-2407022250?currency=USD)
...
Introduction: Crews, Agents, Tasks and Tools
CrewAI helps developers build AI Agents with 4 core concepts: Crews, Agents, Tasks, and Tools:
- A
Crew
is a team ofAgents
working together to accomplish some tasks. - A
Task
, such as “Search flights according to criteria”, is a goal assigned to a specializedAgent
(e.g., a Flight Booking Agent). - An
Agent
can be seen as a specialized text-only GPT that receives a set ofTools
to perform actions (e.g., search on Google, navigate to this URL).
Example
Here is an example of a Crew assembled to research a given topic and write an article.
The Agents: A Researcher and a Writer
First, let’s define 2 Agents, one specialized in researching a topic and another in writing articles:
researcher = Agent(
role='Senior Researcher',
goal='Uncover groundbreaking technologies in {topic}',
backstory=(
"Driven by curiosity, you're at the forefront of"
"innovation, eager to explore and share knowledge that could change"
"the world."
),
tools=[search_tool],
)
writer = Agent(
role='Writer',
goal='Narrate compelling tech stories about {topic}',
backstory=(
"With a flair for simplifying complex topics, you craft"
"engaging narratives that captivate and educate, bringing new"
"discoveries to light in an accessible manner."
),
tools=[search_tool]
)
Each Agent gets:
- a
role
that helps theCrew
select the best Agent for a givenTask
. - a
goal
that frames theAgent
decision-making process when iterating on aTask
. - a
backstory
providing context to theAgent
’srole
andgoal
.
Both Agents get access to a search_tool
(SerperDevTool
instance) to perform searches with Google Search.
The Tasks: writing and researching
Let’s now define 2 tasks: researching a topic and writing an article.
research_task = Task(
description=(
"Identify the next big trend in {topic}."
"Focus on identifying pros and cons and the overall narrative."
"Your final report should clearly articulate the key points,"
"its market opportunities, and potential risks."
),
expected_output='A comprehensive 3 paragraphs long report on the latest AI trends.',
agent=researcher,
)
write_task = Task(
description=(
"Compose an insightful article on {topic}."
"Focus on the latest trends and how it's impacting the industry."
"This article should be easy to understand, engaging, and positive."
),
expected_output='A 4 paragraph article on {topic} advancements formatted as markdown.',
agent=writer,
output_file='new-blog-post.md' # Example of output customization
)
A Task’s description
can be compared to a prompt, while the expected_output
helps format the result of the Task
.
As expected, the write_task
gets assigned to the writer
Agent and the research_task
to the researcher
Agent.
Agents and Tasks look very similar: do I need both?
Indeed, in a simple example as this one, the Agent
and Task
look alike. In real-world applications, an Agent
gets to
perform multiple tasks. Then, an Agent
represents the expertise (goal
, backstory
) with a set of skills (tools
), while a Task
is a goal to accomplish.
Assembling the Crew
As covered earlier, a Crew
defines a set of Task
to be performed sequentially by a team of Agents
.
Note that Tasks
share a context, explaining why the research task comes before the writing task.
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
memory=True,
cache=True,
max_rpm=100,
)
result = crew.kickoff(inputs={'topic': 'AI in healthcare'})
print(result)
Let’s now build our Flight Booking Crew with these fresh new concepts!
1. Our Flight Booking Crew
Before jumping into the setup and code, let’s step back and look at how to assemble a Crew that helps book flights.
From a user input like “Sofia to Berlin one-way on 26th May”, our Flight Booking Crew should print the top 5 flights as follows:
Here are our top 5 picks from Sofia to Berlin on 2nd July 2024:
1. **Ryanair**
- Departure: 21:35
- Arrival: 22:50
- Duration: 2 hours 15 minutes
- Layovers: Direct
- Price: $18
- Booking: [Ryanair](https://www.skyscanner.net/transport/flights/sof/ber/240702/0/config/16440-2407022135--31915-0-9828-2407022250?currency=USD)
...
To achieve this goal, our Crew will navigate to https://www.skyscanner.net, perform a search, and extract each flight detail, which translates to the following steps:
- Parse the user request (“Sofia to Berlin one-way on 26th May”) to build a valid Skyscanner search URL
- Navigate to the Skyscanner search URL and extract the top 5 flights
- For each flight, navigate to the flight details URL to extract the available providers (airlines)
- Summarize the flights’ information
To perform those steps, we will create 2 Agents:
- The “Flights” Agent, responsible for looking for flights
- The “Summarize” Agent, responsible for summarizing the available flights as a comprehensive list
The “Search Flights” Agent will need:
- A custom
SkyScanner
tool to translate the user input into a valid Skyscanner search URL - A Browserbase tool to navigate on SkyScanner and interact with the web page
Finally, we will define 2 tasks: “Search Flights” and “Search Booking Providers”.
We can visualize our Flight Booking Crew as follows:
![](https://mintlify.s3-us-west-1.amazonaws.com/browserbase/images/guides/crewai.png)
Our Crew comprises 2 Agents, 2 Tools, and 2 Tasks.
Let’s implement our Crew!
2. Installation
Let’s setup the project by installing the required dependencies:
pip install crewai 'crewai[tools]' html2text playwright python-dotenv
Create a .env
file with the following variables and their respective values:
OPENAI_API_KEY=
BROWSERBASE_API_KEY=
BROWSERBASE_PROJECT_ID=
# our Flight Booking's "Search Flights" Agent will have to load a lot of context (heavy webpages as text),
# let's configure a specific OpenAI model to avoid token size limits:
OPENAI_MODEL_NAME=gpt-4-turbo
Where can I find my OpenAI and Browserbase API Keys?
- Get your Browserbase API Key and Project ID from your Settings page.
- Get your OpenAI API Key from the OpenAI Platform.
3. Create the Tools
While CrewAI provides a wide range of tools (e.g., the SerperDevTool to perform searches with Google Search), our “Search Flights” Agent needs 2 custom tools:
- a custom
SkyScanner
tool to assemble a valid Skyscanner search URL - a Browserbase loader to navigate and interact with the web pages
The Browserbase Tool
The SkyScanner website relies heavily on JavaScript and performs a live flight search, making it hard to interact with:
![](https://mintlify.s3-us-west-1.amazonaws.com/browserbase/images/integrations/crewai/skyscanner.png)
The page is fully loaded, however the flights are still being searched.
Fortunately, leveraging Browserbase’s headless browsers makes loading and interacting with such websites easier while benefiting from its Stealth features.
Let’s take a look at our custom Browserbase Tool implementation:
import os
from crewai_tools import tool
from playwright.sync_api import sync_playwright
from html2text import html2text
from time import sleep
@tool("Browserbase tool")
def browserbase(url: str):
"""
Loads a URL using a headless webbrowser
:param url: The URL to load
:return: The text content of the page
"""
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(
"wss://connect.browserbase.com?enableProxy=true&apiKey="
+ os.environ["BROWSERBASE_API_KEY"]
)
context = browser.contexts[0]
page = context.pages[0]
page.goto(url)
# Wait for the flight search to finish
sleep(5)
content = html2text(page.content())
browser.close()
return content
Custom Tool definition
A custom Tool
is composed of 3 elements:
- a name, via the
@tool("name")
decorator - a description defining the purpose of the tool along with its parameters
- a function that contains the tool’s logic
The description, provided as a multi-line comment, is used by the Agents to evaluate the best-fitted Tool
to help complete a given Task
.
A description can also provide instructions on the parameters. Here, we instruct that the unique url
parameter should be a URL.
Browserbase Tool Logic
The Browserbase tool utilizes the playwright
library along with the Browserbase Connect API to initiate a headless browser session. This setup allows interaction with web pages as follows:
browser = playwright.chromium.connect_over_cdp(
"wss://connect.browserbase.com?enableProxy=true&apiKey="
+ os.environ["BROWSERBASE_API_KEY"]
)
Then, it leverages the html2text
library to convert the webpage’s content to text and return it to the Agent for processing.
The SkyScanner Tool
Agents are capable of reasoning but cannot build a valid Skyscanner search URL from the ground up.
To help our “Flights” Agent, we will create a simple SkyScanner Tool below:
from crewai_tools import tool
from typing import Optional
@tool("SkyScanner tool")
def skyscanner(
departure: str, destination: str, date: int, return_date: Optional[int] = 0
) -> str:
"""
Generates a SkyScanner URL for flights between departure and destination on the specified date.
:param departure: The IATA code for the departure airport (e.g., 'sof' for Sofia)
:param destination: The IATA code for the destination airport (e.g., 'ber' for Berlin)
:param date: The date of the flight in the format 'yymmdd'
:return_date: Only for two-way tickets. The date of return flight in the format 'yymmdd'
:return: The SkyScanner URL for the specified flight search
"""
return f"https://www.skyscanner.net/transport/flights/{departure}/{destination}/{date}/{return_date}?currency=USD"
The SkyScanner tool describes multiple parameters with specific format instructions.
For example: date: The date of the flight in the format 'yymmdd'
This illustrates the flexibility of Tools that can rely on the Agents
powerful reasoning capabilities to solve formatting challenges that generally require some preprocessing.
4. Set up the Agents
Our Flights Agent now has the tools to navigate the SkyScanner website from a high-level user input (“Sofia to Berlin one-way on 26th May”).
Let’s now set up our 2 Agents:
from crewai import Agent
# import our tools
from browserbase import browserbase
from skyscanner import skyscanner
flights_agent = Agent(
role="Flights",
goal="Search flights",
backstory="I am an agent that can search for flights.",
tools=[skyscanner, browserbase],
allow_delegation=False,
)
summarize_agent = Agent(
role="Summarize",
goal="Summarize content",
backstory="I am an agent that can summarize text.",
allow_delegation=False,
)
As outlined in the introduction, an Agent
needs 3 properties: a role
, a goal
, and a backstory
.
The role of our two Agents is to orchestrate the tools (build the URL, then navigate to it) and extract the information from the webpages’ text. For this reason, their definition is straightforward.
What is the role of the Summarize Agent?
Through our iterations in building this Flight Booker, we realized that the Crew, with a single Flights Agent was struggling to distinguish flights from flight providers (booking links).
The Summarize Agent, as we will cover in the next section, is not assigned to any task. It is created and assigned to the Crew to help digest the text extracted from the web pages and distinguish the flights from the providers (booking links).
4. Define the Tasks
Let’s now define the core part of our Flight Booking Crew, the Tasks
.
From a given flight criteria, our Crew should print the 5 first available flights with their associated booking link. To achieve such a result, our Crew needs to:
- Navigate to the Skyscanner search URL and extract the top 5 flights
- For each flight, navigate to the flight details URL to extract the available providers and booking links
The “Search flights” Task
Our Search flights Task is bound to our Flights Agent, getting access to our custom tools:
from crewai import Task
# Agents definitions...
output_search_example = """
Here are our top 5 flights from Sofia to Berlin on 24th May 2024:
1. Bulgaria Air: Departure: 14:45, Arrival: 15:55, Duration: 2 hours 10 minutes, Layovers: Munich, 2 hours layover, Price: $123, Details: https://www.skyscanner.net/transport/flights/sof/ber/240524/240526/config/16440-2405241445--32474-0-9828-2405241555|9828-2405262255--32474-0-16440-2405270205
"""
search_task = Task(
description=(
"Search flights according to criteria {request}. Current year: {current_year}"
),
expected_output=output_search_example,
agent=flights,
)
The description
will be provided to the Flights Agent who will call:
- The SkyScanner Tool to build a valid SkyScanner search URL
- Then, leverage the Browserbase Tool to get the flight results as text
- Finally, using the
output_search_example
and with the help of the Summarize Agent, it will return a list of 5 flights
Why do we provide the current_year
?
Most users will prompt a relative date, for example: “Sofia to Berlin one-way on 26th May”.
An Agent’s reasoning relies on OpenAI that lacks some intuition on relative date (OpenAI will always think we are in 2022).
For this reason, we need to specify the current year in the prompt (Task’s description
).
The “Search Booking Providers” Task
The Search Booking Providers Task relies heavily on the Agent
reasoning capabilities:
from crewai import Task
# Agents definitions...
output_providers_example = """
Here are our top 5 picks from Sofia to Berlin on 24th May 2024:
1. Bulgaria Air:
- Departure: 14:45
- Arrival: 15:55
- Duration: 2 hours 10 minutes
- Layovers: Munich, 2 hours layover
- Price: $123
- Booking: [MyTrip](https://www.skyscanner.net/transport_deeplink/4.0/UK/en-GB/GBP/ctuk/1/16440.9828.2024-05-26/air/trava/flights?itinerary=flight|-32474|319|16440|2024-05-26T21:05|9828|2024-05-26T22:15|130|-|-|-&carriers=-32474&operators=-32474&passengers=1&channel=website&cabin_class=economy&fps_session_id=20287887-26ad-45dc-b225-28fb4b9d8357&ticket_price=126.90&is_npt=false&is_multipart=false&client_id=skyscanner_website&request_id=4b423165-9b7b-4281-9596-cfcd6b0bb4e0&q_ids=H4sIAAAAAAAA_-NS52JJLinNFmLh2NHAKMXM8cRHoeH7yU1sRkwKjEWsqXm67k5VzO5OAQASECl8KQAAAA|8257781087420252411|2&q_sources=JACQUARD&commercial_filters=false&q_datetime_utc=2024-05-22T13:45:58&pqid=true&booking_panel_option_guid=dfb1f593-22dc-4565-8540-5f4f70979b9b&index=0&isbp=1&posidx=0&qid=16440-2405262105--32474-0-9828-2405262215&sort=BEST&stops=0&tabs=CombinedDayView&pre_redirect_id=7cdb112a-3842-4a51-b228-1cbcbc4c8094&redirect_id=a8541976-84a8-4161-849c-c7a6343125ae&is_acorn_referral=true)
"""
search_booking_providers_task = Task(
description="Load every flight individually and find available booking providers",
expected_output=output_providers_example,
agent=flights_agent,
)
By asking to “Load every flight individually”, the Flights Agent will understand that it needs to locate a URL to navigate to for each flight result.
The Search Booking Providers will indirectly rely on the Summarize Agent to consolidate the flights result and individual flight providers’ results as showcased in output_providers_example
.
4. Assemble our Flight Booking Crew
It is time to assemble our Crew by arranging the Task
in the correct order (search flights, then gather providers and booking links):
import sys
import datetime
from crewai import Crew, Process, Task, Agent
from bb import browserbase
from skyscanner import skyscanner
from dotenv import load_dotenv
load_dotenv() # take environment variables from .env.
# Tasks and Agents definitions...
crew = Crew(
agents=[flights_agent, summarize_agent],
tasks=[search_task, search_booking_providers_task],
# let's cap the number of OpenAI requests as the Agents
# may have to do multiple costly calls with large context
max_rpm=100,
)
result = crew.kickoff(
inputs={
"request": sys.argv[1],
"current_year": datetime.date.today().year,
}
)
print(result)
The Crew must complete the Search Flight task followed by the Search Booking Providers task.
As covered earlier, the Summarize Agent gets assigned to the Crew
- not to a Task
- to help consolidate the flights and providers into a simple list.
Let the Crew kick off!
A Crew
process starts by calling the kickoff()
method.
Our Crew needs 2 inputs: the user input (“Sofia to Berlin one-way on 26th May”) and the current year.
5. Running a Flight Booking search
Our CrewAI program is now complete!
Let’s give it a try and look at its execution steps in detail.
Running the program
OpenAI cost
Expect each run of the program to cost around $0.50 OpenAI credits.
The Agent reasoning relies heavily on OpenAI and sends large chunks of text (the webpages), resulting in significant contexts (~50k context tokens per run).
Let’s search for a one-way flight from New York to San Francisco by running:
python3 main.py "Sofia to Berlin one-way on 26th June"
As the program starts running in verbose mode, you should see some logs stream in your terminal; let’s take a closer look at the steps.
A close look at the Crew steps
Looking at the debugging logs streamed to the terminal helps us understand how our crew works.
Let’s explore the logs in the following steps:
Once finished, our program prints the final answered returned by the Crew
:
Here are our top 5 picks from Sofia to Berlin on 26th June 2024:
1. Bulgaria Air:
- Departure: 14:45
- Arrival: 15:55
- Duration: 2 hours 10 minutes
- Layovers: Direct
- Price: $124
- Booking: [Trip.com](https://www.skyscanner.net/transport_deeplink/4.0/UK/en-GB/USD/ctuk/1/16440.9828.2024-06-26/air/trava/flights?itinerary=flight|-32474|319|16440|2024-06-26T14:45|9828|2024-06-26T15:55|130|-|-|-&carriers=-32474&operators=-32474&passengers=1&channel=website&cabin_class=economy&fps_session_id=95e725cd-b90e-4cbe-a034-e2cfa58668f2&ticket_price=123.90&is_npt=false&is_multipart=false&client_id=skyscanner_website&request_id=ab0c1ce0-1806-40cc-9380-0d8dbfe212ba&q_ids=H4sIAAAAAAAA_-NS52JJLinNFmLh2NHAKMXM8cRHoWHL281sRkwKjEWsqXm67k5VzKHBLgCqsGeQKQAAAA|-5188320953107801340|2&q_sources=JACQUARD&commercial_filters=false&q_datetime_utc=2024-06-04T20:57:53&pqid=true&booking_panel_option_guid=b29e2270-b6f1-4e68-92b2-a92fd16f3183&index=0&isbp=1&posidx=0&qid=16440-2406261445--32474-0-9828-2406261555&sort=BEST&stops=0&tabs=CombinedDayView&pre_redirect_id=b60c45a8-bfa6-4391-bedb-0a562e7bc4c8&redirect_id=dd9bd124-1adc-4570-a5ab-688b6e77b622&is_acorn_referral=true)
2. Austrian Airlines:
- Departure: 16:05
- Arrival: 18:55
- Duration: 3 hours 50 minutes
- Layovers: Vienna
- Price: $171
- Booking: [Austrian Airlines](https://www.skyscanner.net/transport/flights/sof/ber/240626/0/config/16440-2406261605--32544-1-9828-2406261855?currency=USD)
3. Wizz Air + Wizz Air Malta:
- Departure: 14:45
- Arrival: 21:25
- Duration: 7 hours 40 minutes
- Layovers: Rome Fiumicino
- Price: $54
- Booking: [Kiwi.com](https://www.skyscanner.net/transport_deeplink/4.0/UK/en-GB/USD/skyp/1/16440.9828.2024-06-26/air/trava/flights?itinerary=flight|-31669|4315|16440|2024-06-26T14:45|11493|2024-06-26T15:45|120|-|-|-;flight|-30596|6071|11493|2024-06-26T19:10|9828|2024-06-26T21:25|135|-|-|-&carriers=-31669,-30596&operators=-31669;-30596&passengers=1&channel=website&cabin_class=economy&fps_session_id=e3999c49-ae49-449c-838e-ede4d2315c18&ticket_price=54.00&is_npt=false&is_multipart=false&client_id=skyscanner_website&request_id=37dc049f-3716-4554-aaf8-3cb8872fc756&q_ids=H4sIAAAAAAAA_-OS5mIpzq4sEGLh2NHAKMXM8cRHoWHL281sRkwKjADIIpcVHQAAAA|-8805270969201518612|2&q_sources=JACQUARD&commercial_filters=false&q_datetime_utc=2024-06-04T20:57:54&transfer_protection=protected&pqid=true&booking_panel_option_guid=2550dbf1-9e4d-4c33-bed1-da3788509857&index=2&isbp=1&posidx=0&qid=16440-2406261445--31669%2C-30596-1-9828-2406262125&sort=BEST&stops=1&tabs=CombinedDayView&pre_redirect_id=757b22c7-9e0a-490d-b05d-c8a44aa10c3b&redirect_id=e1dc6447-e927-4b88-b4d1-a6c7edc23b65&is_acorn_referral=true)
4. LOT:
- Departure: 14:40
- Arrival: 18:20
- Duration: 4 hours 40 minutes
- Layovers: Warsaw Chopin
- Price: $155
- Booking: [LOT](https://www.skyscanner.net/transport/flights/sof/ber/240626/0/config/16440-2406261440--32093-1-9828-2406261820?currency=USD)
5. Lufthansa:
- Departure: 19:30
- Arrival: 22:35
- Duration: 4 hours 5 minutes
- Layovers: Munich
- Price: $170
- Booking: [Lufthansa](https://www.skyscanner.net/transport/flights/sof/ber/240626/0/config/16440-2406261930--32090-1-9828-2406262235?currency=USD)
Wrapping up
CrewAI provides a powerful way to develop AI Agents. The traditional approach of Prompt Engineering is replaced by instructions that leverage the Agent
’s reasoning capabilities.
As we covered in this example, the Agents are capable of completing Tasks
defined with high-level instructions (ex: “Load every flight individually and find available booking providers”)
Combined with Browserbase headless browsers, crewAI helps create powerful AI Agents that automate human tasks or provide support in accessing data not accessible through public APIs.
View the source code on GitHub
Clone this flight booker repo to try it out!
Was this page helpful?