(1/6) OpenAI API Mastery: Innovating with GPT-4 Turbo and DALL·E 3 – Parallel Function Calling

Welcome to part 1 of the course! My name is Dirk van Meerveld and I will be your host and guide for this series in which we’re going to be exploring all the new features of the OpenAI APIs and what we can do with them.

To get started we’re going to be looking at the OpenAI function calling updates, especially the new ability to call multiple functions in parallel. We’ll also discuss some of the important syntax changes to go along with this and other new functionality in the API.

Let’s create a new folder and file in our base directory to get started.

📁FINX_OPENAI_UPDATES (root project folder)
    📁1_Parallel_function_calling
        📄function_descriptions.py

Function Descriptions

Open up function_descriptions.py.

Here we’ll describe the functions. These objects are for ChatGPT to know what functions are available and what names they have.

It describes what the function does and what parameters it needs as input. Notice that this is all text.

The only purpose of these objects is for ChatGPT to know what functions are available, when it should use a particular function, and what arguments it needs to provide to call a specific function. As such, they are not the functions themselves, which we have separately, but merely a description of the functions.

Let’s get started:

describe_get_current_weather = {
    "type": "function",
    "function": {

Note the syntax has slightly changed from what function calling used to be. We now wrap the entire object inside a "function" key and also have a "type": "function" key-value pair on the outermost level.

        "name": "get_current_weather",
        "description": "This function provides the current weather in a specific location.",

The name we provide here is the name that ChatGPT will use when it wants to call this particular function.

        "parameters": {

Here you describe to ChatGPT when you want it to call this function, and what the purpose of this function is.

            "type": "object",

Here you describe what parameters this function needs to be able to run.

The overall parameters are an object, and as properties, it needs a location which is of type string. We also provide a description of what this parameter should contain, namely the name of a city.

Note the required key, which is an array of the required parameters (you can specify multiple parameters here).

            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location as a city name, e.g. Amsterdam.",
                },
            },
            "required": ["location"],
        },
    },
}

So the whole description is :

describe_get_current_weather = {
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "This function provides the current weather in a specific location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location as a city name, e.g. Amsterdam.",
                },
            },
            "required": ["location"],
        },
    },
}

Now we have the second one which is much the same:

describe_get_weather_forecast = {
    "type": "function",
    "function": {
        "name": "get_weather_forecast",
        "description": "This function provides the weather forecast in a specific location for a specified number of days.",

Here we have multiple parameters. Note that only one of them is required.

        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location as a city name, e.g. Amsterdam.",
                },
                "days": {
                    "type": "integer",
                    "description": "The number of days to forecast, between 1 and 14.",
                },
            },
            "required": ["location"],
        },
    },
}

The entire function_descriptions.py file now looks like this:

describe_get_current_weather = {
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "This function provides the current weather in a specific location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location as a city name, e.g. Amsterdam.",
                },
            },
            "required": ["location"],
        },
    },
}


describe_get_weather_forecast = {
    "type": "function",
    "function": {
        "name": "get_weather_forecast",
        "description": "This function provides the weather forecast in a specific location for a specified number of days.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location as a city name, e.g. Amsterdam.",
                },
                "days": {
                    "type": "integer",
                    "description": "The number of days to forecast, between 1 and 14.",
                },
            },
            "required": ["location"],
        },
    },
}

Prompt Setup

Ok go ahead and close that, and create another file in the '1_Parallel_function_calling' folder called 'prompt_setup.py'. This is where we’ll set up the prompt for ChatGPT to use.

📁FINX_OPENAI_UPDATES (root project folder)
    📁1_Parallel_function_calling
        📄function_descriptions.py
        📄prompt_setup.py

Inside put the following variable:

current_and_forecast_setup = "You are a regular ChatGPT chatbot, just like normal, however, you also have access to some functions that can be called if you need them. One will provide the current weather and one will provide the weather forecast. IF THE USER DOES NOT ASK A WEATHER RELATED QUESTION JUST ANSWER THEM AS NORMAL WITHOUT CALLING ANY FUNCTIONS."

This is just a basic prompt setup telling the model it has functions available but also emphasizing that we don’t want to use them if they are not needed to answer the question. You can always play around with the specific wording and details of this prompt to see what works best for you.

We put it in a separate file to keep large string variables outside the main code to keep it readable as in a larger project the setup would likely be longer and have several versions.

Weather API

Now save and close that file as well. It’s time to create the actual functions that we’re going to be giving to ChatGPT to call. Create a new file in the same folder called 'weather.py':

