Parallel Function Calling for Financial Statements

In this guide, we'll discuss how to use OpenAI's parallel function calling to answer investment research questions using financial statements.

a year ago   •   9 min read

By Peter Foy

One of the many notable releases at OpenAI's DevDay was parallel function calling. In case you're unfamiliar with function calling, it allows you to connect OpenAI models to third party APIs and functions.

As OpenAI writes:

In an API call, you can describe functions and have the model intelligently choose to output a JSON object containing arguments to call one or many functions.

Function calling has been available for several months, although it was limited to single function calls at a time. This meant that if a user asked for a query that required multiple API calls, the task would not be fully completed and users have to ask follow-up questions...i.e. not ideal.

This is summarized well in the image below from DevDay:

Source: OpenAI DevDay

Now, with the latest API update, we're able to build agents that can call multiple functions in parallel and answer more complex queries.

Parallel function calling is the model's ability to perform multiple function calls together, allowing the effects and results of these function calls to be resolved in parallel. 

In this guide, we'll walk through how to use parallel function calling for financial statements that uses three API calls from Financial Modeling Prep: income statements, balance sheets, and cash flow statements.

This will allow users to ask questions about multiple statement types, for example:

Analyze Amazon's profitability and liquidity for the past two annual periods based on their income statements, balance sheets, and cash flow statements

Step 0: Installs & Imports

To get started, let's create a new Colab notebook and pip install --ugprade opeani if you haven't done so already.

Next, we'll add our OPENAI_API_KEY as an environment variable and then import and instantiate the OpenAI class as follows:

from openai import OpenAI
import json
import requests

client = OpenAI()

We'll also use the Financial Modeling Prep API to retrieve financial statements data, so will set our FMP_API_KEY as an environment variable as well.

Step 1: Define Functions

The first step is to define functions the model has access to, for example in the documentation, they provide a dummy function for getting the current weather in 3 locations.

For this example, let's write three functions to retrieve information from the financial statement endpoints:

  • Income statements
  • Balance sheets
  • Cash flow statements
def get_income_statement(ticker, period, limit):
    url = f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}?period={period}&limit={limit}&apikey={FMP_API_KEY}"
    response = requests.get(url)
    return json.dumps(response.json())

def get_balance_sheet(ticker, period, limit):
    url = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{ticker}?period={period}&limit={limit}&apikey={FMP_API_KEY}"
    response = requests.get(url)
    return json.dumps(response.json())

def get_cash_flow_statement(ticker, period, limit):
    url = f"https://financialmodelingprep.com/api/v3/cash-flow-statement/{ticker}?period={period}&limit={limit}&apikey={FMP_API_KEY}"
    response = requests.get(url)
    return json.dumps(response.json())

Step 2: Send messages & functions to the model

Next, let's create another function called run_conversation() that will:

  • Send the conversation and available functions to the model
  • Check if the model wants to call a function
  • Call the function(s)
  • Send data for each function and and function response to the model
  • Extend the conversation with the function response
  • Get a new model response that can see the function previous

2.1 Define run_conversation function

The run_conversation function will initiate an interaction to analyze financial statements with our three definied functions:

  • It takes three parameters: ticker (the company to analyze), period (annual or quarterly), and limit (the number of past statements to analyze)

2.2 Create an initial user message

Next, we'll create an initial messages object with the user role and the following content:

Analyze the financial statements for {ticker} for the last {limit} {period} periods. You have access to Financial Modeling Prep API and will be fed relevant data based on the user's request.

2.3 Define available functions

Next, we will create a list of tools containing the functions the model can call:

  • Each function is defined with a name and expected parameters (i.e. ticker, period, limit)
