Skip to content

Commit

Permalink
fast food bot order
Browse files Browse the repository at this point in the history
  • Loading branch information
kakakuoka committed Aug 22, 2024
1 parent 6e6dd49 commit be06308
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 0 deletions.
117 changes: 117 additions & 0 deletions telephony-bot-api/fastfood-order-bot-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Project Overview

The Project is to create a fast food ordering voicebot using Langchain and Voicegain Telephony Bot API.
The bot has the ability to order from a predefined menu that includes burgers, wraps, drinks, fries and potato wedges.
The bot can distinguish between all vegetarian items and non-veg items.
The user has the ability to customize their order by adding topics / add-ons to their burger or wrap
and have the ability to pick from 3 different sizes for the fries and potato wedges.
They can further create custom combo orders and have a choice between 3 - main item with a drink,
main item with a side of their fries or potato wedges, main with a side and a drink.
If an order qualifies for a combo order - the bot automatically creates a combo for the user.
The bot can summarize the users order at any point along with calculating the total cost of the order.
The user can further edit their order at any point - by either changing an item (replace item, change quantity etc)
or deleting an item entirely.

# Project Steps

There are mainly 3 steps to it -
- Implement the Bot Logic
- Implement a Telephony Bot API service
- Setting up the AIVR bot on Voicegain Customer Portal.

Each of these work hand in hand to create the bot.

## 1. Bot Logic

For the bot logic, we use LangChain which is a framework to build with LLMs by chaining interoperable components.
This helps in managing the workflow of the bot.

For Langchain Reference kindly refer the Documentation here - https://python.langchain.com/v0.2/docs/introduction/

If the reader is interested in the detailed bot implementation, kindly read through ---

The bot logic is powered by LangChain and OpenAI's API.
In order to create the bot framework, chains are used to link the various functions that the bot can perform.
By using tools, the program is able to create a basic function framework that ChatGPT can use depending on the task.
It is important to note that the function description is key as this informs ChatGPT
what the function of each of these functions are, and can respectively pick the appropriate function for the task.
This logic for bot is present in [fast_food_bot.py](fast_food_bot.py).

The core functions are therefore created as tool functions in LangChain.
Functions such as giving the order summary and the total order cost have their own function as well,
so as to prevent possible errors arising from ChatGPT.

### 1.1 Tools

- get_menu: Returns the menu and prices for burgers, wraps, fries, wedges, and drinks.
- get_add_ons: Lists all possible add-ons and their costs.
- get_combos(main_item, side=None, drink=None): Provides possible meal combos based on user selections.
- update_order(new_order: OrderItem): Updates the current order, handles adding, updating, and deleting items.
- get_final_price: Calculates the total price of the current order.
- get_breakdown: Provides a detailed breakdown of the current order.

### 1.2 Agents and Chains

- ChatPromptTemplate: Defines the conversation prompt template.
- create_tool_calling_agent: Creates an agent that uses the defined tools and prompt.
- AgentExecutor: Manages the execution of the agent with tools.
- RunnableWithMessageHistory: Manages conversation history and processes the inputs

### 1.3 Steps to run the bot locally -

- Install the requirements in requirements.txt
- Set your openai api key in 'fast_food_bot.py' file.
- Run the main.py file to start the local uvicorn server.
- Run the http_client_example now to interact with the bot via text.


## 2. Telephony Bot API Integration

Reference Document: https://console.voicegain.ai/api-documentation#tag/aivr-callback

Implement POST / PUT / DELETE

- POST: We create a new call session with unique 'csid'. They can be used to uniquely identify the call.
- PUT: If we get user output, call CHAIN to relay information to the bot. If not, reprompt the user for a response.
- DELETE: Delete the session from Chain's memory

This is the component that integrates Voicegain's Telephony Bot API with the function of the langChain bot.
This adds the ability to interact with the bot through speech, rather than text,
thereby enhancing the user interaction experience. This logic is present in [main.py](main.py).


