From 3a091a0ea1c46ff574834078e7c8aca054aaecb7 Mon Sep 17 00:00:00 2001 From: Almis90 Date: Mon, 3 Jul 2023 16:23:59 +0300 Subject: [PATCH 1/7] Add fallback method for HttpClient.Send on unsupported platforms --- OpenAI.SDK/Extensions/HttpclientExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/OpenAI.SDK/Extensions/HttpclientExtensions.cs b/OpenAI.SDK/Extensions/HttpclientExtensions.cs index 4b184962..6f105d1a 100644 --- a/OpenAI.SDK/Extensions/HttpclientExtensions.cs +++ b/OpenAI.SDK/Extensions/HttpclientExtensions.cs @@ -30,12 +30,24 @@ public static HttpResponseMessage PostAsStreamAsync(this HttpClient client, stri request.Content = content; #if NET6_0_OR_GREATER - return client.Send(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + try + { + return client.Send(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + } + catch (PlatformNotSupportedException) + { + return SendRequestPreNet6(client, request, cancellationToken); + } #else + return SendRequestPreNet6(client, request, cancellationToken); +#endif + } + + public static HttpResponseMessage SendRequestPreNet6(HttpClient client, HttpRequestMessage request, CancellationToken cancellationToken) + { var responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); var response = responseTask.GetAwaiter().GetResult(); return response; -#endif } public static async Task PostFileAndReadAsAsync(this HttpClient client, string uri, HttpContent content, CancellationToken cancellationToken = default) From 493865f8e343cfb112f2f63bb555c0059eadac64 Mon Sep 17 00:00:00 2001 From: Almis90 Date: Wed, 5 Jul 2023 19:23:23 +0300 Subject: [PATCH 2/7] Refactored http request creation and disposal Addressed an issue related to the using statement that was trying to send a disposed HttpRequestMessage in the catch block --- OpenAI.SDK/Extensions/HttpclientExtensions.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/OpenAI.SDK/Extensions/HttpclientExtensions.cs b/OpenAI.SDK/Extensions/HttpclientExtensions.cs index 6f105d1a..054ed940 100644 --- a/OpenAI.SDK/Extensions/HttpclientExtensions.cs +++ b/OpenAI.SDK/Extensions/HttpclientExtensions.cs @@ -25,9 +25,7 @@ public static HttpResponseMessage PostAsStreamAsync(this HttpClient client, stri var content = JsonContent.Create(requestModel, null, settings); - using var request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); - request.Content = content; + using var request = CreatePostEventStreamRequest(uri, content); #if NET6_0_OR_GREATER try @@ -36,20 +34,31 @@ public static HttpResponseMessage PostAsStreamAsync(this HttpClient client, stri } catch (PlatformNotSupportedException) { - return SendRequestPreNet6(client, request, cancellationToken); + using var newRequest = CreatePostEventStreamRequest(uri, content); + + return SendRequestPreNet6(client, newRequest, cancellationToken); } #else return SendRequestPreNet6(client, request, cancellationToken); #endif } - public static HttpResponseMessage SendRequestPreNet6(HttpClient client, HttpRequestMessage request, CancellationToken cancellationToken) + private static HttpResponseMessage SendRequestPreNet6(HttpClient client, HttpRequestMessage request, CancellationToken cancellationToken) { var responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); var response = responseTask.GetAwaiter().GetResult(); return response; } + private static HttpRequestMessage CreatePostEventStreamRequest(string uri, HttpContent content) + { + var request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + request.Content = content; + + return request; + } + public static async Task PostFileAndReadAsAsync(this HttpClient client, string uri, HttpContent content, CancellationToken cancellationToken = default) { var response = await client.PostAsync(uri, content, cancellationToken); From ef1a7f4d21dc759d0040fc6470ecd8d5c45430ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Velv=C3=A1rt=20Andr=C3=A1s?= Date: Tue, 1 Aug 2023 21:25:48 +0200 Subject: [PATCH 3/7] Added FunctionCallingHelper, related tests and playground demo. Modified OpenAI.Utilities to use the OpenAI library from the solution instead of Nuget. --- .../FunctionCallingHelperTests.cs | 160 +++++++++++++ .../OpenAI.Utilities.Tests.csproj | 30 +++ OpenAI.Utilities.Tests/Usings.cs | 2 + OpenAI.Utilities/FunctionCallingHelper.cs | 212 ++++++++++++++++++ OpenAI.Utilities/OpenAI.Utilities.csproj | 5 +- OpenAI.UtilitiesPlayground/Program.cs | 127 +++++++++-- OpenAI.sln | 9 +- 7 files changed, 521 insertions(+), 24 deletions(-) create mode 100644 OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs create mode 100644 OpenAI.Utilities.Tests/OpenAI.Utilities.Tests.csproj create mode 100644 OpenAI.Utilities.Tests/Usings.cs create mode 100644 OpenAI.Utilities/FunctionCallingHelper.cs diff --git a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs new file mode 100644 index 00000000..dce9b417 --- /dev/null +++ b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs @@ -0,0 +1,160 @@ +using OpenAI.ObjectModels.RequestModels; + +namespace OpenAI.Utilities.Tests; + +public class FunctionCallingHelperTests +{ + [Fact] + public void VerifyGetFunctionDefinition() + { + var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("TestFunction")!); + + functionDefinition.Name.ShouldBe("TestFunction"); + functionDefinition.Description.ShouldBe("Test Function"); + functionDefinition.Parameters.ShouldNotBeNull(); + functionDefinition.Parameters.Properties!.Count.ShouldBe(9); + + var intParameter = functionDefinition.Parameters.Properties["intParameter"]; + intParameter.Description.ShouldBe("Int Parameter"); + intParameter.Type.ShouldBe("integer"); + + var floatParameter = functionDefinition.Parameters.Properties["floatParameter"]; + floatParameter.Description.ShouldBe("Float Parameter"); + floatParameter.Type.ShouldBe("number"); + + var boolParameter = functionDefinition.Parameters.Properties["boolParameter"]; + boolParameter.Description.ShouldBe("Bool Parameter"); + boolParameter.Type.ShouldBe("boolean"); + + var stringParameter = functionDefinition.Parameters.Properties["stringParameter"]; + stringParameter.Description.ShouldBe("String Parameter"); + stringParameter.Type.ShouldBe("string"); + + var enumValues = new List {"Value1", "Value2", "Value3"}; + + var enumParameter = functionDefinition.Parameters.Properties["enumParameter"]; + enumParameter.Description.ShouldBe("Enum Parameter"); + enumParameter.Type.ShouldBe("string"); + enumParameter.Enum.ShouldBe(enumValues); + + + var enumParameter2 = functionDefinition.Parameters.Properties["enumParameter2"]; + enumParameter2.Description.ShouldBe("Enum Parameter 2"); + enumParameter2.Type.ShouldBe("string"); + enumParameter2.Enum.ShouldBe(enumValues); + + functionDefinition.Parameters.Properties.ShouldNotContainKey("overriddenNameParameter"); + functionDefinition.Parameters.Properties.ShouldContainKey("OverriddenName"); + } + + [Fact] + public void VerifyGetFunctionDefinitions() + { + var obj = new FunctionCallingTestClass(); + var functionDefinitions = FunctionCallingHelper.GetFunctionDefinitions(obj); + + functionDefinitions.Count.ShouldBe(2); + + var functionDefinition = functionDefinitions.First(x => x.Name == "TestFunction"); + functionDefinition.Description.ShouldBe("Test Function"); + functionDefinition.Parameters.ShouldNotBeNull(); + functionDefinition.Parameters.Properties!.Count.ShouldBe(9); + + var functionDefinition2 = functionDefinitions.First(x => x.Name == "SecondFunction"); + functionDefinition2.Description.ShouldBe("Second Function"); + functionDefinition2.Parameters.ShouldNotBeNull(); + functionDefinition2.Parameters.Properties!.Count.ShouldBe(0); + } + + [Fact] + public void VerifyCallFunction_Simple() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "SecondFunction", + }; + + var result = FunctionCallingHelper.CallFunction(functionCall, obj); + result.ShouldBe("Hello"); + } + + [Fact] + public void VerifyCallFunction_Complex() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "TestFunction", + // arguments is a json dictionary + Arguments = "{\"intParameter\": 1, \"floatParameter\": 2.0, \"boolParameter\": true, \"stringParameter\": \"Hello\", \"enumParameter\": \"Value1\", \"enumParameter2\": \"Value2\", \"requiredIntParameter\": 1, \"notRequiredIntParameter\": 2, \"OverriddenName\": 3}" + + }; + + var result = FunctionCallingHelper.CallFunction(functionCall, obj); + result.ShouldBe(5); + + obj.IntParameter.ShouldBe(1); + obj.FloatParameter.ShouldBe(2.0f); + obj.BoolParameter.ShouldBe(true); + obj.StringParameter.ShouldBe("Hello"); + obj.EnumParameter.ShouldBe(TestEnum.Value1); + obj.EnumParameter2.ShouldBe(TestEnum.Value2); + obj.RequiredIntParameter.ShouldBe(1); + obj.NotRequiredIntParameter.ShouldBe(2); + obj.OverriddenNameParameter.ShouldBe(3); + } +} + +internal class FunctionCallingTestClass +{ + public int IntParameter; + public float FloatParameter; + public bool BoolParameter; + public string StringParameter; + public TestEnum EnumParameter; + public TestEnum EnumParameter2; + public int RequiredIntParameter; + public int? NotRequiredIntParameter; + public int OverriddenNameParameter; + + [FunctionDescription("Test Function")] + public int TestFunction( + [ParameterDescription("Int Parameter")] int intParameter, + [ParameterDescription("Float Parameter")] float floatParameter, + [ParameterDescription("Bool Parameter")] bool boolParameter, + [ParameterDescription("String Parameter")] string stringParameter, + [ParameterDescription(Description = "Enum Parameter", Enum = "Value1, Value2, Value3")] TestEnum enumParameter, + [ParameterDescription("Enum Parameter 2")] TestEnum enumParameter2, + [ParameterDescription(Description = "Required Int Parameter", Required= true)] int requiredIntParameter, + [ParameterDescription(Description = "Not required Int Parameter", Required = false)] int notRequiredIntParameter, + [ParameterDescription(Name = "OverriddenName", Description = "Overridden")] int overriddenNameParameter) + { + IntParameter = intParameter; + FloatParameter = floatParameter; + BoolParameter = boolParameter; + StringParameter = stringParameter; + EnumParameter = enumParameter; + EnumParameter2 = enumParameter2; + RequiredIntParameter = requiredIntParameter; + NotRequiredIntParameter = notRequiredIntParameter; + OverriddenNameParameter = overriddenNameParameter; + + return 5; + } + + [FunctionDescription("Second Function")] + public string SecondFunction() + { + return "Hello"; + } +} + +public enum TestEnum +{ + Value1, + Value2, + Value3 +} \ No newline at end of file diff --git a/OpenAI.Utilities.Tests/OpenAI.Utilities.Tests.csproj b/OpenAI.Utilities.Tests/OpenAI.Utilities.Tests.csproj new file mode 100644 index 00000000..3247a108 --- /dev/null +++ b/OpenAI.Utilities.Tests/OpenAI.Utilities.Tests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/OpenAI.Utilities.Tests/Usings.cs b/OpenAI.Utilities.Tests/Usings.cs new file mode 100644 index 00000000..bd8299f6 --- /dev/null +++ b/OpenAI.Utilities.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using Shouldly; \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCallingHelper.cs b/OpenAI.Utilities/FunctionCallingHelper.cs new file mode 100644 index 00000000..8a75a6c0 --- /dev/null +++ b/OpenAI.Utilities/FunctionCallingHelper.cs @@ -0,0 +1,212 @@ +using System.Reflection; +using System.Text.Json; +using OpenAI.Builders; +using OpenAI.ObjectModels.RequestModels; +using OpenAI.ObjectModels.SharedModels; + +namespace OpenAI.Utilities; + +/// +/// Helper methods for Function Calling +/// +public static class FunctionCallingHelper +{ + /// + /// Returns a from the provided method, using any and attributes + /// + /// the method to create the from + /// the created. + public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) + { + var methodDescriptionAttribute = methodInfo.GetCustomAttribute(); + + var result = new FunctionDefinitionBuilder( + methodDescriptionAttribute?.Name ?? methodInfo.Name, methodDescriptionAttribute?.Description); + + var parameters = methodInfo.GetParameters().ToList(); + + foreach (var parameter in parameters) + { + var parameterDescriptionAttribute = parameter.GetCustomAttribute(); + var description = parameterDescriptionAttribute?.Description; + + PropertyDefinition definition; + + switch (parameter.ParameterType) + { + case { } t when t.IsAssignableFrom(typeof(int)): + definition = PropertyDefinition.DefineInteger(description); + break; + case { } t when t.IsAssignableFrom(typeof(float)): + definition = PropertyDefinition.DefineNumber(description); + break; + case { } t when t.IsAssignableFrom(typeof(bool)): + definition = PropertyDefinition.DefineBoolean(description); + break; + case { } t when t.IsAssignableFrom(typeof(string)): + definition = PropertyDefinition.DefineString(description); + break; + case { IsEnum: true }: + + var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) + ? Enum.GetNames(parameter.ParameterType).ToList() + : parameterDescriptionAttribute!.Enum!.Split(",").Select(x => x.Trim()).ToList(); + + + definition = + PropertyDefinition.DefineEnum(enumValues, description); + break; + default: + throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); + } + + result.AddParameter( + parameterDescriptionAttribute?.Name ?? parameter.Name!, + definition, + parameterDescriptionAttribute?.Required ?? true); + } + + return result.Build(); + } + + /// + /// Enumerates the methods in the provided object, and a returns a of for all methods + /// marked with a + /// + /// the object to analyze + public static List GetFunctionDefinitions(object obj) + { + var type = obj.GetType(); + return GetFunctionDefinitions(type); + } + + /// + /// Enumerates the methods in the provided type, and a returns a of for all methods + /// + /// The type to analyze + public static List GetFunctionDefinitions(Type type) + { + var methods = type.GetMethods(); + + var result = methods + .Select(method => new + { + method, + methodDescriptionAttribute = method.GetCustomAttribute() + }) + .Where(@t => @t.methodDescriptionAttribute != null) + .Select(@t => GetFunctionDefinition(@t.method)).ToList(); + + return result; + } + + /// + /// Calls the function on the provided object, using the provided and returns the result of the call + /// + /// The FunctionCall provided by the LLM + /// the object with the method / function to be executed + /// The return type + public static T? CallFunction(FunctionCall functionCall, object obj) + { + if (functionCall == null) + throw new ArgumentNullException(nameof(functionCall)); + + if (functionCall.Name == null) + throw new Exception("Function name is null"); + + var methodInfo = obj.GetType().GetMethod(functionCall.Name); + + if (methodInfo == null) + throw new Exception($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); + + if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) + throw new Exception( + $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); + + var parameters = methodInfo.GetParameters().ToList(); + var arguments = functionCall.ParseArguments(); + var args = new List(); + + foreach (var parameter in parameters) + { + ParameterDescriptionAttribute? parameterDescriptionAttribute = + parameter.GetCustomAttribute(); + + var name = parameterDescriptionAttribute?.Name ?? parameter.Name!; + var argument = arguments.FirstOrDefault(x => x.Key == name); + + if (argument.Key == null) + throw new Exception($"Argument '{name}' not found"); + + var value = parameter.ParameterType.IsEnum ? + Enum.Parse(parameter.ParameterType, argument.Value.ToString()!) : + ((JsonElement)argument.Value).Deserialize(parameter.ParameterType); + + args.Add(value); + } + + T? result = (T?)methodInfo.Invoke(obj, args.ToArray()); + return result; + } +} + +/// +/// Attribute to mark a method as a function, and provide a description for the function. Can also be used to override the Name of the function +/// +[AttributeUsage(AttributeTargets.Method)] +public class FunctionDescriptionAttribute : Attribute +{ + /// + /// Name of the function. If not provided, the name of the method will be used. + /// + public string? Name { get; set; } + /// + /// Description of the function + /// + public string? Description { get; set; } + + /// + /// Creates a new instance of the with the provided description + /// + public FunctionDescriptionAttribute(string? description = null) + { + Description = description; + } +} + +/// +/// Attribute to describe a parameter of a function. Can also be used to override the Name of the parameter +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class ParameterDescriptionAttribute : Attribute +{ + /// + /// Name of the parameter. If not provided, the name of the parameter will be used. + /// + public string? Name { get; set; } + /// + /// Description of the parameter + /// + public string? Description { get; set; } + /// + /// Type of the parameter. If not provided, the type of the parameter will be inferred from the parameter type + /// + public string? Type { get; set; } + /// + /// Enum values of the parameter in a comma separated string. If not provided, the enum values will be inferred from the parameter type + /// + public string? Enum { get; set; } + /// + /// Whether the parameter is required. If not provided, the parameter will be required. Default is true + /// + public bool Required { get; set; } = true; + + /// + /// Creates a new instance of the with the provided description + /// + public ParameterDescriptionAttribute(string? description = null) + { + Description = description; + } +} + diff --git a/OpenAI.Utilities/OpenAI.Utilities.csproj b/OpenAI.Utilities/OpenAI.Utilities.csproj index af60d107..21128fe1 100644 --- a/OpenAI.Utilities/OpenAI.Utilities.csproj +++ b/OpenAI.Utilities/OpenAI.Utilities.csproj @@ -41,10 +41,13 @@ - + + + + \ No newline at end of file diff --git a/OpenAI.UtilitiesPlayground/Program.cs b/OpenAI.UtilitiesPlayground/Program.cs index 44891af4..fddd7406 100644 --- a/OpenAI.UtilitiesPlayground/Program.cs +++ b/OpenAI.UtilitiesPlayground/Program.cs @@ -26,27 +26,110 @@ var sdk = serviceProvider.GetRequiredService(); -IEmbeddingTools embeddingTools = new EmbeddingTools(sdk,500,Models.TextEmbeddingAdaV2); -var dataFrame = await embeddingTools.ReadFilesAndCreateEmbeddingDataAsCsv(Path.Combine("Data", "OpenAI"),"processed/scraped.csv"); -var dataFrame2 = embeddingTools.LoadEmbeddedDataFromCsv("processed/scraped.csv"); +//await ExerciseEmbeddingTools(sdk); +await ExerciseFunctionCalling(sdk); -do +async Task ExerciseFunctionCalling(IOpenAIService openAIService) { - Console.WriteLine("Ask a question:"); - var question = Console.ReadLine(); - if (question != null) - { - var context = embeddingTools.CreateContext(question, dataFrame); - - var completionResponse = await sdk.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest() - { - Model = Models.Gpt_4, - Messages = new List() - { - ChatMessage.FromSystem($"Answer the question based on the context below, and if the question can't be answered based on the context, say \"I don't know\".\n\nContext: {context}"), - ChatMessage.FromUser(question) - } - }); - Console.WriteLine(completionResponse.Successful ? completionResponse.Choices.First().Message.Content : completionResponse.Error?.Message); - } -} while (true); \ No newline at end of file + var calculator = new Calculator(); + var req = new ChatCompletionCreateRequest(); + req.Functions = FunctionCallingHelper.GetFunctionDefinitions(calculator); + req.Messages = new List + { + 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.FunctionCall != null) + Console.WriteLine($"Invoking {response.FunctionCall.Name} with params: {response.FunctionCall.Arguments}"); + else + Console.WriteLine(response.Content); + + req.Messages.Add(response); + + if (response.FunctionCall != null) + { + var functionCall = response.FunctionCall; + var result = FunctionCallingHelper.CallFunction(functionCall, calculator); + response.Content = result.ToString(); + } + + } while (req.Messages.Last().FunctionCall != null); +} + + +async Task ExerciseEmbeddingTools(IOpenAIService openAIService) +{ + IEmbeddingTools embeddingTools = new EmbeddingTools(openAIService, 500, Models.TextEmbeddingAdaV2); + + var dataFrame = + await embeddingTools.ReadFilesAndCreateEmbeddingDataAsCsv(Path.Combine("Data", "OpenAI"), + "processed/scraped.csv"); + + var dataFrame2 = embeddingTools.LoadEmbeddedDataFromCsv("processed/scraped.csv"); + + do + { + Console.WriteLine("Ask a question:"); + var question = Console.ReadLine(); + + if (question != null) + { + var context = embeddingTools.CreateContext(question, dataFrame); + + var completionResponse = await openAIService.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest() + { + Model = Models.Gpt_4, + Messages = new List() + { + ChatMessage.FromSystem( + $"Answer the question based on the context below, and if the question can't be answered based on the context, say \"I don't know\".\n\nContext: {context}"), + ChatMessage.FromUser(question) + } + }); + + Console.WriteLine(completionResponse.Successful + ? completionResponse.Choices.First().Message.Content + : completionResponse.Error?.Message); + } + } while (true); +} + +class Calculator +{ + [FunctionDescription("Adds two numbers.")] + public float Add(float a, float b) => a + b; + + [FunctionDescription("Subtracts two numbers.")] + public float Subtract(float a, float b) => 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) + }; + } + + public enum AdvancedOperators + { + Multiply, Divide + } +} diff --git a/OpenAI.sln b/OpenAI.sln index 4bcf38b7..96e37dd2 100644 --- a/OpenAI.sln +++ b/OpenAI.sln @@ -15,9 +15,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Playground", "OpenAI EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{27B22566-7173-4B05-B0FB-09BF7FD9332C}" ProjectSection(SolutionItems) = preProject + .github\workflows\BuildAndDeployBetalgoOpenAI.yml = .github\workflows\BuildAndDeployBetalgoOpenAI.yml .github\workflows\BuildAndDeployBetalgoOpenAIUtilities.yml = .github\workflows\BuildAndDeployBetalgoOpenAIUtilities.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - .github\workflows\BuildAndDeployBetalgoOpenAI.yml = .github\workflows\BuildAndDeployBetalgoOpenAI.yml Readme.md = Readme.md EndProjectSection EndProject @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Utilities", "OpenAI. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.UtilitiesPlayground", "OpenAI.UtilitiesPlayground\OpenAI.UtilitiesPlayground.csproj", "{A2CEF336-DE84-41A2-880E-84B2871FF929}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Utilities.Tests", "OpenAI.Utilities.Tests\OpenAI.Utilities.Tests.csproj", "{8BC7E997-8591-4ED4-BA4A-486B21419A3E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {A2CEF336-DE84-41A2-880E-84B2871FF929}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2CEF336-DE84-41A2-880E-84B2871FF929}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2CEF336-DE84-41A2-880E-84B2871FF929}.Release|Any CPU.Build.0 = Release|Any CPU + {8BC7E997-8591-4ED4-BA4A-486B21419A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BC7E997-8591-4ED4-BA4A-486B21419A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BC7E997-8591-4ED4-BA4A-486B21419A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BC7E997-8591-4ED4-BA4A-486B21419A3E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +62,7 @@ Global {29A85621-10FF-4CA4-9555-D73DCA0D776E} = {ADCA5A92-4E8E-4574-8E5F-DDE4C03A4F49} {ADB482B9-7F95-41E6-888F-2DD6712D64BF} = {B614DB37-E04E-454F-8C99-EF69BC503745} {A2CEF336-DE84-41A2-880E-84B2871FF929} = {ADCA5A92-4E8E-4574-8E5F-DDE4C03A4F49} + {8BC7E997-8591-4ED4-BA4A-486B21419A3E} = {C0FFCEC4-22E9-423B-B10B-DAF24446FA49} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0A5FEB47-118A-4556-9183-C427C3082E8E} From 632ad9f26d267eed5449ac14d89b17d9d929bc92 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Fri, 4 Aug 2023 17:27:08 +0100 Subject: [PATCH 4/7] Small cleanup and changes for function calling utility --- .../FunctionCallingHelperTests.cs | 17 +- OpenAI.Utilities/FunctionCallingHelper.cs | 372 +++++++++--------- OpenAI.UtilitiesPlayground/Program.cs | 114 +----- .../TestHelpers/EmbeddingTestHelpers.cs | 47 +++ .../TestHelpers/FunctionCallingTestHelpers.cs | 84 ++++ 5 files changed, 343 insertions(+), 291 deletions(-) create mode 100644 OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs create mode 100644 OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs diff --git a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs index dce9b417..50099f73 100644 --- a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs +++ b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs @@ -106,6 +106,21 @@ public void VerifyCallFunction_Complex() obj.NotRequiredIntParameter.ShouldBe(2); obj.OverriddenNameParameter.ShouldBe(3); } + + [Fact] + public void VerifyCallFunction_ArgumentsDoNotMatch() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "TestFunction", + Arguments = "{\"intParameter\": \"invalid\", \"floatParameter\": true, \"boolParameter\": 1, \"stringParameter\": 123, \"enumParameter\": \"NonExistentValue\"}" + }; + + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } + } internal class FunctionCallingTestClass @@ -113,7 +128,7 @@ internal class FunctionCallingTestClass public int IntParameter; public float FloatParameter; public bool BoolParameter; - public string StringParameter; + public string StringParameter = null!; public TestEnum EnumParameter; public TestEnum EnumParameter2; public int RequiredIntParameter; diff --git a/OpenAI.Utilities/FunctionCallingHelper.cs b/OpenAI.Utilities/FunctionCallingHelper.cs index 8a75a6c0..1e73e081 100644 --- a/OpenAI.Utilities/FunctionCallingHelper.cs +++ b/OpenAI.Utilities/FunctionCallingHelper.cs @@ -11,143 +11,153 @@ namespace OpenAI.Utilities; /// public static class FunctionCallingHelper { - /// - /// Returns a from the provided method, using any and attributes - /// - /// the method to create the from - /// the created. - public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) - { - var methodDescriptionAttribute = methodInfo.GetCustomAttribute(); - - var result = new FunctionDefinitionBuilder( - methodDescriptionAttribute?.Name ?? methodInfo.Name, methodDescriptionAttribute?.Description); - - var parameters = methodInfo.GetParameters().ToList(); - - foreach (var parameter in parameters) - { - var parameterDescriptionAttribute = parameter.GetCustomAttribute(); - var description = parameterDescriptionAttribute?.Description; - - PropertyDefinition definition; - - switch (parameter.ParameterType) - { - case { } t when t.IsAssignableFrom(typeof(int)): - definition = PropertyDefinition.DefineInteger(description); - break; - case { } t when t.IsAssignableFrom(typeof(float)): - definition = PropertyDefinition.DefineNumber(description); - break; - case { } t when t.IsAssignableFrom(typeof(bool)): - definition = PropertyDefinition.DefineBoolean(description); - break; - case { } t when t.IsAssignableFrom(typeof(string)): - definition = PropertyDefinition.DefineString(description); - break; - case { IsEnum: true }: - - var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) - ? Enum.GetNames(parameter.ParameterType).ToList() - : parameterDescriptionAttribute!.Enum!.Split(",").Select(x => x.Trim()).ToList(); - - - definition = - PropertyDefinition.DefineEnum(enumValues, description); - break; - default: - throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); - } - - result.AddParameter( - parameterDescriptionAttribute?.Name ?? parameter.Name!, - definition, - parameterDescriptionAttribute?.Required ?? true); - } - - return result.Build(); - } - - /// - /// Enumerates the methods in the provided object, and a returns a of for all methods - /// marked with a - /// - /// the object to analyze - public static List GetFunctionDefinitions(object obj) - { - var type = obj.GetType(); - return GetFunctionDefinitions(type); - } - - /// - /// Enumerates the methods in the provided type, and a returns a of for all methods - /// - /// The type to analyze - public static List GetFunctionDefinitions(Type type) - { - var methods = type.GetMethods(); - - var result = methods - .Select(method => new - { - method, - methodDescriptionAttribute = method.GetCustomAttribute() - }) - .Where(@t => @t.methodDescriptionAttribute != null) - .Select(@t => GetFunctionDefinition(@t.method)).ToList(); - - return result; - } - - /// - /// Calls the function on the provided object, using the provided and returns the result of the call - /// - /// The FunctionCall provided by the LLM - /// the object with the method / function to be executed - /// The return type - public static T? CallFunction(FunctionCall functionCall, object obj) - { - if (functionCall == null) - throw new ArgumentNullException(nameof(functionCall)); - - if (functionCall.Name == null) - throw new Exception("Function name is null"); - - var methodInfo = obj.GetType().GetMethod(functionCall.Name); - - if (methodInfo == null) - throw new Exception($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); - - if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) - throw new Exception( - $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); - - var parameters = methodInfo.GetParameters().ToList(); - var arguments = functionCall.ParseArguments(); - var args = new List(); - - foreach (var parameter in parameters) - { - ParameterDescriptionAttribute? parameterDescriptionAttribute = - parameter.GetCustomAttribute(); - - var name = parameterDescriptionAttribute?.Name ?? parameter.Name!; - var argument = arguments.FirstOrDefault(x => x.Key == name); - - if (argument.Key == null) - throw new Exception($"Argument '{name}' not found"); - - var value = parameter.ParameterType.IsEnum ? - Enum.Parse(parameter.ParameterType, argument.Value.ToString()!) : - ((JsonElement)argument.Value).Deserialize(parameter.ParameterType); - - args.Add(value); - } - - T? result = (T?)methodInfo.Invoke(obj, args.ToArray()); - return result; - } + /// + /// Returns a from the provided method, using any and attributes + /// + /// the method to create the from + /// the created. + public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) + { + var methodDescriptionAttribute = methodInfo.GetCustomAttribute(); + + var result = new FunctionDefinitionBuilder( + methodDescriptionAttribute?.Name ?? methodInfo.Name, methodDescriptionAttribute?.Description); + + var parameters = methodInfo.GetParameters().ToList(); + + foreach (var parameter in parameters) + { + var parameterDescriptionAttribute = parameter.GetCustomAttribute(); + var description = parameterDescriptionAttribute?.Description; + + PropertyDefinition definition; + + switch (parameter.ParameterType) + { + case { } t when t.IsAssignableFrom(typeof(int)): + definition = PropertyDefinition.DefineInteger(description); + break; + case { } t when t.IsAssignableFrom(typeof(float)): + definition = PropertyDefinition.DefineNumber(description); + break; + case { } t when t.IsAssignableFrom(typeof(bool)): + definition = PropertyDefinition.DefineBoolean(description); + break; + case { } t when t.IsAssignableFrom(typeof(string)): + definition = PropertyDefinition.DefineString(description); + break; + case { IsEnum: true }: + + var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) + ? Enum.GetNames(parameter.ParameterType).ToList() + : parameterDescriptionAttribute.Enum.Split(",").Select(x => x.Trim()).ToList(); + + + definition = + PropertyDefinition.DefineEnum(enumValues, description); + break; + default: + throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); + } + + result.AddParameter( + parameterDescriptionAttribute?.Name ?? parameter.Name!, + definition, + parameterDescriptionAttribute?.Required ?? true); + } + + return result.Build(); + } + + /// + /// Enumerates the methods in the provided object, and a returns a of for all methods + /// marked with a + /// + /// the object to analyze + public static List GetFunctionDefinitions(object obj) + { + var type = obj.GetType(); + return GetFunctionDefinitions(type); + } + + /// + /// Enumerates the methods in the provided type, and a returns a of for all methods + /// + /// The type to analyze + /// + public static List GetFunctionDefinitions() + { + return GetFunctionDefinitions(typeof(T)); + } + + /// + /// Enumerates the methods in the provided type, and a returns a of for all methods + /// + /// The type to analyze + public static List GetFunctionDefinitions(Type type) + { + var methods = type.GetMethods(); + + var result = methods + .Select(method => new + { + method, + methodDescriptionAttribute = method.GetCustomAttribute() + }) + .Where(@t => @t.methodDescriptionAttribute != null) + .Select(@t => GetFunctionDefinition(@t.method)).ToList(); + + return result; + } + + /// + /// Calls the function on the provided object, using the provided and returns the result of the call + /// + /// The FunctionCall provided by the LLM + /// the object with the method / function to be executed + /// The return type + public static T? CallFunction(FunctionCall functionCall, object obj) + { + if (functionCall == null) + throw new ArgumentNullException(nameof(functionCall)); + + if (functionCall.Name == null) + throw new Exception("Function name is null"); + + var methodInfo = obj.GetType().GetMethod(functionCall.Name); + + if (methodInfo == null) + throw new Exception($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); + + if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) + throw new Exception( + $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); + + var parameters = methodInfo.GetParameters().ToList(); + var arguments = functionCall.ParseArguments(); + var args = new List(); + + foreach (var parameter in parameters) + { + ParameterDescriptionAttribute? parameterDescriptionAttribute = + parameter.GetCustomAttribute(); + + var name = parameterDescriptionAttribute?.Name ?? parameter.Name!; + var argument = arguments.FirstOrDefault(x => x.Key == name); + + if (argument.Key == null) + throw new Exception($"Argument '{name}' not found"); + + var value = parameter.ParameterType.IsEnum ? + Enum.Parse(parameter.ParameterType, argument.Value.ToString()!) : + ((JsonElement)argument.Value).Deserialize(parameter.ParameterType); + + args.Add(value); + } + + T? result = (T?)methodInfo.Invoke(obj, args.ToArray()); + return result; + } } /// @@ -156,22 +166,22 @@ public static List GetFunctionDefinitions(Type type) [AttributeUsage(AttributeTargets.Method)] public class FunctionDescriptionAttribute : Attribute { - /// - /// Name of the function. If not provided, the name of the method will be used. - /// - public string? Name { get; set; } - /// - /// Description of the function - /// - public string? Description { get; set; } - - /// - /// Creates a new instance of the with the provided description - /// - public FunctionDescriptionAttribute(string? description = null) - { - Description = description; - } + /// + /// Name of the function. If not provided, the name of the method will be used. + /// + public string? Name { get; set; } + /// + /// Description of the function + /// + public string? Description { get; set; } + + /// + /// Creates a new instance of the with the provided description + /// + public FunctionDescriptionAttribute(string? description = null) + { + Description = description; + } } /// @@ -180,33 +190,33 @@ public FunctionDescriptionAttribute(string? description = null) [AttributeUsage(AttributeTargets.Parameter)] public class ParameterDescriptionAttribute : Attribute { - /// - /// Name of the parameter. If not provided, the name of the parameter will be used. - /// - public string? Name { get; set; } - /// - /// Description of the parameter - /// - public string? Description { get; set; } - /// - /// Type of the parameter. If not provided, the type of the parameter will be inferred from the parameter type - /// - public string? Type { get; set; } - /// - /// Enum values of the parameter in a comma separated string. If not provided, the enum values will be inferred from the parameter type - /// - public string? Enum { get; set; } - /// - /// Whether the parameter is required. If not provided, the parameter will be required. Default is true - /// - public bool Required { get; set; } = true; - - /// - /// Creates a new instance of the with the provided description - /// - public ParameterDescriptionAttribute(string? description = null) - { - Description = description; - } + /// + /// Name of the parameter. If not provided, the name of the parameter will be used. + /// + public string? Name { get; set; } + /// + /// Description of the parameter + /// + public string? Description { get; set; } + /// + /// Type of the parameter. If not provided, the type of the parameter will be inferred from the parameter type + /// + public string? Type { get; set; } + /// + /// Enum values of the parameter in a comma separated string. If not provided, the enum values will be inferred from the parameter type + /// + public string? Enum { get; set; } + /// + /// Whether the parameter is required. If not provided, the parameter will be required. Default is true + /// + public bool Required { get; set; } = true; + + /// + /// Creates a new instance of the with the provided description + /// + public ParameterDescriptionAttribute(string? description = null) + { + Description = description; + } } diff --git a/OpenAI.UtilitiesPlayground/Program.cs b/OpenAI.UtilitiesPlayground/Program.cs index fddd7406..b736180b 100644 --- a/OpenAI.UtilitiesPlayground/Program.cs +++ b/OpenAI.UtilitiesPlayground/Program.cs @@ -3,9 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using OpenAI.Extensions; using OpenAI.Interfaces; -using OpenAI.ObjectModels; -using OpenAI.ObjectModels.RequestModels; -using OpenAI.Utilities; +using OpenAI.UtilitiesPlayground.TestHelpers; var builder = new ConfigurationBuilder() .AddUserSecrets(); @@ -26,110 +24,8 @@ var sdk = serviceProvider.GetRequiredService(); -//await ExerciseEmbeddingTools(sdk); -await ExerciseFunctionCalling(sdk); +//await EmbeddingTestHelpers.ExerciseEmbeddingTools(sdk); +await FunctionCallingTestHelpers.ExerciseFunctionCalling(sdk); -async Task ExerciseFunctionCalling(IOpenAIService openAIService) -{ - var calculator = new Calculator(); - var req = new ChatCompletionCreateRequest(); - req.Functions = FunctionCallingHelper.GetFunctionDefinitions(calculator); - req.Messages = new List - { - 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.FunctionCall != null) - Console.WriteLine($"Invoking {response.FunctionCall.Name} with params: {response.FunctionCall.Arguments}"); - else - Console.WriteLine(response.Content); - - req.Messages.Add(response); - - if (response.FunctionCall != null) - { - var functionCall = response.FunctionCall; - var result = FunctionCallingHelper.CallFunction(functionCall, calculator); - response.Content = result.ToString(); - } - - } while (req.Messages.Last().FunctionCall != null); -} - - -async Task ExerciseEmbeddingTools(IOpenAIService openAIService) -{ - IEmbeddingTools embeddingTools = new EmbeddingTools(openAIService, 500, Models.TextEmbeddingAdaV2); - - var dataFrame = - await embeddingTools.ReadFilesAndCreateEmbeddingDataAsCsv(Path.Combine("Data", "OpenAI"), - "processed/scraped.csv"); - - var dataFrame2 = embeddingTools.LoadEmbeddedDataFromCsv("processed/scraped.csv"); - - do - { - Console.WriteLine("Ask a question:"); - var question = Console.ReadLine(); - - if (question != null) - { - var context = embeddingTools.CreateContext(question, dataFrame); - - var completionResponse = await openAIService.ChatCompletion.CreateCompletion( - new ChatCompletionCreateRequest() - { - Model = Models.Gpt_4, - Messages = new List() - { - ChatMessage.FromSystem( - $"Answer the question based on the context below, and if the question can't be answered based on the context, say \"I don't know\".\n\nContext: {context}"), - ChatMessage.FromUser(question) - } - }); - - Console.WriteLine(completionResponse.Successful - ? completionResponse.Choices.First().Message.Content - : completionResponse.Error?.Message); - } - } while (true); -} - -class Calculator -{ - [FunctionDescription("Adds two numbers.")] - public float Add(float a, float b) => a + b; - - [FunctionDescription("Subtracts two numbers.")] - public float Subtract(float a, float b) => 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) - }; - } - - public enum AdvancedOperators - { - Multiply, Divide - } -} +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); \ No newline at end of file diff --git a/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs b/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs new file mode 100644 index 00000000..cb2c31a5 --- /dev/null +++ b/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs @@ -0,0 +1,47 @@ +using OpenAI.Interfaces; +using OpenAI.ObjectModels; +using OpenAI.ObjectModels.RequestModels; +using OpenAI.Utilities; + +namespace OpenAI.UtilitiesPlayground.TestHelpers; + +public static class EmbeddingTestHelpers +{ + public static async Task ExerciseEmbeddingTools(IOpenAIService openAIService) + { + IEmbeddingTools embeddingTools = new EmbeddingTools(openAIService, 500, Models.TextEmbeddingAdaV2); + + var dataFrame = + await embeddingTools.ReadFilesAndCreateEmbeddingDataAsCsv(Path.Combine("Data", "OpenAI"), + "processed/scraped.csv"); + + var dataFrame2 = embeddingTools.LoadEmbeddedDataFromCsv("processed/scraped.csv"); + + do + { + Console.WriteLine("Ask a question:"); + var question = Console.ReadLine(); + + if (question != null) + { + var context = embeddingTools.CreateContext(question, dataFrame); + + var completionResponse = await openAIService.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest + { + Model = Models.Gpt_4, + Messages = new List + { + ChatMessage.FromSystem( + $"Answer the question based on the context below, and if the question can't be answered based on the context, say \"I don't know\".\n\nContext: {context}"), + ChatMessage.FromUser(question) + } + }); + + Console.WriteLine(completionResponse.Successful + ? completionResponse.Choices.First().Message.Content + : completionResponse.Error?.Message); + } + } while (true); + } +} \ No newline at end of file diff --git a/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs b/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs new file mode 100644 index 00000000..123a7fb5 --- /dev/null +++ b/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs @@ -0,0 +1,84 @@ +using System.Globalization; +using OpenAI.Interfaces; +using OpenAI.ObjectModels; +using OpenAI.ObjectModels.RequestModels; +using OpenAI.Utilities; + +namespace OpenAI.UtilitiesPlayground.TestHelpers; + +public static class FunctionCallingTestHelpers +{ + public static async Task ExerciseFunctionCalling(IOpenAIService openAIService) + { + var calculator = new Calculator(); + var req = new ChatCompletionCreateRequest + { + //Functions = FunctionCallingHelper.GetFunctionDefinitions(calculator), + //Functions = FunctionCallingHelper.GetFunctionDefinitions(typeof(Calculator)), + Functions = FunctionCallingHelper.GetFunctionDefinitions(), + Messages = new List + { + 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.FunctionCall != null) + { + Console.WriteLine($"Invoking {response.FunctionCall.Name} with params: {response.FunctionCall.Arguments}"); + } + else + { + Console.WriteLine(response.Content); + } + + req.Messages.Add(response); + + if (response.FunctionCall != null) + { + var functionCall = response.FunctionCall; + var result = FunctionCallingHelper.CallFunction(functionCall, calculator); + response.Content = result.ToString(CultureInfo.CurrentCulture); + } + } while (req.Messages.Last().FunctionCall != null); + } + + + public class Calculator + { + [FunctionDescription("Adds two numbers.")] + public float Add(float a, float b) => a + b; + + [FunctionDescription("Subtracts two numbers.")] + public float Subtract(float a, float b) => 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) + }; + } + + public enum AdvancedOperators + { + Multiply, Divide + } + } +} \ No newline at end of file From a5d93376d4f7ed7f02174a7a53efb76d0e1dded9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Velv=C3=A1rt=20Andr=C3=A1s?= Date: Sun, 6 Aug 2023 14:57:55 +0200 Subject: [PATCH 5/7] Fixed Type override for ParameterDescriptionAttribute Added InvalidFunctionCallException Added / updated relevant tests --- .../FunctionCallingHelperTests.cs | 84 ++++++++++++++++++- OpenAI.Utilities/FunctionCallingHelper.cs | 79 ++++++++++------- OpenAI.lutconfig | 6 ++ 3 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 OpenAI.lutconfig diff --git a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs index 50099f73..cab2056e 100644 --- a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs +++ b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs @@ -47,13 +47,22 @@ public void VerifyGetFunctionDefinition() functionDefinition.Parameters.Properties.ShouldContainKey("OverriddenName"); } + [Fact] + public void VerifyTypeOverride() + { + var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("ThirdFunction")!); + + var overriddenNameParameter = functionDefinition.Parameters.Properties["overriddenTypeParameter"]; + overriddenNameParameter.Type.ShouldBe("string"); + overriddenNameParameter.Description.ShouldBe("Overridden type parameter"); + } + [Fact] public void VerifyGetFunctionDefinitions() { - var obj = new FunctionCallingTestClass(); - var functionDefinitions = FunctionCallingHelper.GetFunctionDefinitions(obj); + var functionDefinitions = FunctionCallingHelper.GetFunctionDefinitions(); - functionDefinitions.Count.ShouldBe(2); + functionDefinitions.Count.ShouldBe(3); var functionDefinition = functionDefinitions.First(x => x.Name == "TestFunction"); functionDefinition.Description.ShouldBe("Test Function"); @@ -64,6 +73,11 @@ public void VerifyGetFunctionDefinitions() functionDefinition2.Description.ShouldBe("Second Function"); functionDefinition2.Parameters.ShouldNotBeNull(); functionDefinition2.Parameters.Properties!.Count.ShouldBe(0); + + var functionDefinition3 = functionDefinitions.First(x => x.Name == "ThirdFunction"); + functionDefinition3.Description.ShouldBe("Third Function"); + functionDefinition3.Parameters.ShouldNotBeNull(); + functionDefinition3.Parameters.Properties!.Count.ShouldBe(1); } [Fact] @@ -121,6 +135,62 @@ public void VerifyCallFunction_ArgumentsDoNotMatch() Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); } + [Fact] + public void CallFunctionShouldThrowIfObjIsNull() + { + var functionCall = new FunctionCall + { + Name = "SecondFunction", + }; + + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, null!)); + } + + [Fact] + public void CallFunctionShouldThrowIfFunctionCallIsNull() + { + var obj = new FunctionCallingTestClass(); + + Should.Throw(() => FunctionCallingHelper.CallFunction(null!, obj)); + } + + [Fact] + public void CallFunctionShouldThrowIfFunctionCallNameIsNotSet() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = null!, + }; + + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } + + [Fact] + public void CallFunctionShouldThrowIfFunctionCallNameIsNotValid() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "NonExistentFunction", + }; + + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } + + [Fact] + public void CallFunctionShouldThrowIfInvalidReturnType() + { + var obj = new FunctionCallingTestClass(); + var functionCall = new FunctionCall() + { + Name = "SecondFunction", + }; + + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } } internal class FunctionCallingTestClass @@ -134,6 +204,7 @@ internal class FunctionCallingTestClass public int RequiredIntParameter; public int? NotRequiredIntParameter; public int OverriddenNameParameter; + public string OverriddenTypeParameter = null!; [FunctionDescription("Test Function")] public int TestFunction( @@ -146,6 +217,7 @@ public int TestFunction( [ParameterDescription(Description = "Required Int Parameter", Required= true)] int requiredIntParameter, [ParameterDescription(Description = "Not required Int Parameter", Required = false)] int notRequiredIntParameter, [ParameterDescription(Name = "OverriddenName", Description = "Overridden")] int overriddenNameParameter) + { IntParameter = intParameter; FloatParameter = floatParameter; @@ -165,6 +237,12 @@ public string SecondFunction() { return "Hello"; } + + [FunctionDescription("Third Function")] + public void ThirdFunction([ParameterDescription(Type = "string", Description = "Overridden type parameter")] int overriddenTypeParameter) + { + OverriddenTypeParameter = overriddenTypeParameter.ToString(); + } } public enum TestEnum diff --git a/OpenAI.Utilities/FunctionCallingHelper.cs b/OpenAI.Utilities/FunctionCallingHelper.cs index 1e73e081..90e088a6 100644 --- a/OpenAI.Utilities/FunctionCallingHelper.cs +++ b/OpenAI.Utilities/FunctionCallingHelper.cs @@ -32,32 +32,40 @@ public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) PropertyDefinition definition; - switch (parameter.ParameterType) + switch (parameter.ParameterType, parameterDescriptionAttribute?.Type == null) { - case { } t when t.IsAssignableFrom(typeof(int)): - definition = PropertyDefinition.DefineInteger(description); - break; - case { } t when t.IsAssignableFrom(typeof(float)): - definition = PropertyDefinition.DefineNumber(description); - break; - case { } t when t.IsAssignableFrom(typeof(bool)): - definition = PropertyDefinition.DefineBoolean(description); - break; - case { } t when t.IsAssignableFrom(typeof(string)): - definition = PropertyDefinition.DefineString(description); - break; - case { IsEnum: true }: - - var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) - ? Enum.GetNames(parameter.ParameterType).ToList() - : parameterDescriptionAttribute.Enum.Split(",").Select(x => x.Trim()).ToList(); - - - definition = - PropertyDefinition.DefineEnum(enumValues, description); - break; - default: - throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); + case (_, false): + definition = new PropertyDefinition() + { + Type = parameterDescriptionAttribute!.Type!, + Description = description, + }; + + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(int)): + definition = PropertyDefinition.DefineInteger(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(float)): + definition = PropertyDefinition.DefineNumber(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(bool)): + definition = PropertyDefinition.DefineBoolean(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(string)): + definition = PropertyDefinition.DefineString(description); + break; + case ({ IsEnum: true }, _): + + var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) + ? Enum.GetNames(parameter.ParameterType).ToList() + : parameterDescriptionAttribute.Enum.Split(",").Select(x => x.Trim()).ToList(); + + definition = + PropertyDefinition.DefineEnum(enumValues, description); + + break; + default: + throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); } result.AddParameter( @@ -122,15 +130,18 @@ public static List GetFunctionDefinitions(Type type) throw new ArgumentNullException(nameof(functionCall)); if (functionCall.Name == null) - throw new Exception("Function name is null"); + throw new InvalidFunctionCallException("Function Name is null"); + + if (obj == null) + throw new ArgumentNullException(nameof(obj)); var methodInfo = obj.GetType().GetMethod(functionCall.Name); if (methodInfo == null) - throw new Exception($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); + throw new InvalidFunctionCallException($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) - throw new Exception( + throw new InvalidFunctionCallException( $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); var parameters = methodInfo.GetParameters().ToList(); @@ -160,6 +171,18 @@ public static List GetFunctionDefinitions(Type type) } } +/// +/// Exception thrown when a function call is invalid +/// +public class InvalidFunctionCallException : Exception +{ + /// + /// Creates a new instance of the with the provided message + /// + public InvalidFunctionCallException(string message) : base(message) + { } +} + /// /// Attribute to mark a method as a function, and provide a description for the function. Can also be used to override the Name of the function /// diff --git a/OpenAI.lutconfig b/OpenAI.lutconfig new file mode 100644 index 00000000..596a8603 --- /dev/null +++ b/OpenAI.lutconfig @@ -0,0 +1,6 @@ + + + true + true + 180000 + \ No newline at end of file From ce6a75d21e3a5024fd59dff451c015cf2873c1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Velv=C3=A1rt=20Andr=C3=A1s?= Date: Sun, 6 Aug 2023 15:10:44 +0200 Subject: [PATCH 6/7] Added test for calling method with overridden type --- .../FunctionCallingHelperTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs index cab2056e..c07dd475 100644 --- a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs +++ b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs @@ -191,6 +191,21 @@ public void CallFunctionShouldThrowIfInvalidReturnType() Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); } + + [Fact] + public void VerifyCallFunctionWithOverriddenType() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "ThirdFunction", + Arguments = "{\"overriddenTypeParameter\": 1}" + }; + + FunctionCallingHelper.CallFunction(functionCall, obj); + obj.OverriddenTypeParameter.ShouldBe("1"); + } } internal class FunctionCallingTestClass From a2f8929b0f8114dccbbe7f6963d6ba1b704045a6 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Sun, 6 Aug 2023 20:47:13 +0100 Subject: [PATCH 7/7] Code Cleanup and version bump. Betalgo.OpenAI 7.1.4 Betalgo.OpenAI.Utilities 7.0.2 --- OpenAI.Playground/OpenAI.Playground.csproj | 12 +- .../TestHelpers/ChatCompletionTestHelper.cs | 20 +- .../TestHelpers/CompletionTestHelper.cs | 2 +- .../OpenAIServiceCollectionExtensions.cs | 2 +- OpenAI.SDK/Managers/OpenAIChatCompletions.cs | 30 +- OpenAI.SDK/Managers/OpenAIFile.cs | 1 - OpenAI.SDK/Managers/OpenAIService.cs | 24 +- OpenAI.SDK/ObjectModels/Models.cs | 23 +- .../ChatCompletionCreateRequest.cs | 40 +- .../RequestModels/CompletionCreateRequest.cs | 2 +- .../RequestModels/EditCreateRequest.cs | 2 +- .../RequestModels/EmbeddingCreateRequest.cs | 2 +- .../ResponseModels/BaseResponse.cs | 33 +- OpenAI.SDK/OpenAI.csproj | 6 +- .../FunctionCallingHelperTests.cs | 420 +++++++++--------- OpenAI.Utilities/EmbedStaticValues.cs | 10 - .../Embedding/EmbedStaticValues.cs | 9 + .../{ => Embedding}/EmbeddingTools.cs | 4 +- .../Embedding/TextEmbeddingData.cs | 8 + .../{ => Extensions}/StringExtensions.cs | 3 +- .../FunctionCalling/FunctionCallingHelper.cs | 187 ++++++++ .../FunctionDescriptionAttribute.cs | 27 ++ .../InvalidFunctionCallException.cs | 14 + .../ParameterDescriptionAttribute.cs | 42 ++ OpenAI.Utilities/FunctionCallingHelper.cs | 245 ---------- OpenAI.Utilities/OpenAI.Utilities.csproj | 8 +- OpenAI.Utilities/TextEmbeddingData.cs | 9 - .../TestHelpers/EmbeddingTestHelpers.cs | 2 +- .../TestHelpers/FunctionCallingTestHelpers.cs | 25 +- 29 files changed, 630 insertions(+), 582 deletions(-) delete mode 100644 OpenAI.Utilities/EmbedStaticValues.cs create mode 100644 OpenAI.Utilities/Embedding/EmbedStaticValues.cs rename OpenAI.Utilities/{ => Embedding}/EmbeddingTools.cs (99%) create mode 100644 OpenAI.Utilities/Embedding/TextEmbeddingData.cs rename OpenAI.Utilities/{ => Extensions}/StringExtensions.cs (99%) create mode 100644 OpenAI.Utilities/FunctionCalling/FunctionCallingHelper.cs create mode 100644 OpenAI.Utilities/FunctionCalling/FunctionDescriptionAttribute.cs create mode 100644 OpenAI.Utilities/FunctionCalling/InvalidFunctionCallException.cs create mode 100644 OpenAI.Utilities/FunctionCalling/ParameterDescriptionAttribute.cs delete mode 100644 OpenAI.Utilities/FunctionCallingHelper.cs delete mode 100644 OpenAI.Utilities/TextEmbeddingData.cs diff --git a/OpenAI.Playground/OpenAI.Playground.csproj b/OpenAI.Playground/OpenAI.Playground.csproj index d9bcc660..e616a202 100644 --- a/OpenAI.Playground/OpenAI.Playground.csproj +++ b/OpenAI.Playground/OpenAI.Playground.csproj @@ -2,7 +2,8 @@ Exe - net7.0;net6.0;netstandard2.0 + net7.0 + true PackageReference enable @@ -21,18 +22,19 @@ - + + - + diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index dcd0027b..085d3a65 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -104,12 +104,12 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) 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 {"celsius", "fahrenheit"},"The temperature unit to use. Infer this from the users location.")) + .AddParameter("format", PropertyDefinition.DefineEnum(new List {"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() { Type = "string",Description = "The city and state, e.g. San Francisco, CA"}) + .AddParameter("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"}) .AddParameter("format", PropertyDefinition.DefineEnum(new List {"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() @@ -130,7 +130,7 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) 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.") }, - Functions = new List { fn1, fn2, fn3, fn4 }, + Functions = new List {fn1, fn2, fn3, fn4}, // optionally, to force a specific function: // FunctionCall = new Dictionary { { "name", "get_current_weather" } }, MaxTokens = 50, @@ -188,17 +188,17 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) 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 {"celsius", "fahrenheit"},"The temperature unit to use. Infer this from the users location.")) + .AddParameter("format", PropertyDefinition.DefineEnum(new List {"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("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"}) .AddParameter("format", PropertyDefinition.DefineEnum(new List {"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(); @@ -214,14 +214,14 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) Messages = new List { ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."), - + // to test weather forecast functions: - ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days."), + ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.") // or to test array functions, use this instead: // ChatMessage.FromUser("The combination is: One. Two. Three. Four. Five."), }, - Functions = new List { fn1, fn2, fn3, fn4 }, + Functions = new List {fn1, fn2, fn3, fn4}, // optionally, to force a specific function: // FunctionCall = new Dictionary { { "name", "get_current_weather" } }, MaxTokens = 50, @@ -242,7 +242,7 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) Message: Function call: identify_number_sequence values: [1, 2, 3, 4, 5] - */ + */ await foreach (var completionResult in completionResults) { diff --git a/OpenAI.Playground/TestHelpers/CompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/CompletionTestHelper.cs index 19b59650..0d12bb95 100644 --- a/OpenAI.Playground/TestHelpers/CompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/CompletionTestHelper.cs @@ -223,4 +223,4 @@ public static async Task RunSimpleCompletionStreamTestWithCancellationToken(IOpe throw; } } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/Extensions/OpenAIServiceCollectionExtensions.cs b/OpenAI.SDK/Extensions/OpenAIServiceCollectionExtensions.cs index 27dbe39b..efc4fdf2 100644 --- a/OpenAI.SDK/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/OpenAI.SDK/Extensions/OpenAIServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ public static IHttpClientBuilder AddOpenAIService(this IServiceCollection servic return services.AddHttpClient(); } - + public static IHttpClientBuilder AddOpenAIService(this IServiceCollection services, string name, Action? setupAction = null) where TServiceInterface : class, IOpenAIService { diff --git a/OpenAI.SDK/Managers/OpenAIChatCompletions.cs b/OpenAI.SDK/Managers/OpenAIChatCompletions.cs index 1628e5bc..8018c604 100644 --- a/OpenAI.SDK/Managers/OpenAIChatCompletions.cs +++ b/OpenAI.SDK/Managers/OpenAIChatCompletions.cs @@ -1,9 +1,9 @@ -using OpenAI.Extensions; +using System.Runtime.CompilerServices; +using System.Text.Json; +using OpenAI.Extensions; using OpenAI.Interfaces; using OpenAI.ObjectModels.RequestModels; using OpenAI.ObjectModels.ResponseModels; -using System.Runtime.CompilerServices; -using System.Text.Json; namespace OpenAI.Managers; @@ -82,19 +82,18 @@ public async IAsyncEnumerable CreateCompletionAsSt /// /// This helper class attempts to reassemble a function call response - /// that was split up across several streamed chunks. + /// that was split up across several streamed chunks. /// Note that this only works for the first message in each response, /// and ignores the others; if OpenAI ever changes their response format /// this will need to be adjusted. /// private class ReassemblyContext { - private FunctionCall? FnCall = null; + private FunctionCall? FnCall; public bool IsFnAssemblyActive => FnCall != null; - /// /// Detects if a response block is a part of a multi-chunk /// streamed function call response. As long as that's true, @@ -105,7 +104,10 @@ private class ReassemblyContext public void Process(ChatCompletionCreateResponse block) { var firstChoice = block.Choices?.FirstOrDefault(); - if (firstChoice == null) { return; } // not a valid state? nothing to do + if (firstChoice == null) + { + return; + } // not a valid state? nothing to do var isStreamingFnCall = IsStreamingFunctionCall(); var justStarted = false; @@ -136,12 +138,16 @@ public void Process(ChatCompletionCreateResponse block) } // Returns true if we're actively streaming, and also have a partial function call in the response - bool IsStreamingFunctionCall() => - firstChoice.FinishReason == null && // actively streaming, and - firstChoice.Message?.FunctionCall != null; // have a function call + bool IsStreamingFunctionCall() + { + return firstChoice.FinishReason == null && // actively streaming, and + firstChoice.Message?.FunctionCall != null; + } // have a function call - string ExtractArgsSoFar() => - block.Choices?.FirstOrDefault()?.Message?.FunctionCall?.Arguments ?? ""; + string ExtractArgsSoFar() + { + return block.Choices?.FirstOrDefault()?.Message?.FunctionCall?.Arguments ?? ""; + } } } } \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIFile.cs b/OpenAI.SDK/Managers/OpenAIFile.cs index eaf0e377..ec93830a 100644 --- a/OpenAI.SDK/Managers/OpenAIFile.cs +++ b/OpenAI.SDK/Managers/OpenAIFile.cs @@ -1,7 +1,6 @@ using System.Net.Http.Json; using OpenAI.Extensions; using OpenAI.Interfaces; -using OpenAI.ObjectModels.ResponseModels; using OpenAI.ObjectModels.ResponseModels.FileResponseModels; using OpenAI.ObjectModels.SharedModels; diff --git a/OpenAI.SDK/Managers/OpenAIService.cs b/OpenAI.SDK/Managers/OpenAIService.cs index 9494a9e6..3a356529 100644 --- a/OpenAI.SDK/Managers/OpenAIService.cs +++ b/OpenAI.SDK/Managers/OpenAIService.cs @@ -8,13 +8,13 @@ namespace OpenAI.Managers; //TODO Find a way to show default request values in documentation public partial class OpenAIService : IOpenAIService, IDisposable { + private readonly bool _disposeHttpClient; private readonly IOpenAiEndpointProvider _endpointProvider; private readonly HttpClient _httpClient; private string? _defaultModelId; - private bool _disposeHttpClient; [ActivatorUtilitiesConstructor] - public OpenAIService(IOptions settings,HttpClient httpClient) + public OpenAIService(IOptions settings, HttpClient httpClient) : this(settings.Value, httpClient) { } @@ -60,6 +60,15 @@ public OpenAIService(OpenAiOptions settings, HttpClient? httpClient = null) _defaultModelId = settings.DefaultModelId; } + /// + /// Method to dispose the HttpContext if created internally. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + /// public IModelService Models => this; @@ -109,15 +118,6 @@ public void SetDefaultModelId(string modelId) return _defaultModelId; } - /// - /// Method to dispose the HttpContext if created internally. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - protected virtual void Dispose(bool disposing) { if (disposing) @@ -128,4 +128,4 @@ protected virtual void Dispose(bool disposing) } } } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/Models.cs b/OpenAI.SDK/ObjectModels/Models.cs index 60edd172..5fab9b29 100644 --- a/OpenAI.SDK/ObjectModels/Models.cs +++ b/OpenAI.SDK/ObjectModels/Models.cs @@ -67,9 +67,11 @@ public enum Model CodeCushmanV1, CodeDavinciV2, + [Obsolete("Use Gpt_3_5_Turbo instead")] ChatGpt3_5Turbo, Gpt_3_5_Turbo, + [Obsolete("Use Gpt_3_5_Turbo_0301 instead")] ChatGpt3_5Turbo0301, Gpt_3_5_Turbo_0301, @@ -133,15 +135,16 @@ public enum Subject public static string Gpt_4_32k_0314 => "gpt-4-32k-0314"; /// - /// Snapshot of gpt-4 from June 13th 2023 with function calling data. Unlike gpt-4, this model will not receive updates, - /// and will be deprecated 3 months after a new version is released. + /// Snapshot of gpt-4 from June 13th 2023 with function calling data. Unlike gpt-4, this model will not receive + /// updates, + /// and will be deprecated 3 months after a new version is released. /// 8,192 tokens Up to Sep 2021 /// public static string Gpt_4_0613 => "gpt-4-0613"; /// - /// Snapshot of gpt-4-32 from June 13th 2023. Unlike gpt-4-32k, this model will not receive updates, - /// and will be deprecated 3 months after a new version is released. + /// Snapshot of gpt-4-32 from June 13th 2023. Unlike gpt-4-32k, this model will not receive updates, + /// and will be deprecated 3 months after a new version is released. /// 32,768 tokens Up to Sep 2021 /// public static string Gpt_4_32k_0613 => "gpt-4-32k-0613"; @@ -199,6 +202,7 @@ public enum Subject /// [Obsolete("Use Gpt_3_5_Turbo instead, this is just a naming change,the field will be removed in next versions")] public static string ChatGpt3_5Turbo => "gpt-3.5-turbo"; + /// /// Most capable GPT-3.5 model and optimized for chat at 1/10th the cost of text-davinci-003. Will be updated with our /// latest model iteration. @@ -207,7 +211,7 @@ public enum Subject public static string Gpt_3_5_Turbo => "gpt-3.5-turbo"; /// - /// Same capabilities as the standard gpt-3.5-turbo model but with 4 times the context. + /// Same capabilities as the standard gpt-3.5-turbo model but with 4 times the context. /// 16,384 tokens Up to Sep 2021 /// public static string Gpt_3_5_Turbo_16k => "gpt-3.5-turbo-16k"; @@ -219,6 +223,7 @@ public enum Subject /// [Obsolete("Use Gpt_3_5_Turbo_0301 instead, this is just a naming change,the field will be removed in next versions")] public static string ChatGpt3_5Turbo0301 => "gpt-3.5-turbo-0301"; + /// /// Snapshot of gpt-3.5-turbo from March 1st 2023. Unlike gpt-3.5-turbo, this model will not receive updates, and will /// only be supported for a three month period ending on June 1st 2023. @@ -227,15 +232,15 @@ public enum Subject public static string Gpt_3_5_Turbo_0301 => "gpt-3.5-turbo-0301"; /// - /// Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, - /// this model will not receive updates, and will be deprecated 3 months after a new version is released. + /// Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, + /// this model will not receive updates, and will be deprecated 3 months after a new version is released. /// 4,096 tokens Up to Sep 2021 /// public static string Gpt_3_5_Turbo_0613 => "gpt-3.5-turbo-0613"; /// - /// Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, - /// this model will not receive updates, and will be deprecated 3 months after a new version is released. + /// Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, + /// this model will not receive updates, and will be deprecated 3 months after a new version is released. /// 4,096 tokens Up to Sep 2021 /// public static string Gpt_3_5_Turbo_16k_0613 => "gpt-3.5-turbo-16k-0613"; diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index 20d7b0f3..f8580f75 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Text.Json; using System.Text.Json.Serialization; using OpenAI.Interfaces; using OpenAI.ObjectModels.SharedModels; @@ -17,15 +16,14 @@ public class ChatCompletionCreateRequest : IModelValidate, IOpenAiModels.ITemper [JsonPropertyName("messages")] public IList Messages { get; set; } - /// + /// /// A list of functions the model may generate JSON inputs for. /// [JsonIgnore] public IList? Functions { get; set; } - - [JsonIgnore] - public object? FunctionsAsObject { get; set; } - + + [JsonIgnore] public object? FunctionsAsObject { get; set; } + [JsonPropertyName("functions")] public object? FunctionCalculated { @@ -134,6 +132,20 @@ public IList? StopCalculated [JsonPropertyName("logit_bias")] public object? LogitBias { get; set; } + + /// + /// String or object. Controls how the model responds to function calls. + /// "none" means the model does not call a function, and responds to the end-user. + /// "auto" means the model can pick between an end-user or calling a function. + /// "none" is the default when no functions are present. "auto" is the default if functions are present. + /// Specifying a particular function via {"name": "my_function"} forces the model to call that function. + /// (Note: in C# specify that as: + /// FunctionCall = new Dictionary<string, string> { { "name", "my_function" } } + /// ). + /// + [JsonPropertyName("function_call")] + public object? FunctionCall { get; set; } + /// /// ID of the model to use. For models supported see start with Gpt_ /// @@ -158,18 +170,4 @@ public IEnumerable Validate() /// [JsonPropertyName("user")] public string User { get; set; } - - - /// - /// String or object. Controls how the model responds to function calls. - /// "none" means the model does not call a function, and responds to the end-user. - /// "auto" means the model can pick between an end-user or calling a function. - /// "none" is the default when no functions are present. "auto" is the default if functions are present. - /// Specifying a particular function via {"name": "my_function"} forces the model to call that function. - /// (Note: in C# specify that as: - /// FunctionCall = new Dictionary<string, string> { { "name", "my_function" } } - /// ). - /// - [JsonPropertyName("function_call")] - public object? FunctionCall { get; set; } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/CompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/CompletionCreateRequest.cs index b2ed5627..e33b3258 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/CompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/CompletionCreateRequest.cs @@ -207,4 +207,4 @@ public IEnumerable Validate() /// [JsonPropertyName("user")] public string? User { get; set; } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/EditCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/EditCreateRequest.cs index 6c17c7ae..c6371e87 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/EditCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/EditCreateRequest.cs @@ -60,4 +60,4 @@ public IEnumerable Validate() /// [JsonPropertyName("temperature")] public float? Temperature { get; set; } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/EmbeddingCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/EmbeddingCreateRequest.cs index bae410b0..07e39444 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/EmbeddingCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/EmbeddingCreateRequest.cs @@ -63,4 +63,4 @@ public IEnumerable Validate() { throw new NotImplementedException(); } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs index 1d1c5602..4137409a 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs @@ -47,37 +47,34 @@ public record BaseResponse public class Error { - [JsonPropertyName("code")] - public string? Code { get; set; } + [JsonPropertyName("code")] public string? Code { get; set; } - [JsonPropertyName("param")] - public string? Param { get; set; } + [JsonPropertyName("param")] public string? Param { get; set; } - [JsonPropertyName("type")] - public string? Type { get; set; } - [JsonIgnore] - public string? Message { get; private set; } - [JsonIgnore] - public List Messages { get; private set; } + [JsonPropertyName("type")] public string? Type { get; set; } + + [JsonIgnore] public string? Message { get; private set; } + + [JsonIgnore] public List Messages { get; private set; } [JsonPropertyName("message")] [JsonConverter(typeof(MessageConverter))] - public object MessageObject - { + public object MessageObject + { set { switch (value) { case string s: Message = s; - Messages = new List { s }; + Messages = new List {s}; break; - case List list when list.All(i => i is JsonElement): + case List list when list.All(i => i is JsonElement): Messages = list.Cast().Select(e => e.GetString()).ToList(); Message = string.Join(Environment.NewLine, Messages); break; } - } + } } public class MessageConverter : JsonConverter @@ -88,10 +85,12 @@ public class MessageConverter : JsonConverter { return reader.GetString(); } - else if (reader.TokenType == JsonTokenType.StartArray) + + if (reader.TokenType == JsonTokenType.StartArray) { return JsonSerializer.Deserialize>(ref reader, options); } + throw new JsonException(); } @@ -100,4 +99,4 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp JsonSerializer.Serialize(writer, value, options); } } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/OpenAI.csproj b/OpenAI.SDK/OpenAI.csproj index 8548fcc0..8c9956d7 100644 --- a/OpenAI.SDK/OpenAI.csproj +++ b/OpenAI.SDK/OpenAI.csproj @@ -1,7 +1,8 @@  - net7.0;net6.0;netstandard2.0 + net7.0 + enable enable Latest @@ -10,7 +11,8 @@ https://openai.com/ OpenAI-Betalgo.png true - 7.1.3 + OpenAI SDK by Betalgo + 7.1.4 Tolga Kayhan, Betalgo Betalgo Up Ltd. OpenAI ChatGPT, Whisper, GPT-4 and DALL·E dotnet SDK diff --git a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs index c07dd475..2a85814e 100644 --- a/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs +++ b/OpenAI.Utilities.Tests/FunctionCallingHelperTests.cs @@ -1,126 +1,127 @@ using OpenAI.ObjectModels.RequestModels; +using OpenAI.Utilities.FunctionCalling; namespace OpenAI.Utilities.Tests; public class FunctionCallingHelperTests { - [Fact] - public void VerifyGetFunctionDefinition() - { - var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("TestFunction")!); - - functionDefinition.Name.ShouldBe("TestFunction"); - functionDefinition.Description.ShouldBe("Test Function"); - functionDefinition.Parameters.ShouldNotBeNull(); - functionDefinition.Parameters.Properties!.Count.ShouldBe(9); - - var intParameter = functionDefinition.Parameters.Properties["intParameter"]; - intParameter.Description.ShouldBe("Int Parameter"); - intParameter.Type.ShouldBe("integer"); - - var floatParameter = functionDefinition.Parameters.Properties["floatParameter"]; - floatParameter.Description.ShouldBe("Float Parameter"); - floatParameter.Type.ShouldBe("number"); - - var boolParameter = functionDefinition.Parameters.Properties["boolParameter"]; - boolParameter.Description.ShouldBe("Bool Parameter"); - boolParameter.Type.ShouldBe("boolean"); - - var stringParameter = functionDefinition.Parameters.Properties["stringParameter"]; - stringParameter.Description.ShouldBe("String Parameter"); - stringParameter.Type.ShouldBe("string"); - - var enumValues = new List {"Value1", "Value2", "Value3"}; - - var enumParameter = functionDefinition.Parameters.Properties["enumParameter"]; - enumParameter.Description.ShouldBe("Enum Parameter"); - enumParameter.Type.ShouldBe("string"); - enumParameter.Enum.ShouldBe(enumValues); - - - var enumParameter2 = functionDefinition.Parameters.Properties["enumParameter2"]; - enumParameter2.Description.ShouldBe("Enum Parameter 2"); - enumParameter2.Type.ShouldBe("string"); - enumParameter2.Enum.ShouldBe(enumValues); - - functionDefinition.Parameters.Properties.ShouldNotContainKey("overriddenNameParameter"); - functionDefinition.Parameters.Properties.ShouldContainKey("OverriddenName"); - } - - [Fact] - public void VerifyTypeOverride() - { - var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("ThirdFunction")!); - - var overriddenNameParameter = functionDefinition.Parameters.Properties["overriddenTypeParameter"]; - overriddenNameParameter.Type.ShouldBe("string"); - overriddenNameParameter.Description.ShouldBe("Overridden type parameter"); - } - - [Fact] - public void VerifyGetFunctionDefinitions() - { - var functionDefinitions = FunctionCallingHelper.GetFunctionDefinitions(); - - functionDefinitions.Count.ShouldBe(3); - - var functionDefinition = functionDefinitions.First(x => x.Name == "TestFunction"); - functionDefinition.Description.ShouldBe("Test Function"); - functionDefinition.Parameters.ShouldNotBeNull(); - functionDefinition.Parameters.Properties!.Count.ShouldBe(9); - - var functionDefinition2 = functionDefinitions.First(x => x.Name == "SecondFunction"); - functionDefinition2.Description.ShouldBe("Second Function"); - functionDefinition2.Parameters.ShouldNotBeNull(); - functionDefinition2.Parameters.Properties!.Count.ShouldBe(0); - - var functionDefinition3 = functionDefinitions.First(x => x.Name == "ThirdFunction"); - functionDefinition3.Description.ShouldBe("Third Function"); - functionDefinition3.Parameters.ShouldNotBeNull(); - functionDefinition3.Parameters.Properties!.Count.ShouldBe(1); - } - - [Fact] - public void VerifyCallFunction_Simple() - { - var obj = new FunctionCallingTestClass(); - - var functionCall = new FunctionCall - { - Name = "SecondFunction", - }; - - var result = FunctionCallingHelper.CallFunction(functionCall, obj); - result.ShouldBe("Hello"); - } - - [Fact] - public void VerifyCallFunction_Complex() - { - var obj = new FunctionCallingTestClass(); - - var functionCall = new FunctionCall - { - Name = "TestFunction", - // arguments is a json dictionary - Arguments = "{\"intParameter\": 1, \"floatParameter\": 2.0, \"boolParameter\": true, \"stringParameter\": \"Hello\", \"enumParameter\": \"Value1\", \"enumParameter2\": \"Value2\", \"requiredIntParameter\": 1, \"notRequiredIntParameter\": 2, \"OverriddenName\": 3}" - - }; - - var result = FunctionCallingHelper.CallFunction(functionCall, obj); - result.ShouldBe(5); - - obj.IntParameter.ShouldBe(1); - obj.FloatParameter.ShouldBe(2.0f); - obj.BoolParameter.ShouldBe(true); - obj.StringParameter.ShouldBe("Hello"); - obj.EnumParameter.ShouldBe(TestEnum.Value1); - obj.EnumParameter2.ShouldBe(TestEnum.Value2); - obj.RequiredIntParameter.ShouldBe(1); - obj.NotRequiredIntParameter.ShouldBe(2); - obj.OverriddenNameParameter.ShouldBe(3); - } - + [Fact] + public void VerifyGetFunctionDefinition() + { + var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("TestFunction")!); + + functionDefinition.Name.ShouldBe("TestFunction"); + functionDefinition.Description.ShouldBe("Test Function"); + functionDefinition.Parameters.ShouldNotBeNull(); + functionDefinition.Parameters.Properties!.Count.ShouldBe(9); + + var intParameter = functionDefinition.Parameters.Properties["intParameter"]; + intParameter.Description.ShouldBe("Int Parameter"); + intParameter.Type.ShouldBe("integer"); + + var floatParameter = functionDefinition.Parameters.Properties["floatParameter"]; + floatParameter.Description.ShouldBe("Float Parameter"); + floatParameter.Type.ShouldBe("number"); + + var boolParameter = functionDefinition.Parameters.Properties["boolParameter"]; + boolParameter.Description.ShouldBe("Bool Parameter"); + boolParameter.Type.ShouldBe("boolean"); + + var stringParameter = functionDefinition.Parameters.Properties["stringParameter"]; + stringParameter.Description.ShouldBe("String Parameter"); + stringParameter.Type.ShouldBe("string"); + + var enumValues = new List {"Value1", "Value2", "Value3"}; + + var enumParameter = functionDefinition.Parameters.Properties["enumParameter"]; + enumParameter.Description.ShouldBe("Enum Parameter"); + enumParameter.Type.ShouldBe("string"); + enumParameter.Enum.ShouldBe(enumValues); + + + var enumParameter2 = functionDefinition.Parameters.Properties["enumParameter2"]; + enumParameter2.Description.ShouldBe("Enum Parameter 2"); + enumParameter2.Type.ShouldBe("string"); + enumParameter2.Enum.ShouldBe(enumValues); + + functionDefinition.Parameters.Properties.ShouldNotContainKey("overriddenNameParameter"); + functionDefinition.Parameters.Properties.ShouldContainKey("OverriddenName"); + } + + [Fact] + public void VerifyTypeOverride() + { + var functionDefinition = FunctionCallingHelper.GetFunctionDefinition(typeof(FunctionCallingTestClass).GetMethod("ThirdFunction")!); + + var overriddenNameParameter = functionDefinition.Parameters.Properties["overriddenTypeParameter"]; + overriddenNameParameter.Type.ShouldBe("string"); + overriddenNameParameter.Description.ShouldBe("Overridden type parameter"); + } + + [Fact] + public void VerifyGetFunctionDefinitions() + { + var functionDefinitions = FunctionCallingHelper.GetFunctionDefinitions(); + + functionDefinitions.Count.ShouldBe(3); + + var functionDefinition = functionDefinitions.First(x => x.Name == "TestFunction"); + functionDefinition.Description.ShouldBe("Test Function"); + functionDefinition.Parameters.ShouldNotBeNull(); + functionDefinition.Parameters.Properties!.Count.ShouldBe(9); + + var functionDefinition2 = functionDefinitions.First(x => x.Name == "SecondFunction"); + functionDefinition2.Description.ShouldBe("Second Function"); + functionDefinition2.Parameters.ShouldNotBeNull(); + functionDefinition2.Parameters.Properties!.Count.ShouldBe(0); + + var functionDefinition3 = functionDefinitions.First(x => x.Name == "ThirdFunction"); + functionDefinition3.Description.ShouldBe("Third Function"); + functionDefinition3.Parameters.ShouldNotBeNull(); + functionDefinition3.Parameters.Properties!.Count.ShouldBe(1); + } + + [Fact] + public void VerifyCallFunction_Simple() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "SecondFunction" + }; + + var result = FunctionCallingHelper.CallFunction(functionCall, obj); + result.ShouldBe("Hello"); + } + + [Fact] + public void VerifyCallFunction_Complex() + { + var obj = new FunctionCallingTestClass(); + + var functionCall = new FunctionCall + { + Name = "TestFunction", + // arguments is a json dictionary + Arguments = + "{\"intParameter\": 1, \"floatParameter\": 2.0, \"boolParameter\": true, \"stringParameter\": \"Hello\", \"enumParameter\": \"Value1\", \"enumParameter2\": \"Value2\", \"requiredIntParameter\": 1, \"notRequiredIntParameter\": 2, \"OverriddenName\": 3}" + }; + + var result = FunctionCallingHelper.CallFunction(functionCall, obj); + result.ShouldBe(5); + + obj.IntParameter.ShouldBe(1); + obj.FloatParameter.ShouldBe(2.0f); + obj.BoolParameter.ShouldBe(true); + obj.StringParameter.ShouldBe("Hello"); + obj.EnumParameter.ShouldBe(TestEnum.Value1); + obj.EnumParameter2.ShouldBe(TestEnum.Value2); + obj.RequiredIntParameter.ShouldBe(1); + obj.NotRequiredIntParameter.ShouldBe(2); + obj.OverriddenNameParameter.ShouldBe(3); + } + [Fact] public void VerifyCallFunction_ArgumentsDoNotMatch() { @@ -138,131 +139,140 @@ public void VerifyCallFunction_ArgumentsDoNotMatch() [Fact] public void CallFunctionShouldThrowIfObjIsNull() { - var functionCall = new FunctionCall - { - Name = "SecondFunction", - }; + var functionCall = new FunctionCall + { + Name = "SecondFunction" + }; - Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, null!)); + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, null!)); } [Fact] public void CallFunctionShouldThrowIfFunctionCallIsNull() { - var obj = new FunctionCallingTestClass(); + var obj = new FunctionCallingTestClass(); - Should.Throw(() => FunctionCallingHelper.CallFunction(null!, obj)); - } + Should.Throw(() => FunctionCallingHelper.CallFunction(null!, obj)); + } [Fact] public void CallFunctionShouldThrowIfFunctionCallNameIsNotSet() { - var obj = new FunctionCallingTestClass(); + var obj = new FunctionCallingTestClass(); - var functionCall = new FunctionCall - { - Name = null!, - }; + var functionCall = new FunctionCall + { + Name = null! + }; - Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); - } + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } [Fact] public void CallFunctionShouldThrowIfFunctionCallNameIsNotValid() { - var obj = new FunctionCallingTestClass(); + var obj = new FunctionCallingTestClass(); - var functionCall = new FunctionCall - { - Name = "NonExistentFunction", - }; + var functionCall = new FunctionCall + { + Name = "NonExistentFunction" + }; - Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); - } + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + } [Fact] public void CallFunctionShouldThrowIfInvalidReturnType() { - var obj = new FunctionCallingTestClass(); - var functionCall = new FunctionCall() - { - Name = "SecondFunction", - }; + var obj = new FunctionCallingTestClass(); + var functionCall = new FunctionCall + { + Name = "SecondFunction" + }; - Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); + Should.Throw(() => FunctionCallingHelper.CallFunction(functionCall, obj)); } [Fact] public void VerifyCallFunctionWithOverriddenType() { - var obj = new FunctionCallingTestClass(); + var obj = new FunctionCallingTestClass(); - var functionCall = new FunctionCall - { - Name = "ThirdFunction", - Arguments = "{\"overriddenTypeParameter\": 1}" - }; + var functionCall = new FunctionCall + { + Name = "ThirdFunction", + Arguments = "{\"overriddenTypeParameter\": 1}" + }; - FunctionCallingHelper.CallFunction(functionCall, obj); - obj.OverriddenTypeParameter.ShouldBe("1"); - } + FunctionCallingHelper.CallFunction(functionCall, obj); + obj.OverriddenTypeParameter.ShouldBe("1"); + } } internal class FunctionCallingTestClass { - public int IntParameter; - public float FloatParameter; - public bool BoolParameter; - public string StringParameter = null!; - public TestEnum EnumParameter; - public TestEnum EnumParameter2; - public int RequiredIntParameter; - public int? NotRequiredIntParameter; - public int OverriddenNameParameter; - public string OverriddenTypeParameter = null!; - - [FunctionDescription("Test Function")] - public int TestFunction( - [ParameterDescription("Int Parameter")] int intParameter, - [ParameterDescription("Float Parameter")] float floatParameter, - [ParameterDescription("Bool Parameter")] bool boolParameter, - [ParameterDescription("String Parameter")] string stringParameter, - [ParameterDescription(Description = "Enum Parameter", Enum = "Value1, Value2, Value3")] TestEnum enumParameter, - [ParameterDescription("Enum Parameter 2")] TestEnum enumParameter2, - [ParameterDescription(Description = "Required Int Parameter", Required= true)] int requiredIntParameter, - [ParameterDescription(Description = "Not required Int Parameter", Required = false)] int notRequiredIntParameter, - [ParameterDescription(Name = "OverriddenName", Description = "Overridden")] int overriddenNameParameter) - - { - IntParameter = intParameter; - FloatParameter = floatParameter; - BoolParameter = boolParameter; - StringParameter = stringParameter; - EnumParameter = enumParameter; - EnumParameter2 = enumParameter2; - RequiredIntParameter = requiredIntParameter; - NotRequiredIntParameter = notRequiredIntParameter; - OverriddenNameParameter = overriddenNameParameter; - - return 5; - } - - [FunctionDescription("Second Function")] - public string SecondFunction() - { - return "Hello"; - } - - [FunctionDescription("Third Function")] - public void ThirdFunction([ParameterDescription(Type = "string", Description = "Overridden type parameter")] int overriddenTypeParameter) - { - OverriddenTypeParameter = overriddenTypeParameter.ToString(); - } + public bool BoolParameter; + public TestEnum EnumParameter; + public TestEnum EnumParameter2; + public float FloatParameter; + public int IntParameter; + public int? NotRequiredIntParameter; + public int OverriddenNameParameter; + public string OverriddenTypeParameter = null!; + public int RequiredIntParameter; + public string StringParameter = null!; + + [FunctionDescription("Test Function")] + public int TestFunction( + [ParameterDescription("Int Parameter")] + int intParameter, + [ParameterDescription("Float Parameter")] + float floatParameter, + [ParameterDescription("Bool Parameter")] + bool boolParameter, + [ParameterDescription("String Parameter")] + string stringParameter, + [ParameterDescription(Description = "Enum Parameter", Enum = "Value1, Value2, Value3")] + TestEnum enumParameter, + [ParameterDescription("Enum Parameter 2")] + TestEnum enumParameter2, + [ParameterDescription(Description = "Required Int Parameter", Required = true)] + int requiredIntParameter, + [ParameterDescription(Description = "Not required Int Parameter", Required = false)] + int notRequiredIntParameter, + [ParameterDescription(Name = "OverriddenName", Description = "Overridden")] + int overriddenNameParameter) + + { + IntParameter = intParameter; + FloatParameter = floatParameter; + BoolParameter = boolParameter; + StringParameter = stringParameter; + EnumParameter = enumParameter; + EnumParameter2 = enumParameter2; + RequiredIntParameter = requiredIntParameter; + NotRequiredIntParameter = notRequiredIntParameter; + OverriddenNameParameter = overriddenNameParameter; + + return 5; + } + + [FunctionDescription("Second Function")] + public string SecondFunction() + { + return "Hello"; + } + + [FunctionDescription("Third Function")] + public void ThirdFunction([ParameterDescription(Type = "string", Description = "Overridden type parameter")] int overriddenTypeParameter) + { + OverriddenTypeParameter = overriddenTypeParameter.ToString(); + } } public enum TestEnum { - Value1, - Value2, - Value3 + Value1, + Value2, + Value3 } \ No newline at end of file diff --git a/OpenAI.Utilities/EmbedStaticValues.cs b/OpenAI.Utilities/EmbedStaticValues.cs deleted file mode 100644 index 597b0507..00000000 --- a/OpenAI.Utilities/EmbedStaticValues.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenAI.Utilities -{ - public static class EmbedStaticValues - { - public const string NTokens = "NToken"; - public const string Distances = "Distance"; - public const string Embeddings ="Embedding"; - public const string Text = "Text"; - } -} diff --git a/OpenAI.Utilities/Embedding/EmbedStaticValues.cs b/OpenAI.Utilities/Embedding/EmbedStaticValues.cs new file mode 100644 index 00000000..74092a3f --- /dev/null +++ b/OpenAI.Utilities/Embedding/EmbedStaticValues.cs @@ -0,0 +1,9 @@ +namespace OpenAI.Utilities.Embedding; + +public static class EmbedStaticValues +{ + public const string NTokens = "NToken"; + public const string Distances = "Distance"; + public const string Embeddings = "Embedding"; + public const string Text = "Text"; +} \ No newline at end of file diff --git a/OpenAI.Utilities/EmbeddingTools.cs b/OpenAI.Utilities/Embedding/EmbeddingTools.cs similarity index 99% rename from OpenAI.Utilities/EmbeddingTools.cs rename to OpenAI.Utilities/Embedding/EmbeddingTools.cs index 314d0604..e37e756e 100644 --- a/OpenAI.Utilities/EmbeddingTools.cs +++ b/OpenAI.Utilities/Embedding/EmbeddingTools.cs @@ -9,7 +9,7 @@ using OpenAI.ObjectModels.ResponseModels; using OpenAI.Tokenizer.GPT3; -namespace OpenAI.Utilities; +namespace OpenAI.Utilities.Embedding; // ReSharper disable MemberCanBePrivate.Global public interface IEmbeddingTools @@ -28,7 +28,7 @@ public interface IEmbeddingTools /// The paths to the files or directories. /// The name of the output file. /// The DataFrame containing the embedding data. - /// Thrown when no files are found in the provided paths. + /// Thrown when no files are found in the provided paths. Task ReadFilesAndCreateEmbeddingDataAsCsv(IEnumerable pathsToDirectoriesOrFiles, string outputFileName); string CreateContext(string question, DataFrame df, int maxLen = 1800); diff --git a/OpenAI.Utilities/Embedding/TextEmbeddingData.cs b/OpenAI.Utilities/Embedding/TextEmbeddingData.cs new file mode 100644 index 00000000..931db197 --- /dev/null +++ b/OpenAI.Utilities/Embedding/TextEmbeddingData.cs @@ -0,0 +1,8 @@ +namespace OpenAI.Utilities.Embedding; + +public class TextEmbeddingData +{ + public string FileName { get; set; } + public string Text { get; set; } + public int NToken { get; set; } +} \ No newline at end of file diff --git a/OpenAI.Utilities/StringExtensions.cs b/OpenAI.Utilities/Extensions/StringExtensions.cs similarity index 99% rename from OpenAI.Utilities/StringExtensions.cs rename to OpenAI.Utilities/Extensions/StringExtensions.cs index f858bfd2..f13b774e 100644 --- a/OpenAI.Utilities/StringExtensions.cs +++ b/OpenAI.Utilities/Extensions/StringExtensions.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; namespace OpenAI.Utilities; + public static partial class StringExtensions { private static readonly Regex NewlineToSpaceRegex = NewlineToSpace(); @@ -23,4 +24,4 @@ public static string RemoveNewlines(this string input) [GeneratedRegex(" {2,}")] private static partial Regex MultipleSpacesToSingleSpace(); -} +} \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCalling/FunctionCallingHelper.cs b/OpenAI.Utilities/FunctionCalling/FunctionCallingHelper.cs new file mode 100644 index 00000000..019cb228 --- /dev/null +++ b/OpenAI.Utilities/FunctionCalling/FunctionCallingHelper.cs @@ -0,0 +1,187 @@ +using System.Reflection; +using System.Text.Json; +using OpenAI.Builders; +using OpenAI.ObjectModels.RequestModels; +using OpenAI.ObjectModels.SharedModels; + +namespace OpenAI.Utilities.FunctionCalling; + +/// +/// Helper methods for Function Calling +/// +public static class FunctionCallingHelper +{ + /// + /// Returns a from the provided method, using any + /// and attributes + /// + /// the method to create the from + /// the created. + public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) + { + var methodDescriptionAttribute = methodInfo.GetCustomAttribute(); + + var result = new FunctionDefinitionBuilder( + methodDescriptionAttribute?.Name ?? methodInfo.Name, methodDescriptionAttribute?.Description); + + var parameters = methodInfo.GetParameters().ToList(); + + foreach (var parameter in parameters) + { + var parameterDescriptionAttribute = parameter.GetCustomAttribute(); + var description = parameterDescriptionAttribute?.Description; + + PropertyDefinition definition; + + switch (parameter.ParameterType, parameterDescriptionAttribute?.Type == null) + { + case (_, false): + definition = new PropertyDefinition + { + Type = parameterDescriptionAttribute!.Type!, + Description = description + }; + + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(int)): + definition = PropertyDefinition.DefineInteger(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(float)): + definition = PropertyDefinition.DefineNumber(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(bool)): + definition = PropertyDefinition.DefineBoolean(description); + break; + case ({ } t, _) when t.IsAssignableFrom(typeof(string)): + definition = PropertyDefinition.DefineString(description); + break; + case ({IsEnum: true}, _): + + var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) + ? Enum.GetNames(parameter.ParameterType).ToList() + : parameterDescriptionAttribute.Enum.Split(",").Select(x => x.Trim()).ToList(); + + definition = + PropertyDefinition.DefineEnum(enumValues, description); + + break; + default: + throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); + } + + result.AddParameter( + parameterDescriptionAttribute?.Name ?? parameter.Name!, + definition, + parameterDescriptionAttribute?.Required ?? true); + } + + return result.Build(); + } + + /// + /// Enumerates the methods in the provided object, and a returns a of + /// for all methods + /// marked with a + /// + /// the object to analyze + public static List GetFunctionDefinitions(object obj) + { + var type = obj.GetType(); + return GetFunctionDefinitions(type); + } + + /// + /// Enumerates the methods in the provided type, and a returns a of + /// for all methods + /// + /// The type to analyze + /// + public static List GetFunctionDefinitions() + { + return GetFunctionDefinitions(typeof(T)); + } + + /// + /// Enumerates the methods in the provided type, and a returns a of + /// for all methods + /// + /// The type to analyze + public static List GetFunctionDefinitions(Type type) + { + var methods = type.GetMethods(); + + var result = methods + .Select(method => new + { + method, + methodDescriptionAttribute = method.GetCustomAttribute() + }) + .Where(t => t.methodDescriptionAttribute != null) + .Select(t => GetFunctionDefinition(t.method)).ToList(); + + return result; + } + + /// + /// Calls the function on the provided object, using the provided and returns the result of + /// the call + /// + /// The FunctionCall provided by the LLM + /// the object with the method / function to be executed + /// The return type + public static T? CallFunction(FunctionCall functionCall, object obj) + { + if (functionCall == null) + { + throw new ArgumentNullException(nameof(functionCall)); + } + + if (functionCall.Name == null) + { + throw new InvalidFunctionCallException("Function Name is null"); + } + + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + var methodInfo = obj.GetType().GetMethod(functionCall.Name); + + if (methodInfo == null) + { + throw new InvalidFunctionCallException($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); + } + + if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) + { + throw new InvalidFunctionCallException( + $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); + } + + var parameters = methodInfo.GetParameters().ToList(); + var arguments = functionCall.ParseArguments(); + var args = new List(); + + foreach (var parameter in parameters) + { + var parameterDescriptionAttribute = + parameter.GetCustomAttribute(); + + var name = parameterDescriptionAttribute?.Name ?? parameter.Name!; + var argument = arguments.FirstOrDefault(x => x.Key == name); + + if (argument.Key == null) + { + throw new Exception($"Argument '{name}' not found"); + } + + var value = parameter.ParameterType.IsEnum ? Enum.Parse(parameter.ParameterType, argument.Value.ToString()!) : ((JsonElement) argument.Value).Deserialize(parameter.ParameterType); + + args.Add(value); + } + + var result = (T?) methodInfo.Invoke(obj, args.ToArray()); + return result; + } +} \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCalling/FunctionDescriptionAttribute.cs b/OpenAI.Utilities/FunctionCalling/FunctionDescriptionAttribute.cs new file mode 100644 index 00000000..9b4d0d6f --- /dev/null +++ b/OpenAI.Utilities/FunctionCalling/FunctionDescriptionAttribute.cs @@ -0,0 +1,27 @@ +namespace OpenAI.Utilities.FunctionCalling; + +/// +/// Attribute to mark a method as a function, and provide a description for the function. Can also be used to override +/// the Name of the function +/// +[AttributeUsage(AttributeTargets.Method)] +public class FunctionDescriptionAttribute : Attribute +{ + /// + /// Creates a new instance of the with the provided description + /// + public FunctionDescriptionAttribute(string? description = null) + { + Description = description; + } + + /// + /// Name of the function. If not provided, the name of the method will be used. + /// + public string? Name { get; set; } + + /// + /// Description of the function + /// + public string? Description { get; set; } +} \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCalling/InvalidFunctionCallException.cs b/OpenAI.Utilities/FunctionCalling/InvalidFunctionCallException.cs new file mode 100644 index 00000000..ddb75e46 --- /dev/null +++ b/OpenAI.Utilities/FunctionCalling/InvalidFunctionCallException.cs @@ -0,0 +1,14 @@ +namespace OpenAI.Utilities.FunctionCalling; + +/// +/// Exception thrown when a function call is invalid +/// +public class InvalidFunctionCallException : Exception +{ + /// + /// Creates a new instance of the with the provided message + /// + public InvalidFunctionCallException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCalling/ParameterDescriptionAttribute.cs b/OpenAI.Utilities/FunctionCalling/ParameterDescriptionAttribute.cs new file mode 100644 index 00000000..748facb8 --- /dev/null +++ b/OpenAI.Utilities/FunctionCalling/ParameterDescriptionAttribute.cs @@ -0,0 +1,42 @@ +namespace OpenAI.Utilities.FunctionCalling; + +/// +/// Attribute to describe a parameter of a function. Can also be used to override the Name of the parameter +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class ParameterDescriptionAttribute : Attribute +{ + /// + /// Creates a new instance of the with the provided description + /// + public ParameterDescriptionAttribute(string? description = null) + { + Description = description; + } + + /// + /// Name of the parameter. If not provided, the name of the parameter will be used. + /// + public string? Name { get; set; } + + /// + /// Description of the parameter + /// + public string? Description { get; set; } + + /// + /// Type of the parameter. If not provided, the type of the parameter will be inferred from the parameter type + /// + public string? Type { get; set; } + + /// + /// Enum values of the parameter in a comma separated string. If not provided, the enum values will be inferred from + /// the parameter type + /// + public string? Enum { get; set; } + + /// + /// Whether the parameter is required. If not provided, the parameter will be required. Default is true + /// + public bool Required { get; set; } = true; +} \ No newline at end of file diff --git a/OpenAI.Utilities/FunctionCallingHelper.cs b/OpenAI.Utilities/FunctionCallingHelper.cs deleted file mode 100644 index 90e088a6..00000000 --- a/OpenAI.Utilities/FunctionCallingHelper.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using OpenAI.Builders; -using OpenAI.ObjectModels.RequestModels; -using OpenAI.ObjectModels.SharedModels; - -namespace OpenAI.Utilities; - -/// -/// Helper methods for Function Calling -/// -public static class FunctionCallingHelper -{ - /// - /// Returns a from the provided method, using any and attributes - /// - /// the method to create the from - /// the created. - public static FunctionDefinition GetFunctionDefinition(MethodInfo methodInfo) - { - var methodDescriptionAttribute = methodInfo.GetCustomAttribute(); - - var result = new FunctionDefinitionBuilder( - methodDescriptionAttribute?.Name ?? methodInfo.Name, methodDescriptionAttribute?.Description); - - var parameters = methodInfo.GetParameters().ToList(); - - foreach (var parameter in parameters) - { - var parameterDescriptionAttribute = parameter.GetCustomAttribute(); - var description = parameterDescriptionAttribute?.Description; - - PropertyDefinition definition; - - switch (parameter.ParameterType, parameterDescriptionAttribute?.Type == null) - { - case (_, false): - definition = new PropertyDefinition() - { - Type = parameterDescriptionAttribute!.Type!, - Description = description, - }; - - break; - case ({ } t, _) when t.IsAssignableFrom(typeof(int)): - definition = PropertyDefinition.DefineInteger(description); - break; - case ({ } t, _) when t.IsAssignableFrom(typeof(float)): - definition = PropertyDefinition.DefineNumber(description); - break; - case ({ } t, _) when t.IsAssignableFrom(typeof(bool)): - definition = PropertyDefinition.DefineBoolean(description); - break; - case ({ } t, _) when t.IsAssignableFrom(typeof(string)): - definition = PropertyDefinition.DefineString(description); - break; - case ({ IsEnum: true }, _): - - var enumValues = string.IsNullOrEmpty(parameterDescriptionAttribute?.Enum) - ? Enum.GetNames(parameter.ParameterType).ToList() - : parameterDescriptionAttribute.Enum.Split(",").Select(x => x.Trim()).ToList(); - - definition = - PropertyDefinition.DefineEnum(enumValues, description); - - break; - default: - throw new Exception($"Parameter type '{parameter.ParameterType}' not supported"); - } - - result.AddParameter( - parameterDescriptionAttribute?.Name ?? parameter.Name!, - definition, - parameterDescriptionAttribute?.Required ?? true); - } - - return result.Build(); - } - - /// - /// Enumerates the methods in the provided object, and a returns a of for all methods - /// marked with a - /// - /// the object to analyze - public static List GetFunctionDefinitions(object obj) - { - var type = obj.GetType(); - return GetFunctionDefinitions(type); - } - - /// - /// Enumerates the methods in the provided type, and a returns a of for all methods - /// - /// The type to analyze - /// - public static List GetFunctionDefinitions() - { - return GetFunctionDefinitions(typeof(T)); - } - - /// - /// Enumerates the methods in the provided type, and a returns a of for all methods - /// - /// The type to analyze - public static List GetFunctionDefinitions(Type type) - { - var methods = type.GetMethods(); - - var result = methods - .Select(method => new - { - method, - methodDescriptionAttribute = method.GetCustomAttribute() - }) - .Where(@t => @t.methodDescriptionAttribute != null) - .Select(@t => GetFunctionDefinition(@t.method)).ToList(); - - return result; - } - - /// - /// Calls the function on the provided object, using the provided and returns the result of the call - /// - /// The FunctionCall provided by the LLM - /// the object with the method / function to be executed - /// The return type - public static T? CallFunction(FunctionCall functionCall, object obj) - { - if (functionCall == null) - throw new ArgumentNullException(nameof(functionCall)); - - if (functionCall.Name == null) - throw new InvalidFunctionCallException("Function Name is null"); - - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - var methodInfo = obj.GetType().GetMethod(functionCall.Name); - - if (methodInfo == null) - throw new InvalidFunctionCallException($"Method '{functionCall.Name}' on type '{obj.GetType()}' not found"); - - if (!methodInfo.ReturnType.IsAssignableTo(typeof(T))) - throw new InvalidFunctionCallException( - $"Method '{functionCall.Name}' on type '{obj.GetType()}' has return type '{methodInfo.ReturnType}' but expected '{typeof(T)}'"); - - var parameters = methodInfo.GetParameters().ToList(); - var arguments = functionCall.ParseArguments(); - var args = new List(); - - foreach (var parameter in parameters) - { - ParameterDescriptionAttribute? parameterDescriptionAttribute = - parameter.GetCustomAttribute(); - - var name = parameterDescriptionAttribute?.Name ?? parameter.Name!; - var argument = arguments.FirstOrDefault(x => x.Key == name); - - if (argument.Key == null) - throw new Exception($"Argument '{name}' not found"); - - var value = parameter.ParameterType.IsEnum ? - Enum.Parse(parameter.ParameterType, argument.Value.ToString()!) : - ((JsonElement)argument.Value).Deserialize(parameter.ParameterType); - - args.Add(value); - } - - T? result = (T?)methodInfo.Invoke(obj, args.ToArray()); - return result; - } -} - -/// -/// Exception thrown when a function call is invalid -/// -public class InvalidFunctionCallException : Exception -{ - /// - /// Creates a new instance of the with the provided message - /// - public InvalidFunctionCallException(string message) : base(message) - { } -} - -/// -/// Attribute to mark a method as a function, and provide a description for the function. Can also be used to override the Name of the function -/// -[AttributeUsage(AttributeTargets.Method)] -public class FunctionDescriptionAttribute : Attribute -{ - /// - /// Name of the function. If not provided, the name of the method will be used. - /// - public string? Name { get; set; } - /// - /// Description of the function - /// - public string? Description { get; set; } - - /// - /// Creates a new instance of the with the provided description - /// - public FunctionDescriptionAttribute(string? description = null) - { - Description = description; - } -} - -/// -/// Attribute to describe a parameter of a function. Can also be used to override the Name of the parameter -/// -[AttributeUsage(AttributeTargets.Parameter)] -public class ParameterDescriptionAttribute : Attribute -{ - /// - /// Name of the parameter. If not provided, the name of the parameter will be used. - /// - public string? Name { get; set; } - /// - /// Description of the parameter - /// - public string? Description { get; set; } - /// - /// Type of the parameter. If not provided, the type of the parameter will be inferred from the parameter type - /// - public string? Type { get; set; } - /// - /// Enum values of the parameter in a comma separated string. If not provided, the enum values will be inferred from the parameter type - /// - public string? Enum { get; set; } - /// - /// Whether the parameter is required. If not provided, the parameter will be required. Default is true - /// - public bool Required { get; set; } = true; - - /// - /// Creates a new instance of the with the provided description - /// - public ParameterDescriptionAttribute(string? description = null) - { - Description = description; - } -} - diff --git a/OpenAI.Utilities/OpenAI.Utilities.csproj b/OpenAI.Utilities/OpenAI.Utilities.csproj index 21128fe1..cfefa8a1 100644 --- a/OpenAI.Utilities/OpenAI.Utilities.csproj +++ b/OpenAI.Utilities/OpenAI.Utilities.csproj @@ -10,7 +10,7 @@ https://openai.com/ OpenAI-Betalgo.png true - 7.0.1 + 7.0.2 Tolga Kayhan, Betalgo Betalgo Up Ltd. Utility tools for Betalgo.OpenAI @@ -41,13 +41,9 @@ + - - - - - \ No newline at end of file diff --git a/OpenAI.Utilities/TextEmbeddingData.cs b/OpenAI.Utilities/TextEmbeddingData.cs deleted file mode 100644 index 3ed491ba..00000000 --- a/OpenAI.Utilities/TextEmbeddingData.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OpenAI.Utilities -{ - public class TextEmbeddingData - { - public string FileName { get; set; } - public string Text { get; set; } - public int NToken { get; set; } - } -} diff --git a/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs b/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs index cb2c31a5..3b9b1edd 100644 --- a/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs +++ b/OpenAI.UtilitiesPlayground/TestHelpers/EmbeddingTestHelpers.cs @@ -1,7 +1,7 @@ using OpenAI.Interfaces; using OpenAI.ObjectModels; using OpenAI.ObjectModels.RequestModels; -using OpenAI.Utilities; +using OpenAI.Utilities.Embedding; namespace OpenAI.UtilitiesPlayground.TestHelpers; diff --git a/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs b/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs index 123a7fb5..aed7c958 100644 --- a/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs +++ b/OpenAI.UtilitiesPlayground/TestHelpers/FunctionCallingTestHelpers.cs @@ -2,7 +2,7 @@ using OpenAI.Interfaces; using OpenAI.ObjectModels; using OpenAI.ObjectModels.RequestModels; -using OpenAI.Utilities; +using OpenAI.Utilities.FunctionCalling; namespace OpenAI.UtilitiesPlayground.TestHelpers; @@ -56,14 +56,26 @@ public static async Task ExerciseFunctionCalling(IOpenAIService openAIService) } 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) => a + b; + public float Add(float a, float b) + { + return a + b; + } [FunctionDescription("Subtracts two numbers.")] - public float Subtract(float a, float b) => a - b; + 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) @@ -75,10 +87,5 @@ public float AdvancedMath(float a, float b, AdvancedOperators advancedOperator) _ => throw new ArgumentOutOfRangeException(nameof(advancedOperator), advancedOperator, null) }; } - - public enum AdvancedOperators - { - Multiply, Divide - } } } \ No newline at end of file