📁FINX_OPENAI_UPDATES (root project folder)
    📁1_Parallel_function_calling
        📄function_descriptions.py
        📄prompt_setup.py
        📄weather.py

First, sign up for a free account on weatherapi.com.

They will give you pro for 14 days for free but it will automatically switch back to free afterward and you don’t have to provide any payment or credit card information, so don’t worry about it, you can use this API for free without any hassle.

Now create a '.env' file in the base directory of your project:

📁FINX_OPENAI_UPDATES (root project folder)
    📁1_Parallel_function_calling
        📄function_descriptions.py
        📄prompt_setup.py
        📄weather.py
    📄.env

And inside this file put both your weatherapi API key and OpenAI API key using the following syntax, making sure not to use quotes or spaces:

CHATGPT_API_KEY=supersecretchatgptapikeygoeshere
WEATHER_API_KEY=yoursupersecretweatherapikeygoeshere

Close and save that file so we can load our secret API keys from this file later. Now open a terminal and run the following command:

pip install python-decouple

Writing the Functions

This library will allow us to load our API keys from the .env file we just created. Now open up weather.py (which is still empty) and put the following code inside:

from decouple import config
from json import dumps
import requests

Config will allow us to easily read the content of our .env file, allowing us to load our API keys without coding their values in our code. The json module is a part of Python’s standard library and provides methods for working with JSON data.

The dumps function is used to convert a Python object into a JSON string, which basically holds the same information but in a string format. This is useful as ChatGPT cannot take Python objects as input, but it can take strings.

Finally, the requests module is a Python library used for making HTTP requests, providing us with a simple API. We’ll use it to send requests to the weatherapi.com API.

Now we define a simple function below:

def get_current_weather(location) -> str:
    if not location:
        return (
            "Please provide a location and call the get_current_weather_function again."
        )
    API_params = {
        "key": config("WEATHER_API_KEY"),
        "q": location,
        "aqi": "no",
        "alerts": "no",
    }

We use the config function to load the API key from the .env file. (make sure the name matches exactly and the .env file does not have any spaces).

Q holds the location, and AQI (air quality index) and alerts are not needed.

Continue inside the function:

    response: requests.models.Response = requests.get(
        "http://api.weatherapi.com/v1/current.json", params=API_params
    )
    str_response: str = dumps(response.json())
    return str_response

We make a get request, passing in our URL and parameters, and get a response object, which contains the server’s response.

We then convert the response to a dictionary by calling the .json method and convert this dictionary to a string using the dumps function we imported above.

This is the whole function:

def get_current_weather(location) -> str:
    if not location:
        return (
            "Please provide a location and call the get_current_weather_function again."
        )
    API_params = {
        "key": config("WEATHER_API_KEY"),
        "q": location,
        "aqi": "no",
        "alerts": "no",
    }
    response: requests.models.Response = requests.get(
        "http://api.weatherapi.com/v1/current.json", params=API_params
    )
    str_response: str = dumps(response.json())
    return str_response

Testing

Give it a quick test run to make sure it’s working. Add the print statement below and run your file:

print(get_current_weather("Seoul"))

You should see something like this in your terminal:

{"location": {"name": "Seoul", "region": "", "country": "South Korea", "lat": 37.57, "lon": 127.0, "tz_id": "Asia/Seoul", "localtime_epoch": 1699705164, "localtime": "2023-11-11 21:19"}, "current": {"last_updated_epoch": 1699704900, "last_updated": "2023-11-11 21:15", "temp_c": 1.0, "temp_f": 33.8, "is_day": 0, "condition": {"text": "Clear", "icon": "//cdn.weatherapi.com/weather/64x64/night/113.png", "code": 1000}, "wind_mph": 6.9, "wind_kph": 11.2, "wind_degree": 330, "wind_dir": "NNW", "pressure_mb": 1029.0, "pressure_in": 30.39, "precip_mm": 0.0, "precip_in": 0.0, "humidity": 55, "cloud": 0, "feelslike_c": -3.1, "feelslike_f": 26.3, "vis_km": 10.0, "vis_miles": 6.0, "uv": 1.0, "gust_mph": 12.1, "gust_kph": 19.4}}

Make sure you comment out the print statement so it won’t run every time we import this file in the future.

Writing the Second Function

Now we’ll create a second function to get the weather forecast.

This one is a bit more complicated as we need to provide a number of days to forecast. We’ll also need to do some error handling to make sure the user provides a valid number of days.