### 2.1 Functions

- create_session(request: TelephonyBotPostReq): Adds a new call session with unique sid, csid.
- update_session(request: TelephonyBotPutReq): Updates an existing order using the sid, csid based on the user input. Forwards input to the Fast Food Bot and returns the response.
- delete_session(request: TelephonyBotDeleteReq): Removes the call session and associated chat history from the in-memory store by csid.


## 3. Create AIVR App on Voicegain Portal.

By making an AIVR App on the Voicegain Portal, we can enable the speech component to our food ordering bot. This has mainly 3 steps -

- Hosting Bot on a server
- Providing the URL in the AIVR App created on the Voicegain Portal

We explore these steps in detail below.

### 3.1 Hosting Bot on Server

When you've run the [main.py](main.py) file and the bot is running locally on your device, you need to host the bot on a public URL for using on AIVR app.
One of the options for that is to use Ngrok - https://dashboard.ngrok.com/get-started/setup/windows

### 3.2 Setting up AIVR App

After you've configured the public URL for the food ordering bot, we need to setup the AIVR App using the following steps -

1. Login to the Voicegain console - https://console.voicegain.ai/specific/aivr-apps <img src="screenshots/Screenshot-1.png"/>
2. Buy a phone number in the phone management section. Click on buy number, add your area code and buy. This will be used later in the Main DNS field for AIVR App. <img src="screenshots/Screenshot-2.png"/> <img src="screenshots/Screenshot-3.png"/>
3. Now move to 'Phone Apps' section from 'Phone Management' in the toolbar on the left. Add new AIVR App using the blue button on top-right and filling in the details. Remember to add the number you purchased in the 'Main DNIS' section. <img src="screenshots/Screenshot-4.png"/> <img src="screenshots/Screenshot-5.png"/>
4. Click on Add Logic at the end and fill in details. The public URL where you host the bot goes in here <img src="screenshots/Screenshot-6.png"/>
5. After that click on OK and Save to save your AIVR bot. You should get a screen like this with a number in Main DNIS field - <img src="screenshots/Screenshot-7.png"/>

Now you can call on the phone number in the Main DNIS and you can use the food ordering bot on call.

### Credits -
Voicegain Team
233 changes: 233 additions & 0 deletions telephony-bot-api/fastfood-order-bot-example/fast_food_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.tools import tool
from langchain.agents import AgentExecutor, create_tool_calling_agent
import os
from typing import List

os.environ["OPENAI_API_KEY"] = 'YOUR-OPENAI-KEY'

llm = ChatOpenAI(model="gpt-4o-mini")


