Create an AI Chatbot: Function Calling with OpenAI Assistant

Alozie Igbokwe
9 min readMar 23, 2024

--

Welcome back to our series on crafting AI chatbots with OpenAI. In today’s installment, Part 2, we delve into the sophisticated realm of leveraging OpenAI’s Assistant to make function calls.

This powerful feature enables our chatbot to parse and transform unstructured data to a structured format that our code can easily understand, execute specific functions based on that data, and deliver responses that are dynamically informed by the outcomes of these function calls.

For those who might have missed it, Part 1 laid the groundwork by introducing how to set up the Retrieval Functionality — a crucial step that our current discussion builds upon. I highly recommend revisiting the first article to grasp the foundational concepts thoroughly.

Today, we’re moving to the next tool in the Assistant’s API wheelhouse, demonstrating how function calling can significantly enhance the OpenAI Assistant’s capability to interact with, and respond to, user inputs in a more personalized and context-aware manner.

Join us as we explore the mechanics and advantages of integrating function calls into your chatbot’s repertoire, a leap towards creating more interactive and intelligent conversational agents.

Step 1 : Function Overview

def get_records_by_lead_name(lead_name: str):
# Airtable settings
base_id = os.environ.get("base_id")
table_id = os.environ.get("table_id")
api_key_airtable = os.environ.get("api_key_airtable")

# Filter setup
column_name = 'Leads Name'
encoded_filter_formula = urllib.parse.quote(f"{{{column_name}}}='{lead_name}'")

# Airtable API URL
api_url = f"https://api.airtable.com/v0/{base_id}/{table_id}?filterByFormula={encoded_filter_formula}"

# Headers for authentication
headers = {
'Authorization': f'Bearer {api_key_airtable}'
}

# Make the API request
response = requests.get(api_url, headers=headers)

if response.status_code == 200:
# Successful request
records = response.json().get('records', [])
return records
else:
# Failed request, log details
error_info = response.text
try:
# Attempt to parse JSON error message
error_info = json.loads(response.text)
except json.JSONDecodeError:
# Response is not JSON formatted
pass


print(f"Failed to retrieve data. Status Code: {response.status_code}. Response: {error_info}")
return None

Let’s dive into the specific function we’ll empower our AI assistant to invoke..

We’re focusing on get_records_by_lead_name, a function designed to query our Airtable database for specific records. Here's how it unfolds:

  1. Environment Setup: Initially, we gather essential Airtable settings from our environment variables, including the base ID, table ID, and API key. These elements are critical for constructing our query and ensuring secure access to the database.
  2. Query Preparation: We define the name of the column we’re interested in ‘Leads Name’ and prepare a filter formula(encoded_filter_formula ). This formula is crafted to search for records where the lead’s name matches our input, ensuring we’re pinpointing the exact data needed.
  3. Constructing the API Request: With our filter in place, we construct the URL for the Airtable API(api_url). This URL includes our base and table IDs, alongside our filter formula, tailored to fetch records that match our specified criteria.
  4. Authentication: To authenticate our request, we prepare a header containing our Airtable API key. This step is crucial for gaining authorized access to our database.
  5. Executing the Request: We then send a GET request to Airtable using the requests library, passing in our API URL and headers. This request aims to retrieve records filtered by our criteria.
  6. Handling the Response: Upon receiving a response, we check the status code. For a successful request (status code 200), we parse and return the records found. If the request fails, we attempt to parse and log the error message, providing insight into what went wrong.

In other words the purpose is for the function to query all the data form Airtable that relates to the lead the user is specifically asking about

Part 2 : Tool List

tools_list = [{
"type": "function",
"function": {

"name": "get_records_by_lead_name",
"description": "Retrieve infomation of the specfic lead like when's the next meeting with them, their email and etc",
"parameters": {
"type": "object",
"properties": {
"lead_name": {
"type": "string",
"description": "The lead's name"
}
},
"required": ["lead_name"]
}
}
}]

