diff --git a/.github/workflows/Publish-Nuget.yml b/.github/workflows/Publish-Nuget.yml index 859d27b4..3c8ba366 100644 --- a/.github/workflows/Publish-Nuget.yml +++ b/.github/workflows/Publish-Nuget.yml @@ -31,32 +31,55 @@ env: jobs: build: if: ${{ !github.event_name == 'pull_request' || !github.event.pull_request.draft }} - runs-on: windows-latest + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} - - uses: microsoft/setup-msbuild@v1 + - run: dotnet restore + + - run: dotnet build --configuration Release --no-restore + - name: Test Packages - run: dotnet test --configuration Release if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' }} + run: dotnet test --configuration Release --collect:"XPlat Code Coverage" --logger:trx --no-build --no-restore --results-directory ./test-results env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_ORGANIZATION_ID: ${{ secrets.OPENAI_ORGANIZATION_ID }} - - name: Build Pack and Publish NuGet Package + - name: Publish Test Results + if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: test-results/**/*.trx + comment_mode: off + report_individual_runs: true + compare_to_earlier_commit: false + + - name: Code Coverage Summary Report + if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: test-results/**/coverage.cobertura.xml + badge: true + format: 'markdown' + output: 'both' + + - name: Write Coverage Job Summary + if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} + run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY + + - name: Pack and Publish NuGet Package run: | $projectPath = "${{ github.workspace }}\OpenAI-DotNet" $proxyProjectPath = "${{ github.workspace }}\OpenAI-DotNet-Proxy" - # build all - dotnet build --configuration Release - # pack OpenAI-DotNet dotnet pack $projectPath --configuration Release --include-symbols $out = "$projectPath\bin\Release" diff --git a/OpenAI-DotNet-Tests/AbstractTestFixture.cs b/OpenAI-DotNet-Tests/AbstractTestFixture.cs index ec492660..1facbe32 100644 --- a/OpenAI-DotNet-Tests/AbstractTestFixture.cs +++ b/OpenAI-DotNet-Tests/AbstractTestFixture.cs @@ -1,6 +1,6 @@ -using System; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using System; using System.Net.Http; namespace OpenAI.Tests diff --git a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj index 2565cf58..0ae86c67 100644 --- a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj +++ b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/OpenAI-DotNet-Tests/TestFixture_00_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs similarity index 99% rename from OpenAI-DotNet-Tests/TestFixture_00_Authentication.cs rename to OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs index e29afba9..8c589730 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_Authentication.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs @@ -6,7 +6,7 @@ namespace OpenAI.Tests { - internal class TestFixture_00_Authentication + internal class TestFixture_00_00_Authentication { [SetUp] public void Setup() diff --git a/OpenAI-DotNet-Tests/TextFixture_11_Proxy.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs similarity index 96% rename from OpenAI-DotNet-Tests/TextFixture_11_Proxy.cs rename to OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs index d4f386bf..f962fcde 100644 --- a/OpenAI-DotNet-Tests/TextFixture_11_Proxy.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs @@ -6,7 +6,7 @@ namespace OpenAI.Tests { - internal class TextFixture_11_Proxy : AbstractTestFixture + internal class TestFixture_00_01_Proxy : AbstractTestFixture { [Test] public async Task Test_01_Health() diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index df71e694..9ce073d7 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using OpenAI.Chat; +using OpenAI.Models; using OpenAI.Tests.Weather; using System; using System.Collections.Generic; @@ -23,32 +24,24 @@ public async Task Test_01_GetChatCompletion() new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; - var chatRequest = new ChatRequest(messages, number: 2); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 2); + var chatRequest = new ChatRequest(messages, Model.GPT4); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsNotEmpty(response.Choices); - foreach (var choice in result.Choices) + foreach (var choice in response.Choices) { Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); } - result.GetUsage(); - - Console.WriteLine(result.LimitRequests); - Console.WriteLine(result.RemainingRequests); - Console.WriteLine(result.ResetRequests); - Console.WriteLine(result.LimitTokens); - Console.WriteLine(result.RemainingTokens); - Console.WriteLine(result.ResetTokens); + response.GetUsage(); } [Test] public async Task Test_02_GetChatStreamingCompletion() { Assert.IsNotNull(OpenAIClient.ChatEndpoint); - const int choiceCount = 2; var messages = new List { new Message(Role.System, "You are a helpful assistant."), @@ -56,14 +49,8 @@ public async Task Test_02_GetChatStreamingCompletion() new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; - var chatRequest = new ChatRequest(messages, number: choiceCount); - var cumulativeDelta = new List(); - - for (var i = 0; i < choiceCount; i++) - { - cumulativeDelta.Add(string.Empty); - } - + var chatRequest = new ChatRequest(messages); + var cumulativeDelta = string.Empty; var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); @@ -72,24 +59,17 @@ public async Task Test_02_GetChatStreamingCompletion() foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null)) { - cumulativeDelta[choice.Index] += choice.Delta.Content; + cumulativeDelta += choice.Delta.Content; } }); - Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); - Assert.IsTrue(response.Choices.Count == choiceCount); - - for (var i = 0; i < choiceCount; i++) - { - var choice = response.Choices[i]; - Assert.IsFalse(string.IsNullOrEmpty(choice?.Message?.Content)); - Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); - Assert.IsTrue(choice.Message.Role == Role.Assistant); - var deltaContent = cumulativeDelta[i]; - Assert.IsTrue(choice.Message.Content.Equals(deltaContent)); - } - + var choice = response.FirstChoice; + Assert.IsFalse(string.IsNullOrEmpty(choice?.Message?.Content)); + Console.WriteLine($"[{choice!.Index}] {choice.Message!.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); + Assert.IsTrue(choice.Message.Role == Role.Assistant); + Assert.IsTrue(choice.Message.Content!.Equals(cumulativeDelta)); + Console.WriteLine(response.ToString()); response.GetUsage(); } @@ -104,16 +84,24 @@ public async Task Test_03_GetChatStreamingCompletionEnumerableAsync() new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; - var chatRequest = new ChatRequest(messages, number: 2); - await foreach (var result in OpenAIClient.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) + var cumulativeDelta = string.Empty; + var chatRequest = new ChatRequest(messages); + await foreach (var partialResponse in OpenAIClient.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.NotZero(result.Choices.Count); + Assert.IsNotNull(partialResponse); + Assert.NotNull(partialResponse.Choices); + Assert.NotZero(partialResponse.Choices.Count); + + foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null)) + { + cumulativeDelta += choice.Delta.Content; + } } + + Console.WriteLine(cumulativeDelta); } - [Test] + //[Test] [Obsolete] public async Task Test_04_GetChatFunctionCompletion() { @@ -155,54 +143,54 @@ public async Task Test_04_GetChatFunctionCompletion() }; var chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(result.FirstChoice.Message.Content)) + if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); } - Assert.IsTrue(result.FirstChoice.FinishReason == "function_call"); - Assert.IsTrue(result.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); - Console.WriteLine($"{result.FirstChoice.Message.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(result.FirstChoice.Message.Function.Arguments.ToString()); + Assert.IsTrue(response.FirstChoice.FinishReason == "function_call"); + Assert.IsTrue(response.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Function.Arguments}"); + var functionArgs = JsonSerializer.Deserialize(response.FirstChoice.Message.Function.Arguments.ToString()); var functionResult = WeatherService.GetCurrentWeather(functionArgs); Assert.IsNotNull(functionResult); messages.Add(new Message(Role.Function, functionResult, nameof(WeatherService.GetCurrentWeather))); Console.WriteLine($"{Role.Function}: {functionResult}"); chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Console.WriteLine(result); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Console.WriteLine(response); } - [Test] + //[Test] [Obsolete] public async Task Test_05_GetChatFunctionCompletion_Streaming() { @@ -243,64 +231,64 @@ public async Task Test_05_GetChatFunctionCompletion_Streaming() }; var chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - var result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(result.FirstChoice.Message.Content)) + if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto"); - result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); } - Assert.IsTrue(result.FirstChoice.FinishReason == "function_call"); - Assert.IsTrue(result.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); - Console.WriteLine($"{result.FirstChoice.Message.Function.Arguments}"); + Assert.IsTrue(response.FirstChoice.FinishReason == "function_call"); + Assert.IsTrue(response.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(result.FirstChoice.Message.Function.Arguments.ToString()); + var functionArgs = JsonSerializer.Deserialize(response.FirstChoice.Message.Function.Arguments.ToString()); var functionResult = WeatherService.GetCurrentWeather(functionArgs); Assert.IsNotNull(functionResult); messages.Add(new Message(Role.Function, functionResult, nameof(WeatherService.GetCurrentWeather))); Console.WriteLine($"{Role.Function}: {functionResult}"); } - [Test] + //[Test] [Obsolete] public async Task Test_06_GetChatFunctionForceCompletion() { @@ -342,13 +330,13 @@ public async Task Test_06_GetChatFunctionForceCompletion() }; var chatRequest = new ChatRequest(messages, functions: functions); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -358,18 +346,18 @@ public async Task Test_06_GetChatFunctionForceCompletion() functions: functions, functionCall: nameof(WeatherService.GetCurrentWeather), model: "gpt-3.5-turbo-0613"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); - - Assert.IsTrue(result.FirstChoice.FinishReason == "stop"); - Assert.IsTrue(result.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); - Console.WriteLine($"{result.FirstChoice.Message.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(result.FirstChoice.Message.Function.Arguments.ToString()); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); + + Assert.IsTrue(response.FirstChoice.FinishReason == "stop"); + Assert.IsTrue(response.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Function.Arguments}"); + var functionArgs = JsonSerializer.Deserialize(response.FirstChoice.Message.Function.Arguments.ToString()); var functionResult = WeatherService.GetCurrentWeather(functionArgs); Assert.IsNotNull(functionResult); messages.Add(new Message(Role.Function, functionResult, nameof(WeatherService.GetCurrentWeather))); @@ -416,45 +404,45 @@ public async Task Test_07_GetChatToolCompletion() ["required"] = new JsonArray { "location", "unit" } }) }; - var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(result.FirstChoice.Message.Content)) + if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); } - var usedTool = result.FirstChoice.Message.ToolCalls[0]; - Assert.IsTrue(result.FirstChoice.FinishReason == "tool_calls"); + 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)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); + 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); @@ -462,8 +450,8 @@ public async Task Test_07_GetChatToolCompletion() messages.Add(new Message(usedTool, functionResult)); Console.WriteLine($"{Role.Tool}: {functionResult}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Console.WriteLine(result); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Console.WriteLine(response); } [Test] @@ -504,57 +492,57 @@ public async Task Test_08_GetChatToolCompletion_Streaming() ["required"] = new JsonArray { "location", "unit" } }) }; - var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - var result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(result.FirstChoice.Message.Content)) + if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); } - Assert.IsTrue(result.FirstChoice.FinishReason == "tool_calls"); - var usedTool = result.FirstChoice.Message.ToolCalls[0]; + 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)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); + 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()); @@ -603,15 +591,14 @@ public async Task Test_09_GetChatToolForceCompletion() ["required"] = new JsonArray { "location", "unit" } }) }; - var chatRequest = new ChatRequest(messages, tools: tools); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -620,19 +607,20 @@ public async Task Test_09_GetChatToolForceCompletion() messages, tools: tools, toolChoice: nameof(WeatherService.GetCurrentWeather)); - result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Assert.IsTrue(result.Choices.Count == 1); - messages.Add(result.FirstChoice.Message); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsTrue(response.Choices.Count == 1); + messages.Add(response.FirstChoice.Message); - var usedTool = result.FirstChoice.Message.ToolCalls[0]; - Assert.IsTrue(result.FirstChoice.FinishReason == "stop"); + Assert.IsTrue(response.FirstChoice.FinishReason == "stop"); + var usedTool = response.FirstChoice.Message.ToolCalls[0]; + Assert.IsNotNull(usedTool); Assert.IsTrue(usedTool.Function.Name == nameof(WeatherService.GetCurrentWeather)); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Console.WriteLine($"{usedTool.Function.Arguments}"); - var functionArgs = JsonSerializer.Deserialize(result.FirstChoice.Message.ToolCalls[0].Function.Arguments.ToString()); + var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); var functionResult = WeatherService.GetCurrentWeather(functionArgs); Assert.IsNotNull(functionResult); messages.Add(new Message(usedTool, functionResult)); @@ -653,11 +641,11 @@ public async Task Test_10_GetChatVision() }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); - var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishDetails}"); - result.GetUsage(); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); + response.GetUsage(); } [Test] @@ -674,16 +662,16 @@ public async Task Test_11_GetChatVisionStreaming() }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); - var result = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => + var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { Assert.IsNotNull(partialResponse); Assert.NotNull(partialResponse.Choices); Assert.NotZero(partialResponse.Choices.Count); }); - Assert.IsNotNull(result); - Assert.IsNotNull(result.Choices); - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishDetails}"); - result.GetUsage(); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); + response.GetUsage(); } } } \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestFixture_04_Edits.cs b/OpenAI-DotNet-Tests/TestFixture_04_Edits.cs index 9f343db3..b49200bb 100644 --- a/OpenAI-DotNet-Tests/TestFixture_04_Edits.cs +++ b/OpenAI-DotNet-Tests/TestFixture_04_Edits.cs @@ -8,7 +8,6 @@ namespace OpenAI.Tests [Obsolete] internal class TestFixture_04_Edits : AbstractTestFixture { - [Test] public async Task Test_1_GetBasicEdit() { Assert.IsNotNull(OpenAIClient.EditsEndpoint); @@ -18,7 +17,7 @@ public async Task Test_1_GetBasicEdit() Assert.NotNull(result.Choices); Assert.NotZero(result.Choices.Count); Console.WriteLine(result); - Assert.IsTrue(result.ToString().Contains("week")); + Assert.IsTrue(result.ToString()?.Contains("week")); } } } diff --git a/OpenAI-DotNet-Tests/TestFixture_05_Images.cs b/OpenAI-DotNet-Tests/TestFixture_05_Images.cs index 49100810..09b9a931 100644 --- a/OpenAI-DotNet-Tests/TestFixture_05_Images.cs +++ b/OpenAI-DotNet-Tests/TestFixture_05_Images.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using OpenAI.Images; +using OpenAI.Models; using System; using System.IO; using System.Threading.Tasks; @@ -13,16 +14,16 @@ public async Task Test_1_GenerateImages() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var request = new ImageGenerationRequest("A house riding a velociraptor", Models.Model.DallE_2); - var results = await OpenAIClient.ImagesEndPoint.GenerateImageAsync(request); + var request = new ImageGenerationRequest("A house riding a velociraptor", Model.DallE_2); + var imageResults = await OpenAIClient.ImagesEndPoint.GenerateImageAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); - Assert.IsNotEmpty(results[0]); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } @@ -31,16 +32,16 @@ public async Task Test_2_GenerateImages_B64_Json() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var request = new ImageGenerationRequest("A house riding a velociraptor", Models.Model.DallE_2, responseFormat: ResponseFormat.B64_Json); - var results = await OpenAIClient.ImagesEndPoint.GenerateImageAsync(request); + var request = new ImageGenerationRequest("A house riding a velociraptor", Model.DallE_2, responseFormat: ResponseFormat.B64_Json); + var imageResults = await OpenAIClient.ImagesEndPoint.GenerateImageAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); - Assert.IsNotEmpty(results[0]); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } @@ -49,18 +50,19 @@ public async Task Test_3_GenerateImageEdits() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var imageAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_original.png"); - var maskAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_mask.png"); + var imageAssetPath = Path.GetFullPath("../../../Assets/image_edit_original.png"); + var maskAssetPath = Path.GetFullPath("../../../Assets/image_edit_mask.png"); var request = new ImageEditRequest(imageAssetPath, maskAssetPath, "A sunlit indoor lounge area with a pool containing a flamingo", size: ImageSize.Small); - var results = await OpenAIClient.ImagesEndPoint.CreateImageEditAsync(request); + var imageResults = await OpenAIClient.ImagesEndPoint.CreateImageEditAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } @@ -69,18 +71,19 @@ public async Task Test_4_GenerateImageEdits_B64_Json() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var imageAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_original.png"); - var maskAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_mask.png"); + var imageAssetPath = Path.GetFullPath("../../../Assets/image_edit_original.png"); + var maskAssetPath = Path.GetFullPath("../../../Assets/image_edit_mask.png"); var request = new ImageEditRequest(imageAssetPath, maskAssetPath, "A sunlit indoor lounge area with a pool containing a flamingo", size: ImageSize.Small, responseFormat: ResponseFormat.B64_Json); - var results = await OpenAIClient.ImagesEndPoint.CreateImageEditAsync(request); + var imageResults = await OpenAIClient.ImagesEndPoint.CreateImageEditAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } @@ -89,16 +92,17 @@ public async Task Test_5_GenerateImageVariations() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var imageAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_original.png"); + var imageAssetPath = Path.GetFullPath("../../../Assets/image_edit_original.png"); var request = new ImageVariationRequest(imageAssetPath, size: ImageSize.Small); - var results = await OpenAIClient.ImagesEndPoint.CreateImageVariationAsync(request); + var imageResults = await OpenAIClient.ImagesEndPoint.CreateImageVariationAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } @@ -107,16 +111,17 @@ public async Task Test_6_GenerateImageVariations_B64_Json() { Assert.IsNotNull(OpenAIClient.ImagesEndPoint); - var imageAssetPath = Path.GetFullPath(@"..\..\..\Assets\image_edit_original.png"); + var imageAssetPath = Path.GetFullPath("../../../Assets/image_edit_original.png"); var request = new ImageVariationRequest(imageAssetPath, size: ImageSize.Small, responseFormat: ResponseFormat.B64_Json); - var results = await OpenAIClient.ImagesEndPoint.CreateImageVariationAsync(request); + var imageResults = await OpenAIClient.ImagesEndPoint.CreateImageVariationAsync(request); - Assert.IsNotNull(results); - Assert.NotZero(results.Count); + Assert.IsNotNull(imageResults); + Assert.NotZero(imageResults.Count); - foreach (var result in results) + foreach (var image in imageResults) { - Console.WriteLine(result); + Assert.IsNotNull(image); + Console.WriteLine(image); } } } diff --git a/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs b/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs index 73d0fc7a..0247b1da 100644 --- a/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs +++ b/OpenAI-DotNet-Tests/TestFixture_06_Embeddings.cs @@ -9,9 +9,9 @@ internal class TestFixture_06_Embeddings : AbstractTestFixture public async Task Test_1_CreateEmbedding() { Assert.IsNotNull(OpenAIClient.EmbeddingsEndpoint); - var result = await OpenAIClient.EmbeddingsEndpoint.CreateEmbeddingAsync("The food was delicious and the waiter..."); - Assert.IsNotNull(result); - Assert.IsNotEmpty(result.Data); + var embedding = await OpenAIClient.EmbeddingsEndpoint.CreateEmbeddingAsync("The food was delicious and the waiter..."); + Assert.IsNotNull(embedding); + Assert.IsNotEmpty(embedding.Data); } [Test] @@ -23,9 +23,9 @@ public async Task Test_2_CreateEmbeddingsWithMultipleInputs() "The food was delicious and the waiter...", "The food was terrible and the waiter..." }; - var result = await OpenAIClient.EmbeddingsEndpoint.CreateEmbeddingAsync(embeddings); - Assert.IsNotNull(result); - Assert.AreEqual(result.Data.Count, 2); + var embedding = await OpenAIClient.EmbeddingsEndpoint.CreateEmbeddingAsync(embeddings); + Assert.IsNotNull(embedding); + Assert.AreEqual(embedding.Data.Count, 2); } } } diff --git a/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs b/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs index cfde4fd8..d5f92029 100644 --- a/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs +++ b/OpenAI-DotNet-Tests/TestFixture_07_Audio.cs @@ -12,7 +12,7 @@ internal class TestFixture_07_Audio : AbstractTestFixture public async Task Test_1_Transcription() { Assert.IsNotNull(OpenAIClient.AudioEndpoint); - var transcriptionAudio = Path.GetFullPath(@"..\..\..\Assets\T3mt39YrlyLoq8laHSdf.mp3"); + var transcriptionAudio = Path.GetFullPath("../../../Assets/T3mt39YrlyLoq8laHSdf.mp3"); using var request = new AudioTranscriptionRequest(transcriptionAudio, temperature: 0.1f, language: "en"); var result = await OpenAIClient.AudioEndpoint.CreateTranscriptionAsync(request); Assert.IsNotNull(result); @@ -23,7 +23,7 @@ public async Task Test_1_Transcription() public async Task Test_2_Translation() { Assert.IsNotNull(OpenAIClient.AudioEndpoint); - var translationAudio = Path.GetFullPath(@"..\..\..\Assets\Ja-botchan_1-1_1-2.mp3"); + var translationAudio = Path.GetFullPath("../../../Assets/Ja-botchan_1-1_1-2.mp3"); using var request = new AudioTranslationRequest(Path.GetFullPath(translationAudio)); var result = await OpenAIClient.AudioEndpoint.CreateTranslationAsync(request); Assert.IsNotNull(result); @@ -43,7 +43,7 @@ async Task ChunkCallback(ReadOnlyMemory chunkCallback) var result = await OpenAIClient.AudioEndpoint.CreateSpeechAsync(request, ChunkCallback); Assert.IsFalse(result.IsEmpty); - await File.WriteAllBytesAsync(@"..\..\..\Assets\HelloWorld.mp3", result.ToArray()); + await File.WriteAllBytesAsync("../../../Assets/HelloWorld.mp3", result.ToArray()); } } -} \ No newline at end of file +} diff --git a/OpenAI-DotNet-Tests/TestFixture_08_Files.cs b/OpenAI-DotNet-Tests/TestFixture_08_Files.cs index 2474af12..44d3ef14 100644 --- a/OpenAI-DotNet-Tests/TestFixture_08_Files.cs +++ b/OpenAI-DotNet-Tests/TestFixture_08_Files.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Threading.Tasks; namespace OpenAI.Tests @@ -14,14 +15,12 @@ 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") }); - await File.WriteAllTextAsync("test.jsonl", testData); + await File.WriteAllTextAsync("test.jsonl", JsonSerializer.Serialize(testData, OpenAIClient.DefaultJsonSerializerOptions)); Assert.IsTrue(File.Exists("test.jsonl")); var result = await OpenAIClient.FilesEndpoint.UploadFileAsync("test.jsonl", "fine-tune"); - Assert.IsNotNull(result); Assert.IsTrue(result.FileName == "test.jsonl"); Console.WriteLine($"{result.Id} -> {result.Object}"); - File.Delete("test.jsonl"); Assert.IsFalse(File.Exists("test.jsonl")); } @@ -30,12 +29,12 @@ public async Task Test_01_UploadFile() public async Task Test_02_ListFiles() { Assert.IsNotNull(OpenAIClient.FilesEndpoint); - var result = await OpenAIClient.FilesEndpoint.ListFilesAsync(); + var fileList = await OpenAIClient.FilesEndpoint.ListFilesAsync(); - Assert.IsNotNull(result); - Assert.IsNotEmpty(result); + Assert.IsNotNull(fileList); + Assert.IsNotEmpty(fileList); - foreach (var file in result) + foreach (var file in fileList) { var fileInfo = await OpenAIClient.FilesEndpoint.GetFileInfoAsync(file); Assert.IsNotNull(fileInfo); @@ -47,12 +46,12 @@ public async Task Test_02_ListFiles() public async Task Test_03_DownloadFile() { Assert.IsNotNull(OpenAIClient.FilesEndpoint); - var files = await OpenAIClient.FilesEndpoint.ListFilesAsync(); + var fileList = await OpenAIClient.FilesEndpoint.ListFilesAsync(); - Assert.IsNotNull(files); - Assert.IsNotEmpty(files); + Assert.IsNotNull(fileList); + Assert.IsNotEmpty(fileList); - var testFileData = files[0]; + var testFileData = fileList[0]; var result = await OpenAIClient.FilesEndpoint.DownloadFileAsync(testFileData, Directory.GetCurrentDirectory()); Assert.IsNotNull(result); @@ -67,20 +66,20 @@ public async Task Test_03_DownloadFile() public async Task Test_04_DeleteFiles() { Assert.IsNotNull(OpenAIClient.FilesEndpoint); - var files = await OpenAIClient.FilesEndpoint.ListFilesAsync(); - Assert.IsNotNull(files); - Assert.IsNotEmpty(files); + var fileList = await OpenAIClient.FilesEndpoint.ListFilesAsync(); + Assert.IsNotNull(fileList); + Assert.IsNotEmpty(fileList); - foreach (var file in files) + foreach (var file in fileList) { var result = await OpenAIClient.FilesEndpoint.DeleteFileAsync(file); Assert.IsTrue(result); Console.WriteLine($"{file.Id} -> deleted"); } - files = await OpenAIClient.FilesEndpoint.ListFilesAsync(); - Assert.IsNotNull(files); - Assert.IsEmpty(files); + fileList = await OpenAIClient.FilesEndpoint.ListFilesAsync(); + Assert.IsNotNull(fileList); + Assert.IsEmpty(fileList); } } } diff --git a/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs b/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs index 91b61870..5944879c 100644 --- a/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs +++ b/OpenAI-DotNet-Tests/TestFixture_09_FineTuning.cs @@ -7,15 +7,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace OpenAI.Tests { internal class TestFixture_09_FineTuning : AbstractTestFixture { - private async Task CreateTestTrainingDataAsync() + private async Task CreateTestTrainingDataAsync() { - var conersations = new List + var conversations = new List { new Conversation(new List { @@ -78,10 +79,8 @@ private async Task CreateTestTrainingDataAsync() new Message(Role.Assistant, "Around 384,400 kilometers. Give or take a few, like that really matters.") }) }; - const string localTrainingDataPath = "fineTunesTestTrainingData.jsonl"; - await File.WriteAllLinesAsync(localTrainingDataPath, conersations.Select(conversation => conversation.ToString())); - + await File.WriteAllLinesAsync(localTrainingDataPath, conversations.Select(conversation => JsonSerializer.Serialize(conversation, OpenAIClient.DefaultJsonSerializerOptions))); var fileData = await OpenAIClient.FilesEndpoint.UploadFileAsync(localTrainingDataPath, "fine-tune"); File.Delete(localTrainingDataPath); Assert.IsFalse(File.Exists(localTrainingDataPath)); @@ -106,10 +105,12 @@ public async Task Test_02_ListFineTuneJobs() Assert.IsNotNull(OpenAIClient.FineTuningEndpoint); var list = await OpenAIClient.FineTuningEndpoint.ListJobsAsync(); Assert.IsNotNull(list); - Assert.IsNotEmpty(list.Jobs); + Assert.IsNotEmpty(list.Items); - foreach (var job in list.Jobs.OrderByDescending(job => job.CreatedAt)) + foreach (var job in list.Items.OrderByDescending(job => job.CreatedAt)) { + Assert.IsNotNull(job); + Assert.IsNotNull(job.Client); Console.WriteLine($"{job.Id} -> {job.CreatedAt} | {job.Status}"); } } @@ -118,15 +119,15 @@ public async Task Test_02_ListFineTuneJobs() public async Task Test_03_RetrieveFineTuneJobInfo() { Assert.IsNotNull(OpenAIClient.FineTuningEndpoint); - var list = await OpenAIClient.FineTuningEndpoint.ListJobsAsync(); - Assert.IsNotNull(list); - Assert.IsNotEmpty(list.Jobs); + var jobList = await OpenAIClient.FineTuningEndpoint.ListJobsAsync(); + Assert.IsNotNull(jobList); + Assert.IsNotEmpty(jobList.Items); - foreach (var job in list.Jobs.OrderByDescending(job => job.CreatedAt)) + foreach (var job in jobList.Items.OrderByDescending(job => job.CreatedAt)) { - var request = await OpenAIClient.FineTuningEndpoint.GetJobInfoAsync(job); - Assert.IsNotNull(request); - Console.WriteLine($"{request.Id} -> {request.Status}"); + var response = await OpenAIClient.FineTuningEndpoint.GetJobInfoAsync(job); + Assert.IsNotNull(response); + Console.WriteLine($"{job.Id} -> {job.CreatedAt} | {job.Status}"); } } @@ -136,23 +137,21 @@ public async Task Test_04_ListFineTuneEvents() Assert.IsNotNull(OpenAIClient.FineTuningEndpoint); var list = await OpenAIClient.FineTuningEndpoint.ListJobsAsync(); Assert.IsNotNull(list); - Assert.IsNotEmpty(list.Jobs); + Assert.IsNotEmpty(list.Items); - foreach (var job in list.Jobs) + foreach (var job in list.Items) { - if (job.Status == JobStatus.Cancelled) - { - continue; - } + if (job.Status == JobStatus.Cancelled) { continue; } var eventList = await OpenAIClient.FineTuningEndpoint.ListJobEventsAsync(job); Assert.IsNotNull(eventList); - Assert.IsNotEmpty(eventList.Events); - - Console.WriteLine($"{job.Id} -> status: {job.Status} | event count: {eventList.Events.Count}"); + Assert.IsNotEmpty(eventList.Items); + Console.WriteLine($"{job.Id} -> status: {job.Status} | event count: {eventList.Items.Count}"); - foreach (var @event in eventList.Events.OrderByDescending(@event => @event.CreatedAt)) + foreach (var @event in eventList.Items.OrderByDescending(@event => @event.CreatedAt)) { + Assert.IsNotNull(@event); + Assert.IsNotNull(@event.Client); Console.WriteLine($" {@event.CreatedAt} [{@event.Level}] {@event.Message}"); } @@ -166,9 +165,9 @@ public async Task Test_05_CancelFineTuneJob() Assert.IsNotNull(OpenAIClient.FineTuningEndpoint); var list = await OpenAIClient.FineTuningEndpoint.ListJobsAsync(); Assert.IsNotNull(list); - Assert.IsNotEmpty(list.Jobs); + Assert.IsNotEmpty(list.Items); - foreach (var job in list.Jobs) + foreach (var job in list.Items) { if (job.Status is > JobStatus.NotStarted and < JobStatus.Succeeded) { diff --git a/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs b/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs index fd3465c3..421a0952 100644 --- a/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs +++ b/OpenAI-DotNet-Tests/TestFixture_10_Moderations.cs @@ -12,8 +12,8 @@ public async Task Test_1_Moderate() { Assert.IsNotNull(OpenAIClient.ModerationsEndpoint); - var violationResponse = await OpenAIClient.ModerationsEndpoint.GetModerationAsync("I want to kill them."); - Assert.IsTrue(violationResponse); + var isViolation = await OpenAIClient.ModerationsEndpoint.GetModerationAsync("I want to kill them."); + Assert.IsTrue(isViolation); var response = await OpenAIClient.ModerationsEndpoint.CreateModerationAsync(new ModerationsRequest("I love you")); Assert.IsNotNull(response); diff --git a/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs b/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs new file mode 100644 index 00000000..273d68f7 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_11_Assistants.cs @@ -0,0 +1,154 @@ +using NUnit.Framework; +using OpenAI.Assistants; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace OpenAI.Tests +{ + internal class TestFixture_11_Assistants : AbstractTestFixture + { + private static AssistantResponse testAssistant; + + [Test] + public async Task Test_01_CreateAssistant() + { + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + const string testFilePath = "assistant_test_1.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + 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", + name: "test-assistant", + description: "Used for unit testing.", + instructions: "You are test assistant", + metadata: new Dictionary + { + ["int"] = "1", + ["test"] = Guid.NewGuid().ToString() + }, + tools: new[] + { + Tool.Retrieval + }, + files: new[] { file.Id }); + var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(request); + Assert.IsNotNull(assistant); + 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.IsNotEmpty(assistant.Metadata); + testAssistant = assistant; + Console.WriteLine($"{assistant} -> {assistant.Metadata["test"]}"); + } + + [Test] + public async Task Test_02_ListAssistants() + { + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var assistantsList = await OpenAIClient.AssistantsEndpoint.ListAssistantsAsync(); + Assert.IsNotNull(assistantsList); + Assert.IsNotEmpty(assistantsList.Items); + + foreach (var assistant in assistantsList.Items) + { + var retrieved = OpenAIClient.AssistantsEndpoint.RetrieveAssistantAsync(assistant); + Assert.IsNotNull(retrieved); + Console.WriteLine($"{assistant} -> {assistant.CreatedAt}"); + } + } + + [Test] + public async Task Test_03_ModifyAssistants() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var request = new CreateAssistantRequest( + model: "gpt-4-1106-preview", + name: "Test modified", + description: "Modified description", + instructions: "You are modified test assistant"); + var assistant = await testAssistant.ModifyAsync(request); + Assert.IsNotNull(assistant); + 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.IsTrue(assistant.Metadata.ContainsKey("test")); + Console.WriteLine($"{assistant.Id} -> modified"); + } + + [Test] + public async Task Test_04_01_UploadAssistantFile() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + const string testFilePath = "assistant_test_2.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + var file = testAssistant.UploadFileAsync(testFilePath); + Assert.IsNotNull(file); + } + + [Test] + public async Task Test_04_02_ListAssistantFiles() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsNotEmpty(filesList.Items); + + foreach (var file in filesList.Items) + { + Assert.IsNotNull(file); + var retrieved = await testAssistant.RetrieveFileAsync(file); + Assert.IsNotNull(retrieved); + Assert.IsTrue(retrieved.Id == file.Id); + Console.WriteLine($"{retrieved.AssistantId}'s file -> {retrieved.Id}"); + // TODO 400 Bad Request error when attempting to download assistant files. Likely OpenAI bug. + //var downloadPath = await retrieved.DownloadFileAsync(Directory.GetCurrentDirectory(), true); + //Console.WriteLine($"downloaded {retrieved} -> {downloadPath}"); + //Assert.IsTrue(File.Exists(downloadPath)); + //File.Delete(downloadPath); + //Assert.IsFalse(File.Exists(downloadPath)); + } + } + + [Test] + public async Task Test_04_03_DeleteAssistantFiles() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsNotEmpty(filesList.Items); + + foreach (var file in filesList.Items) + { + Assert.IsNotNull(file); + var isDeleted = await testAssistant.DeleteFileAsync(file); + Assert.IsTrue(isDeleted); + Console.WriteLine($"Deleted {file.Id}"); + } + + filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsEmpty(filesList.Items); + } + + [Test] + public async Task Test_05_DeleteAssistant() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var result = await testAssistant.DeleteAsync(); + Assert.IsTrue(result); + Console.WriteLine($"{testAssistant.Id} -> deleted"); + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs new file mode 100644 index 00000000..e7823a47 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs @@ -0,0 +1,448 @@ +using NUnit.Framework; +using OpenAI.Assistants; +using OpenAI.Files; +using OpenAI.Tests.Weather; +using OpenAI.Threads; +using System; +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 +{ + /// + /// https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb + /// + internal class TestFixture_12_Threads : AbstractTestFixture + { + private static RunResponse testRun; + private static ThreadResponse testThread; + private static MessageResponse testMessage; + private static AssistantResponse testAssistant; + + [Test] + public async Task Test_01_CreateThread() + { + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(new CreateThreadRequest( + new List + { + "Test message" + }, + new Dictionary + { + ["test"] = nameof(Test_01_CreateThread) + })); + Assert.IsNotNull(thread); + Assert.IsNotNull(thread.Metadata); + Assert.IsNotEmpty(thread.Metadata); + testThread = thread; + Console.WriteLine($"Create thread {thread.Id} -> {thread.CreatedAt}"); + } + + [Test] + public async Task Test_02_RetrieveThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var thread = await testThread.UpdateAsync(); + Assert.IsNotNull(thread); + Assert.AreEqual(testThread.Id, thread.Id); + Assert.IsNotNull(thread.Metadata); + Console.WriteLine($"Retrieve thread {thread.Id} -> {thread.CreatedAt}"); + } + + [Test] + public async Task Test_03_ModifyThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var newMetadata = new Dictionary + { + ["test"] = nameof(Test_03_ModifyThread) + }; + var thread = await testThread.ModifyAsync(newMetadata); + Assert.IsNotNull(thread); + Assert.AreEqual(testThread.Id, thread.Id); + Assert.IsNotNull(thread.Metadata); + Console.WriteLine($"Modify thread {thread.Id} -> {thread.Metadata["test"]}"); + } + + [Test] + public async Task Test_04_01_CreateMessage() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + const string testFilePath = "assistant_test_1.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(testFilePath, "assistants"); + Assert.NotNull(file); + File.Delete(testFilePath); + Assert.IsFalse(File.Exists(testFilePath)); + await testThread.CreateMessageAsync("hello world!"); + var request = new CreateMessageRequest("Test create message", + new[] { file.Id }, + new Dictionary + { + ["test"] = nameof(Test_04_01_CreateMessage) + }); + MessageResponse message; + try + { + message = await testThread.CreateMessageAsync(request); + } + finally + { + await CleanupFileAsync(file); + } + + Assert.IsNotNull(message); + Assert.AreEqual(testThread.Id, message.ThreadId); + testMessage = message; + } + + [Test] + public async Task Test_04_02_ListMessages() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var message1 = await testThread.CreateMessageAsync("Test message 1"); + Assert.IsNotNull(message1); + var message2 = await testThread.CreateMessageAsync("Test message 2"); + Assert.IsNotNull(message2); + var list = await testThread.ListMessagesAsync(); + Assert.IsNotNull(list); + Assert.IsNotEmpty(list.Items); + + foreach (var message in list.Items) + { + Assert.NotNull(message); + var threadMessage = await testThread.RetrieveMessageAsync(message); + Assert.NotNull(threadMessage); + Console.WriteLine($"[{threadMessage.Id}] {threadMessage.Role}: {threadMessage.PrintContent()}"); + var updated = await message.UpdateAsync(); + Assert.IsNotNull(updated); + } + } + + [Test] + public async Task Test_04_03_ModifyMessage() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var metadata = new Dictionary + { + ["test"] = nameof(Test_04_03_ModifyMessage) + }; + var modified = await testMessage.ModifyAsync(metadata); + Assert.IsNotNull(modified); + Assert.IsNotNull(modified.Metadata); + Assert.IsTrue(modified.Metadata["test"].Equals(nameof(Test_04_03_ModifyMessage))); + Console.WriteLine($"Modify message metadata: {modified.Id} -> {modified.Metadata["test"]}"); + metadata.Add("test2", nameof(Test_04_03_ModifyMessage)); + var modifiedThreadMessage = await testThread.ModifyMessageAsync(modified, metadata); + Assert.IsNotNull(modifiedThreadMessage); + Assert.IsNotNull(modifiedThreadMessage.Metadata); + Console.WriteLine($"Modify message metadata: {modifiedThreadMessage.Id} -> {string.Join("\n", modifiedThreadMessage.Metadata.Select(meta => $"[{meta.Key}] {meta.Value}"))}"); + } + + [Test] + public async Task Test_04_04_UploadAndDownloadMessageFiles() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var file1 = await CreateTestFileAsync("test_1.txt"); + var file2 = await CreateTestFileAsync("test_2.txt"); + try + { + var createRequest = new CreateMessageRequest("Test content with files", new[] { file1.Id, file2.Id }); + var message = await testThread.CreateMessageAsync(createRequest); + var fileList = await message.ListFilesAsync(); + Assert.IsNotNull(fileList); + Assert.AreEqual(2, fileList.Items.Count); + + foreach (var file in fileList.Items) + { + var retrieved = await message.RetrieveFileAsync(file); + Assert.IsNotNull(retrieved); + Console.WriteLine(file.Id); + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + //var filePath = await message.DownloadFileContentAsync(file, Directory.GetCurrentDirectory(), true); + //Assert.IsFalse(string.IsNullOrWhiteSpace(filePath)); + //Assert.IsTrue(File.Exists(filePath)); + //File.Delete(filePath); + } + + var threadList = await testThread.ListFilesAsync(message); + Assert.IsNotNull(threadList); + Assert.IsNotEmpty(threadList.Items); + + //foreach (var file in threadList.Items) + //{ + // // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + // var filePath = await file.DownloadContentAsync(Directory.GetCurrentDirectory(), true); + // Assert.IsFalse(string.IsNullOrWhiteSpace(filePath)); + // Assert.IsTrue(File.Exists(filePath)); + // File.Delete(filePath); + //} + } + finally + { + await CleanupFileAsync(file1); + await CleanupFileAsync(file2); + } + } + + [Test] + public async Task Test_05_DeleteThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + Console.WriteLine($"Deleted thread {testThread.Id}"); + } + + + + [Test] + public async Task Test_06_01_CreateRun() + { + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var assistant = await OpenAIClient.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")); + Assert.NotNull(assistant); + testAssistant = assistant; + var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(); + Assert.NotNull(thread); + + try + { + var message = thread.CreateMessageAsync("I need to solve the equation `3x + 11 = 14`. Can you help me?"); + Assert.NotNull(message); + var run = await thread.CreateRunAsync(assistant); + Assert.IsNotNull(run); + var threadRun = thread.CreateRunAsync(); + Assert.NotNull(threadRun); + } + finally + { + await thread.DeleteAsync(); + } + } + + [Test] + public async Task Test_06_02_CreateThreadAndRun() + { + Assert.NotNull(testAssistant); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + 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 testAssistant.CreateThreadAndRunAsync(threadRequest); + Assert.IsNotNull(run); + Console.WriteLine($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}"); + testRun = run; + var thread = await run.GetThreadAsync(); + Assert.NotNull(thread); + testThread = thread; + } + + [Test] + public async Task Test_06_03_ListRunsAndSteps() + { + Assert.NotNull(testThread); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var runList = await testThread.ListRunsAsync(); + Assert.IsNotNull(runList); + Assert.IsNotEmpty(runList.Items); + + foreach (var run in runList.Items) + { + Assert.IsNotNull(run); + Assert.IsNotNull(run.Client); + var retrievedRun = await run.UpdateAsync(); + Assert.IsNotNull(retrievedRun); + Console.WriteLine($"[{retrievedRun.Id}] {retrievedRun.Status} | {retrievedRun.CreatedAt}"); + } + } + + [Test] + public async Task Test_06_04_ModifyRun() + { + Assert.NotNull(testRun); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + // run in Queued and InProgress can't be modified + var run = await testRun.WaitForStatusChangeAsync(); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Completed); + var metadata = new Dictionary + { + ["test"] = nameof(Test_06_04_ModifyRun) + }; + var modified = await run.ModifyAsync(metadata); + Assert.IsNotNull(modified); + Assert.AreEqual(run.Id, modified.Id); + Assert.IsNotNull(modified.Metadata); + Assert.Contains("test", modified.Metadata.Keys.ToList()); + Assert.AreEqual(nameof(Test_06_04_ModifyRun), modified.Metadata["test"]); + } + + [Test] + public async Task Test_06_05_CancelRun() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(testAssistant); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var run = await testThread.CreateRunAsync(testAssistant); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Queued); + run = await run.CancelAsync(); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Cancelling); + + try + { + // waiting while run is cancelling + run = await run.WaitForStatusChangeAsync(); + } + catch (Exception e) + { + // Sometimes runs will get stuck in Cancelling state, + // for now we just log when it happens. + Console.WriteLine(e); + } + + Assert.IsTrue(run.Status is RunStatus.Cancelled or RunStatus.Cancelling); + } + + [Test] + public async Task Test_06_06_TestCleanup() + { + if (testAssistant != null) + { + var isDeleted = await testAssistant.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + + if (testThread != null) + { + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + } + + [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?"); + testThread = await run.GetThreadAsync(); + // waiting while run is Queued and InProgress + run = await run.WaitForStatusChangeAsync(); + Assert.IsNotNull(run); + Assert.AreEqual(RunStatus.RequiresAction, run.Status); + Assert.IsNotNull(run.RequiredAction); + Assert.IsNotNull(run.RequiredAction.SubmitToolOutputs); + Assert.IsNotEmpty(run.RequiredAction.SubmitToolOutputs.ToolCalls); + + var runStepList = await run.ListRunStepsAsync(); + Assert.IsNotNull(runStepList); + Assert.IsNotEmpty(runStepList.Items); + + foreach (var runStep in runStepList.Items) + { + Assert.IsNotNull(runStep); + Assert.IsNotNull(runStep.Client); + var retrievedRunStep = await runStep.UpdateAsync(); + Assert.IsNotNull(retrievedRunStep); + Console.WriteLine($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); + var retrieveStepRunStep = await run.RetrieveRunStepAsync(runStep.Id); + Assert.IsNotNull(retrieveStepRunStep); + } + + 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.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); + // waiting while run in Queued and InProgress + run = await run.WaitForStatusChangeAsync(); + Assert.AreEqual(RunStatus.Completed, run.Status); + var messages = await run.ListMessagesAsync(); + Assert.IsNotNull(messages); + Assert.IsNotEmpty(messages.Items); + + foreach (var message in messages.Items.OrderBy(response => response.CreatedAt)) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message.Content); + Console.WriteLine($"{message.Role}: {message.PrintContent()}"); + } + } + + [Test] + public async Task Test_07_02_TestCleanup() + { + if (testAssistant != null) + { + var isDeleted = await testAssistant.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + + if (testThread != null) + { + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + } + + private async Task CreateTestFileAsync(string filePath) + { + await File.WriteAllTextAsync(filePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(filePath)); + var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(filePath, "assistants"); + File.Delete(filePath); + Assert.IsFalse(File.Exists(filePath)); + return file; + } + + private async Task CleanupFileAsync(FileResponse file) + { + var isDeleted = await OpenAIClient.FilesEndpoint.DeleteFileAsync(file); + Assert.IsTrue(isDeleted); + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs b/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs new file mode 100644 index 00000000..94d53cf6 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestServices/WeatherArgs.cs @@ -0,0 +1,15 @@ +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 4158e4cd..eb8c962d 100644 --- a/OpenAI-DotNet-Tests/TestServices/WeatherService.cs +++ b/OpenAI-DotNet-Tests/TestServices/WeatherService.cs @@ -1,23 +1,8 @@ -using System.Text.Json.Serialization; - -namespace OpenAI.Tests.Weather +namespace OpenAI.Tests.Weather { internal class WeatherService { public static string GetCurrentWeather(WeatherArgs weatherArgs) - { - return $"The current weather in {weatherArgs.Location} is 20 {weatherArgs.Unit}"; - } - } - - 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; } + => $"The current weather in {weatherArgs.Location} is 20 {weatherArgs.Unit}"; } } \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/AssistantExtensions.cs b/OpenAI-DotNet/Assistants/AssistantExtensions.cs new file mode 100644 index 00000000..cfcd84cc --- /dev/null +++ b/OpenAI-DotNet/Assistants/AssistantExtensions.cs @@ -0,0 +1,158 @@ +using OpenAI.Files; +using OpenAI.Threads; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI.Assistants +{ + public static class AssistantExtensions + { + /// + /// Modify the assistant. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task ModifyAsync(this AssistantResponse assistant, CreateAssistantRequest request, CancellationToken cancellationToken = default) + { + request = new CreateAssistantRequest(assistant: assistant, model: request.Model, name: request.Name, description: request.Description, instructions: request.Instructions, tools: request.Tools, files: request.FileIds, metadata: request.Metadata); + return await assistant.Client.AssistantsEndpoint.ModifyAssistantAsync(assistantId: assistant.Id, request: request, cancellationToken: cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + } + + /// + /// Delete the assistant. + /// + /// . + /// Optional, . + /// True, if the was successfully deleted. + public static async Task DeleteAsync(this AssistantResponse assistant, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.DeleteAssistantAsync(assistant.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Create a thread and run it. + /// + /// . + /// Optional, . + /// Optional, . + /// . + public static async Task CreateThreadAndRunAsync(this AssistantResponse assistant, CreateThreadRequest request = null, CancellationToken cancellationToken = default) + => await assistant.Client.ThreadsEndpoint.CreateThreadAndRunAsync(new CreateThreadAndRunRequest(assistant.Id, createThreadRequest: request), cancellationToken).ConfigureAwait(false); + + #region Files + + /// + /// Returns a list of assistant files. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this AssistantResponse assistant, ListQuery query = null, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.ListFilesAsync(assistant.Id, query, cancellationToken).ConfigureAwait(false); + + /// + /// Attach a file to the . + /// + /// . + /// + /// A (with purpose="assistants") that the assistant should use. + /// Useful for tools like retrieval and code_interpreter that can access files. + /// + /// Optional, . + /// . + public static async Task AttachFileAsync(this AssistantResponse assistant, FileResponse file, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.AttachFileAsync(assistant.Id, file, cancellationToken).ConfigureAwait(false); + + /// + /// Uploads a new file at the specified and attaches it to the . + /// + /// . + /// The local file path to upload. + /// Optional, . + /// . + public static async Task UploadFileAsync(this AssistantResponse assistant, string filePath, CancellationToken cancellationToken = default) + { + var file = await assistant.Client.FilesEndpoint.UploadFileAsync(new FileUploadRequest(filePath, "assistant"), cancellationToken).ConfigureAwait(false); + return await assistant.AttachFileAsync(file, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves the . + /// + /// . + /// The ID of the file we're getting. + /// Optional, . + /// . + public static async Task RetrieveFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.RetrieveFileAsync(assistant.Id, fileId, cancellationToken).ConfigureAwait(false); + + // TODO 400 bad request errors. Likely OpenAI bug downloading assistant file content. + ///// + ///// Downloads the to the specified . + ///// + ///// . + ///// The directory to download the file into. + ///// Optional, delete the cached file. Defaults to false. + ///// Optional, . + ///// The full path of the downloaded file. + //public static async Task DownloadFileAsync(this AssistantFileResponse assistantFile, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await assistantFile.Client.FilesEndpoint.DownloadFileAsync(assistantFile.Id, directory, deleteCachedFile, cancellationToken).ConfigureAwait(false); + + /// + /// Remove AssistantFile. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use . + /// + /// . + /// Optional, . + /// True, if file was removed. + public static async Task RemoveFileAsync(this AssistantFileResponse file, CancellationToken cancellationToken = default) + => await file.Client.AssistantsEndpoint.RemoveFileAsync(file.AssistantId, file.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Remove AssistantFile. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use . + /// + /// . + /// The ID of the file to remove. + /// Optional, . + /// True, if file was removed. + public static async Task RemoveFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.RemoveFileAsync(assistant.Id, fileId, cancellationToken).ConfigureAwait(false); + + /// + /// Removes and Deletes a file from the assistant. + /// + /// . + /// Optional, . + /// True, if the file was successfully removed from the assistant and deleted. + public static async Task DeleteFileAsync(this AssistantFileResponse file, CancellationToken cancellationToken = default) + { + var isRemoved = await file.RemoveFileAsync(cancellationToken).ConfigureAwait(false); + return isRemoved && await file.Client.FilesEndpoint.DeleteFileAsync(file.Id, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes and Deletes a file from the . + /// + /// . + /// The ID of the file to delete. + /// Optional, . + /// True, if the file was successfully removed from the assistant and deleted. + public static async Task DeleteFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + { + var isRemoved = await assistant.Client.AssistantsEndpoint.RemoveFileAsync(assistant.Id, fileId, cancellationToken).ConfigureAwait(false); + return isRemoved && await assistant.Client.FilesEndpoint.DeleteFileAsync(fileId, cancellationToken).ConfigureAwait(false); + } + + #endregion Files + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/AssistantFileResponse.cs b/OpenAI-DotNet/Assistants/AssistantFileResponse.cs new file mode 100644 index 00000000..b6dc132a --- /dev/null +++ b/OpenAI-DotNet/Assistants/AssistantFileResponse.cs @@ -0,0 +1,46 @@ +using System; +using System.Text.Json.Serialization; + +namespace OpenAI.Assistants +{ + /// + /// File attached to an assistant. + /// + public sealed class AssistantFileResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always assistant.file. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the assistant file was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The assistant ID that the file is attached to. + /// + [JsonInclude] + [JsonPropertyName("assistant_id")] + public string AssistantId { get; private set; } + + public static implicit operator string(AssistantFileResponse file) => file?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/AssistantResponse.cs b/OpenAI-DotNet/Assistants/AssistantResponse.cs new file mode 100644 index 00000000..9d6e0b65 --- /dev/null +++ b/OpenAI-DotNet/Assistants/AssistantResponse.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Assistants +{ + /// + /// Purpose-built AI that uses OpenAI�s models and calls tools. + /// + public sealed class AssistantResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always assistant. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the assistant was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + [JsonInclude] + [JsonPropertyName("name")] + public string Name { get; private set; } + + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + [JsonInclude] + [JsonPropertyName("description")] + public string Description { get; private set; } + + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + [JsonInclude] + [JsonPropertyName("model")] + public string Model { get; private set; } + + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + [JsonInclude] + [JsonPropertyName("instructions")] + public string Instructions { get; private set; } + + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + [JsonInclude] + [JsonPropertyName("tools")] + public IReadOnlyList Tools { get; private set; } + + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + [JsonInclude] + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; private set; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonInclude] + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; private set; } + + public static implicit operator string(AssistantResponse assistant) => assistant?.Id; + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs new file mode 100644 index 00000000..230368b9 --- /dev/null +++ b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs @@ -0,0 +1,159 @@ +using OpenAI.Extensions; +using OpenAI.Files; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI.Assistants +{ + public sealed class AssistantsEndpoint : BaseEndPoint + { + internal AssistantsEndpoint(OpenAIClient api) : base(api) { } + + protected override string Root => "assistants"; + + /// + /// Get list of assistants. + /// + /// . + /// Optional, . + /// + public async Task> ListAssistantsAsync(ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Create an assistant. + /// + /// . + /// Optional, . + /// . + public async Task CreateAssistantAsync(CreateAssistantRequest request = null, CancellationToken cancellationToken = default) + { + request ??= new CreateAssistantRequest(); + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Retrieves an assistant. + /// + /// The ID of the assistant to retrieve. + /// Optional, . + /// . + public async Task RetrieveAssistantAsync(string assistantId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Modifies an assistant. + /// + /// The ID of the assistant to modify. + /// . + /// Optional, . + /// . + public async Task ModifyAssistantAsync(string assistantId, CreateAssistantRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{assistantId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Delete an assistant. + /// + /// The ID of the assistant to delete. + /// Optional, . + /// True, if the assistant was deleted. + public async Task DeleteAssistantAsync(string assistantId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.DeleteAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #region Files + + /// + /// Returns a list of assistant files. + /// + /// The ID of the assistant the file belongs to. + /// . + /// Optional, . + /// . + public async Task> ListFilesAsync(string assistantId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{assistantId}/files", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Attach a file to an assistant. + /// + /// The ID of the assistant for which to attach a file. + /// + /// A (with purpose="assistants") that the assistant should use. + /// Useful for tools like retrieval and code_interpreter that can access files. + /// + /// Optional, . + /// . + public async Task AttachFileAsync(string assistantId, FileResponse file, CancellationToken cancellationToken = default) + { + if (file?.Purpose?.Equals("assistants") != true) + { + throw new InvalidOperationException($"{nameof(file)}.{nameof(file.Purpose)} must be 'assistants'!"); + } + + var jsonContent = JsonSerializer.Serialize(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Retrieves an AssistantFile. + /// + /// The ID of the assistant who the file belongs to. + /// The ID of the file we're getting. + /// Optional, . + /// . + public async Task RetrieveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Remove an assistant file. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use the File delete endpoint instead. + /// + /// The ID of the assistant that the file belongs to. + /// The ID of the file to delete. + /// Optional, . + /// True, if file was removed. + public async Task RemoveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #endregion Files + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs new file mode 100644 index 00000000..0a732f8a --- /dev/null +++ b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Assistants +{ + public sealed class CreateAssistantRequest + { + /// + /// Constructor + /// + /// + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + public CreateAssistantRequest(AssistantResponse assistant, string model = null, string name = null, string description = null, string instructions = null, IEnumerable tools = null, IEnumerable files = null, IReadOnlyDictionary metadata = null) + : this(string.IsNullOrWhiteSpace(model) ? assistant.Model : model, string.IsNullOrWhiteSpace(name) ? assistant.Name : name, string.IsNullOrWhiteSpace(description) ? assistant.Description : description, string.IsNullOrWhiteSpace(instructions) ? assistant.Instructions : instructions, tools ?? assistant.Tools, files ?? assistant.FileIds, metadata ?? assistant.Metadata) + { + } + + /// + /// Constructor. + /// + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + public CreateAssistantRequest(string model = null, string name = null, string description = null, string instructions = null, IEnumerable tools = null, IEnumerable files = null, IReadOnlyDictionary metadata = null) + { + Model = string.IsNullOrWhiteSpace(model) ? Models.Model.GPT3_5_Turbo : model; + Name = name; + Description = description; + Instructions = instructions; + Tools = tools?.ToList(); + FileIds = files?.ToList(); + Metadata = metadata; + } + + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + [JsonPropertyName("model")] + public string Model { get; } + + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + [JsonPropertyName("description")] + public string Description { get; } + + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + [JsonPropertyName("instructions")] + public string Instructions { get; } + + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + [JsonPropertyName("tools")] + public IReadOnlyList Tools { get; } + + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs index bb4284a0..8de57131 100644 --- a/OpenAI-DotNet/Audio/AudioEndpoint.cs +++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs @@ -1,5 +1,5 @@ -using System; -using OpenAI.Extensions; +using OpenAI.Extensions; +using System; using System.IO; using System.Net.Http; using System.Text.Json; diff --git a/OpenAI-DotNet/AuthInfo.cs b/OpenAI-DotNet/Authentication/AuthInfo.cs similarity index 100% rename from OpenAI-DotNet/AuthInfo.cs rename to OpenAI-DotNet/Authentication/AuthInfo.cs diff --git a/OpenAI-DotNet/OpenAIAuthentication.cs b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs similarity index 100% rename from OpenAI-DotNet/OpenAIAuthentication.cs rename to OpenAI-DotNet/Authentication/OpenAIAuthentication.cs diff --git a/OpenAI-DotNet/OpenAIClientSettings.cs b/OpenAI-DotNet/Authentication/OpenAIClientSettings.cs similarity index 100% rename from OpenAI-DotNet/OpenAIClientSettings.cs rename to OpenAI-DotNet/Authentication/OpenAIClientSettings.cs diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs index c41b7c01..cd4ba12a 100644 --- a/OpenAI-DotNet/Chat/ChatEndpoint.cs +++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs @@ -33,7 +33,7 @@ public async Task GetCompletionAsync(ChatRequest chatRequest, Canc var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl("/completions"), jsonContent, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return response.DeserializeResponse(responseAsString, OpenAIClient.JsonSerializationOptions); + return response.Deserialize(responseAsString, Api); } /// @@ -43,7 +43,6 @@ public async Task GetCompletionAsync(ChatRequest chatRequest, Canc /// An action to be called as each new result arrives. /// Optional, . /// . - /// Raised when the HTTP request fails public async Task StreamCompletionAsync(ChatRequest chatRequest, Action resultHandler, CancellationToken cancellationToken = default) { chatRequest.Stream = true; @@ -68,7 +67,7 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A Console.WriteLine(eventData); } - var partialResponse = response.DeserializeResponse(eventData, OpenAIClient.JsonSerializationOptions); + var partialResponse = response.Deserialize(eventData, Api); if (chatResponse == null) { @@ -86,7 +85,7 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A if (chatResponse == null) { return null; } - chatResponse.SetResponseData(response.Headers); + chatResponse.SetResponseData(response.Headers, Api); resultHandler?.Invoke(chatResponse); return chatResponse; } @@ -99,7 +98,6 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A /// The chat request which contains the message content. /// Optional, . /// . - /// Raised when the HTTP request fails public async IAsyncEnumerable StreamCompletionEnumerableAsync(ChatRequest chatRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default) { chatRequest.Stream = true; @@ -124,7 +122,7 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync(Chat Console.WriteLine(eventData); } - var partialResponse = response.DeserializeResponse(eventData, OpenAIClient.JsonSerializationOptions); + var partialResponse = response.Deserialize(eventData, Api); if (chatResponse == null) { @@ -142,7 +140,7 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync(Chat if (chatResponse == null) { yield break; } - chatResponse.SetResponseData(response.Headers); + chatResponse.SetResponseData(response.Headers, Api); yield return chatResponse; } } diff --git a/OpenAI-DotNet/Chat/Choice.cs b/OpenAI-DotNet/Chat/Choice.cs index dd357c36..87f12862 100644 --- a/OpenAI-DotNet/Chat/Choice.cs +++ b/OpenAI-DotNet/Chat/Choice.cs @@ -28,7 +28,7 @@ public sealed class Choice public override string ToString() => Message?.Content?.ToString() ?? Delta?.Content ?? string.Empty; - public static implicit operator string(Choice choice) => choice.ToString(); + public static implicit operator string(Choice choice) => choice?.ToString(); internal void CopyFrom(Choice other) { diff --git a/OpenAI-DotNet/Chat/Delta.cs b/OpenAI-DotNet/Chat/Delta.cs index ac2b8248..9cd6e0e4 100644 --- a/OpenAI-DotNet/Chat/Delta.cs +++ b/OpenAI-DotNet/Chat/Delta.cs @@ -7,7 +7,7 @@ namespace OpenAI.Chat public sealed class Delta { /// - /// The of the author of this message. + /// The of the author of this message. /// [JsonInclude] [JsonPropertyName("role")] diff --git a/OpenAI-DotNet/Chat/FinishDetails.cs b/OpenAI-DotNet/Chat/FinishDetails.cs index d1ab531e..7d2f49fc 100644 --- a/OpenAI-DotNet/Chat/FinishDetails.cs +++ b/OpenAI-DotNet/Chat/FinishDetails.cs @@ -10,6 +10,6 @@ public sealed class FinishDetails public override string ToString() => Type; - public static implicit operator string(FinishDetails details) => details.ToString(); + public static implicit operator string(FinishDetails details) => details?.ToString(); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/Message.cs b/OpenAI-DotNet/Chat/Message.cs index 36edc542..4c5a47de 100644 --- a/OpenAI-DotNet/Chat/Message.cs +++ b/OpenAI-DotNet/Chat/Message.cs @@ -23,7 +23,7 @@ public Message(Role role, string content, string name, Function function) /// Creates a new message to insert into a chat conversation. /// /// - /// The of the author of this message. + /// The of the author of this message. /// /// /// The contents of the message. @@ -40,7 +40,7 @@ public Message(Role role, IEnumerable content, string name = null) /// Creates a new message to insert into a chat conversation. /// /// - /// The of the author of this message. + /// The of the author of this message. /// /// /// The contents of the message. @@ -72,7 +72,7 @@ public Message(Tool tool, IEnumerable content) } /// - /// The of the author of this message. + /// The of the author of this message. /// [JsonInclude] [JsonPropertyName("role")] diff --git a/OpenAI-DotNet/BaseEndPoint.cs b/OpenAI-DotNet/Common/BaseEndPoint.cs similarity index 100% rename from OpenAI-DotNet/BaseEndPoint.cs rename to OpenAI-DotNet/Common/BaseEndPoint.cs diff --git a/OpenAI-DotNet/BaseResponse.cs b/OpenAI-DotNet/Common/BaseResponse.cs similarity index 91% rename from OpenAI-DotNet/BaseResponse.cs rename to OpenAI-DotNet/Common/BaseResponse.cs index 3ecc409a..1e29dda7 100644 --- a/OpenAI-DotNet/BaseResponse.cs +++ b/OpenAI-DotNet/Common/BaseResponse.cs @@ -5,6 +5,12 @@ namespace OpenAI { public abstract class BaseResponse { + /// + /// The this response was generated from. + /// + [JsonIgnore] + public OpenAIClient Client { get; internal set; } + /// /// The server-side processing time as reported by the API. This can be useful for debugging where a delay occurs. /// @@ -33,7 +39,7 @@ public abstract class BaseResponse /// The maximum number of requests that are permitted before exhausting the rate limit. /// - [JsonIgnore] + [JsonIgnore] public int? LimitRequests { get; internal set; } /// diff --git a/OpenAI-DotNet/Chat/ContentType.cs b/OpenAI-DotNet/Common/ContentType.cs similarity index 89% rename from OpenAI-DotNet/Chat/ContentType.cs rename to OpenAI-DotNet/Common/ContentType.cs index 79a75059..35981cc3 100644 --- a/OpenAI-DotNet/Chat/ContentType.cs +++ b/OpenAI-DotNet/Common/ContentType.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace OpenAI.Chat +namespace OpenAI { public enum ContentType { diff --git a/OpenAI-DotNet/Common/DeletedResponse.cs b/OpenAI-DotNet/Common/DeletedResponse.cs new file mode 100644 index 00000000..d6e2db3b --- /dev/null +++ b/OpenAI-DotNet/Common/DeletedResponse.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace OpenAI +{ + internal sealed class DeletedResponse + { + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + [JsonInclude] + [JsonPropertyName("deleted")] + public bool Deleted { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Event.cs b/OpenAI-DotNet/Common/Event.cs similarity index 58% rename from OpenAI-DotNet/Event.cs rename to OpenAI-DotNet/Common/Event.cs index 865375d5..7193fa20 100644 --- a/OpenAI-DotNet/Event.cs +++ b/OpenAI-DotNet/Common/Event.cs @@ -3,7 +3,8 @@ namespace OpenAI { - public sealed class Event + [Obsolete("use EventResponse")] + public sealed class Event : BaseResponse { [JsonInclude] [JsonPropertyName("object")] @@ -11,10 +12,13 @@ public sealed class Event [JsonInclude] [JsonPropertyName("created_at")] - public int CreatedAtUnixTime { get; private set; } + public int CreatedAtUnixTimeSeconds { get; private set; } + + [Obsolete("use CreatedAtUnixTimeSeconds")] + public int CreatedAtUnixTime => CreatedAtUnixTimeSeconds; [JsonIgnore] - public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTime).DateTime; + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; [JsonInclude] [JsonPropertyName("level")] @@ -23,5 +27,7 @@ public sealed class Event [JsonInclude] [JsonPropertyName("message")] public string Message { get; private set; } + + public static implicit operator EventResponse(Event @event) => new EventResponse(@event); } } diff --git a/OpenAI-DotNet/Common/EventResponse.cs b/OpenAI-DotNet/Common/EventResponse.cs new file mode 100644 index 00000000..46a9f534 --- /dev/null +++ b/OpenAI-DotNet/Common/EventResponse.cs @@ -0,0 +1,39 @@ +using System; +using System.Text.Json.Serialization; + +namespace OpenAI +{ + public sealed class EventResponse : BaseResponse + { + public EventResponse() { } + +#pragma warning disable CS0618 // Type or member is obsolete + internal EventResponse(Event @event) + { + Object = @event.Object; + CreatedAtUnixTimeSeconds = @event.CreatedAtUnixTimeSeconds; + Level = @event.Level; + Message = @event.Message; + } +#pragma warning restore CS0618 // Type or member is obsolete + + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + [JsonInclude] + [JsonPropertyName("level")] + public string Level { get; private set; } + + [JsonInclude] + [JsonPropertyName("message")] + public string Message { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/Function.cs b/OpenAI-DotNet/Common/Function.cs similarity index 96% rename from OpenAI-DotNet/Chat/Function.cs rename to OpenAI-DotNet/Common/Function.cs index 53d89d3a..912988d2 100644 --- a/OpenAI-DotNet/Chat/Function.cs +++ b/OpenAI-DotNet/Common/Function.cs @@ -1,13 +1,15 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace OpenAI.Chat +namespace OpenAI { /// - /// + /// /// public class Function { + public Function() { } + internal Function(Function other) => CopyFrom(other); /// diff --git a/OpenAI-DotNet/Common/IListResponse.cs b/OpenAI-DotNet/Common/IListResponse.cs new file mode 100644 index 00000000..d5120476 --- /dev/null +++ b/OpenAI-DotNet/Common/IListResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace OpenAI +{ + public interface IListResponse + where TObject : BaseResponse + { + IReadOnlyList Items { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/ImageUrl.cs b/OpenAI-DotNet/Common/ImageUrl.cs similarity index 74% rename from OpenAI-DotNet/Chat/ImageUrl.cs rename to OpenAI-DotNet/Common/ImageUrl.cs index 5cf44354..11a5b275 100644 --- a/OpenAI-DotNet/Chat/ImageUrl.cs +++ b/OpenAI-DotNet/Common/ImageUrl.cs @@ -1,14 +1,11 @@ using System.Text.Json.Serialization; -namespace OpenAI.Chat +namespace OpenAI { public sealed class ImageUrl { [JsonConstructor] - public ImageUrl(string url) - { - Url = url; - } + public ImageUrl(string url) => Url = url; [JsonInclude] [JsonPropertyName("url")] diff --git a/OpenAI-DotNet/Common/ListQuery.cs b/OpenAI-DotNet/Common/ListQuery.cs new file mode 100644 index 00000000..54ec5c79 --- /dev/null +++ b/OpenAI-DotNet/Common/ListQuery.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; + +namespace OpenAI +{ + public sealed class ListQuery + { + /// + /// List Query. + /// + /// + /// A limit on the number of objects to be returned. + /// Limit can range between 1 and 100, and the default is 20. + /// + /// + /// Sort order by the 'created_at' timestamp of the objects. + /// + /// + /// A cursor for use in pagination. + /// after is an object ID that defines your place in the list. + /// For instance, if you make a list request and receive 100 objects, ending with obj_foo, + /// your subsequent call can include after=obj_foo in order to fetch the next page of the list. + /// + /// + /// A cursor for use in pagination. before is an object ID that defines your place in the list. + /// For instance, if you make a list request and receive 100 objects, ending with obj_foo, + /// your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + /// + public ListQuery(int? limit = null, SortOrder order = SortOrder.Descending, string after = null, string before = null) + { + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public int? Limit { get; set; } + + public SortOrder Order { get; set; } + + public string After { get; set; } + + public string Before { get; set; } + + public static implicit operator Dictionary(ListQuery query) + { + if (query == null) { return null; } + var parameters = new Dictionary(); + + if (query.Limit.HasValue) + { + parameters.Add("limit", query.Limit.ToString()); + } + + switch (query.Order) + { + case SortOrder.Descending: + parameters.Add("order", "desc"); + break; + case SortOrder.Ascending: + parameters.Add("order", "asc"); + break; + } + + if (!string.IsNullOrEmpty(query.After)) + { + parameters.Add("after", query.After); + } + + if (!string.IsNullOrEmpty(query.Before)) + { + parameters.Add("before", query.Before); + } + + return parameters; + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Common/ListResponse.cs b/OpenAI-DotNet/Common/ListResponse.cs new file mode 100644 index 00000000..18afb19b --- /dev/null +++ b/OpenAI-DotNet/Common/ListResponse.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI +{ + public sealed class ListResponse : BaseResponse, IListResponse + where TObject : BaseResponse + { + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + [JsonInclude] + [JsonPropertyName("data")] + public IReadOnlyList Items { get; private set; } + + [JsonInclude] + [JsonPropertyName("has_more")] + public bool HasMore { get; private set; } + + [JsonInclude] + [JsonPropertyName("first_id")] + public string FirstId { get; private set; } + + [JsonInclude] + [JsonPropertyName("last_id")] + public string LastId { get; private set; } + } +} diff --git a/OpenAI-DotNet/Chat/Role.cs b/OpenAI-DotNet/Common/Role.cs similarity index 88% rename from OpenAI-DotNet/Chat/Role.cs rename to OpenAI-DotNet/Common/Role.cs index 6bf77533..82ab906a 100644 --- a/OpenAI-DotNet/Chat/Role.cs +++ b/OpenAI-DotNet/Common/Role.cs @@ -1,6 +1,6 @@ using System; -namespace OpenAI.Chat +namespace OpenAI { public enum Role { diff --git a/OpenAI-DotNet/Common/SortOrder.cs b/OpenAI-DotNet/Common/SortOrder.cs new file mode 100644 index 00000000..8f92dede --- /dev/null +++ b/OpenAI-DotNet/Common/SortOrder.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace OpenAI +{ + public enum SortOrder + { + [EnumMember(Value = "desc")] + Descending, + [EnumMember(Value = "asc")] + Ascending, + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/Tool.cs b/OpenAI-DotNet/Common/Tool.cs similarity index 85% rename from OpenAI-DotNet/Chat/Tool.cs rename to OpenAI-DotNet/Common/Tool.cs index 331cfb17..8c7b8c66 100644 --- a/OpenAI-DotNet/Chat/Tool.cs +++ b/OpenAI-DotNet/Common/Tool.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace OpenAI.Chat +namespace OpenAI { public sealed class Tool { @@ -14,6 +14,12 @@ public Tool(Function function) Type = nameof(function); } + public static implicit operator Tool(Function function) => new Tool(function); + + public static Tool Retrieval { get; } = new Tool { Type = "retrieval" }; + + public static Tool CodeInterpreter { get; } = new Tool { Type = "code_interpreter" }; + [JsonInclude] [JsonPropertyName("id")] public string Id { get; private set; } @@ -29,10 +35,9 @@ public Tool(Function function) [JsonInclude] [JsonPropertyName("function")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Function Function { get; private set; } - public static implicit operator Tool(Function function) => new Tool(function); - internal void CopyFrom(Tool other) { if (!string.IsNullOrWhiteSpace(other?.Id)) diff --git a/OpenAI-DotNet/Usage.cs b/OpenAI-DotNet/Common/Usage.cs similarity index 100% rename from OpenAI-DotNet/Usage.cs rename to OpenAI-DotNet/Common/Usage.cs diff --git a/OpenAI-DotNet/Completions/CompletionResponse.cs b/OpenAI-DotNet/Completions/CompletionResponse.cs new file mode 100644 index 00000000..4bfad93a --- /dev/null +++ b/OpenAI-DotNet/Completions/CompletionResponse.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Completions +{ + /// + /// Represents a result from calling the . + /// + public sealed class CompletionResponse : BaseResponse + { + public CompletionResponse() { } + +#pragma warning disable CS0618 // Type or member is obsolete + internal CompletionResponse(CompletionResult result) + { + Id = result.Id; + Object = result.Object; + CreatedUnixTimeSeconds = result.CreatedUnixTime; + Model = result.Model; + Completions = result.Completions; + } +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// The identifier of the result, which may be used during troubleshooting + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The time when the result was generated in unix epoch format + /// + [JsonInclude] + [JsonPropertyName("created")] + public int CreatedUnixTimeSeconds { get; private set; } + + /// + /// The time when the result was generated. + /// + [JsonIgnore] + public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTimeSeconds).DateTime; + + [JsonInclude] + [JsonPropertyName("model")] + public string Model { get; private set; } + + /// + /// The completions returned by the API. Depending on your request, there may be 1 or many choices. + /// + [JsonInclude] + [JsonPropertyName("choices")] + public IReadOnlyList Completions { get; private set; } + + [JsonIgnore] + public Choice FirstChoice => Completions?.FirstOrDefault(choice => choice.Index == 0); + + public override string ToString() => FirstChoice?.ToString() ?? string.Empty; + + public static implicit operator string(CompletionResponse response) => response?.ToString(); + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Completions/CompletionResult.cs b/OpenAI-DotNet/Completions/CompletionResult.cs index 6e61d22d..0e05cef9 100644 --- a/OpenAI-DotNet/Completions/CompletionResult.cs +++ b/OpenAI-DotNet/Completions/CompletionResult.cs @@ -8,6 +8,7 @@ namespace OpenAI.Completions /// /// Represents a result from calling the . /// + [Obsolete("use CompletionResponse")] public sealed class CompletionResult : BaseResponse { /// @@ -50,6 +51,8 @@ public sealed class CompletionResult : BaseResponse public override string ToString() => FirstChoice?.ToString() ?? string.Empty; - public static implicit operator string(CompletionResult response) => response.ToString(); + public static implicit operator string(CompletionResult result) => result?.ToString(); + + public static implicit operator CompletionResponse(CompletionResult result) => new CompletionResponse(result); } } diff --git a/OpenAI-DotNet/Completions/CompletionsEndpoint.cs b/OpenAI-DotNet/Completions/CompletionsEndpoint.cs index 9499675b..a9a537f9 100644 --- a/OpenAI-DotNet/Completions/CompletionsEndpoint.cs +++ b/OpenAI-DotNet/Completions/CompletionsEndpoint.cs @@ -50,7 +50,7 @@ internal CompletionsEndpoint(OpenAIClient api) : base(api) { } /// The scale of the penalty for how often a token is used. /// Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. /// Include the log probabilities on the logprobs most likely tokens, which can be found - /// in -> . So for example, if logprobs is 10, + /// in -> . So for example, if logprobs is 10, /// the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob /// of the sampled token, so there may be up to logprobs+1 elements in the response. /// Echo back the prompt in addition to the completion. @@ -59,9 +59,11 @@ internal CompletionsEndpoint(OpenAIClient api) : base(api) { } /// Optional, to use when calling the API. /// Defaults to . /// Optional, . - /// Asynchronously returns the completion result. - /// Look in its property for the completions. - public async Task CreateCompletionAsync( + /// + /// Asynchronously returns the completion result. + /// Look in its property for the completions. + /// + public async Task CreateCompletionAsync( string prompt = null, IEnumerable prompts = null, string suffix = null, @@ -101,16 +103,17 @@ public async Task CreateCompletionAsync( /// /// The request to send to the API. /// Optional, . - /// Asynchronously returns the completion result. - /// Look in its property for the completions. - /// Raised when the HTTP request fails - public async Task CreateCompletionAsync(CompletionRequest completionRequest, CancellationToken cancellationToken = default) + /// + /// Asynchronously returns the completion result. + /// Look in its property for the completions. + /// + public async Task CreateCompletionAsync(CompletionRequest completionRequest, CancellationToken cancellationToken = default) { completionRequest.Stream = false; var jsonContent = JsonSerializer.Serialize(completionRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return response.DeserializeResponse(responseAsString, OpenAIClient.JsonSerializationOptions); + return response.Deserialize(responseAsString, Api); } #endregion Non-Streaming @@ -139,7 +142,7 @@ public async Task CreateCompletionAsync(CompletionRequest comp /// The scale of the penalty for how often a token is used. /// Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. /// Include the log probabilities on the logProbabilities most likely tokens, - /// which can be found in -> . + /// which can be found in -> . /// So for example, if logProbabilities is 10, the API will return a list of the 10 most likely tokens. /// If logProbabilities is supplied, the API will always return the logProbabilities of the sampled token, /// so there may be up to logProbabilities+1 elements in the response. @@ -149,11 +152,13 @@ public async Task CreateCompletionAsync(CompletionRequest comp /// Optional, to use when calling the API. /// Defaults to . /// Optional, . - /// An async enumerable with each of the results as they come in. + /// + /// An async enumerable with each of the results as they come in. /// See the C# docs - /// for more details on how to consume an async enumerable. + /// for more details on how to consume an async enumerable. + /// public async Task StreamCompletionAsync( - Action resultHandler, + Action resultHandler, string prompt = null, IEnumerable prompts = null, string suffix = null, @@ -194,8 +199,7 @@ public async Task StreamCompletionAsync( /// The request to send to the API. /// An action to be called as each new result arrives. /// Optional, . - /// Raised when the HTTP request fails - public async Task StreamCompletionAsync(CompletionRequest completionRequest, Action resultHandler, CancellationToken cancellationToken = default) + public async Task StreamCompletionAsync(CompletionRequest completionRequest, Action resultHandler, CancellationToken cancellationToken = default) { completionRequest.Stream = true; var jsonContent = JsonSerializer.Serialize(completionRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); @@ -214,7 +218,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act { if (string.IsNullOrWhiteSpace(eventData)) { continue; } - resultHandler(response.DeserializeResponse(eventData, OpenAIClient.JsonSerializationOptions)); + resultHandler(response.Deserialize(eventData, Api)); } else { @@ -226,7 +230,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act /// /// Ask the API to complete the prompt(s) using the specified request, and stream the results as they come in.
/// If you are not using C# 8 supporting IAsyncEnumerable{T} or if you are using the .NET Framework, - /// you may need to use instead. + /// you may need to use instead. ///
/// The prompt to generate from /// The prompts to generate from @@ -245,7 +249,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act /// The scale of the penalty for how often a token is used. /// Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. /// Include the log probabilities on the logProbabilities most likely tokens, - /// which can be found in -> . + /// which can be found in -> . /// So for example, if logProbabilities is 10, the API will return a list of the 10 most likely tokens. /// If logProbabilities is supplied, the API will always return the logProbabilities of the sampled token, /// so there may be up to logProbabilities+1 elements in the response. @@ -258,7 +262,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act /// An async enumerable with each of the results as they come in. /// See the C# docs /// for more details on how to consume an async enumerable. - public IAsyncEnumerable StreamCompletionEnumerableAsync( + public IAsyncEnumerable StreamCompletionEnumerableAsync( string prompt = null, IEnumerable prompts = null, string suffix = null, @@ -295,15 +299,16 @@ public IAsyncEnumerable StreamCompletionEnumerableAsync( /// /// Ask the API to complete the prompt(s) using the specified request, and stream the results as they come in.
/// If you are not using C# 8 supporting IAsyncEnumerable{T} or if you are using the .NET Framework, - /// you may need to use instead. + /// you may need to use instead. ///
/// The request to send to the API. /// Optional, . - /// An async enumerable with each of the results as they come in. + /// + /// An async enumerable with each of the results as they come in. /// See - /// for more details on how to consume an async enumerable. - /// Raised when the HTTP request fails - public async IAsyncEnumerable StreamCompletionEnumerableAsync(CompletionRequest completionRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default) + /// for more details on how to consume an async enumerable. + /// + public async IAsyncEnumerable StreamCompletionEnumerableAsync(CompletionRequest completionRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default) { completionRequest.Stream = true; var jsonContent = JsonSerializer.Serialize(completionRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); @@ -321,7 +326,7 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync( if (streamData.TryGetEventStreamData(out var eventData)) { if (string.IsNullOrWhiteSpace(eventData)) { continue; } - yield return response.DeserializeResponse(eventData, OpenAIClient.JsonSerializationOptions); + yield return response.Deserialize(eventData, Api); } else { diff --git a/OpenAI-DotNet/Edits/Choice.cs b/OpenAI-DotNet/Edits/Choice.cs index f9bc27ab..ef5b2eff 100644 --- a/OpenAI-DotNet/Edits/Choice.cs +++ b/OpenAI-DotNet/Edits/Choice.cs @@ -1,7 +1,9 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; namespace OpenAI.Edits { + [Obsolete("Deprecated")] public sealed class Choice { [JsonInclude] diff --git a/OpenAI-DotNet/Edits/EditRequest.cs b/OpenAI-DotNet/Edits/EditRequest.cs index ca19bb84..21e77b18 100644 --- a/OpenAI-DotNet/Edits/EditRequest.cs +++ b/OpenAI-DotNet/Edits/EditRequest.cs @@ -1,7 +1,9 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; namespace OpenAI.Edits { + [Obsolete("Deprecated")] public sealed class EditRequest { /// diff --git a/OpenAI-DotNet/Edits/EditResponse.cs b/OpenAI-DotNet/Edits/EditResponse.cs index 3113b49f..65b56128 100644 --- a/OpenAI-DotNet/Edits/EditResponse.cs +++ b/OpenAI-DotNet/Edits/EditResponse.cs @@ -4,22 +4,26 @@ namespace OpenAI.Edits { + [Obsolete("deprecated")] public sealed class EditResponse : BaseResponse { [JsonInclude] [JsonPropertyName("object")] public string Object { get; private set; } - /// - /// The time when the result was generated in unix epoch format - /// [JsonInclude] [JsonPropertyName("created")] - public int CreatedUnixTime { get; private set; } + public int CreatedAtUnixTimeSeconds { get; private set; } + + [Obsolete("use CreatedAtUnixTimeSeconds")] + public int CreatedUnixTime => CreatedAtUnixTimeSeconds; + + [JsonIgnore] + [Obsolete("use CreatedAt")] + public DateTime Created => CreatedAt; - /// The time when the result was generated [JsonIgnore] - public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; [JsonInclude] [JsonPropertyName("choices")] @@ -33,10 +37,8 @@ public sealed class EditResponse : BaseResponse /// Gets the text of the first edit, representing the main result /// public override string ToString() - { - return Choices is { Count: > 0 } + => Choices is { Count: > 0 } ? Choices[0] : "Edit result has no valid output"; - } } } diff --git a/OpenAI-DotNet/Edits/EditsEndpoint.cs b/OpenAI-DotNet/Edits/EditsEndpoint.cs index 033aecd3..519f814b 100644 --- a/OpenAI-DotNet/Edits/EditsEndpoint.cs +++ b/OpenAI-DotNet/Edits/EditsEndpoint.cs @@ -1,5 +1,5 @@ -using System; -using OpenAI.Extensions; +using OpenAI.Extensions; +using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -10,7 +10,7 @@ namespace OpenAI.Edits /// Given a prompt and an instruction, the model will return an edited version of the prompt.
/// ///
- [Obsolete] + [Obsolete("Deprecated")] public sealed class EditsEndpoint : BaseEndPoint { /// @@ -64,7 +64,7 @@ public async Task CreateEditAsync(EditRequest request, Cancellatio var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return response.DeserializeResponse(responseAsString, OpenAIClient.JsonSerializationOptions); + return response.Deserialize(responseAsString, Api); } } } diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs index f0da9c4b..a6e0eb30 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs @@ -68,7 +68,7 @@ public async Task CreateEmbeddingAsync(EmbeddingsRequest req var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return response.DeserializeResponse(responseAsString, OpenAIClient.JsonSerializationOptions); + return response.Deserialize(responseAsString, Api); } } } diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs index 3b3a9acd..16554f34 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs @@ -18,7 +18,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. diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index 8706c034..0fc56def 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -29,8 +29,18 @@ internal static class ResponseExtensions NumberDecimalSeparator = "." }; - internal static void SetResponseData(this BaseResponse response, HttpResponseHeaders headers) + internal static void SetResponseData(this BaseResponse response, HttpResponseHeaders headers, OpenAIClient client) { + if (response is IListResponse listResponse) + { + foreach (var item in listResponse.Items) + { + SetResponseData(item, headers, client); + } + } + + response.Client = client; + if (headers == null) { return; } if (headers.TryGetValues(RequestId, out var requestId)) @@ -116,10 +126,10 @@ internal static async Task CheckResponseAsync(this HttpResponseMessage response, } } - internal static T DeserializeResponse(this HttpResponseMessage response, string json, JsonSerializerOptions settings) where T : BaseResponse + internal static T Deserialize(this HttpResponseMessage response, string json, OpenAIClient client) where T : BaseResponse { - var result = JsonSerializer.Deserialize(json, settings); - result.SetResponseData(response.Headers); + var result = JsonSerializer.Deserialize(json, OpenAIClient.JsonSerializationOptions); + result.SetResponseData(response.Headers, client); return result; } } diff --git a/OpenAI-DotNet/Files/FileData.cs b/OpenAI-DotNet/Files/FileData.cs index 2cc9cb31..e781cfd1 100644 --- a/OpenAI-DotNet/Files/FileData.cs +++ b/OpenAI-DotNet/Files/FileData.cs @@ -3,39 +3,66 @@ namespace OpenAI.Files { - public sealed class FileData + /// + /// The File object represents a document that has been uploaded to OpenAI. + /// + [Obsolete("use FileResponse")] + public sealed class FileData : BaseResponse { + /// + /// The file identifier, which can be referenced in the API endpoints. + /// [JsonInclude] [JsonPropertyName("id")] public string Id { get; private set; } + /// + /// The object type, which is always 'file'. + /// [JsonInclude] [JsonPropertyName("object")] public string Object { get; private set; } + /// + /// The size of the file, in bytes. + /// [JsonInclude] [JsonPropertyName("bytes")] public int Size { get; private set; } + /// + /// The Unix timestamp (in seconds) for when the file was created. + /// [JsonInclude] [JsonPropertyName("created_at")] - public int CreatedUnixTime { get; private set; } + public int CreatedAtUnixTimeSeconds { get; private set; } [JsonIgnore] - public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + [Obsolete("Use CreatedAtUnixTimeSeconds")] + public int CreatedUnixTime => CreatedAtUnixTimeSeconds; + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The name of the file. + /// [JsonInclude] [JsonPropertyName("filename")] public string FileName { get; private set; } + /// + /// The intended purpose of the file. + /// Supported values are 'fine-tune', 'fine-tune-results', 'assistants', and 'assistants_output'. + /// [JsonInclude] [JsonPropertyName("purpose")] public string Purpose { get; private set; } - [JsonInclude] - [JsonPropertyName("status")] - public string Status { get; private set; } + public static implicit operator string(FileData fileData) => fileData?.ToString(); + + public static implicit operator FileResponse(FileData fileData) => new FileResponse(fileData); - public static implicit operator string(FileData fileData) => fileData.Id; + public override string ToString() => Id; } } diff --git a/OpenAI-DotNet/Files/FileResponse.cs b/OpenAI-DotNet/Files/FileResponse.cs new file mode 100644 index 00000000..1437bfa3 --- /dev/null +++ b/OpenAI-DotNet/Files/FileResponse.cs @@ -0,0 +1,79 @@ +using System; +using System.Text.Json.Serialization; + +namespace OpenAI.Files +{ + /// + /// The File object represents a document that has been uploaded to OpenAI. + /// + public sealed class FileResponse : BaseResponse + { + public FileResponse() { } + +#pragma warning disable CS0618 // Type or member is obsolete + internal FileResponse(FileData file) + { + Id = file.Id; + Object = file.Object; + Size = file.Size; + CreatedAtUnixTimeSeconds = file.CreatedAtUnixTimeSeconds; + FileName = file.FileName; + Purpose = file.Purpose; + } +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// The file identifier, which can be referenced in the API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always 'file'. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The size of the file, in bytes. + /// + [JsonInclude] + [JsonPropertyName("bytes")] + public int Size { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the file was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + [Obsolete("Use CreatedAtUnixTimeSeconds")] + public int CreatedUnixTime => CreatedAtUnixTimeSeconds; + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The name of the file. + /// + [JsonInclude] + [JsonPropertyName("filename")] + public string FileName { get; private set; } + + /// + /// The intended purpose of the file. + /// Supported values are 'fine-tune', 'fine-tune-results', 'assistants', and 'assistants_output'. + /// + [JsonInclude] + [JsonPropertyName("purpose")] + public string Purpose { get; private set; } + + public static implicit operator string(FileResponse file) => file?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs index 6f957c93..f8e74193 100644 --- a/OpenAI-DotNet/Files/FilesEndpoint.cs +++ b/OpenAI-DotNet/Files/FilesEndpoint.cs @@ -19,13 +19,7 @@ public sealed class FilesEndpoint : BaseEndPoint private class FilesList { [JsonPropertyName("data")] - public List Data { get; set; } - } - - private class FileDeleteResponse - { - [JsonPropertyName("deleted")] - public bool Deleted { get; set; } + public IReadOnlyList Files { get; set; } } /// @@ -37,14 +31,21 @@ public FilesEndpoint(OpenAIClient api) : base(api) { } /// /// Returns a list of files that belong to the user's organization. /// + /// List files with a specific purpose. /// Optional, . - /// List of . - /// - public async Task> ListFilesAsync(CancellationToken cancellationToken = default) + /// List of . + public async Task> ListFilesAsync(string purpose = null, CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); + Dictionary query = null; + + if (!string.IsNullOrWhiteSpace(purpose)) + { + query = new Dictionary { { nameof(purpose), purpose } }; + } + + var response = await Api.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(resultAsString, OpenAIClient.JsonSerializationOptions)?.Data; + return JsonSerializer.Deserialize(resultAsString, OpenAIClient.JsonSerializationOptions)?.Files; } /// @@ -61,9 +62,8 @@ public async Task> ListFilesAsync(CancellationToken canc /// fields representing your training examples. /// /// Optional, . - /// . - /// - public async Task UploadFileAsync(string filePath, string purpose, CancellationToken cancellationToken = default) + /// . + public async Task UploadFileAsync(string filePath, string purpose, CancellationToken cancellationToken = default) => await UploadFileAsync(new FileUploadRequest(filePath, purpose), cancellationToken).ConfigureAwait(false); /// @@ -73,9 +73,8 @@ public async Task UploadFileAsync(string filePath, string purpose, Can /// /// . /// Optional, . - /// . - /// - public async Task UploadFileAsync(FileUploadRequest request, CancellationToken cancellationToken = default) + /// . + public async Task UploadFileAsync(FileUploadRequest request, CancellationToken cancellationToken = default) { using var fileData = new MemoryStream(); using var content = new MultipartFormDataContent(); @@ -86,7 +85,7 @@ public async Task UploadFileAsync(FileUploadRequest request, Cancellat var response = await Api.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } /// @@ -95,7 +94,6 @@ public async Task UploadFileAsync(FileUploadRequest request, Cancellat /// The ID of the file to use for this request /// Optional, . /// True, if file was successfully deleted. - /// public async Task DeleteFileAsync(string fileId, CancellationToken cancellationToken = default) { return await InternalDeleteFileAsync(1).ConfigureAwait(false); @@ -118,7 +116,7 @@ async Task InternalDeleteFileAsync(int attempt) throw new HttpRequestException($"{nameof(DeleteFileAsync)} Failed! HTTP status code: {response.StatusCode}. Response: {responseAsString}"); } - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } } @@ -127,13 +125,12 @@ async Task InternalDeleteFileAsync(int attempt) /// /// The ID of the file to use for this request. /// Optional, . - /// - /// - public async Task GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default) + /// + public async Task GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default) { var response = await Api.Client.GetAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } /// @@ -144,7 +141,6 @@ public async Task GetFileInfoAsync(string fileId, CancellationToken ca /// Optional, delete cached file. Default is false. /// Optional, /// The full path of the downloaded file. - /// public async Task DownloadFileAsync(string fileId, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) { var fileData = await GetFileInfoAsync(fileId, cancellationToken).ConfigureAwait(false); @@ -154,13 +150,12 @@ public async Task DownloadFileAsync(string fileId, string directory, boo /// /// Downloads the specified file. /// - /// to download. + /// to download. /// The directory to download the file into. /// Optional, delete cached file. Default is false. /// Optional, /// The full path of the downloaded file. - /// - public async Task DownloadFileAsync(FileData fileData, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + public async Task DownloadFileAsync(FileResponse fileData, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(directory)) { diff --git a/OpenAI-DotNet/FineTuning/EventList.cs b/OpenAI-DotNet/FineTuning/EventList.cs index a3d0c177..af6746ba 100644 --- a/OpenAI-DotNet/FineTuning/EventList.cs +++ b/OpenAI-DotNet/FineTuning/EventList.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace OpenAI.FineTuning { + [Obsolete("Use ListResponse")] public sealed class EventList { [JsonInclude] diff --git a/OpenAI-DotNet/FineTuning/FineTuneJob.cs b/OpenAI-DotNet/FineTuning/FineTuneJob.cs index 96f17261..785db76f 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJob.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJob.cs @@ -4,7 +4,8 @@ namespace OpenAI.FineTuning { - public sealed class FineTuneJob + [Obsolete("use FineTuneJobResponse")] + public sealed class FineTuneJob : BaseResponse { [JsonInclude] [JsonPropertyName("object")] @@ -20,17 +21,31 @@ public sealed class FineTuneJob [JsonInclude] [JsonPropertyName("created_at")] - public int? CreatedAtUnixTime { get; private set; } + public int? CreateAtUnixTimeSeconds { get; private set; } [JsonIgnore] - public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTime ?? 0).DateTime; + [Obsolete("Use CreateAtUnixTimeSeconds")] + public int? CreatedAtUnixTime => CreateAtUnixTimeSeconds; + + [JsonIgnore] + public DateTime? CreatedAt + => CreateAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CreateAtUnixTimeSeconds.Value).DateTime + : null; [JsonInclude] [JsonPropertyName("finished_at")] - public int? FinishedAtUnixTime { get; private set; } + public int? FinishedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + [Obsolete("Use FinishedAtUnixTimeSeconds")] + public int? FinishedAtUnixTime => CreateAtUnixTimeSeconds; [JsonIgnore] - public DateTime FinishedAt => DateTimeOffset.FromUnixTimeSeconds(FinishedAtUnixTime ?? 0).DateTime; + public DateTime? FinishedAt + => FinishedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FinishedAtUnixTimeSeconds.Value).DateTime + : null; [JsonInclude] [JsonPropertyName("fine_tuned_model")] @@ -67,6 +82,10 @@ public sealed class FineTuneJob [JsonIgnore] public IReadOnlyList Events { get; internal set; } = new List(); - public static implicit operator string(FineTuneJob job) => job.Id; + public static implicit operator FineTuneJobResponse(FineTuneJob job) => new FineTuneJobResponse(job); + + public static implicit operator string(FineTuneJob job) => job?.ToString(); + + public override string ToString() => Id; } } diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobList.cs b/OpenAI-DotNet/FineTuning/FineTuneJobList.cs index 8bec5b60..53966a7f 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJobList.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJobList.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace OpenAI.FineTuning { + [Obsolete("Use ListResponse")] public sealed class FineTuneJobList { [JsonInclude] diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs new file mode 100644 index 00000000..48f5941a --- /dev/null +++ b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.FineTuning +{ + public sealed class FineTuneJobResponse : BaseResponse + { + public FineTuneJobResponse() { } + +#pragma warning disable CS0618 // Type or member is obsolete + internal FineTuneJobResponse(FineTuneJob job) + { + Object = job.Object; + Id = job.Id; + Model = job.Model; + CreateAtUnixTimeSeconds = job.CreateAtUnixTimeSeconds; + FinishedAtUnixTimeSeconds = job.FinishedAtUnixTimeSeconds; + FineTunedModel = job.FineTunedModel; + OrganizationId = job.OrganizationId; + ResultFiles = job.ResultFiles; + Status = job.Status; + ValidationFile = job.ValidationFile; + TrainingFile = job.TrainingFile; + HyperParameters = job.HyperParameters; + TrainedTokens = job.TrainedTokens; + events = new List(job.Events.Count); + + foreach (var jobEvent in job.Events) + { + jobEvent.Client = Client; + events.Add(jobEvent); + } + } +#pragma warning restore CS0618 // Type or member is obsolete + + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + [JsonInclude] + [JsonPropertyName("model")] + public string Model { get; private set; } + + [JsonInclude] + [JsonPropertyName("created_at")] + public int? CreateAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CreatedAt + => CreateAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CreateAtUnixTimeSeconds.Value).DateTime + : null; + + [JsonInclude] + [JsonPropertyName("finished_at")] + public int? FinishedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? FinishedAt + => FinishedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FinishedAtUnixTimeSeconds.Value).DateTime + : null; + + [JsonInclude] + [JsonPropertyName("fine_tuned_model")] + public string FineTunedModel { get; private set; } + + [JsonInclude] + [JsonPropertyName("organization_id")] + public string OrganizationId { get; private set; } + + [JsonInclude] + [JsonPropertyName("result_files")] + public IReadOnlyList ResultFiles { get; private set; } + + [JsonInclude] + [JsonPropertyName("status")] + public JobStatus Status { get; private set; } + + [JsonInclude] + [JsonPropertyName("validation_file")] + public string ValidationFile { get; private set; } + + [JsonInclude] + [JsonPropertyName("training_file")] + public string TrainingFile { get; private set; } + + [JsonInclude] + [JsonPropertyName("hyperparameters")] + public HyperParams HyperParameters { get; private set; } + + [JsonInclude] + [JsonPropertyName("trained_tokens")] + public int? TrainedTokens { get; private set; } + + private List events = new List(); + + [JsonIgnore] + public IReadOnlyList Events + { + get => events; + internal set + { + events = value?.ToList() ?? new List(); + + foreach (var @event in events) + { + @event.Client = Client; + } + } + } + + public static implicit operator string(FineTuneJobResponse job) => job?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs index 661a0640..d04a80b2 100644 --- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs +++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs @@ -1,6 +1,6 @@ using OpenAI.Extensions; +using System; using System.Collections.Generic; -using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -9,7 +9,8 @@ namespace OpenAI.FineTuning { /// /// Manage fine-tuning jobs to tailor a model to your specific training data.
- /// + ///
+ /// ///
public sealed class FineTuningEndpoint : BaseEndPoint { @@ -27,24 +28,16 @@ public FineTuningEndpoint(OpenAIClient api) : base(api) { } /// . /// Optional, . /// . - /// . - public async Task CreateJobAsync(CreateFineTuneJobRequest jobRequest, CancellationToken cancellationToken = default) + public async Task CreateJobAsync(CreateFineTuneJobRequest jobRequest, CancellationToken cancellationToken = default) { var jsonContent = JsonSerializer.Serialize(jobRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl("/jobs"), jsonContent, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } - /// - /// List your organization's fine-tuning jobs. - /// - /// Number of fine-tuning jobs to retrieve (Default 20). - /// Identifier for the last job from the previous pagination request. - /// Optional, . - /// List of s. - /// . - public async Task ListJobsAsync(int? limit = null, string after = null, CancellationToken cancellationToken = default) + [Obsolete("Use new overload")] + public async Task ListJobsAsync(int? limit, string after, CancellationToken cancellationToken) { var parameters = new Dictionary(); @@ -63,19 +56,31 @@ public async Task ListJobsAsync(int? limit = null, string after return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } + /// + /// List your organization's fine-tuning jobs. + /// + /// . + /// Optional, . + /// List of s. + public async Task> ListJobsAsync(ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl("/jobs", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + /// /// Gets info about the fine-tune job. /// /// . /// Optional, . /// . - /// - public async Task GetJobInfoAsync(string jobId, CancellationToken cancellationToken = default) + public async Task GetJobInfoAsync(string jobId, CancellationToken cancellationToken = default) { var response = await Api.Client.GetAsync(GetUrl($"/jobs/{jobId}"), cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - var job = JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); - job.Events = (await ListJobEventsAsync(job, cancellationToken: cancellationToken).ConfigureAwait(false)).Events; + var job = JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); + job.Events = (await ListJobEventsAsync(job, cancellationToken: cancellationToken).ConfigureAwait(false))?.Items; return job; } @@ -85,25 +90,16 @@ public async Task GetJobInfoAsync(string jobId, CancellationToken c /// to cancel. /// Optional, . /// . - /// public async Task CancelJobAsync(string jobId, CancellationToken cancellationToken = default) { var response = await Api.Client.PostAsync(GetUrl($"/jobs/{jobId}/cancel"), null!, cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - var result = JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); + var result = JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); return result.Status == JobStatus.Cancelled; } - /// - /// Get fine-grained status updates for a fine-tune job. - /// - /// . - /// Number of fine-tuning jobs to retrieve (Default 20). - /// Identifier for the last from the previous pagination request. - /// Optional, . - /// List of events for . - /// - public async Task ListJobEventsAsync(string jobId, int? limit = null, string after = null, CancellationToken cancellationToken = default) + [Obsolete("use new overload")] + public async Task ListJobEventsAsync(string jobId, int? limit, string after, CancellationToken cancellationToken) { var parameters = new Dictionary(); @@ -121,5 +117,19 @@ public async Task ListJobEventsAsync(string jobId, int? limit = null, var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } + + /// + /// Get fine-grained status updates for a fine-tune job. + /// + /// . + /// . + /// Optional, . + /// List of events for . + public async Task> ListJobEventsAsync(string jobId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } } } diff --git a/OpenAI-DotNet/Images/ImageResult.cs b/OpenAI-DotNet/Images/ImageResult.cs index 92d30762..079aef15 100644 --- a/OpenAI-DotNet/Images/ImageResult.cs +++ b/OpenAI-DotNet/Images/ImageResult.cs @@ -2,7 +2,7 @@ namespace OpenAI.Images { - internal class ImageResult + public sealed class ImageResult { [JsonInclude] [JsonPropertyName("url")] @@ -11,5 +11,17 @@ internal class ImageResult [JsonInclude] [JsonPropertyName("b64_json")] public string B64_Json { get; private set; } + + [JsonInclude] + [JsonPropertyName("revised_prompt")] + public string RevisedPrompt { get; private set; } + + public static implicit operator string(ImageResult result) => result?.ToString(); + + public override string ToString() + => !string.IsNullOrWhiteSpace(Url) + ? Url + : !string.IsNullOrWhiteSpace(B64_Json) + ? B64_Json : null; } } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index fbd271f1..c732e915 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -46,7 +45,7 @@ internal ImagesEndpoint(OpenAIClient api) : base(api) { } /// /// A list of generated texture urls to download. [Obsolete] - public async Task> GenerateImageAsync( + public async Task> GenerateImageAsync( string prompt, int numberOfResults = 1, ImageSize size = ImageSize.Large, @@ -61,12 +60,10 @@ public async Task> GenerateImageAsync( /// /// Optional, . /// A list of generated texture urls to download. - /// - public async Task> GenerateImageAsync(ImageGenerationRequest request, CancellationToken cancellationToken = default) + public async Task> GenerateImageAsync(ImageGenerationRequest request, CancellationToken cancellationToken = default) { var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var endpoint = GetUrl($"/generations{(Api.OpenAIClientSettings.IsAzureDeployment ? ":submit" : string.Empty)}"); - var response = await Api.Client.PostAsync(endpoint, jsonContent, cancellationToken).ConfigureAwait(false); + var response = await Api.Client.PostAsync(GetUrl("/generations"), jsonContent, cancellationToken).ConfigureAwait(false); return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); } @@ -99,9 +96,8 @@ public async Task> GenerateImageAsync(ImageGenerationReque /// /// Optional, . /// A list of generated texture urls to download. - /// - [Obsolete] - public async Task> CreateImageEditAsync( + [Obsolete("Use new constructor")] + public async Task> CreateImageEditAsync( string image, string mask, string prompt, @@ -118,8 +114,7 @@ public async Task> CreateImageEditAsync( /// /// Optional, . /// A list of generated texture urls to download. - /// - public async Task> CreateImageEditAsync(ImageEditRequest request, CancellationToken cancellationToken = default) + public async Task> CreateImageEditAsync(ImageEditRequest request, CancellationToken cancellationToken = default) { using var content = new MultipartFormDataContent(); using var imageData = new MemoryStream(); @@ -170,9 +165,8 @@ public async Task> CreateImageEditAsync(ImageEditRequest r /// /// Optional, . /// A list of generated texture urls to download. - /// - [Obsolete] - public async Task> CreateImageVariationAsync( + [Obsolete("Use new constructor")] + public async Task> CreateImageVariationAsync( string imagePath, int numberOfResults = 1, ImageSize size = ImageSize.Large, @@ -187,8 +181,7 @@ public async Task> CreateImageVariationAsync( /// /// Optional, . /// A list of generated texture urls to download. - /// - public async Task> CreateImageVariationAsync(ImageVariationRequest request, CancellationToken cancellationToken = default) + public async Task> CreateImageVariationAsync(ImageVariationRequest request, CancellationToken cancellationToken = default) { using var content = new MultipartFormDataContent(); using var imageData = new MemoryStream(); @@ -208,17 +201,17 @@ public async Task> CreateImageVariationAsync(ImageVariatio return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); } - private async Task> DeserializeResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + private async Task> DeserializeResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) { var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - var imagesResponse = response.DeserializeResponse(resultAsString, OpenAIClient.JsonSerializationOptions); + var imagesResponse = response.Deserialize(resultAsString, Api); if (imagesResponse?.Results is not { Count: not 0 }) { throw new HttpRequestException($"{nameof(DeserializeResponseAsync)} returned no results! HTTP status code: {response.StatusCode}. Response body: {resultAsString}"); } - return imagesResponse.Results.Select(imageResult => string.IsNullOrWhiteSpace(imageResult.Url) ? imageResult.B64_Json : imageResult.Url).ToList(); + return imagesResponse.Results; } } } diff --git a/OpenAI-DotNet/Images/ImagesResponse.cs b/OpenAI-DotNet/Images/ImagesResponse.cs index 7bc011a7..79254f75 100644 --- a/OpenAI-DotNet/Images/ImagesResponse.cs +++ b/OpenAI-DotNet/Images/ImagesResponse.cs @@ -3,7 +3,7 @@ namespace OpenAI.Images { - internal class ImagesResponse : BaseResponse + internal sealed class ImagesResponse : BaseResponse { [JsonInclude] [JsonPropertyName("created")] diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs index 359f8358..583a5374 100644 --- a/OpenAI-DotNet/Models/Model.cs +++ b/OpenAI-DotNet/Models/Model.cs @@ -30,7 +30,7 @@ public Model(string id, string ownedBy = null) /// Allows a model to be implicitly cast to the string of its id. ///
/// The to cast to a string. - public static implicit operator string(Model model) => model.Id; + public static implicit operator string(Model model) => model?.ToString(); /// /// Allows a string to be implicitly cast as a diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs index 13de8856..c0be66d8 100644 --- a/OpenAI-DotNet/Models/ModelsEndpoint.cs +++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs @@ -1,7 +1,6 @@ using OpenAI.Extensions; using System; using System.Collections.Generic; -using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -23,21 +22,6 @@ private sealed class ModelsList public List Models { get; private set; } } - private sealed class DeleteModelResponse - { - [JsonInclude] - [JsonPropertyName("id")] - public string Id { get; private set; } - - [JsonInclude] - [JsonPropertyName("object")] - public string Object { get; private set; } - - [JsonInclude] - [JsonPropertyName("deleted")] - public bool Deleted { get; private set; } - } - /// public ModelsEndpoint(OpenAIClient api) : base(api) { } @@ -49,7 +33,6 @@ public ModelsEndpoint(OpenAIClient api) : base(api) { } /// /// Optional, /// Asynchronously returns the list of all s - /// Raised when the HTTP request fails public async Task> GetModelsAsync(CancellationToken cancellationToken = default) { var response = await Api.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); @@ -63,7 +46,6 @@ public async Task> GetModelsAsync(CancellationToken cancell /// The id/name of the model to get more details about /// Optional, /// Asynchronously returns the with all available properties - /// Raised when the HTTP request fails public async Task GetModelDetailsAsync(string id, CancellationToken cancellationToken = default) { var response = await Api.Client.GetAsync(GetUrl($"/{id}"), cancellationToken).ConfigureAwait(false); @@ -77,7 +59,6 @@ public async Task GetModelDetailsAsync(string id, CancellationToken cance /// The to delete. /// Optional, /// True, if fine-tuned model was successfully deleted. - /// public async Task DeleteFineTuneModelAsync(string modelId, CancellationToken cancellationToken = default) { var model = await GetModelDetailsAsync(modelId, cancellationToken).ConfigureAwait(false); @@ -93,7 +74,7 @@ public async Task DeleteFineTuneModelAsync(string modelId, CancellationTok { var response = await Api.Client.DeleteAsync(GetUrl($"/{model.Id}"), cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } catch (Exception e) { diff --git a/OpenAI-DotNet/Models/Permission.cs b/OpenAI-DotNet/Models/Permission.cs index c252a02f..c046cf12 100644 --- a/OpenAI-DotNet/Models/Permission.cs +++ b/OpenAI-DotNet/Models/Permission.cs @@ -15,10 +15,13 @@ public sealed class Permission [JsonInclude] [JsonPropertyName("created")] - public int CreatedAtUnixTime { get; private set; } + public int CreatedAtUnitTimeSeconds { get; private set; } + + [Obsolete("use CreatedAtUnitTimeSeconds")] + public int CreatedAtUnixTime => CreatedAtUnitTimeSeconds; [JsonIgnore] - public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTime).DateTime; + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnitTimeSeconds).DateTime; [JsonInclude] [JsonPropertyName("allow_create_engine")] diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs index 92709066..0dc918f5 100644 --- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs +++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs @@ -1,6 +1,5 @@ using OpenAI.Extensions; using System.Linq; -using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -46,13 +45,12 @@ public async Task GetModerationAsync(string input, string model = null, Ca ///
/// /// Optional, . - /// Raised when the HTTP request fails public async Task CreateModerationAsync(ModerationsRequest request, CancellationToken cancellationToken = default) { var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); - var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return response.DeserializeResponse(resultAsString, OpenAIClient.JsonSerializationOptions); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); } } } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index d05ef468..3e0733df 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -5,10 +5,11 @@ false Stephen Hodgson OpenAI-DotNet - A simple C# .NET client library for OpenAI to use though their RESTful API. Independently developed, this is not an official library and I am not affiliated with OpenAI. An OpenAI API account is required. + A simple C# .NET client library for OpenAI to use though their RESTful API. +Independently developed, this is not an official library and I am not affiliated with OpenAI. +An OpenAI API account is required. Forked from [OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet). - More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-api). 2023 @@ -17,8 +18,22 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- https://github.com/RageAgainstThePixel/OpenAI-DotNet OpenAI, AI, ML, API, gpt-4, gpt-3.5-tubo, gpt-3, chatGPT, chat-gpt, gpt-2, gpt, dall-e-2, dall-e-3 OpenAI API - 7.2.3 - Version 7.2.3 + 7.3.0 + Version 7.3.0 +- Added AgentsEndpoint +- Added ThreadsEndpoint +- Updated ImagesEndpoint return types to ImageResult list +- Updated FilesEndpoint.ListFilesAsync with optional purpose filter query parameter. +- Refactored list responses with a more generic ListQuery and ListResponse<TObject> pattern + - EventList -> ListResponse<EventResponse> + - FineTuneJobList -> ListResponse<FineTuneJobResponse> +- Standardized names for timestamps to have suffix: UnixTimeSeconds +- Standardized response class names (existing classes depreciated) + - FileData -> FileResponse + - CompletionResult -> CompletonResponse + - Event -> EventResponse + - FineTuneJob -> FineTuneJobResponse +Version 7.2.3 - Added support for reading RateLimit information from the Headers Version 7.2.2 - Fixed Image Generation for Azure diff --git a/OpenAI-DotNet/OpenAIClient.cs b/OpenAI-DotNet/OpenAIClient.cs index 501665f2..af69bb1e 100644 --- a/OpenAI-DotNet/OpenAIClient.cs +++ b/OpenAI-DotNet/OpenAIClient.cs @@ -1,4 +1,5 @@ -using OpenAI.Audio; +using OpenAI.Assistants; +using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Completions; using OpenAI.Edits; @@ -9,6 +10,7 @@ using OpenAI.Images; using OpenAI.Models; using OpenAI.Moderations; +using OpenAI.Threads; using System; using System.Net.Http; using System.Net.Http.Headers; @@ -50,15 +52,17 @@ public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, OpenAIClie ModelsEndpoint = new ModelsEndpoint(this); CompletionsEndpoint = new CompletionsEndpoint(this); ChatEndpoint = new ChatEndpoint(this); -#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete EditsEndpoint = new EditsEndpoint(this); -#pragma warning restore CS0612 // Type or member is obsolete +#pragma warning restore CS0618 // Type or member is obsolete ImagesEndPoint = new ImagesEndpoint(this); EmbeddingsEndpoint = new EmbeddingsEndpoint(this); AudioEndpoint = new AudioEndpoint(this); FilesEndpoint = new FilesEndpoint(this); FineTuningEndpoint = new FineTuningEndpoint(this); ModerationsEndpoint = new ModerationsEndpoint(this); + ThreadsEndpoint = new ThreadsEndpoint(this); + AssistantsEndpoint = new AssistantsEndpoint(this); } private HttpClient SetupClient(HttpClient client = null) @@ -68,6 +72,7 @@ private HttpClient SetupClient(HttpClient client = null) PooledConnectionLifetime = TimeSpan.FromMinutes(15) }); client.DefaultRequestHeaders.Add("User-Agent", "OpenAI-DotNet"); + client.DefaultRequestHeaders.Add("OpenAI-Beta", "assistants=v1"); if (!OpenAIClientSettings.BaseRequestUrlFormat.Contains(OpenAIClientSettings.AzureOpenAIDomain) && (string.IsNullOrWhiteSpace(OpenAIAuthentication.ApiKey) || @@ -102,13 +107,12 @@ private HttpClient SetupClient(HttpClient client = null) /// /// The to use when making calls to the API. /// - internal static readonly JsonSerializerOptions JsonSerializationOptions = new JsonSerializerOptions + internal static JsonSerializerOptions JsonSerializationOptions { get; private set; } = DefaultJsonSerializerOptions; + + internal static JsonSerializerOptions DefaultJsonSerializerOptions { get; } = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = - { - new JsonStringEnumConverterFactory() - } + Converters = { new JsonStringEnumConverterFactory() } }; /// @@ -121,10 +125,28 @@ private HttpClient SetupClient(HttpClient client = null) /// internal OpenAIClientSettings OpenAIClientSettings { get; } + private bool enableDebug; + /// - /// Enables or disables debugging for the whole client. + /// Enables or disables debugging for all endpoints. /// - public bool EnableDebug { get; set; } + public bool EnableDebug + { + get => enableDebug; + set + { + enableDebug = value; + + JsonSerializationOptions = enableDebug + ? DefaultJsonSerializerOptions + : new JsonSerializerOptions + { + WriteIndented = enableDebug, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverterFactory() } + }; + } + } /// /// List and describe the various models available in the API. @@ -153,7 +175,7 @@ private HttpClient SetupClient(HttpClient client = null) /// Given a prompt and an instruction, the model will return an edited version of the prompt.
/// ///
- [Obsolete] + [Obsolete("Deprecated")] public EditsEndpoint EditsEndpoint { get; } /// @@ -182,7 +204,8 @@ private HttpClient SetupClient(HttpClient client = null) /// /// Manage fine-tuning jobs to tailor a model to your specific training data.
- /// + ///
+ /// ///
public FineTuningEndpoint FineTuningEndpoint { get; } @@ -192,5 +215,17 @@ private HttpClient SetupClient(HttpClient client = null) /// ///
public ModerationsEndpoint ModerationsEndpoint { get; } + + /// + /// Build assistants that can call models and use tools to perform tasks.
+ /// + ///
+ public AssistantsEndpoint AssistantsEndpoint { get; } + + /// + /// Create threads that assistants can interact with.
+ /// + ///
+ public ThreadsEndpoint ThreadsEndpoint { get; } } -} +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/Annotation.cs b/OpenAI-DotNet/Threads/Annotation.cs new file mode 100644 index 00000000..204fc425 --- /dev/null +++ b/OpenAI-DotNet/Threads/Annotation.cs @@ -0,0 +1,45 @@ +using OpenAI.Extensions; +using OpenAI.Threads; +using System.Text.Json.Serialization; + +namespace OpenAI +{ + public sealed class Annotation + { + [JsonInclude] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public AnnotationType Type { get; private set; } + + /// + /// The text in the message content that needs to be replaced. + /// + [JsonInclude] + [JsonPropertyName("text")] + public string Text { get; private set; } + + /// + /// A citation within the message that points to a specific quote from a + /// specific File associated with the assistant or the message. + /// Generated when the assistant uses the 'retrieval' tool to search files. + /// + [JsonInclude] + [JsonPropertyName("file_citation")] + public FileCitation FileCitation { get; private set; } + + /// + /// A URL for the file that's generated when the assistant used the 'code_interpreter' tool to generate a file. + /// + [JsonInclude] + [JsonPropertyName("file_path")] + public FilePath FilePath { get; private set; } + + [JsonInclude] + [JsonPropertyName("start_index")] + public int StartIndex { get; private set; } + + [JsonInclude] + [JsonPropertyName("end_index")] + public int EndIndex { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/AnnotationType.cs b/OpenAI-DotNet/Threads/AnnotationType.cs new file mode 100644 index 00000000..2f180f0c --- /dev/null +++ b/OpenAI-DotNet/Threads/AnnotationType.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace OpenAI +{ + public enum AnnotationType + { + [EnumMember(Value = "file_citation")] + FileCitation, + [EnumMember(Value = "file_path")] + FilePath + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CodeInterpreter.cs b/OpenAI-DotNet/Threads/CodeInterpreter.cs new file mode 100644 index 00000000..260bc8fc --- /dev/null +++ b/OpenAI-DotNet/Threads/CodeInterpreter.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CodeInterpreter + { + /// + /// The input to the Code Interpreter tool call. + /// + [JsonInclude] + [JsonPropertyName("input")] + public string Input { get; private set; } + + /// + /// The outputs from the Code Interpreter tool call. + /// Code Interpreter can output one or more items, including text (logs) or images (image). + /// Each of these are represented by a different object type. + /// + [JsonInclude] + [JsonPropertyName("outputs")] + public IReadOnlyList Outputs { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CodeInterpreterImageOutput.cs b/OpenAI-DotNet/Threads/CodeInterpreterImageOutput.cs new file mode 100644 index 00000000..1bef8dd3 --- /dev/null +++ b/OpenAI-DotNet/Threads/CodeInterpreterImageOutput.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CodeInterpreterImageOutput + { + /// + /// The file ID of the image. + /// + [JsonInclude] + [JsonPropertyName("file_id")] + public string FileId { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CodeInterpreterOutputType.cs b/OpenAI-DotNet/Threads/CodeInterpreterOutputType.cs new file mode 100644 index 00000000..9353318e --- /dev/null +++ b/OpenAI-DotNet/Threads/CodeInterpreterOutputType.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace OpenAI.Threads +{ + public enum CodeInterpreterOutputType + { + [EnumMember(Value = "logs")] + Logs, + [EnumMember(Value = "image")] + Image + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs new file mode 100644 index 00000000..77c5662e --- /dev/null +++ b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs @@ -0,0 +1,30 @@ +using OpenAI.Extensions; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CodeInterpreterOutputs + { + /// + /// Output type + /// + [JsonInclude] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public CodeInterpreterOutputType Type { get; private set; } + + /// + /// The text output from the Code Interpreter tool call. + /// + [JsonInclude] + [JsonPropertyName("logs")] + public string Logs { get; private set; } + + /// + /// Code interpreter image output + /// + [JsonInclude] + [JsonPropertyName("image")] + public CodeInterpreterImageOutput Image { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/Content.cs b/OpenAI-DotNet/Threads/Content.cs new file mode 100644 index 00000000..85002bed --- /dev/null +++ b/OpenAI-DotNet/Threads/Content.cs @@ -0,0 +1,32 @@ +using OpenAI.Extensions; +using System; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class Content + { + [JsonInclude] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ContentType Type { get; private set; } + + [JsonInclude] + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public TextContent Text { get; private set; } + + [JsonInclude] + [JsonPropertyName("image_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ImageUrl ImageUrl { get; private set; } + + public override string ToString() + => Type switch + { + ContentType.Text => Text.Value, + ContentType.ImageUrl => ImageUrl.Url, + _ => throw new ArgumentOutOfRangeException() + }; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ContentText.cs b/OpenAI-DotNet/Threads/ContentText.cs new file mode 100644 index 00000000..2da02852 --- /dev/null +++ b/OpenAI-DotNet/Threads/ContentText.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI +{ + public sealed class ContentText + { + public ContentText(string value) => Value = value; + + /// + /// The data that makes up the text. + /// + [JsonInclude] + [JsonPropertyName("value")] + public string Value { get; private set; } + + /// + /// Annotations + /// + [JsonInclude] + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IReadOnlyList Annotations { get; private set; } + + public static implicit operator ContentText(string value) => new ContentText(value); + + public static implicit operator string(ContentText text) => text?.ToString(); + + public override string ToString() => Value; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CreateMessageRequest.cs b/OpenAI-DotNet/Threads/CreateMessageRequest.cs new file mode 100644 index 00000000..8aa8a03a --- /dev/null +++ b/OpenAI-DotNet/Threads/CreateMessageRequest.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CreateMessageRequest + { + public static implicit operator CreateMessageRequest(string content) + => new CreateMessageRequest(content); + + /// + /// Constructor. + /// + /// + /// + /// + public CreateMessageRequest(string content, IEnumerable fieldIds = null, IReadOnlyDictionary metadata = null) + { + Role = Role.User; + Content = content; + FileIds = fieldIds?.ToList(); + Metadata = metadata; + } + + /// + /// The role of the entity that is creating the message. + /// + /// + /// Currently only user is supported. + /// + [JsonPropertyName("role")] + public Role Role { get; } + + /// + /// The content of the message. + /// + [JsonPropertyName("content")] + public string Content { get; } + + /// + /// A list of File IDs that the message should use. There can be a maximum of 10 files attached to a message. + /// Useful for tools like retrieval and code_interpreter that can access and use files. + /// + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CreateRunRequest.cs b/OpenAI-DotNet/Threads/CreateRunRequest.cs new file mode 100644 index 00000000..88fba8e6 --- /dev/null +++ b/OpenAI-DotNet/Threads/CreateRunRequest.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CreateRunRequest + { + public CreateRunRequest(string assistantId, CreateRunRequest request) + : this(assistantId, request?.Model, request?.Instructions, request?.Tools, request?.Metadata) + { + } + + public CreateRunRequest(string assistantId, string model = null, string instructions = null, IEnumerable tools = null, IReadOnlyDictionary metadata = null) + { + AssistantId = assistantId; + Model = model; + Instructions = instructions; + Tools = tools?.ToList(); + Metadata = metadata; + } + + /// + /// The ID of the assistant used for execution of this run. + /// + [JsonPropertyName("assistant_id")] + public string AssistantId { get; } + + /// + /// The model that the assistant used for this run. + /// + [JsonPropertyName("model")] + public string Model { get; } + + /// + /// The instructions that the assistant used for this run. + /// + [JsonPropertyName("instructions")] + public string Instructions { get; } + + /// + /// The list of tools that the assistant used for this run. + /// + [JsonPropertyName("tools")] + public IReadOnlyList Tools { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs new file mode 100644 index 00000000..9ae286a2 --- /dev/null +++ b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CreateThreadAndRunRequest + { + public CreateThreadAndRunRequest(string assistantId, CreateThreadAndRunRequest request) + : this(assistantId, request?.Model, request?.Instructions, request?.Tools, request?.Metadata) + { + } + + /// + /// Constructor. + /// + /// + /// The ID of the assistant to use to execute this run. + /// + /// + /// The ID of the Model to be used to execute this run. + /// If a value is provided here, it will override the model associated with the assistant. + /// If not, the model associated with the assistant will be used. + /// + /// + /// Override the default system message of the assistant. + /// This is useful for modifying the behavior on a per-run basis. + /// + /// + /// Override the tools the assistant can use for this run. + /// This is useful for modifying the behavior on a per-run basis. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// + /// Optional, . + /// + public CreateThreadAndRunRequest(string assistantId, string model = null, string instructions = null, IReadOnlyList tools = null, IReadOnlyDictionary metadata = null, CreateThreadRequest createThreadRequest = null) + { + AssistantId = assistantId; + Model = model; + Instructions = instructions; + Tools = tools; + Metadata = metadata; + ThreadRequest = createThreadRequest; + } + + /// + /// The ID of the assistant to use to execute this run. + /// + [JsonPropertyName("assistant_id")] + public string AssistantId { get; } + + /// + /// The ID of the Model to be used to execute this run. + /// If a value is provided here, it will override the model associated with the assistant. + /// If not, the model associated with the assistant will be used. + /// + [JsonPropertyName("model")] + public string Model { get; } + + /// + /// Override the default system message of the assistant. + /// This is useful for modifying the behavior on a per-run basis. + /// + [JsonPropertyName("instructions")] + public string Instructions { get; } + + /// + /// Override the tools the assistant can use for this run. + /// This is useful for modifying the behavior on a per-run basis. + /// + [JsonPropertyName("tools")] + public IReadOnlyList Tools { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [JsonPropertyName("thread")] + public CreateThreadRequest ThreadRequest { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CreateThreadRequest.cs b/OpenAI-DotNet/Threads/CreateThreadRequest.cs new file mode 100644 index 00000000..101dcd7c --- /dev/null +++ b/OpenAI-DotNet/Threads/CreateThreadRequest.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class CreateThreadRequest + { + /// + /// Constructor. + /// + /// + /// A list of messages to start the thread with. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + public CreateThreadRequest(IEnumerable messages = null, IReadOnlyDictionary metadata = null) + { + Messages = messages?.ToList(); + Metadata = metadata; + } + + /// + /// A list of messages to start the thread with. + /// + [JsonPropertyName("messages")] + public IReadOnlyList Messages { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + + public static implicit operator CreateThreadRequest(string message) => new CreateThreadRequest(new[] { new Message(message) }); + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/FileCitation.cs b/OpenAI-DotNet/Threads/FileCitation.cs new file mode 100644 index 00000000..798616fb --- /dev/null +++ b/OpenAI-DotNet/Threads/FileCitation.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class FileCitation + { + /// + /// The ID of the specific File the citation is from. + /// + [JsonInclude] + [JsonPropertyName("file_id")] + public string FileId { get; private set; } + + /// + /// The specific quote in the file. + /// + [JsonInclude] + [JsonPropertyName("file_id")] + public string Quote { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/FilePath.cs b/OpenAI-DotNet/Threads/FilePath.cs new file mode 100644 index 00000000..77d0b768 --- /dev/null +++ b/OpenAI-DotNet/Threads/FilePath.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class FilePath + { + /// + /// The ID of the file that was generated. + /// + [JsonInclude] + [JsonPropertyName("file_id")] + public string FileId { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/FunctionCall.cs b/OpenAI-DotNet/Threads/FunctionCall.cs new file mode 100644 index 00000000..df75e1bb --- /dev/null +++ b/OpenAI-DotNet/Threads/FunctionCall.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class FunctionCall + { + /// + /// The name of the function. + /// + [JsonInclude] + [JsonPropertyName("name")] + public string Name { get; private set; } + + /// + /// The arguments that the model expects you to pass to the function. + /// + [JsonInclude] + [JsonPropertyName("arguments")] + public string Arguments { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/Message.cs b/OpenAI-DotNet/Threads/Message.cs new file mode 100644 index 00000000..64017a46 --- /dev/null +++ b/OpenAI-DotNet/Threads/Message.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class Message + { + public static implicit operator Message(string content) => new Message(content); + + /// + /// Constructor. + /// + /// + /// The content of the message. + /// + /// + /// A list of File IDs that the message should use. + /// There can be a maximum of 10 files attached to a message. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access and use files. + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + public Message(string content, IEnumerable fileIds = null, IReadOnlyDictionary metadata = null) + { + Role = Role.User; + Content = content; + FileIds = fileIds?.ToList(); + Metadata = metadata; + } + + /// + /// The role of the entity that is creating the message. + /// Currently only user is supported. + /// + [JsonPropertyName("role")] + public Role Role { get; } + + /// + /// The content of the message. + /// + [JsonPropertyName("content")] + public string Content { get; } + + /// + /// A list of File IDs that the message should use. + /// There can be a maximum of 10 files attached to a message. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access and use files. + /// + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/MessageFileResponse.cs b/OpenAI-DotNet/Threads/MessageFileResponse.cs new file mode 100644 index 00000000..242a390e --- /dev/null +++ b/OpenAI-DotNet/Threads/MessageFileResponse.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class MessageFileResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always thread.message.file. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the message file was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The ID of the message that the File is attached to. + /// + [JsonInclude] + [JsonPropertyName("message_id")] + public string MessageId { get; private set; } + + public static implicit operator string(MessageFileResponse response) => response?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/MessageResponse.cs b/OpenAI-DotNet/Threads/MessageResponse.cs new file mode 100644 index 00000000..bcffb26a --- /dev/null +++ b/OpenAI-DotNet/Threads/MessageResponse.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// A message created by an Assistant or a user. + /// Messages can include text, images, and other files. + /// Messages stored as a list on the Thread. + /// + public sealed class MessageResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always thread. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The thread ID that this message belongs to. + /// + [JsonInclude] + [JsonPropertyName("thread_id")] + public string ThreadId { get; private set; } + + /// + /// The entity that produced the message. One of user or assistant. + /// + [JsonInclude] + [JsonPropertyName("role")] + public Role Role { get; private set; } + + /// + /// The content of the message in array of text and/or images. + /// + [JsonInclude] + [JsonPropertyName("content")] + public IReadOnlyList Content { get; private set; } + + /// + /// If applicable, the ID of the assistant that authored this message. + /// + [JsonInclude] + [JsonPropertyName("assistant_id")] + public string AssistantId { get; private set; } + + /// + /// If applicable, the ID of the run associated with the authoring of this message. + /// + [JsonInclude] + [JsonPropertyName("run_id")] + public string RunId { get; private set; } + + /// + /// A list of file IDs that the assistant should use. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access files. + /// A maximum of 10 files can be attached to a message. + /// + [JsonInclude] + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; private set; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonInclude] + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; private set; } + + public static implicit operator string(MessageResponse message) => message?.ToString(); + + public override string ToString() => Id; + + /// + /// Formats all of the items into a single string, + /// putting each item on a new line. + /// + /// of all . + public string PrintContent() => string.Join("\n", Content.Select(content => content.ToString())); + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RequiredAction.cs b/OpenAI-DotNet/Threads/RequiredAction.cs new file mode 100644 index 00000000..3025d684 --- /dev/null +++ b/OpenAI-DotNet/Threads/RequiredAction.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class RequiredAction + { + [JsonInclude] + [JsonPropertyName("type")] + public string Type { get; private set; } + + /// + /// Details on the tool outputs needed for this run to continue. + /// + [JsonInclude] + [JsonPropertyName("submit_tool_outputs")] + public SubmitToolOutputs SubmitToolOutputs { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunLastError.cs b/OpenAI-DotNet/Threads/RunLastError.cs new file mode 100644 index 00000000..b529a48b --- /dev/null +++ b/OpenAI-DotNet/Threads/RunLastError.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class RunLastError + { + /// + /// One of server_error or rate_limit_exceeded. + /// + [JsonInclude] + [JsonPropertyName("code")] + public string Code { get; private set; } + + /// + /// A human-readable description of the error. + /// + [JsonInclude] + [JsonPropertyName("message")] + public string Message { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunResponse.cs b/OpenAI-DotNet/Threads/RunResponse.cs new file mode 100644 index 00000000..b627fa2e --- /dev/null +++ b/OpenAI-DotNet/Threads/RunResponse.cs @@ -0,0 +1,183 @@ +using OpenAI.Extensions; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// An invocation of an Assistant on a Thread. + /// The Assistant uses it�s configuration and the Thread�s Messages to perform tasks by calling models and tools. + /// As part of a Run, the Assistant appends Messages to the Thread. + /// + public sealed class RunResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always run. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The thread ID that this run belongs to. + /// + [JsonInclude] + [JsonPropertyName("thread_id")] + public string ThreadId { get; private set; } + + /// + /// The ID of the assistant used for execution of this run. + /// + [JsonInclude] + [JsonPropertyName("assistant_id")] + public string AssistantId { get; private set; } + + /// + /// The status of the run. + /// + [JsonInclude] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public RunStatus Status { get; private set; } + + /// + /// Details on the action required to continue the run. + /// Will be null if no action is required. + /// + [JsonInclude] + [JsonPropertyName("required_action")] + public RequiredAction RequiredAction { get; private set; } + + /// + /// The Last error Associated with this run. + /// Will be null if there are no errors. + /// + [JsonInclude] + [JsonPropertyName("last_error")] + public RunLastError LastError { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The Unix timestamp (in seconds) for when the run will expire. + /// + [JsonInclude] + [JsonPropertyName("expires_at")] + public int? ExpiresAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? ExpiresAt + => ExpiresAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was started. + /// + [JsonInclude] + [JsonPropertyName("started_at")] + public int? StartedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? StartedAt + => StartedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(StartedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was cancelled. + /// + [JsonInclude] + [JsonPropertyName("cancelled_at")] + public int? CancelledAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CancelledAt + => CancelledAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CancelledAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run failed. + /// + [JsonInclude] + [JsonPropertyName("failed_at")] + public int? FailedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? FailedAt + => FailedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FailedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was completed. + /// + [JsonInclude] + [JsonPropertyName("completed_at")] + public int? CompletedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CompletedAt + => CompletedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CompletedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The model that the assistant used for this run. + /// + [JsonInclude] + [JsonPropertyName("model")] + public string Model { get; private set; } + + /// + /// The instructions that the assistant used for this run. + /// + [JsonInclude] + [JsonPropertyName("instructions")] + public string Instructions { get; private set; } + + /// + /// The list of tools that the assistant used for this run. + /// + [JsonInclude] + [JsonPropertyName("tools")] + public IReadOnlyList Tools { get; private set; } + + /// + /// The list of File IDs the assistant used for this run. + /// + [JsonInclude] + [JsonPropertyName("file_ids")] + public IReadOnlyList FileIds { get; private set; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonInclude] + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; private set; } + + public static implicit operator string(RunResponse run) => run?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunStatus.cs b/OpenAI-DotNet/Threads/RunStatus.cs new file mode 100644 index 00000000..da50011d --- /dev/null +++ b/OpenAI-DotNet/Threads/RunStatus.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; + +namespace OpenAI.Threads +{ + public enum RunStatus + { + [EnumMember(Value = "queued")] + Queued, + [EnumMember(Value = "in_progress")] + InProgress, + [EnumMember(Value = "requires_action")] + RequiresAction, + [EnumMember(Value = "cancelling")] + Cancelling, + [EnumMember(Value = "cancelled")] + Cancelled, + [EnumMember(Value = "failed")] + Failed, + [EnumMember(Value = "completed")] + Completed, + [EnumMember(Value = "expired")] + Expired + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunStepMessageCreation.cs b/OpenAI-DotNet/Threads/RunStepMessageCreation.cs new file mode 100644 index 00000000..a3697097 --- /dev/null +++ b/OpenAI-DotNet/Threads/RunStepMessageCreation.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class RunStepMessageCreation + { + /// + /// The ID of the message that was created by this run step. + /// + [JsonInclude] + [JsonPropertyName("message_id")] + public string MessageId { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunStepResponse.cs b/OpenAI-DotNet/Threads/RunStepResponse.cs new file mode 100644 index 00000000..c9359d1a --- /dev/null +++ b/OpenAI-DotNet/Threads/RunStepResponse.cs @@ -0,0 +1,154 @@ +using OpenAI.Extensions; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// A detailed list of steps the Assistant took as part of a Run. + /// An Assistant can call tools or create Messages during it�s run. + /// Examining Run Steps allows you to introspect how the Assistant is getting to it�s final results. + /// + public sealed class RunStepResponse : BaseResponse + { + /// + /// The identifier of the run step, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + /// + /// The ID of the assistant associated with the run step. + /// + [JsonInclude] + [JsonPropertyName("assistant_id")] + public string AssistantId { get; private set; } + + /// + /// The ID of the thread that was run. + /// + [JsonInclude] + [JsonPropertyName("thread_id")] + public string ThreadId { get; private set; } + + /// + /// The ID of the run that this run step is a part of. + /// + [JsonInclude] + [JsonPropertyName("run_id")] + public string RunId { get; private set; } + + /// + /// The type of run step. + /// + [JsonInclude] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public RunStepType Type { get; private set; } + + /// + /// The status of the run step. + /// + [JsonInclude] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public RunStatus Status { get; private set; } + + /// + /// The details of the run step. + /// + [JsonInclude] + [JsonPropertyName("step_details")] + public StepDetails StepDetails { get; private set; } + + /// + /// The last error associated with this run step. Will be null if there are no errors. + /// + [JsonInclude] + [JsonPropertyName("last_error")] + public RunLastError LastError { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the run step was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int? CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CreatedAt + => CreatedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step expired. A step is considered expired if the parent run is expired. + /// + [JsonInclude] + [JsonPropertyName("expires_at")] + public int? ExpiresAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? ExpiresAt + => ExpiresAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step was cancelled. + /// + [JsonInclude] + [JsonPropertyName("cancelled_at")] + public int? CancelledAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CancelledAt + => CancelledAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CancelledAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step failed. + /// + [JsonInclude] + [JsonPropertyName("failed_at")] + public int? FailedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? FailedAt + => FailedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FailedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step completed. + /// + [JsonInclude] + [JsonPropertyName("completed_at")] + public int? CompletedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime? CompletedAt + => CompletedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CompletedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonInclude] + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; private set; } + + public static implicit operator string(RunStepResponse runStep) => runStep?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/RunStepType.cs b/OpenAI-DotNet/Threads/RunStepType.cs new file mode 100644 index 00000000..6ba91a26 --- /dev/null +++ b/OpenAI-DotNet/Threads/RunStepType.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace OpenAI.Threads +{ + public enum RunStepType + { + [EnumMember(Value = "message_creation")] + MessageCreation, + [EnumMember(Value = "tool_calls")] + ToolCalls + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/StepDetails.cs b/OpenAI-DotNet/Threads/StepDetails.cs new file mode 100644 index 00000000..32b3c684 --- /dev/null +++ b/OpenAI-DotNet/Threads/StepDetails.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// The details of the run step. + /// + public sealed class StepDetails + { + /// + /// Details of the message creation by the run step. + /// + [JsonInclude] + [JsonPropertyName("message_creation")] + public RunStepMessageCreation MessageCreation { get; private set; } + + /// + /// An array of tool calls the run step was involved in. + /// These can be associated with one of three types of tools: 'code_interpreter', 'retrieval', or 'function'. + /// + [JsonInclude] + [JsonPropertyName("tool_calls")] + public IReadOnlyList ToolCalls { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/SubmitToolOutputs.cs b/OpenAI-DotNet/Threads/SubmitToolOutputs.cs new file mode 100644 index 00000000..c496c3f7 --- /dev/null +++ b/OpenAI-DotNet/Threads/SubmitToolOutputs.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class SubmitToolOutputs + { + /// + /// A list of the relevant tool calls. + /// + [JsonInclude] + [JsonPropertyName("tool_calls")] + public IReadOnlyList ToolCalls { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs b/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs new file mode 100644 index 00000000..97fab3c0 --- /dev/null +++ b/OpenAI-DotNet/Threads/SubmitToolOutputsRequest.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class SubmitToolOutputsRequest + { + /// + /// Tool output to be submitted. + /// + /// . + public SubmitToolOutputsRequest(ToolOutput toolOutput) + : this(new[] { toolOutput }) + { + } + + /// + /// A list of tools for which the outputs are being submitted. + /// + /// Collection of tools for which the outputs are being submitted. + public SubmitToolOutputsRequest(IEnumerable toolOutputs) + { + ToolOutputs = toolOutputs?.ToList(); + } + + /// + /// A list of tools for which the outputs are being submitted. + /// + [JsonPropertyName("tool_outputs")] + public IReadOnlyList ToolOutputs { get; } + + public static implicit operator SubmitToolOutputsRequest(ToolOutput toolOutput) => new SubmitToolOutputsRequest(toolOutput); + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/TextContent.cs b/OpenAI-DotNet/Threads/TextContent.cs new file mode 100644 index 00000000..1485e661 --- /dev/null +++ b/OpenAI-DotNet/Threads/TextContent.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class TextContent + { + /// + /// The data that makes up the text. + /// + [JsonInclude] + [JsonPropertyName("value")] + public string Value { get; private set; } + + /// + /// Annotations + /// + [JsonInclude] + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IReadOnlyList Annotations { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ThreadExtensions.cs b/OpenAI-DotNet/Threads/ThreadExtensions.cs new file mode 100644 index 00000000..8d84470f --- /dev/null +++ b/OpenAI-DotNet/Threads/ThreadExtensions.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI.Threads +{ + public static class ThreadExtensions + { + /// + /// Updates this thread with the latest snapshot from OpenAI. + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this ThreadResponse thread, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.RetrieveThreadAsync(thread, cancellationToken).ConfigureAwait(false); + + /// + /// Modify the thread. + /// + /// + /// Only the can be modified. + /// + /// . + /// The metadata to set on the thread. + /// Optional, . + /// . + public static async Task ModifyAsync(this ThreadResponse thread, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ModifyThreadAsync(thread, metadata, cancellationToken).ConfigureAwait(false); + + /// + /// Deletes the thread. + /// + /// . + /// Optional, . + /// True, if the thread was successfully deleted. + public static async Task DeleteAsync(this ThreadResponse thread, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.DeleteThreadAsync(thread, cancellationToken).ConfigureAwait(false); + + #region Messages + + /// + /// Create a new message for this thread. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task CreateMessageAsync(this ThreadResponse thread, CreateMessageRequest request, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateMessageAsync(thread.Id, request, cancellationToken).ConfigureAwait(false); + + /// + /// List the messages associated to this thread. + /// + /// . + /// Optional, . + /// Optional, . + /// + public static async Task> ListMessagesAsync(this ThreadResponse thread, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListMessagesAsync(thread.Id, query, cancellationToken).ConfigureAwait(false); + + /// + /// Retrieve a message. + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this MessageResponse message, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.RetrieveMessageAsync(message.ThreadId, message.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Retrieve a message. + /// + /// . + /// The id of the message to get. + /// Optional, . + /// . + public static async Task RetrieveMessageAsync(this ThreadResponse thread, string messageId, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.RetrieveMessageAsync(thread.Id, messageId, cancellationToken).ConfigureAwait(false); + + /// + /// Modify a message. + /// + /// + /// Only the can be modified. + /// + /// . + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public static async Task ModifyAsync(this MessageResponse message, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.ModifyMessageAsync(message, metadata, cancellationToken).ConfigureAwait(false); + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// . + /// The id of the message to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public static async Task ModifyMessageAsync(this ThreadResponse thread, string messageId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ModifyMessageAsync(thread, messageId, metadata, cancellationToken).ConfigureAwait(false); + + #endregion Messages + + #region Files + + /// + /// Returns a list of message files. + /// + /// . + /// The id of the message that the files belongs to. + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this ThreadResponse thread, string messageId, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListFilesAsync(thread.Id, messageId, query, cancellationToken).ConfigureAwait(false); + + /// + /// Returns a list of message files. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this MessageResponse message, ListQuery query = null, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.ListFilesAsync(message.ThreadId, message.Id, query, cancellationToken).ConfigureAwait(false); + + /// + /// Retrieve message file. + /// + /// . + /// The id of the file being retrieved. + /// Optional, . + /// . + public static async Task RetrieveFileAsync(this MessageResponse message, string fileId, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.RetrieveFileAsync(message.ThreadId, message.Id, fileId, cancellationToken).ConfigureAwait(false); + + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + ///// + ///// Downloads a message file content to local disk. + ///// + ///// . + ///// The id of the file being retrieved. + ///// Directory to save the file content. + ///// Optional, delete cached file. Defaults to false. + ///// Optional, . + ///// Path to the downloaded file content. + //public static async Task DownloadFileContentAsync(this MessageResponse message, string fileId, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await message.Client.FilesEndpoint.DownloadFileAsync(fileId, directory, deleteCachedFile, cancellationToken).ConfigureAwait(false); + + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + ///// + ///// Downloads a message file content to local disk. + ///// + ///// . + ///// Directory to save the file content. + ///// Optional, delete cached file. Defaults to false. + ///// Optional, . + ///// Path to the downloaded file content. + //public static async Task DownloadContentAsync(this MessageFileResponse file, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await file.Client.FilesEndpoint.DownloadFileAsync(file.Id, directory, deleteCachedFile, cancellationToken).ConfigureAwait(false); + + #endregion Files + + #region Runs + + /// + /// Create a run. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task CreateRunAsync(this ThreadResponse thread, CreateRunRequest request = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateRunAsync(thread, request, cancellationToken).ConfigureAwait(false); + + /// + /// Create a run. + /// + /// . + /// Id of the assistant to use for the run. + /// Optional, . + /// . + public static async Task CreateRunAsync(this ThreadResponse thread, string assistantId, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateRunAsync(thread, new CreateRunRequest(assistantId), cancellationToken).ConfigureAwait(false); + + /// + /// Gets the thread associated to the . + /// + /// . + /// Optional, . + /// . + public static async Task GetThreadAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveThreadAsync(run.ThreadId, cancellationToken).ConfigureAwait(false); + + /// + /// List all of the runs associated to a thread. + /// + /// . + /// . + /// Optional, . + /// + public static async Task> ListRunsAsync(this ThreadResponse thread, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListRunsAsync(thread.Id, query, cancellationToken).ConfigureAwait(false); + + /// + /// Get the latest status of the . + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveRunAsync(run.ThreadId, run.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Modifies a run. + /// + /// + /// Only the can be modified. + /// + /// to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// Optional, . + /// . + public static async Task ModifyAsync(this RunResponse run, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.ModifyRunAsync(run.ThreadId, run.Id, metadata, cancellationToken).ConfigureAwait(false); + + /// + /// Waits for to change. + /// + /// . + /// Optional, time in milliseconds to wait before polling status. + /// Optional, timeout in seconds to cancel polling.
Defaults to 30 seconds.
Set to -1 for indefinite. + /// Optional, . + /// . + public static async Task WaitForStatusChangeAsync(this RunResponse run, int? pollingInterval = null, int? timeout = null, CancellationToken cancellationToken = default) + { + using var cts = timeout is null or < 0 ? new CancellationTokenSource(TimeSpan.FromSeconds(timeout ?? 30)) : new CancellationTokenSource(); + using var chainedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); + RunResponse result; + do + { + result = await run.UpdateAsync(cancellationToken: chainedCts.Token).ConfigureAwait(false); + await Task.Delay(pollingInterval ?? 500, chainedCts.Token).ConfigureAwait(false); + } while (result.Status is RunStatus.Queued or RunStatus.InProgress or RunStatus.Cancelling); + return result; + } + + /// + /// 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. + /// . + /// 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); + + /// + /// Returns a list of run steps belonging to a run. + /// + /// to list run steps for. + /// Optional, . + /// 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); + + /// + /// Retrieves a run step. + /// + /// to retrieve step for. + /// Id of the run step. + /// Optional, . + /// . + public static async Task RetrieveRunStepAsync(this RunResponse run, string runStepId, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveRunStepAsync(run.ThreadId, run.Id, runStepId, cancellationToken).ConfigureAwait(false); + + /// + /// Retrieves a run step. + /// + /// to retrieve. + /// Optional, . + /// . + public static async Task UpdateAsync(this RunStepResponse runStep, CancellationToken cancellationToken = default) + => await runStep.Client.ThreadsEndpoint.RetrieveRunStepAsync(runStep.ThreadId, runStep.RunId, runStep.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Cancels a run that is . + /// + /// to cancel. + /// Optional, . + /// . + public static async Task CancelAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.CancelRunAsync(run.ThreadId, run.Id, cancellationToken).ConfigureAwait(false); + + /// + /// Returns a list of messages for a given thread that the run belongs to. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListMessagesAsync(this RunResponse run, ListQuery query = null, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.ListMessagesAsync(run.ThreadId, query, cancellationToken); + + #endregion Runs + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ThreadResponse.cs b/OpenAI-DotNet/Threads/ThreadResponse.cs new file mode 100644 index 00000000..b860abf1 --- /dev/null +++ b/OpenAI-DotNet/Threads/ThreadResponse.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// A conversation session between an Assistant and a user. + /// Threads store Messages and automatically handle truncation to fit content into a model�s context. + /// + public sealed class ThreadResponse : BaseResponse + { + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always thread. + /// + [JsonInclude] + [JsonPropertyName("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [JsonInclude] + [JsonPropertyName("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [JsonInclude] + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; private set; } + + public static implicit operator string(ThreadResponse thread) => thread?.ToString(); + + public override string ToString() => Id; + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs new file mode 100644 index 00000000..c5f5fdca --- /dev/null +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -0,0 +1,354 @@ +using OpenAI.Extensions; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI.Threads +{ + /// + /// Create threads that assistants can interact with.
+ /// + ///
+ public sealed class ThreadsEndpoint : BaseEndPoint + { + public ThreadsEndpoint(OpenAIClient api) : base(api) { } + + protected override string Root => "threads"; + + /// + /// Create a thread. + /// + /// . + /// Optional, . + /// . + public async Task CreateThreadAsync(CreateThreadRequest request = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.PostAsync(GetUrl(), request != null ? JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug) : null, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Retrieves a thread. + /// + /// The id of the to retrieve. + /// Optional, . + /// . + public async Task RetrieveThreadAsync(string threadId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Modifies a thread. + /// + /// + /// Only the can be modified. + /// + /// The id of the to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyThreadAsync(string threadId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Delete a thread. + /// + /// The id of the to delete. + /// Optional, . + /// True, if was successfully deleted. + public async Task DeleteThreadAsync(string threadId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #region Messages + + /// + /// Create a message. + /// + /// The id of the thread to create a message for. + /// + /// Optional, . + /// . + public async Task CreateMessageAsync(string threadId, CreateMessageRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Returns a list of messages for a given thread. + /// + /// The id of the thread the messages belong to. + /// . + /// Optional, . + /// . + public async Task> ListMessagesAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/messages", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Retrieve a message. + /// + /// The id of the thread to which this message belongs. + /// The id of the message to retrieve. + /// Optional, . + /// . + public async Task RetrieveMessageAsync(string threadId, string messageId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyMessageAsync(MessageResponse message, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await ModifyMessageAsync(message.ThreadId, message.Id, metadata, cancellationToken).ConfigureAwait(false); + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// The id of the thread to which this message belongs. + /// The id of the message to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyMessageAsync(string threadId, string messageId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + #endregion Messages + + #region Files + + /// + /// Returns a list of message files. + /// + /// The id of the thread that the message and files belong to. + /// The id of the message that the files belongs to. + /// . + /// Optional, . + /// . + public async Task> ListFilesAsync(string threadId, string messageId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Retrieve message file. + /// + /// The id of the thread to which the message and file belong. + /// The id of the message the file belongs to. + /// The id of the file being retrieved. + /// Optional, . + /// . + public async Task RetrieveFileAsync(string threadId, string messageId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + #endregion Files + + #region Runs + + /// + /// Returns a list of runs belonging to a thread. + /// + /// The id of the thread the run belongs to. + /// . + /// Optional, . + /// + public async Task> ListRunsAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/runs", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Create a run. + /// + /// The id of the thread to run. + /// + /// Optional, . + /// . + public async Task CreateRunAsync(string threadId, CreateRunRequest request = null, CancellationToken cancellationToken = default) + { + if (request == null || string.IsNullOrWhiteSpace(request.AssistantId)) + { + var assistant = await Api.AssistantsEndpoint.CreateAssistantAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + request = new CreateRunRequest(assistant, request); + } + + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Create a thread and run it in one request. + /// + /// . + /// Optional, . + /// . + public async Task CreateThreadAndRunAsync(CreateThreadAndRunRequest request = null, CancellationToken cancellationToken = default) + { + if (request == null || string.IsNullOrWhiteSpace(request.AssistantId)) + { + var assistant = await Api.AssistantsEndpoint.CreateAssistantAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + request = new CreateThreadAndRunRequest(assistant, request); + } + + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl("/runs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Retrieves a run. + /// + /// The id of the thread that was run. + /// The id of the run to retrieve. + /// Optional, . + /// . + public async Task RetrieveRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Modifies a run. + /// + /// + /// Only the can be modified. + /// + /// The id of the thread that was run. + /// The id of the to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// Optional, . + /// . + public async Task ModifyRunAsync(string threadId, string runId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// 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. + /// + /// The id of the thread to which this run belongs. + /// The id of the run that requires the tool output submission. + /// . + /// Optional, . + /// . + public async Task SubmitToolOutputsAsync(string threadId, string runId, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Returns a list of run steps belonging to a run. + /// + /// The id of the thread to which the run and run step belongs. + /// The id of the run to which the run step belongs. + /// Optional, . + /// Optional, . + /// . + public async Task> ListRunStepsAsync(string threadId, string runId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize>(responseAsString, Api); + } + + /// + /// Retrieves a run step. + /// + /// The id of the thread to which the run and run step belongs. + /// The id of the run to which the run step belongs. + /// The id of the run step to retrieve. + /// Optional, . + /// . + public async Task RetrieveRunStepAsync(string threadId, string runId, string stepId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + /// + /// Cancels a run that is . + /// + /// The id of the thread to which this run belongs. + /// The id of the run to cancel. + /// Optional, . + /// . + public async Task CancelRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + { + var response = await Api.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), content: null, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + return response.Deserialize(responseAsString, Api); + } + + #endregion Runs + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ToolCall.cs b/OpenAI-DotNet/Threads/ToolCall.cs new file mode 100644 index 00000000..37dc4a0d --- /dev/null +++ b/OpenAI-DotNet/Threads/ToolCall.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + public sealed class ToolCall + { + /// + /// The ID of the tool call. + /// This ID must be referenced when you submit the tool outputs in using the Submit tool outputs to run endpoint. + /// + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; private set; } + + /// + /// The type of tool call the output is required for. For now, this is always 'function'. + /// + [JsonInclude] + [JsonPropertyName("type")] + public string Type { get; private set; } + + /// + /// The function definition. + /// + [JsonInclude] + [JsonPropertyName("function")] + public FunctionCall FunctionCall { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ToolOutput.cs b/OpenAI-DotNet/Threads/ToolOutput.cs new file mode 100644 index 00000000..a89f2e26 --- /dev/null +++ b/OpenAI-DotNet/Threads/ToolOutput.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Threads +{ + /// + /// Tool function call output + /// + public sealed class ToolOutput + { + /// + /// Constructor. + /// + /// + /// The ID of the tool call in the within the the output is being submitted for. + /// + /// + /// The output of the tool call to be submitted to continue the run. + /// + [JsonConstructor] + public ToolOutput(string toolCallId, string output) + { + ToolCallId = toolCallId; + Output = output; + } + + /// + /// The ID of the tool call in the within the the output is being submitted for. + /// + [JsonPropertyName("tool_call_id")] + public string ToolCallId { get; } + + /// + /// The output of the tool call to be submitted to continue the run. + /// + [JsonPropertyName("output")] + public string Output { get; } + } +} \ No newline at end of file diff --git a/README.md b/README.md index f7c2c83d..fa292bad 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ [![NuGet version (OpenAI-DotNet-Proxy)](https://img.shields.io/nuget/v/OpenAI-DotNet-Proxy.svg?label=OpenAI-DotNet-Proxy&logo=nuget)](https://www.nuget.org/packages/OpenAI-DotNet-Proxy/) [![Nuget Publish](https://github.com/RageAgainstThePixel/OpenAI-DotNet/actions/workflows/Publish-Nuget.yml/badge.svg)](https://github.com/RageAgainstThePixel/OpenAI-DotNet/actions/workflows/Publish-Nuget.yml) -A simple C# .NET client library for [OpenAI](https://openai.com/) to use though their RESTful API. Independently developed, this is not an official library and I am not affiliated with OpenAI. An OpenAI API account is required. +A simple C# .NET client library for [OpenAI](https://openai.com/) to use though their RESTful API. +Independently developed, this is not an official library and I am not affiliated with OpenAI. +An OpenAI API account is required. Forked from [OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet). - More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-api). > This repository is available to transfer to the OpenAI organization if they so choose to accept it. @@ -45,41 +46,76 @@ Install-Package OpenAI-DotNet - [List Models](#list-models) - [Retrieve Models](#retrieve-model) - [Delete Fine Tuned Model](#delete-fine-tuned-model) -- [Completions](#completions) - - [Streaming](#completion-streaming) +- [Assistants](#assistants) :new: + - [List Assistants](#list-assistants) :new: + - [Create Assistant](#create-assistant) :new: + - [Retrieve Assistant](#retrieve-assistant) :new: + - [Modify Assistant](#modify-assistant) :new: + - [Delete Assistant](#delete-assistant) :new: + - [List Assistant Files](#list-assistant-files) :new: + - [Attach File to Assistant](#attach-file-to-assistant) :new: + - [Upload File to Assistant](#upload-file-to-assistant) :new: + - [Retrieve File from Assistant](#retrieve-file-from-assistant) :new: + - [Remove File from Assistant](#remove-file-from-assistant) :new: + - [Delete File from Assistant](#delete-file-from-assistant) :new: +- [Threads](#threads) :new: + - [Create Thread](#create-thread) :new: + - [Create Thread and Run](#create-thread-and-run) :new: + - [Retrieve Thread](#retrieve-thread) :new: + - [Modify Thread](#modify-thread) :new: + - [Delete Thread](#delete-thread) :new: + - [Thread Messages](#thread-messages) :new: + - [List Messages](#list-thread-messages) :new: + - [Create Message](#create-thread-message) :new: + - [Retrieve Message](#retrieve-thread-message) :new: + - [Modify Message](#modify-thread-message) :new: + - [Thread Message Files](#thread-message-files) :new: + - [List Message Files](#list-thread-message-files) :new: + - [Retrieve Message File](#retrieve-thread-message-file) :new: + - [Thread Runs](#thread-runs) :new: + - [List Runs](#list-thread-runs) :new: + - [Create Run](#create-thread-run) :new: + - [Retrieve Run](#retrieve-thread-run) :new: + - [Modify Run](#modify-thread-run) :new: + - [Submit Tool Outputs to Run](#thread-submit-tool-outputs-to-run) :new: + - [List Run Steps](#list-thread-run-steps) :new: + - [Retrieve Run Step](#retrieve-thread-run-step) :new: + - [Cancel Run](#cancel-thread-run) :new: - [Chat](#chat) - [Chat Completions](#chat-completions) - [Streaming](#chat-streaming) - [Tools](#chat-tools) :new: - [Vision](#chat-vision) :new: -- [Edits](#edits) - - [Create Edit](#create-edit) -- [Embeddings](#embeddings) - - [Create Embedding](#create-embeddings) - [Audio](#audio) - [Create Speech](#create-speech) - [Create Transcription](#create-transcription) - [Create Translation](#create-translation) -- [Images](#images) - - [Create Image](#create-image) - - [Edit Image](#edit-image) - - [Create Image Variation](#create-image-variation) -- [Files](#files) - - [List Files](#list-files) +- [Images](#images) :construction: + - [Create Image](#create-image) :construction: + - [Edit Image](#edit-image) :construction: + - [Create Image Variation](#create-image-variation) :construction: +- [Files](#files) :construction: + - [List Files](#list-files) :construction: - [Upload File](#upload-file) - [Delete File](#delete-file) - - [Retrieve File Info](#retrieve-file-info) + - [Retrieve File](#retrieve-file-info) :construction: - [Download File Content](#download-file-content) -- [Fine Tuning](#fine-tuning) - - [Create Fine Tune Job](#create-fine-tune-job) - - [List Fine Tune Jobs](#list-fine-tune-jobs) - - [Retrieve Fine Tune Job Info](#retrieve-fine-tune-job-info) +- [Fine Tuning](#fine-tuning) :construction: + - [Create Fine Tune Job](#create-fine-tune-job) :construction: + - [List Fine Tune Jobs](#list-fine-tune-jobs) :construction: + - [Retrieve Fine Tune Job Info](#retrieve-fine-tune-job-info) :construction: - [Cancel Fine Tune Job](#cancel-fine-tune-job) - - [List Fine Tune Job Events](#list-fine-tune-job-events) + - [List Fine Tune Job Events](#list-fine-tune-job-events) :construction: +- [Embeddings](#embeddings) + - [Create Embedding](#create-embeddings) +- [Completions](#completions) :construction: + - [Streaming](#completion-streaming) :construction: - [Moderations](#moderations) - [Create Moderation](#create-moderation) +- ~~[Edits](#edits)~~ :warning: Deprecated + - ~~[Create Edit](#create-edit)~~ :warning: Deprecated -### Authentication +### [Authentication](https://platform.openai.com/docs/api-reference/authentication) There are 3 ways to provide your API keys, in order of precedence: @@ -152,7 +188,7 @@ Use your system's environment variables specify an api key and organization to u var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); ``` -### [Azure OpenAI](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/) +### [Azure OpenAI](https://learn.microsoft.com/en-us/azure/cognitive-services/openai) You can also choose to use Microsoft's Azure OpenAI deployments as well. @@ -302,50 +338,495 @@ Delete a fine-tuned model. You must have the Owner role in your organization. ```csharp var api = new OpenAIClient(); -var result = await api.ModelsEndpoint.DeleteFineTuneModelAsync("your-fine-tuned-model"); -Assert.IsTrue(result); +var isDeleted = await api.ModelsEndpoint.DeleteFineTuneModelAsync("your-fine-tuned-model"); +Assert.IsTrue(isDeleted); ``` -### [Completions](https://platform.openai.com/docs/api-reference/completions) +### [Assistants](https://platform.openai.com/docs/api-reference/assistants) -Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. +> :warning: Beta Feature -The Completions API is accessed via `OpenAIClient.CompletionsEndpoint` +Build assistants that can call models and use tools to perform tasks. + +- [Assistants Guide](https://platform.openai.com/docs/assistants) +- [OpenAI Assistants Cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb) + +The Assistants API is accessed via `OpenAIClient.AssistantsEndpoint` + +#### [List Assistants](https://platform.openai.com/docs/api-reference/assistants/listAssistants) + +Returns a list of assistants. ```csharp var api = new OpenAIClient(); -var result = await api.CompletionsEndpoint.CreateCompletionAsync("One Two Three One Two", temperature: 0.1, model: Model.Davinci); -Console.WriteLine(result); +var assistantsList = await OpenAIClient.AssistantsEndpoint.ListAssistantsAsync(); + +foreach (var assistant in assistantsList.Items) +{ + Console.WriteLine($"{assistant} -> {assistant.CreatedAt}"); +} ``` -> To get the `CompletionResult` (which is mostly metadata), use its implicit string operator to get the text if all you want is the completion choice. +#### [Create Assistant](https://platform.openai.com/docs/api-reference/assistants/createAssistant) -#### Completion Streaming +Create an assistant with a model and instructions. -Streaming allows you to get results are they are generated, which can help your application feel more responsive, especially on slow models like Davinci. +```csharp +var api = new OpenAIClient(); +var request = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(request); +``` + +#### [Retrieve Assistant](https://platform.openai.com/docs/api-reference/assistants/getAssistant) + +Retrieves an assistant. + +```csharp +var api = new OpenAIClient(); +var assistant = await OpenAIClient.AssistantsEndpoint.RetrieveAssistantAsync("assistant-id"); +Console.WriteLine($"{assistant} -> {assistant.CreatedAt}"); +``` + +#### [Modify Assistant](https://platform.openai.com/docs/api-reference/assistants/modifyAssistant) + +Modifies an assistant. + +```csharp +var api = new OpenAIClient(); +var createRequest = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(createRequest); +var modifyRequest = new CreateAssistantRequest("gpt-4-1106-preview"); +var modifiedAssistant = await api.AssistantsEndpoint.ModifyAsync(assistant.Id, modifyRequest); +// OR AssistantExtension for easier use! +var modifiedAssistantEx = await assistant.ModifyAsync(modifyRequest); +``` + +#### [Delete Assistant](https://platform.openai.com/docs/api-reference/assistants/deleteAssistant) + +Delete an assistant. + +```csharp +var api = new OpenAIClient(); +var isDeleted = await api.AssistantsEndpoint.DeleteAssistantAsync("assistant-id"); +// OR AssistantExtension for easier use! +var isDeleted = await assistant.DeleteAsync(); +Assert.IsTrue(isDeleted); +``` + +#### [List Assistant Files](https://platform.openai.com/docs/api-reference/assistants/listAssistantFiles) + +Returns a list of assistant files. + +```csharp +var api = new OpenAIClient(); +var filesList = await api.AssistantsEndpoint.ListFilesAsync("assistant-id"); +// OR AssistantExtension for easier use! +var filesList = await assistant.ListFilesAsync(); + +foreach (var file in filesList.Items) +{ + Console.WriteLine($"{file.AssistantId}'s file -> {file.Id}"); +} +``` + +#### [Attach File to Assistant](https://platform.openai.com/docs/api-reference/assistants/createAssistantFile) + +Create an assistant file by attaching a File to an assistant. + +```csharp +var api = new OpenAIClient(); +var filePath = "assistant_test_2.txt"; +await File.WriteAllTextAsync(filePath, "Knowledge is power!"); +var fileUploadRequest = new FileUploadRequest(filePath, "assistant"); +var file = await api.FilesEndpoint.UploadFileAsync(fileUploadRequest); +var assistantFile = await api.AssistantsEndpoint.AttachFileAsync("assistant-id", file.Id); +// OR use extension method for convenience! +var assistantFIle = await assistant.AttachFileAsync(file); +``` + +#### [Upload File to Assistant](#upload-file) + +Uploads ***and*** attaches a file to an assistant. + +> Assistant extension method, for extra convenience! + +```csharp +var api = new OpenAIClient(); +var filePath = "assistant_test_2.txt"; +await File.WriteAllTextAsync(filePath, "Knowledge is power!"); +var assistantFile = await assistant.UploadFileAsync(filePath); +``` + +#### [Retrieve File from Assistant](https://platform.openai.com/docs/api-reference/assistants/getAssistantFile) + +Retrieves an AssistantFile. + +```csharp +var api = new OpenAIClient(); +var assistantFile = await api.AssistantsEndpoint.RetrieveFileAsync("assistant-id", "file-id"); +// OR AssistantExtension for easier use! +var assistantFile = await assistant.RetrieveFileAsync(fileId); +Console.WriteLine($"{assistantFile.AssistantId}'s file -> {assistantFile.Id}"); +``` + +#### [Remove File from Assistant](https://platform.openai.com/docs/api-reference/assistants/deleteAssistantFile) + +Remove a file from an assistant. + +> Note: The file will remain in your organization until [deleted with FileEndpoint](#delete-file). + +```csharp +var api = new OpenAIClient(); +var isRemoved = await api.AssistantsEndpoint.RemoveFileAsync("assistant-id", "file-id"); +// OR use extension method for convenience! +var isRemoved = await assistant.RemoveFileAsync("file-id"); +Assert.IsTrue(isRemoved); +``` + +#### [Delete File from Assistant](#delete-file) + +Removes a file from the assistant and then deletes the file from the organization. + +> Assistant extension method, for extra convenience! + +```csharp +var api = new OpenAIClient(); +var isDeleted = await assistant.DeleteFileAsync("file-id"); +Assert.IsTrue(isDeleted); +``` + +### [Threads](https://platform.openai.com/docs/api-reference/threads) + +> :warning: Beta Feature + +Create Threads that [Assistants](#assistants) can interact with. + +The Threads API is accessed via `OpenAIClient.ThreadsEndpoint` + +#### [Create Thread](https://platform.openai.com/docs/api-reference/threads/createThread) + +Create a thread. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +Console.WriteLine($"Create thread {thread.Id} -> {thread.CreatedAt}"); +``` + +#### [Create Thread and Run](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun) + +Create a thread and run it in one request. + +> See also: [Thread Runs](#thread-runs) + +```csharp +var api = new OpenAIClient(); +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")); +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); +Console.WriteLine($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}"); +``` + +#### [Retrieve Thread](https://platform.openai.com/docs/api-reference/threads/getThread) + +Retrieves a thread. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.RetrieveThreadAsync("thread-id"); +// OR if you simply wish to get the latest state of a thread +thread = await thread.UpdateAsync(); +Console.WriteLine($"Retrieve thread {thread.Id} -> {thread.CreatedAt}"); +``` + +#### [Modify Thread](https://platform.openai.com/docs/api-reference/threads/modifyThread) + +Modifies a thread. + +> Note: Only the metadata can be modified. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +var metadata = new Dictionary +{ + { "key", "custom thread metadata" } +} +thread = await api.ThreadsEndpoint.ModifyThreadAsync(thread.Id, metadata); +// OR use extension method for convenience! +thread = await thread.ModifyAsync(metadata); +Console.WriteLine($"Modify thread {thread.Id} -> {thread.Metadata["key"]}"); +``` + +#### [Delete Thread](https://platform.openai.com/docs/api-reference/threads/deleteThread) + +Delete a thread. + +```csharp +var api = new OpenAIClient(); +var isDeleted = await api.ThreadsEndpoint.DeleteThreadAsync("thread-id"); +// OR use extension method for convenience! +var isDeleted = await thread.DeleteAsync(); +Assert.IsTrue(isDeleted); +``` + +#### [Thread Messages](https://platform.openai.com/docs/api-reference/messages) + +Create messages within threads. + +##### [List Thread Messages](https://platform.openai.com/docs/api-reference/messages/listMessages) + +Returns a list of messages for a given thread. + +```csharp +var api = new OpenAIClient(); +var messageList = await api.ThreadsEndpoint.ListMessagesAsync("thread-id"); +// OR use extension method for convenience! +var messageList = await thread.ListMessagesAsync(); + +foreach (var message in messageList.Items) +{ + Console.WriteLine($"{message.Id}: {message.Role}: {message.PrintContent()}"); +} +``` + +##### [Create Thread Message](https://platform.openai.com/docs/api-reference/messages/createMessage) + +Create a message. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +var request = new CreateMessageRequest("Hello world!"); +var message = await api.ThreadsEndpoint.CreateMessageAsync(thread.Id, request); +// OR use extension method for convenience! +var message = await thread.CreateMessageAsync("Hello World!"); +Console.WriteLine($"{message.Id}: {message.Role}: {message.PrintContent()}"); +``` + +##### [Retrieve Thread Message](https://platform.openai.com/docs/api-reference/messages/getMessage) + +Retrieve a message. + +```csharp +var api = new OpenAIClient(); +var message = await api.ThreadsEndpoint.RetrieveMessageAsync("thread-id", "message-id"); +// OR use extension methods for convenience! +var message = await thread.RetrieveMessageAsync("message-id"); +var message = await message.UpdateAsync(); +Console.WriteLine($"{message.Id}: {message.Role}: {message.PrintContent()}"); +``` + +##### [Modify Thread Message](https://platform.openai.com/docs/api-reference/messages/modifyMessage) + +Modify a message. + +> Note: Only the message metadata can be modified. + +```csharp +var api = new OpenAIClient(); +var metadata = new Dictionary +{ + { "key", "custom message metadata" } +}; +var message = await api.ThreadsEndpoint.ModifyMessageAsync("thread-id", "message-id", metadata); +// OR use extension method for convenience! +var message = await message.ModifyAsync(metadata); +Console.WriteLine($"Modify message metadata: {message.Id} -> {message.Metadata["key"]}"); +``` + +##### Thread Message Files + +###### [List Thread Message Files](https://platform.openai.com/docs/api-reference/messages/listMessageFiles) + +Returns a list of message files. + +```csharp +var api = new OpenAIClient(); +var fileList = await api.ThreadsEndpoint.ListFilesAsync("thread-id", "message-Id"); +// OR use extension method for convenience! +var fileList = await thread.ListFilesAsync("message-id"); +var fileList = await message.ListFilesAsync(); + +foreach (var file in fileList.Items) +{ + Console.WriteLine(file.Id); +} +``` + +###### [Retrieve Thread Message File](https://platform.openai.com/docs/api-reference/messages/getMessageFile) + +Retrieves a message file. + +```csharp +var api = new OpenAIClient(); +var file = await api.ThreadsEndpoint.RetrieveFileAsync("thread-id", "message-id", "file-id"); +// OR use extension method for convenience! +var file = await message.RetrieveFileAsync(); +Console.WriteLine(file.Id); +``` + +#### [Thread Runs](https://platform.openai.com/docs/api-reference/runs) + +Represents an execution run on a thread. + +##### [List Thread Runs](https://platform.openai.com/docs/api-reference/runs/listRuns) + +Returns a list of runs belonging to a thread. + +```csharp +var api = new OpenAIClient(); +var runList = await api.ThreadsEndpoint.ListRunsAsync("thread-id"); +// OR use extension method for convenience! +var runList = await thread.ListRunsAsync(); + +foreach (var run in runList.Items) +{ + Console.WriteLine($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +} +``` + +##### [Create Thread Run](https://platform.openai.com/docs/api-reference/runs/createRun) + +Create a run. ```csharp var api = new OpenAIClient(); +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")); +var thread = await OpenAIClient.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); +Console.WriteLine($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +``` + +##### [Retrieve Thread Run](https://platform.openai.com/docs/api-reference/runs/getRun) + +Retrieves a run. + +```csharp +var api = new OpenAIClient(); +var run = await api.ThreadsEndpoint.RetrieveRunAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var run = await thread.RetrieveRunAsync("run-id"); +var run = await run.UpdateAsync(); +Console.WriteLine($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +``` + +##### [Modify Thread Run](https://platform.openai.com/docs/api-reference/runs/modifyRun) + +Modifies a run. + +> Note: Only the metadata can be modified. -await api.CompletionsEndpoint.StreamCompletionAsync(result => +```csharp +var api = new OpenAIClient(); +var metadata = new Dictionary { - foreach (var choice in result.Completions) + { "key", "custom run metadata" } +}; +var run = await api.ThreadsEndpoint.ModifyRunAsync("thread-id", "run-id", metadata); +// OR use extension method for convenience! +var run = await run.ModifyAsync(metadata); +Console.WriteLine($"Modify run {run.Id} -> {run.Metadata["key"]}"); +``` + +##### [Thread Submit Tool Outputs to Run](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) + +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. + +```csharp +var api = new OpenAIClient(); +var function = new Function( + nameof(WeatherService.GetCurrentWeather), + "Get the current weather in a given location", + new JsonObject { - Console.WriteLine(choice); - } -}, "My name is Roger and I am a principal software engineer at Salesforce. This is my resume:", maxTokens: 200, temperature: 0.5, presencePenalty: 0.1, frequencyPenalty: 0.1, model: Model.Davinci); + ["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?"); +// 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); +// waiting while run in Queued and InProgress +run = await run.WaitForStatusChangeAsync(); +var messages = await run.ListMessagesAsync(); + +foreach (var message in messages.Items.OrderBy(response => response.CreatedAt)) +{ + Console.WriteLine($"{message.Role}: {message.PrintContent()}"); +} ``` -Or if using [`IAsyncEnumerable{T}`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-5.0) ([C# 8.0+](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8)) +##### [List Thread Run Steps](https://platform.openai.com/docs/api-reference/runs/listRunSteps) + +Returns a list of run steps belonging to a run. ```csharp var api = new OpenAIClient(); -await foreach (var token in api.CompletionsEndpoint.StreamCompletionEnumerableAsync("My name is Roger and I am a principal software engineer at Salesforce. This is my resume:", maxTokens: 200, temperature: 0.5, presencePenalty: 0.1, frequencyPenalty: 0.1, model: Model.Davinci)) +var runStepList = await api.ThreadsEndpoint.ListRunStepsAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var runStepList = await run.ListRunStepsAsync(); + +foreach (var runStep in runStepList.Items) { - Console.WriteLine(token); + Console.WriteLine($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); } ``` +##### [Retrieve Thread Run Step](https://platform.openai.com/docs/api-reference/runs/getRunStep) + +Retrieves a run step. + +```csharp +var api = new OpenAIClient(); +var runStep = await api.ThreadsEndpoint.RetrieveRunStepAsync("thread-id", "run-id", "step-id"); +// OR use extension method for convenience! +var runStep = await run.RetrieveRunStepAsync("step-id"); +var runStep = await runStep.UpdateAsync(); +Console.WriteLine($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); +``` + +##### [Cancel Thread Run](https://platform.openai.com/docs/api-reference/runs/cancelRun) + +Cancels a run that is `in_progress`. + +```csharp +var api = new OpenAIClient(); +var isCancelled = await api.ThreadsEndpoint.CancelRunAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var isCancelled = await run.CancelAsync(); +Assert.IsTrue(isCancelled); +``` + ### [Chat](https://platform.openai.com/docs/api-reference/chat) Given a chat conversation, the model will return a chat completion response. @@ -365,12 +846,12 @@ var messages = new List new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; -var chatRequest = new ChatRequest(messages, Model.GPT3_5_Turbo); -var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); -Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content}"); +var chatRequest = new ChatRequest(messages, Model.GPT4); +var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content}"); ``` -##### [Chat Streaming](https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream) +#### [Chat Streaming](https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream) ```csharp var api = new OpenAIClient(); @@ -381,21 +862,16 @@ var messages = new List new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; -var chatRequest = new ChatRequest(messages, Model.GPT3_5_Turbo, number: 2); -await api.ChatEndpoint.StreamCompletionAsync(chatRequest, result => +var chatRequest = new ChatRequest(messages); +var response = await api.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { - foreach (var choice in result.Choices.Where(choice => !string.IsNullOrEmpty(choice.Delta?.Content))) - { - // Partial response content - Console.WriteLine(choice.Delta.Content); - } - - foreach (var choice in result.Choices.Where(choice => !string.IsNullOrEmpty(choice.Message?.Content))) + foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null)) { - // Completed response content - Console.WriteLine($"{choice.Message.Role}: {choice.Message.Content}"); + Console.Write(choice.Delta.Content); } }); +var choice = response.FirstChoice; +Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); ``` Or if using [`IAsyncEnumerable{T}`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-5.0) ([C# 8.0+](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8)) @@ -409,24 +885,20 @@ var messages = new List new Message(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new Message(Role.User, "Where was it played?"), }; -var chatRequest = new ChatRequest(messages, Model.GPT4); // gpt4 access required -await foreach (var result in api.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) +var cumulativeDelta = string.Empty; +var chatRequest = new ChatRequest(messages); +await foreach (var partialResponse in OpenAIClient.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) { - foreach (var choice in result.Choices.Where(choice => !string.IsNullOrEmpty(choice.Delta?.Content))) + foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null)) { - // Partial response content - Console.WriteLine(choice.Delta.Content); - } - - foreach (var choice in result.Choices.Where(choice => !string.IsNullOrEmpty(choice.Message?.Content))) - { - // Completed response content - Console.WriteLine($"{choice.Message.Role}: {choice.Message.Content}"); + cumulativeDelta += choice.Delta.Content; } } + +Console.WriteLine(cumulativeDelta); ``` -##### [Chat Tools](https://platform.openai.com/docs/guides/function-calling) +#### [Chat Tools](https://platform.openai.com/docs/guides/function-calling) ```csharp var api = new OpenAIClient(); @@ -468,32 +940,32 @@ var tools = new List }; var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); -var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); -messages.Add(result.FirstChoice.Message); +var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +messages.Add(response.FirstChoice.Message); -Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); +Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); -result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); -messages.Add(result.FirstChoice.Message); +messages.Add(response.FirstChoice.Message); -if (!string.IsNullOrEmpty(result.FirstChoice.Message.Content)) +if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); - result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); + response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); } -var usedTool = result.FirstChoice.Message.ToolCalls[0]; -Console.WriteLine($"{result.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); +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); @@ -511,7 +983,9 @@ Console.WriteLine($"{Role.Tool}: {functionResult}"); // Tool: The current weather in Glasgow, Scotland is 20 celsius ``` -##### [Chat Vision](https://platform.openai.com/docs/guides/vision) +#### [Chat Vision](https://platform.openai.com/docs/guides/vision) + +> :warning: Beta Feature ```csharp var api = new OpenAIClient(); @@ -525,45 +999,8 @@ var messages = new List }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); -var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); -Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishDetails}"); -``` - -### [Edits](https://platform.openai.com/docs/api-reference/edits) - -> Deprecated, and soon to be removed. - -Given a prompt and an instruction, the model will return an edited version of the prompt. - -The Edits API is accessed via `OpenAIClient.EditsEndpoint` - -#### [Create Edit](https://platform.openai.com/docs/api-reference/edits/create) - -Creates a new edit for the provided input, instruction, and parameters using the provided input and instruction. - -```csharp -var api = new OpenAIClient(); -var request = new EditRequest("What day of the wek is it?", "Fix the spelling mistakes"); -var result = await api.EditsEndpoint.CreateEditAsync(request); -Console.WriteLine(result); -``` - -### [Embeddings](https://platform.openai.com/docs/api-reference/embeddings) - -Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. - -Related guide: [Embeddings](https://platform.openai.com/docs/guides/embeddings) - -The Edits API is accessed via `OpenAIClient.EmbeddingsEndpoint` - -#### [Create Embeddings](https://platform.openai.com/docs/api-reference/embeddings/create) - -Creates an embedding vector representing the input text. - -```csharp -var api = new OpenAIClient(); -var result = await api.EmbeddingsEndpoint.CreateEmbeddingAsync("The food was delicious and the waiter...", Models.Embedding_Ada_002); -Console.WriteLine(result); +var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); ``` ### [Audio](https://platform.openai.com/docs/api-reference/audio) @@ -585,8 +1022,8 @@ async Task ChunkCallback(ReadOnlyMemory chunkCallback) await Task.CompletedTask; } -var result = await api.AudioEndpoint.CreateSpeechAsync(request, ChunkCallback); -await File.WriteAllBytesAsync(@"..\..\..\Assets\HelloWorld.mp3", result.ToArray()); +var response = await api.AudioEndpoint.CreateSpeechAsync(request, ChunkCallback); +await File.WriteAllBytesAsync("../../../Assets/HelloWorld.mp3", response.ToArray()); ``` #### [Create Transcription](https://platform.openai.com/docs/api-reference/audio/createTranscription) @@ -596,8 +1033,8 @@ Transcribes audio into the input language. ```csharp var api = new OpenAIClient(); var request = new AudioTranscriptionRequest(Path.GetFullPath(audioAssetPath), language: "en"); -var result = await api.AudioEndpoint.CreateTranscriptionAsync(request); -Console.WriteLine(result); +var response = await api.AudioEndpoint.CreateTranscriptionAsync(request); +Console.WriteLine(response); ``` #### [Create Translation](https://platform.openai.com/docs/api-reference/audio/create) @@ -607,8 +1044,8 @@ Translates audio into into English. ```csharp var api = new OpenAIClient(); var request = new AudioTranslationRequest(Path.GetFullPath(audioAssetPath)); -var result = await api.AudioEndpoint.CreateTranslationAsync(request); -Console.WriteLine(result); +var response = await api.AudioEndpoint.CreateTranslationAsync(request); +Console.WriteLine(response); ``` ### [Images](https://platform.openai.com/docs/api-reference/images) @@ -623,13 +1060,13 @@ Creates an image given a prompt. ```csharp var api = new OpenAIClient(); -var request = new ImageGenerationRequest("A house riding a velociraptor", Models.Model.DallE_2); -var results = await api.ImagesEndPoint.GenerateImageAsync(request); +var request = new ImageGenerationRequest("A house riding a velociraptor", Models.Model.DallE_3); +var imageResults = await api.ImagesEndPoint.GenerateImageAsync(request); -foreach (var result in results) +foreach (var image in imageResults) { - Console.WriteLine(result); - // result == file://path/to/image.png or b64_string + Console.WriteLine(image); + // image == url or b64_string } ``` @@ -640,12 +1077,12 @@ Creates an edited or extended image given an original image and a prompt. ```csharp var api = new OpenAIClient(); var request = new ImageEditRequest(imageAssetPath, maskAssetPath, "A sunlit indoor lounge area with a pool containing a flamingo", size: ImageSize.Small); -var results = await api.ImagesEndPoint.CreateImageEditAsync(request); +var imageResults = await api.ImagesEndPoint.CreateImageEditAsync(request); -foreach (var result in results) +foreach (var image in imageResults) { - Console.WriteLine(result); - // result == file://path/to/image.png or b64_string + Console.WriteLine(image); + // image == url or b64_string } ``` @@ -656,12 +1093,12 @@ Creates a variation of a given image. ```csharp var api = new OpenAIClient(); var request = new ImageVariationRequest(imageAssetPath, size: ImageSize.Small); -var results = await api.ImagesEndPoint.CreateImageVariationAsync(request); +var imageResults = await api.ImagesEndPoint.CreateImageVariationAsync(request); -foreach (var result in results) +foreach (var image in imageResults) { - Console.WriteLine(result); - // result == file://path/to/image.png or b64_string + Console.WriteLine(image); + // image == url or b64_string } ``` @@ -677,22 +1114,24 @@ Returns a list of files that belong to the user's organization. ```csharp var api = new OpenAIClient(); -var files = await api.FilesEndpoint.ListFilesAsync(); +var fileList = await api.FilesEndpoint.ListFilesAsync(); -foreach (var file in files) +foreach (var file in fileList) { Console.WriteLine($"{file.Id} -> {file.Object}: {file.FileName} | {file.Size} bytes"); } ``` -#### [Upload File](https://platform.openai.com/docs/api-reference/files/upload) +#### [Upload File](https://platform.openai.com/docs/api-reference/files/create) + +Upload a file that can be used across various endpoints. The size of all the files uploaded by one organization can be up to 100 GB. -Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact us if you need to increase the storage limit. +The size of individual files can be a maximum of 512 MB. See the Assistants Tools guide to learn more about the types of files supported. The Fine-tuning API only supports .jsonl files. ```csharp var api = new OpenAIClient(); -var fileData = await api.FilesEndpoint.UploadFileAsync("path/to/your/file.jsonl", "fine-tune"); -Console.WriteLine(fileData.Id); +var file = await api.FilesEndpoint.UploadFileAsync("path/to/your/file.jsonl", "fine-tune"); +Console.WriteLine(file.Id); ``` #### [Delete File](https://platform.openai.com/docs/api-reference/files/delete) @@ -701,8 +1140,8 @@ Delete a file. ```csharp var api = new OpenAIClient(); -var result = await api.FilesEndpoint.DeleteFileAsync(fileData); -Assert.IsTrue(result); +var isDeleted = await api.FilesEndpoint.DeleteFileAsync(fileId); +Assert.IsTrue(isDeleted); ``` #### [Retrieve File Info](https://platform.openai.com/docs/api-reference/files/retrieve) @@ -711,13 +1150,13 @@ Returns information about a specific file. ```csharp var api = new OpenAIClient(); -var fileData = await GetFileInfoAsync(fileId); -Console.WriteLine($"{fileData.Id} -> {fileData.Object}: {fileData.FileName} | {fileData.Size} bytes"); +var file = await GetFileInfoAsync(fileId); +Console.WriteLine($"{file.Id} -> {file.Object}: {file.FileName} | {file.Size} bytes"); ``` #### [Download File Content](https://platform.openai.com/docs/api-reference/files/retrieve-content) -Downloads the specified file. +Downloads the file content to the specified directory. ```csharp var api = new OpenAIClient(); @@ -754,11 +1193,11 @@ List your organization's fine-tuning jobs. ```csharp var api = new OpenAIClient(); -var list = await api.FineTuningEndpoint.ListJobsAsync(); +var jobList = await api.FineTuningEndpoint.ListJobsAsync(); -foreach (var job in list.Jobs) +foreach (var job in jobList.Items.OrderByDescending(job => job.CreatedAt))) { - Console.WriteLine($"{job.Id} -> {job.Status}"); + Console.WriteLine($"{job.Id} -> {job.CreatedAt} | {job.Status}"); } ``` @@ -769,7 +1208,7 @@ Gets info about the fine-tune job. ```csharp var api = new OpenAIClient(); var job = await api.FineTuningEndpoint.GetJobInfoAsync(fineTuneJob); -Console.WriteLine($"{job.Id} -> {job.Status}"); +Console.WriteLine($"{job.Id} -> {job.CreatedAt} | {job.Status}"); ``` #### [Cancel Fine Tune Job](https://platform.openai.com/docs/api-reference/fine-tunes/cancel) @@ -778,8 +1217,8 @@ Immediately cancel a fine-tune job. ```csharp var api = new OpenAIClient(); -var result = await api.FineTuningEndpoint.CancelFineTuneJobAsync(fineTuneJob); -Assert.IsTrue(result); +var isCancelled = await api.FineTuningEndpoint.CancelFineTuneJobAsync(fineTuneJob); +Assert.IsTrue(isCancelled); ``` #### [List Fine Tune Job Events](https://platform.openai.com/docs/api-reference/fine-tuning/list-events) @@ -791,9 +1230,67 @@ var api = new OpenAIClient(); var eventList = await api.FineTuningEndpoint.ListJobEventsAsync(fineTuneJob); Console.WriteLine($"{fineTuneJob.Id} -> status: {fineTuneJob.Status} | event count: {eventList.Events.Count}"); -foreach (var @event in eventList.Events.OrderByDescending(@event => @event.CreatedAt)) +foreach (var @event in eventList.Items.OrderByDescending(@event => @event.CreatedAt)) +{ + Console.WriteLine($" {@event.CreatedAt} [{@event.Level}] {@event.Message}"); +} +``` + +### [Embeddings](https://platform.openai.com/docs/api-reference/embeddings) + +Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. + +Related guide: [Embeddings](https://platform.openai.com/docs/guides/embeddings) + +The Edits API is accessed via `OpenAIClient.EmbeddingsEndpoint` + +#### [Create Embeddings](https://platform.openai.com/docs/api-reference/embeddings/create) + +Creates an embedding vector representing the input text. + +```csharp +var api = new OpenAIClient(); +var response = await api.EmbeddingsEndpoint.CreateEmbeddingAsync("The food was delicious and the waiter...", Models.Embedding_Ada_002); +Console.WriteLine(response); +``` + +### [Completions](https://platform.openai.com/docs/api-reference/completions) + +Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. + +The Completions API is accessed via `OpenAIClient.CompletionsEndpoint` + +```csharp +var api = new OpenAIClient(); +var response = await api.CompletionsEndpoint.CreateCompletionAsync("One Two Three One Two", temperature: 0.1, model: Model.Davinci); +Console.WriteLine(response); +``` + +> To get the `CompletionResponse` (which is mostly metadata), use its implicit string operator to get the text if all you want is the completion choice. + +#### Completion Streaming + +Streaming allows you to get results are they are generated, which can help your application feel more responsive, especially on slow models like Davinci. + +```csharp +var api = new OpenAIClient(); + +await api.CompletionsEndpoint.StreamCompletionAsync(response => +{ + foreach (var choice in response.Completions) + { + Console.WriteLine(choice); + } +}, "My name is Roger and I am a principal software engineer at Salesforce. This is my resume:", maxTokens: 200, temperature: 0.5, presencePenalty: 0.1, frequencyPenalty: 0.1, model: Model.Davinci); +``` + +Or if using [`IAsyncEnumerable{T}`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-5.0) ([C# 8.0+](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8)) + +```csharp +var api = new OpenAIClient(); +await foreach (var partialResponse in api.CompletionsEndpoint.StreamCompletionEnumerableAsync("My name is Roger and I am a principal software engineer at Salesforce. This is my resume:", maxTokens: 200, temperature: 0.5, presencePenalty: 0.1, frequencyPenalty: 0.1, model: Model.Davinci)) { - Console.WriteLine($" {@event.CreatedAt} [{@event.Level}] {@event.Message.Replace("\n", " ")}"); + Console.WriteLine(partialResponse); } ``` @@ -811,14 +1308,41 @@ Classifies if text violates OpenAI's Content Policy. ```csharp var api = new OpenAIClient(); -var response = await api.ModerationsEndpoint.GetModerationAsync("I want to kill them."); -Assert.IsTrue(response); +var isViolation = await api.ModerationsEndpoint.GetModerationAsync("I want to kill them."); +Assert.IsTrue(isViolation); +``` + +Additionally you can also get the scores of a given input. + +```csharp +var response = await OpenAIClient.ModerationsEndpoint.CreateModerationAsync(new ModerationsRequest("I love you")); +Assert.IsNotNull(response); +Console.WriteLine(response.Results?[0]?.Scores?.ToString()); ``` --- +### [Edits](https://platform.openai.com/docs/api-reference/edits) + +> Deprecated, and soon to be removed. + +Given a prompt and an instruction, the model will return an edited version of the prompt. + +The Edits API is accessed via `OpenAIClient.EditsEndpoint` + +#### [Create Edit](https://platform.openai.com/docs/api-reference/edits/create) + +Creates a new edit for the provided input, instruction, and parameters using the provided input and instruction. + +```csharp +var api = new OpenAIClient(); +var request = new EditRequest("What day of the wek is it?", "Fix the spelling mistakes"); +var response = await api.EditsEndpoint.CreateEditAsync(request); +Console.WriteLine(response); +``` + ## License ![CC-0 Public Domain](https://licensebuttons.net/p/zero/1.0/88x31.png) -This library is licensed CC-0, in the public domain. You can use it for whatever you want, publicly or privately, without worrying about permission or licensing or whatever. It's just a wrapper around the OpenAI API, so you still need to get access to OpenAI from them directly. I am not affiliated with OpenAI and this library is not endorsed by them, I just have beta access and wanted to make a C# library to access it more easily. Hopefully others find this useful as well. Feel free to open a PR if there's anything you want to contribute. \ No newline at end of file +This library is licensed CC-0, in the public domain. You can use it for whatever you want, publicly or privately, without worrying about permission or licensing or whatever. It's just a wrapper around the OpenAI API, so you still need to get access to OpenAI from them directly. I am not affiliated with OpenAI and this library is not endorsed by them, I just have beta access and wanted to make a C# library to access it more easily. Hopefully others find this useful as well. Feel free to open a PR if there's anything you want to contribute.