# tool to contain the menu of the restaurant
@tool
def get_menu():
"""Get fast food restaurant menu and price for each item. The price is in dollars
the menu has burgers and wraps. There are also fries and wedges that can be ordered, and there are 3 different sizes for them as well."""
menu = {
"burgers": [
{"name": "Cheeseburger", "type": "non-veg", "price": 10},
{"name": "Black Bean Burger", "type": "veg", "price": 12},
{"name": "Hamburger", "type": "non-veg", "price": 12},
{"name": "Crispy Chicken Burger", "type": "non-veg", "price": 12},
{"name": "Veggie Burger", "type": "veg", "price": 12},
{"name": "Fish Burger", "type": "non-veg", "price": 12},
{"name": "BBQ Chicken Burger", "type": "non-veg", "price": 12},
{"name": "Spicy Chicken Burger", "type": "non-veg", "price": 12},
{"name": "Turkey Burger", "type": "non-veg", "price": 12},
{"name": "Mushroom Swiss Burger", "type": "veg", "price": 12}
],
"wraps": [
{"name": "Grilled Chicken Wrap", "type": "non-veg", "price": 10},
{"name": "Spicy Chicken Wrap", "type": "non-veg", "price": 10},
{"name": "BBQ Chicken Wrap", "type": "non-veg", "price": 10},
{"name": "Veggie Wrap", "type": "veg", "price": 10},
{"name": "Turkey Wrap", "type": "non-veg", "price": 10},
{"name": "Buffalo Chicken Wrap", "type": "non-veg", "price": 10},
{"name": "Chicken Caesar Wrap", "type": "non-veg", "price": 10},
{"name": "Steak Wrap", "type": "non-veg", "price": 10},
{"name": "Falafel Wrap", "type": "veg", "price": 10},
{"name": "Hummus Veggie Wrap", "type": "veg", "price": 10}
],
"fries": [
{"name": "Regular Fries", "sizes": {"Small": 2, "Medium": 3, "Large": 4}},
{"name": "Curly Fries", "sizes": {"Small": 2.5, "Medium": 3.5, "Large": 4.5}},
{"name": "Sweet Potato Fries", "sizes": {"Small": 3, "Medium": 4, "Large": 5}},
{"name": "Waffle Fries", "sizes": {"Small": 3, "Medium": 4, "Large": 5}},
{"name": "Cajun Fries", "sizes": {"Small": 2.5, "Medium": 3.5, "Large": 4.5}},
{"name": "Chili Cheese Fries", "sizes": {"Small": 4, "Medium": 5, "Large": 6}},
{"name": "Garlic Fries", "sizes": {"Small": 3, "Medium": 4, "Large": 5}},
{"name": "Truffle Fries", "sizes": {"Small": 4.5, "Medium": 5.5, "Large": 6.5}},
{"name": "Loaded Fries", "sizes": {"Small": 5, "Medium": 6, "Large": 7}},
{"name": "Cheese Fries", "sizes": {"Small": 3.5, "Medium": 4.5, "Large": 5.5}}
],
"wedges": [
{"name": "Potato Wedges", "sizes": {"Small": 3, "Medium": 4, "Large": 5}},
{"name": "Cajun Wedges", "sizes": {"Small": 3.5, "Medium": 4.5, "Large": 5.5}},
{"name": "Garlic Parmesan Wedges", "sizes": {"Small": 4, "Medium": 5, "Large": 6}},
{"name": "Buffalo Wedges", "sizes": {"Small": 4, "Medium": 5, "Large": 6}},
{"name": "Loaded Potato Wedges", "sizes": {"Small": 5, "Medium": 6, "Large": 7}},
{"name": "Cheesy Wedges", "sizes": {"Small": 3.5, "Medium": 4.5, "Large": 5.5}},
{"name": "BBQ Wedges", "sizes": {"Small": 4, "Medium": 5, "Large": 6}},
{"name": "Spicy Wedges", "sizes": {"Small": 3.5, "Medium": 4.5, "Large": 5.5}},
{"name": "Truffle Wedges", "sizes": {"Small": 4.5, "Medium": 5.5, "Large": 6.5}},
{"name": "Herb Wedges", "sizes": {"Small": 3.5, "Medium": 4.5, "Large": 5.5}}
],
"drinks": [
{"name": "Coca-Cola", "size": "Medium", "price": 2},
{"name": "Pepsi", "size": "Medium", "price": 2},
{"name": "Sprite", "size": "Medium", "price": 2},
{"name": "Fanta Orange", "size": "Medium", "price": 2},
{"name": "Dr. Pepper", "size": "Medium", "price": 2},
{"name": "Mountain Dew", "size": "Medium", "price": 2},
{"name": "Root Beer", "size": "Medium", "price": 2},
{"name": "Iced Tea", "size": "Medium", "price": 2},
{"name": "Lemonade", "size": "Medium", "price": 2},
{"name": "Fruit Punch", "size": "Medium", "price": 2}
]
}
return menu