To bridge the gap between the OpenAI assistant and our bespoke function, we employ a configuration variable named tools_list. This setup outlines how the assistant can tap into our custom function. Here's a closer look at the setup process:

  • Function Configuration: Within tools_list, we define a dictionary that encapsulates the essence of the get_records_by_lead_name function. This includes:
  • Function Name and Description: We specify the function’s name and a brief description that outlines its purpose — to fetch comprehensive details about a specific lead, such as their next meeting, email address, and more.
  • Parameters: This section is crucial. It delineates the expected input for the function, in this case, lead_name. By defining the input as a string and providing a clear description, we ensure the assistant understands exactly what information it needs to gather from the user's query and structure in json format so the input can be passed and properly read by the function.
  • Integration with the Assistant: To enable the assistant to utilize this function, we assign tools_list to the assistant's tools configuration. This direct assignment informs the assistant of the available external functions it can invoke, including the conditions under which each function should be executed.

This structured approach does not merely inform the assistant about the existence of the get_records_by_lead_name function. It goes a step further by specifying the function's role, the nature of its parameters, and the context in which it should be activated.

As a result, when the assistant encounters queries related to specific leads, it knows to call this function, using the extracted lead's name to fetch and return detailed information while with unrelated queries, like those asking for healthy pizza options, it knows not to call the function.

Part 3 : Getting the Assistant to call the function

assistant = client.beta.assistants.create(
name= "Airtable Func Call",
instructions="You are a sale chatbot. Use the data you pull from our airtable database to answer question on our potential clients.",
model="gpt-4-1106-preview",
tools=tools_list
)

We then assign tools_list to the tools parameter during the assistant's creation.

Then after we invoke our thread/assistant through client.beta.threads.runs.create we face a certain juncture in our execution flow, especially when managing the thread run invoked and monitored via client.beta.threads.runs.retrieve, we encounter a pivotal condition: the transition to a 'requires_action' status.

This particular status is a signal, indicating a moment where our assistant stands at a crossroads, awaiting further instructions.

When the status shifts to ‘requires_action’, it’s a clear indication that the assistant demands additional input to continue its task effectively. Unlike scenarios solely reliant on retrieval functionality, where the flow proceeds smoothly without external interventions, the function calling functionality introduces an intricate layer. Here, the assistant finds itself in need of executing a specific, predefined function to progress.

elif run_status.status == 'requires_action':
print("Function Calling")
required_actions = run_status.required_action.submit_tool_outputs.model_dump()
print(required_actions)

This requirement for action sets the stage for a unique operational phase where we create a flow using elif during our WHILE loop that allows the assistant to move past this state and execute & retrieve necessary output from the function

"object": "thread.run",
"required_action": {
"submit_tool_outputs": {
"tool_calls": [
{
"id": "call_wQFyLr9mKOEAJ78tliS9RDGT",
"function": {
"arguments": "{\"lead_name\":\"David\"}",
"name": "get_records_by_lead_name"
},
"type": "function"
}
]
},
"type": "submit_tool_outputs"
},
"started_at": 1710918507,
"status": "requires_action",

In the scenario where the run’s status is “required action,” the run object houses a required_action list. Specifically, within required_action, there's a submit_tool_outputs segment that enumerates tool_calls. Each tool_call represents a directive for the assistant to invoke an external function crucial for advancing the conversation. An example of such a directive might include a function call to get_records_by_lead_name, complete with necessary arguments to pass like {"lead_name":"David"} for its execution

# Retrieving the current run status
run_status = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
print(run_status.model_dump_json(indent=4))

We get these details by printing the run status details each time we check it, we enable a deep dive into the run object’s state, particularly when it’s in “required action.” This comparison helps demystify the logic governing the assistant’s operation in various states.

for action in required_actions["tool_calls"]:
func_name = action['function']['name']
arguments = json.loads(action['function']['arguments'])

