Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add travel planner sample #117

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gen-ai/Assistants/travel_planner/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
*.log
*.env
158 changes: 158 additions & 0 deletions gen-ai/Assistants/travel_planner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Travel Planner Assistant

## Overview
This sample provides a guide to use the new Web Browse tool with the Azure OpenAI Assistants.
This tool is based on Bing Search API and allows to easily implement a public web data grounding.


Given LLMs have data available only up to a cut off date, it might not handle questions that require up-to-date information.
And this is where the Web Browse tool comes in handy!


## Objective
The objective of this sample is to create an OpenAI assistant that can help you plan your trip and which will use the Web Browse
tool whenever it needs to get the latest information related to the trip.

The assistant will be implemented through a CLI in python (command line interface) which the user can use to interact with the assistant.

By the end of this tutorial, you should be able to:
- Create an OpenAI assistant that uses the Web Browse tool

## Programming Languages
- Python

## Estimated Runtime: 10 mins

## Pre-requisites
- A [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) resource (API key + endpoint)
- A [Bing Search](https://www.microsoft.com/en-us/bing/apis/bing-custom-search-api?msockid=05017416a2426182001960bfa3e36056) resource
- The Bing resource should be a Bing Search v7 resource and the SKU should be S15 or S16
- The Azure OpenAI resource needs to have 'Contributor' role on the selected Bing resource to use it
- Python 3.10 or later

## Running the sample

### Step 1: Fill in the environment variables
Create an `.env` file with the following environment variables:
```commandline
OPENAI_KEY="<openai_key>"
OPENAI_ENDPOINT="<openai_endpoint>"
BING_RESOURCE_ID="<bing_resource_id>"
```

Note: The first time you run, an assistant will be created. Its id will be stored in a new `assistant.env` file, which is used to load the assistant in the following runs.


### Step 2: Install requirements
```commandline
pip install -r requirements.txt
```

### Step 3: Run the sample
```commandline
python app.py
```

In order to exit the application you can type `exit`.
```commandline
Your input: exit
```

## Example
```commandline
I'm a travel planner copilot.
Please let me know what you are looking for and I'll try to suggest a nice trip for you!

Your input: I'm trying to plan a trip in august and that lasts 5 days. I'm looking for a place that is warm and is not expected to rain. It would be great if there is a rock concert by the same time, like foo fighters or metallica. I would be leaving from Quebec city. Could you come up with a suggestion for this trip?

Based on your preferences and the available information, here’s a trip suggestion for you!

**City:** Foxborough, Massachusetts
**Start and End Date:** 2024-08-16 to 2024-08-21

**Cost Breakdown:**
- Flights: A roundtrip flight from Quebec City to Boston Logan International Airport may cost approximately CAD 350-550, depending on the booking class and how far in advance you book.
- Accommodation: For a mid-range hotel, expect to pay about USD 150-250 per night. Total for 5 nights: approx. USD 750-1250.
- Car Rental: A car rental for the duration might cost around USD 300-500.
- Concert Tickets: Depending on seating, a ticket for the Metallica concert might range from USD 100 to 400 or more.
- Food and Miscellaneous: Approximately USD 50-100 per day. Total for 5 days: USD 250-500.
- Total Estimate: CAD 350-550 (Flight) + USD 1600-2650 (Stay, Car, Concert, Misc) exchanged to CAD at current rates.

**Weather Forecast:** The historical average for Foxborough in August has been warm with temperatures around 86°F (30°C). Typically, there’s a moderate chance of precipitation but relatively warm weather overall.

**Attractions:**
- **Metallica Concert**: Metallica will be rocking out at the Gillette Stadium in Foxborough on August 16, 2024. Make sure to book your tickets in advance as these events tend to sell out quickly!
- **Patriot Place**: Next to Gillette Stadium, this venue offers shopping, dining, and entertainment options to enjoy during your stay.
- **The Hall at Patriot Place**: A great place for sports fans to visit, featuring New England Patriots’ history and memorabilia.
- **Nearby Boston**: If time permits, take a trip into Boston to walk the Freedom Trail, visit the Museum of Fine Arts, or enjoy the local cuisine.

**Useful Information:**
- Book concert tickets as soon as possible to ensure availability and potentially better prices.
- Renting a car would be optimal for commuting between Boston and Foxborough, especially after the concert.
- Check the weather forecast closer to the departure date for a more accurate outlook and to plan your packing appropriately.

Remember to book accommodations and flights early for better rates, and have a fantastic time rocking out with Metallica!
```

## Understanding the Solution

### OpenAI Client

- Create an OpenAI client with:
- at least `2024-07-01-preview` version
- passing the header "X-Ms-Enable-Preview": "true"
```python
client = AzureOpenAI(
api_key=openai_key,
api_version="2024-07-01-preview",
azure_endpoint=openai_endpoint,
default_headers={"X-Ms-Enable-Preview": "true"}
)
```

### Assistant
- Create the assistant with file search and browser tools
- browser tool needs the bing resource id
- file search tool needs the vector store id
```python
# assistant.py
assistant = client.beta.assistants.create(
name="Travel planner copilot",
instructions='''
You are travel planner that helps people plan trips across the world.

The user might give you constraints like:
- destination
- weather preference
- attractions preference
- date preference

When asked for up-to-date information, you should use the browser tool.

You should try to give a plan in the following format:
- city
- start and end date
- cost breakdown
- weather forecast
- attractions and any useful information about tickets.
''',
tools=[{
"type": "browser",
"browser": {
"bing_resource_id": bing_resource_id
}
}],
model="gpt-4-1106-preview",
)
```

## FAQ

## How can I validate the browser tool was called?
You can validate the browser tool was called by checking the logs (`app.log` file). You should see a log similar to the following:
```
INFO:event_handler:completed calling tool browser
```

If you want you can also debug the `event_handler.py` file.
When the tool call is completed, the `on_tool_call_done` method is called. You can add a breakpoint there to check the response.
45 changes: 45 additions & 0 deletions gen-ai/Assistants/travel_planner/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
import logging

from openai import AzureOpenAI
from dotenv import load_dotenv

from cli import Cli
from assistant import setup_assistant

load_dotenv()
logging.basicConfig(
filename='app.log',
format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.INFO
)

logger = logging.getLogger(__name__)


if __name__ == "__main__":
bing_resource_id = os.getenv("BING_RESOURCE_ID")
openai_key = os.getenv("OPENAI_KEY")
openai_endpoint = os.getenv("OPENAI_ENDPOINT")

# validate environment variables
if bing_resource_id is None:
raise ValueError("BING_RESOURCE_ID is not set")
if openai_key is None:
raise ValueError("API_KEY is not set")
if openai_endpoint is None:
raise ValueError("AZURE_ENDPOINT is not set")

client = AzureOpenAI(
api_key=openai_key,
api_version="2024-07-01-preview",
azure_endpoint=openai_endpoint,
default_headers={"X-Ms-Enable-Preview": "true"}
)

assistant_id = setup_assistant(client=client, bing_resource_id=bing_resource_id)

runner = Cli(client, assistant_id)

runner.run()
78 changes: 78 additions & 0 deletions gen-ai/Assistants/travel_planner/assistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
import logging
from typing import TextIO

from dotenv import load_dotenv
from openai import AzureOpenAI

assistant_id_env_name = "ASSISTANT_ID"
assistant_env_filename = "assistant.env"

load_dotenv(dotenv_path=assistant_env_filename)
file_paths = [
"./assets/contoso_case.txt"
]


logger = logging.getLogger(__name__)


def setup_assistant(client: AzureOpenAI, bing_resource_id: str) -> str:
with open(assistant_env_filename, "a") as env_file:
assistant_id = get_or_create_assistant(client, bing_resource_id, env_file)

return assistant_id


def get_or_create_assistant(client: AzureOpenAI, bing_resource_id: str, env_file: TextIO) -> str:
assistant_id = os.getenv(assistant_id_env_name)

if assistant_id is not None:
try:
# validates vector store exists
client.beta.assistants.retrieve(assistant_id=assistant_id)
logger.info("Assistant with id {} already exists".format(assistant_id))
return assistant_id
except Exception as ex:
raise Exception(f"Error retrieving assistant with id {assistant_id}: {ex}")

assistant = client.beta.assistants.create(
name="Travel planner copilot",
instructions='''
You are travel planner that helps people plan trips across the world.

The user might give you constraints like:
- destination
- weather preference
- attractions preference
- date preference

When asked for up-to-date information, you should use the browser tool.

You should try to give a plan in the following format:
- city
- start and end date
- cost breakdown
- weather forecast
- attractions and any useful information about tickets.
''',
tools=[{
"type": "browser",
"browser": {
"bing_resource_id": bing_resource_id
}
}],
model="gpt-4-1106-preview",
)
assistant_id = assistant.id

logger.info("Created new assistant with id {}".format(assistant_id))

# stores the id in the assistant.env file
write_env(env_file, assistant_id_env_name, assistant_id)

return assistant_id


def write_env(env_file: TextIO, key: str, value: str):
env_file.write("{}=\"{}\"\n".format(key, value))
43 changes: 43 additions & 0 deletions gen-ai/Assistants/travel_planner/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging

from openai import AzureOpenAI

from event_handler import EventHandler


logger = logging.getLogger(__name__)


class Cli:
def __init__(self, client: AzureOpenAI, assistant_id: str):
self.client = client
self.assistant_id = assistant_id

def run(self):
thread = self.client.beta.threads.create()

logger.info("starting conversation with assistant (assistant_id={}, thread_id={})".format(self.assistant_id, thread.id))

print('''
I'm a law firm assistant.
How can I help you with court cases!
''')

while True:
user_input = input("\nYour input: ")

if user_input == "exit":
print("Exiting conversation with assistant")
break

self.client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content=user_input
)

