OpenAI assistants step-by-step

Lalo Fuentes
7 min readFeb 27, 2024

Why assistants?

Maybe we should ask before, what are assistants? From a simple view assistants are an easier approach from OpenAI to simplify Generative AI/ RAG tools, -> tools you use to build applications using an LLM to find answers based on specific data (e.g. your company data).

source: https://www.ml6.eu/blogpost/leveraging-llms-on-your-domain-specific-knowledge-base

OpenaAI assistants are adding a lot of power for building apps over LLMs. It goes a step beyond the basic approach of using function calls, like OpenAI API or the Langchain framework, mainly by adding some facilities:

  • simpler functions definition. Just add your code and a JSON schema explaining what your function does and your function is ready to work!
  • unlimited prompt length. There are no more problems with large prompts (input or outputs), assistants take care of the dirty work
  • prompt memory. You don’t need to recall details of your conversation to the assistant, it manages the old messages to keep track of your conversation
  • conversations history. Conversations are automatically stored, just like ChatGPT does.
  • Extra tools: Retrieval (including doc files) and Code interpreter (will will talk about these two later).

How OpenAI assistants work?

Note: to follow the process in this article you need an OpenAI account with an API key.

There are two ways to make an assistant:

  1. you can create your own assistant from the OpenAI site and then use it to check its abilities on the Playground tool (check the image below)
assistant details in OpenAI dashboard

2. code it with Python (see next)

OpenAI assistant code for building an expert stock data analyst

This sample code uses the Yahoo Finance API yfinance to answer user questions about market behavior (you can find the whole code in my repository: https://github.com/lalo-fuentes/openai-assistant.git)

Before diving into the code let me try to explain how the assistant objects structure works:

assistants structure from https://platform.openai.com/docs/assistants/how-it-works
  • assistant: this object contains information about the model (gpt-4, gpt-3.5-turbo), the tools (functions, retrieve, code_interpreter), instructions (what the assistant does), …
  • thread: contains messages (keeps the messages history)
  • messages: user messages (queries), system messages (AI responses)
  • run: executes threads
  • steps: steps done by runs (like tool calls)
Dependencies among assistants objects

The code

We are going to divide our code into two modules: the functions and the app.

Functions to interact with the external world, like fetching data from an API, will be in the functions module (will see the contents later)

So let’s dive into the main module assistant.py

First of all, let’s declare the libraries and get the OpenAI API key

from openai import OpenAI
import os
import time
from def_tools import *
import json
# Import API keys
import a_env_vars
os.environ["OPENAI_API_KEY"] = a_env_vars.OPENAI_API_KEY

Then, we initiate or create the assistant:

# Create assistant

client = OpenAI()
assist_name ='Market.00'

# --------------------------------------------------------------------
def initiate_assistant():

system_msg = '''
- You are an expert stock data market analyst
- You can fetch reliable data through the functions available
- Always use get_todays_date function when you need to know today's date or a relative date
'''
# check for existing assistants
my_assistants = client.beta.assistants.list(
order="desc",
limit="5",
)

result = fetch_assistant_by_name(my_assistants.data, assist_name)
if result is None:
my_assistant = client.beta.assistants.create(
instructions=system_msg,
name=assist_name,
tools=ftools,
model="gpt-3.5-turbo",
)
else:
my_assistant = client.beta.assistants.retrieve(result.id)

return my_assistant

# -------------------------------------------
def fetch_assistant_by_name(data, assist_name):
for assistant in data:
if assistant.name == assist_name:
return assistant
return None

In this app, we always use the same assistant. If it doesn’t exist we create it, if it was already created we fetch it by its name. Of course, you can use the assistants as you want, you can create a new one each time you start your application or even delete the assistants before closing your app.

Note: OpenAI offers you the ability to manage your assistant from your OpeanAI dashboard (check Assistants in your menu). You can create, modify, list, retrieve, or delete, your assistants from the code.

assistants list on OpenAI dashboard

Then we create the thread:

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def open_thread():
new_thread = client.beta.threads.create()
return new_thread

We have a very simple approach, we just create a new thread each time we enter the app. The OpenAI library allows you to create, modify, retrieve, or delete your threads.

Note: As in the assistant, you can manage your threads from the OpenAI dashboard. Go to Settings / Organization / Features and capabilities, to make them visible in your dashboard.

Threads on OpenAI dashboard

Now we add a user message to the thread

# --------------------------------------------------------------------
# add user message to current thread
def add_query(thread, query):
thread_message = client.beta.threads.messages.create(
thread.id,
role="user",
content=query,
)
return thread_message

Finally, we run the thread, i.e. we ask for a reply to the model

# ---------------------------------------------------------------------------------------
def runOpenai(thread_id,assistant_id):
run = client.beta.threads.runs.create(
thread_id=thread_id,
assistant_id= assistant_id,
)
while True:
run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
print(run.status)
if run.status == 'completed':
break
elif run.status == 'requires_action':
tool_calls = run.required_action.submit_tool_outputs.tool_calls
tool_outputs = callTools(tool_calls)
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run.id,
tool_outputs=tool_outputs
)
time.sleep(.5)
return run