if func_name == "get_records_by_lead_name":
output = get_records_by_lead_name(lead_name=arguments['lead_name'])
output_string = json.dumps(output)

In responding to these directives, our code meticulously parses through each entry in tool_calls, pinpointing the specific function to be executed and argument to be passed. It decodes the provided arguments from JSON format into a python dictionary, then uses those parameters to execute the function and set its response equal to the output variable.

  tool_outputs.append({
"tool_call_id": action['id'],
"output": output_string
})
else:
raise ValueError(f"Unknown function: {func_name}")

print("Tool Outputs")
print(tool_outputs)

Upon successful function execution, the result is transformed into a JSON string (output_string) and appended to tool_outputs. Initially empty, this list aggregates the outcomes of all essential tool calls, linking each result back to its corresponding tool_call_id.

[{'tool_call_id': 'call_wQFyLr9mKOEAJ78tliS9RDGT', 'output': '[{"id": "recfXjj4DOuXOU5XC", "createdTime": "2023–11–25T01:17:34.000Z", "fields": {"Company Size": "50–100", "Lead\'s Email": "david@gmail.com ", "Where was leads found": "Email Marketing Campaign ", "Next Meeting": "November 27, 2023 8:27pm est", "Meeting Notes": "Alozie(Salesmen) needs to prepare a basic demo of one chatbots we previously built\\n", "Leads Name": "David"}}]'}]

Here visible example of what has been added to the tool_outputs. You can see it has a unique id along with the output the function returned inside the list.

print("Submitting outputs back to the Assistant…")
client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
else:
print("Waiting for the Assistant to process…")
time.sleep(5)

The crucial step comes next: submitting these collected outputs back to the assistant via client.beta.threads.runs.submit_tool_outputs. This method takes tool_outputs as a parameter, alongside identifiers for the thread and run, effectively informing the assistant of the external function’s outcome.

Submitting the tool outputs allows the assistant to incorporate the retrieved data into its processing, enabling it to generate a response that might depend on information from an external database or application.

Once submitted, the run status transitions from “requires_action” back to “queue” and eventually to “completed” as the assistant finishes processing. At this point, the responses can be retrieved and displayed, showcasing the assistant’s ability to incorporate external data dynamically into its interactions

Part 4 : The Output

Here the output our code gave(The Assistant Response is at the end of the output) :