print("\nAssistant: ", end="", flush=True)
event_handler = EventHandler()
with self.client.beta.threads.runs.stream(assistant_id=self.assistant_id, thread_id=thread.id,
event_handler=event_handler) as stream:
stream.until_done()
51 changes: 51 additions & 0 deletions gen-ai/Assistants/travel_planner/event_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import logging
from typing import Any

from openai import AssistantEventHandler
from openai.types.beta.threads.runs import ToolCall
from openai.types.beta.threads import Text, TextDelta


logger = logging.getLogger(__name__)


class EventHandler(AssistantEventHandler):
def __init__(self):
super().__init__()
self.is_processing_annotation = False

def on_exception(self, exception: Exception) -> None:
logger.error("please try again. an exception occurred: {}".format(exception))

def on_tool_call_created(self, tool_call: Any):
logger.info("started calling tool {}".format(get_tool_type(tool_call)))

def on_tool_call_done(self, tool_call: ToolCall) -> None:
logger.info("completed calling tool {}".format(get_tool_type(tool_call)))

def on_text_delta(self, delta: TextDelta, snapshot: Text) -> None:
print(delta.value, end="", flush=True)

def on_text_done(self, text: Text) -> None:
is_first_url_citation = True
for annotation in text.annotations:
if annotation.type == "url_citation":
if is_first_url_citation:
print("\nUrl citations: \n", end="", flush=True)
is_first_url_citation = False
title = annotation.model_extra['url_citation']['title']
url = annotation.model_extra['url_citation']['url']
print("* {} - [{}]({})\n".format(annotation.text, title, url), end="", flush=True)

def on_timeout(self) -> None:
logger.warning("timeout occurred. please try again")

def on_end(self) -> None:
logger.info("completed conversation with assistant")


def get_tool_type(tool_call: Any) -> str:
if isinstance(tool_call, dict):
return tool_call['type']
else:
return tool_call.type
2 changes: 2 additions & 0 deletions gen-ai/Assistants/travel_planner/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
openai==1.30.1
python-dotenv==0.21.0