def run_conversation(ticker, period, limit):
    # Step 1: send the conversation and available functions to the model
    messages = [{"role": "user", "content": f"Analyze the financial statements for {ticker} for the last {limit} {period} periods. You have access to Financial Modeling Prep API and will be fed relevant data based on the user's request.`"}]
    tools = [
        {
            "type": "function",
            "function": {
                "name": "get_income_statement",
                "parameters": {"type": "object", "properties": {"ticker": {"type": "string"}, "period": {"type": "string"}, "limit": {"type": "integer"}}},
            },
        },
        {
            "type": "function",
            "function": {
                "name": "get_balance_sheet",
                "parameters": {"type": "object", "properties": {"ticker": {"type": "string"}, "period": {"type": "string"}, "limit": {"type": "integer"}}},
            },
        },
        {
            "type": "function",
            "function": {
                "name": "get_cash_flow_statement",
                "parameters": {"type": "object", "properties": {"ticker": {"type": "string"}, "period": {"type": "string"}, "limit": {"type": "integer"}}},
            },
        }
    ]

Step 2: Make an initial API call

Now that we've defined which functions the model has access to, we can make our initial API call which will intelligently decide which functions to call based on our initial user message:

  • Here we use the client.chat.completions.create() method
  • We define the model as the new GPT-4 Turbo gpt-4-1106-preview model, although we can also use the gpt-3.5-turbo-1106 model
  • We pass in our messages, the tools list, and set tool_choice=auto (if we want to force the model to call a speciifc function we can just add the function name here)

    # Previous code...
    
    # Initial API call
    response = client.chat.completions.create(
        model="gpt-4-1106-preview",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    

Step 3: Extract & process tool calls from the response

With this initial API call that decides which functions to use, we can extract the tools_calls as follows:

3.1 Extract tool calls:

  • First we'll extract tool_calls with response.choices[0].message.tool_calls which the model has decided to execute based on the initial user message

3.2 Map available function calls:

  • available_functions = {...}: Here, a dictionary of available_functions is created, mapping function names (like 'get_income_statement') to the actual Python functions defined earlier. This dictionary will be used to match the model's requested functions to the corresponding function.

3.3 Process tool calls and append responses

  • if tool_calls:: This checks if there are any tool calls to process. If the list is not empty, the following steps are executed.
  • Looping through tool calls:
    • for tool_call in tool_calls:: Iterates over each tool call in tool_calls.
    • function_name = tool_call.function.name: Gets the name of the function to be called from the tool call.
    • function_to_call = available_functions[function_name]: Retrieves the corresponding function using the function_name.
    • function_args = json.loads(tool_call.function.arguments): Parses the arguments for the function call from JSON format to a Python dictionary.
    • function_response = function_to_call(**function_args): Calls the Python function with the parsed arguments and stores the response.

3.4 Appending Responses to Messages

  • We then creates a new message for each function response, ensuring to include:
    • tool_call_id: The ID of the tool call being responded to. This links the response back to the specific request made by the model.
    • role: Set to "assistant", indicating that this message is a response from the assistant (or the system) rather than the user.
    • name: The name of the function that was called.
    • content: The actual response data from the function call.
  • messages.append(...): This newly created message is appended to the messages list, which will be sent back to the OpenAI model in a subsequent API call.
    # Extract tool calls
    tool_calls = response.choices[0].message.tool_calls

    # Process each tool call and append responses
    available_functions = {
        "get_income_statement": get_income_statement,
        "get_balance_sheet": get_balance_sheet,
        "get_cash_flow_statement": get_cash_flow_statement
    }

    if tool_calls:
        for tool_call in tool_calls:
            function_name = tool_call.function.name
            function_to_call = available_functions[function_name]
            function_args = json.loads(tool_call.function.arguments)
            function_response = function_to_call(**function_args)

            messages.append(
                {
                    "tool_call_id": tool_call.id,
                    "role": "assistant",
                    "name": function_name,
                    "content": function_response,
                }
            )

Step 4: Make a second API call with tool responses

The last step is to make a second API with the user messages and the tool responses:

4.1 Second API Call:

second_response = client.chat.completions.create(...): This second API call is sends back the responses generated from the tool calls (i.e. financial statement data) to the model.

    • Here we use GPT-4 Turbo again and pass the updated messages list to the model. This list now includes the original user query and the responses from the tool calls.

4.2 Return the Second Response

Next we extract and return the content of the message from the second response, which should contain the analysis or answer based on the financial data provided through the tool responses.

    • We also have a fallback in the case that no function calls were made and no external data was fetched.
        # Second API call with tool responses
        second_response = client.chat.completions.create(
            model="gpt-4-1106-preview",
            messages=messages
        )

        return second_response.choices[0].message.content

    return "No function calls were made."

Analyzing Financial Statements with Function Calling

Now, we can test this out by running the function and passing in our desired stock, period, and limit:

# Example usage
print(run_conversation("NVDA", "quarter", 2))

...which gives the following response (click to expand):

GPT-4 Turbo Response

Based on the provided financial data for NVIDIA Corporation (NVDA) for the calendar year 2024 over the first and second quarters, here is an analysis:

### Q2 2024 vs. Q1 2024

**Revenue**
- Q2 Revenue: $13.507 billion
- Q1 Revenue: $7.192 billion
- **Observation**: NVDA has seen a significant increase in revenue between the first and second quarters, more than 87%.

**Net Income**
- Q2 Net Income: $6.188 billion
- Q2 Net Income Ratio: 45.81%
- Q1 Net Income: $2.043 billion
- Q1 Net Income Ratio: 28.41%
- **Observation**: Net income and net income ratio have both seen substantial improvements in Q2, suggesting improved profitability.

**Earnings Per Share (EPS)**
- Q2 EPS (diluted): $2.48
- Q1 EPS (diluted): $0.82
- **Observation**: There has been a significant increase in the EPS, which implies higher returns for the shareholders in the second quarter.

**Total Assets**
- Q2 Total Assets: $49.555 billion
- Q1 Total Assets: $44.460 billion
- **Observation**: NVDA's assets have grown between the first and second quarters, indicating an expansion in the company's resources and operational capacity.

**Total Liabilities**
- Q2 Total Liabilities: $22.054 billion
- Q1 Total Liabilities: $19.94 billion
- **Observation**: Liabilities have increased slightly, which could be related to financing growth or operations.

**Cash Flows from Operations**
- Q2 Operating Cash Flow: $6.348 billion
- Q1 Operating Cash Flow: $2.911 billion
- **Observation**: There is a strong increase in cash flows from operating activities from Q1 to Q2.

**Capital Expenditure**
- Q2 Capital Expenditure: -$289 million
- Q1 Capital Expenditure: -$248 million
- **Observation**: NVDA has invested more in capital expenditures in Q2, which is consistent with investing in future growth.

**Free Cash Flow**
- Q2 Free Cash Flow: $6.059 billion
- Q1 Free Cash Flow: $2.663 billion
- **Observation**: Free cash flow has more than doubled from Q1 to Q2, providing NVDA with more liquidity for investments, dividends, or buybacks.

**Debt Level**
- Q2 Total Debt: $10.746 billion
- Q1 Total Debt: $11.893 billion
- **Observation**: There has been a slight decrease in total debt in Q2, which could indicate debt repayments or refinancing activities.

### Summary

NVIDIA has shown notable improvement in financial performance from the first quarter to the second quarter of the fiscal year 2024, with significant increases in revenue, net income, and cash flows from operations. These figures display a positive trajectory in the company's financial health and operating efficiency. The increase in total assets and a slight decrease in total debt also depict a robust balance sheet. Investors may view these improvements positively, as reflected by a significant rise in earnings per share.

Please note that this analysis is based on the information provided and does not consider external factors, such as market conditions, industry trends, or company updates post the knowledge cutoff date. For a more comprehensive analysis and investment decisions, further research and context may be needed.

Not bad.

We can also print out the second_response to see the raw data that was fed to GPT to verify the accuracy of the input data & response.


Summary: Parallel Function Calling

Parallel function calling is another important development from OpenAI on the path towards building fully autonomous AI agents.

By calling multiple functions in parallel, we're able to build LLM-enabled apps that significantly more complex questions, whether it's related to financial analysis or any other data source.

Lastly, it's important to note that we can use function calling with the new Assistants API. This allows you to combine functions with OpenAI's built-in tools like knowledge retrieval and code interpreter, which we'll cover in a future tutorial.

Spread the word

Keep reading