Skip to content

Function Calling

Tolga Kayhan edited this page May 18, 2024 · 2 revisions

Code samples may appear a bit lengthy for function calls, but that does not necessarily mean they are complicated. We have two different samples. The first one demonstrates basic usage in a couple of different ways. The second example utilizes an experimental utility library, which is fairly easy to use, but as mentioned, it is still experimental.

Example 1 - Chat Function Call:

public static async Task RunChatFunctionCallTest(IOpenAIService sdk)
{
    ConsoleExtensions.WriteLine("Chat Tool Functions Call Testing is starting:", ConsoleColor.Cyan);

    // example taken from:
    // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb

    var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather")
        .AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA"))
        .AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
        .Validate()
        .Build();

    var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast")
        .AddParameter("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"})
        .AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
        .AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast"))
        .Validate()
        .Build();
    var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'")
        .Build();

    var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message")
        .AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user")))
        .Build();
    try
    {
        ConsoleExtensions.WriteLine("Chat Function Call Test:", ConsoleColor.DarkCyan);

        var request = new ChatCompletionCreateRequest
        {
            Messages = new List<ChatMessage>
            {
                ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."),
                ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.")
            },
            Tools = new List<ToolDefinition> { ToolDefinition.DefineFunction(fn1), ToolDefinition.DefineFunction(fn2), ToolDefinition.DefineFunction(fn3), ToolDefinition.DefineFunction(fn4) },
            // optionally, to force a specific function:
            //ToolChoice = ToolChoice.FunctionChoice("get_current_weather"),
            // or auto tool choice:
            //ToolChoice = ToolChoice.Auto,
            MaxTokens = 50,
            Model = Models.Gpt_3_5_Turbo
        };

        var completionResult = await sdk.ChatCompletion.CreateCompletion(request);

        /*  expected output along the lines of:
         
            Message:
            Function call:  get_n_day_weather_forecast
              location: Chicago, USA
              format: celsius
              num_days: 5
        */


        if (completionResult.Successful)
        {
            var choice = completionResult.Choices.First();
            Console.WriteLine($"Message:        {choice.Message.Content}");

            var tools = choice.Message.ToolCalls;
            if (tools != null)
            {
                request.Messages.Add(choice.Message);

                Console.WriteLine($"Tools: {tools.Count}");
                foreach (var toolCall in tools)
                {
                    Console.WriteLine($"  {toolCall.Id}: {toolCall.FunctionCall}");

                    var fn = toolCall.FunctionCall;
                    if (fn != null)
                    {
                        Console.WriteLine($"  Function call:  {fn.Name}");
                        foreach (var entry in fn.ParseArguments())
                        {
                            Console.WriteLine($"    {entry.Key}: {entry.Value}");
                        }

                        if (fn.Name == "get_n_day_weather_forecast")
                        {
                            request.Messages.Add(ChatMessage.FromTool("10 Degrees", toolCall.Id!));
                        }
                    }
                }
            }
        }
        else
        {
            if (completionResult.Error == null)
            {
                throw new Exception("Unknown Error");
            }

            Console.WriteLine($"{completionResult.Error.Code}: {completionResult.Error.Message}");
        }

        var completionResultAfterTool = await sdk.ChatCompletion.CreateCompletion(request); 
        
        if (completionResultAfterTool.Successful)
        {
            Console.WriteLine(completionResultAfterTool.Choices.First().Message.Content);
        }
        else
        {
            if (completionResultAfterTool.Error == null)
            {
                throw new Exception("Unknown Error");
            }

            Console.WriteLine($"{completionResultAfterTool.Error.Code}: {completionResultAfterTool.Error.Message}");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

Example 2 - Chat Function Call As Stream

public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk)
{
    ConsoleExtensions.WriteLine("Chat Tool Functions Call Stream Testing is starting:", ConsoleColor.Cyan);

    // example taken from:
    // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb

    var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather")
        .AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA"))
        .AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
        .Validate()
        .Build();

    var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast")
        .AddParameter("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"})
        .AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
        .AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast"))
        .Validate()
        .Build();

    var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'")
        .Build();

    var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message")
        .AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user")))
        .Build();   
    var fn5 = new FunctionDefinitionBuilder("google_search", "Gets a result from Google Search")
        .AddParameter("search_term", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Search Term")))
        .Build();
    var fn6 = new FunctionDefinitionBuilder("getURL", "Downloads the content of given website")
        .AddParameter("URL", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Search Term")))
        .Build();

    try
    {
        ConsoleExtensions.WriteLine("Chat Function Call Test:", ConsoleColor.DarkCyan);

        var request = new ChatCompletionCreateRequest
        {
            Messages = new List<ChatMessage>
            {
                ChatMessage.FromSystem("You are a bot that performs internet searches and also downloads content from websites."),
                // to test weather forecast functions:
                ChatMessage.FromUser("I need you to first search Google for \"Cat\" and, at the same time, download the contents from https://www.wired.com."),
                //ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days and also current weather.")
                // or to test array functions, use this instead:
                // ChatMessage.FromUser("And also The combination is: One. Two. Three. Four. Five."),
            },
            Tools = new List<ToolDefinition> { ToolDefinition.DefineFunction(fn1), ToolDefinition.DefineFunction(fn2), ToolDefinition.DefineFunction(fn3), ToolDefinition.DefineFunction(fn4), ToolDefinition.DefineFunction(fn5), ToolDefinition.DefineFunction(fn6) },
            // optionally, to force a specific function:
            //ToolChoice = ToolChoice.FunctionChoice("get_current_weather"),
            // or auto tool choice:
            ToolChoice = ToolChoice.Auto,
            //MaxTokens = 50,
            Model = Models.Gpt_4_1106_preview
        };

        var completionResults = sdk.ChatCompletion.CreateCompletionAsStream(request);

        /*  when testing weather forecasts, expected output should be along the lines of:
         
            Message:
            Function call:  get_n_day_weather_forecast
              location: Chicago, USA
              format: celsius
              num_days: 5
        */

        /*  when testing array functions, expected output should be along the lines of:
         
            Message:
            Function call:  identify_number_sequence
              values: [1, 2, 3, 4, 5]
        */
        var functionArguments = new Dictionary<int, string>();
        await foreach (var completionResult in completionResults)
        {
            if (completionResult.Successful)
            {
                var choice = completionResult.Choices.First();
                Console.WriteLine($"Message:        {choice.Message.Content}");

                var tools = choice.Message.ToolCalls;
                if (tools != null)
                {
                    request.Messages.Add(choice.Message);

                    Console.WriteLine($"Tools: {tools.Count}");
                    for (int i = 0; i < tools.Count; i++)
                    {
                        var toolCall = tools[i];
                        Console.WriteLine($"  {toolCall.Id}: {toolCall.FunctionCall}");

                        var fn = toolCall.FunctionCall;
                        if (fn != null)
                        {
                            if (!string.IsNullOrEmpty(fn.Name))
                            {
                                Console.WriteLine($"  Function call:  {fn.Name}");
                            }
                            
                            if (!string.IsNullOrEmpty(fn.Arguments))
                            {
                                if (functionArguments.TryGetValue(i, out var currentArguments))
                                {
                                    currentArguments += fn.Arguments;
                                }
                                else
                                {
                                    currentArguments = fn.Arguments;
                                }
                                functionArguments[i] = currentArguments;
                                fn.Arguments = currentArguments;

                                try
                                {
                                    foreach (var entry in fn.ParseArguments())
                                    {
                                        Console.WriteLine($"    {entry.Key}: {entry.Value}");
                                    }
                                }
                                catch (Exception)
                                {
                                    // ignore
                                }
                            }

                            if (fn.Name == "google_search")
                            {
                                request.Messages.Add(ChatMessage.FromTool("Tom", toolCall.Id!));
                            }

                            if (fn.Name == "getURL")
                            {
                                request.Messages.Add(ChatMessage.FromTool("News", toolCall.Id!));
                            }
                        }
                    }
                }
            }
            else
            {
                if (completionResult.Error == null)
                {
                    throw new Exception("Unknown Error");
                }

                Console.WriteLine($"{completionResult.Error.Code}: {completionResult.Error.Message}");
            }
        }

        var completionResultsAfterTool = sdk.ChatCompletion.CreateCompletionAsStream(request);

        await foreach (var completion in completionResultsAfterTool)
        {
            if (completion.Successful)
            {
                Console.Write(completion.Choices.First().Message.Content);
            }
            else
            {
                if (completion.Error == null)
                {
                    throw new Exception("Unknown Error");
                }

                Console.WriteLine($"{completion.Error.Code}: {completion.Error.Message}");
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

Example 3:

For this example you also need to use Betalgo.OpenAI.Utilities library

public static async Task ExerciseFunctionCalling(IOpenAIService openAIService)
{
    var calculator = new Calculator();
    var req = new ChatCompletionCreateRequest
    {
        //Functions = FunctionCallingHelper.GetFunctionDefinitions(calculator),
        //Functions = FunctionCallingHelper.GetFunctionDefinitions(typeof(Calculator)),
        Tools = FunctionCallingHelper.GetToolDefinitions<Calculator>(),
        Messages = new List<ChatMessage>
        {
            ChatMessage.FromSystem("You are a helpful assistant."),
            ChatMessage.FromUser("What is 2 + 2 * 6?") // GPT4 is needed for this
            //ChatMessage.FromUser("What is 2 + 6?"),  // GPT3.5 is enough for this
        }
    };

    do
    {
        var reply = await openAIService.ChatCompletion.CreateCompletion(req, Models.Gpt_4_0613);

        if (!reply.Successful)
        {
            Console.WriteLine(reply.Error?.Message);
            break;
        }

        var response = reply.Choices.First().Message;

        if (response.ToolCalls != null)
        {
            Console.WriteLine($"Invoking {response.ToolCalls.First().FunctionCall.Name} with params: {response.ToolCalls.First().FunctionCall.Arguments}");
        }
        else
        {
            Console.WriteLine(response.Content);
        }

        req.Messages.Add(response);

        if (response.ToolCalls != null)
        {
            var functionCall = response.ToolCalls.First().FunctionCall;
            var result = FunctionCallingHelper.CallFunction<float>(functionCall!, calculator);
            response.Content = result.ToString(CultureInfo.CurrentCulture);
        }
    } while (req.Messages.Last().FunctionCall != null);
}


public class Calculator
{
    public enum AdvancedOperators
    {
        Multiply,
        Divide
    }

    [FunctionDescription("Adds two numbers.")]
    public float Add(float a, float b)
    {
        return a + b;
    }

    [FunctionDescription("Subtracts two numbers.")]
    public float Subtract(float a, float b)
    {
        return a - b;
    }

    [FunctionDescription("Performs advanced math operators on two numbers.")]
    public float AdvancedMath(float a, float b, AdvancedOperators advancedOperator)
    {
        return advancedOperator switch
        {
            AdvancedOperators.Multiply => a * b,
            AdvancedOperators.Divide => a / b,
            _ => throw new ArgumentOutOfRangeException(nameof(advancedOperator), advancedOperator, null)
        };
    }
}
Clone this wiki locally