# list of all add ons for the items
@tool
def get_add_ons():
"""all possible add ons """
add_ons = [
{"name": "Extra Cheese", "cost": "1"},
{"name": "Lettuce", "cost": "1"},
{"name": "Tomatoes", "cost": "1"},
{"name": "Bacon", "cost": "1.5"},
{"name": "Avocado", "cost": "2"},
{"name": "Jalapenos", "cost": "0.5"},
{"name": "Extra Patty", "cost": "3"},
{"name": "Guacamole", "cost": "2"},
{"name": "Pickles", "cost": "0.5"},
{"name": "Onions", "cost": "1"},
{"name": "Mushrooms", "cost": "1.5"},
{"name": "BBQ Sauce", "cost": "0.5"},
{"name": "Ranch Dressing", "cost": "0.5"},
{"name": "Honey Mustard", "cost": "0.5"},
{"name": "Chipotle Sauce", "cost": "0.5"},
]
return add_ons


# tool to help define what combos are possible
@tool
def get_combos(main_item: str, side: str = None, drink: str = None):
"""Create a meal combo of the user's choice. The user can select a burger or wrap from the menu,
along with an optional drink and either fries or wedges with that as well, which is also optional."""
combos = {
"Main with a Side": {"main_item": main_item, "side": side, "price": 10},
"Main with a Drink": {"main_item": main_item, "drink": drink, "price": 11},
"Main with Side and Drink": {"main_item": main_item, "side": side, "drink": drink, "price": 12}
}
return combos


class OrderItem(BaseModel):
item: str = Field(description="the item user ordered from the menu")
price: float = Field(description="price of the item")
delete: bool = Field(description="Whether the user want to delete this item")
quantity: int = Field(description="the amount of items ordered")
add_on: str = Field(description="the add on item ordered from the menu")
price_add_on: int = Field(description="the price of the add on items")
combined_cost: int = Field(description="the cost of the order including the items and the odd ons")


# to store the current order
my_order: List[OrderItem] = []


@tool
def update_order(new_order: OrderItem):
"""This function needs to be called when the user order something, or update their order, or delete something"""
global my_order

if new_order.delete:
# Remove the item from the order
my_order = [item for item in my_order if not (item.item == new_order.item and item.add_on == new_order.add_on)]
else:
item_found = False
for item in my_order:
if item.item == new_order.item and item.add_on == new_order.add_on:
item_found = True
if new_order.quantity > 0:
item.quantity = new_order.quantity
item.price = new_order.price
item.price_add_on = new_order.price_add_on
item.combined_cost = item.price + item.price_add_on
else:
my_order.remove(item)
break

if not item_found and new_order.quantity > 0:
my_order.append(new_order)

print(f"Updated order: {my_order}")


@tool
def get_final_price():
""" Get the final price of the order """
global my_order
total_price = sum(item.combined_cost * item.quantity for item in my_order)
return total_price


# Function to get breakdown of the current order
@tool
def get_breakdown():
""" function to get the cost breakdown of the order """
global my_order
to_return = ""
total = 0
for item in my_order:
to_return += f'{item.item} with {item.add_on}: cost {item.combined_cost}\n'
total += item.combined_cost
to_return += f'total order cost is {total}'
return to_return


tools = [get_menu, update_order, get_final_price, get_add_ons, get_combos, get_breakdown]


prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant for a fast food ordering service. Answer all questions to the best of your ability."
"You are a voice bot, so only return sentences without formatting. ",
),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)

chain = prompt | llm


MEMORY_HISTORY_STORE = {}


# storing chat history
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in MEMORY_HISTORY_STORE:
MEMORY_HISTORY_STORE[session_id] = InMemoryChatMessageHistory()
return MEMORY_HISTORY_STORE[session_id]


agent = create_tool_calling_agent(llm, tools, prompt)

agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

CHAIN = RunnableWithMessageHistory(
agent_executor,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="output",
)


if __name__ == "__main__":
while True:
input_text = input("input: ")
output = CHAIN.invoke({"input": input_text}, config={'configurable': {'session_id': 'foo'}})["output"]
print(output)
Loading

0 comments on commit be06308

Please sign in to comment.