AI Assistants API - Things to know
In this post we will build a simple application McHelper (Assistant for McDonalds) whose goals is to take orders from the customers and suggest or recommend items they can buy aka upsell without being pushy.
Here is what we will learn : AI Assistant API, Retrieval Plugin & Streamlit
At a high level we can break the AI Assistants into 4 different concepts
You can think of assistants as applications or tool for a given use case in our use case it's McHelper that is an AI Assistant for taking orders for Mc Donalds.
We can think of threads as the ChatGPT threads or conversations that we are all aware of. One issue with using previous chat completion api was to maintain conversation history on the client side and pass it to ChatGPT as and when necessary. With AI Assistants that is solved as we don't have to maintain the history. However, with this approach there are 2 things that we lose control of
Message are the individual conversations between user and assistants. They are always attached to the thread. One thing to note here is that nothing happens when a message a created in a thread. We have have to run the thread in a given application context to get response from the assistant. Run object has different status from being queued to completed. It also includes many other statuses as described in flow chart below
Let's start by writing helper functions to help with calling AI Assistants most of the code is repurposed from the OpenAI Cookbook.
import openai
import json
from openai import OpenAI
import os
import json
import time
client = OpenAI()
model = "gpt-4-1106-preview"
FILE_SUFFIX = "assistant_id.txt"
# Pretty printing helper
def pretty_print(messages):
print("# Messages")
for m in messages:
print(f"{m.role}: {m.content[0].text.value}")
print()
# Waiting in a loop
def wait_on_run(run, thread):
while run.status == "queued" or run.status == "in_progress":
run = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id,
)
time.sleep(0.5)
return run
def create_assistant(name, instructions, retrieval_file=None):
ASSISTANT_ID_FILE = f"{name}_{FILE_SUFFIX}"
assistant_id = None
if not os.path.exists(ASSISTANT_ID_FILE):
uploaded_file = client.files.create(
file=open(
retrieval_file,
"rb",
),
purpose="assistants",
)
assistant = client.beta.assistants.create(
name=name,
instructions=instructions,
model="gpt-4-1106-preview",
tools=[{"type": "retrieval"}],
file_ids=[uploaded_file.id],
)
assistant_id = assistant.id
with open(ASSISTANT_ID_FILE, "w") as fd:
fd.write(assistant.id)
else:
with open(ASSISTANT_ID_FILE) as data_file:
assistant_id = data_file.read().strip()
return assistant_id
def show_json(obj):
print(json.loads(obj.model_dump_json()))
def submit_message(assistant_id, thread, user_message):
client.beta.threads.messages.create(
thread_id=thread.id, role="user", content=user_message
)
return client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant_id,
)
def get_response(thread):
return client.beta.threads.messages.list(thread_id=thread.id, order="asc")
def create_thread_and_run(ASSISTANT_ID, user_input):
thread = client.beta.threads.create()
run = submit_message(ASSISTANT_ID, thread, user_input)
return thread, run
With that lets look at wrapping the whole flow in a streamlit app.
from assistants import *
import streamlit as st
import base64
import os
from dotenv import load_dotenv
load_dotenv()
st.title("McHelper Bot")
st.header("McDonald's Drive thru assistant")
instructions = """
Role: As McHelper, your primary role is to assist customers in choosing from the McDonald's menu.
Your focus should be on providing information about the menu items, including descriptions, ingredients, and any special promotions.
You should suggest additional items to complement a customer's order, but limit these suggestions to maximum one during the interaction to avoid being perceived as pushy or irritating.
Approach: You possess extensive knowledge of the McDonald's menu (provided to use as a knowledge base) and are skilled in guiding customers through their choices, ensuring they are aware of all relevant options. You take note of any specific dietary preferences or requests, such as vegetarian, vegan, or gluten-free needs. Your suggestions for additional items should be based on the customer's
current selection and their stated preferences.
Interaction: Maintain a friendly and helpful demeanor, prioritizing the customer's needs and preferences.
After making one thoughtful suggestions, focus solely on providing information and answering questions about the menu.
Avoid repeated or aggressive upselling techniques. Your goal is to ensure a pleasant and efficient ordering experience,
respecting the customer's choices and time.
"""
if "assistant_id" not in st.session_state:
print("Creating assistant")
assistant_id = create_assistant(
"McHelper", instructions, retrieval_file="mcdonalds_menu.txt" # taken from their website
)
st.session_state.assistant_id = assistant_id
# Initialize chat history
if "messages" not in st.session_state:
st.session_state.messages = []
# Display chat messages from history on app rerun
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Accept user input
if prompt := st.chat_input("What is up?"):
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
# Display user message in chat message container
with st.chat_message("user"):
st.markdown(prompt)
# Display assistant response in chat message container
with st.chat_message("assistant"):
tmp_run = None
messages = []
if "thread" not in st.session_state and "run" not in st.session_state:
thread, run = create_thread_and_run(st.session_state.assistant_id, prompt)
st.session_state.thread = thread
st.session_state.run = run
_ = wait_on_run(run, st.session_state.thread)
messages = get_response(st.session_state.thread)
else:
run = submit_message(
st.session_state.assistant_id, st.session_state.thread, prompt
)
_ = wait_on_run(run, st.session_state.thread)
messages = get_response(st.session_state.thread)
pretty_print(messages)
message_placeholder = st.empty()
full_response = ""
assistant_message = messages.data[-1].content[0].text.value
message_placeholder.write(assistant_message)
st.session_state.messages.append(
{"role": "assistant", "content": assistant_message}
)
The above code doesn't contain the code for using Elevenlabs but will cover it in the next post. If you are interested in similar content do subscribe to AIForFunAndProfit
I am planning to hook up this assistant with Twilio API to take orders and upsell using a simple phone call. If there are any other interesting use cases you can think of do let me know.