OpenAI API Functions & Embeddings Course (1/7): Simple Function Request

πŸ’‘ Full Course with Videos and Course Certificate (PDF): https://academy.finxter.com/university/openai-api-function-calls-and-embeddings/

Course Overview

πŸ‘‹ Hi, and welcome. I’m Dirk van Meerveld, and I’ll be your host and guide for this tutorial series, where we’ll be focussing on OpenAi’s ChatGPT function calls and the embeddings API.

Function calls will allow us to make ChatGPT even smarter by giving it access to additional information or our own custom functionality through calling functions and feeding the return back again to ChatGPT.

Embeddings will allow us to compare certain pieces of text by meaning instead of by exact character and word matches, which is very powerful.

Both of these tools are game changers and have mind-blowing potential in the ever-expanding field of AI. So let’s jump right in! Part 1 will be a bit longer as we do some setup work that we’ll use throughout the coming tutorials.

A Simple ChatGPT Call

I assume if you’re watching this you are at least somewhat familiar with ChatGPT calls using Python. We’ll quickly cover the basics so we’re all on the same page setup-wise, but won’t go too far into the details and basic settings. If you’re using ChatGPT for the first time, I recommend you start with my ‘Giggle Search‘ tutorial series instead, also available on the Finxter Academy.

For the purposes of this tutorial, I will be storing my API keys in a .env file and reading them using the config function in the decouple package. You can follow along with my method or use environment variables on your local machine or any other method you prefer. Just make sure to never ever hardcode API keys in your source code!!

Run the below in your terminal to install the decouple package if you don’t have it installed already:

pip install python-decouple

Now create a .env, which is simply a file by the name of '.env' with no name but only the extension of .env, in the base directory and add the following line to it:

CHATGPT_API_KEY=superdupersecretapikeygoeshere

Make sure to insert your own API key from Openai.com, or sign up for an account if you do not have one yet. Also, do not use any spaces as it will not work for .env files.

Now create a new Python file in the base directory called Aa_get_joke.py. You can give it a different name, but I’ll be using the letters A to G as there are 7 parts to this tutorial series and this will make it easy for learning and later reference purposes. Of course, you would not want to structure a real software project in this way, but for progressive lessons, it will make things nice and alphabetically ordered for us.

A quick overview of the basics before we dive in

Now add the following code to your Aa_get_joke.py file:

from decouple import config
import openai

openai.api_key = config("CHATGPT_API_KEY")
JOKE_SETUP = "I will give you a subject each time for all the following prompts. You will return to me a joke, but it should not be too long (4 lines at most). Please do not provide an introduction like 'Here's a joke for you' but get straight into the joke."

First, we import config from the decouple package, which will allow us to read our API key from the .env file.

Then we import the openai package, which we will use to make our ChatGPT calls. We then set our API key using the config function from the decouple package, which will read our API key from the .env file.

Finally, we set a constant variable called JOKE_SETUP, which contains a setup with instructions for ChatGPT to follow. We will be asking ChatGPT to generate a joke for us, an idea taken from the ‘Giggle search’ tutorial series also available on the Finxter academy.

Now continue with the following code:

def get_joke(query):
    result = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        temperature=0.4,
        max_tokens=200,
        messages=[
            {"role": "system", "content": JOKE_SETUP},
            {"role": "user", "content": query},
        ],
    )
    return result["choices"][0]["message"]["content"]

We define a function called get_joke, which is just a simple ChatGPT API call.

We take a query as an argument, which is the subject of the joke we want ChatGPT to generate for us.

We then call the ChatCompletion.create function from the openai package, and save the result in a variable named result.

We pass in the model we want to use and set the temperature to 0.4, which is a measure of how creative we want ChatGPT to be. The higher the temperature, the more creative ChatGPT will be, but the more nonsensical the response will potentially get.

πŸ§‘β€πŸ’» Recommended: ChatGPT API Temperature

max_tokens is self-explanatory. Note that you can leave out the temperature parameter for a default value.

messages is a list of dictionaries, which will contain the messages we want to send to ChatGPT. Each dictionary has a role and a content key, and these messages will function as a sort of history of the conversation so far for ChatGPT to use.

We set the first message to be a system message, which is the setup we defined earlier, and the second message to be the query that was passed into the function.

ChatGPT will send an object in response to us which looks something like the below:

{
    "choices": [
        {
        "finish_reason": "stop",
        "index": 0,
        "message": {
            "content": "Why don't penguins like talking to strangers at parties?\nBecause they find it hard to break the ice!",
            "role": "assistant"
        }
        }
    ],
    "created": 1690110711,
    "id": "chatcmpl-7fRI7RKqBl9y1oGv86ShTenOd9km5",
    "model": "gpt-3.5-turbo-0613",
    "object": "chat.completion",
    "usage": {
        "completion_tokens": 22,
        "prompt_tokens": 70,
        "total_tokens": 92
    }
}

As the OpenAI module has already parsed the JSON into an object for us we don’t have to worry about this and we simply index into the object like so ‘return result["choices"][0]["message"]["content"]‘ to get the response message from ChatGPT.

Now try running your function by adding a print statement to the bottom of your file like so:

print(get_joke("Penguins"))

And you get a response in the terminal that looks something like this:

Why don't penguins like talking to strangers at parties?
Because they find it hard to break the ice!

Ok, now that we have reviewed the bare basics let’s get into the fun stuff!

Creating a Function for ChatGPT to Call

Before we go ham having ChatGPT calling functions for us we should first create a function for ChatGPT to call.

Let’s start with a simple function that will allow ChatGPT access to an API that will extend its functionality so we can make our joke generator more powerful. In your base directory, create a folder named ‘apis‘ and then create a random_word.py file inside of it like so:

> apis
    > random_word.py

Now add the following code to your random_word.py file:

import requests

def get_random_word() -> str:
    response = requests.get("https://random-word-api.vercel.app/api?words=1")
    return response.json()[0]

We first import the request library so we can make an API call. Then we define a function called get_random_word, which will return a random word to us. We make a get request to the random word API, stating we only want 1 word back, and then return index 0 of the JSON response, which will be the random word.

In case you’re not familiar with the ‘-> str‘ syntax, it’s just a type hint we can add that states that this function is supposed to return a string. It’s not necessary and does not affect the functionality of the code, but without going too deep into typing here which we won’t, sometimes it’s just nice to state that this function we just created will return a string type, for clarity.

Now let’s test our function by adding the following code to the bottom of your random_word.py file:

print(get_random_word())

And you should get a random word printed to the terminal, like so:

demanding

Make sure to comment out or remove the print(get_random_word()) line again before continuing, as we don’t want to print a random word every time we import this file.

Message History

Great! Now we have a function that will return a random word to us, we can close this file for now and use it in our ChatGPT function calling later. Before we get into function calling though I want to make one more helper function for us to use throughout this whole tutorial series.

As ChatGPT will be telling us to call functions and we call them and then feed the return back into ChatGPT which in turn returns a message for the end user again the message history is going to get a bit complicated.

We will end up with a message history list of dictionaries that will look roughly like this:

[
    {"role": "system", "content": "Setup here"},
    {"role": "user", "content": "Query here"},
    {"role": "assistant", //call a function //},
    {"role": "function", "content": //function response//},
    {"role": "assistant", "content": //ChatGPT response to enduser//},
]

As the purpose of this tutorial series is to get a good understanding of how ChatGPT deals with function calls, and the message history will generally be a huge garbled mess with long function responses in there, we will create a simple helper to help us pretty print the above style message history to the console. This will greatly enhance our learning experience over the coming tutorials as we can see exactly what we are doing every step along the way.

First, create a new folder called 'utils' in your base directory, and then create a new file called 'printer.py' inside of it like so:

> utils
    > printer.py

Now add the following code to your printer.py file:

class ColorPrinter:
    _color_mapping = {
        "system": "\033[33m",       # Yellow
        "user": "\033[32m",         # Green
        "function": "\033[34m",     # Blue
        "assistant": "\033[35m",    # Purple
        "header": "\033[36m",       # Cyan
        "undefined": "\033[37m",    # White
        "closing_tag": "\033[00m",
    }

First, we define a class called ColorPrinter, and then we define a dictionary called color_mapping, which will map the different roles to different colors. We will use this to print the different roles in different colors to the console.

Note the in front of the dictionary name. This is a convention in Python that states that this variable is private, and should not be accessed outside of the class. It’s not enforced by the language, but it’s a convention that is good to follow. The weird-looking codes are ANSI escape codes, which are used to color text in the terminal.

Now add the following method to your ColorPrinter class below the _color_mapping property:

def _color_text_line(message) -> str:
    color_closing_tag = ColorPrinter._color_mapping["closing_tag"]
    try:
        role = message["role"]
        color_open_tag = ColorPrinter._color_mapping[role]
    except KeyError:
        role = "undefined"
        color_open_tag = ColorPrinter._color_mapping[role]
    try:
        if message["content"]:
            message = message["content"]
        else:
            function_name = message["function_call"]["name"]
            function_args = message["function_call"]["arguments"]
            message = f"{function_name}({function_args})"
    except KeyError:
        message = "undefined"
    return f"{color_open_tag}{role} : {message}{color_closing_tag}"

We use an _ in front of the name again, indicating that this method is only for use inside the class itself.

We take a message as an argument and will return a string (the -> str part is optional, but can help with code readability). We first set the color_closing_tag variable to the closing tag from our _color_mapping dictionary.

Then we try to get the role from the message, extracting its accompanying color from the _color_mapping object, and if it’s not there we set it to undefined.

The second try gets the content from the message and saves it in a variable named 'message', but if there is no content we assume it’s a function call and try to extract the function name and arguments from the message instead. If there is no function call we set the message to undefined. Finally, we return a string with the role and the message, with the color tags around it.

Now add the following method to your ColorPrinter class below the _color_text_line method:

def color_print(messages) -> None:
    cyan_open_tag = ColorPrinter._color_mapping["header"]
    color_closing_tag = ColorPrinter._color_mapping["closing_tag"]
    print(f"\n{cyan_open_tag}###### Conversation History ######{color_closing_tag}")
    for message in messages:
        print(ColorPrinter._color_text_line(message))
    print(f"{cyan_open_tag}##################################{color_closing_tag}\n")

This is the public interface, so we did not prepend an _ character. The method takes messages as an argument and returns nothing (None), as it just prints to the console (again the -> None part is optional for clarity, we won’t get deeper into type hinting here).

We first set the cyan_open_tag and color_closing_tag variables using our _color_mapping object, and then print a header to the console. We then loop over the messages and print each message to the console using the _color_text_line method we defined earlier. Finally, we print a closing header to the console.

Your whole helper class inside the utils/printer.py file now looks like this:

class ColorPrinter:
    _color_mapping = {
        "system": "\033[33m",  # Yellow
        "user": "\033[32m",  # Green
        "function": "\033[34m",  # Blue
        "assistant": "\033[35m",  # Purple
        "header": "\033[36m",  # Cyan
        "undefined": "\033[37m",  # White
        "closing_tag": "\033[00m",
    }

    def _color_text_line(message) -> str:
        color_closing_tag = ColorPrinter._color_mapping["closing_tag"]
        try:
            role = message["role"]
            color_open_tag = ColorPrinter._color_mapping[role]
        except KeyError:
            role = "undefined"
            color_open_tag = ColorPrinter._color_mapping[role]
        try:
            if message["content"]:
                message = message["content"]
            else:
                function_name = message["function_call"]["name"]
                function_args = message["function_call"]["arguments"]
                message = f"{function_name}({function_args})"
        except KeyError:
            message = "undefined"
        return f"{color_open_tag}{role} : {message}{color_closing_tag}"

    def color_print(messages) -> None:
        cyan_open_tag = ColorPrinter._color_mapping["header"]
        color_closing_tag = ColorPrinter._color_mapping["closing_tag"]
        print(f"\n{cyan_open_tag}###### Conversation History ######{color_closing_tag}")
        for message in messages:
            print(ColorPrinter._color_text_line(message))
        print(f"{cyan_open_tag}##################################{color_closing_tag}\n")

That was quite a lot of work for a simple helper function, but it will help us see and understand exactly what is going on for all the remaining parts of this tutorial series.

Calling a Function from ChatGPT

Create a new file in your base directory named Ab_get_joke_w_function.py.

(Again, I will be prefixing the files with A/B/C, etc, to make them alphabetically line up for clarity as a tutorial/teaching tool. This is obviously not a best practice you should follow outside this tutorial series.)

Inside this file first add some imports:

import openai
from decouple import config

from apis.random_word import get_random_word
from utils.printer import ColorPrinter as Printer

First, we import openai and config (to read the .env file for our API key), and then we import both helpers we made earlier. Now let’s set our API key in the openai module and define our prompt setup in a separate variable.

openai.api_key = config("CHATGPT_API_KEY")

JOKE_SETUP = """
You will be given a subject by the user. You will return a joke, but it should not be too long (4 lines at most). You will not provide an introduction like 'Here's a joke for you' but get straight into the joke.
There is a function called 'get_random_word'. If the user does not provide a subject, you should call this function and use the result as the subject. If the user does provide a subject, you should not call this function. The only exception is if the user asks for a random joke, in which case you should call the function and use the result as the subject.
Example: {user: 'penguins'} = Do not call the function => provide a joke about penguins.
Example: {user: ''} = Call the function => provide a joke about the result of the function.
Example: {user: 'soul music'} = Do not call the function => provide a joke about soul music.
Example: {user: 'random'} = Call the function => provide a joke about the result of the function.
Example: {user: 'guitars'} = Do not call the function => provide a joke about guitars.
Example: {user: 'give me a random joke'} = Call the function => provide a joke about the result of the function.
"""

We defined the prompt setup up top because it is very long and better put in a separate variable for code readability. You will notice the description is quite lengthy and includes a lot of examples exactly of what we expect. When you’re having trouble getting ChatGPT to do exactly what you want it to do, examples are a powerful tool to use.

Now define a function named 'get_joke_result':

def get_joke_result(query):
    messages = [
        {"role": "system", "content": JOKE_SETUP},
        {"role": "user", "content": query},
    ]

The function takes a user query as an argument and then sets up a message history list of dictionaries, which we will use to feed into ChatGPT. We set the first message to be a system message and pass in the setup we defined earlier as content. The second message is the user query that was passed into the function.

Below the messages list, still inside your get_joke_result function, add a list named ‘functions‘:

functions = [
    {
        "name": "get_random_word",
        "description": "Get a subject for your joke.",
        "parameters": {
            "type": "object",
            "properties": {
                "number_of_words": {
                    "type": "integer",
                    "description": "The number of words to generate.",
                }
            },
        },
    }
]

This is a list of dictionaries, which contains the functions ChatGPT will be able to call.

πŸ§‘β€πŸ’» Note that this is not an object that has any real functionality, it’s more us describing in text what this function does, so ChatGPT has a rough idea of what this function is and what it can be used for. The function name doesn’t even have to be the real name of the function, as ChatGPT will not directly call the function itself, which you’ll see in a moment. For the description just literally say what the function does.

In the properties, we can describe the arguments the function needs to be called. ChatGPT will generate these arguments when requesting the function call from us.

Even though the get_random_word helper function we defined doesn’t need any arguments it’s easier to get ChatGPT to stop complaining if we request an argument, so we pretend it needs a number_of_words argument, which we will simply ignore later. We will get deeper into arguments in the coming parts.

Now add the following code below the functions list (still inside the get_joke_result function):

first_response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="auto",  # auto is default
)["choices"][0]["message"]
messages.append(first_response)

So we create a ChatCompletion call using the openai module and save the result in a variable named first_response.

We pass in the model we want to use, the messages we defined earlier, the functions we defined earlier, and set the function_call to auto, which means ChatGPT gets to decide whether or not a function call is needed.

The ["choices"][0]["message"] at the end is just to index into the response object and get the message from it, which is what we save in our message history using the append method.

Now below and still inside the get_joke_result function add:

if first_response.get("function_call"):
    function_response = get_random_word()

    messages.append(
        {
            "role": "function",
            "name": "get_random_word",
            "content": function_response,
        }
    )

    second_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
    )["choices"][0]["message"]
    messages.append(second_response)
    Printer.color_print(messages)
    return second_response["content"]

Printer.color_print(messages)
return first_response["content"]

We test if the first_response we got back has a property named ‘function_call‘, indicating that ChatGPT wanted to call a function. If it wanted to call a function, we simply set a variable named ‘function_response‘ to the result of calling our get_random_word helper.

We ignore the argument as stated before, and only have a single potential function that can be called for now, so we don’t have to worry about that. Note that we are the ones actually calling the function, not ChatGPT!

We then append a new message to our message history, which is the function response. We set the role to function, the name to the name of the function, and the content to the function response.

We then create a new ChatCompletion call, passing in the model and the messages, now containing the result of our function call as well, and save the result in a variable named second_response. We append the second_response to our message history and then print the message history to the console using our helper function.

Finally, we return the content of the second_response as the final result.

If the first_response did not have a function_call property, we simply bypass the if block and print the message history to the console and return the content of the first_response as the final result.

Your get_joke_result function now looks like this:

def get_joke_result(query):
    messages = [
        {"role": "system", "content": JOKE_SETUP},
        {"role": "user", "content": query},
    ]

    functions = [
        {
            "name": "get_random_word",
            "description": "Get a subject for your joke.",
            "parameters": {
                "type": "object",
                "properties": {
                    "number_of_words": {
                        "type": "integer",
                        "description": "The number of words to generate.",
                    }
                },
            },
        }
    ]

    first_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto is default
    )["choices"][0]["message"]
    messages.append(first_response)

    if first_response.get("function_call"):
        function_response = get_random_word()

        messages.append(
            {
                "role": "function",
                "name": "get_random_word",
                "content": function_response,
            }
        )

        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        )["choices"][0]["message"]
        messages.append(second_response)
        Printer.color_print(messages)
        return second_response["content"]

    Printer.color_print(messages)
    return first_response["content"]