Create the run, then, retrieve the run until it is ‘completed’.

If your user query involves the use of a function the run.status will ask about ‘requires_action’. tool_calls will contain the function name the LLM is asking to call.

We are using the function callTools() to call the functions the model is proposing. Check here:

# ------------------------------------------------------------------------------------------------------------
def callTools(tool_calls):
tool_outputs = []
for t in tool_calls:
functionName = t.function.name
attributes = json.loads(t.function.arguments)
args = list(attributes.values())
# Get the real function from its name
function = globals().get(functionName)
try:
if args:
functionResponse = function(*args)
else:
functionResponse = function()
except Exception as e:
functionResponse = { "status" : 'Error in function call ' + functionName + '(' + t.function.arguments + ')', "error": str(e) }
tool_outputs.append({ "tool_call_id": t.id , "output": json.dumps(functionResponse) })
return tool_outputs

As you may noticed, we are dumping the response into a JSON schema (preferred by LLMs)

And finally, this is the main logic:

# ---------------- main logic -----------------------------------------
my_assistant = initiate_assistant()

thread = open_thread()

while True:
query = input("Enter your query: ")
message = add_query(thread, query)

run = runOpenai(thread.id, my_assistant.id)
messages = client.beta.threads.messages.list(thread_id=thread.id)
response = messages.data[0].content[0].text.value

print(response)

The functions module

Wait, wait, of course, there is still the functions module missing, here you go:

import json
import yfinance as yf

from datetime import date

# functions schemas

ftools = [{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Useful when you want to get the closing price of a stock between two dates.",
"parameters": {
"type": "object",
"properties": {
"ticker": {"type": "string", "description": "The stock ticker symbol"},
"start_date": {"type": "string", "description": "The first date to fetch data (YYYY-MM-DD)"},
"end_date": {"type": "string", "description": "The last date to fetch data + 1 day (YYYY-MM-DD)"}
},
"required": ["ticker", "start_date", "end_date"]
}
}
},
{
"type": "function",
"function": {
"name": "get_todays_date",
"description": "Useful when you want to know today's date. Useful when the user asks for data from a relative date, like: last price, current price, last week price, last month price, etc.",
"parameters": {
"type": "object",
"properties": {}
}
}
}]

def get_stock_price(ticker, start_date, end_date):
ticker_data = yf.Ticker(ticker)
data = ticker_data.history(period="1d", start=start_date, end=end_date)
data_list = []
for i in range(len(data)):
data_list.append({"date": data.index[i].strftime("%Y-%m-%d"), "price": data.iloc[i]["Close"], "currency": ticker_data.info["currency"]})
return json.dumps(data_list)

def get_todays_date():
# Get todays date
today = date.today()
# Convert date to ISO 8601 chain format
today_iso = today.isoformat()
# return a JSON object date
return json.dumps({"today": today_iso})

# ---------------------------------------------------------------------------

As you can see you need to declare first your functions in a JSON mode so the LLM can understand what they do and what arguments they need.

Then, just write your code to define your functions. These functions are returning JSON data, this is not mandatory but is a good practice to interact with LLMs.

Results

This is a sample of an iteration with the app. I’m printing the run.status just for better comprehension. The first action the LLM takes is asking for today’s date (models don’t know what day it is), then, it asks for yesterday’s closing price passing the right arguments: ticker, yesterday_date, and yesterday_date+1. Finally, it prints the answer.

Enter your query: give me the closing price of tesla from yesterday                

in_progress
in_progress
requires_action
get_todays_date
[]
in_progress
in_progress
requires_action
get_stock_price
['TSLA', '2024-02-26', '2024-02-27']
in_progress
completed
The closing price of Tesla from yesterday (February 26, 2024) was $199.40 USD.

Extra tools

As stated at the beginning of this article assistants offer you the capability, besides the functions tools already seen, to use two powerful built-in tools: Retrieval (including files) and Code interpreter

  • The retrieval will allow you to add files to your account (.pdf, .md, .docx. ,csv, and many more) and then retrieve data for your app. e.g. a chatbot that finds responses from company documentation.
  • Code interpreter. This can be a very powerful tool if it works as promised. “Code Interpreter allows the Assistants API to write and run Python code in a sandboxed execution environment. This tool can process files with diverse data and formatting, and generate files with data and images of graphs. Code Interpreter allows your Assistant to run code iteratively to solve challenging code and math problems. When your Assistant writes code that fails to run, it can iterate on this code by attempting to run different code until the code execution succeeds.” https://platform.openai.com/docs/assistants/tools/code-interpreter

How you use them:

assistant = client.beta.assistants.create(
instructions="You are a personal math tutor. When asked a math question, write and run code to answer the question.",
model="gpt-4-turbo-preview",
tools=[{"type": "code_interpreter"}]
)

Final considerations

  • Assistants are still on Beta, so expect the unexpected
  • Working with your data gives you some stability, but you are never safe from hallucinations
  • Assistants are big token eaters -> $$$. If you are using lots of data to retrieve and process prepare your wallet

--

--

Lalo Fuentes

Expert IT developer. Taking advantage of AI power to improve the world.