def get_weather_forecast(location, days=7) -> str:
    try:
        days = 1 if days < 1 else 14 if days > 14 else days
    except TypeError:
        days = 7

We take a location and set a default of 7 days.

If the days variable is less than 1 we set it to 1, but if it’s more than 14 we set it to 14. If neither condition is true the user provided a valid value and we just use the input argument value.

Finally, if some weird type gets passed in we just default to 7 days.

    params = {
        "key": config("WEATHER_API_KEY"),
        "q": location,
        "days": days,
        "aqi": "no",
        "alerts": "no",
    }

    response: requests.models.Response = requests.get(
        "http://api.weatherapi.com/v1/forecast.json", params=params
    )

Parameters are largely the same except we have a number of days now. The only problem is that the API sends back a lot of data, even hourly data so 24 entries per day, which is way too much, so we need to do some filtering:

    response: dict = response.json()
    filtered_response = {}
    filtered_response["location"] = response["location"]
    filtered_response["current"] = response["current"]
    filtered_response["forecast"] = [
        [day["date"], day["day"]] for day in response["forecast"]["forecastday"]
    ]
    return dumps(filtered_response)

First convert the response to a dictionary. Keep the location and the current weather by copying them from the response to the empty dictionary named filtered_response we just created.

For the forecast, we only want the daily data, as the hourly data will completely overload the response. The line just extracts only the data we want and is based on the structure of the response from the API.

I don’t want to get too deeply into it here as this course is on OpenAI and not on list comprehensions but basically, we extract the date and day data from each day in the forecast and put it in a list.

Finally, we convert the filtered_response dictionary to a string and return it, without all the hourly data that the API sent to us.

The second function now looks like this:

def get_weather_forecast(location, days=7) -> str:
    try:
        days = 1 if days < 1 else 14 if days > 14 else days
    except TypeError:
        days = 7

    params = {
        "key": config("WEATHER_API_KEY"),
        "q": location,
        "days": days,
        "aqi": "no",
        "alerts": "no",
    }

    response: requests.models.Response = requests.get(
        "http://api.weatherapi.com/v1/forecast.json", params=params
    )

    response: dict = response.json()
    filtered_response = {}
    filtered_response["location"] = response["location"]
    filtered_response["current"] = response["current"]
    filtered_response["forecast"] = [
        [day["date"], day["day"]] for day in response["forecast"]["forecastday"]
    ]
    return dumps(filtered_response)

Give it a test run:

print(get_weather_forecast("Seoul", days=3))

And you should get a fairly large output in your terminal. Again, make sure you comment out the print statement so it won’t run every time we import this file in the future.

Parallel Function Calling

Ok go ahead and close your weather.py file. We’re done with it for now. Now we’ll create a new file called 'parallel_function_calling.py' in the same folder:

📁FINX_OPENAI_UPDATES (root project folder)
    📁1_Parallel_function_calling
        📄function_descriptions.py
        📄prompt_setup.py
        📄weather.py
        📄parallel_function_calling.py
    📄.env

Important! Before we get started make sure you run this in a terminal window:

pip install openai --upgrade

This gets the latest version of the openai library, to make sure your syntax is the same as mine, as there are quite some differences between the old and new versions, which we’ll be going over in the coming parts.

Open the parallel_function_calling.py file and let’s have some fun!

import json
from decouple import config
from openai import OpenAI
from typing import Callable

We import the built-in json module to work with JSON data, config to load our OpenAI API key from the .env file, and OpenAI to access the API.

Note that the syntax is different, where we would just import 'openai' itself in the past, in this new version of the openai library, we need to import OpenAI instead. The 'Callable' from typing is just used to clear up something in our code later on.

from weather import get_current_weather, get_weather_forecast
from prompt_setup import current_and_forecast_setup
from function_descriptions import (
    describe_get_current_weather,
    describe_get_weather_forecast,
)

Here we just import our own stuff we prepared ahead of time.

MODEL = "gpt-3.5-turbo-1106"
client = OpenAI(api_key=config("OPENAI_API_KEY"))

Define the model up top, and then we create a 'client' by calling the OpenAI class we imported and passing in the api_key by loading it from the .env file using config.

This is part of the new standard syntax, we will interact with this client object to make API calls to OpenAI’s various API endpoints from here on.

Utility Printer Function

Now create a quick utility to print the output in a more readable manner:

def quick_dirty_printer(messages):
    """
    Prints messages in alternating colors (irrespective of role) and the final message in green. (92 is green, 93 is yellow, 94 is blue)
    """
    for index, message in enumerate(messages):
        if index ==