So let’s try this out. First, add the following to the bottom of your file:

print(get_joke_result("penguins"))

And then run your file. You should get a response in the terminal that looks something like this:

###### Conversation History ######
system :
You will be given a subject by the user. You will return a joke, but it should not be too long (4 lines at most). You will not provide an introduction like 'Here's a joke for you' but get straight into the joke.
There is a function called 'get_random_word'. If the user does not provide a subject, you should call this function and use the result as the subject. If the user does provide a subject, you should not call this function. The only exception is if the user asks for a random
joke, in which case you should call the function and use the result as the subject.
Example: {user: 'penguins'} = Do not call the function => provide a joke about penguins.
Example: {user: ''} = Call the function => provide a joke about the result of the function.
Example: {user: 'soul music'} = Do not call the function => provide a joke about soul music.
Example: {user: 'random'} = Call the function => provide a joke about the result of the function.
Example: {user: 'guitars'} = Do not call the function => provide a joke about guitars.
Example: {user: 'give me a random joke'} = Call the function => provide a joke about the result of the function.

user : penguins
assistant : Why don't penguins like talking to strangers at parties? They find it hard to break the ice!
##################################

Why don't penguins like talking to strangers at parties? They find it hard to break the ice!

As we can see, no function was called, since the user provided a valid subject. So far so good.

