Lesson 1: Simple Function Request

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

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. 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