Assistant created with ID: asst_8WcUU6OI4kMEJZJSgUHuIlII
Creating a Thread for a new user conversation.....
Thread created with ID: thread_oBBeiP70wwx7yuMpXLf8LCxk
Adding user's message to the Thread: What do we need to prepare for in our next meeting with David
Message added to the Thread.
Running the Assistant to generate a response...
Run created with ID: run_6jkBdWUF6guERs9PfDTb96ZL and status: queued
{
"id": "run_6jkBdWUF6guERs9PfDTb96ZL",
"assistant_id": "asst_8WcUU6OI4kMEJZJSgUHuIlII",
"cancelled_at": null,
"completed_at": null,
"created_at": 1710921202,
"expires_at": 1710921802,
"failed_at": null,
"file_ids": [],
"instructions": "You are a sale chatbot. Use the data you pull from our airtable database to answer question on our potential clients.",
"last_error": null,
"metadata": {},
"model": "gpt-4-1106-preview",
"object": "thread.run",
"required_action": null,
"started_at": null,
"status": "queued",
"thread_id": "thread_oBBeiP70wwx7yuMpXLf8LCxk",
"tools": [
{
"function": {
"name": "get_records_by_lead_name",
"parameters": {
"type": "object",
"properties": {
"lead_name": {
"type": "string",
"description": "The lead's name"
}
},
"required": [
"lead_name"
]
},
"description": "Retrieve infomation of the specfic lead like when's the next meeting with them, their email and etc"
},
"type": "function"
}
],
"usage": null
}
{
"id": "run_6jkBdWUF6guERs9PfDTb96ZL",
"assistant_id": "asst_8WcUU6OI4kMEJZJSgUHuIlII",
"cancelled_at": null,
"completed_at": null,
"created_at": 1710921202,
"expires_at": 1710921802,
"failed_at": null,
"file_ids": [],
"instructions": "You are a sale chatbot. Use the data you pull from our airtable database to answer question on our potential clients.",
"last_error": null,
"metadata": {},
"model": "gpt-4-1106-preview",
"object": "thread.run",
"required_action": {
"submit_tool_outputs": {
"tool_calls": [
{
"id": "call_C4f29ji8cmyjSSuJeCXOsWS9",
"function": {
"arguments": "{\"lead_name\":\"David\"}",
"name": "get_records_by_lead_name"
},
"type": "function"
}
]
},
"type": "submit_tool_outputs"
},
"started_at": 1710921202,
"status": "requires_action",
"thread_id": "thread_oBBeiP70wwx7yuMpXLf8LCxk",
"tools": [
{
"function": {
"name": "get_records_by_lead_name",
"parameters": {
"type": "object",
"properties": {
"lead_name": {
"type": "string",
"description": "The lead's name"
}
},
"required": [
"lead_name"
]
},
"description": "Retrieve infomation of the specfic lead like when's the next meeting with them, their email and etc"
},
"type": "function"
}
],
"usage": null
}
Function Calling
Run Required Action State
{'tool_calls': [{'id': 'call_C4f29ji8cmyjSSuJeCXOsWS9', 'function': {'arguments': '{"lead_name":"David"}', 'name': 'get_records_by_lead_name'}, 'type': 'function'}]}
Tool Outputs
[{'tool_call_id': 'call_C4f29ji8cmyjSSuJeCXOsWS9', 'output': '[{"id": "recfXjj4DOuXOU5XC", "createdTime": "2023-11-25T01:17:34.000Z", "fields": {"Company Size": "50-100", "Lead\'s Email": "david@gmail.com ", "Where was leads found": "Email Marketing Campaign ", "Next Meeting": "November 27, 2023 8:27pm est", "Meeting Notes": "Alozie(Salesmen) needs to prepare a basic demo of one chatbots we previously built\\n", "Leads Name": "David"}}]'}]
Submitting outputs back to the Assistant...
{
"id": "run_6jkBdWUF6guERs9PfDTb96ZL",
"assistant_id": "asst_8WcUU6OI4kMEJZJSgUHuIlII",
"cancelled_at": null,
"completed_at": 1710921213,
"created_at": 1710921202,
"expires_at": null,
"failed_at": null,
"file_ids": [],
"instructions": "You are a sale chatbot. Use the data you pull from our airtable database to answer question on our potential clients.",
"last_error": null,
"metadata": {},
"model": "gpt-4-1106-preview",
"object": "thread.run",
"required_action": null,
"started_at": 1710921210,
"status": "completed",
"thread_id": "thread_oBBeiP70wwx7yuMpXLf8LCxk",
"tools": [
{
"function": {
"name": "get_records_by_lead_name",
"parameters": {
"type": "object",
"properties": {
"lead_name": {
"type": "string",
"description": "The lead's name"
}
},
"required": [
"lead_name"
]
},
"description": "Retrieve infomation of the specfic lead like when's the next meeting with them, their email and etc"
},
"type": "function"
}
],
"usage": {
"prompt_tokens": 793,
"completion_tokens": 85,
"total_tokens": 878
}
}
Assistant: For our next meeting with David, we need to prepare a basic demo of one of the chatbots we previously built. The meeting is scheduled for November 27, 2023, at 8:27 PM EST. Please ensure that the demo is ready before the meeting time to provide a comprehensive overview to David.
User: What do we need to prepare for in our next meeting with David

Additional Resources:

--

--

Alozie Igbokwe

I write articles on AI Agents. Here is my YT Channel if you want to see walkthroughs on AI Agents you can build for your business -https://shorturl.at/5s1tN