diff --git a/.github/workflows/Publish-Nuget.yml b/.github/workflows/Publish-Nuget.yml index 17e408cd..2aa79d96 100644 --- a/.github/workflows/Publish-Nuget.yml +++ b/.github/workflows/Publish-Nuget.yml @@ -27,7 +27,9 @@ on: permissions: contents: read pages: write + checks: write id-token: write + pull-requests: write concurrency: group: ${{ github.ref }} diff --git a/OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs b/OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs index 28f83732..7b1586df 100644 --- a/OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs +++ b/OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs @@ -26,7 +26,7 @@ public class OpenAIProxyStartup private IAuthenticationFilter authenticationFilter; // Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83 - private static readonly HashSet ExcludedHeaders = new HashSet + private static readonly HashSet ExcludedHeaders = new() { HeaderNames.Connection, HeaderNames.TransferEncoding, diff --git a/OpenAI-DotNet-Tests-Proxy/Program.cs b/OpenAI-DotNet-Tests-Proxy/Program.cs index 2000c16c..b0f01e1c 100644 --- a/OpenAI-DotNet-Tests-Proxy/Program.cs +++ b/OpenAI-DotNet-Tests-Proxy/Program.cs @@ -1,3 +1,5 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using OpenAI.Proxy; diff --git a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs index 8c589730..ede548af 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs @@ -1,3 +1,5 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + using NUnit.Framework; using System; using System.IO; diff --git a/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs index f962fcde..9a2b063b 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using System; using System.Net; using System.Net.Http; diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs new file mode 100644 index 00000000..52bd32c1 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs @@ -0,0 +1,40 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; +using System; + +namespace OpenAI.Tests +{ + internal class TestFixture_00_02_Extensions + { + [Test] + public void Test_01_Tools() + { + var tools = Tool.GetAllAvailableTools(); + + for (var i = 0; i < tools.Count; i++) + { + var tool = tools[i]; + + if (tool.Type != "function") + { + Console.Write($" \"{tool.Type}\""); + } + else + { + Console.Write($" \"{tool.Function.Name}\""); + } + + if (tool.Function?.Parameters != null) + { + Console.Write($": {tool.Function.Parameters}"); + } + + if (i < tools.Count - 1) + { + Console.Write(",\n"); + } + } + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestFixture_01_Models.cs b/OpenAI-DotNet-Tests/TestFixture_01_Models.cs index 1f045ee1..572da802 100644 --- a/OpenAI-DotNet-Tests/TestFixture_01_Models.cs +++ b/OpenAI-DotNet-Tests/TestFixture_01_Models.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using System; using System.Linq; using System.Threading.Tasks; diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index 991d1a87..c7fddc48 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -1,3 +1,5 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + using NUnit.Framework; using OpenAI.Chat; using OpenAI.Models; @@ -5,8 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; using System.Threading.Tasks; namespace OpenAI.Tests @@ -19,10 +19,10 @@ public async Task Test_01_01_GetChatCompletion() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, "Who won the world series in 2020?"), - new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), - new Message(Role.User, "Where was it played?"), + new(Role.System, "You are a helpful assistant."), + new(Role.User, "Who won the world series in 2020?"), + new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), + new(Role.User, "Where was it played?"), }; var chatRequest = new ChatRequest(messages, Model.GPT4); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); @@ -44,10 +44,10 @@ public async Task Test_01_02_GetChatStreamingCompletion() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, "Who won the world series in 2020?"), - new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), - new Message(Role.User, "Where was it played?"), + new(Role.System, "You are a helpful assistant."), + new(Role.User, "Who won the world series in 2020?"), + new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), + new(Role.User, "Where was it played?"), }; var chatRequest = new ChatRequest(messages); var cumulativeDelta = string.Empty; @@ -81,10 +81,10 @@ public async Task Test_01_03_JsonMode() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant designed to output JSON."), - new Message(Role.User, "Who won the world series in 2020?"), + new(Role.System, "You are a helpful assistant designed to output JSON."), + new(Role.User, "Who won the world series in 2020?"), }; - var chatRequest = new ChatRequest(messages, "gpt-4-1106-preview", responseFormat: ChatResponseFormat.Json); + var chatRequest = new ChatRequest(messages, "gpt-4-turbo-preview", responseFormat: ChatResponseFormat.Json); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); @@ -104,10 +104,10 @@ public async Task Test_01_04_GetChatStreamingCompletionEnumerableAsync() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, "Who won the world series in 2020?"), - new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), - new Message(Role.User, "Where was it played?"), + new(Role.System, "You are a helpful assistant."), + new(Role.User, "Who won the world series in 2020?"), + new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), + new(Role.User, "Where was it played?"), }; var cumulativeDelta = string.Empty; var chatRequest = new ChatRequest(messages); @@ -133,8 +133,8 @@ public async Task Test_02_01_GetChatToolCompletion() var messages = new List { - new Message(Role.System, "You are a helpful weather assistant."), - new Message(Role.User, "What's the weather like today?"), + new(Role.System, "You are a helpful weather assistant."), + new(Role.User, "What's the weather like today?"), }; foreach (var message in messages) @@ -142,30 +142,7 @@ public async Task Test_02_01_GetChatToolCompletion() Console.WriteLine($"{message.Role}: {message.Content}"); } - var tools = new List - { - new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray {"celsius", "fahrenheit"} - } - }, - ["required"] = new JsonArray { "location", "unit" } - }) - }; + var tools = Tool.GetAllAvailableTools(false); var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); @@ -190,7 +167,7 @@ public async Task Test_02_01_GetChatToolCompletion() { Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); - var unitMessage = new Message(Role.User, "celsius"); + var unitMessage = new Message(Role.User, "Fahrenheit"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); @@ -203,11 +180,10 @@ public async Task Test_02_01_GetChatToolCompletion() Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls"); var usedTool = response.FirstChoice.Message.ToolCalls[0]; Assert.IsNotNull(usedTool); - Assert.IsTrue(usedTool.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync))); Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Console.WriteLine($"{usedTool.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); - var functionResult = WeatherService.GetCurrentWeather(functionArgs); + var functionResult = await usedTool.InvokeFunctionAsync(); Assert.IsNotNull(functionResult); messages.Add(new Message(usedTool, functionResult)); Console.WriteLine($"{Role.Tool}: {functionResult}"); @@ -222,8 +198,8 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful weather assistant."), - new Message(Role.User, "What's the weather like today?"), + new(Role.System, "You are a helpful weather assistant."), + new(Role.User, "What's the weather like today?"), }; foreach (var message in messages) @@ -231,30 +207,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming() Console.WriteLine($"{message.Role}: {message.Content}"); } - var tools = new List - { - new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray {"celsius", "fahrenheit"} - } - }, - ["required"] = new JsonArray { "location", "unit" } - }) - }; + var tools = Tool.GetAllAvailableTools(false); var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { @@ -286,7 +239,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming() { Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); - var unitMessage = new Message(Role.User, "celsius"); + var unitMessage = new Message(Role.User, "Fahrenheit"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); @@ -304,12 +257,10 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming() Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls"); var usedTool = response.FirstChoice.Message.ToolCalls[0]; Assert.IsNotNull(usedTool); - Assert.IsTrue(usedTool.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync))); Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Console.WriteLine($"{usedTool.Function.Arguments}"); - - var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); - var functionResult = WeatherService.GetCurrentWeather(functionArgs); + var functionResult = await usedTool.InvokeFunctionAsync(); Assert.IsNotNull(functionResult); messages.Add(new Message(usedTool, functionResult)); Console.WriteLine($"{Role.Tool}: {functionResult}"); @@ -330,36 +281,12 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful weather assistant."), - new Message(Role.User, "What's the weather like today in San Diego and LA?"), + new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."), + new(Role.User, "What's the weather like today in Los Angeles, USA and Tokyo, Japan?"), }; - var tools = new List - { - new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray { "celsius", "fahrenheit" } - } - }, - ["required"] = new JsonArray { "location", "unit" } - }) - }; - - var chatRequest = new ChatRequest(messages, model: "gpt-4-1106-preview", tools: tools, toolChoice: "auto"); + var tools = Tool.GetAllAvailableTools(false); + var chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "auto"); var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); @@ -376,10 +303,11 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming() foreach (var toolCall in toolCalls) { - messages.Add(new Message(toolCall, "Sunny!")); + var output = await toolCall.InvokeFunctionAsync(); + messages.Add(new Message(toolCall, output)); } - chatRequest = new ChatRequest(messages, model: "gpt-4-1106-preview", tools: tools, toolChoice: "auto"); + chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "auto"); response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); @@ -391,8 +319,8 @@ public async Task Test_02_04_GetChatToolForceCompletion() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful weather assistant."), - new Message(Role.User, "What's the weather like today?"), + new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."), + new(Role.User, "What's the weather like today?"), }; foreach (var message in messages) @@ -400,30 +328,7 @@ public async Task Test_02_04_GetChatToolForceCompletion() Console.WriteLine($"{message.Role}: {message.Content}"); } - var tools = new List - { - new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray {"celsius", "fahrenheit"} - } - }, - ["required"] = new JsonArray { "location", "unit" } - }) - }; + var tools = Tool.GetAllAvailableTools(false); var chatRequest = new ChatRequest(messages, tools: tools); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); @@ -433,13 +338,13 @@ public async Task Test_02_04_GetChatToolForceCompletion() Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); - var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); + var locationMessage = new Message(Role.User, "I'm in New York, USA"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest( messages, tools: tools, - toolChoice: nameof(WeatherService.GetCurrentWeather)); + toolChoice: nameof(WeatherService.GetCurrentWeatherAsync)); response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); @@ -450,11 +355,10 @@ public async Task Test_02_04_GetChatToolForceCompletion() Assert.IsTrue(response.FirstChoice.FinishReason == "stop"); var usedTool = response.FirstChoice.Message.ToolCalls[0]; Assert.IsNotNull(usedTool); - Assert.IsTrue(usedTool.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync))); Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Console.WriteLine($"{usedTool.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); - var functionResult = WeatherService.GetCurrentWeather(functionArgs); + var functionResult = await usedTool.InvokeFunctionAsync(); Assert.IsNotNull(functionResult); messages.Add(new Message(usedTool, functionResult)); Console.WriteLine($"{Role.Tool}: {functionResult}"); @@ -466,8 +370,8 @@ public async Task Test_03_01_GetChatVision() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, new List + new(Role.System, "You are a helpful assistant."), + new(Role.User, new List { "What's in this image?", new ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageDetail.Low) @@ -487,8 +391,8 @@ public async Task Test_03_02_GetChatVisionStreaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, new List + new(Role.System, "You are a helpful assistant."), + new(Role.User, new List { "What's in this image?", new ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageDetail.Low) @@ -513,10 +417,10 @@ public async Task Test_04_01_GetChatLogProbs() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, "Who won the world series in 2020?"), - new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), - new Message(Role.User, "Where was it played?"), + new(Role.System, "You are a helpful assistant."), + new(Role.User, "Who won the world series in 2020?"), + new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), + new(Role.User, "Where was it played?"), }; var chatRequest = new ChatRequest(messages, Model.GPT3_5_Turbo, topLogProbs: 1); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); @@ -538,10 +442,10 @@ public async Task Test_04_02_GetChatLogProbsSteaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new Message(Role.System, "You are a helpful assistant."), - new Message(Role.User, "Who won the world series in 2020?"), - new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), - new Message(Role.User, "Where was it played?"), + new(Role.System, "You are a helpful assistant."), + new(Role.User, "Who won the world series in 2020?"), + new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), + new(Role.User, "Where was it played?"), }; var chatRequest = new ChatRequest(messages, topLogProbs: 1); var cumulativeDelta = string.Empty; diff --git a/OpenAI-DotNet-Tests/TestFixture_05_Images.cs b/OpenAI-DotNet-Tests/TestFixture_05_Images.cs index a6c760c2..31989e02 100644 --- a/OpenAI-DotNet-Tests/TestFixture_05_Images.cs +++ b/OpenAI-DotNet-Tests/TestFixture_05_Images.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Images; using OpenAI.Models; using System; diff --git a/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs b/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs index 050f3d14..b6590572 100644 --- a/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs +++ b/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using System.Threading.Tasks; using OpenAI.Models; @@ -14,7 +16,7 @@ public async Task Test_1_CreateEmbedding() Assert.IsNotNull(embedding); Assert.IsNotEmpty(embedding.Data); } - + [Test] public async Task Test_2_CreateEmbeddingWithDimensions() { diff --git a/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs b/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs index d5f92029..0673677a 100644 --- a/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs +++ b/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Audio; using System; using System.IO; diff --git a/OpenAI-DotNet-Tests/TestFixture_08_Files.cs b/OpenAI-DotNet-Tests/TestFixture_08_Files.cs index b869ccc2..63bcd64c 100644 --- a/OpenAI-DotNet-Tests/TestFixture_08_Files.cs +++ b/OpenAI-DotNet-Tests/TestFixture_08_Files.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Chat; using System; using System.Collections.Generic; @@ -13,7 +15,7 @@ internal class TestFixture_08_Files : AbstractTestFixture public async Task Test_01_UploadFile() { Assert.IsNotNull(OpenAIClient.FilesEndpoint); - var testData = new Conversation(new List { new Message(Role.Assistant, "I'm a learning language model") }); + var testData = new Conversation(new List { new(Role.Assistant, "I'm a learning language model") }); await File.WriteAllTextAsync("test.jsonl", testData); Assert.IsTrue(File.Exists("test.jsonl")); var result = await OpenAIClient.FilesEndpoint.UploadFileAsync("test.jsonl", "fine-tune"); diff --git a/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs b/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs index bd77f2a5..e9bf5814 100644 --- a/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs +++ b/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Chat; using OpenAI.Files; using OpenAI.FineTuning; @@ -17,65 +19,65 @@ private async Task CreateTestTrainingDataAsync() { var conversations = new List { - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "What's the capital of France?"), - new Message(Role.Assistant, "Paris, as if everyone doesn't know that already.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "What's the capital of France?"), + new(Role.Assistant, "Paris, as if everyone doesn't know that already.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "Who wrote 'Romeo and Juliet'?"), - new Message(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "Who wrote 'Romeo and Juliet'?"), + new(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "How far is the Moon from Earth?"), - new Message(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "How far is the Moon from Earth?"), + new(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "What's the capital of France?"), - new Message(Role.Assistant, "Paris, as if everyone doesn't know that already.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "What's the capital of France?"), + new(Role.Assistant, "Paris, as if everyone doesn't know that already.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "Who wrote 'Romeo and Juliet'?"), - new Message(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "Who wrote 'Romeo and Juliet'?"), + new(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "How far is the Moon from Earth?"), - new Message(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "How far is the Moon from Earth?"), + new(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "What's the capital of France?"), - new Message(Role.Assistant, "Paris, as if everyone doesn't know that already.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "What's the capital of France?"), + new(Role.Assistant, "Paris, as if everyone doesn't know that already.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "Who wrote 'Romeo and Juliet'?"), - new Message(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "Who wrote 'Romeo and Juliet'?"), + new(Role.Assistant, "Oh, just some guy named William Shakespeare. Ever heard of him?") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "How far is the Moon from Earth?"), - new Message(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "How far is the Moon from Earth?"), + new(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") }), - new Conversation(new List + new(new List { - new Message(Role.System, "Marv is a factual chatbot that is also sarcastic."), - new Message(Role.User, "How far is the Moon from Earth?"), - new Message(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") + new(Role.System, "Marv is a factual chatbot that is also sarcastic."), + new(Role.User, "How far is the Moon from Earth?"), + new(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") }) }; const string localTrainingDataPath = "fineTunesTestTrainingData.jsonl"; diff --git a/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs b/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs index 632d30a9..9fbfe2e2 100644 --- a/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs +++ b/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Moderations; using System; using System.Threading.Tasks; diff --git a/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs b/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs index 28bed9aa..bf0df605 100644 --- a/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs +++ b/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Assistants; using System; using System.Collections.Generic; @@ -21,7 +23,7 @@ public async Task Test_01_CreateAssistant() var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(testFilePath, "assistants"); File.Delete(testFilePath); Assert.IsFalse(File.Exists(testFilePath)); - var request = new CreateAssistantRequest("gpt-3.5-turbo-1106", + var request = new CreateAssistantRequest("gpt-3.5-turbo", name: "test-assistant", description: "Used for unit testing.", instructions: "You are test assistant", @@ -40,7 +42,7 @@ public async Task Test_01_CreateAssistant() Assert.AreEqual("test-assistant", assistant.Name); Assert.AreEqual("Used for unit testing.", assistant.Description); Assert.AreEqual("You are test assistant", assistant.Instructions); - Assert.AreEqual("gpt-3.5-turbo-1106", assistant.Model); + Assert.AreEqual("gpt-3.5-turbo", assistant.Model); Assert.IsNotEmpty(assistant.Metadata); testAssistant = assistant; Console.WriteLine($"{assistant} -> {assistant.Metadata["test"]}"); @@ -68,7 +70,7 @@ public async Task Test_03_ModifyAssistants() Assert.IsNotNull(testAssistant); Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); var request = new CreateAssistantRequest( - model: "gpt-4-1106-preview", + model: "gpt-4-turbo-preview", name: "Test modified", description: "Modified description", instructions: "You are modified test assistant"); @@ -77,7 +79,7 @@ public async Task Test_03_ModifyAssistants() Assert.AreEqual("Test modified", assistant.Name); Assert.AreEqual("Modified description", assistant.Description); Assert.AreEqual("You are modified test assistant", assistant.Instructions); - Assert.AreEqual("gpt-4-1106-preview", assistant.Model); + Assert.AreEqual("gpt-4-turbo-preview", assistant.Model); Assert.IsTrue(assistant.Metadata.ContainsKey("test")); Console.WriteLine($"{assistant.Id} -> modified"); } diff --git a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs index 327d1cbd..c3d67f9c 100644 --- a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs +++ b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; using OpenAI.Assistants; using OpenAI.Files; using OpenAI.Tests.Weather; @@ -7,8 +9,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; using System.Threading.Tasks; namespace OpenAI.Tests @@ -216,7 +216,7 @@ public async Task Test_06_01_CreateRun() new CreateAssistantRequest( name: "Math Tutor", instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", - model: "gpt-4-1106-preview")); + model: "gpt-4-turbo-preview")); Assert.NotNull(assistant); testAssistant = assistant; var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(); @@ -344,29 +344,13 @@ public async Task Test_06_06_TestCleanup() [Test] public async Task Test_07_01_SubmitToolOutput() { - var function = new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray { "celsius", "fahrenheit" } - } - }, - ["required"] = new JsonArray { "location", "unit" } - }); - testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(new CreateAssistantRequest(tools: new Tool[] { function })); - var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature in celsius now?"); + var tools = new List + { + Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync)) + }; + var assistantRequest = new CreateAssistantRequest(tools: tools, instructions: "You are a helpful weather assistant. Use the appropriate unit based on geographical location."); + testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(assistantRequest); + var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature now?"); testThread = await run.GetThreadAsync(); // waiting while run is Queued and InProgress run = await run.WaitForStatusChangeAsync(); @@ -394,13 +378,17 @@ public async Task Test_07_01_SubmitToolOutput() var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0]; Assert.AreEqual("function", toolCall.Type); Assert.IsNotNull(toolCall.FunctionCall); - Assert.AreEqual(nameof(WeatherService.GetCurrentWeather), toolCall.FunctionCall.Name); + Assert.IsTrue(toolCall.FunctionCall.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync))); Assert.IsNotNull(toolCall.FunctionCall.Arguments); Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(toolCall.FunctionCall.Arguments); - var functionResult = WeatherService.GetCurrentWeather(functionArgs); - var toolOutput = new ToolOutput(toolCall.Id, functionResult); - run = await run.SubmitToolOutputsAsync(toolOutput); + var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls); + + foreach (var toolOutput in toolOutputs) + { + Console.WriteLine($"tool call output: {toolOutput.Output}"); + } + + run = await run.SubmitToolOutputsAsync(toolOutputs); // waiting while run in Queued and InProgress run = await run.WaitForStatusChangeAsync(); Assert.AreEqual(RunStatus.Completed, run.Status); diff --git a/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs b/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs deleted file mode 100644 index 94d53cf6..00000000 --- a/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace OpenAI.Tests.Weather -{ - internal class WeatherArgs - { - [JsonPropertyName("location")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string Location { get; set; } - - [JsonPropertyName("unit")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string Unit { get; set; } - } -} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestServices/WeatherService.cs b/OpenAI-DotNet-Tests/TestServices/WeatherService.cs index eb8c962d..a54327bc 100644 --- a/OpenAI-DotNet-Tests/TestServices/WeatherService.cs +++ b/OpenAI-DotNet-Tests/TestServices/WeatherService.cs @@ -1,8 +1,32 @@ -namespace OpenAI.Tests.Weather +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace OpenAI.Tests.Weather { - internal class WeatherService + internal static class WeatherService { - public static string GetCurrentWeather(WeatherArgs weatherArgs) - => $"The current weather in {weatherArgs.Location} is 20 {weatherArgs.Unit}"; + internal enum WeatherUnit + { + Celsius, + Fahrenheit + } + + [Function("Get the current weather in a given location")] + public static async Task GetCurrentWeatherAsync(string location, WeatherUnit unit) + { + var temp = new Random().Next(-10, 40); + + temp = unit switch + { + WeatherUnit.Fahrenheit => CelsiusToFahrenheit(temp), + _ => temp + }; + + return await Task.FromResult($"The current weather in {location} is {temp}\u00b0 {unit}"); + } + + public static int CelsiusToFahrenheit(int celsius) => (celsius * 9 / 5) + 32; } } \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/AssistantExtensions.cs b/OpenAI-DotNet/Assistants/AssistantExtensions.cs index 4944b4fb..7c28f109 100644 --- a/OpenAI-DotNet/Assistants/AssistantExtensions.cs +++ b/OpenAI-DotNet/Assistants/AssistantExtensions.cs @@ -2,7 +2,10 @@ using OpenAI.Files; using OpenAI.Threads; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -173,5 +176,79 @@ public static async Task DeleteFileAsync(this AssistantResponse assistant, } #endregion Files + + #region Tools + + /// + /// Invoke the assistant's tool function using the . + /// + /// . + /// . + /// Tool output result as + public static string InvokeToolCall(this AssistantResponse assistant, ToolCall toolCall) + { + var tool = assistant.Tools.FirstOrDefault(tool => toolCall.Type == "function" && tool.Function.Name == toolCall.FunctionCall.Name) ?? + throw new InvalidOperationException($"Failed to find a valid tool for [{toolCall.Id}] {toolCall.Type}"); + tool.Function.Arguments = toolCall.FunctionCall.Arguments; + return tool.InvokeFunction(); + } + + /// + /// Invoke the assistant's tool function using the . + /// + /// . + /// . + /// Optional, . + /// Tool output result as + public static async Task InvokeToolCallAsync(this AssistantResponse assistant, ToolCall toolCall, CancellationToken cancellationToken = default) + { + var tool = assistant.Tools.FirstOrDefault(tool => toolCall.Type == "function" && tool.Function.Name == toolCall.FunctionCall.Name) ?? + throw new InvalidOperationException($"Failed to find a valid tool for [{toolCall.Id}] {toolCall.Type}"); + tool.Function.Arguments = toolCall.FunctionCall.Arguments; + return await tool.InvokeFunctionAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Calls the tool's function, with the provided arguments from the toolCall and returns the output. + /// + /// . + /// . + /// . + public static ToolOutput GetToolOutput(this AssistantResponse assistant, ToolCall toolCall) + => new(toolCall.Id, assistant.InvokeToolCall(toolCall)); + + /// + /// Calls each tool's function, with the provided arguments from the toolCalls and returns the outputs. + /// + /// . + /// A collection of s. + /// A collection of s. + public static IReadOnlyList GetToolOutputs(this AssistantResponse assistant, IEnumerable toolCalls) + => toolCalls.Select(assistant.GetToolOutput).ToList(); + + /// + /// Calls the tool's function, with the provided arguments from the toolCall and returns the output. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task GetToolOutputAsync(this AssistantResponse assistant, ToolCall toolCall, CancellationToken cancellationToken = default) + { + var output = await assistant.InvokeToolCallAsync(toolCall, cancellationToken).ConfigureAwait(false); + return new ToolOutput(toolCall.Id, output); + } + + /// + /// Calls each tool's function, with the provided arguments from the toolCalls and returns the outputs. + /// + /// . + /// A collection of s. + /// Optional, . + /// A collection of s. + public static async Task> GetToolOutputsAsync(this AssistantResponse assistant, IEnumerable toolCalls, CancellationToken cancellationToken = default) + => await Task.WhenAll(toolCalls.Select(async toolCall => await assistant.GetToolOutputAsync(toolCall, cancellationToken).ConfigureAwait(false))).ConfigureAwait(false); + + #endregion Tools } } diff --git a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs index a7582795..f6b228f7 100644 --- a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs +++ b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs @@ -36,7 +36,7 @@ public sealed class OpenAIAuthentication /// Allows implicit casting from a string, so that a simple string API key can be provided in place of an instance of . /// /// The API key to convert into a . - public static implicit operator OpenAIAuthentication(string key) => new OpenAIAuthentication(key); + public static implicit operator OpenAIAuthentication(string key) => new(key); private OpenAIAuthentication(AuthInfo authInfo) => this.authInfo = authInfo; diff --git a/OpenAI-DotNet/Authentication/OpenAIClientSettings.cs b/OpenAI-DotNet/Authentication/OpenAIClientSettings.cs index da1a0fcb..ad73a7de 100644 --- a/OpenAI-DotNet/Authentication/OpenAIClientSettings.cs +++ b/OpenAI-DotNet/Authentication/OpenAIClientSettings.cs @@ -116,10 +116,10 @@ public OpenAIClientSettings(string resourceName, string deploymentId, string api internal bool IsAzureDeployment => BaseRequestUrlFormat.Contains(AzureOpenAIDomain); - private readonly Dictionary defaultQueryParameters = new Dictionary(); + private readonly Dictionary defaultQueryParameters = new(); internal IReadOnlyDictionary DefaultQueryParameters => defaultQueryParameters; - public static OpenAIClientSettings Default { get; } = new OpenAIClientSettings(); + public static OpenAIClientSettings Default { get; } = new(); } } diff --git a/OpenAI-DotNet/Chat/ChatRequest.cs b/OpenAI-DotNet/Chat/ChatRequest.cs index 11285fc3..5aa2e2a2 100644 --- a/OpenAI-DotNet/Chat/ChatRequest.cs +++ b/OpenAI-DotNet/Chat/ChatRequest.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace OpenAI.Chat @@ -23,12 +22,13 @@ public ChatRequest( int? number = null, double? presencePenalty = null, ChatResponseFormat responseFormat = ChatResponseFormat.Text, + int? seed = null, string[] stops = null, double? temperature = null, double? topP = null, int? topLogProbs = null, string user = null) - : this(messages, model, frequencyPenalty, logitBias, maxTokens, number, presencePenalty, responseFormat, number, stops, temperature, topP, topLogProbs, user) + : this(messages, model, frequencyPenalty, logitBias, maxTokens, number, presencePenalty, responseFormat, seed, stops, temperature, topP, topLogProbs, user) { var tooList = tools?.ToList(); @@ -43,15 +43,8 @@ public ChatRequest( if (!toolChoice.Equals("none") && !toolChoice.Equals("auto")) { - var tool = new JsonObject - { - ["type"] = "function", - ["function"] = new JsonObject - { - ["name"] = toolChoice - } - }; - ToolChoice = tool; + var tool = tooList.FirstOrDefault(t => t.Function.Name.Contains(toolChoice)); + ToolChoice = tool ?? throw new ArgumentException($"The specified tool choice '{toolChoice}' was not found in the list of tools"); } else { diff --git a/OpenAI-DotNet/Chat/Content.cs b/OpenAI-DotNet/Chat/Content.cs index 36a4d9f0..431e8dee 100644 --- a/OpenAI-DotNet/Chat/Content.cs +++ b/OpenAI-DotNet/Chat/Content.cs @@ -51,8 +51,8 @@ public Content(ContentType type, string input) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ImageUrl ImageUrl { get; private set; } - public static implicit operator Content(string input) => new Content(ContentType.Text, input); + public static implicit operator Content(string input) => new(ContentType.Text, input); - public static implicit operator Content(ImageUrl imageUrl) => new Content(imageUrl); + public static implicit operator Content(ImageUrl imageUrl) => new(imageUrl); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/ResponseFormat.cs b/OpenAI-DotNet/Chat/ResponseFormat.cs index b0864dd9..aac7d158 100644 --- a/OpenAI-DotNet/Chat/ResponseFormat.cs +++ b/OpenAI-DotNet/Chat/ResponseFormat.cs @@ -18,6 +18,6 @@ public sealed class ResponseFormat public static implicit operator ChatResponseFormat(ResponseFormat format) => format.Type; - public static implicit operator ResponseFormat(ChatResponseFormat format) => new ResponseFormat(format); + public static implicit operator ResponseFormat(ChatResponseFormat format) => new(format); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Common/Event.cs b/OpenAI-DotNet/Common/Event.cs index 71ecc16d..049dddb4 100644 --- a/OpenAI-DotNet/Common/Event.cs +++ b/OpenAI-DotNet/Common/Event.cs @@ -30,6 +30,6 @@ public sealed class Event : BaseResponse [JsonPropertyName("message")] public string Message { get; private set; } - public static implicit operator EventResponse(Event @event) => new EventResponse(@event); + public static implicit operator EventResponse(Event @event) => new(@event); } } diff --git a/OpenAI-DotNet/Common/Function.cs b/OpenAI-DotNet/Common/Function.cs index d27111cb..aefea2dd 100644 --- a/OpenAI-DotNet/Common/Function.cs +++ b/OpenAI-DotNet/Common/Function.cs @@ -1,18 +1,24 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; namespace OpenAI { /// /// /// - public class Function + public sealed class Function { public Function() { } - internal Function(Function other) => CopyFrom(other); + private const string NameRegex = "^[a-zA-Z0-9_-]{1,64}$"; /// /// Creates a new function description to insert into a chat conversation. @@ -25,19 +31,39 @@ public Function() { } /// An optional description of the function, used by the API to determine if it is useful to include in the response. /// /// - /// An optional JSON object describing the parameters of the function that the model should generate in JSON schema format (json-schema.org). + /// An optional JSON object describing the parameters of the function that the model can generate. /// /// - /// The arguments to use when calling the function. + /// An optional JSON object describing the arguments to use when invoking the function. /// public Function(string name, string description = null, JsonNode parameters = null, JsonNode arguments = null) { + if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex)) + { + throw new ArgumentException($"The name of the function does not conform to naming standards: {NameRegex}"); + } + Name = name; Description = description; Parameters = parameters; Arguments = arguments; } + internal Function(Function other) => CopyFrom(other); + + internal Function(string name, string description, JsonObject parameters, MethodInfo method) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex)) + { + throw new ArgumentException($"The name of the function does not conform to naming standards: {NameRegex}"); + } + + Name = name; + Description = description; + Parameters = parameters; + functionCache[Name] = method; + } + /// /// The name of the function to generate arguments for.
/// May contain a-z, A-Z, 0-9, and underscores and dashes, with a maximum length of 64 characters. @@ -100,7 +126,7 @@ public JsonNode Arguments return arguments; } - private set => arguments = value; + internal set => arguments = value; } internal void CopyFrom(Function other) @@ -125,5 +151,99 @@ internal void CopyFrom(Function other) parametersString += other.Parameters.ToString(); } } + + #region Function Invoking Utilities + + private static readonly Dictionary functionCache = new(); + + public string Invoke() + { + var (method, invokeArgs) = ValidateFunctionArguments(); + var result = method.Invoke(null, invokeArgs); + return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions); + } + + public async Task InvokeAsync(CancellationToken cancellationToken = default) + { + var (method, invokeArgs) = ValidateFunctionArguments(cancellationToken); + var task = (Task)method.Invoke(null, invokeArgs); + + if (task is null) + { + throw new InvalidOperationException($"The function {Name} did not return a Task."); + } + + await task.ConfigureAwait(false); + + if (method.ReturnType == typeof(Task)) + { + return string.Empty; + } + + var result = method.ReturnType.GetProperty(nameof(Task.Result))?.GetValue(task); + return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions); + } + + private (MethodInfo method, object[] invokeArgs) ValidateFunctionArguments(CancellationToken cancellationToken = default) + { + if (Parameters != null && Arguments == null) + { + throw new ArgumentException($"Function {Name} has parameters but no arguments are set."); + } + + if (!functionCache.TryGetValue(Name, out var method)) + { + if (!Name.Contains('_')) + { + throw new InvalidOperationException($"Failed to lookup and invoke function \"{Name}\""); + } + + var type = Type.GetType(Name[..Name.LastIndexOf('_')].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid type for {Name}"); + method = type.GetMethod(Name[(Name.LastIndexOf('_') + 1)..].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid method for {Name}"); + functionCache[Name] = method; + } + + var requestedArgs = JsonSerializer.Deserialize>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions); + var methodParams = method.GetParameters(); + var invokeArgs = new object[methodParams.Length]; + + for (var i = 0; i < methodParams.Length; i++) + { + var parameter = methodParams[i]; + + if (parameter.Name == null) + { + throw new InvalidOperationException($"Failed to find a valid parameter name for {method.DeclaringType}.{method.Name}()"); + } + + if (requestedArgs.TryGetValue(parameter.Name, out var value)) + { + if (parameter.ParameterType == typeof(CancellationToken)) + { + invokeArgs[i] = cancellationToken; + } + else if (value is JsonElement element) + { + invokeArgs[i] = JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType, OpenAIClient.JsonSerializationOptions); + } + else + { + invokeArgs[i] = value; + } + } + else if (parameter.HasDefaultValue) + { + invokeArgs[i] = parameter.DefaultValue; + } + else + { + throw new ArgumentException($"Missing argument for parameter '{parameter.Name}'"); + } + } + + return (method, invokeArgs); + } + + #endregion Function Invoking Utilities } } diff --git a/OpenAI-DotNet/Common/FunctionAttribute.cs b/OpenAI-DotNet/Common/FunctionAttribute.cs new file mode 100644 index 00000000..9e2bbe73 --- /dev/null +++ b/OpenAI-DotNet/Common/FunctionAttribute.cs @@ -0,0 +1,17 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace OpenAI +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class FunctionAttribute : Attribute + { + public FunctionAttribute(string description = null) + { + Description = description; + } + + public string Description { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Common/Tool.cs b/OpenAI-DotNet/Common/Tool.cs index d35f542a..92e29d5c 100644 --- a/OpenAI-DotNet/Common/Tool.cs +++ b/OpenAI-DotNet/Common/Tool.cs @@ -1,6 +1,13 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using OpenAI.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; namespace OpenAI { @@ -16,11 +23,11 @@ public Tool(Function function) Type = nameof(function); } - public static implicit operator Tool(Function function) => new Tool(function); + public static implicit operator Tool(Function function) => new(function); - public static Tool Retrieval { get; } = new Tool { Type = "retrieval" }; + public static Tool Retrieval { get; } = new() { Type = "retrieval" }; - public static Tool CodeInterpreter { get; } = new Tool { Type = "code_interpreter" }; + public static Tool CodeInterpreter { get; } = new() { Type = "code_interpreter" }; [JsonInclude] [JsonPropertyName("id")] @@ -40,6 +47,11 @@ public Tool(Function function) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Function Function { get; private set; } + public string InvokeFunction() => Function.Invoke(); + + public async Task InvokeFunctionAsync(CancellationToken cancellationToken = default) + => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false); + internal void CopyFrom(Tool other) { if (!string.IsNullOrWhiteSpace(other?.Id)) @@ -69,5 +81,76 @@ internal void CopyFrom(Tool other) } } } + + private static List toolCache = new() + { + Retrieval, + CodeInterpreter + }; + + /// + /// Gets a list of all available tools. + /// + /// + /// This method will scan all assemblies for methods decorated with the . + /// + /// Optional, Whether to include the default tools (Retrieval and CodeInterpreter). + /// Optional, Whether to force an update of the tool cache. + /// A list of all available tools. + public static IReadOnlyList GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false) + { + if (forceUpdate || toolCache.All(tool => tool.Type != "function")) + { + var tools = new List(); + tools.AddRange( + from assembly in AppDomain.CurrentDomain.GetAssemblies() + from type in assembly.GetTypes() + from method in type.GetMethods() + let functionAttribute = method.GetCustomAttribute() + where functionAttribute != null + let name = $"{type.FullName}.{method.Name}".Replace('.', '_') + let description = functionAttribute.Description + let parameters = method.GenerateJsonSchema() + select new Function(name, description, parameters, method) + into function + select new Tool(function)); + + foreach (var newTool in tools.Where(knownTool => !toolCache.Any(tool => tool.Type == "function" && tool.Function.Name == knownTool.Function.Name))) + { + toolCache.Add(newTool); + } + } + + return !includeDefaults + ? toolCache.Where(tool => tool.Type == "function").ToList() + : toolCache; + } + + /// + /// Get or create a tool from a method. + /// + /// + /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.
+ /// The method doesn't need to be decorated with the .
+ ///
+ /// The type containing the method. + /// The name of the method. + /// Optional, The description of the method. + /// The tool for the method. + public static Tool GetOrCreateTool(Type type, string methodName, string description = null) + { + var method = type.GetMethod(methodName) ?? + throw new InvalidOperationException($"Failed to find a valid method for {type.FullName}.{methodName}()"); + var functionName = $"{type.FullName}.{method.Name}".Replace('.', '_'); + + foreach (var knownTool in toolCache.Where(knownTool => knownTool.Type == "function" && knownTool.Function.Name == functionName)) + { + return knownTool; + } + + var tool = new Tool(new Function(functionName, description ?? string.Empty, method.GenerateJsonSchema(), method)); + toolCache.Add(tool); + return tool; + } } } \ No newline at end of file diff --git a/OpenAI-DotNet/Common/Usage.cs b/OpenAI-DotNet/Common/Usage.cs index 5180c54a..2849d9a2 100644 --- a/OpenAI-DotNet/Common/Usage.cs +++ b/OpenAI-DotNet/Common/Usage.cs @@ -49,7 +49,7 @@ internal void CopyFrom(Usage other) public override string ToString() => JsonSerializer.Serialize(this, OpenAIClient.JsonSerializationOptions); public static Usage operator +(Usage a, Usage b) - => new Usage( + => new( (a.PromptTokens ?? 0) + (b.PromptTokens ?? 0), (a.CompletionTokens ?? 0) + (b.CompletionTokens ?? 0), (a.TotalTokens ?? 0) + (b.TotalTokens ?? 0)); diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs index b59e44fe..1b5edf74 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs @@ -20,7 +20,7 @@ public sealed class EmbeddingsRequest /// /// /// ID of the model to use.
- /// Defaults to: + /// Defaults to: /// /// /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. @@ -57,8 +57,7 @@ public EmbeddingsRequest(string input, string model = null, string user = null, /// The number of dimensions the resulting output embeddings should have. /// Only supported in text-embedding-3 and later models /// - public EmbeddingsRequest(IEnumerable input, string model = null, string user = null, - int? dimensions = null) + public EmbeddingsRequest(IEnumerable input, string model = null, string user = null, int? dimensions = null) { Input = input?.ToList(); @@ -77,7 +76,7 @@ public EmbeddingsRequest(IEnumerable input, string model = null, string [JsonPropertyName("model")] public string Model { get; } - + [JsonPropertyName("dimensions")] public int? Dimensions { get; } diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index 19eaa39a..53166452 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -25,7 +25,7 @@ internal static class ResponseExtensions private const string XRateLimitResetRequests = "x-ratelimit-reset-requests"; private const string XRateLimitResetTokens = "x-ratelimit-reset-tokens"; - private static readonly NumberFormatInfo numberFormatInfo = new NumberFormatInfo + private static readonly NumberFormatInfo numberFormatInfo = new() { NumberGroupSeparator = ",", NumberDecimalSeparator = "." diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs new file mode 100644 index 00000000..91160f91 --- /dev/null +++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs @@ -0,0 +1,97 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json.Nodes; + +namespace OpenAI.Extensions +{ + internal static class TypeExtensions + { + public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo) + { + var schema = new JsonObject + { + ["type"] = "object", + ["properties"] = new JsonObject() + }; + var requiredParameters = new JsonArray(); + + foreach (var parameter in methodInfo.GetParameters()) + { + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + throw new InvalidOperationException($"Failed to find a valid parameter name for {methodInfo.DeclaringType}.{methodInfo.Name}()"); + } + + if (!parameter.HasDefaultValue) + { + requiredParameters.Add(parameter.Name); + } + + schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType); + } + + if (requiredParameters.Count > 0) + { + schema["required"] = requiredParameters; + } + + return schema; + } + + public static JsonObject GenerateJsonSchema(this Type type) + { + var schema = new JsonObject(); + + if (type.IsEnum) + { + schema["type"] = "string"; + schema["enum"] = new JsonArray(); + + foreach (var value in Enum.GetValues(type)) + { + schema["enum"].AsArray().Add(value.ToString()); + } + } + else if (type.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))) + { + schema["type"] = "array"; + schema["items"] = GenerateJsonSchema(type.GetElementType() ?? type.GetGenericArguments()[0]); + } + else if (type.IsClass && type != typeof(string)) + { + schema["type"] = "object"; + var properties = type.GetProperties(); + var propertiesInfo = new JsonObject(); + var requiredProperties = new JsonArray(); + + foreach (var property in properties) + { + var propertyInfo = GenerateJsonSchema(property.PropertyType); + + if (Nullable.GetUnderlyingType(property.PropertyType) == null) + { + requiredProperties.Add(property.Name); + } + + propertiesInfo[property.Name] = propertyInfo; + } + + schema["properties"] = propertiesInfo; + + if (requiredProperties.Count > 0) + { + schema["required"] = requiredProperties; + } + } + else + { + schema["type"] = type.Name.ToLower(); + } + + return schema; + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Files/FileData.cs b/OpenAI-DotNet/Files/FileData.cs index 8488beb1..f15b1dc6 100644 --- a/OpenAI-DotNet/Files/FileData.cs +++ b/OpenAI-DotNet/Files/FileData.cs @@ -63,7 +63,7 @@ public sealed class FileData : BaseResponse public static implicit operator string(FileData fileData) => fileData?.ToString(); - public static implicit operator FileResponse(FileData fileData) => new FileResponse(fileData); + public static implicit operator FileResponse(FileData fileData) => new(fileData); public override string ToString() => Id; } diff --git a/OpenAI-DotNet/FineTuning/FineTuneJob.cs b/OpenAI-DotNet/FineTuning/FineTuneJob.cs index 3ec115d5..7419b8d2 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJob.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJob.cs @@ -84,7 +84,7 @@ public DateTime? FinishedAt [JsonIgnore] public IReadOnlyList Events { get; internal set; } = new List(); - public static implicit operator FineTuneJobResponse(FineTuneJob job) => new FineTuneJobResponse(job); + public static implicit operator FineTuneJobResponse(FineTuneJob job) => new(job); public static implicit operator string(FineTuneJob job) => job?.ToString(); diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs index 39698615..9674e2ac 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs @@ -101,7 +101,7 @@ public DateTime? FinishedAt [JsonPropertyName("trained_tokens")] public int? TrainedTokens { get; private set; } - private List events = new List(); + private List events = new(); [JsonIgnore] public IReadOnlyList Events diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 39cbe3e9..2aebec37 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -28,8 +28,15 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx True True - 7.6.5 + 7.7.0 +Version 7.7.0 +- Added Tool call and Function call Utilities and helper methods + - Added FunctionAttribute to decorate methods to be used in function calling + - Chat.Message.ToolCalls can be directly invoked using Function.Invoke() or Function.InvokeAsync(CancellationToken) + - Assistant tool call outputs can be easily generated using assistnat.GetToolOutputAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls) + - Check updated docs for more details and examples +- Fixed ChatRequest seed parameter not being set correctly Version 7.6.5 - Updated api key prefix checks to only be enforced for OpenAI domain Version 7.6.4 diff --git a/OpenAI-DotNet/OpenAIClient.cs b/OpenAI-DotNet/OpenAIClient.cs index d7509b92..bbadb3ef 100644 --- a/OpenAI-DotNet/OpenAIClient.cs +++ b/OpenAI-DotNet/OpenAIClient.cs @@ -109,7 +109,7 @@ private void Dispose(bool disposing) /// /// The to use when making calls to the API. /// - internal static JsonSerializerOptions JsonSerializationOptions { get; } = new JsonSerializerOptions + internal static JsonSerializerOptions JsonSerializationOptions { get; } = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverterFactory() } diff --git a/OpenAI-DotNet/Threads/ContentText.cs b/OpenAI-DotNet/Threads/ContentText.cs index a2dc32c2..11851941 100644 --- a/OpenAI-DotNet/Threads/ContentText.cs +++ b/OpenAI-DotNet/Threads/ContentText.cs @@ -24,7 +24,7 @@ public sealed class ContentText [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public IReadOnlyList Annotations { get; private set; } - public static implicit operator ContentText(string value) => new ContentText(value); + public static implicit operator ContentText(string value) => new(value); public static implicit operator string(ContentText text) => text?.ToString(); diff --git a/OpenAI-DotNet/Threads/CreateThreadRequest.cs b/OpenAI-DotNet/Threads/CreateThreadRequest.cs index cb0d1c6b..8f1568cf 100644 --- a/OpenAI-DotNet/Threads/CreateThreadRequest.cs +++ b/OpenAI-DotNet/Threads/CreateThreadRequest.cs @@ -39,6 +39,6 @@ public CreateThreadRequest(IEnumerable messages = null, IReadOnlyDictio [JsonPropertyName("metadata")] public IReadOnlyDictionary Metadata { get; } - public static implicit operator CreateThreadRequest(string message) => new CreateThreadRequest(new[] { new Message(message) }); + public static implicit operator CreateThreadRequest(string message) => new(new[] { new Message(message) }); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/Message.cs b/OpenAI-DotNet/Threads/Message.cs index f60795b9..f2e539c1 100644 --- a/OpenAI-DotNet/Threads/Message.cs +++ b/OpenAI-DotNet/Threads/Message.cs @@ -8,7 +8,7 @@ namespace OpenAI.Threads { public sealed class Message { - public static implicit operator Message(string content) => new Message(content); + public static implicit operator Message(string content) => new(content); /// /// Constructor. diff --git a/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs b/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs index 6c05c0ce..3f7432ce 100644 --- a/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs +++ b/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs @@ -22,9 +22,7 @@ public SubmitToolOutputsRequest(ToolOutput toolOutput) /// /// Collection of tools for which the outputs are being submitted. public SubmitToolOutputsRequest(IEnumerable toolOutputs) - { - ToolOutputs = toolOutputs?.ToList(); - } + => ToolOutputs = toolOutputs?.ToList(); /// /// A list of tools for which the outputs are being submitted. @@ -32,6 +30,10 @@ public SubmitToolOutputsRequest(IEnumerable toolOutputs) [JsonPropertyName("tool_outputs")] public IReadOnlyList ToolOutputs { get; } - public static implicit operator SubmitToolOutputsRequest(ToolOutput toolOutput) => new SubmitToolOutputsRequest(toolOutput); + public static implicit operator SubmitToolOutputsRequest(ToolOutput toolOutput) => new(toolOutput); + + public static implicit operator SubmitToolOutputsRequest(ToolOutput[] toolOutputs) => new(toolOutputs); + + public static implicit operator SubmitToolOutputsRequest(List toolOutputs) => new(toolOutputs); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ThreadExtensions.cs b/OpenAI-DotNet/Threads/ThreadExtensions.cs index a56199df..7fa049cf 100644 --- a/OpenAI-DotNet/Threads/ThreadExtensions.cs +++ b/OpenAI-DotNet/Threads/ThreadExtensions.cs @@ -282,7 +282,19 @@ public static async Task WaitForStatusChangeAsync(this RunResponse /// Optional, . /// . public static async Task SubmitToolOutputsAsync(this RunResponse run, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default) - => await run.Client.ThreadsEndpoint.SubmitToolOutputsAsync(run.ThreadId, run.Id, request, cancellationToken); + => await run.Client.ThreadsEndpoint.SubmitToolOutputsAsync(run.ThreadId, run.Id, request, cancellationToken).ConfigureAwait(false); + + /// + /// When a run has the status: "requires_action" and required_action.type is submit_tool_outputs, + /// this endpoint can be used to submit the outputs from the tool calls once they're all completed. + /// All outputs must be submitted in a single request. + /// + /// to submit outputs for. + /// s + /// Optional, . + /// . + public static async Task SubmitToolOutputsAsync(this RunResponse run, IEnumerable outputs, CancellationToken cancellationToken = default) + => await run.SubmitToolOutputsAsync(new SubmitToolOutputsRequest(outputs), cancellationToken).ConfigureAwait(false); /// /// Returns a list of run steps belonging to a run. @@ -292,7 +304,7 @@ public static async Task SubmitToolOutputsAsync(this RunResponse ru /// Optional, . /// . public static async Task> ListRunStepsAsync(this RunResponse run, ListQuery query = null, CancellationToken cancellationToken = default) - => await run.Client.ThreadsEndpoint.ListRunStepsAsync(run.ThreadId, run.Id, query, cancellationToken); + => await run.Client.ThreadsEndpoint.ListRunStepsAsync(run.ThreadId, run.Id, query, cancellationToken).ConfigureAwait(false); /// /// Retrieves a run step. @@ -330,7 +342,7 @@ public static async Task CancelAsync(this RunResponse run, Cancella /// Optional, . /// . public static async Task> ListMessagesAsync(this RunResponse run, ListQuery query = null, CancellationToken cancellationToken = default) - => await run.Client.ThreadsEndpoint.ListMessagesAsync(run.ThreadId, query, cancellationToken); + => await run.Client.ThreadsEndpoint.ListMessagesAsync(run.ThreadId, query, cancellationToken).ConfigureAwait(false); #endregion Runs } diff --git a/README.md b/README.md index ea921ce5..3b1304cb 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,7 @@ Create an assistant with a model and instructions. ```csharp using var api = new OpenAIClient(); -var request = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var request = new CreateAssistantRequest("gpt-3.5-turbo"); var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(request); ``` @@ -418,10 +418,10 @@ Modifies an assistant. ```csharp using var api = new OpenAIClient(); -var createRequest = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var createRequest = new CreateAssistantRequest("gpt-3.5-turbo"); var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(createRequest); -var modifyRequest = new CreateAssistantRequest("gpt-4-1106-preview"); -var modifiedAssistant = await api.AssistantsEndpoint.ModifyAsync(assistant.Id, modifyRequest); +var modifyRequest = new CreateAssistantRequest("gpt-4-turbo-preview"); +var modifiedAssistant = await api.AssistantsEndpoint.ModifyAssistantAsync(assistant.Id, modifyRequest); // OR AssistantExtension for easier use! var modifiedAssistantEx = await assistant.ModifyAsync(modifyRequest); ``` @@ -550,7 +550,7 @@ var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( new CreateAssistantRequest( name: "Math Tutor", instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", - model: "gpt-4-1106-preview")); + model: "gpt-4-turbo-preview")); var messages = new List { "I need to solve the equation `3x + 11 = 14`. Can you help me?" }; var threadRequest = new CreateThreadRequest(messages); var run = await assistant.CreateThreadAndRunAsync(threadRequest); @@ -726,7 +726,7 @@ var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( new CreateAssistantRequest( name: "Math Tutor", instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", - model: "gpt-4-1106-preview")); + model: "gpt-4-turbo-preview")); var thread = await api.ThreadsEndpoint.CreateThreadAsync(); var message = await thread.CreateMessageAsync("I need to solve the equation `3x + 11 = 14`. Can you help me?"); var run = await thread.CreateRunAsync(assistant); @@ -770,37 +770,27 @@ When a run has the status: `requires_action` and `required_action.type` is `subm ```csharp using var api = new OpenAIClient(); -var function = new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray { "celsius", "fahrenheit" } - } - }, - ["required"] = new JsonArray { "location", "unit" } - }); -testAssistant = await api.AssistantsEndpoint.CreateAssistantAsync(new CreateAssistantRequest(tools: new Tool[] { function })); -var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature in celsius now?"); +var tools = new List +{ + // Use a predefined tool + Tool.Retrieval, + // Or create a tool from a type and the name of the method you want to use for function calling + Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync)) +}; +var assistantRequest = new CreateAssistantRequest(tools: tools, instructions: "You are a helpful weather assistant. Use the appropriate unit based on geographical location."); +var testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(assistantRequest); +var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature now?"); // waiting while run is Queued and InProgress run = await run.WaitForStatusChangeAsync(); -var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0]; -Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}"); -var functionArgs = JsonSerializer.Deserialize(toolCall.FunctionCall.Arguments); -var functionResult = WeatherService.GetCurrentWeather(functionArgs); -var toolOutput = new ToolOutput(toolCall.Id, functionResult); -run = await run.SubmitToolOutputsAsync(toolOutput); +// Invoke all of the tool call functions and return the tool outputs. +var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls); + +foreach (var toolOutput in toolOutputs) +{ + Console.WriteLine($"tool call output: {toolOutput.Output}"); +} +// submit the tool outputs +run = await run.SubmitToolOutputsAsync(toolOutputs); // waiting while run in Queued and InProgress run = await run.WaitForStatusChangeAsync(); var messages = await run.ListMessagesAsync(); @@ -939,31 +929,7 @@ foreach (var message in messages) } // Define the tools that the assistant is able to use: -var tools = new List -{ - new Function( - nameof(WeatherService.GetCurrentWeather), - "Get the current weather in a given location", - new JsonObject - { - ["type"] = "object", - ["properties"] = new JsonObject - { - ["location"] = new JsonObject - { - ["type"] = "string", - ["description"] = "The city and state, e.g. San Francisco, CA" - }, - ["unit"] = new JsonObject - { - ["type"] = "string", - ["enum"] = new JsonArray {"celsius", "fahrenheit"} - } - }, - ["required"] = new JsonArray { "location", "unit" } - }) -}; - +var tools = Tool.GetAllAvailableTools(includeDefaults: false); var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); messages.Add(response.FirstChoice.Message); @@ -992,8 +958,8 @@ if (!string.IsNullOrEmpty(response.ToString())) var usedTool = response.FirstChoice.Message.ToolCalls[0]; Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Console.WriteLine($"{usedTool.Function.Arguments}"); -var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); -var functionResult = WeatherService.GetCurrentWeather(functionArgs); +// Invoke the used tool to get the function result! +var functionResult = await usedTool.InvokeFunctionAsync(); messages.Add(new Message(usedTool, functionResult)); Console.WriteLine($"{Role.Tool}: {functionResult}"); // System: You are a helpful weather assistant. @@ -1045,7 +1011,7 @@ var messages = new List new Message(Role.System, "You are a helpful assistant designed to output JSON."), new Message(Role.User, "Who won the world series in 2020?"), }; -var chatRequest = new ChatRequest(messages, "gpt-4-1106-preview", responseFormat: ChatResponseFormat.Json); +var chatRequest = new ChatRequest(messages, "gpt-4-turbo-preview", responseFormat: ChatResponseFormat.Json); var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); foreach (var choice in response.Choices)