Now replace the print statement with the following print statement:

print(get_joke_result("random"))

And run your file again. You should get a response in the terminal that looks something like this:

###### Conversation History ######
system :
You will be given a subject by the user. You will return a joke, but it should not be too long (4 lines at most). You will not provide an introduction like 'Here's a joke for you' but get straight into the joke.
There is a function called 'get_random_word'. If the user does not provide a subject, you should call this function and use the result as the subject. If the user does provide a subject, you should not call this function. The only exception is if the user asks for a random
joke, in which case you should call the function and use the result as the subject.
Example: {user: 'penguins'} = Do not call the function => provide a joke about penguins.
Example: {user: ''} = Call the function => provide a joke about the result of the function.
Example: {user: 'soul music'} = Do not call the function => provide a joke about soul music.
Example: {user: 'random'} = Call the function => provide a joke about the result of the function.
Example: {user: 'guitars'} = Do not call the function => provide a joke about guitars.
Example: {user: 'give me a random joke'} = Call the function => provide a joke about the result of the function.

user : random
assistant : get_random_word({})
function : unlikable
assistant : Why did the unlikable computer go to therapy? It had too many bad connections!
##################################

Why did the unlikable computer go to therapy? It had too many bad connections!

We can see from our pretty printed history (which will have colors in your terminal) that the assistant first requested us to call a function. The function returned a word and the assistant (ChatGPT) then used that word as the subject for the joke. Great!

Now replace the print statement with the following print statement to finish up our testing and see what happens if the user provides no query at all:

print(get_joke_result(""))

Go ahead and run your file again:

###### Conversation History ######
system :
You will be given a subject by the user. You will return a joke, but it should not be too long (4 lines at most). You will not provide an introduction like 'Here's a joke for you' but get straight into the joke.
There is a function called 'get_random_word'. If the user does not provide a subject, you should call this function and use the result as the subject. If the user does provide a subject, you should not call this function. The only exception is if the user asks for a random
joke, in which case you should call the function and use the result as the subject.
Example: {user: 'penguins'} = Do not call the function => provide a joke about penguins.
Example: {user: ''} = Call the function => provide a joke about the result of the function.
Example: {user: 'soul music'} = Do not call the function => provide a joke about soul music.
Example: {user: 'random'} = Call the function => provide a joke about the result of the function.
Example: {user: 'guitars'} = Do not call the function => provide a joke about guitars.
Example: {user: 'give me a random joke'} = Call the function => provide a joke about the result of the function.

user : undefined
assistant : Can you tell me a joke?
function : payee
assistant : Why did the payee bring a ladder to the bank?

Because they heard the interest rates were climbing!
##################################

Why did the payee bring a ladder to the bank?

Because they heard the interest rates were climbing!

Yep, still works fine! Note that the function call only triggered when we provided no subject or asked for something random. If we provide a subject ChatGPT will just work as normal as it doesn’t need the function.

Now that we know how to make a simple function call let’s take things to the next level in part 2. See you there!

πŸ’‘ Full Course with Videos and Course Certificate (PDF): https://academy.finxter.com/university/openai-api-function-calls-and-embeddings/