diff --git a/OpenAI.sln b/OpenAI.sln index d6350d85f..a69fd88d4 100644 --- a/OpenAI.sln +++ b/OpenAI.sln @@ -6,7 +6,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI", "src\OpenAI.csproj EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Examples", "examples\OpenAI.Examples.csproj", "{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Tests", "tests\OpenAI.Tests.csproj", "{6F156401-2544-41D7-B204-3148C51C1D09}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Tests", "tests\OpenAI.Tests.csproj", "{6F156401-2544-41D7-B204-3148C51C1D09}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ClientModel", "..\azure-sdk-for-net\sdk\core\System.ClientModel\src\System.ClientModel.csproj", "{8CDFA283-1045-45D3-B86A-56DE301B331B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,6 +28,10 @@ Global {6F156401-2544-41D7-B204-3148C51C1D09}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F156401-2544-41D7-B204-3148C51C1D09}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F156401-2544-41D7-B204-3148C51C1D09}.Release|Any CPU.Build.0 = Release|Any CPU + {8CDFA283-1045-45D3-B86A-56DE301B331B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CDFA283-1045-45D3-B86A-56DE301B331B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CDFA283-1045-45D3-B86A-56DE301B331B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CDFA283-1045-45D3-B86A-56DE301B331B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -33,4 +39,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A97F4B90-2591-4689-B1F8-5F21FE6D6CAE} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/README.md b/README.md index 017165071..88e4b572c 100644 --- a/README.md +++ b/README.md @@ -115,11 +115,11 @@ When you request a chat completion, the default behavior is for the server to ge The client library offers a convenient approach to working with streaming chat completions. If you wanted to re-write the example from the previous section using streaming, rather than calling the `ChatClient`'s `CompleteChat` method, you would call its `CompleteChatStreaming` method instead: ```csharp -ResultCollection updates +CollectionResult updates = client.CompleteChatStreaming("Say 'this is a test.'"); ``` -Notice that the returned value is a `ResultCollection` instance, which can be enumerated to process the streaming response chunks as they arrive: +Notice that the returned value is a `CollectionResult` instance, which can be enumerated to process the streaming response chunks as they arrive: ```csharp Console.WriteLine($"[ASSISTANT]:"); @@ -132,10 +132,10 @@ foreach (StreamingChatCompletionUpdate update in updates) } ``` -Alternatively, you can do this asynchronously by calling the `CompleteChatStreamingAsync` method to get an `AsyncResultCollection` and enumerate it using `await foreach`: +Alternatively, you can do this asynchronously by calling the `CompleteChatStreamingAsync` method to get an `AsyncCollectionResult` and enumerate it using `await foreach`: ```csharp -AsyncResultCollection updates +AsyncCollectionResult updates = client.CompleteChatStreamingAsync("Say 'this is a test.'"); Console.WriteLine($"[ASSISTANT]:"); @@ -528,7 +528,7 @@ Finally, you can use the `AssistantClient`'s `GetMessages` method to retrieve th For illustrative purposes, you could print the messages to the console and also save any images produced by the assistant to local storage: ```csharp -PageableCollection messages = assistantClient.GetMessages(threadRun.ThreadId, ListOrder.OldestFirst); +PageCollection messages = assistantClient.GetMessages(threadRun.ThreadId, ListOrder.OldestFirst); foreach (ThreadMessage message in messages) { @@ -640,10 +640,10 @@ AssistantThread thread = assistantClient.CreateThread(new ThreadCreationOptions( }); ``` -With the assistant and thread prepared, use the `CreateRunStreaming` method to get an enumerable `ResultCollection`. You can then iterate over this collection with `foreach`. For async calling patterns, use `CreateRunStreamingAsync` and iterate over the `AsyncResultCollection` with `await foreach`, instead. Note that streaming variants also exist for `CreateThreadAndRunStreaming` and `SubmitToolOutputsToRunStreaming`. +With the assistant and thread prepared, use the `CreateRunStreaming` method to get an enumerable `CollectionResult`. You can then iterate over this collection with `foreach`. For async calling patterns, use `CreateRunStreamingAsync` and iterate over the `AsyncCollectionResult` with `await foreach`, instead. Note that streaming variants also exist for `CreateThreadAndRunStreaming` and `SubmitToolOutputsToRunStreaming`. ```csharp -ResultCollection streamingUpdates = assistantClient.CreateRunStreaming( +CollectionResult streamingUpdates = assistantClient.CreateRunStreaming( thread, assistant, new RunCreationOptions() diff --git a/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs b/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs index fb9189ac9..f4a1afde7 100644 --- a/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs +++ b/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs @@ -1,149 +1,144 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.IO; -using System.Threading; +//using NUnit.Framework; +//using OpenAI.Assistants; +//using OpenAI.Files; +//using System; +//using System.ClientModel; +//using System.Collections.Generic; +//using System.IO; +//using System.Threading; -namespace OpenAI.Examples; +//namespace OpenAI.Examples; -public partial class AssistantExamples -{ - [Test] - public void Example01_RetrievalAugmentedGeneration() - { - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - FileClient fileClient = openAIClient.GetFileClient(); - AssistantClient assistantClient = openAIClient.GetAssistantClient(); +//public partial class AssistantExamples +//{ +// [Test] +// public void Example01_RetrievalAugmentedGeneration() +// { +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); +// FileClient fileClient = openAIClient.GetFileClient(); +// AssistantClient assistantClient = openAIClient.GetAssistantClient(); - // First, let's contrive a document we'll use retrieval with and upload it. - using Stream document = BinaryData.FromString(""" - { - "description": "This document contains the sale history data for Contoso products.", - "sales": [ - { - "month": "January", - "by_product": { - "113043": 15, - "113045": 12, - "113049": 2 - } - }, - { - "month": "February", - "by_product": { - "113045": 22 - } - }, - { - "month": "March", - "by_product": { - "113045": 16, - "113055": 5 - } - } - ] - } - """).ToStream(); +// // First, let's contrive a document we'll use retrieval with and upload it. +// using Stream document = BinaryData.FromString(""" +// { +// "description": "This document contains the sale history data for Contoso products.", +// "sales": [ +// { +// "month": "January", +// "by_product": { +// "113043": 15, +// "113045": 12, +// "113049": 2 +// } +// }, +// { +// "month": "February", +// "by_product": { +// "113045": 22 +// } +// }, +// { +// "month": "March", +// "by_product": { +// "113045": 16, +// "113055": 5 +// } +// } +// ] +// } +// """).ToStream(); - OpenAIFileInfo salesFile = fileClient.UploadFile( - document, - "monthly_sales.json", - FileUploadPurpose.Assistants); +// OpenAIFileInfo salesFile = fileClient.UploadFile( +// document, +// "monthly_sales.json", +// FileUploadPurpose.Assistants); - // Now, we'll create a client intended to help with that data - AssistantCreationOptions assistantOptions = new() - { - Name = "Example: Contoso sales RAG", - Instructions = - "You are an assistant that looks up sales data and helps visualize the information based" - + " on user queries. When asked to generate a graph, chart, or other visualization, use" - + " the code interpreter tool to do so.", - Tools = - { - new FileSearchToolDefinition(), - new CodeInterpreterToolDefinition(), - }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([salesFile.Id]), - } - } - }, - }; +// // Now, we'll create a client intended to help with that data +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Example: Contoso sales RAG", +// Instructions = +// "You are an assistant that looks up sales data and helps visualize the information based" +// + " on user queries. When asked to generate a graph, chart, or other visualization, use" +// + " the code interpreter tool to do so.", +// Tools = +// { +// new FileSearchToolDefinition(), +// new CodeInterpreterToolDefinition(), +// }, +// ToolResources = new() +// { +// FileSearch = new() +// { +// NewVectorStores = +// { +// new VectorStoreCreationHelper([salesFile.Id]), +// } +// } +// }, +// }; - Assistant assistant = assistantClient.CreateAssistant("gpt-4o", assistantOptions); +// Assistant assistant = assistantClient.CreateAssistant("gpt-4o", assistantOptions); - // Now we'll create a thread with a user query about the data already associated with the assistant, then run it - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { "How well did product 113045 sell in February? Graph its trend over time." } - }; +// // Now we'll create a thread with a user query about the data already associated with the assistant, then run it +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { "How well did product 113045 sell in February? Graph its trend over time." } +// }; - ThreadRun threadRun = assistantClient.CreateThreadAndRun(assistant.Id, threadOptions); +// // Passing ReturnWhen.Completed means CreateThreadAndRun will return control after the run is complete. +// ThreadRunOperation runOperation = assistantClient.CreateThreadAndRun(ReturnWhen.Completed, assistant.Id, threadOptions); - // Check back to see when the run is done - do - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - threadRun = assistantClient.GetRun(threadRun.ThreadId, threadRun.Id); - } while (!threadRun.Status.IsTerminal); +// // Finally, we'll print out the full history for the thread that includes the augmented generation +// PageCollection messagePages +// = assistantClient.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); +// IEnumerable messages = messagePages.GetAllValues(); - // Finally, we'll print out the full history for the thread that includes the augmented generation - PageableCollection messages - = assistantClient.GetMessages(threadRun.ThreadId, ListOrder.OldestFirst); +// foreach (ThreadMessage message in messages) +// { +// Console.Write($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// if (!string.IsNullOrEmpty(contentItem.Text)) +// { +// Console.WriteLine($"{contentItem.Text}"); - foreach (ThreadMessage message in messages) - { - Console.Write($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - if (!string.IsNullOrEmpty(contentItem.Text)) - { - Console.WriteLine($"{contentItem.Text}"); +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// } - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - } +// // Include annotations, if any. +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// if (!string.IsNullOrEmpty(annotation.InputFileId)) +// { +// Console.WriteLine($"* File citation, file ID: {annotation.InputFileId}"); +// } +// if (!string.IsNullOrEmpty(annotation.OutputFileId)) +// { +// Console.WriteLine($"* File output, new file ID: {annotation.OutputFileId}"); +// } +// } +// } +// if (!string.IsNullOrEmpty(contentItem.ImageFileId)) +// { +// OpenAIFileInfo imageInfo = fileClient.GetFile(contentItem.ImageFileId); +// BinaryData imageBytes = fileClient.DownloadFile(contentItem.ImageFileId); +// using FileStream stream = File.OpenWrite($"{imageInfo.Filename}.png"); +// imageBytes.ToStream().CopyTo(stream); - // Include annotations, if any. - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - if (!string.IsNullOrEmpty(annotation.InputFileId)) - { - Console.WriteLine($"* File citation, file ID: {annotation.InputFileId}"); - } - if (!string.IsNullOrEmpty(annotation.OutputFileId)) - { - Console.WriteLine($"* File output, new file ID: {annotation.OutputFileId}"); - } - } - } - if (!string.IsNullOrEmpty(contentItem.ImageFileId)) - { - OpenAIFileInfo imageInfo = fileClient.GetFile(contentItem.ImageFileId); - BinaryData imageBytes = fileClient.DownloadFile(contentItem.ImageFileId); - using FileStream stream = File.OpenWrite($"{imageInfo.Filename}.png"); - imageBytes.ToStream().CopyTo(stream); +// Console.WriteLine($""); +// } +// } +// Console.WriteLine(); +// } - Console.WriteLine($""); - } - } - Console.WriteLine(); - } - - // Optionally, delete any persistent resources you no longer need. - _ = assistantClient.DeleteThread(threadRun.ThreadId); - _ = assistantClient.DeleteAssistant(assistant); - _ = fileClient.DeleteFile(salesFile); - } -} +// // Optionally, delete any persistent resources you no longer need. +// _ = assistantClient.DeleteThread(runOperation.ThreadId); +// _ = assistantClient.DeleteAssistant(assistant); +// _ = fileClient.DeleteFile(salesFile); +// } +//} diff --git a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs index 6cde83f33..f44725355 100644 --- a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs +++ b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs @@ -1,150 +1,144 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +//using NUnit.Framework; +//using OpenAI.Assistants; +//using OpenAI.Files; +//using System; +//using System.ClientModel; +//using System.Collections.Generic; +//using System.IO; +//using System.Threading; +//using System.Threading.Tasks; -namespace OpenAI.Examples; +//namespace OpenAI.Examples; -public partial class AssistantExamples -{ - [Test] - public async Task Example01_RetrievalAugmentedGenerationAsync() - { - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - FileClient fileClient = openAIClient.GetFileClient(); - AssistantClient assistantClient = openAIClient.GetAssistantClient(); +//public partial class AssistantExamples +//{ +// [Test] +// public async Task Example01_RetrievalAugmentedGenerationAsync() +// { +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); +// FileClient fileClient = openAIClient.GetFileClient(); +// AssistantClient assistantClient = openAIClient.GetAssistantClient(); - // First, let's contrive a document we'll use retrieval with and upload it. - using Stream document = BinaryData.FromString(""" - { - "description": "This document contains the sale history data for Contoso products.", - "sales": [ - { - "month": "January", - "by_product": { - "113043": 15, - "113045": 12, - "113049": 2 - } - }, - { - "month": "February", - "by_product": { - "113045": 22 - } - }, - { - "month": "March", - "by_product": { - "113045": 16, - "113055": 5 - } - } - ] - } - """).ToStream(); +// // First, let's contrive a document we'll use retrieval with and upload it. +// using Stream document = BinaryData.FromString(""" +// { +// "description": "This document contains the sale history data for Contoso products.", +// "sales": [ +// { +// "month": "January", +// "by_product": { +// "113043": 15, +// "113045": 12, +// "113049": 2 +// } +// }, +// { +// "month": "February", +// "by_product": { +// "113045": 22 +// } +// }, +// { +// "month": "March", +// "by_product": { +// "113045": 16, +// "113055": 5 +// } +// } +// ] +// } +// """).ToStream(); - OpenAIFileInfo salesFile = await fileClient.UploadFileAsync( - document, - "monthly_sales.json", - FileUploadPurpose.Assistants); +// OpenAIFileInfo salesFile = await fileClient.UploadFileAsync( +// document, +// "monthly_sales.json", +// FileUploadPurpose.Assistants); - // Now, we'll create a client intended to help with that data - AssistantCreationOptions assistantOptions = new() - { - Name = "Example: Contoso sales RAG", - Instructions = - "You are an assistant that looks up sales data and helps visualize the information based" - + " on user queries. When asked to generate a graph, chart, or other visualization, use" - + " the code interpreter tool to do so.", - Tools = - { - new FileSearchToolDefinition(), - new CodeInterpreterToolDefinition(), - }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([salesFile.Id]), - } - } - }, - }; +// // Now, we'll create a client intended to help with that data +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Example: Contoso sales RAG", +// Instructions = +// "You are an assistant that looks up sales data and helps visualize the information based" +// + " on user queries. When asked to generate a graph, chart, or other visualization, use" +// + " the code interpreter tool to do so.", +// Tools = +// { +// new FileSearchToolDefinition(), +// new CodeInterpreterToolDefinition(), +// }, +// ToolResources = new() +// { +// FileSearch = new() +// { +// NewVectorStores = +// { +// new VectorStoreCreationHelper([salesFile.Id]), +// } +// } +// }, +// }; - Assistant assistant = await assistantClient.CreateAssistantAsync("gpt-4o", assistantOptions); +// Assistant assistant = await assistantClient.CreateAssistantAsync("gpt-4o", assistantOptions); - // Now we'll create a thread with a user query about the data already associated with the assistant, then run it - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { "How well did product 113045 sell in February? Graph its trend over time." } - }; +// // Now we'll create a thread with a user query about the data already associated with the assistant, then run it +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { "How well did product 113045 sell in February? Graph its trend over time." } +// }; - ThreadRun threadRun = await assistantClient.CreateThreadAndRunAsync(assistant.Id, threadOptions); +// ThreadRunOperation runOperation = await assistantClient.CreateThreadAndRunAsync(ReturnWhen.Completed, assistant.Id, threadOptions); - // Check back to see when the run is done - do - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - threadRun = assistantClient.GetRun(threadRun.ThreadId, threadRun.Id); - } while (!threadRun.Status.IsTerminal); +// // Finally, we'll print out the full history for the thread that includes the augmented generation +// AsyncPageCollection messagePages +// = assistantClient.GetMessagesAsync(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); +// IAsyncEnumerable messages = messagePages.GetAllValuesAsync(); - // Finally, we'll print out the full history for the thread that includes the augmented generation - AsyncPageableCollection messages - = assistantClient.GetMessagesAsync(threadRun.ThreadId, ListOrder.OldestFirst); +// await foreach (ThreadMessage message in messages) +// { +// Console.Write($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// if (!string.IsNullOrEmpty(contentItem.Text)) +// { +// Console.WriteLine($"{contentItem.Text}"); - await foreach (ThreadMessage message in messages) - { - Console.Write($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - if (!string.IsNullOrEmpty(contentItem.Text)) - { - Console.WriteLine($"{contentItem.Text}"); +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// } - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - } +// // Include annotations, if any. +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// if (!string.IsNullOrEmpty(annotation.InputFileId)) +// { +// Console.WriteLine($"* File citation, file ID: {annotation.InputFileId}"); +// } +// if (!string.IsNullOrEmpty(annotation.OutputFileId)) +// { +// Console.WriteLine($"* File output, new file ID: {annotation.OutputFileId}"); +// } +// } +// } +// if (!string.IsNullOrEmpty(contentItem.ImageFileId)) +// { +// OpenAIFileInfo imageInfo = await fileClient.GetFileAsync(contentItem.ImageFileId); +// BinaryData imageBytes = await fileClient.DownloadFileAsync(contentItem.ImageFileId); +// using FileStream stream = File.OpenWrite($"{imageInfo.Filename}.png"); +// imageBytes.ToStream().CopyTo(stream); - // Include annotations, if any. - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - if (!string.IsNullOrEmpty(annotation.InputFileId)) - { - Console.WriteLine($"* File citation, file ID: {annotation.InputFileId}"); - } - if (!string.IsNullOrEmpty(annotation.OutputFileId)) - { - Console.WriteLine($"* File output, new file ID: {annotation.OutputFileId}"); - } - } - } - if (!string.IsNullOrEmpty(contentItem.ImageFileId)) - { - OpenAIFileInfo imageInfo = await fileClient.GetFileAsync(contentItem.ImageFileId); - BinaryData imageBytes = await fileClient.DownloadFileAsync(contentItem.ImageFileId); - using FileStream stream = File.OpenWrite($"{imageInfo.Filename}.png"); - imageBytes.ToStream().CopyTo(stream); +// Console.WriteLine($""); +// } +// } +// Console.WriteLine(); +// } - Console.WriteLine($""); - } - } - Console.WriteLine(); - } - - // Optionally, delete any persistent resources you no longer need. - _ = await assistantClient.DeleteThreadAsync(threadRun.ThreadId); - _ = await assistantClient.DeleteAssistantAsync(assistant); - _ = await fileClient.DeleteFileAsync(salesFile); - } -} +// // Optionally, delete any persistent resources you no longer need. +// _ = await assistantClient.DeleteThreadAsync(runOperation.ThreadId); +// _ = await assistantClient.DeleteAssistantAsync(assistant); +// _ = await fileClient.DeleteFileAsync(salesFile); +// } +//} diff --git a/examples/Assistants/Example02_FunctionCalling.cs b/examples/Assistants/Example02_FunctionCalling.cs index a60491379..5ec562f37 100644 --- a/examples/Assistants/Example02_FunctionCalling.cs +++ b/examples/Assistants/Example02_FunctionCalling.cs @@ -1,192 +1,198 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; - -namespace OpenAI.Examples; - -public partial class AssistantExamples -{ - [Test] - public void Example02_FunctionCalling() - { - #region - string GetCurrentLocation() - { - // Call a location API here. - return "San Francisco"; - } - - const string GetCurrentLocationFunctionName = "get_current_location"; - - FunctionToolDefinition getLocationTool = new() - { - FunctionName = GetCurrentLocationFunctionName, - Description = "Get the user's current location" - }; - - string GetCurrentWeather(string location, string unit = "celsius") - { - // Call a weather API here. - return $"31 {unit}"; - } - - const string GetCurrentWeatherFunctionName = "get_current_weather"; - - FunctionToolDefinition getWeatherTool = new() - { - FunctionName = GetCurrentWeatherFunctionName, - Description = "Get the current weather in a given location", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. Boston, MA" - }, - "unit": { - "type": "string", - "enum": [ "celsius", "fahrenheit" ], - "description": "The temperature unit to use. Infer this from the specified location." - } - }, - "required": [ "location" ] - } - """), - }; - #endregion - - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - - #region - // Create an assistant that can call the function tools. - AssistantCreationOptions assistantOptions = new() - { - Name = "Example: Function Calling", - Instructions = - "Don't make assumptions about what values to plug into functions." - + " Ask for clarification if a user request is ambiguous.", - Tools = { getLocationTool, getWeatherTool }, - }; - - Assistant assistant = client.CreateAssistant("gpt-4-turbo", assistantOptions); - #endregion - - #region - // Create a thread with an initial user message and run it. - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { "What's the weather like today?" } - }; - - ThreadRun run = client.CreateThreadAndRun(assistant.Id, threadOptions); - #endregion - - #region - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - run = client.GetRun(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case GetCurrentLocationFunctionName: - { - string toolResult = GetCurrentLocation(); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - case GetCurrentWeatherFunctionName: - { - // The arguments that the model wants to use to call the function are specified as a - // stringified JSON object based on the schema defined in the tool definition. Note that - // the model may hallucinate arguments too. Consequently, it is important to do the - // appropriate parsing and validation before calling the function. - using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); - bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); - bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); - - if (!hasLocation) - { - throw new ArgumentNullException(nameof(location), "The location argument is required."); - } - - string toolResult = hasUnit - ? GetCurrentWeather(location.GetString(), unit.GetString()) - : GetCurrentWeather(location.GetString()); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - PageableCollection messages - = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - } -} +//using NUnit.Framework; +//using OpenAI.Assistants; +//using System; +//using System.ClientModel; +//using System.Collections.Generic; +//using System.Text.Json; +//using System.Threading; + +//namespace OpenAI.Examples; + +//public partial class AssistantExamples +//{ +// [Test] +// public void Example02_FunctionCalling() +// { +// #region Define Functions +// string GetCurrentLocation() +// { +// // Call a location API here. +// return "San Francisco"; +// } + +// const string GetCurrentLocationFunctionName = "get_current_location"; + +// FunctionToolDefinition getLocationTool = new() +// { +// FunctionName = GetCurrentLocationFunctionName, +// Description = "Get the user's current location" +// }; + +// string GetCurrentWeather(string location, string unit = "celsius") +// { +// // Call a weather API here. +// return $"31 {unit}"; +// } + +// const string GetCurrentWeatherFunctionName = "get_current_weather"; + +// FunctionToolDefinition getWeatherTool = new() +// { +// FunctionName = GetCurrentWeatherFunctionName, +// Description = "Get the current weather in a given location", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g. Boston, MA" +// }, +// "unit": { +// "type": "string", +// "enum": [ "celsius", "fahrenheit" ], +// "description": "The temperature unit to use. Infer this from the specified location." +// } +// }, +// "required": [ "location" ] +// } +// """), +// }; +// #endregion + +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); + +// #region Create Assistant +// // Create an assistant that can call the function tools. +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Example: Function Calling", +// Instructions = +// "Don't make assumptions about what values to plug into functions." +// + " Ask for clarification if a user request is ambiguous.", +// Tools = { getLocationTool, getWeatherTool }, +// }; + +// Assistant assistant = client.CreateAssistant("gpt-4-turbo", assistantOptions); +// #endregion + +// #region Create Thread and Run +// // Create a thread with an initial user message and run it. +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { "What's the weather like today?" } +// }; + +// ThreadRunOperation runOperation = client.CreateThreadAndRun(ReturnWhen.Started, assistant.Id, threadOptions); +// #endregion + +// #region Submit tool outputs to run + +// // Wait for status changes in order to detect if operation requires input. +// do +// { +// runOperation.WaitForStatusChange(); + +// // If the run requires action, resolve them. +// if (runOperation.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// // TODO: Maybe improve API around this? + +// foreach (RequiredAction action in runOperation.Value.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case GetCurrentLocationFunctionName: +// { +// string toolResult = GetCurrentLocation(); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// case GetCurrentWeatherFunctionName: +// { +// // The arguments that the model wants to use to call the function are specified as a +// // stringified JSON object based on the schema defined in the tool definition. Note that +// // the model may hallucinate arguments too. Consequently, it is important to do the +// // appropriate parsing and validation before calling the function. +// using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); +// bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); +// bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); + +// if (!hasLocation) +// { +// throw new ArgumentNullException(nameof(location), "The location argument is required."); +// } + +// string toolResult = hasUnit +// ? GetCurrentWeather(location.GetString(), unit.GetString()) +// : GetCurrentWeather(location.GetString()); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// runOperation.SubmitToolOutputsToRun(toolOutputs); +// } +// } +// while (!runOperation.HasCompleted); + +// #endregion + +// #region Get and display messages + +// // If the run completed successfully, list the messages and display their content +// if (runOperation.Status == RunStatus.Completed) +// { +// PageCollection messagePages +// = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); +// IEnumerable messages = messagePages.GetAllValues(); + +// foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// } +// else +// { +// throw new NotImplementedException(runOperation.Status.ToString()); +// } +// #endregion +// } +//} diff --git a/examples/Assistants/Example02_FunctionCallingAsync.cs b/examples/Assistants/Example02_FunctionCallingAsync.cs index 08bbd0bde..4a1072b62 100644 --- a/examples/Assistants/Example02_FunctionCallingAsync.cs +++ b/examples/Assistants/Example02_FunctionCallingAsync.cs @@ -1,192 +1,192 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading.Tasks; - -namespace OpenAI.Examples; - -public partial class AssistantExamples -{ - [Test] - public async Task Example02_FunctionCallingAsync() - { - #region - string GetCurrentLocation() - { - // Call a location API here. - return "San Francisco"; - } - - const string GetCurrentLocationFunctionName = "get_current_location"; - - FunctionToolDefinition getLocationTool = new() - { - FunctionName = GetCurrentLocationFunctionName, - Description = "Get the user's current location" - }; - - string GetCurrentWeather(string location, string unit = "celsius") - { - // Call a weather API here. - return $"31 {unit}"; - } - - const string GetCurrentWeatherFunctionName = "get_current_weather"; - - FunctionToolDefinition getWeatherTool = new() - { - FunctionName = GetCurrentWeatherFunctionName, - Description = "Get the current weather in a given location", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. Boston, MA" - }, - "unit": { - "type": "string", - "enum": [ "celsius", "fahrenheit" ], - "description": "The temperature unit to use. Infer this from the specified location." - } - }, - "required": [ "location" ] - } - """), - }; - #endregion - - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - - #region - // Create an assistant that can call the function tools. - AssistantCreationOptions assistantOptions = new() - { - Name = "Example: Function Calling", - Instructions = - "Don't make assumptions about what values to plug into functions." - + " Ask for clarification if a user request is ambiguous.", - Tools = { getLocationTool, getWeatherTool }, - }; - - Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); - #endregion - - #region - // Create a thread with an initial user message and run it. - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { "What's the weather like today?" } - }; - - ThreadRun run = await client.CreateThreadAndRunAsync(assistant.Id, threadOptions); - #endregion - - #region - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - await Task.Delay(TimeSpan.FromSeconds(1)); - run = await client.GetRunAsync(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case GetCurrentLocationFunctionName: - { - string toolResult = GetCurrentLocation(); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - case GetCurrentWeatherFunctionName: - { - // The arguments that the model wants to use to call the function are specified as a - // stringified JSON object based on the schema defined in the tool definition. Note that - // the model may hallucinate arguments too. Consequently, it is important to do the - // appropriate parsing and validation before calling the function. - using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); - bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); - bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); - - if (!hasLocation) - { - throw new ArgumentNullException(nameof(location), "The location argument is required."); - } - - string toolResult = hasUnit - ? GetCurrentWeather(location.GetString(), unit.GetString()) - : GetCurrentWeather(location.GetString()); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = await client.SubmitToolOutputsToRunAsync(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - AsyncPageableCollection messages - = client.GetMessagesAsync(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - await foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - } -} +//using NUnit.Framework; +//using OpenAI.Assistants; +//using System; +//using System.ClientModel; +//using System.Collections.Generic; +//using System.Text.Json; +//using System.Threading.Tasks; + +//namespace OpenAI.Examples; + +//public partial class AssistantExamples +//{ +// [Test] +// public async Task Example02_FunctionCallingAsync() +// { +// #region +// string GetCurrentLocation() +// { +// // Call a location API here. +// return "San Francisco"; +// } + +// const string GetCurrentLocationFunctionName = "get_current_location"; + +// FunctionToolDefinition getLocationTool = new() +// { +// FunctionName = GetCurrentLocationFunctionName, +// Description = "Get the user's current location" +// }; + +// string GetCurrentWeather(string location, string unit = "celsius") +// { +// // Call a weather API here. +// return $"31 {unit}"; +// } + +// const string GetCurrentWeatherFunctionName = "get_current_weather"; + +// FunctionToolDefinition getWeatherTool = new() +// { +// FunctionName = GetCurrentWeatherFunctionName, +// Description = "Get the current weather in a given location", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g. Boston, MA" +// }, +// "unit": { +// "type": "string", +// "enum": [ "celsius", "fahrenheit" ], +// "description": "The temperature unit to use. Infer this from the specified location." +// } +// }, +// "required": [ "location" ] +// } +// """), +// }; +// #endregion + +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); + +// #region +// // Create an assistant that can call the function tools. +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Example: Function Calling", +// Instructions = +// "Don't make assumptions about what values to plug into functions." +// + " Ask for clarification if a user request is ambiguous.", +// Tools = { getLocationTool, getWeatherTool }, +// }; + +// Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); +// #endregion + +// #region +// // Create a thread with an initial user message and run it. +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { "What's the weather like today?" } +// }; + +// ThreadRunOperation runOperation = await client.CreateThreadAndRunAsync(ReturnWhen.Started, assistant.Id, threadOptions); +// #endregion + +// #region +// // Poll the run until it is no longer queued or in progress. +// while (!runOperation.HasCompleted) +// { +// await Task.Delay(TimeSpan.FromSeconds(1)); + +// // If the run requires action, resolve them. +// if (runOperation.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// foreach (RequiredAction action in runOperation.Value.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case GetCurrentLocationFunctionName: +// { +// string toolResult = GetCurrentLocation(); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// case GetCurrentWeatherFunctionName: +// { +// // The arguments that the model wants to use to call the function are specified as a +// // stringified JSON object based on the schema defined in the tool definition. Note that +// // the model may hallucinate arguments too. Consequently, it is important to do the +// // appropriate parsing and validation before calling the function. +// using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); +// bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); +// bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); + +// if (!hasLocation) +// { +// throw new ArgumentNullException(nameof(location), "The location argument is required."); +// } + +// string toolResult = hasUnit +// ? GetCurrentWeather(location.GetString(), unit.GetString()) +// : GetCurrentWeather(location.GetString()); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// await runOperation.SubmitToolOutputsToRunAsync(toolOutputs); +// } +// } +// #endregion + +// #region +// // With the run complete, list the messages and display their content +// if (runOperation.Status == RunStatus.Completed) +// { +// AsyncPageCollection messagePages +// = client.GetMessagesAsync(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); +// IAsyncEnumerable messages = messagePages.GetAllValuesAsync(); + +// await foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// } +// else +// { +// throw new NotImplementedException(runOperation.Status.ToString()); +// } +// #endregion +// } +//} diff --git a/examples/Assistants/Example02b_FunctionCallingStreaming.cs b/examples/Assistants/Example02b_FunctionCallingStreaming.cs index cb3bf7343..08f79450f 100644 --- a/examples/Assistants/Example02b_FunctionCallingStreaming.cs +++ b/examples/Assistants/Example02b_FunctionCallingStreaming.cs @@ -1,139 +1,152 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using System; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace OpenAI.Examples; - -public partial class AssistantExamples -{ - [Test] - public async Task Example02b_FunctionCallingStreaming() - { - // This example parallels the content at the following location: - // https://platform.openai.com/docs/assistants/tools/function-calling/function-calling-beta - #region Step 1 - Define Functions - - // First, define the functions that the assistant will use in its defined tools. - - FunctionToolDefinition getTemperatureTool = new() - { - FunctionName = "get_current_temperature", - Description = "Gets the current temperature at a specific location.", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g., San Francisco, CA" - }, - "unit": { - "type": "string", - "enum": ["Celsius", "Fahrenheit"], - "description": "The temperature unit to use. Infer this from the user's location." - } - } - } - """), - }; - - FunctionToolDefinition getRainProbabilityTool = new() - { - FunctionName = "get_current_rain_probability", - Description = "Gets the current forecasted probability of rain at a specific location," - + " represented as a percent chance in the range of 0 to 100.", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g., San Francisco, CA" - } - }, - "required": ["location"] - } - """), - }; - - #endregion - - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - - #region Create a new assistant with function tools - // Create an assistant that can call the function tools. - AssistantCreationOptions assistantOptions = new() - { - Name = "Example: Function Calling", - Instructions = - "Don't make assumptions about what values to plug into functions." - + " Ask for clarification if a user request is ambiguous.", - Tools = { getTemperatureTool, getRainProbabilityTool }, - }; - - Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); - #endregion - - #region Step 2 - Create a thread and add messages - AssistantThread thread = await client.CreateThreadAsync(); - ThreadMessage message = await client.CreateMessageAsync( - thread, - MessageRole.User, - [ - "What's the weather in San Francisco today and the likelihood it'll rain?" - ]); - #endregion - - #region Step 3 - Initiate a streaming run - // TODO: replace this with finalized enumerable result pattern - AsyncResultCollection asyncUpdates - = client.CreateRunStreamingAsync(thread, assistant); - - ThreadRun currentRun = null; - do - { - currentRun = null; - List outputsToSubmit = []; - await foreach (StreamingUpdate update in asyncUpdates) - { - if (update is RunUpdate runUpdate) - { - currentRun = runUpdate; - } - else if (update is RequiredActionUpdate requiredActionUpdate) - { - if (requiredActionUpdate.FunctionName == getTemperatureTool.FunctionName) - { - outputsToSubmit.Add(new ToolOutput(requiredActionUpdate.ToolCallId, "57")); - } - else if (requiredActionUpdate.FunctionName == getRainProbabilityTool.FunctionName) - { - outputsToSubmit.Add(new ToolOutput(requiredActionUpdate.ToolCallId, "25%")); - } - } - else if (update is MessageContentUpdate contentUpdate) - { - Console.Write(contentUpdate.Text); - } - } - if (outputsToSubmit.Count > 0) - { - asyncUpdates = client.SubmitToolOutputsToRunStreamingAsync(currentRun, outputsToSubmit); - } - } - while (currentRun?.Status.IsTerminal == false); - - #endregion - - // Optionally, delete the resources for tidiness if no longer needed. - RequestOptions noThrowOptions = new() { ErrorOptions = ClientErrorBehaviors.NoThrow }; - _ = await client.DeleteThreadAsync(thread.Id, noThrowOptions); - _ = await client.DeleteAssistantAsync(assistant.Id, noThrowOptions); - } -} +//using NUnit.Framework; +//using OpenAI.Assistants; +//using System; +//using System.ClientModel; +//using System.ClientModel.Primitives; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text.Json; +//using System.Threading.Tasks; + +//namespace OpenAI.Examples; + +//public partial class AssistantExamples +//{ +// [Test] +// public async Task Example02b_FunctionCallingStreaming() +// { +// // This example parallels the content at the following location: +// // https://platform.openai.com/docs/assistants/tools/function-calling/function-calling-beta +// #region Step 1 - Define Functions + +// // First, define the functions that the assistant will use in its defined tools. + +// FunctionToolDefinition getTemperatureTool = new() +// { +// FunctionName = "get_current_temperature", +// Description = "Gets the current temperature at a specific location.", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g., San Francisco, CA" +// }, +// "unit": { +// "type": "string", +// "enum": ["Celsius", "Fahrenheit"], +// "description": "The temperature unit to use. Infer this from the user's location." +// } +// } +// } +// """), +// }; + +// FunctionToolDefinition getRainProbabilityTool = new() +// { +// FunctionName = "get_current_rain_probability", +// Description = "Gets the current forecasted probability of rain at a specific location," +// + " represented as a percent chance in the range of 0 to 100.", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g., San Francisco, CA" +// } +// }, +// "required": ["location"] +// } +// """), +// }; + +// #endregion + +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); + +// #region Create a new assistant with function tools +// // Create an assistant that can call the function tools. +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Example: Function Calling", +// Instructions = +// "Don't make assumptions about what values to plug into functions." +// + " Ask for clarification if a user request is ambiguous.", +// Tools = { getTemperatureTool, getRainProbabilityTool }, +// }; + +// Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); +// #endregion + +// #region Step 2 - Create a thread and add messages +// AssistantThread thread = await client.CreateThreadAsync(); +// ThreadMessage message = await client.CreateMessageAsync( +// thread, +// MessageRole.User, +// [ +// "What's the weather in San Francisco today and the likelihood it'll rain?" +// ]); +// #endregion + +// #region Step 3 - Initiate a streaming run + +// // Protocol implementation + +// string json = $"{{\"assistant_id\":\"{assistant.Id}\",\"stream\":true}}"; + +// BinaryContent content = BinaryContent.Create(BinaryData.FromString(json)); +// RequestOptions options = new() { BufferResponse = false }; + +// OperationResult result = await client.CreateRunAsync(thread.Id, content); +// StreamingThreadRunOperation runOperation = StreamingThreadRunOperation.FromResult(result); + +// string status = null; +// do +// { +// // TODO: Move to convenience - add test for protocol +// status = await runOperation.WaitForStatusChangeAsync(options: default); +// if (status == "requires_action") +// { +// ClientResult runResult = await runOperation.GetRunAsync(options: default); + +// using JsonDocument doc = JsonDocument.Parse(runResult.GetRawResponse().Content); +// IEnumerable toolCallJsonElements = doc.RootElement +// .GetProperty("required_action") +// .GetProperty("submit_tool_outputs") +// .GetProperty("tool_calls").EnumerateArray(); + +// List outputsToSubmit = []; + +// foreach (JsonElement toolCallJsonElement in toolCallJsonElements) +// { +// string functionName = toolCallJsonElement.GetProperty("function").GetProperty("name").GetString(); +// string toolCallId = toolCallJsonElement.GetProperty("id").GetString(); + +// if (functionName == getTemperatureTool.FunctionName) +// { +// outputsToSubmit.Add(new ToolOutput(toolCallId, "57")); +// } +// else if (functionName == getRainProbabilityTool.FunctionName) +// { +// outputsToSubmit.Add(new ToolOutput(toolCallId, "25%")); +// } +// } +// } +// } while (status == "created" || +// status == "queued" || +// status == "requires_action" || +// status == "in_progress" || +// status == "cancelling"); + +// #endregion + +// // Optionally, delete the resources for tidiness if no longer needed. +// RequestOptions noThrowOptions = new() { ErrorOptions = ClientErrorBehaviors.NoThrow }; +// _ = await client.DeleteThreadAsync(thread.Id, noThrowOptions); +// _ = await client.DeleteAssistantAsync(assistant.Id, noThrowOptions); +// } +//} diff --git a/examples/Assistants/Example03_ListAssistantsWithPagination.cs b/examples/Assistants/Example03_ListAssistantsWithPagination.cs index 1c49470c4..e8640f1c5 100644 --- a/examples/Assistants/Example03_ListAssistantsWithPagination.cs +++ b/examples/Assistants/Example03_ListAssistantsWithPagination.cs @@ -2,6 +2,7 @@ using OpenAI.Assistants; using System; using System.ClientModel; +using System.Collections.Generic; namespace OpenAI.Examples; @@ -16,7 +17,8 @@ public void Example03_ListAssistantsWithPagination() int count = 0; - PageableCollection assistants = client.GetAssistants(); + PageCollection assitantPages = client.GetAssistants(); + IEnumerable assistants = assitantPages.GetAllValues(); foreach (Assistant assistant in assistants) { Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); diff --git a/examples/Assistants/Example03_ListAssistantsWithPaginationAsync.cs b/examples/Assistants/Example03_ListAssistantsWithPaginationAsync.cs index 145dcfeab..fa67a713f 100644 --- a/examples/Assistants/Example03_ListAssistantsWithPaginationAsync.cs +++ b/examples/Assistants/Example03_ListAssistantsWithPaginationAsync.cs @@ -2,6 +2,7 @@ using OpenAI.Assistants; using System; using System.ClientModel; +using System.Collections.Generic; using System.Threading.Tasks; namespace OpenAI.Examples; @@ -17,7 +18,8 @@ public async Task Example03_ListAssistantsWithPaginationAsync() int count = 0; - AsyncPageableCollection assistants = client.GetAssistantsAsync(); + AsyncPageCollection assitantPages = client.GetAssistantsAsync(); + IAsyncEnumerable assistants = assitantPages.GetAllValuesAsync(); await foreach (Assistant assistant in assistants) { Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); diff --git a/examples/Assistants/Example04_AllTheTools.cs b/examples/Assistants/Example04_AllTheTools.cs index 30c8fc9fd..67a298488 100644 --- a/examples/Assistants/Example04_AllTheTools.cs +++ b/examples/Assistants/Example04_AllTheTools.cs @@ -1,201 +1,201 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; - -namespace OpenAI.Examples; - -public partial class AssistantExamples -{ - [Test] - public void Example04_AllTheTools() - { -#pragma warning disable OPENAI001 - - #region Define a function tool - static string GetNameOfFamilyMember(string relation) - => relation switch - { - { } when relation.Contains("father") => "John Doe", - { } when relation.Contains("mother") => "Jane Doe", - _ => throw new ArgumentException(relation, nameof(relation)) - }; - - FunctionToolDefinition getNameOfFamilyMemberTool = new() - { - FunctionName = nameof(GetNameOfFamilyMember), - Description = "Provided a family relation type like 'father' or 'mother', " - + "gets the name of the related person from the user.", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "relation": { - "type": "string", - "description": "The relation to the user to query, e.g. 'mother' or 'father'" - } - }, - "required": [ "relation" ] - } - """), - }; - - #region Upload a mock file for use with file search - FileClient fileClient = new(); - OpenAIFileInfo favoriteNumberFile = fileClient.UploadFile( - BinaryData.FromString(""" - This file contains the favorite numbers for individuals. - - John Doe: 14 - Bob Doe: 32 - Jane Doe: 44 - """).ToStream(), - "favorite_numbers.txt", - FileUploadPurpose.Assistants); - #endregion - - #region Create an assistant with functions, file search, and code interpreter all enabled - AssistantClient client = new(); - Assistant assistant = client.CreateAssistant("gpt-4-turbo", new AssistantCreationOptions() - { - Instructions = "Use functions to resolve family relations into the names of people. Use file search to " - + " look up the favorite numbers of people. Use code interpreter to create graphs of lines.", - Tools = { getNameOfFamilyMemberTool, new FileSearchToolDefinition(), new CodeInterpreterToolDefinition() }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([favoriteNumberFile.Id]), - }, - }, - }, - }); - #endregion - - #region Create a new thread and start a run - AssistantThread thread = client.CreateThread(new ThreadCreationOptions() - { - InitialMessages = - { - "Create a graph of a line with a slope that's my father's favorite number " - + "and an offset that's my mother's favorite number.", - "Include people's names in your response and cite where you found them." - } - }); - - ThreadRun run = client.CreateRun(thread, assistant); - #endregion - - #region Complete the run, calling functions as needed - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - run = client.GetRun(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case nameof(GetNameOfFamilyMember): - { - using JsonDocument argumentsDocument = JsonDocument.Parse(action.FunctionArguments); - string relation = argumentsDocument.RootElement.TryGetProperty("relation", out JsonElement relationProperty) - ? relationProperty.GetString() - : null; - string toolResult = GetNameOfFamilyMember(relation); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - PageableCollection messages - = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - #endregion - - #region List run steps for details about tool calls - PageableCollection runSteps = client.GetRunSteps(run, resultOrder: ListOrder.OldestFirst); - foreach (RunStep step in runSteps) - { - Console.WriteLine($"Run step: {step.Status}"); - foreach (RunStepToolCall toolCall in step.Details.ToolCalls) - { - Console.WriteLine($" --> Tool call: {toolCall.ToolKind}"); - foreach (RunStepCodeInterpreterOutput output in toolCall.CodeInterpreterOutputs) - { - Console.WriteLine($" --> Output: {output.ImageFileId}"); - } - } - } - #endregion - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - - #region Clean up any temporary resources that are no longer needed - _ = client.DeleteThread(thread); - _ = client.DeleteAssistant(assistant); - _ = fileClient.DeleteFile(favoriteNumberFile.Id); - #endregion - } -} +//using NUnit.Framework; +//using OpenAI.Assistants; +//using OpenAI.Files; +//using System; +//using System.ClientModel; +//using System.Collections.Generic; +//using System.Text.Json; +//using System.Threading; + +//namespace OpenAI.Examples; + +//public partial class AssistantExamples +//{ +// [Test] +// public void Example04_AllTheTools() +// { +//#pragma warning disable OPENAI001 + +// #region Define a function tool +// static string GetNameOfFamilyMember(string relation) +// => relation switch +// { +// { } when relation.Contains("father") => "John Doe", +// { } when relation.Contains("mother") => "Jane Doe", +// _ => throw new ArgumentException(relation, nameof(relation)) +// }; + +// FunctionToolDefinition getNameOfFamilyMemberTool = new() +// { +// FunctionName = nameof(GetNameOfFamilyMember), +// Description = "Provided a family relation type like 'father' or 'mother', " +// + "gets the name of the related person from the user.", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "relation": { +// "type": "string", +// "description": "The relation to the user to query, e.g. 'mother' or 'father'" +// } +// }, +// "required": [ "relation" ] +// } +// """), +// }; + +// #region Upload a mock file for use with file search +// FileClient fileClient = new(); +// OpenAIFileInfo favoriteNumberFile = fileClient.UploadFile( +// BinaryData.FromString(""" +// This file contains the favorite numbers for individuals. + +// John Doe: 14 +// Bob Doe: 32 +// Jane Doe: 44 +// """).ToStream(), +// "favorite_numbers.txt", +// FileUploadPurpose.Assistants); +// #endregion + +// #region Create an assistant with functions, file search, and code interpreter all enabled +// AssistantClient client = new(); +// Assistant assistant = client.CreateAssistant("gpt-4-turbo", new AssistantCreationOptions() +// { +// Instructions = "Use functions to resolve family relations into the names of people. Use file search to " +// + " look up the favorite numbers of people. Use code interpreter to create graphs of lines.", +// Tools = { getNameOfFamilyMemberTool, new FileSearchToolDefinition(), new CodeInterpreterToolDefinition() }, +// ToolResources = new() +// { +// FileSearch = new() +// { +// NewVectorStores = +// { +// new VectorStoreCreationHelper([favoriteNumberFile.Id]), +// }, +// }, +// }, +// }); +// #endregion + +// #region Create a new thread and start a run +// AssistantThread thread = client.CreateThread(new ThreadCreationOptions() +// { +// InitialMessages = +// { +// "Create a graph of a line with a slope that's my father's favorite number " +// + "and an offset that's my mother's favorite number.", +// "Include people's names in your response and cite where you found them." +// } +// }); + +// ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread, assistant); +// #endregion + +// #region Complete the run, calling functions as needed +// // Poll the run until it is no longer queued or in progress. +// while (!runOperation.HasCompleted) +// { +// Thread.Sleep(TimeSpan.FromSeconds(1)); + +// // If the run requires action, resolve them. +// if (runOperation.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// foreach (RequiredAction action in runOperation.Value.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case nameof(GetNameOfFamilyMember): +// { +// using JsonDocument argumentsDocument = JsonDocument.Parse(action.FunctionArguments); +// string relation = argumentsDocument.RootElement.TryGetProperty("relation", out JsonElement relationProperty) +// ? relationProperty.GetString() +// : null; +// string toolResult = GetNameOfFamilyMember(relation); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// runOperation.SubmitToolOutputsToRun(toolOutputs); +// } +// } +// #endregion + +// #region +// // With the run complete, list the messages and display their content +// if (runOperation.Status == RunStatus.Completed) +// { +// PageCollection messagePages +// = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); +// IEnumerable messages = messagePages.GetAllValues(); + +// foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// #endregion + +// #region List run steps for details about tool calls +// PageCollection runSteps = runOperation.GetRunSteps(new RunStepCollectionOptions() { Order = ListOrder.OldestFirst }); +// foreach (RunStep step in runSteps.GetAllValues()) +// { +// Console.WriteLine($"Run step: {step.Status}"); +// foreach (RunStepToolCall toolCall in step.Details.ToolCalls) +// { +// Console.WriteLine($" --> Tool call: {toolCall.ToolKind}"); +// foreach (RunStepCodeInterpreterOutput output in toolCall.CodeInterpreterOutputs) +// { +// Console.WriteLine($" --> Output: {output.ImageFileId}"); +// } +// } +// } +// #endregion +// } +// else +// { +// throw new NotImplementedException(runOperation.Status.ToString()); +// } +// #endregion + +// #region Clean up any temporary resources that are no longer needed +// _ = client.DeleteThread(thread); +// _ = client.DeleteAssistant(assistant); +// _ = fileClient.DeleteFile(favoriteNumberFile.Id); +// #endregion +// } +//} diff --git a/examples/Assistants/Example05_AssistantsWithVision.cs b/examples/Assistants/Example05_AssistantsWithVision.cs index e73222261..f996ec93c 100644 --- a/examples/Assistants/Example05_AssistantsWithVision.cs +++ b/examples/Assistants/Example05_AssistantsWithVision.cs @@ -1,72 +1,72 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; +//using NUnit.Framework; +//using OpenAI.Assistants; +//using OpenAI.Files; +//using System; +//using System.ClientModel; -namespace OpenAI.Examples; +//namespace OpenAI.Examples; -public partial class AssistantExamples -{ - [Test] - public void Example05_AssistantsWithVision() - { - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - FileClient fileClient = openAIClient.GetFileClient(); - AssistantClient assistantClient = openAIClient.GetAssistantClient(); +//public partial class AssistantExamples +//{ +// [Test] +// public void Example05_AssistantsWithVision() +// { +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); +// FileClient fileClient = openAIClient.GetFileClient(); +// AssistantClient assistantClient = openAIClient.GetAssistantClient(); - OpenAIFileInfo pictureOfAppleFile = fileClient.UploadFile( - "picture-of-apple.jpg", - FileUploadPurpose.Vision); - Uri linkToPictureOfOrange = new("https://platform.openai.com/fictitious-files/picture-of-orange.png"); +// OpenAIFileInfo pictureOfAppleFile = fileClient.UploadFile( +// "picture-of-apple.jpg", +// FileUploadPurpose.Vision); +// Uri linkToPictureOfOrange = new("https://platform.openai.com/fictitious-files/picture-of-orange.png"); - Assistant assistant = assistantClient.CreateAssistant( - "gpt-4o", - new AssistantCreationOptions() - { - Instructions = "When asked a question, attempt to answer very concisely. " - + "Prefer one-sentence answers whenever feasible." - }); +// Assistant assistant = assistantClient.CreateAssistant( +// "gpt-4o", +// new AssistantCreationOptions() +// { +// Instructions = "When asked a question, attempt to answer very concisely. " +// + "Prefer one-sentence answers whenever feasible." +// }); - AssistantThread thread = assistantClient.CreateThread(new ThreadCreationOptions() - { - InitialMessages = - { - new ThreadInitializationMessage( - MessageRole.User, - [ - "Hello, assistant! Please compare these two images for me:", - MessageContent.FromImageFileId(pictureOfAppleFile.Id), - MessageContent.FromImageUrl(linkToPictureOfOrange), - ]), - } - }); +// AssistantThread thread = assistantClient.CreateThread(new ThreadCreationOptions() +// { +// InitialMessages = +// { +// new ThreadInitializationMessage( +// MessageRole.User, +// [ +// "Hello, assistant! Please compare these two images for me:", +// MessageContent.FromImageFileId(pictureOfAppleFile.Id), +// MessageContent.FromImageUrl(linkToPictureOfOrange), +// ]), +// } +// }); - ResultCollection streamingUpdates = assistantClient.CreateRunStreaming( - thread, - assistant, - new RunCreationOptions() - { - AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", - }); +// CollectionResult streamingUpdates = assistantClient.CreateRunStreaming( +// thread, +// assistant, +// new RunCreationOptions() +// { +// AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", +// }); - foreach (StreamingUpdate streamingUpdate in streamingUpdates) - { - if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) - { - Console.WriteLine($"--- Run started! ---"); - } - if (streamingUpdate is MessageContentUpdate contentUpdate) - { - Console.Write(contentUpdate.Text); - } - } +// foreach (StreamingUpdate streamingUpdate in streamingUpdates) +// { +// if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) +// { +// Console.WriteLine($"--- Run started! ---"); +// } +// if (streamingUpdate is MessageContentUpdate contentUpdate) +// { +// Console.Write(contentUpdate.Text); +// } +// } - // Delete temporary resources, if desired - _ = fileClient.DeleteFile(pictureOfAppleFile); - _ = assistantClient.DeleteThread(thread); - _ = assistantClient.DeleteAssistant(assistant); - } -} +// // Delete temporary resources, if desired +// _ = fileClient.DeleteFile(pictureOfAppleFile); +// _ = assistantClient.DeleteThread(thread); +// _ = assistantClient.DeleteAssistant(assistant); +// } +//} diff --git a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs index 68e50ed50..34241a380 100644 --- a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs +++ b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs @@ -1,72 +1,72 @@ -using NUnit.Framework; -using OpenAI.Assistants; -using OpenAI.Files; -using System; -using System.ClientModel; -using System.Threading.Tasks; +//using NUnit.Framework; +//using OpenAI.Assistants; +//using OpenAI.Files; +//using System; +//using System.ClientModel; +//using System.Threading.Tasks; -namespace OpenAI.Examples; +//namespace OpenAI.Examples; -public partial class AssistantExamples -{ - [Test] - public async Task Example05_AssistantsWithVisionAsync() - { - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - FileClient fileClient = openAIClient.GetFileClient(); - AssistantClient assistantClient = openAIClient.GetAssistantClient(); +//public partial class AssistantExamples +//{ +// [Test] +// public async Task Example05_AssistantsWithVisionAsync() +// { +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// OpenAIClient openAIClient = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); +// FileClient fileClient = openAIClient.GetFileClient(); +// AssistantClient assistantClient = openAIClient.GetAssistantClient(); - OpenAIFileInfo pictureOfAppleFile = await fileClient.UploadFileAsync( - "picture-of-apple.jpg", - FileUploadPurpose.Vision); - Uri linkToPictureOfOrange = new("https://platform.openai.com/fictitious-files/picture-of-orange.png"); +// OpenAIFileInfo pictureOfAppleFile = await fileClient.UploadFileAsync( +// "picture-of-apple.jpg", +// FileUploadPurpose.Vision); +// Uri linkToPictureOfOrange = new("https://platform.openai.com/fictitious-files/picture-of-orange.png"); - Assistant assistant = await assistantClient.CreateAssistantAsync( - "gpt-4o", - new AssistantCreationOptions() - { - Instructions = "When asked a question, attempt to answer very concisely. " - + "Prefer one-sentence answers whenever feasible." - }); +// Assistant assistant = await assistantClient.CreateAssistantAsync( +// "gpt-4o", +// new AssistantCreationOptions() +// { +// Instructions = "When asked a question, attempt to answer very concisely. " +// + "Prefer one-sentence answers whenever feasible." +// }); - AssistantThread thread = await assistantClient.CreateThreadAsync(new ThreadCreationOptions() - { - InitialMessages = - { - new ThreadInitializationMessage( - MessageRole.User, - [ - "Hello, assistant! Please compare these two images for me:", - MessageContent.FromImageFileId(pictureOfAppleFile.Id), - MessageContent.FromImageUrl(linkToPictureOfOrange), - ]), - } - }); +// AssistantThread thread = await assistantClient.CreateThreadAsync(new ThreadCreationOptions() +// { +// InitialMessages = +// { +// new ThreadInitializationMessage( +// MessageRole.User, +// [ +// "Hello, assistant! Please compare these two images for me:", +// MessageContent.FromImageFileId(pictureOfAppleFile.Id), +// MessageContent.FromImageUrl(linkToPictureOfOrange), +// ]), +// } +// }); - AsyncResultCollection streamingUpdates = assistantClient.CreateRunStreamingAsync( - thread, - assistant, - new RunCreationOptions() - { - AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", - }); +// AsyncCollectionResult streamingUpdates = assistantClient.CreateRunStreamingAsync( +// thread, +// assistant, +// new RunCreationOptions() +// { +// AdditionalInstructions = "When possible, try to sneak in puns if you're asked to compare things.", +// }); - await foreach (StreamingUpdate streamingUpdate in streamingUpdates) - { - if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) - { - Console.WriteLine($"--- Run started! ---"); - } - if (streamingUpdate is MessageContentUpdate contentUpdate) - { - Console.Write(contentUpdate.Text); - } - } +// await foreach (StreamingUpdate streamingUpdate in streamingUpdates) +// { +// if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) +// { +// Console.WriteLine($"--- Run started! ---"); +// } +// if (streamingUpdate is MessageContentUpdate contentUpdate) +// { +// Console.Write(contentUpdate.Text); +// } +// } - _ = await fileClient.DeleteFileAsync(pictureOfAppleFile); - _ = await assistantClient.DeleteThreadAsync(thread); - _ = await assistantClient.DeleteAssistantAsync(assistant); - } -} +// _ = await fileClient.DeleteFileAsync(pictureOfAppleFile); +// _ = await assistantClient.DeleteThreadAsync(thread); +// _ = await assistantClient.DeleteAssistantAsync(assistant); +// } +//} diff --git a/examples/Chat/Example02_SimpleChatStreaming.cs b/examples/Chat/Example02_SimpleChatStreaming.cs index cad64d9b0..50b8938f0 100644 --- a/examples/Chat/Example02_SimpleChatStreaming.cs +++ b/examples/Chat/Example02_SimpleChatStreaming.cs @@ -12,7 +12,7 @@ public void Example02_SimpleChatStreaming() { ChatClient client = new(model: "gpt-4o", Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - ResultCollection updates + CollectionResult updates = client.CompleteChatStreaming("Say 'this is a test.'"); Console.WriteLine($"[ASSISTANT]:"); diff --git a/examples/Chat/Example02_SimpleChatStreamingAsync.cs b/examples/Chat/Example02_SimpleChatStreamingAsync.cs index 123b5e887..c22bb4d8f 100644 --- a/examples/Chat/Example02_SimpleChatStreamingAsync.cs +++ b/examples/Chat/Example02_SimpleChatStreamingAsync.cs @@ -13,7 +13,7 @@ public async Task Example02_SimpleChatStreamingAsync() { ChatClient client = new(model: "gpt-4o", Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - AsyncResultCollection updates + AsyncCollectionResult updates = client.CompleteChatStreamingAsync("Say 'this is a test.'"); Console.WriteLine($"[ASSISTANT]:"); diff --git a/examples/Chat/Example04_FunctionCallingStreaming.cs b/examples/Chat/Example04_FunctionCallingStreaming.cs index e0e799d89..3f0770692 100644 --- a/examples/Chat/Example04_FunctionCallingStreaming.cs +++ b/examples/Chat/Example04_FunctionCallingStreaming.cs @@ -38,7 +38,7 @@ public void Example04_FunctionCallingStreaming() Dictionary indexToFunctionName = []; Dictionary indexToFunctionArguments = []; StringBuilder contentBuilder = new(); - ResultCollection chatUpdates + CollectionResult chatUpdates = client.CompleteChatStreaming(messages, options); foreach (StreamingChatCompletionUpdate chatUpdate in chatUpdates) diff --git a/examples/Chat/Example04_FunctionCallingStreamingAsync.cs b/examples/Chat/Example04_FunctionCallingStreamingAsync.cs index dc3eae564..6fac2c494 100644 --- a/examples/Chat/Example04_FunctionCallingStreamingAsync.cs +++ b/examples/Chat/Example04_FunctionCallingStreamingAsync.cs @@ -39,7 +39,7 @@ public async Task Example04_FunctionCallingStreamingAsync() Dictionary indexToFunctionName = []; Dictionary indexToFunctionArguments = []; StringBuilder contentBuilder = new(); - AsyncResultCollection chatUpdates + AsyncCollectionResult chatUpdates = client.CompleteChatStreamingAsync(messages, options); await foreach (StreamingChatCompletionUpdate chatUpdate in chatUpdates) diff --git a/src/Custom/Assistants/AssistantClient.Convenience.cs b/src/Custom/Assistants/AssistantClient.Convenience.cs index 510bbee5f..026d2bc18 100644 --- a/src/Custom/Assistants/AssistantClient.Convenience.cs +++ b/src/Custom/Assistants/AssistantClient.Convenience.cs @@ -28,7 +28,6 @@ public virtual Task> ModifyAssistantAsync(Assistant assi public virtual ClientResult ModifyAssistant(Assistant assistant, AssistantModificationOptions options) => ModifyAssistant(assistant?.Id, options); - /// /// Deletes an existing . /// @@ -126,39 +125,39 @@ public virtual ClientResult CreateMessage( => CreateMessage(thread?.Id, role, content, options); /// - /// Returns a collection of instances from an existing . + /// Gets a page collection holding instances from an existing . /// /// The thread to list messages from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of messages that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetMessagesAsync( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetMessagesAsync( AssistantThread thread, - ListOrder? resultOrder = default) + MessageCollectionOptions options = default) { Argument.AssertNotNull(thread, nameof(thread)); - return GetMessagesAsync(thread.Id, resultOrder); + return GetMessagesAsync(thread.Id, options); } /// - /// Returns a collection of instances from an existing . + /// Gets a page collection holding instances from an existing . /// /// The thread to list messages from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of messages that can be enumerated using foreach. - public virtual PageableCollection GetMessages( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetMessages( AssistantThread thread, - ListOrder? resultOrder = default) + MessageCollectionOptions options = default) { Argument.AssertNotNull(thread, nameof(thread)); - return GetMessages(thread.Id, resultOrder); + return GetMessages(thread.Id, options); } /// @@ -219,8 +218,12 @@ public virtual ClientResult DeleteMessage(ThreadMessage message) /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual Task> CreateRunAsync(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) - => CreateRunAsync(thread?.Id, assistant?.Id, options); + public virtual async Task CreateRunAsync( + ReturnWhen returnWhen, + AssistantThread thread, + Assistant assistant, + RunCreationOptions options = null) + => await CreateRunAsync(returnWhen, thread?.Id, assistant?.Id, options).ConfigureAwait(false); /// /// Begins a new that evaluates a using a specified @@ -230,21 +233,26 @@ public virtual Task> CreateRunAsync(AssistantThread thre /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual ClientResult CreateRun(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) - => CreateRun(thread?.Id, assistant?.Id, options); - - /// - /// Begins a new streaming that evaluates a using a specified - /// . - /// - /// The thread that the run should evaluate. - /// The assistant that should be used when evaluating the thread. - /// Additional options for the run. - public virtual AsyncResultCollection CreateRunStreamingAsync( - AssistantThread thread, - Assistant assistant, + public virtual ThreadRunOperation CreateRun( + ReturnWhen returnWhen, + AssistantThread thread, + Assistant assistant, RunCreationOptions options = null) - => CreateRunStreamingAsync(thread?.Id, assistant?.Id, options); + => CreateRun(returnWhen, thread?.Id, assistant?.Id, options); + + // TODO: is async variant needed if OperationResult has sync and async methods? + ///// + ///// Begins a new streaming that evaluates a using a specified + ///// . + ///// + ///// The thread that the run should evaluate. + ///// The assistant that should be used when evaluating the thread. + ///// Additional options for the run. + //public virtual StreamingThreadRunOperation CreateRunStreamingAsync( + // AssistantThread thread, + // Assistant assistant, + // RunCreationOptions options = null) + // => CreateRunStreamingAsync(thread?.Id, assistant?.Id, options); /// /// Begins a new streaming that evaluates a using a specified @@ -253,7 +261,7 @@ public virtual AsyncResultCollection CreateRunStreamingAsync( /// The thread that the run should evaluate. /// The assistant that should be used when evaluating the thread. /// Additional options for the run. - public virtual ResultCollection CreateRunStreaming( + public virtual StreamingThreadRunOperation CreateRunStreaming( AssistantThread thread, Assistant assistant, RunCreationOptions options = null) @@ -266,11 +274,12 @@ public virtual ResultCollection CreateRunStreaming( /// Options for the new thread that will be created. /// Additional options to apply to the run that will begin. /// A new . - public virtual Task> CreateThreadAndRunAsync( + public virtual async Task CreateThreadAndRunAsync( + ReturnWhen returnWhen, Assistant assistant, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null) - => CreateThreadAndRunAsync(assistant?.Id, threadOptions, runOptions); + => await CreateThreadAndRunAsync(returnWhen, assistant?.Id, threadOptions, runOptions).ConfigureAwait(false); /// /// Creates a new thread and immediately begins a run against it using the specified . @@ -279,187 +288,70 @@ public virtual Task> CreateThreadAndRunAsync( /// Options for the new thread that will be created. /// Additional options to apply to the run that will begin. /// A new . - public virtual ClientResult CreateThreadAndRun( - Assistant assistant, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null) - => CreateThreadAndRun(assistant?.Id, threadOptions, runOptions); - - /// - /// Creates a new thread and immediately begins a streaming run against it using the specified . - /// - /// The assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - public virtual AsyncResultCollection CreateThreadAndRunStreamingAsync( - Assistant assistant, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null) - => CreateThreadAndRunStreamingAsync(assistant?.Id, threadOptions, runOptions); - - /// - /// Creates a new thread and immediately begins a streaming run against it using the specified . - /// - /// The assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - public virtual ResultCollection CreateThreadAndRunStreaming( + public virtual ThreadRunOperation CreateThreadAndRun( + ReturnWhen returnWhen, Assistant assistant, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null) - => CreateThreadAndRunStreaming(assistant?.Id, threadOptions, runOptions); - - /// - /// Returns a collection of instances associated with an existing . + => CreateThreadAndRun(returnWhen, assistant?.Id, threadOptions, runOptions); + + ///// + ///// Creates a new thread and immediately begins a streaming run against it using the specified . + ///// + ///// The assistant that the new run should use. + ///// Options for the new thread that will be created. + ///// Additional options to apply to the run that will begin. + //public virtual AsyncCollectionResult CreateThreadAndRunStreamingAsync( + // Assistant assistant, + // ThreadCreationOptions threadOptions = null, + // RunCreationOptions runOptions = null) + // => CreateThreadAndRunStreamingAsync(assistant?.Id, threadOptions, runOptions); + + ///// + ///// Creates a new thread and immediately begins a streaming run against it using the specified . + ///// + ///// The assistant that the new run should use. + ///// Options for the new thread that will be created. + ///// Additional options to apply to the run that will begin. + //public virtual CollectionResult CreateThreadAndRunStreaming( + // Assistant assistant, + // ThreadCreationOptions threadOptions = null, + // RunCreationOptions runOptions = null) + // => CreateThreadAndRunStreaming(assistant?.Id, threadOptions, runOptions); + + /// + /// Gets a page collection holding instances associated with an existing . /// /// The thread that runs in the list should be associated with. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of runs that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetRunsAsync( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetRunsAsync( AssistantThread thread, - ListOrder? resultOrder = default) + RunCollectionOptions options = default) { Argument.AssertNotNull(thread, nameof(thread)); - return GetRunsAsync(thread.Id, resultOrder); + return GetRunsAsync(thread.Id, options); } /// - /// Returns a collection of instances associated with an existing . + /// Gets a page collection holding instances associated with an existing . /// /// The thread that runs in the list should be associated with. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of runs that can be enumerated using foreach. - public virtual PageableCollection GetRuns( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetRuns( AssistantThread thread, - ListOrder? resultOrder = default) + RunCollectionOptions options = default) { Argument.AssertNotNull(thread, nameof(thread)); - return GetRuns(thread.Id, resultOrder); - } - - /// - /// Gets a refreshed instance of an existing . - /// - /// The run to get a refreshed instance of. - /// A new instance with updated information. - public virtual Task> GetRunAsync(ThreadRun run) - => GetRunAsync(run?.ThreadId, run?.Id); - - /// - /// Gets a refreshed instance of an existing . - /// - /// The run to get a refreshed instance of. - /// A new instance with updated information. - public virtual ClientResult GetRun(ThreadRun run) - => GetRun(run?.ThreadId, run?.Id); - - /// - /// Submits a collection of required tool call outputs to a run and resumes the run. - /// - /// The run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - /// The , updated after the submission was processed. - public virtual Task> SubmitToolOutputsToRunAsync( - ThreadRun run, - IEnumerable toolOutputs) - => SubmitToolOutputsToRunAsync(run?.ThreadId, run?.Id, toolOutputs); - - /// - /// Submits a collection of required tool call outputs to a run and resumes the run. - /// - /// The run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - /// The , updated after the submission was processed. - public virtual ClientResult SubmitToolOutputsToRun( - ThreadRun run, - IEnumerable toolOutputs) - => SubmitToolOutputsToRun(run?.ThreadId, run?.Id, toolOutputs); - - /// - /// Submits a collection of required tool call outputs to a run and resumes the run with streaming enabled. - /// - /// The run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - public virtual AsyncResultCollection SubmitToolOutputsToRunStreamingAsync( - ThreadRun run, - IEnumerable toolOutputs) - => SubmitToolOutputsToRunStreamingAsync(run?.ThreadId, run?.Id, toolOutputs); - - /// - /// Submits a collection of required tool call outputs to a run and resumes the run with streaming enabled. - /// - /// The run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - public virtual ResultCollection SubmitToolOutputsToRunStreaming( - ThreadRun run, - IEnumerable toolOutputs) - => SubmitToolOutputsToRunStreaming(run?.ThreadId, run?.Id, toolOutputs); - - /// - /// Cancels an in-progress . - /// - /// The run to cancel. - /// An updated instance, reflecting the new status of the run. - public virtual Task> CancelRunAsync(ThreadRun run) - => CancelRunAsync(run?.ThreadId, run?.Id); - - /// - /// Cancels an in-progress . - /// - /// The run to cancel. - /// An updated instance, reflecting the new status of the run. - public virtual ClientResult CancelRun(ThreadRun run) - => CancelRun(run?.ThreadId, run?.Id); - - /// - /// Gets a collection of instances associated with a . - /// - /// The run to list run steps from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of run steps that can be enumerated using await foreach. - public virtual PageableCollection GetRunSteps( - ThreadRun run, - ListOrder? resultOrder = default) - { - Argument.AssertNotNull(run, nameof(run)); - - return GetRunSteps(run.ThreadId, run.Id, resultOrder); - } - - /// - /// Gets a collection of instances associated with a . - /// - /// The run to list run steps from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// A collection of run steps that can be enumerated using foreach. - public virtual AsyncPageableCollection GetRunStepsAsync( - ThreadRun run, - ListOrder? resultOrder = default) - { - Argument.AssertNotNull(run, nameof(run)); - - return GetRunStepsAsync(run.ThreadId, run.Id, resultOrder); + return GetRuns(thread.Id, options); } } diff --git a/src/Custom/Assistants/AssistantClient.Protocol.cs b/src/Custom/Assistants/AssistantClient.Protocol.cs index e6912702e..baa4766f9 100644 --- a/src/Custom/Assistants/AssistantClient.Protocol.cs +++ b/src/Custom/Assistants/AssistantClient.Protocol.cs @@ -1,6 +1,8 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace OpenAI.Assistants; @@ -40,7 +42,7 @@ public virtual ClientResult CreateAssistant(BinaryContent content, RequestOption } /// - /// [Protocol Method] Returns a list of assistants. + /// [Protocol Method] Returns a paginated collection of assistants. /// /// /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the @@ -62,15 +64,15 @@ public virtual ClientResult CreateAssistant(BinaryContent content, RequestOption /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetAssistantsAsync(int? limit, string order, string after, string before, RequestOptions options) + /// A collection of service responses, each holding a page of values. + public virtual IAsyncEnumerable GetAssistantsAsync(int? limit, string order, string after, string before, RequestOptions options) { - using PipelineMessage message = CreateGetAssistantsRequest(limit, order, after, before, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + AssistantsPageEnumerator enumerator = new AssistantsPageEnumerator(_pipeline, _endpoint, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// [Protocol Method] Returns a list of assistants. + /// [Protocol Method] Returns a paginated collection of assistants. /// /// /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the @@ -92,11 +94,11 @@ public virtual async Task GetAssistantsAsync(int? limit, string or /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetAssistants(int? limit, string order, string after, string before, RequestOptions options) + /// A collection of service responses, each holding a page of values. + public virtual IEnumerable GetAssistants(int? limit, string order, string after, string before, RequestOptions options) { - using PipelineMessage message = CreateGetAssistantsRequest(limit, order, after, before, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + AssistantsPageEnumerator enumerator = new AssistantsPageEnumerator(_pipeline, _endpoint, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); } /// @@ -213,13 +215,75 @@ public virtual Task CreateMessageAsync(string threadId, BinaryCont public virtual ClientResult CreateMessage(string threadId, BinaryContent content, RequestOptions options = null) => _messageSubClient.CreateMessage(threadId, content, options); - /// - public virtual Task GetMessagesAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) - => _messageSubClient.GetMessagesAsync(threadId, limit, order, after, before, options); + /// + /// [Protocol Method] Returns a paginated collection of messages for a given thread. + /// + /// The ID of the [thread](/docs/api-reference/threads) the messages belong to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IAsyncEnumerable GetMessagesAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - /// - public virtual ClientResult GetMessages(string threadId, int? limit, string order, string after, string before, RequestOptions options) - => _messageSubClient.GetMessages(threadId, limit, order, after, before, options); + MessagesPageEnumerator enumerator = new MessagesPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// [Protocol Method] Returns a paginated collection of messages for a given thread. + /// + /// The ID of the [thread](/docs/api-reference/threads) the messages belong to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IEnumerable GetMessages(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + MessagesPageEnumerator enumerator = new MessagesPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); + } /// public virtual Task GetMessageAsync(string threadId, string messageId, RequestOptions options) @@ -243,77 +307,189 @@ public virtual Task DeleteMessageAsync(string threadId, string mes public virtual ClientResult DeleteMessage(string threadId, string messageId, RequestOptions options) => _messageSubClient.DeleteMessage(threadId, messageId, options); - /// - public virtual Task CreateThreadAndRunAsync(BinaryContent content, RequestOptions options = null) - => _runSubClient.CreateThreadAndRunAsync(content, options); - - /// - public virtual ClientResult CreateThreadAndRun(BinaryContent content, RequestOptions options = null) - => _runSubClient.CreateThreadAndRun(content, options = null); - - /// - public virtual Task CreateRunAsync(string threadId, BinaryContent content, RequestOptions options = null) - => _runSubClient.CreateRunAsync(threadId, content, options); - - /// - public virtual ClientResult CreateRun(string threadId, BinaryContent content, RequestOptions options = null) - => _runSubClient.CreateRun(threadId, content, options); + public virtual /*async*/ Task CreateThreadAndRunAsync( + ReturnWhen returnWhen, + BinaryContent content, + RequestOptions options = null) + { + throw new NotImplementedException(); + //ClientResult result = await _runSubClient.CreateThreadAndRunAsync(content, options).ConfigureAwait(false); + + //// Protocol level: get values needed to create subclient from response + //PipelineResponse response = result.GetRawResponse(); + //using JsonDocument doc = JsonDocument.Parse(response.Content); + //string threadId = doc.RootElement.GetProperty("thread_id"u8).GetString()!; + //string runId = doc.RootElement.GetProperty("id"u8).GetString()!; + + //// Create the poller + //ThreadRunPoller poller = new ThreadRunPoller(_pipeline, _endpoint, result, threadId, runId, options); + + //// Create the operation subclient + //ThreadRunOperation operation = new ThreadRunOperation( + // _pipeline, _endpoint, + // threadId, runId, result.GetRawResponse(), + // poller); + + //if (returnWhen == ReturnWhen.Started) + //{ + // return operation; + //} + + //operation.WaitForCompletionResult(); + //return operation; + } - /// - public virtual Task GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) - => _runSubClient.GetRunsAsync(threadId, limit, order, after, before, options); + public virtual ThreadRunOperation CreateThreadAndRun( + ReturnWhen returnWhen, + BinaryContent content, + RequestOptions options = null) + { + throw new NotImplementedException(); + //ClientResult result = _runSubClient.CreateThreadAndRun(content, options); + + //// Protocol level: get values needed to create subclient from response + //PipelineResponse response = result.GetRawResponse(); + //using JsonDocument doc = JsonDocument.Parse(response.Content); + //string threadId = doc.RootElement.GetProperty("thread_id"u8).GetString()!; + //string runId = doc.RootElement.GetProperty("id"u8).GetString()!; + + //// Create the poller + //ThreadRunPoller poller = new ThreadRunPoller(_pipeline, _endpoint, result, threadId, runId, options); + + //// Create the operation subclient + //ThreadRunOperation operation = new ThreadRunOperation( + // _pipeline, _endpoint, + // threadId, runId, result.GetRawResponse(), + // poller); + + //if (returnWhen == ReturnWhen.Started) + //{ + // return operation; + //} + + //operation.WaitForCompletionResult(); + //return operation; + } - /// - public virtual ClientResult GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) - => _runSubClient.GetRuns(threadId, limit, order, after, before, options); + /// + /// [Protocol Method] Returns a paginated collection of runs belonging to a thread. + /// + /// The ID of the thread the run belongs to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IAsyncEnumerable GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - /// - public virtual Task GetRunAsync(string threadId, string runId, RequestOptions options) - => _runSubClient.GetRunAsync(threadId, runId, options); + RunsPageEnumerator enumerator = new RunsPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); + } - /// - public virtual ClientResult GetRun(string threadId, string runId, RequestOptions options) - => _runSubClient.GetRun(threadId, runId, options); + /// + /// [Protocol Method] Returns a paginated collection of runs belonging to a thread. + /// + /// The ID of the thread the run belongs to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IEnumerable GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - /// - public virtual Task ModifyRunAsync(string threadId, string runId, BinaryContent content, RequestOptions options = null) - => _runSubClient.ModifyRunAsync(threadId, runId, content, options); + RunsPageEnumerator enumerator = new RunsPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); + } - /// - public virtual ClientResult ModifyRun(string threadId, string runId, BinaryContent content, RequestOptions options = null) - => _runSubClient.ModifyRun(threadId, runId, content, options); + public virtual async Task CreateRunAsync( + ReturnWhen returnWhen, + string threadId, + BinaryContent content, + RequestOptions options = null) + { + ClientResult result = await _runSubClient.CreateRunAsync(threadId, content, options).ConfigureAwait(false); + PipelineResponse response = result.GetRawResponse(); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, response); - /// - public virtual Task CancelRunAsync(string threadId, string runId, RequestOptions options) - => _runSubClient.CancelRunAsync(threadId, runId, options); + if (returnWhen == ReturnWhen.Started) + { + return operation; + } - /// - public virtual ClientResult CancelRun(string threadId, string runId, RequestOptions options) - => _runSubClient.CancelRun(threadId, runId, options); + await operation.WaitAsync(options.CancellationToken).ConfigureAwait(false); + return operation; + } - /// - public virtual Task SubmitToolOutputsToRunAsync(string threadId, string runId, BinaryContent content, RequestOptions options = null) - => _runSubClient.SubmitToolOutputsToRunAsync(threadId, runId, content, options); + public virtual ThreadRunOperation CreateRun( + ReturnWhen returnWhen, + string threadId, + BinaryContent content, + RequestOptions options = null) + { + options ??= new(); - /// - public virtual ClientResult SubmitToolOutputsToRun(string threadId, string runId, BinaryContent content, RequestOptions options = null) - => _runSubClient.SubmitToolOutputsToRun(threadId, runId, content, options); + ClientResult result = _runSubClient.CreateRun(threadId, content, options); + PipelineResponse response = result.GetRawResponse(); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, response); - /// - public virtual Task GetRunStepsAsync(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) - => _runSubClient.GetRunStepsAsync(threadId, runId, limit, order, after, before, options); + if (returnWhen == ReturnWhen.Started) + { + return operation; + } - /// - public virtual ClientResult GetRunSteps(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) - => _runSubClient.GetRunSteps(threadId, runId, limit, order, after, before, options); + // TODO: + // Note that this will poll over requested streaming, which is undesired... + operation.Wait(options.CancellationToken); + return operation; + } - /// - public virtual Task GetRunStepAsync(string threadId, string runId, string stepId, RequestOptions options) - => _runSubClient.GetRunStepAsync(threadId, runId, stepId, options); + /// + internal virtual Task CreateRunAsync(string threadId, BinaryContent content, RequestOptions options = null) + => _runSubClient.CreateRunAsync(threadId, content, options); - /// - public virtual ClientResult GetRunStep(string threadId, string runId, string stepId, RequestOptions options) - => _runSubClient.GetRunStep(threadId, runId, stepId, options); + /// + internal virtual ClientResult CreateRun(string threadId, BinaryContent content, RequestOptions options = null) + => _runSubClient.CreateRun(threadId, content, options); /// public virtual Task CreateThreadAsync(BinaryContent content, RequestOptions options = null) diff --git a/src/Custom/Assistants/AssistantClient.cs b/src/Custom/Assistants/AssistantClient.cs index 155409142..525f1031c 100644 --- a/src/Custom/Assistants/AssistantClient.cs +++ b/src/Custom/Assistants/AssistantClient.cs @@ -3,11 +3,9 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using static OpenAI.InternalListHelpers; namespace OpenAI.Assistants; @@ -103,33 +101,101 @@ public virtual ClientResult CreateAssistant(string model, AssistantCr } /// - /// Returns a collection of instances. + /// Gets a page collection holding instances. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// A collection of assistants that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetAssistantsAsync(ListOrder? resultOrder = null, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetAssistantsAsync( + AssistantCollectionOptions options = default, + CancellationToken cancellationToken = default) + { + AssistantsPageEnumerator enumerator = new(_pipeline, _endpoint, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetAssistantsAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + AssistantsPageToken pageToken = AssistantsPageToken.FromToken(firstPageToken); + AssistantsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection holding instances. + /// + /// Options describing the collection to return. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetAssistants( + AssistantCollectionOptions options = default, + CancellationToken cancellationToken = default) { - return CreateAsyncPageable((continuationToken, pageSize) - => GetAssistantsAsync(pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + AssistantsPageEnumerator enumerator = new(_pipeline, _endpoint, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } /// - /// Returns a collection of instances. + /// Rehydrates a page collection holding instances from a page token. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Page token corresponding to the first page of the collection to rehydrate. /// A token that can be used to cancel this method call. - /// A collection of assistants that can be enumerated using foreach. - public virtual PageableCollection GetAssistants(ListOrder? resultOrder = null, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetAssistants( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) { - return CreatePageable((continuationToken, pageSize) - => GetAssistants(pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + AssistantsPageToken pageToken = AssistantsPageToken.FromToken(firstPageToken); + AssistantsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } /// @@ -289,7 +355,7 @@ public virtual async Task> CreateMessageAsync( string threadId, MessageRole role, IEnumerable content, - MessageCreationOptions options = null, + MessageCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -336,45 +402,113 @@ public virtual ClientResult CreateMessage( } /// - /// Returns a collection of instances from an existing . + /// Gets a page collection of instances from an existing . /// /// The ID of the thread to list messages from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// A collection of messages that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetMessagesAsync( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetMessagesAsync( string threadId, - ListOrder? resultOrder = null, + MessageCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - return CreateAsyncPageable((continuationToken, pageSize) - => GetMessagesAsync(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + MessagesPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Returns a collection of instances from an existing . + /// Rehydrates a page collection of instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetMessagesAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + MessagesPageToken pageToken = MessagesPageToken.FromToken(firstPageToken); + MessagesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection holding instances from an existing . /// /// The ID of the thread to list messages from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// A collection of messages that can be enumerated using foreach. - public virtual PageableCollection GetMessages( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetMessages( string threadId, - ListOrder? resultOrder = null, + MessageCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - return CreatePageable((continuationToken, pageSize) - => GetMessages(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + MessagesPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetMessages( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + MessagesPageToken pageToken = MessagesPageToken.FromToken(firstPageToken); + MessagesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } /// @@ -489,17 +623,22 @@ public virtual ClientResult DeleteMessage(string threadId, string messageI /// Additional options for the run. /// A token that can be used to cancel this method call. /// A new instance. - public virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null, CancellationToken cancellationToken = default) + public virtual /*async*/ Task CreateRunAsync( + ReturnWhen returnWhen, + string threadId, + string assistantId, + RunCreationOptions options = null, + CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - options ??= new(); - options.AssistantId = assistantId; - options.Stream = null; + throw new NotImplementedException(); - ClientResult protocolResult = await CreateRunAsync(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions()) - .ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + //Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + //Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); + //options ??= new(); + //options.AssistantId = assistantId; + //options.Stream = null; + + //return await CreateRunAsync(returnWhen, threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions()).ConfigureAwait(false); } /// @@ -510,205 +649,115 @@ public virtual async Task> CreateRunAsync(string threadI /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A token that can be used to cancel this method call. - /// A new instance. - public virtual ClientResult CreateRun(string threadId, string assistantId, RunCreationOptions options = null, CancellationToken cancellationToken = default) + /// TODO + public virtual ThreadRunOperation CreateRun( + ReturnWhen returnWhen, + string threadId, + string assistantId, + RunCreationOptions options = null, + CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); + options ??= new(); options.AssistantId = assistantId; options.Stream = null; - ClientResult protocolResult = CreateRun(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions()); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ClientResult result = CreateRun(threadId, assistantId, options, cancellationToken); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, + value: result, + status: result.Value.Status, + result.GetRawResponse()); + + if (returnWhen == ReturnWhen.Started) + { + return operation; + } + + operation.Wait(cancellationToken); + return operation; } + // Note: these become internal + // TODO: can we merge this with public ones? + /// - /// Begins a new streaming that evaluates a using a specified + /// Begins a new that evaluates a using a specified /// . /// /// The ID of the thread that the run should evaluate. /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A token that can be used to cancel this method call. - public virtual AsyncResultCollection CreateRunStreamingAsync( - string threadId, - string assistantId, - RunCreationOptions options = null, - CancellationToken cancellationToken = default) + /// A new instance. + internal virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - options ??= new(); options.AssistantId = assistantId; - options.Stream = true; + options.Stream = null; - async Task getResultAsync() => - await CreateRunAsync(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions(streaming: true)) + ClientResult protocolResult = await CreateRunAsync(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions()) .ConfigureAwait(false); - - return new AsyncStreamingUpdateCollection(getResultAsync); + return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); } /// - /// Begins a new streaming that evaluates a using a specified + /// Begins a new that evaluates a using a specified /// . /// /// The ID of the thread that the run should evaluate. /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A token that can be used to cancel this method call. - public virtual ResultCollection CreateRunStreaming( - string threadId, - string assistantId, - RunCreationOptions options = null, - CancellationToken cancellationToken = default) + /// A new instance. + internal virtual ClientResult CreateRun(string threadId, string assistantId, RunCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - options ??= new(); options.AssistantId = assistantId; - options.Stream = true; - - ClientResult getResult() => CreateRun(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions(streaming: true)); - - return new StreamingUpdateCollection(getResult); - } - - /// - /// Creates a new thread and immediately begins a run against it using the specified . - /// - /// The ID of the assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - /// A token that can be used to cancel this method call. - /// A new . - public virtual async Task> CreateThreadAndRunAsync( - string assistantId, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null, - CancellationToken cancellationToken = default) - { - runOptions ??= new(); - runOptions.Stream = null; - BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); - ClientResult protocolResult = await CreateThreadAndRunAsync(protocolContent, cancellationToken.ToRequestOptions()).ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); - } + options.Stream = null; - /// - /// Creates a new thread and immediately begins a run against it using the specified . - /// - /// The ID of the assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - /// A token that can be used to cancel this method call. - /// A new . - public virtual ClientResult CreateThreadAndRun( - string assistantId, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null, - CancellationToken cancellationToken = default) - { - runOptions ??= new(); - runOptions.Stream = null; - BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); - ClientResult protocolResult = CreateThreadAndRun(protocolContent, cancellationToken.ToRequestOptions()); + ClientResult protocolResult = CreateRun(threadId, options.ToBinaryContent(), cancellationToken.ToRequestOptions()); return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); } /// - /// Creates a new thread and immediately begins a streaming run against it using the specified . - /// - /// The ID of the assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - /// A token that can be used to cancel this method call. - public virtual AsyncResultCollection CreateThreadAndRunStreamingAsync( - string assistantId, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null, - CancellationToken cancellationToken = default) - { - Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - - runOptions ??= new(); - runOptions.Stream = true; - BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); - - async Task getResultAsync() => - await CreateThreadAndRunAsync(protocolContent, cancellationToken.ToRequestOptions(streaming: true)) - .ConfigureAwait(false); - - return new AsyncStreamingUpdateCollection(getResultAsync); - } - - /// - /// Creates a new thread and immediately begins a streaming run against it using the specified . - /// - /// The ID of the assistant that the new run should use. - /// Options for the new thread that will be created. - /// Additional options to apply to the run that will begin. - /// A token that can be used to cancel this method call. - public virtual ResultCollection CreateThreadAndRunStreaming( - string assistantId, - ThreadCreationOptions threadOptions = null, - RunCreationOptions runOptions = null, - CancellationToken cancellationToken = default) - { - Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - - runOptions ??= new(); - runOptions.Stream = true; - BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); - - ClientResult getResult() => CreateThreadAndRun(protocolContent, cancellationToken.ToRequestOptions(streaming: true)); - - return new StreamingUpdateCollection(getResult); - } - - /// - /// Returns a collection of instances associated with an existing . + /// Gets an existing from a known . /// - /// The ID of the thread that runs in the list should be associated with. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// The ID of the thread to retrieve the run from. + /// The ID of the run to retrieve. /// A token that can be used to cancel this method call. - /// A collection of runs that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetRunsAsync( - string threadId, - ListOrder? resultOrder = default, - CancellationToken cancellationToken = default) + /// TODO + public virtual async Task GetRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - return CreateAsyncPageable((continuationToken, pageSize) - => GetRunsAsync(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, threadId, runId); + await operation.UpdateAsync(cancellationToken).ConfigureAwait(false); + return operation; } /// - /// Returns a collection of instances associated with an existing . + /// Rehydrates a from a rehydration token. /// - /// The ID of the thread that runs in the list should be associated with. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Rehydration token corresponding to the run operation to rehydrate. /// A token that can be used to cancel this method call. - /// A collection of runs that can be enumerated using foreach. - public virtual PageableCollection GetRuns( - string threadId, - ListOrder? resultOrder = default, + /// TODO + public virtual async Task GetRunAsync( + ContinuationToken rehydrationToken, CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); - return CreatePageable((continuationToken, pageSize) - => GetRuns(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + ThreadRunOperationToken token = ThreadRunOperationToken.FromToken(rehydrationToken); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, token); + await operation.UpdateAsync(cancellationToken).ConfigureAwait(false); + return operation; } /// @@ -717,241 +766,269 @@ public virtual PageableCollection GetRuns( /// The ID of the thread to retrieve the run from. /// The ID of the run to retrieve. /// A token that can be used to cancel this method call. - /// The existing instance. - public virtual async Task> GetRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + /// TODO + public virtual ThreadRunOperation GetRun(string threadId, string runId, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - ClientResult protocolResult = await GetRunAsync(threadId, runId, cancellationToken.ToRequestOptions()).ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ThreadRunOperationToken token = new ThreadRunOperationToken(threadId, runId); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, token); + operation.Update(cancellationToken); + return operation; } /// - /// Gets an existing from a known . + /// Rehydrates a from a rehydration token. /// - /// The ID of the thread to retrieve the run from. - /// The ID of the run to retrieve. + /// Rehydration token corresponding to the run operation to rehydrate. /// A token that can be used to cancel this method call. - /// The existing instance. - public virtual ClientResult GetRun(string threadId, string runId, CancellationToken cancellationToken = default) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - ClientResult protocolResult = GetRun(threadId, runId, cancellationToken.ToRequestOptions()); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); - } - - /// - /// Submits a collection of required tool call outputs to a run and resumes the run. - /// - /// The thread ID of the thread being run. - /// The ID of the run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - /// A token that can be used to cancel this method call. - /// The , updated after the submission was processed. - public virtual async Task> SubmitToolOutputsToRunAsync( - string threadId, - string runId, - IEnumerable toolOutputs, + /// TODO + public virtual ThreadRunOperation GetRun( + ContinuationToken rehydrationToken, CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); - BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs).ToBinaryContent(); - ClientResult protocolResult = await SubmitToolOutputsToRunAsync(threadId, runId, content, cancellationToken.ToRequestOptions()) - .ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ThreadRunOperationToken token = ThreadRunOperationToken.FromToken(rehydrationToken); + ThreadRunOperation operation = new ThreadRunOperation(_pipeline, _endpoint, token); + operation.Update(cancellationToken); + return operation; } /// - /// Submits a collection of required tool call outputs to a run and resumes the run. + /// Begins a new streaming that evaluates a using a specified + /// . /// - /// The thread ID of the thread being run. - /// The ID of the run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// + /// The ID of the thread that the run should evaluate. + /// The ID of the assistant that should be used when evaluating the thread. + /// Additional options for the run. /// A token that can be used to cancel this method call. - /// The , updated after the submission was processed. - public virtual ClientResult SubmitToolOutputsToRun( + public virtual StreamingThreadRunOperation CreateRunStreaming( string threadId, - string runId, - IEnumerable toolOutputs, + string assistantId, + RunCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs).ToBinaryContent(); - ClientResult protocolResult = SubmitToolOutputsToRun(threadId, runId, content, cancellationToken.ToRequestOptions()); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); - } + Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); - /// - /// Submits a collection of required tool call outputs to a run and resumes the run with streaming enabled. - /// - /// The thread ID of the thread being run. - /// The ID of the run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// - /// A token that can be used to cancel this method call. - public virtual AsyncResultCollection SubmitToolOutputsToRunStreamingAsync( - string threadId, - string runId, - IEnumerable toolOutputs, - CancellationToken cancellationToken = default) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + options ??= new(); + options.AssistantId = assistantId; + options.Stream = true; - BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs.ToList(), stream: true, null) - .ToBinaryContent(); + BinaryContent content = options.ToBinaryContent(); async Task getResultAsync() => - await SubmitToolOutputsToRunAsync(threadId, runId, content, cancellationToken.ToRequestOptions(streaming: true)) + await _runSubClient.CreateRunAsync(threadId, content, cancellationToken.ToRequestOptions()) .ConfigureAwait(false); - return new AsyncStreamingUpdateCollection(getResultAsync); + ClientResult getResult() => + _runSubClient.CreateRun(threadId, content, cancellationToken.ToRequestOptions()); + + return new StreamingThreadRunOperation(_pipeline, _endpoint, getResultAsync, getResult); } /// - /// Submits a collection of required tool call outputs to a run and resumes the run with streaming enabled. + /// Creates a new thread and immediately begins a run against it using the specified . /// - /// The thread ID of the thread being run. - /// The ID of the run that reached a requires_action status. - /// - /// The tool outputs, corresponding to instances from the run. - /// + /// The ID of the assistant that the new run should use. + /// Options for the new thread that will be created. + /// Additional options to apply to the run that will begin. /// A token that can be used to cancel this method call. - public virtual ResultCollection SubmitToolOutputsToRunStreaming( - string threadId, - string runId, - IEnumerable toolOutputs, + /// A new . + public virtual async Task CreateThreadAndRunAsync( + ReturnWhen returnWhen, + string assistantId, + ThreadCreationOptions threadOptions = null, + RunCreationOptions runOptions = null, CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs.ToList(), stream: true, null) - .ToBinaryContent(); - - ClientResult getResult() => SubmitToolOutputsToRun(threadId, runId, content, cancellationToken.ToRequestOptions(streaming: true)); - - return new StreamingUpdateCollection(getResult); + runOptions ??= new(); + runOptions.Stream = null; + BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); + return await CreateThreadAndRunAsync(returnWhen, protocolContent, cancellationToken.ToRequestOptions()).ConfigureAwait(false); } /// - /// Cancels an in-progress . + /// Creates a new thread and immediately begins a run against it using the specified . /// - /// The ID of the thread associated with the run. - /// The ID of the run to cancel. + /// The ID of the assistant that the new run should use. + /// Options for the new thread that will be created. + /// Additional options to apply to the run that will begin. /// A token that can be used to cancel this method call. - /// An updated instance, reflecting the new status of the run. - public virtual async Task> CancelRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + /// A new . + public virtual ThreadRunOperation CreateThreadAndRun( + ReturnWhen returnWhen, + string assistantId, + ThreadCreationOptions threadOptions = null, + RunCreationOptions runOptions = null, + CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - ClientResult protocolResult = await CancelRunAsync(threadId, runId, cancellationToken.ToRequestOptions()).ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + runOptions ??= new(); + runOptions.Stream = null; + BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); + return CreateThreadAndRun(returnWhen, protocolContent, cancellationToken.ToRequestOptions()); } + ///// + ///// Creates a new thread and immediately begins a streaming run against it using the specified . + ///// + ///// The ID of the assistant that the new run should use. + ///// Options for the new thread that will be created. + ///// Additional options to apply to the run that will begin. + ///// A token that can be used to cancel this method call. + //public virtual AsyncCollectionResult CreateThreadAndRunStreamingAsync( + // string assistantId, + // ThreadCreationOptions threadOptions = null, + // RunCreationOptions runOptions = null, + // CancellationToken cancellationToken = default) + //{ + // Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); + + // runOptions ??= new(); + // runOptions.Stream = true; + // BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); + + // async Task getResultAsync() => + // await CreateThreadAndRunAsync(protocolContent, cancellationToken.ToRequestOptions(streaming: true)) + // .ConfigureAwait(false); + + // return new AsyncStreamingUpdateCollection(getResultAsync); + //} + + ///// + ///// Creates a new thread and immediately begins a streaming run against it using the specified . + ///// + ///// The ID of the assistant that the new run should use. + ///// Options for the new thread that will be created. + ///// Additional options to apply to the run that will begin. + ///// A token that can be used to cancel this method call. + //public virtual CollectionResult CreateThreadAndRunStreaming( + // string assistantId, + // ThreadCreationOptions threadOptions = null, + // RunCreationOptions runOptions = null, + // CancellationToken cancellationToken = default) + //{ + // Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); + + // runOptions ??= new(); + // runOptions.Stream = true; + // BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); + + // ClientResult getResult() => CreateThreadAndRun(protocolContent, cancellationToken.ToRequestOptions(streaming: true)); + + // return new StreamingUpdateCollection(getResult); + //} + /// - /// Cancels an in-progress . + /// Gets a page collection holding instances associated with an existing . /// - /// The ID of the thread associated with the run. - /// The ID of the run to cancel. + /// The ID of the thread that runs in the list should be associated with. + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// An updated instance, reflecting the new status of the run. - public virtual ClientResult CancelRun(string threadId, string runId, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetRunsAsync( + string threadId, + RunCollectionOptions options = default, + CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - ClientResult protocolResult = CancelRun(threadId, runId, cancellationToken.ToRequestOptions()); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + RunsPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Gets a collection of instances associated with a . + /// Rehydrates a page collection holding instances from a page token. /// - /// The ID of the thread associated with the run. - /// The ID of the run to list run steps from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Page token corresponding to the first page of the collection to rehydrate. /// A token that can be used to cancel this method call. - /// A collection of run steps that can be enumerated using await foreach. - public virtual AsyncPageableCollection GetRunStepsAsync( - string threadId, - string runId, - ListOrder? resultOrder = default, + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetRunsAsync( + ContinuationToken firstPageToken, CancellationToken cancellationToken = default) { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - return CreateAsyncPageable((continuationToken, pageSize) - => GetRunStepsAsync(threadId, runId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + RunsPageToken pageToken = RunsPageToken.FromToken(firstPageToken); + RunsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Gets a collection of instances associated with a . + /// Gets a page collection holding instances associated with an existing . /// - /// The ID of the thread associated with the run. - /// The ID of the run to list run steps from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// The ID of the thread that runs in the list should be associated with. + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// A collection of run steps that can be enumerated using foreach. - public virtual PageableCollection GetRunSteps( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetRuns( string threadId, - string runId, - ListOrder? resultOrder = default, + RunCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - return CreatePageable((continuationToken, pageSize) - => GetRunSteps(threadId, runId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); - } + RunsPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); - /// - /// Gets a single run step from a run. - /// - /// The ID of the thread associated with the run. - /// The ID of the run. - /// The ID of the run step. - /// A token that can be used to cancel this method call. - /// A instance corresponding to the specified step. - public virtual async Task> GetRunStepAsync(string threadId, string runId, string stepId, CancellationToken cancellationToken = default) - { - ClientResult protocolResult = await GetRunStepAsync(threadId, runId, stepId, cancellationToken.ToRequestOptions()).ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, RunStep.FromResponse); + return PageCollectionHelpers.Create(enumerator); } /// - /// Gets a single run step from a run. + /// Rehydrates a page collection holding instances from a page token. /// - /// The ID of the thread associated with the run. - /// The ID of the run. - /// The ID of the run step. + /// Page token corresponding to the first page of the collection to rehydrate. /// A token that can be used to cancel this method call. - /// A instance corresponding to the specified step. - public virtual ClientResult GetRunStep(string threadId, string runId, string stepId, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetRuns( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) { - ClientResult protocolResult = GetRunStep(threadId, runId, stepId, cancellationToken.ToRequestOptions()); - return CreateResultFromProtocol(protocolResult, RunStep.FromResponse); + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + RunsPageToken pageToken = RunsPageToken.FromToken(firstPageToken); + RunsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } private static BinaryContent CreateThreadAndRunProtocolContent( diff --git a/src/Custom/Assistants/AssistantCollectionOptions.cs b/src/Custom/Assistants/AssistantCollectionOptions.cs new file mode 100644 index 000000000..731401eff --- /dev/null +++ b/src/Custom/Assistants/AssistantCollectionOptions.cs @@ -0,0 +1,33 @@ +namespace OpenAI.Assistants; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class AssistantCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public AssistantCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } +} diff --git a/src/Custom/Assistants/Internal/InternalAssistantMessageClient.Protocol.cs b/src/Custom/Assistants/Internal/InternalAssistantMessageClient.Protocol.cs index 04717a612..fb54903bc 100644 --- a/src/Custom/Assistants/Internal/InternalAssistantMessageClient.Protocol.cs +++ b/src/Custom/Assistants/Internal/InternalAssistantMessageClient.Protocol.cs @@ -45,76 +45,6 @@ public virtual ClientResult CreateMessage(string threadId, BinaryContent content return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); } - /// - /// [Protocol Method] Returns a list of messages for a given thread. - /// - /// The ID of the [thread](/docs/api-reference/threads) the messages belong to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// is null. - /// is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetMessagesAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - - using PipelineMessage message = CreateGetMessagesRequest(threadId, limit, order, after, before, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Returns a list of messages for a given thread. - /// - /// The ID of the [thread](/docs/api-reference/threads) the messages belong to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// is null. - /// is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetMessages(string threadId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - - using PipelineMessage message = CreateGetMessagesRequest(threadId, limit, order, after, before, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - /// /// [Protocol Method] Retrieve a message. /// diff --git a/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs b/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs index 626228d85..aa857afc0 100644 --- a/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs +++ b/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs @@ -120,376 +120,4 @@ public virtual ClientResult CreateRun(string threadId, BinaryContent content, Re } } } - - /// - /// [Protocol Method] Returns a list of runs belonging to a thread. - /// - /// The ID of the thread the run belongs to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// is null. - /// is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - - using PipelineMessage message = CreateGetRunsRequest(threadId, limit, order, after, before, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Returns a list of runs belonging to a thread. - /// - /// The ID of the thread the run belongs to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// is null. - /// is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - - using PipelineMessage message = CreateGetRunsRequest(threadId, limit, order, after, before, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - - /// - /// [Protocol Method] Retrieves a run. - /// - /// The ID of the [thread](/docs/api-reference/threads) that was run. - /// The ID of the run to retrieve. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetRunAsync(string threadId, string runId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateGetRunRequest(threadId, runId, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Retrieves a run. - /// - /// The ID of the [thread](/docs/api-reference/threads) that was run. - /// The ID of the run to retrieve. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetRun(string threadId, string runId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateGetRunRequest(threadId, runId, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - - /// - /// [Protocol Method] Modifies a run. - /// - /// The ID of the [thread](/docs/api-reference/threads) that was run. - /// The ID of the run to modify. - /// The content to send as the body of the request. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task ModifyRunAsync(string threadId, string runId, BinaryContent content, RequestOptions options = null) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNull(content, nameof(content)); - - using PipelineMessage message = CreateModifyRunRequest(threadId, runId, content, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Modifies a run. - /// - /// The ID of the [thread](/docs/api-reference/threads) that was run. - /// The ID of the run to modify. - /// The content to send as the body of the request. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult ModifyRun(string threadId, string runId, BinaryContent content, RequestOptions options = null) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNull(content, nameof(content)); - - using PipelineMessage message = CreateModifyRunRequest(threadId, runId, content, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - - /// - /// [Protocol Method] Cancels a run that is `in_progress`. - /// - /// The ID of the thread to which this run belongs. - /// The ID of the run to cancel. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task CancelRunAsync(string threadId, string runId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateCancelRunRequest(threadId, runId, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Cancels a run that is `in_progress`. - /// - /// The ID of the thread to which this run belongs. - /// The ID of the run to cancel. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult CancelRun(string threadId, string runId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateCancelRunRequest(threadId, runId, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - - /// - /// [Protocol Method] 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](/docs/api-reference/threads) to which this run belongs. - /// The ID of the run that requires the tool output submission. - /// The content to send as the body of the request. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task SubmitToolOutputsToRunAsync(string threadId, string runId, BinaryContent content, RequestOptions options = null) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNull(content, nameof(content)); - - PipelineMessage message = null; - try - { - message = CreateSubmitToolOutputsToRunRequest(threadId, runId, content, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - finally - { - if (options?.BufferResponse != false) - { - message.Dispose(); - } - } - } - - /// - /// [Protocol Method] 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](/docs/api-reference/threads) to which this run belongs. - /// The ID of the run that requires the tool output submission. - /// The content to send as the body of the request. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult SubmitToolOutputsToRun(string threadId, string runId, BinaryContent content, RequestOptions options = null) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNull(content, nameof(content)); - - PipelineMessage message = null; - try - { - message = CreateSubmitToolOutputsToRunRequest(threadId, runId, content, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - finally - { - if (options?.BufferResponse != false) - { - message.Dispose(); - } - } - } - - /// - /// [Protocol Method] Returns a list of run steps belonging to a run. - /// - /// The ID of the thread the run and run steps belong to. - /// The ID of the run the run steps belong to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetRunStepsAsync(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateGetRunStepsRequest(threadId, runId, limit, order, after, before, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] Returns a list of run steps belonging to a run. - /// - /// The ID of the thread the run and run steps belong to. - /// The ID of the run the run steps belong to. - /// - /// 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. `asc` for ascending order and`desc` - /// for descending order. Allowed values: "asc" | "desc" - /// - /// - /// 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. - /// - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// or is null. - /// or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetRunSteps(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - - using PipelineMessage message = CreateGetRunStepsRequest(threadId, runId, limit, order, after, before, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } - - /// - /// [Protocol Method] 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. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// , or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual async Task GetRunStepAsync(string threadId, string runId, string stepId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNullOrEmpty(stepId, nameof(stepId)); - - using PipelineMessage message = CreateGetRunStepRequest(threadId, runId, stepId, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); - } - - /// - /// [Protocol Method] 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. - /// The request options, which can override default behaviors of the client pipeline on a per-call basis. - /// , or is null. - /// , or is an empty string, and was expected to be non-empty. - /// Service returned a non-success status code. - /// The response returned from the service. - public virtual ClientResult GetRunStep(string threadId, string runId, string stepId, RequestOptions options) - { - Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - Argument.AssertNotNullOrEmpty(runId, nameof(runId)); - Argument.AssertNotNullOrEmpty(stepId, nameof(stepId)); - - using PipelineMessage message = CreateGetRunStepRequest(threadId, runId, stepId, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); - } } diff --git a/src/Custom/Assistants/MessageCollectionOptions.cs b/src/Custom/Assistants/MessageCollectionOptions.cs new file mode 100644 index 000000000..213d58419 --- /dev/null +++ b/src/Custom/Assistants/MessageCollectionOptions.cs @@ -0,0 +1,33 @@ +namespace OpenAI.Assistants; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class MessageCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public MessageCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } +} diff --git a/src/Custom/Assistants/RunCollectionOptions.cs b/src/Custom/Assistants/RunCollectionOptions.cs new file mode 100644 index 000000000..d99f1a4b6 --- /dev/null +++ b/src/Custom/Assistants/RunCollectionOptions.cs @@ -0,0 +1,33 @@ +namespace OpenAI.Assistants; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class RunCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public RunCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } +} diff --git a/src/Custom/Assistants/RunStepCollectionOptions.cs b/src/Custom/Assistants/RunStepCollectionOptions.cs new file mode 100644 index 000000000..19ad62937 --- /dev/null +++ b/src/Custom/Assistants/RunStepCollectionOptions.cs @@ -0,0 +1,33 @@ +namespace OpenAI.Assistants; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class RunStepCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public RunStepCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } +} diff --git a/src/Custom/Assistants/Streaming/AsyncStreamingUpdateCollection.cs b/src/Custom/Assistants/Streaming/AsyncStreamingUpdateCollection.cs index c7640f02f..187163a09 100644 --- a/src/Custom/Assistants/Streaming/AsyncStreamingUpdateCollection.cs +++ b/src/Custom/Assistants/Streaming/AsyncStreamingUpdateCollection.cs @@ -15,7 +15,7 @@ namespace OpenAI.Assistants; /// /// Implementation of collection abstraction over streaming assistant updates. /// -internal class AsyncStreamingUpdateCollection : AsyncResultCollection +internal class AsyncStreamingUpdateCollection : AsyncCollectionResult { private readonly Func> _getResultAsync; diff --git a/src/Custom/Assistants/Streaming/StreamingUpdateCollection.cs b/src/Custom/Assistants/Streaming/StreamingUpdateCollection.cs index 85f51273e..954bc2284 100644 --- a/src/Custom/Assistants/Streaming/StreamingUpdateCollection.cs +++ b/src/Custom/Assistants/Streaming/StreamingUpdateCollection.cs @@ -13,7 +13,7 @@ namespace OpenAI.Assistants; /// /// Implementation of collection abstraction over streaming assistant updates. /// -internal class StreamingUpdateCollection : ResultCollection +internal class StreamingUpdateCollection : CollectionResult { private readonly Func _getResult; diff --git a/src/Custom/Chat/ChatClient.cs b/src/Custom/Chat/ChatClient.cs index 0c509b393..72b9a5d7d 100644 --- a/src/Custom/Chat/ChatClient.cs +++ b/src/Custom/Chat/ChatClient.cs @@ -132,7 +132,7 @@ public virtual ClientResult CompleteChat(params ChatMessage[] me /// Additional options for the chat completion request. /// A token that can be used to cancel this method call. /// A streaming result with incremental chat completion updates. - public virtual AsyncResultCollection CompleteChatStreamingAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) + public virtual AsyncCollectionResult CompleteChatStreamingAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNull(messages, nameof(messages)); @@ -156,7 +156,7 @@ async Task getResultAsync() => /// /// The messages to provide as input for chat completion. /// A streaming result with incremental chat completion updates. - public virtual AsyncResultCollection CompleteChatStreamingAsync(params ChatMessage[] messages) + public virtual AsyncCollectionResult CompleteChatStreamingAsync(params ChatMessage[] messages) => CompleteChatStreamingAsync(messages, default(ChatCompletionOptions)); /// @@ -171,7 +171,7 @@ public virtual AsyncResultCollection CompleteChat /// Additional options for the chat completion request. /// A token that can be used to cancel this method call. /// A streaming result with incremental chat completion updates. - public virtual ResultCollection CompleteChatStreaming(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) + public virtual CollectionResult CompleteChatStreaming(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNull(messages, nameof(messages)); @@ -193,7 +193,7 @@ public virtual ResultCollection CompleteChatStrea /// /// The messages to provide as input for chat completion. /// A streaming result with incremental chat completion updates. - public virtual ResultCollection CompleteChatStreaming(params ChatMessage[] messages) + public virtual CollectionResult CompleteChatStreaming(params ChatMessage[] messages) => CompleteChatStreaming(messages, default(ChatCompletionOptions)); private void CreateChatCompletionOptions(IEnumerable messages, ref ChatCompletionOptions options, bool stream = false) diff --git a/src/Custom/Chat/Internal/AsyncStreamingChatCompletionUpdateCollection.cs b/src/Custom/Chat/Internal/AsyncStreamingChatCompletionUpdateCollection.cs index 55bbeb7d8..db3781235 100644 --- a/src/Custom/Chat/Internal/AsyncStreamingChatCompletionUpdateCollection.cs +++ b/src/Custom/Chat/Internal/AsyncStreamingChatCompletionUpdateCollection.cs @@ -15,7 +15,7 @@ namespace OpenAI.Chat; /// /// Implementation of collection abstraction over streaming chat updates. /// -internal class AsyncStreamingChatCompletionUpdateCollection : AsyncResultCollection +internal class AsyncStreamingChatCompletionUpdateCollection : AsyncCollectionResult { private readonly Func> _getResultAsync; diff --git a/src/Custom/Chat/Internal/StreamingChatCompletionUpdateCollection.cs b/src/Custom/Chat/Internal/StreamingChatCompletionUpdateCollection.cs index 0e51009e7..647642c77 100644 --- a/src/Custom/Chat/Internal/StreamingChatCompletionUpdateCollection.cs +++ b/src/Custom/Chat/Internal/StreamingChatCompletionUpdateCollection.cs @@ -14,7 +14,7 @@ namespace OpenAI.Chat; /// /// Implementation of collection abstraction over streaming chat updates. /// -internal class StreamingChatCompletionUpdateCollection : ResultCollection +internal class StreamingChatCompletionUpdateCollection : CollectionResult { private readonly Func _getResult; diff --git a/src/Custom/Common/InternalListHelpers.cs b/src/Custom/Common/InternalListHelpers.cs deleted file mode 100644 index 4537ad780..000000000 --- a/src/Custom/Common/InternalListHelpers.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace OpenAI; - -internal static class InternalListHelpers -{ - internal delegate Task AsyncListResponseFunc(string continuationToken, int? pageSize); - internal delegate ClientResult ListResponseFunc(string continuationToken, int? pageSize); - - internal static AsyncPageableCollection CreateAsyncPageable(AsyncListResponseFunc listResponseFunc) - where U : IJsonModel, IInternalListResponse - { - async Task> pageFunc(string continuationToken, int? pageSize) - => GetPageFromProtocol(await listResponseFunc(continuationToken, pageSize).ConfigureAwait(false)); - return PageableResultHelpers.Create((pageSize) => pageFunc(null, pageSize), pageFunc); - } - - internal static PageableCollection CreatePageable(ListResponseFunc listResponseFunc) - where U : IJsonModel, IInternalListResponse - { - ResultPage pageFunc(string continuationToken, int? pageSize) - => GetPageFromProtocol(listResponseFunc(continuationToken, pageSize)); - return PageableResultHelpers.Create((pageSize) => pageFunc(null, pageSize), pageFunc); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ResultPage GetPageFromProtocol(ClientResult protocolResult) - where UInternalList : IJsonModel, IInternalListResponse - { - PipelineResponse response = protocolResult.GetRawResponse(); - IInternalListResponse values = ModelReaderWriter.Read(response.Content); - return ResultPage.Create(values.Data, values.HasMore ? values.LastId : null, response); - } -} diff --git a/src/Custom/Common/PageableResultHelpers.cs b/src/Custom/Common/PageableResultHelpers.cs deleted file mode 100644 index 23290c932..000000000 --- a/src/Custom/Common/PageableResultHelpers.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Threading.Tasks; - -#nullable enable - -namespace OpenAI; - -internal class PageableResultHelpers -{ - public static PageableCollection Create(Func> firstPageFunc, Func>? nextPageFunc, int? pageSize = default) where T : notnull - { - ResultPage first(string? _, int? pageSizeHint) => firstPageFunc(pageSizeHint); - return new FuncPageable(first, nextPageFunc, pageSize); - } - - public static AsyncPageableCollection Create(Func>> firstPageFunc, Func>>? nextPageFunc, int? pageSize = default) where T : notnull - { - Task> first(string? _, int? pageSizeHint) => firstPageFunc(pageSizeHint); - return new FuncAsyncPageable(first, nextPageFunc, pageSize); - } - - private class FuncAsyncPageable : AsyncPageableCollection where T : notnull - { - private readonly Func>> _firstPageFunc; - private readonly Func>>? _nextPageFunc; - private readonly int? _defaultPageSize; - - public FuncAsyncPageable(Func>> firstPageFunc, Func>>? nextPageFunc, int? defaultPageSize = default) - { - _firstPageFunc = firstPageFunc; - _nextPageFunc = nextPageFunc; - _defaultPageSize = defaultPageSize; - } - - public override async IAsyncEnumerable> AsPages(string? continuationToken = default, int? pageSizeHint = default) - { - Func>>? pageFunc = string.IsNullOrEmpty(continuationToken) ? _firstPageFunc : _nextPageFunc; - - if (pageFunc == null) - { - yield break; - } - - int? pageSize = pageSizeHint ?? _defaultPageSize; - do - { - ResultPage page = await pageFunc(continuationToken, pageSize).ConfigureAwait(false); - SetRawResponse(page.GetRawResponse()); - yield return page; - continuationToken = page.ContinuationToken; - pageFunc = _nextPageFunc; - } - while (!string.IsNullOrEmpty(continuationToken) && pageFunc != null); - } - } - - private class FuncPageable : PageableCollection where T : notnull - { - private readonly Func> _firstPageFunc; - private readonly Func>? _nextPageFunc; - private readonly int? _defaultPageSize; - - public FuncPageable(Func> firstPageFunc, Func>? nextPageFunc, int? defaultPageSize = default) - { - _firstPageFunc = firstPageFunc; - _nextPageFunc = nextPageFunc; - _defaultPageSize = defaultPageSize; - } - - public override IEnumerable> AsPages(string? continuationToken = default, int? pageSizeHint = default) - { - Func>? pageFunc = string.IsNullOrEmpty(continuationToken) ? _firstPageFunc : _nextPageFunc; - - if (pageFunc == null) - { - yield break; - } - - int? pageSize = pageSizeHint ?? _defaultPageSize; - do - { - ResultPage page = pageFunc(continuationToken, pageSize); - SetRawResponse(page.GetRawResponse()); - yield return page; - continuationToken = page.ContinuationToken; - pageFunc = _nextPageFunc; - } - while (!string.IsNullOrEmpty(continuationToken) && pageFunc != null); - } - } -} diff --git a/src/Custom/VectorStores/VectorStoreClient.Convenience.cs b/src/Custom/VectorStores/VectorStoreClient.Convenience.cs index 3917b5171..5330978b4 100644 --- a/src/Custom/VectorStores/VectorStoreClient.Convenience.cs +++ b/src/Custom/VectorStores/VectorStoreClient.Convenience.cs @@ -81,52 +81,38 @@ public virtual ClientResult AddFileToVectorStore(Vec => AddFileToVectorStore(vectorStore?.Id, file?.Id); /// - /// Gets the collection of instances representing file inclusions in the + /// Gets a page collection holding instances that represent file inclusions in the /// specified vector store. /// /// /// The vector store to enumerate the file associations of. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// - /// - /// A collection of instances that can be asynchronously enumerated via - /// await foreach. - /// - public virtual AsyncPageableCollection GetFileAssociationsAsync( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( VectorStore vectorStore, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null) - => GetFileAssociationsAsync(vectorStore?.Id, resultOrder, filter); + VectorStoreFileAssociationCollectionOptions options = default) + => GetFileAssociationsAsync(vectorStore?.Id, options); /// - /// Gets the collection of instances representing file inclusions in the + /// Gets a page collection holding instances that represent file inclusions in the /// specified vector store. /// /// /// The ID vector store to enumerate the file associations of. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// - /// - /// A collection of instances that can be synchronously enumerated via - /// foreach. - /// - public virtual PageableCollection GetFileAssociations( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( VectorStore vectorStore, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null) - => GetFileAssociations(vectorStore?.Id, resultOrder); + VectorStoreFileAssociationCollectionOptions options = default) + => GetFileAssociations(vectorStore?.Id, options); /// /// Gets a instance representing an existing association between a known @@ -229,46 +215,33 @@ public virtual ClientResult CancelBatchFileJob(VectorSt => CancelBatchFileJob(batchJob?.VectorStoreId, batchJob?.BatchId); /// - /// Gets the collection of file associations associated with a vector store batch file job, representing the files + /// Gets a page collection holding file associations associated with a vector store batch file job, representing the files /// that were scheduled for ingestion into the vector store. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// - /// - /// A collection of instances that can be asynchronously enumerated via - /// await foreach. - /// - public virtual AsyncPageableCollection GetFileAssociationsAsync( + /// The vector store batch file job to retrieve file associations from. + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( VectorStoreBatchFileJob batchJob, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null) - => GetFileAssociationsAsync(batchJob?.VectorStoreId, batchJob?.BatchId, resultOrder, filter); + VectorStoreFileAssociationCollectionOptions options = default) + => GetFileAssociationsAsync(batchJob?.VectorStoreId, batchJob?.BatchId, options); /// - /// Gets the collection of file associations associated with a vector store batch file job, representing the files + /// Gets a page collection holding file associations associated with a vector store batch file job, representing the files /// that were scheduled for ingestion into the vector store. /// /// The vector store batch file job to retrieve file associations from. - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// - /// - /// A collection of instances that can be synchronously enumerated via - /// foreach. - /// - public virtual PageableCollection GetFileAssociations( + /// Options describing the collection to return. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( VectorStoreBatchFileJob batchJob, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null) - => GetFileAssociations(batchJob?.VectorStoreId, batchJob?.BatchId, resultOrder, filter); + VectorStoreFileAssociationCollectionOptions options = default) + => GetFileAssociations(batchJob?.VectorStoreId, batchJob?.BatchId, options); } diff --git a/src/Custom/VectorStores/VectorStoreClient.Protocol.cs b/src/Custom/VectorStores/VectorStoreClient.Protocol.cs index 311be206b..c3c9dad7e 100644 --- a/src/Custom/VectorStores/VectorStoreClient.Protocol.cs +++ b/src/Custom/VectorStores/VectorStoreClient.Protocol.cs @@ -1,6 +1,7 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; @@ -25,7 +26,7 @@ namespace OpenAI.VectorStores; public partial class VectorStoreClient { /// - /// [Protocol Method] Returns a list of vector-stores. + /// [Protocol Method] Returns a paginated collection of vector-stores. /// /// /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the @@ -47,16 +48,16 @@ public partial class VectorStoreClient /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual async Task GetVectorStoresAsync(int? limit, string order, string after, string before, RequestOptions options) + public virtual IAsyncEnumerable GetVectorStoresAsync(int? limit, string order, string after, string before, RequestOptions options) { - using PipelineMessage message = CreateGetVectorStoresRequest(limit, order, after, before, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + VectorStoresPageEnumerator enumerator = new VectorStoresPageEnumerator(_pipeline, _endpoint, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// [Protocol Method] Returns a list of vector-stores. + /// [Protocol Method] Returns a paginated collection of vector-stores. /// /// /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the @@ -78,12 +79,12 @@ public virtual async Task GetVectorStoresAsync(int? limit, string /// /// The request options, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual ClientResult GetVectorStores(int? limit, string order, string after, string before, RequestOptions options) + public virtual IEnumerable GetVectorStores(int? limit, string order, string after, string before, RequestOptions options) { - using PipelineMessage message = CreateGetVectorStoresRequest(limit, order, after, before, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + VectorStoresPageEnumerator enumerator = new VectorStoresPageEnumerator(_pipeline, _endpoint, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); } /// @@ -228,7 +229,7 @@ public virtual ClientResult DeleteVectorStore(string vectorStoreId, RequestOptio } /// - /// [Protocol Method] Returns a list of vector store files. + /// [Protocol Method] Returns a paginated collection of vector store files. /// /// The ID of the vector store that the files belong to. /// @@ -254,18 +255,18 @@ public virtual ClientResult DeleteVectorStore(string vectorStoreId, RequestOptio /// is null. /// is an empty string, and was expected to be non-empty. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual async Task GetFileAssociationsAsync(string vectorStoreId, int? limit, string order, string after, string before, string filter, RequestOptions options) + public virtual IAsyncEnumerable GetFileAssociationsAsync(string vectorStoreId, int? limit, string order, string after, string before, string filter, RequestOptions options) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); - using PipelineMessage message = CreateGetVectorStoreFilesRequest(vectorStoreId, limit, order, after, before, filter, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + VectorStoreFilesPageEnumerator enumerator = new VectorStoreFilesPageEnumerator(_pipeline, _endpoint, vectorStoreId, limit, order, after, before, filter, options); + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// [Protocol Method] Returns a list of vector store files. + /// [Protocol Method] Returns a paginated collection of vector store files. /// /// The ID of the vector store that the files belong to. /// @@ -291,14 +292,14 @@ public virtual async Task GetFileAssociationsAsync(string vectorSt /// is null. /// is an empty string, and was expected to be non-empty. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual ClientResult GetFileAssociations(string vectorStoreId, int? limit, string order, string after, string before, string filter, RequestOptions options) + public virtual IEnumerable GetFileAssociations(string vectorStoreId, int? limit, string order, string after, string before, string filter, RequestOptions options) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); - using PipelineMessage message = CreateGetVectorStoreFilesRequest(vectorStoreId, limit, order, after, before, filter, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + VectorStoreFilesPageEnumerator enumerator = new VectorStoreFilesPageEnumerator(_pipeline, _endpoint, vectorStoreId, limit, order, after, before, filter, options); + return PageCollectionHelpers.Create(enumerator); } /// @@ -542,7 +543,7 @@ public virtual ClientResult CancelBatchFileJob(string vectorStoreId, string batc } /// - /// [Protocol Method] Returns a list of vector store files in a batch. + /// [Protocol Method] Returns a paginated collection of vector store files in a batch. /// /// The ID of the vector store that the file batch belongs to. /// The ID of the file batch that the files belong to. @@ -569,19 +570,19 @@ public virtual ClientResult CancelBatchFileJob(string vectorStoreId, string batc /// or is null. /// or is an empty string, and was expected to be non-empty. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual async Task GetFileAssociationsAsync(string vectorStoreId, string batchId, int? limit, string order, string after, string before, string filter, RequestOptions options) + public virtual IAsyncEnumerable GetFileAssociationsAsync(string vectorStoreId, string batchId, int? limit, string order, string after, string before, string filter, RequestOptions options) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); Argument.AssertNotNullOrEmpty(batchId, nameof(batchId)); - using PipelineMessage message = CreateGetFilesInVectorStoreBatchesRequest(vectorStoreId, batchId, limit, order, after, before, filter, options); - return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + VectorStoreFileBatchesPageEnumerator enumerator = new VectorStoreFileBatchesPageEnumerator(_pipeline, _endpoint, vectorStoreId, batchId, limit, order, after, before, filter, options); + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// [Protocol Method] Returns a list of vector store files in a batch. + /// [Protocol Method] Returns a paginated collection of vector store files in a batch. /// /// The ID of the vector store that the file batch belongs to. /// The ID of the file batch that the files belong to. @@ -608,14 +609,14 @@ public virtual async Task GetFileAssociationsAsync(string vectorSt /// or is null. /// or is an empty string, and was expected to be non-empty. /// Service returned a non-success status code. - /// The response returned from the service. + /// A collection of service responses, each holding a page of values. [EditorBrowsable(EditorBrowsableState.Never)] - public virtual ClientResult GetFileAssociations(string vectorStoreId, string batchId, int? limit, string order, string after, string before, string filter, RequestOptions options) + public virtual IEnumerable GetFileAssociations(string vectorStoreId, string batchId, int? limit, string order, string after, string before, string filter, RequestOptions options) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); Argument.AssertNotNullOrEmpty(batchId, nameof(batchId)); - using PipelineMessage message = CreateGetFilesInVectorStoreBatchesRequest(vectorStoreId, batchId, limit, order, after, before, filter, options); - return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + VectorStoreFileBatchesPageEnumerator enumerator = new VectorStoreFileBatchesPageEnumerator(_pipeline, _endpoint, vectorStoreId, batchId, limit, order, after, before, filter, options); + return PageCollectionHelpers.Create(enumerator); } } diff --git a/src/Custom/VectorStores/VectorStoreClient.cs b/src/Custom/VectorStores/VectorStoreClient.cs index e12c35737..0f7f46af6 100644 --- a/src/Custom/VectorStores/VectorStoreClient.cs +++ b/src/Custom/VectorStores/VectorStoreClient.cs @@ -1,14 +1,11 @@ -using OpenAI.Assistants; -using OpenAI.Files; +using OpenAI.Files; using System; using System.ClientModel; using System.ClientModel.Primitives; -using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -using static OpenAI.InternalListHelpers; namespace OpenAI.VectorStores; @@ -52,7 +49,7 @@ public VectorStoreClient(ApiKeyCredential credential, OpenAIClientOptions option OpenAIClient.CreatePipeline(OpenAIClient.GetApiKey(credential, requireExplicitCredential: true), options), OpenAIClient.GetEndpoint(options), options) - {} + { } /// /// Initializes a new instance of that will use an API key from the OPENAI_API_KEY @@ -69,7 +66,7 @@ public VectorStoreClient(OpenAIClientOptions options = null) OpenAIClient.CreatePipeline(OpenAIClient.GetApiKey(), options), OpenAIClient.GetEndpoint(options), options) - {} + { } /// Initializes a new instance of VectorStoreClient. /// The HTTP pipeline for sending and receiving REST requests and responses. @@ -137,38 +134,101 @@ public virtual ClientResult DeleteVectorStore(string vectorStoreId, Cancel } /// - /// Gets the collection of instances for the configured organization. + /// Gets a page collection holding instances for the configured organization. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be asynchronously enumerated via - /// await foreach. - /// - public virtual AsyncPageableCollection GetVectorStoresAsync(ListOrder? resultOrder = null, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetVectorStoresAsync( + VectorStoreCollectionOptions options = default, + CancellationToken cancellationToken = default) { - return CreateAsyncPageable((continuationToken, pageSize) - => GetVectorStoresAsync(pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + VectorStoresPageEnumerator enumerator = new(_pipeline, _endpoint, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Gets the collection of instances for the configured organization. + /// Rehydrates a page collection holding instances from a page token. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// + /// Page token corresponding to the first page of the collection to rehydrate. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be synchronously enumerated via foreach. - /// - public virtual PageableCollection GetVectorStores(ListOrder? resultOrder = null, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetVectorStoresAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoresPageToken pageToken = VectorStoresPageToken.FromToken(firstPageToken); + VectorStoresPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection holding instances for the configured organization. + /// + /// Options describing the collection to return. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetVectorStores( + VectorStoreCollectionOptions options = default, + CancellationToken cancellationToken = default) + { + VectorStoresPageEnumerator enumerator = new(_pipeline, _endpoint, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetVectorStores( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) { - return CreatePageable((continuationToken, pageSize) - => GetVectorStores(pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoresPageToken pageToken = VectorStoresPageToken.FromToken(firstPageToken); + VectorStoresPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } /// @@ -208,61 +268,123 @@ public virtual ClientResult AddFileToVectorStore(str } /// - /// Gets the collection of instances representing file inclusions in the + /// Gets a page collection holding instances that represent file inclusions in the /// specified vector store. /// /// /// The ID of the vector store to enumerate the file associations of. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be asynchronously enumerated via - /// await foreach. - /// - public virtual AsyncPageableCollection GetFileAssociationsAsync( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( string vectorStoreId, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null, + VectorStoreFileAssociationCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); - return CreateAsyncPageable( - (continuationToken, pageSize) => GetFileAssociationsAsync( - vectorStoreId, pageSize, resultOrder?.ToString(), continuationToken, null, filter?.ToString(), cancellationToken.ToRequestOptions())); + + VectorStoreFilesPageEnumerator enumerator = new(_pipeline, _endpoint, + vectorStoreId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + options?.Filter?.ToString(), + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Gets the collection of instances representing file inclusions in the + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoreFilesPageToken pageToken = VectorStoreFilesPageToken.FromToken(firstPageToken); + VectorStoreFilesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.VectorStoreId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + pageToken.Filter, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection holding instances that represent file inclusions in the /// specified vector store. /// /// /// The ID of the vector store to enumerate the file associations of. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be synchronously enumerated via - /// foreach. - /// - public virtual PageableCollection GetFileAssociations(string vectorStoreId, ListOrder? resultOrder = null, VectorStoreFileStatusFilter? filter = null, CancellationToken cancellationToken = default) + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( + string vectorStoreId, + VectorStoreFileAssociationCollectionOptions options = default, + CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); - return CreatePageable( - (continuationToken, pageSize) => GetFileAssociations( - vectorStoreId, pageSize, resultOrder?.ToString(), continuationToken, null, filter?.ToString(), cancellationToken.ToRequestOptions())); + + VectorStoreFilesPageEnumerator enumerator = new(_pipeline, _endpoint, + vectorStoreId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + options?.Filter?.ToString(), + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoreFilesPageToken pageToken = VectorStoreFilesPageToken.FromToken(firstPageToken); + VectorStoreFilesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.VectorStoreId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + pageToken.Filter, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } /// @@ -275,7 +397,7 @@ public virtual PageableCollection GetFileAssociation /// A instance. public virtual async Task> GetFileAssociationAsync( string vectorStoreId, - string fileId, + string fileId, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); @@ -297,7 +419,7 @@ public virtual async Task> GetFileAssoc /// A instance. public virtual ClientResult GetFileAssociation( string vectorStoreId, - string fileId, + string fileId, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); @@ -466,7 +588,7 @@ public virtual ClientResult CancelBatchFileJob(string v } /// - /// Gets the collection of file associations associated with a vector store batch file job, representing the files + /// Gets a page collection of file associations associated with a vector store batch file job, representing the files /// that were scheduled for ingestion into the vector store. /// /// @@ -475,36 +597,36 @@ public virtual ClientResult CancelBatchFileJob(string v /// /// The ID of the batch file job that was previously scheduled. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. - /// - /// - /// A status filter that file associations must match to be included in the collection. - /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be asynchronously enumerated via - /// await foreach. - /// - public virtual AsyncPageableCollection GetFileAssociationsAsync( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( string vectorStoreId, string batchJobId, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null, + VectorStoreFileAssociationCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); Argument.AssertNotNullOrEmpty(batchJobId, nameof(batchJobId)); - return CreateAsyncPageable( - (continuationToken, pageSize) => GetFileAssociationsAsync - (vectorStoreId, batchJobId, pageSize, resultOrder?.ToString(), continuationToken, null, filter?.ToString(), cancellationToken.ToRequestOptions())); + VectorStoreFileBatchesPageEnumerator enumerator = new(_pipeline, _endpoint, + vectorStoreId, + batchJobId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + options?.Filter?.ToString(), + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); } /// - /// Gets the collection of file associations associated with a vector store batch file job, representing the files - /// that were scheduled for ingestion into the vector store. + /// Rehydrates a page collection of file associations from a page token. /// /// /// The ID of the vector store into which the file batch was scheduled for ingestion. @@ -512,30 +634,137 @@ public virtual AsyncPageableCollection GetFileAssoci /// /// The ID of the batch file job that was previously scheduled. /// - /// - /// The order that results should appear in the list according to their created_at - /// timestamp. + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetFileAssociationsAsync( + string vectorStoreId, + string batchJobId, + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoreFileBatchesPageToken pageToken = VectorStoreFileBatchesPageToken.FromToken(firstPageToken); + + if (vectorStoreId != pageToken.VectorStoreId) + { + throw new ArgumentException( + "Invalid page token. 'vectorStoreId' value does not match page token value.", + nameof(vectorStoreId)); + } + + if (batchJobId != pageToken.BatchId) + { + throw new ArgumentException( + "Invalid page token. 'batchJobId' value does not match page token value.", + nameof(vectorStoreId)); + } + + VectorStoreFileBatchesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.VectorStoreId, + pageToken.BatchId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + pageToken.Filter, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection of file associations associated with a vector store batch file job, representing the files + /// that were scheduled for ingestion into the vector store. + /// + /// + /// The ID of the vector store into which the file batch was scheduled for ingestion. /// - /// - /// A status filter that file associations must match to be included in the collection. + /// + /// The ID of the batch file job that was previously scheduled. /// + /// Options describing the collection to return. /// A token that can be used to cancel this method call. - /// - /// A collection of instances that can be synchronously enumerated via - /// foreach. - /// - public virtual PageableCollection GetFileAssociations( + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( string vectorStoreId, string batchJobId, - ListOrder? resultOrder = null, - VectorStoreFileStatusFilter? filter = null, + VectorStoreFileAssociationCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); Argument.AssertNotNullOrEmpty(batchJobId, nameof(batchJobId)); - return CreatePageable( - (continuationToken, pageSize) => GetFileAssociations - (vectorStoreId, batchJobId, pageSize, resultOrder?.ToString(), continuationToken, null, filter?.ToString(), cancellationToken.ToRequestOptions())); + VectorStoreFileBatchesPageEnumerator enumerator = new(_pipeline, _endpoint, + vectorStoreId, + batchJobId, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + options?.Filter?.ToString(), + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Rehydrates a page collection of file associations from a page token. + /// that were scheduled for ingestion into the vector store. + /// + /// + /// The ID of the vector store into which the file batch was scheduled for ingestion. + /// + /// + /// The ID of the batch file job that was previously scheduled. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetFileAssociations( + string vectorStoreId, + string batchJobId, + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + VectorStoreFileBatchesPageToken pageToken = VectorStoreFileBatchesPageToken.FromToken(firstPageToken); + + if (vectorStoreId != pageToken.VectorStoreId) + { + throw new ArgumentException( + "Invalid page token. 'vectorStoreId' value does not match page token value.", + nameof(vectorStoreId)); + } + + if (batchJobId != pageToken.BatchId) + { + throw new ArgumentException( + "Invalid page token. 'batchJobId' value does not match page token value.", + nameof(vectorStoreId)); + } + + VectorStoreFileBatchesPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.VectorStoreId, + pageToken.BatchId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + pageToken.Filter, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); } } diff --git a/src/Custom/VectorStores/VectorStoreCollectionOptions.cs b/src/Custom/VectorStores/VectorStoreCollectionOptions.cs new file mode 100644 index 000000000..de0834bd3 --- /dev/null +++ b/src/Custom/VectorStores/VectorStoreCollectionOptions.cs @@ -0,0 +1,33 @@ +namespace OpenAI.VectorStores; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class VectorStoreCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public VectorStoreCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } +} diff --git a/src/Custom/VectorStores/VectorStoreFileAssociationCollectionOptions.cs b/src/Custom/VectorStores/VectorStoreFileAssociationCollectionOptions.cs new file mode 100644 index 000000000..cf1788a72 --- /dev/null +++ b/src/Custom/VectorStores/VectorStoreFileAssociationCollectionOptions.cs @@ -0,0 +1,38 @@ +namespace OpenAI.VectorStores; + +/// +/// Represents addition options available when requesting a collection of instances. +/// +public class VectorStoreFileAssociationCollectionOptions +{ + /// + /// Creates a new instance of . + /// + public VectorStoreFileAssociationCollectionOptions() { } + + /// + /// The order that results should appear in the list according to + /// their created_at timestamp. + /// + public ListOrder? Order { get; init; } + + /// + /// The number of values to return in a page result. + /// + public int? PageSize { get; init; } + + /// + /// The id of the item preceeding the first item in the collection. + /// + public string AfterId { get; init; } + + /// + /// The id of the item following the last item in the collection. + /// + public string BeforeId { get; init; } + + /// + /// A status filter that file associations must match to be included in the collection. + /// + public VectorStoreFileStatusFilter? Filter { get; init; } +} diff --git a/src/OpenAI.csproj b/src/OpenAI.csproj index 056929393..fe5158395 100644 --- a/src/OpenAI.csproj +++ b/src/OpenAI.csproj @@ -71,6 +71,6 @@ - + diff --git a/src/To.Be.Generated/AssistantsPageEnumerator.cs b/src/To.Be.Generated/AssistantsPageEnumerator.cs new file mode 100644 index 000000000..15dbcc2dc --- /dev/null +++ b/src/To.Be.Generated/AssistantsPageEnumerator.cs @@ -0,0 +1,133 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +internal partial class AssistantsPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly int? _limit; + private readonly string _order; + + private string _after; + + private readonly string _before; + private readonly RequestOptions _options; + + public AssistantsPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + int? limit, string order, string after, string before, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _limit = limit; + _order = order; + _after = after; + _before = before; + _options = options; + } + + public override async Task GetFirstAsync() + => await GetAssistantsAsync(_limit, _order, _after, _before, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetAssistants(_limit, _order, _after, _before, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetAssistantsAsync(_limit, _order, _after, _before, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetAssistants(_limit, _order, _after, _before, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListAssistantsResponse list = ModelReaderWriter.Read(response.Content)!; + + AssistantsPageToken pageToken = AssistantsPageToken.FromOptions(_limit, _order, _after, _before); + AssistantsPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal virtual async Task GetAssistantsAsync(int? limit, string order, string after, string before, RequestOptions options) + { + using PipelineMessage message = CreateGetAssistantsRequest(limit, order, after, before, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetAssistants(int? limit, string order, string after, string before, RequestOptions options) + { + using PipelineMessage message = CreateGetAssistantsRequest(limit, order, after, before, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + private PipelineMessage CreateGetAssistantsRequest(int? limit, string order, string after, string before, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/assistants", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/AssistantsPageToken.cs b/src/To.Be.Generated/AssistantsPageToken.cs new file mode 100644 index 000000000..251e8a421 --- /dev/null +++ b/src/To.Be.Generated/AssistantsPageToken.cs @@ -0,0 +1,142 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class AssistantsPageToken : ContinuationToken +{ + protected AssistantsPageToken(int? limit, string? order, string? after, string? before) + { + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public AssistantsPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new AssistantsPageToken(Limit, Order, After, Before); + } + + public static AssistantsPageToken FromToken(ContinuationToken token) + { + if (token is AssistantsPageToken pageToken) + { + return pageToken; + } + + BinaryData data = token.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create AssistantsPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + return new(limit, order, after, before); + } + + public static AssistantsPageToken FromOptions(int? limit, string? order, string? after, string? before) + => new AssistantsPageToken(limit, order, after, before); +} \ No newline at end of file diff --git a/src/To.Be.Generated/Internal/ContinuableAsyncEnumerator.cs b/src/To.Be.Generated/Internal/ContinuableAsyncEnumerator.cs new file mode 100644 index 000000000..0ebad037b --- /dev/null +++ b/src/To.Be.Generated/Internal/ContinuableAsyncEnumerator.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal class ContinuableAsyncEnumerator : IAsyncEnumerator +{ + private readonly Func?> _getNextEnumeratorAsync; + + private IAsyncEnumerator? _enumerator; + private T? _current; + + public ContinuableAsyncEnumerator(Func?> getNextEnumeratorAsync) + { + _getNextEnumeratorAsync = getNextEnumeratorAsync; + } + + public T Current => _current!; + + public async ValueTask MoveNextAsync() + { + if (_enumerator == null) + { + _enumerator = _getNextEnumeratorAsync(); + + if (_enumerator == null) + { + return false; + } + } + + if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + _enumerator = _getNextEnumeratorAsync(); + + if (_enumerator == null || + !await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + return false; + } + } + + _current = _enumerator.Current; + return true; + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + private async ValueTask DisposeAsyncCore() + { + if (_enumerator is not null) + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + _enumerator = null; + } + } + + //private readonly Func?>> _getNextEnumeratorAsync; + + //private IAsyncEnumerator? _enumerator; + //private T? _current; + + //public ContinuableAsyncEnumerator(Func?>> getNextEnumeratorAsync) + //{ + // _getNextEnumeratorAsync = getNextEnumeratorAsync; + //} + + //public T Current => _current!; + + //public async ValueTask MoveNextAsync() + //{ + // if (_enumerator == null) + // { + // _enumerator = await _getNextEnumeratorAsync().ConfigureAwait(false); + + // if (_enumerator == null) + // { + // return false; + // } + // } + + // if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + // { + // _enumerator = await _getNextEnumeratorAsync().ConfigureAwait(false); + + // if (_enumerator == null || + // !await _enumerator.MoveNextAsync().ConfigureAwait(false)) + // { + // return false; + // } + // } + + // _current = _enumerator.Current; + // return true; + //} + + //public async ValueTask DisposeAsync() + //{ + // await DisposeAsyncCore().ConfigureAwait(false); + + // GC.SuppressFinalize(this); + //} + + //private async ValueTask DisposeAsyncCore() + //{ + // if (_enumerator is not null) + // { + // await _enumerator.DisposeAsync().ConfigureAwait(false); + // _enumerator = null; + // } + //} +} diff --git a/src/To.Be.Generated/Internal/ContinuableEnumerator.cs b/src/To.Be.Generated/Internal/ContinuableEnumerator.cs new file mode 100644 index 000000000..fe6224d9f --- /dev/null +++ b/src/To.Be.Generated/Internal/ContinuableEnumerator.cs @@ -0,0 +1,31 @@ +using System.ClientModel; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal class ContinuableEnumerator : IEnumerator +{ + public T Current => throw new System.NotImplementedException(); + + object IEnumerator.Current => throw new System.NotImplementedException(); + + public void Dispose() + { + throw new System.NotImplementedException(); + } + + public bool MoveNext() + { + throw new System.NotImplementedException(); + } + + public void Reset() + { + throw new System.NotImplementedException(); + } +} diff --git a/src/To.Be.Generated/Internal/OperationPoller.cs b/src/To.Be.Generated/Internal/OperationPoller.cs new file mode 100644 index 000000000..5aa826445 --- /dev/null +++ b/src/To.Be.Generated/Internal/OperationPoller.cs @@ -0,0 +1,23 @@ +//using System.ClientModel; + +//#nullable enable + +//namespace OpenAI; + +//// Convenience version +//// Note: Right now, this just inherits from the protocol poller and adds a T. +//// Do we need more than that? +//internal abstract class OperationPoller : OperationResultPoller +//{ +// private T? _value; + +// protected OperationPoller(ClientResult current) : base(current) +// { +// } + +// public T Value => _value ??= GetValueFromResult(Current); + +// public abstract T GetValueFromResult(ClientResult result); + +// protected override void Update() => _value = GetValueFromResult(Current); +//} diff --git a/src/To.Be.Generated/Internal/OperationResultPoller.cs b/src/To.Be.Generated/Internal/OperationResultPoller.cs new file mode 100644 index 000000000..98b22a059 --- /dev/null +++ b/src/To.Be.Generated/Internal/OperationResultPoller.cs @@ -0,0 +1,66 @@ +using System.ClientModel; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +// Protocol version +// Note: idea here is that this type is generated to be specific to the client. +// Like PageResultEnumerator, the subtype will be generated and wrapped in an +// outer public type. +internal abstract class OperationResultPoller +{ + private const int DefaultWaitMilliseconds = 1000; + + protected OperationResultPoller(ClientResult current) + { + Current = current; + } + + // TODO: Thread-safe assignment? + public ClientResult Current { get; protected set; } + + // Service-specific methods to be generated on the subclient + public abstract Task UpdateStatusAsync(); + + public abstract ClientResult UpdateStatus(); + + public abstract bool HasStopped(ClientResult result); + + // TODO: how does RequestOptions/CancellationToken work? + public async Task WaitForCompletionAsync() + { + bool hasStopped = HasStopped(Current); + + while (!hasStopped) + { + // TODO: implement an interesting wait routine + await Task.Delay(DefaultWaitMilliseconds); + + Current = await UpdateStatusAsync().ConfigureAwait(false); + Update(); + + hasStopped = HasStopped(Current); + } + } + + public void WaitForCompletion() + { + bool hasStopped = HasStopped(Current); + + while (!hasStopped) + { + // TODO: implement an interesting wait routine + Thread.Sleep(DefaultWaitMilliseconds); + + Current = UpdateStatus(); + Update(); + + hasStopped = HasStopped(Current); + } + } + + protected virtual void Update() { } +} \ No newline at end of file diff --git a/src/To.Be.Generated/Internal/PageCollectionHelpers.cs b/src/To.Be.Generated/Internal/PageCollectionHelpers.cs new file mode 100644 index 000000000..b5be39a6e --- /dev/null +++ b/src/To.Be.Generated/Internal/PageCollectionHelpers.cs @@ -0,0 +1,65 @@ +using System.ClientModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal class PageCollectionHelpers +{ + public static PageCollection Create(PageEnumerator enumerator) + => new EnumeratorPageCollection(enumerator); + + public static AsyncPageCollection CreateAsync(PageEnumerator enumerator) + => new AsyncEnumeratorPageCollection(enumerator); + + public static IEnumerable Create(PageResultEnumerator enumerator) + { + while (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + } + + public static async IAsyncEnumerable CreateAsync(PageResultEnumerator enumerator) + { + while (await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + yield return enumerator.Current; + } + } + + private class EnumeratorPageCollection : PageCollection + { + private readonly PageEnumerator _enumerator; + + public EnumeratorPageCollection(PageEnumerator enumerator) + { + _enumerator = enumerator; + } + + protected override PageResult GetCurrentPageCore() + => _enumerator.GetCurrentPage(); + + protected override IEnumerator> GetEnumeratorCore() + => _enumerator; + } + + private class AsyncEnumeratorPageCollection : AsyncPageCollection + { + private readonly PageEnumerator _enumerator; + + public AsyncEnumeratorPageCollection(PageEnumerator enumerator) + { + _enumerator = enumerator; + } + + protected override async Task> GetCurrentPageAsyncCore() + => await _enumerator.GetCurrentPageAsync().ConfigureAwait(false); + + protected override IAsyncEnumerator> GetAsyncEnumeratorCore(CancellationToken cancellationToken = default) + => _enumerator; + } +} diff --git a/src/To.Be.Generated/Internal/PageEnumerator.cs b/src/To.Be.Generated/Internal/PageEnumerator.cs new file mode 100644 index 000000000..6bf35810e --- /dev/null +++ b/src/To.Be.Generated/Internal/PageEnumerator.cs @@ -0,0 +1,60 @@ +using System.ClientModel; +using System.Collections.Generic; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal abstract class PageEnumerator : PageResultEnumerator, + IAsyncEnumerator>, + IEnumerator> +{ + public abstract PageResult GetPageFromResult(ClientResult result); + + public PageResult GetCurrentPage() + { + if (Current is null) + { + return GetPageFromResult(GetFirst()); + } + + return ((IEnumerator>)this).Current; + } + + public async Task> GetCurrentPageAsync() + { + if (Current is null) + { + return GetPageFromResult(await GetFirstAsync().ConfigureAwait(false)); + } + + return ((IEnumerator>)this).Current; + } + + PageResult IEnumerator>.Current + { + get + { + if (Current is null) + { + return default!; + } + + return GetPageFromResult(Current); + } + } + + PageResult IAsyncEnumerator>.Current + { + get + { + if (Current is null) + { + return default!; + } + + return GetPageFromResult(Current); + } + } +} diff --git a/src/To.Be.Generated/Internal/PageResultEnumerator.cs b/src/To.Be.Generated/Internal/PageResultEnumerator.cs new file mode 100644 index 000000000..27629b171 --- /dev/null +++ b/src/To.Be.Generated/Internal/PageResultEnumerator.cs @@ -0,0 +1,75 @@ +using System; +using System.ClientModel; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal abstract class PageResultEnumerator : IAsyncEnumerator, IEnumerator +{ + private ClientResult? _current; + private bool _hasNext = true; + + public ClientResult Current => _current!; + + public abstract Task GetFirstAsync(); + + public abstract ClientResult GetFirst(); + + public abstract Task GetNextAsync(ClientResult result); + + public abstract ClientResult GetNext(ClientResult result); + + public abstract bool HasNext(ClientResult result); + + object IEnumerator.Current => ((IEnumerator)this).Current; + + public bool MoveNext() + { + if (!_hasNext) + { + return false; + } + + if (_current == null) + { + _current = GetFirst(); + } + else + { + _current = GetNext(_current); + } + + _hasNext = HasNext(_current); + return true; + } + + void IEnumerator.Reset() => _current = null; + + void IDisposable.Dispose() { } + + public async ValueTask MoveNextAsync() + { + if (!_hasNext) + { + return false; + } + + if (_current == null) + { + _current = await GetFirstAsync().ConfigureAwait(false); + } + else + { + _current = await GetNextAsync(_current).ConfigureAwait(false); + } + + _hasNext = HasNext(_current); + return true; + } + + ValueTask IAsyncDisposable.DisposeAsync() => default; +} \ No newline at end of file diff --git a/src/To.Be.Generated/Internal/PollingInterval.cs b/src/To.Be.Generated/Internal/PollingInterval.cs new file mode 100644 index 000000000..1812daafe --- /dev/null +++ b/src/To.Be.Generated/Internal/PollingInterval.cs @@ -0,0 +1,31 @@ +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal class PollingInterval +{ + private const int DefaultWaitMilliseconds = 1000; + + private readonly TimeSpan _interval; + + public PollingInterval(TimeSpan? interval = default) + { + _interval = interval ?? new TimeSpan(DefaultWaitMilliseconds); + } + + public async Task WaitAsync() + { + await Task.Delay(_interval); + } + + public void Wait() + { + Thread.Sleep(_interval); + } +} diff --git a/src/To.Be.Generated/MessagesPageEnumerator.cs b/src/To.Be.Generated/MessagesPageEnumerator.cs new file mode 100644 index 000000000..a4a262c33 --- /dev/null +++ b/src/To.Be.Generated/MessagesPageEnumerator.cs @@ -0,0 +1,143 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +internal partial class MessagesPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly string _threadId; + private readonly int? _limit; + private readonly string _order; + + private string _after; + + private readonly string _before; + private readonly RequestOptions _options; + + public MessagesPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + string threadId, + int? limit, string order, string after, string before, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _threadId = threadId; + _limit = limit; + _order = order; + _after = after; + _before = before; + + _options = options; + } + + public override async Task GetFirstAsync() + => await GetMessagesAsync(_threadId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetMessages(_threadId, _limit, _order, _after, _before, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetMessagesAsync(_threadId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetMessages(_threadId, _limit, _order, _after, _before, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListMessagesResponse list = ModelReaderWriter.Read(response.Content)!; + + MessagesPageToken pageToken = MessagesPageToken.FromOptions(_threadId, _limit, _order, _after, _before); + MessagesPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal virtual async Task GetMessagesAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + using PipelineMessage message = CreateGetMessagesRequest(threadId, limit, order, after, before, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetMessages(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + using PipelineMessage message = CreateGetMessagesRequest(threadId, limit, order, after, before, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + private PipelineMessage CreateGetMessagesRequest(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/messages", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/MessagesPageToken.cs b/src/To.Be.Generated/MessagesPageToken.cs new file mode 100644 index 000000000..b11bca627 --- /dev/null +++ b/src/To.Be.Generated/MessagesPageToken.cs @@ -0,0 +1,158 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class MessagesPageToken : ContinuationToken +{ + protected MessagesPageToken(string threadId, int? limit, string? order, string? after, string? before) + { + ThreadId = threadId; + + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public string ThreadId { get; } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + writer.WriteString("threadId", ThreadId); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public MessagesPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new(ThreadId, Limit, Order, lastId, Before); + } + + public static MessagesPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is MessagesPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create MessagesPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + string threadId = null!; + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "threadId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + threadId = reader.GetString()!; + break; + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (threadId is null) + { + throw new ArgumentException("Failed to create MessagesPageToken from provided pageToken.", nameof(pageToken)); + } + + return new(threadId, limit, order, after, before); + } + + public static MessagesPageToken FromOptions(string threadId, int? limit, string? order, string? after, string? before) + => new(threadId, limit, order, after, before); +} \ No newline at end of file diff --git a/src/To.Be.Generated/RunStepsPageEnumerator.cs b/src/To.Be.Generated/RunStepsPageEnumerator.cs new file mode 100644 index 000000000..77660e761 --- /dev/null +++ b/src/To.Be.Generated/RunStepsPageEnumerator.cs @@ -0,0 +1,149 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +internal partial class RunStepsPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly string _threadId; + private readonly string _runId; + + private readonly int? _limit; + private readonly string? _order; + private readonly string? _before; + private readonly RequestOptions _options; + + private string? _after; + + public RunStepsPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + string threadId, string runId, + int? limit, string? order, string? after, string? before, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _threadId = threadId; + _runId = runId; + + _limit = limit; + _order = order; + _after = after; + _before = before; + _options = options; + } + + public override async Task GetFirstAsync() + => await GetRunStepsAsync(_threadId, _runId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetRunSteps(_threadId, _runId, _limit, _order, _after, _before, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetRunStepsAsync(_threadId, _runId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetRunSteps(_threadId, _runId, _limit, _order, _after, _before, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListRunStepsResponse list = ModelReaderWriter.Read(response.Content)!; + + RunStepsPageToken pageToken = RunStepsPageToken.FromOptions(_threadId, _runId, _limit, _order, _after, _before); + RunStepsPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal async virtual Task GetRunStepsAsync(string threadId, string runId, int? limit, string? order, string? after, string? before, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateGetRunStepsRequest(threadId, runId, limit, order, after, before, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetRunSteps(string threadId, string runId, int? limit, string? order, string? after, string? before, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateGetRunStepsRequest(threadId, runId, limit, order, after, before, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + private PipelineMessage CreateGetRunStepsRequest(string threadId, string runId, int? limit, string? order, string? after, string? before, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + uri.AppendPath("/steps", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/RunStepsPageToken.cs b/src/To.Be.Generated/RunStepsPageToken.cs new file mode 100644 index 000000000..65f522815 --- /dev/null +++ b/src/To.Be.Generated/RunStepsPageToken.cs @@ -0,0 +1,168 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class RunStepsPageToken : ContinuationToken +{ + protected RunStepsPageToken(string threadId, string runId, int? limit, string? order, string? after, string? before) + { + ThreadId = threadId; + RunId = runId; + + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public string ThreadId { get; } + + public string RunId { get; } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + writer.WriteString("threadId", ThreadId); + writer.WriteString("runId", RunId); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public RunStepsPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new RunStepsPageToken(ThreadId, RunId, Limit, Order, After, Before); + } + + public static RunStepsPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is RunStepsPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create RunStepsPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + string threadId = null!; + string runId = null!; + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "threadId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + threadId = reader.GetString()!; + break; + case "runId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + runId = reader.GetString()!; + break; + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (threadId is null || runId is null) + { + throw new ArgumentException("Failed to create RunStepsPageToken from provided pageToken.", nameof(pageToken)); + } + + return new(threadId, runId, limit, order, after, before); + } + + public static RunStepsPageToken FromOptions(string threadId, string runId, int? limit, string? order, string? after, string? before) + => new RunStepsPageToken(threadId, runId, limit, order, after, before); +} \ No newline at end of file diff --git a/src/To.Be.Generated/RunsPageEnumerator.cs b/src/To.Be.Generated/RunsPageEnumerator.cs new file mode 100644 index 000000000..3f9ee113f --- /dev/null +++ b/src/To.Be.Generated/RunsPageEnumerator.cs @@ -0,0 +1,141 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +internal partial class RunsPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly string _threadId; + private readonly int? _limit; + private readonly string _order; + + private string _after; + + private readonly string _before; + private readonly RequestOptions _options; + + public RunsPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + string threadId, int? limit, string order, string after, string before, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _threadId = threadId; + _limit = limit; + _order = order; + _after = after; + _before = before; + _options = options; + } + + public override async Task GetFirstAsync() + => await GetRunsAsync(_threadId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetRuns(_threadId, _limit, _order, _after, _before, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetRunsAsync(_threadId, _limit, _order, _after, _before, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetRuns(_threadId, _limit, _order, _after, _before, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListRunsResponse list = ModelReaderWriter.Read(response.Content)!; + + RunsPageToken pageToken = RunsPageToken.FromOptions(_threadId, _limit, _order, _after, _before); + RunsPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal async virtual Task GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + using PipelineMessage message = CreateGetRunsRequest(threadId, limit, order, after, before, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + using PipelineMessage message = CreateGetRunsRequest(threadId, limit, order, after, before, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + private PipelineMessage CreateGetRunsRequest(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/RunsPageToken.cs b/src/To.Be.Generated/RunsPageToken.cs new file mode 100644 index 000000000..e019fe5d4 --- /dev/null +++ b/src/To.Be.Generated/RunsPageToken.cs @@ -0,0 +1,158 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class RunsPageToken : ContinuationToken +{ + protected RunsPageToken(string threadId, int? limit, string? order, string? after, string? before) + { + ThreadId = threadId; + + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public string ThreadId { get; } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + writer.WriteString("threadId", ThreadId); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public RunsPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new RunsPageToken(ThreadId, Limit, Order, After, Before); + } + + public static RunsPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is RunsPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create RunsPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + string threadId = null!; + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "threadId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + threadId = reader.GetString()!; + break; + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (threadId is null) + { + throw new ArgumentException("Failed to create RunsPageToken from provided pageToken.", nameof(pageToken)); + } + + return new(threadId, limit, order, after, before); + } + + public static RunsPageToken FromOptions(string threadId, int? limit, string? order, string? after, string? before) + => new RunsPageToken(threadId, limit, order, after, before); +} \ No newline at end of file diff --git a/src/To.Be.Generated/StreamingThreadRunOperation.cs b/src/To.Be.Generated/StreamingThreadRunOperation.cs new file mode 100644 index 000000000..f4552df0f --- /dev/null +++ b/src/To.Be.Generated/StreamingThreadRunOperation.cs @@ -0,0 +1,257 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +// Streaming version +public partial class StreamingThreadRunOperation : ThreadRunOperation +{ + private readonly Func> _createRunAsync; + private readonly Func _createRun; + + // TODO: don't have this field in two places. + private bool _isCompleted; + + private AsyncStreamingUpdateCollection? _currUpdateCollectionAsync; + + private ContinuableAsyncEnumerator _updateEnumeratorAsync; + + internal StreamingThreadRunOperation( + ClientPipeline pipeline, + Uri endpoint, + + // Note if we pass funcs we don't need to pass in the pipeline. + Func> createRunAsync, + Func createRun) + : base(pipeline, endpoint) + { + _createRunAsync = createRunAsync; + _createRun = createRun; + + _updateEnumeratorAsync = new(GetAsyncUpdateEnumerator); + _currUpdateCollectionAsync ??= new AsyncStreamingUpdateCollection(_createRunAsync); + } + + public override bool IsCompleted + { + get => _isCompleted; + protected set => _isCompleted = value; + } + + public override async Task WaitAsync(CancellationToken cancellationToken = default) + { + // TODO: add validation that stream is only requested and enumerated once. + // TODO: Make sure you can't create the same run twice and/or submit tools twice + // somehow, even accidentally. + + while (await UpdateAsync(cancellationToken).ConfigureAwait(false)) + { + // TODO: only have this in one place. Here or UpdateStatus? + cancellationToken.ThrowIfCancellationRequested(); + } + // TODO: Dispose enumerator + } + + public override void Wait(CancellationToken cancellationToken = default) + { + // Create an instance of an IAsyncEnumerable + StreamingUpdateCollection updates = new StreamingUpdateCollection(_createRun); + + // Enumerate those updates and update the state for each one + foreach (StreamingUpdate update in updates) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (update is RunUpdate runUpdate) + { + ApplyUpdate(runUpdate); + } + } + } + + // Public APIs specific to streaming LRO + public async IAsyncEnumerable GetUpdatesStreamingAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // I think this relies on the fact that there are updates that aren't + // RunUpdates, so we yield a value .. ? + while (await UpdateAsync(cancellationToken).ConfigureAwait(false)) + { + // TODO: only have this in one place. Here or UpdateStatus? + cancellationToken.ThrowIfCancellationRequested(); + + // Hm ... ? + // Note, this could add StreamingUpdate as a public property?? + // If it's an enumerator, do end-users ever care about what's in Current? + yield return _updateEnumeratorAsync.Current; + } + + // TODO: Dispose enumerator + } + + public IEnumerable GetUpdatesStreaming(CancellationToken cancellationToken = default) + { + StreamingUpdateCollection updates = new StreamingUpdateCollection(_createRun); + + // Enumerate those updates and update the state for each one + foreach (StreamingUpdate update in updates) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (update is RunUpdate runUpdate) + { + ApplyUpdate(runUpdate); + } + + yield return update; + } + } + + public override async Task UpdateAsync(CancellationToken cancellationToken = default) + { + // This does: + // 1. Get update + // 2. Apply update + // 3. Returns whether to continue polling/has more updates + + // TODO: use cancellationToken? How is it plumbed into MoveNext? + + if (!await _updateEnumeratorAsync.MoveNextAsync().ConfigureAwait(false)) + { + return false; + } + + StreamingUpdate update = _updateEnumeratorAsync.Current; + if (update is RunUpdate runUpdate) + { + ApplyUpdate(runUpdate); + } + + return !IsCompleted; + } + + public override bool Update(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + private void ApplyUpdate(RunUpdate update) + { + // Set ThreadId + ThreadId ??= update.Value.ThreadId; + + // Set RunId + Id ??= update.Value.Id; + + // Set Status + Status = update.Value.Status; + + // Set Value + Value = update.Value; + + // SetRawResponse + // TODO: This currently won't work as intended because we are setting + // _currUpdateCollectionAsync to assist the ContinuableAsyncEnumerator. + // Revisit this when we close on how enumerator should be exposed. + if (_currUpdateCollectionAsync is not null) + { + SetRawResponse(_currUpdateCollectionAsync.GetRawResponse()); + } + + // Set IsCompleted + IsCompleted = update.Value.Status.IsTerminal; + } + + private IAsyncEnumerator? GetAsyncUpdateEnumerator() + { + IAsyncEnumerator? enumerator = _currUpdateCollectionAsync?.GetAsyncEnumerator(); + + // Only get it once until we reset it. + + // TODO: Make sure this doesn't leak a reference to a network stream + // that then can't be disposed. + _currUpdateCollectionAsync = null; + + return enumerator; + } + + public virtual void SubmitToolOutputsToRunStreaming( + IEnumerable toolOutputs, + CancellationToken cancellationToken = default) + { + if (ThreadId is null || Id is null) + { + throw new InvalidOperationException("Cannot submit tools until first update stream has been applied."); + } + + BinaryContent content = new InternalSubmitToolOutputsRunRequest( + toolOutputs.ToList(), stream: true, null).ToBinaryContent(); + + // TODO: can we do this the same way as this in the other method instead + // of having to take all those funcs? + async Task getResultAsync() => + await SubmitToolOutputsToRunAsync(ThreadId, Id, content, cancellationToken.ToRequestOptions(streaming: true)) + .ConfigureAwait(false); + + // TODO: Ensure we call SetRawResponse for the current operation. + // Note: we'll want to do this for the protocol implementation of this method + // as well. + + // Return the updates as a stream but also update the state as each is returned. + + _currUpdateCollectionAsync = new AsyncStreamingUpdateCollection(getResultAsync); + } + + #region hide + + //// used to defer first request. + //internal virtual async Task CreateRunAsync(string threadId, BinaryContent content, RequestOptions? options = null) + //{ + // Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + // Argument.AssertNotNull(content, nameof(content)); + + // PipelineMessage? message = null; + // try + // { + // message = CreateCreateRunRequest(threadId, content, options); + // return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + // } + // finally + // { + // if (options?.BufferResponse != false) + // { + // message?.Dispose(); + // } + // } + //} + + //internal PipelineMessage CreateCreateRunRequest(string threadId, BinaryContent content, RequestOptions? options) + //{ + // var message = Pipeline.CreateMessage(); + // message.ResponseClassifier = PipelineMessageClassifier200; + // var request = message.Request; + // request.Method = "POST"; + // var uri = new ClientUriBuilder(); + // uri.Reset(_endpoint); + // uri.AppendPath("/threads/", false); + // uri.AppendPath(threadId, true); + // uri.AppendPath("/runs", false); + // request.Uri = uri.ToUri(); + // request.Headers.Set("Accept", "application/json"); + // request.Headers.Set("Content-Type", "application/json"); + // request.Content = content; + // message.Apply(options); + // return message; + //} + + //private static PipelineMessageClassifier? _pipelineMessageClassifier200; + //private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + #endregion +} diff --git a/src/To.Be.Generated/ThreadRunOperation.Protocol.cs b/src/To.Be.Generated/ThreadRunOperation.Protocol.cs new file mode 100644 index 000000000..210dae2c4 --- /dev/null +++ b/src/To.Be.Generated/ThreadRunOperation.Protocol.cs @@ -0,0 +1,714 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +// Protocol version +public partial class ThreadRunOperation : OperationResult +{ + // TODO: fix this - remove protected fields + protected readonly ClientPipeline _pipeline; + protected readonly Uri _endpoint; + + // TODO: Note, convenience type will make these public. Right now we have + // them in two places. + private string? _threadId; + private string? _runId; + private string? _status; + + private bool _isCompleted; + + private PollingInterval _pollingInterval; + + private readonly bool _isStreaming; + + // For use with streaming convenience methods - response hasn't been provided yet. + internal ThreadRunOperation(ClientPipeline pipeline, Uri endpoint) + : base() + { + _pipeline = pipeline; + _endpoint = endpoint; + _pollingInterval = new(); + + // We'd only do this if we were in a streaming convenience subtype. + _isStreaming = true; + } + + // For use with protocol methods where the response has been obtained prior + // to creation of the LRO instance. + internal ThreadRunOperation( + ClientPipeline pipeline, + Uri endpoint, + PipelineResponse response) + : base(response) + { + _pipeline = pipeline; + _endpoint = endpoint; + _pollingInterval = new(); + + if (response.Headers.TryGetValue("Content-Type", out string? contentType)) + { + _isStreaming = contentType == "text/event-stream; charset=utf-8"; + } + + if (!_isStreaming) + { + using JsonDocument doc = JsonDocument.Parse(response.Content); + + _status = doc.RootElement.GetProperty("status"u8).GetString(); + _threadId = doc.RootElement.GetProperty("thread_id"u8).GetString(); + _runId = doc.RootElement.GetProperty("id"u8).GetString(); + + if (_status is null || _threadId is null || _runId is null) + { + throw new ArgumentException("Invalid 'response' body.", nameof(response)); + } + + IsCompleted = GetIsCompleted(_status!); + } + } + + #region OperationResult methods + + public override bool IsCompleted + { + get + { + if (_isStreaming) + { + throw new NotSupportedException("Cannot obtain operation status from streaming operation."); + } + + return _isCompleted; + } + + protected set => _isCompleted = value; + } + + // Note: these have to work for protocol-only. + public override Task WaitAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override void Wait(CancellationToken cancellationToken = default) + { + // See: https://platform.openai.com/docs/assistants/how-it-works/polling-for-updates + + if (_isStreaming) + { + // we would have to read from the string to get the run ID to poll for. + throw new NotSupportedException("Cannot poll for status updates from streaming operation."); + } + + // These should always be set in the constructor. + Debug.Assert(_threadId is not null); + Debug.Assert(_runId is not null); + + // TODO: reimplement around the update enumerator concept. + while (Update(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + _pollingInterval.Wait(); + } + } + + // Note: The methods below are removed/rewritten when convenience-layer is added. + + //public override Task UpdateAsync(CancellationToken cancellationToken = default) + //{ + // throw new NotImplementedException(); + //} + + //public override bool Update(CancellationToken cancellationToken = default) + //{ + // // This does: + // // 1. Get update + // // 2. Apply update + // // 3. Returns whether to continue polling/has more updates + + // ClientResult update = GetUpdate(cancellationToken); + + // ApplyUpdate(update.GetRawResponse()); + + // // Do not continue polling from Wait method if operation is complete, + // // or input is required, since we would poll forever in either state! + // return !IsCompleted && _status != "requires_action"; + //} + + //private Task GetUpdateAsync() + //{ + // throw new NotImplementedException(); + //} + + //private ClientResult GetUpdate(CancellationToken cancellationToken) + //{ + // if (_threadId == null || _runId == null) + // { + // throw new InvalidOperationException("ThreadId or RunId is not set."); + // } + + // // TODO: RequestOptions/CancellationToken logic around this ... ? + // return GetRun(_threadId, _runId, cancellationToken.ToRequestOptions()); + //} + + //private void ApplyUpdate(PipelineResponse response) + //{ + // using JsonDocument doc = JsonDocument.Parse(response.Content); + + // _status = doc.RootElement.GetProperty("status"u8).GetString(); + // _threadId ??= doc.RootElement.GetProperty("thread_id"u8).GetString(); + // _runId ??= doc.RootElement.GetProperty("id"u8).GetString(); + + // IsCompleted = GetIsCompleted(_status!); + + // SetRawResponse(response); + //} + + private static bool GetIsCompleted(string status) + { + bool hasCompleted = + status == "expired" || + status == "completed" || + status == "failed" || + status == "incomplete" || + status == "cancelled"; + + return hasCompleted; + } + + #endregion + + #region Generated protocol methods - i.e. TypeSpec "linked operations" + + // TODO: Decide whether we want these + //// TODO: Note that the CreateRun protocol methods are made internal, i.e. not + //// exposed as part of public API. + + ///// + ///// [Protocol Method] Create a run. + ///// + ///// The ID of the thread to run. + ///// The content to send as the body of the request. + ///// The request options, which can override default behaviors of the client pipeline on a per-call basis. + ///// or is null. + ///// is an empty string, and was expected to be non-empty. + ///// Service returned a non-success status code. + ///// The response returned from the service. + //internal virtual async Task CreateRunAsync(string threadId, BinaryContent content, RequestOptions? options = null) + //{ + // Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + // Argument.AssertNotNull(content, nameof(content)); + + // PipelineMessage? message = null; + // try + // { + // message = CreateCreateRunRequest(threadId, content, options); + // return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + // } + // finally + // { + // if (options?.BufferResponse != false) + // { + // message?.Dispose(); + // } + // } + //} + + ///// + ///// [Protocol Method] Create a run. + ///// + ///// The ID of the thread to run. + ///// The content to send as the body of the request. + ///// The request options, which can override default behaviors of the client pipeline on a per-call basis. + ///// or is null. + ///// is an empty string, and was expected to be non-empty. + ///// Service returned a non-success status code. + ///// The response returned from the service. + //internal virtual ClientResult CreateRun(string threadId, BinaryContent content, RequestOptions? options = null) + //{ + // Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + // Argument.AssertNotNull(content, nameof(content)); + + // PipelineMessage? message = null; + // try + // { + // message = CreateCreateRunRequest(threadId, content, options); + // return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + // } + // finally + // { + // if (options?.BufferResponse != false) + // { + // message?.Dispose(); + // } + // } + //} + + /// + /// [Protocol Method] Retrieves a run. + /// + /// The ID of the [thread](/docs/api-reference/threads) that was run. + /// The ID of the run to retrieve. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task GetRunAsync(string threadId, string runId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateGetRunRequest(threadId, runId, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + /// + /// [Protocol Method] Retrieves a run. + /// + /// The ID of the [thread](/docs/api-reference/threads) that was run. + /// The ID of the run to retrieve. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult GetRun(string threadId, string runId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateGetRunRequest(threadId, runId, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + /// + /// [Protocol Method] Modifies a run. + /// + /// The ID of the [thread](/docs/api-reference/threads) that was run. + /// The ID of the run to modify. + /// The content to send as the body of the request. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task ModifyRunAsync(string threadId, string runId, BinaryContent content, RequestOptions? options = null) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNull(content, nameof(content)); + + using PipelineMessage message = CreateModifyRunRequest(threadId, runId, content, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + /// + /// [Protocol Method] Modifies a run. + /// + /// The ID of the [thread](/docs/api-reference/threads) that was run. + /// The ID of the run to modify. + /// The content to send as the body of the request. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult ModifyRun(string threadId, string runId, BinaryContent content, RequestOptions? options = null) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNull(content, nameof(content)); + + using PipelineMessage message = CreateModifyRunRequest(threadId, runId, content, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + /// + /// [Protocol Method] Cancels a run that is `in_progress`. + /// + /// The ID of the thread to which this run belongs. + /// The ID of the run to cancel. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task CancelRunAsync(string threadId, string runId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateCancelRunRequest(threadId, runId, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + /// + /// [Protocol Method] Cancels a run that is `in_progress`. + /// + /// The ID of the thread to which this run belongs. + /// The ID of the run to cancel. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult CancelRun(string threadId, string runId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + using PipelineMessage message = CreateCancelRunRequest(threadId, runId, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + /// + /// [Protocol Method] 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](/docs/api-reference/threads) to which this run belongs. + /// The ID of the run that requires the tool output submission. + /// The content to send as the body of the request. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task SubmitToolOutputsToRunAsync(string threadId, string runId, BinaryContent content, RequestOptions? options = null) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNull(content, nameof(content)); + + PipelineMessage? message = null; + try + { + message = CreateSubmitToolOutputsToRunRequest(threadId, runId, content, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + finally + { + if (options?.BufferResponse != false) + { + message?.Dispose(); + } + } + } + + /// + /// [Protocol Method] 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](/docs/api-reference/threads) to which this run belongs. + /// The ID of the run that requires the tool output submission. + /// The content to send as the body of the request. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult SubmitToolOutputsToRun(string threadId, string runId, BinaryContent content, RequestOptions? options = null) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNull(content, nameof(content)); + + PipelineMessage? message = null; + try + { + message = CreateSubmitToolOutputsToRunRequest(threadId, runId, content, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + finally + { + if (options?.BufferResponse != false) + { + message?.Dispose(); + } + } + } + + /// + /// [Protocol Method] Returns a paginated collection of run steps belonging to a run. + /// + /// The ID of the thread the run and run steps belong to. + /// The ID of the run the run steps belong to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IAsyncEnumerable GetRunStepsAsync(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + RunStepsPageEnumerator enumerator = new RunStepsPageEnumerator(_pipeline, _endpoint, threadId, runId, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// [Protocol Method] Returns a paginated collection of run steps belonging to a run. + /// + /// The ID of the thread the run and run steps belong to. + /// The ID of the run the run steps belong to. + /// + /// 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. `asc` for ascending order and`desc` + /// for descending order. Allowed values: "asc" | "desc" + /// + /// + /// 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. + /// + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// or is null. + /// or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// A collection of service responses, each holding a page of values. + public virtual IEnumerable GetRunSteps(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + + RunStepsPageEnumerator enumerator = new RunStepsPageEnumerator(_pipeline, _endpoint, threadId, runId, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// [Protocol Method] 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. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// , or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual async Task GetRunStepAsync(string threadId, string runId, string stepId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNullOrEmpty(stepId, nameof(stepId)); + + using PipelineMessage message = CreateGetRunStepRequest(threadId, runId, stepId, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + /// + /// [Protocol Method] 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. + /// The request options, which can override default behaviors of the client pipeline on a per-call basis. + /// , or is null. + /// , or is an empty string, and was expected to be non-empty. + /// Service returned a non-success status code. + /// The response returned from the service. + public virtual ClientResult GetRunStep(string threadId, string runId, string stepId, RequestOptions? options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + Argument.AssertNotNullOrEmpty(runId, nameof(runId)); + Argument.AssertNotNullOrEmpty(stepId, nameof(stepId)); + + using PipelineMessage message = CreateGetRunStepRequest(threadId, runId, stepId, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + internal PipelineMessage CreateCreateRunRequest(string threadId, BinaryContent content, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "POST"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs", false); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + request.Headers.Set("Content-Type", "application/json"); + request.Content = content; + message.Apply(options); + return message; + } + + internal PipelineMessage CreateGetRunRequest(string threadId, string runId, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + internal PipelineMessage CreateModifyRunRequest(string threadId, string runId, BinaryContent content, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "POST"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + request.Headers.Set("Content-Type", "application/json"); + request.Content = content; + message.Apply(options); + return message; + } + + internal PipelineMessage CreateCancelRunRequest(string threadId, string runId, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "POST"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + uri.AppendPath("/cancel", false); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + internal PipelineMessage CreateSubmitToolOutputsToRunRequest(string threadId, string runId, BinaryContent content, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "POST"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + uri.AppendPath("/submit_tool_outputs", false); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + request.Headers.Set("Content-Type", "application/json"); + request.Content = content; + message.Apply(options); + return message; + } + + internal PipelineMessage CreateGetRunStepsRequest(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + uri.AppendPath("/steps", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + internal PipelineMessage CreateGetRunStepRequest(string threadId, string runId, string stepId, RequestOptions? options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/threads/", false); + uri.AppendPath(threadId, true); + uri.AppendPath("/runs/", false); + uri.AppendPath(runId, true); + uri.AppendPath("/steps/", false); + uri.AppendPath(stepId, true); + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + #endregion +} \ No newline at end of file diff --git a/src/To.Be.Generated/ThreadRunOperation.cs b/src/To.Be.Generated/ThreadRunOperation.cs new file mode 100644 index 000000000..9403c2442 --- /dev/null +++ b/src/To.Be.Generated/ThreadRunOperation.cs @@ -0,0 +1,433 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +// Convenience version +public partial class ThreadRunOperation : OperationResult +{ + // Note: these all have to be nullable because the derived streaming type + // cannot set them until it reads the first event from the SSE stream. + + public string? Id { get => _runId; protected set { _runId = value; } } + public string? ThreadId { get => _threadId; protected set { _threadId = value; } } + + public ThreadRun? Value { get; protected set; } + public RunStatus? Status { get; protected set; } + + // For use with polling convenience methods where the response has been + // obtained prior to creation of the LRO type. + internal ThreadRunOperation( + ClientPipeline pipeline, + Uri endpoint, + ThreadRun value, + RunStatus status, + PipelineResponse response) + : base(response) + { + _pipeline = pipeline; + _endpoint = endpoint; + _pollingInterval = new(); + + if (response.Headers.TryGetValue("Content-Type", out string? contentType) && + contentType == "text/event-stream; charset=utf-8") + { + throw new ArgumentException("Cannot create polling operation from streaming response.", nameof(response)); + } + + Value = value; + Status = status; + + ThreadId = value.ThreadId; + Id = value.Id; + + RehydrationToken = new ThreadRunOperationToken(value.ThreadId, value.Id); + } + + internal ThreadRunOperation( + ClientPipeline pipeline, + Uri endpoint, + string threadId, + string runId) + : this(pipeline, endpoint, new ThreadRunOperationToken(threadId, runId)) + { + } + + // For use with rehydration client methods where the response has not been + // obtained yet, but will once the client method makes a call to Update. + internal ThreadRunOperation( + ClientPipeline pipeline, + Uri endpoint, + ThreadRunOperationToken token) : base() + { + _pipeline = pipeline; + _endpoint = endpoint; + _pollingInterval = new(); + + ThreadId = token.ThreadId; + Id = token.RunId; + + RehydrationToken = token; + } + + #region OperationResult methods + + public Task WaitAsync(TimeSpan? pollingInterval, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public void Wait(TimeSpan? pollingInterval, CancellationToken cancellationToken = default) + { + if (_isStreaming) + { + // we would have to read from the string to get the run ID to poll for. + throw new NotSupportedException("Cannot poll for status updates from streaming operation."); + } + + if (pollingInterval is not null) + { + // TODO: don't reallocate + _pollingInterval = new PollingInterval(pollingInterval); + } + + Wait(cancellationToken); + } + + // TODO: evaluate this experiment + // Expose enumerable APIs similar to the streaming ones. + public virtual IAsyncEnumerable GetUpdatesAsync( + TimeSpan? pollingInterval = default, + /*[EnumeratorCancellation]*/ CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + + //while (await UpdateAsync(cancellationToken).ConfigureAwait(false)) + //{ + // cancellationToken.ThrowIfCancellationRequested(); + + // // Hm ... ? + // // Note, this could add StreamingUpdate as a public property?? + // // If it's an enumerator, do end-users ever care about what's in Current? + // yield return _updateEnumeratorAsync.Current; + //} + + //// TODO: Dispose enumerator + } + + public virtual IEnumerable GetUpdates( + TimeSpan? pollingInterval = default, + CancellationToken cancellationToken = default) + { + if (pollingInterval is not null) + { + // TODO: don't reallocate + _pollingInterval = new PollingInterval(pollingInterval); + } + + RunStatus status = Status!.Value; + + do + { + Update(cancellationToken); + + // TODO: only have this in one place. Here or UpdateStatus? + cancellationToken.ThrowIfCancellationRequested(); + + _pollingInterval.Wait(); + + if (status != Status!.Value) + { + // State changed. Return the update. + status = Status!.Value; + + yield return Value!; + } + } + // TODO: technically if we wanted to do it this way, we should yield the + // status change update saying the operation is complete, but this isn't + // happening currently. Figure this out. + while (Update(cancellationToken)); + } + + public override Task UpdateAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override bool Update(CancellationToken cancellationToken = default) + { + // This does: + // 1. Get update + // 2. Apply update + // 3. Returns whether to continue polling/has more updates + + ClientResult runUpdate = GetUpdate(cancellationToken); + + ApplyUpdate(runUpdate); + + // Do not continue polling from Wait method if operation is complete, + // or input is required, since we would poll forever in either state! + return !IsCompleted && Status != RunStatus.RequiresAction; + } + + private Task> GetUpdateAsync() + { + throw new NotImplementedException(); + } + + private ClientResult GetUpdate(CancellationToken cancellationToken) + { + if (_threadId == null || _runId == null) + { + throw new InvalidOperationException("ThreadId or RunId is not set."); + } + + // TODO: RequestOptions/CancellationToken logic around this ... ? + return GetRun(cancellationToken); + } + + private void ApplyUpdate(ClientResult update) + { + Value = update; + Status = update.Value.Status; + IsCompleted = Status.Value.IsTerminal; + + SetRawResponse(update.GetRawResponse()); + } + + #endregion + + #region Convenience overloads of generated protocol methods + + // TODO: decide if we want to keep GetRun methods here - they could enable + // manual polling scenarios if we wanted to do it that way. + + /// + /// Gets an existing from a known . + /// + /// A token that can be used to cancel this method call. + /// The existing instance. + internal virtual async Task> GetRunAsync(CancellationToken cancellationToken = default) + { + ClientResult protocolResult = await GetRunAsync(_threadId!, _runId!, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + } + + /// + /// Gets an existing from a known . + /// + /// A token that can be used to cancel this method call. + /// The existing instance. + internal virtual ClientResult GetRun(CancellationToken cancellationToken = default) + { + ClientResult protocolResult = GetRun(_threadId!, _runId!, cancellationToken.ToRequestOptions()); + return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + } + + /// + /// Cancels an in-progress . + /// + /// A token that can be used to cancel this method call. + /// An updated instance, reflecting the new status of the run. + public virtual async Task> CancelRunAsync(CancellationToken cancellationToken = default) + { + ClientResult protocolResult = await CancelRunAsync(_threadId!, _runId!, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + } + + /// + /// Cancels an in-progress . + /// + /// A token that can be used to cancel this method call. + /// An updated instance, reflecting the new status of the run. + public virtual ClientResult CancelRun(CancellationToken cancellationToken = default) + { + ClientResult protocolResult = CancelRun(_threadId!, _runId!, cancellationToken.ToRequestOptions()); + return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + } + + /// + /// Submits a collection of required tool call outputs to a run and resumes the run. + /// + /// + /// The tool outputs, corresponding to instances from the run. + /// + /// A token that can be used to cancel this method call. + /// The , updated after the submission was processed. + public virtual async Task SubmitToolOutputsToRunAsync( + IEnumerable toolOutputs, + CancellationToken cancellationToken = default) + { + BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs).ToBinaryContent(); + ClientResult protocolResult = await SubmitToolOutputsToRunAsync(_threadId!, _runId!, content, cancellationToken.ToRequestOptions()) + .ConfigureAwait(false); + ClientResult update = CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ApplyUpdate(update); + } + + /// + /// Submits a collection of required tool call outputs to a run and resumes the run. + /// + /// + /// The tool outputs, corresponding to instances from the run. + /// + /// A token that can be used to cancel this method call. + /// The , updated after the submission was processed. + public virtual void SubmitToolOutputsToRun( + IEnumerable toolOutputs, + CancellationToken cancellationToken = default) + { + BinaryContent content = new InternalSubmitToolOutputsRunRequest(toolOutputs).ToBinaryContent(); + ClientResult protocolResult = SubmitToolOutputsToRun(_threadId!, _runId!, content, cancellationToken.ToRequestOptions()); + ClientResult update = CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ApplyUpdate(update); + } + + /// + /// Gets a page collection holding instances associated with a . + /// + /// + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetRunStepsAsync( + RunStepCollectionOptions? options = default, + CancellationToken cancellationToken = default) + { + RunStepsPageEnumerator enumerator = new(_pipeline, _endpoint, + _threadId!, + _runId!, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual AsyncPageCollection GetRunStepsAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + RunStepsPageToken pageToken = RunStepsPageToken.FromToken(firstPageToken); + RunStepsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.RunId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.CreateAsync(enumerator); + } + + /// + /// Gets a page collection holding instances associated with a . + /// + /// + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetRunSteps( + RunStepCollectionOptions? options = default, + CancellationToken cancellationToken = default) + { + RunStepsPageEnumerator enumerator = new(_pipeline, _endpoint, + ThreadId!, + Id!, + options?.PageSize, + options?.Order?.ToString(), + options?.AfterId, + options?.BeforeId, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Rehydrates a page collection holding instances from a page token. + /// + /// Page token corresponding to the first page of the collection to rehydrate. + /// A token that can be used to cancel this method call. + /// holds pages of values. To obtain a collection of values, call + /// . To obtain the current + /// page of values, call . + /// A collection of pages of . + public virtual PageCollection GetRunSteps( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(firstPageToken, nameof(firstPageToken)); + + RunStepsPageToken pageToken = RunStepsPageToken.FromToken(firstPageToken); + RunStepsPageEnumerator enumerator = new(_pipeline, _endpoint, + pageToken.ThreadId, + pageToken.RunId, + pageToken.Limit, + pageToken.Order, + pageToken.After, + pageToken.Before, + cancellationToken.ToRequestOptions()); + + return PageCollectionHelpers.Create(enumerator); + } + + /// + /// Gets a single run step from a run. + /// + /// The ID of the run step. + /// A token that can be used to cancel this method call. + /// A instance corresponding to the specified step. + public virtual async Task> GetRunStepAsync(string stepId, CancellationToken cancellationToken = default) + { + ClientResult protocolResult = await GetRunStepAsync(_threadId!, _runId!, stepId, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return CreateResultFromProtocol(protocolResult, RunStep.FromResponse); + } + + /// + /// Gets a single run step from a run. + /// + /// The ID of the run step. + /// A token that can be used to cancel this method call. + /// A instance corresponding to the specified step. + public virtual ClientResult GetRunStep(string stepId, CancellationToken cancellationToken = default) + { + ClientResult protocolResult = GetRunStep(_threadId!, _runId!, stepId, cancellationToken.ToRequestOptions()); + return CreateResultFromProtocol(protocolResult, RunStep.FromResponse); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ClientResult CreateResultFromProtocol(ClientResult protocolResult, Func responseDeserializer) + { + PipelineResponse pipelineResponse = protocolResult.GetRawResponse(); + T deserializedResultValue = responseDeserializer.Invoke(pipelineResponse); + return ClientResult.FromValue(deserializedResultValue, pipelineResponse); + } + + #endregion +} \ No newline at end of file diff --git a/src/To.Be.Generated/ThreadRunOperationToken.cs b/src/To.Be.Generated/ThreadRunOperationToken.cs new file mode 100644 index 000000000..5fc1e9032 --- /dev/null +++ b/src/To.Be.Generated/ThreadRunOperationToken.cs @@ -0,0 +1,102 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class ThreadRunOperationToken : ContinuationToken +{ + public ThreadRunOperationToken(string threadId, string runId) + { + ThreadId = threadId; + RunId = runId; + } + + public string ThreadId { get; } + + public string RunId { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + + writer.WriteString("threadId", ThreadId); + writer.WriteString("runId", RunId); + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public static ThreadRunOperationToken FromToken(ContinuationToken continuationToken) + { + if (continuationToken is ThreadRunOperationToken token) + { + return token; + } + + BinaryData data = continuationToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create RunOperationToken from provided continuationToken.", nameof(continuationToken)); + } + + Utf8JsonReader reader = new(data); + + string threadId = null!; + string runId = null!; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "threadId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + threadId = reader.GetString()!; + break; + + case "runId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + threadId = reader.GetString()!; + break; + + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (threadId is null || runId is null) + { + throw new ArgumentException("Failed to create RunOperationToken from provided continuationToken.", nameof(continuationToken)); + } + + return new(threadId, runId); + } +} + diff --git a/src/To.Be.Generated/VectorStoreFileBatchesPageEnumerator.cs b/src/To.Be.Generated/VectorStoreFileBatchesPageEnumerator.cs new file mode 100644 index 000000000..cb1a78576 --- /dev/null +++ b/src/To.Be.Generated/VectorStoreFileBatchesPageEnumerator.cs @@ -0,0 +1,155 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal partial class VectorStoreFileBatchesPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly string _vectorStoreId; + private readonly string _batchId; + private readonly int? _limit; + private readonly string? _order; + + private readonly string? _before; + private readonly string? _filter; + private readonly RequestOptions _options; + + private string? _after; + + public VectorStoreFileBatchesPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + string vectorStoreId, string batchId, int? limit, string? order, string? after, string? before, string? filter, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _vectorStoreId = vectorStoreId; + _batchId = batchId; + + _limit = limit; + _order = order; + _after = after; + _before = before; + _filter = filter; + + _options = options; + } + + public override async Task GetFirstAsync() + => await GetFileAssociationsAsync(_vectorStoreId, _batchId, _limit, _order, _after, _before, _filter, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetFileAssociations(_vectorStoreId, _batchId, _limit, _order, _after, _before, _filter, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetFileAssociationsAsync(_vectorStoreId, _batchId, _limit, _order, _after, _before, _filter, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetFileAssociations(_vectorStoreId, _batchId, _limit, _order, _after, _before, _filter, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListVectorStoreFilesResponse list = ModelReaderWriter.Read(response.Content)!; + + VectorStoreFilesPageToken pageToken = VectorStoreFilesPageToken.FromOptions(_vectorStoreId, _limit, _order, _after, _before, _filter); + VectorStoreFilesPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal virtual async Task GetFileAssociationsAsync(string vectorStoreId, string batchId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); + Argument.AssertNotNullOrEmpty(batchId, nameof(batchId)); + + using PipelineMessage message = CreateGetFilesInVectorStoreBatchesRequest(vectorStoreId, batchId, limit, order, after, before, filter, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetFileAssociations(string vectorStoreId, string batchId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); + Argument.AssertNotNullOrEmpty(batchId, nameof(batchId)); + + using PipelineMessage message = CreateGetFilesInVectorStoreBatchesRequest(vectorStoreId, batchId, limit, order, after, before, filter, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + internal PipelineMessage CreateGetFilesInVectorStoreBatchesRequest(string vectorStoreId, string batchId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/vector_stores/", false); + uri.AppendPath(vectorStoreId, true); + uri.AppendPath("/file_batches/", false); + uri.AppendPath(batchId, true); + uri.AppendPath("/files", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + if (filter != null) + { + uri.AppendQuery("filter", filter, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/VectorStoreFileBatchesPageToken.cs b/src/To.Be.Generated/VectorStoreFileBatchesPageToken.cs new file mode 100644 index 000000000..8db14097f --- /dev/null +++ b/src/To.Be.Generated/VectorStoreFileBatchesPageToken.cs @@ -0,0 +1,183 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal class VectorStoreFileBatchesPageToken : ContinuationToken +{ + protected VectorStoreFileBatchesPageToken(string vectorStoreId,string batchId, int? limit, string? order, string? after, string? before, string? filter) + { + VectorStoreId = vectorStoreId; + BatchId = batchId; + + Limit = limit; + Order = order; + After = after; + Before = before; + Filter = filter; + } + + public string VectorStoreId { get; } + + public string BatchId { get; } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public string? Filter { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + writer.WriteString("vectorStoreId", VectorStoreId); + writer.WriteString("batchId", BatchId); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + if (Filter is not null) + { + writer.WriteString("filter", Filter); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public VectorStoreFileBatchesPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new(VectorStoreId, BatchId, Limit, Order, lastId, Before, Filter); + } + + public static VectorStoreFileBatchesPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is VectorStoreFileBatchesPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create VectorStoreFileBatchesPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + string vectorStoreId = null!; + string batchId = null!; + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + string? filter = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "vectorStoreId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + vectorStoreId = reader.GetString()!; + break; + case "batchId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + batchId = reader.GetString()!; + break; + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + case "filter": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + filter = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (vectorStoreId is null || + batchId is null) + { + throw new ArgumentException("Failed to create VectorStoreFileBatchesPageToken from provided pageToken.", nameof(pageToken)); + } + + return new(vectorStoreId, batchId, limit, order, after, before, filter); + } + + public static VectorStoreFileBatchesPageToken FromOptions(string vectorStoreId, string batchId, int? limit, string? order, string? after, string? before, string? filter) + => new(vectorStoreId, batchId, limit, order, after, before, filter); +} \ No newline at end of file diff --git a/src/To.Be.Generated/VectorStoreFilesPageEnumerator.cs b/src/To.Be.Generated/VectorStoreFilesPageEnumerator.cs new file mode 100644 index 000000000..d78384e31 --- /dev/null +++ b/src/To.Be.Generated/VectorStoreFilesPageEnumerator.cs @@ -0,0 +1,148 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal partial class VectorStoreFilesPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly string _vectorStoreId; + private readonly int? _limit; + private readonly string? _order; + + private readonly string? _before; + private readonly string? _filter; + private readonly RequestOptions _options; + + private string? _after; + + public VectorStoreFilesPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + string vectorStoreId, + int? limit, string? order, string? after, string? before, string? filter, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _vectorStoreId = vectorStoreId; + _limit = limit; + _order = order; + _after = after; + _before = before; + _filter = filter; + _options = options; + } + + public override async Task GetFirstAsync() + => await GetFileAssociationsAsync(_vectorStoreId, _limit, _order, _after, _before, _filter, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetFileAssociations(_vectorStoreId, _limit, _order, _after, _before, _filter, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetFileAssociationsAsync(_vectorStoreId, _limit, _order, _after, _before, _filter, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetFileAssociations(_vectorStoreId, _limit, _order, _after, _before, _filter, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListVectorStoreFilesResponse list = ModelReaderWriter.Read(response.Content)!; + + VectorStoreFilesPageToken pageToken = VectorStoreFilesPageToken.FromOptions(_vectorStoreId, _limit, _order, _after, _before, _filter); + VectorStoreFilesPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal virtual async Task GetFileAssociationsAsync(string vectorStoreId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); + + using PipelineMessage message = CreateGetVectorStoreFilesRequest(vectorStoreId, limit, order, after, before, filter, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetFileAssociations(string vectorStoreId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(vectorStoreId, nameof(vectorStoreId)); + + using PipelineMessage message = CreateGetVectorStoreFilesRequest(vectorStoreId, limit, order, after, before, filter, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + internal PipelineMessage CreateGetVectorStoreFilesRequest(string vectorStoreId, int? limit, string? order, string? after, string? before, string? filter, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/vector_stores/", false); + uri.AppendPath(vectorStoreId, true); + uri.AppendPath("/files", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + if (filter != null) + { + uri.AppendQuery("filter", filter, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/VectorStoreFilesPageToken.cs b/src/To.Be.Generated/VectorStoreFilesPageToken.cs new file mode 100644 index 000000000..ba7b1f9eb --- /dev/null +++ b/src/To.Be.Generated/VectorStoreFilesPageToken.cs @@ -0,0 +1,171 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal class VectorStoreFilesPageToken : ContinuationToken +{ + protected VectorStoreFilesPageToken(string vectorStoreId, int? limit, string? order, string? after, string? before, string? filter) + { + VectorStoreId = vectorStoreId; + + Limit = limit; + Order = order; + After = after; + Before = before; + Filter = filter; + } + public string VectorStoreId { get; } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public string? Filter { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + writer.WriteString("vectorStoreId", VectorStoreId); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + if (Filter is not null) + { + writer.WriteString("filter", Filter); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public VectorStoreFilesPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new(VectorStoreId, Limit, Order, lastId, Before, Filter); + } + + public static VectorStoreFilesPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is VectorStoreFilesPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create VectorStoreFilesPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + string vectorStoreId = null!; + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + string? filter = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "vectorStoreId": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + vectorStoreId = reader.GetString()!; + break; + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + case "filter": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + filter = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + if (vectorStoreId is null) + { + throw new ArgumentException("Failed to create VectorStoreFilesPageToken from provided pageToken.", nameof(pageToken)); + } + + return new(vectorStoreId, limit, order, after, before, filter); + } + + public static VectorStoreFilesPageToken FromOptions(string vectorStoreId, int? limit, string? order, string? after, string? before, string? filter) + => new(vectorStoreId, limit, order, after, before, filter); +} \ No newline at end of file diff --git a/src/To.Be.Generated/VectorStoresPageEnumerator.cs b/src/To.Be.Generated/VectorStoresPageEnumerator.cs new file mode 100644 index 000000000..4a2aac725 --- /dev/null +++ b/src/To.Be.Generated/VectorStoresPageEnumerator.cs @@ -0,0 +1,132 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal partial class VectorStoresPageEnumerator : PageEnumerator +{ + private readonly ClientPipeline _pipeline; + private readonly Uri _endpoint; + + private readonly int? _limit; + private readonly string _order; + private readonly string _before; + private readonly RequestOptions _options; + + private string _after; + + public VectorStoresPageEnumerator( + ClientPipeline pipeline, + Uri endpoint, + int? limit, string order, string after, string before, + RequestOptions options) + { + _pipeline = pipeline; + _endpoint = endpoint; + + _limit = limit; + _order = order; + _after = after; + _before = before; + _options = options; + } + + public override async Task GetFirstAsync() + => await GetVectorStoresAsync(_limit, _order, _after, _before, _options).ConfigureAwait(false); + + public override ClientResult GetFirst() + => GetVectorStores(_limit, _order, _after, _before, _options); + + public override async Task GetNextAsync(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return await GetVectorStoresAsync(_limit, _order, _after, _before, _options).ConfigureAwait(false); + } + + public override ClientResult GetNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + _after = doc.RootElement.GetProperty("last_id"u8).GetString()!; + + return GetVectorStores(_limit, _order, _after, _before, _options); + } + + public override bool HasNext(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + using JsonDocument doc = JsonDocument.Parse(response.Content); + bool hasMore = doc.RootElement.GetProperty("has_more"u8).GetBoolean(); + + return hasMore; + } + + public override PageResult GetPageFromResult(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + + InternalListVectorStoresResponse list = ModelReaderWriter.Read(response.Content)!; + + VectorStoresPageToken pageToken = VectorStoresPageToken.FromOptions(_limit, _order, _after, _before); + VectorStoresPageToken? nextPageToken = pageToken.GetNextPageToken(list.HasMore, list.LastId); + + return PageResult.Create(list.Data, pageToken, nextPageToken, response); + } + + internal virtual async Task GetVectorStoresAsync(int? limit, string order, string after, string before, RequestOptions options) + { + using PipelineMessage message = CreateGetVectorStoresRequest(limit, order, after, before, options); + return ClientResult.FromResponse(await _pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + internal virtual ClientResult GetVectorStores(int? limit, string order, string after, string before, RequestOptions options) + { + using PipelineMessage message = CreateGetVectorStoresRequest(limit, order, after, before, options); + return ClientResult.FromResponse(_pipeline.ProcessMessage(message, options)); + } + + private PipelineMessage CreateGetVectorStoresRequest(int? limit, string order, string after, string before, RequestOptions options) + { + var message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier200; + var request = message.Request; + request.Method = "GET"; + var uri = new ClientUriBuilder(); + uri.Reset(_endpoint); + uri.AppendPath("/vector_stores", false); + if (limit != null) + { + uri.AppendQuery("limit", limit.Value, true); + } + if (order != null) + { + uri.AppendQuery("order", order, true); + } + if (after != null) + { + uri.AppendQuery("after", after, true); + } + if (before != null) + { + uri.AppendQuery("before", before, true); + } + request.Uri = uri.ToUri(); + request.Headers.Set("Accept", "application/json"); + message.Apply(options); + return message; + } + + private static PipelineMessageClassifier? _pipelineMessageClassifier200; + private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +} diff --git a/src/To.Be.Generated/VectorStoresPageToken.cs b/src/To.Be.Generated/VectorStoresPageToken.cs new file mode 100644 index 000000000..bd4c29a2d --- /dev/null +++ b/src/To.Be.Generated/VectorStoresPageToken.cs @@ -0,0 +1,142 @@ +using System; +using System.ClientModel; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace OpenAI.VectorStores; + +internal class VectorStoresPageToken : ContinuationToken +{ + protected VectorStoresPageToken(int? limit, string? order, string? after, string? before) + { + Limit = limit; + Order = order; + After = after; + Before = before; + } + + public int? Limit { get; } + + public string? Order { get; } + + public string? After { get; } + + public string? Before { get; } + + public override BinaryData ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + + if (Limit.HasValue) + { + writer.WriteNumber("limit", Limit.Value); + } + + if (Order is not null) + { + writer.WriteString("order", Order); + } + + if (After is not null) + { + writer.WriteString("after", After); + } + + if (Before is not null) + { + writer.WriteString("before", Before); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream); + } + + public VectorStoresPageToken? GetNextPageToken(bool hasMore, string? lastId) + { + if (!hasMore || lastId is null) + { + return null; + } + + return new(Limit, Order, lastId, Before); + } + + public static VectorStoresPageToken FromToken(ContinuationToken pageToken) + { + if (pageToken is VectorStoresPageToken token) + { + return token; + } + + BinaryData data = pageToken.ToBytes(); + + if (data.ToMemory().Length == 0) + { + throw new ArgumentException("Failed to create VectorStoresPageToken from provided pageToken.", nameof(pageToken)); + } + + Utf8JsonReader reader = new(data); + + int? limit = null; + string? order = null; + string? after = null; + string? before = null; + + reader.Read(); + + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "limit": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.Number); + limit = reader.GetInt32(); + break; + case "order": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + order = reader.GetString(); + break; + case "after": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + after = reader.GetString(); + break; + case "before": + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.String); + before = reader.GetString(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + return new(limit, order, after, before); + } + + public static VectorStoresPageToken FromOptions(int? limit, string? order, string? after, string? before) + => new(limit, order, after, before); +} \ No newline at end of file diff --git a/tests/Assistants/AssistantTests.cs b/tests/Assistants/AssistantTests.cs index a041ec3e7..ce3517269 100644 --- a/tests/Assistants/AssistantTests.cs +++ b/tests/Assistants/AssistantTests.cs @@ -6,10 +6,11 @@ using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Net.ServerSentEvents; using static OpenAI.Tests.TestHelpers; namespace OpenAI.Tests.Assistants; @@ -53,7 +54,8 @@ public void BasicAssistantOperationsWork() }, }); Assert.That(modifiedAssistant.Id, Is.EqualTo(assistant.Id)); - PageableCollection recentAssistants = client.GetAssistants(); + PageCollection pages = client.GetAssistants(); + IEnumerable recentAssistants = pages.GetAllValues(); Assistant listedAssistant = recentAssistants.FirstOrDefault(pageItem => pageItem.Id == assistant.Id); Assert.That(listedAssistant, Is.Not.Null); Assert.That(listedAssistant.Metadata.TryGetValue(s_cleanupMetadataKey, out string newMetadataValue) && newMetadataValue == "goodbye!"); @@ -130,10 +132,10 @@ public void BasicMessageOperationsWork() }); Assert.That(message.Metadata.TryGetValue("messageMetadata", out metadataValue) && metadataValue == "newValue"); - PageableCollection messagePage = client.GetMessages(thread); - Assert.That(messagePage.Count, Is.EqualTo(1)); - Assert.That(messagePage.First().Id, Is.EqualTo(message.Id)); - Assert.That(messagePage.First().Metadata.TryGetValue("messageMetadata", out metadataValue) && metadataValue == "newValue"); + PageResult messagePage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagePage.Values.Count, Is.EqualTo(1)); + Assert.That(messagePage.Values[0].Id, Is.EqualTo(message.Id)); + Assert.That(messagePage.Values[0].Metadata.TryGetValue("messageMetadata", out metadataValue) && metadataValue == "newValue"); } [Test] @@ -161,461 +163,628 @@ public void ThreadWithInitialMessagesWorks() }; AssistantThread thread = client.CreateThread(options); Validate(thread); - PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.OldestFirst); - Assert.That(messages.Count, Is.EqualTo(2)); - Assert.That(messages.First().Role, Is.EqualTo(MessageRole.User)); - Assert.That(messages.First().Content?.Count, Is.EqualTo(1)); - Assert.That(messages.First().Content[0].Text, Is.EqualTo("Hello, world!")); - Assert.That(messages.ElementAt(1).Content?.Count, Is.EqualTo(2)); - Assert.That(messages.ElementAt(1).Content[0], Is.Not.Null); - Assert.That(messages.ElementAt(1).Content[0].Text, Is.EqualTo("Can you describe this image for me?")); - Assert.That(messages.ElementAt(1).Content[1], Is.Not.Null); - Assert.That(messages.ElementAt(1).Content[1].ImageUrl.AbsoluteUri, Is.EqualTo("https://test.openai.com/image.png")); - } - - [Test] - public void BasicRunOperationsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); - Validate(assistant); - AssistantThread thread = client.CreateThread(); - Validate(thread); - PageableCollection runs = client.GetRuns(thread); - Assert.That(runs.Count, Is.EqualTo(0)); - ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); - Validate(message); - ThreadRun run = client.CreateRun(thread.Id, assistant.Id); - Validate(run); - Assert.That(run.Status, Is.EqualTo(RunStatus.Queued)); - Assert.That(run.CreatedAt, Is.GreaterThan(s_2024)); - ThreadRun retrievedRun = client.GetRun(thread.Id, run.Id); - Assert.That(retrievedRun.Id, Is.EqualTo(run.Id)); - runs = client.GetRuns(thread); - Assert.That(runs.Count, Is.EqualTo(1)); - Assert.That(runs.First().Id, Is.EqualTo(run.Id)); - - PageableCollection messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.GreaterThanOrEqualTo(1)); - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - Assert.That(run.CompletedAt, Is.GreaterThan(s_2024)); - Assert.That(run.RequiredActions.Count, Is.EqualTo(0)); - Assert.That(run.AssistantId, Is.EqualTo(assistant.Id)); - Assert.That(run.FailedAt, Is.Null); - Assert.That(run.IncompleteDetails, Is.Null); - - messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.EqualTo(2)); - - Assert.That(messages.ElementAt(0).Role, Is.EqualTo(MessageRole.Assistant)); - Assert.That(messages.ElementAt(1).Role, Is.EqualTo(MessageRole.User)); - Assert.That(messages.ElementAt(1).Id, Is.EqualTo(message.Id)); + PageResult messagesPage = client.GetMessages(thread, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[0].Content?.Count, Is.EqualTo(1)); + Assert.That(messagesPage.Values[0].Content[0].Text, Is.EqualTo("Hello, world!")); + Assert.That(messagesPage.Values[1].Content?.Count, Is.EqualTo(2)); + Assert.That(messagesPage.Values[1].Content[0], Is.Not.Null); + Assert.That(messagesPage.Values[1].Content[0].Text, Is.EqualTo("Can you describe this image for me?")); + Assert.That(messagesPage.Values[1].Content[1], Is.Not.Null); + Assert.That(messagesPage.Values[1].Content[1].ImageUrl.AbsoluteUri, Is.EqualTo("https://test.openai.com/image.png")); } - [Test] - public void BasicRunStepFunctionalityWorks() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-4o", new AssistantCreationOptions() - { - Tools = { new CodeInterpreterToolDefinition() }, - Instructions = "You help the user with mathematical descriptions and visualizations.", - }); - Validate(assistant); - - FileClient fileClient = new(); - OpenAIFileInfo equationFile = fileClient.UploadFile( - BinaryData.FromString(""" - x,y - 2,5 - 7,14, - 8,22 - """).ToStream(), - "text/csv", - FileUploadPurpose.Assistants); - Validate(equationFile); - - AssistantThread thread = client.CreateThread(new ThreadCreationOptions() - { - InitialMessages = - { - "Describe the contents of any available tool resource file." - + " Graph a linear regression and provide the coefficient of correlation." - + " Explain any code executed to evaluate.", - }, - ToolResources = new() - { - CodeInterpreter = new() - { - FileIds = { equationFile.Id }, - } - } - }); - Validate(thread); - - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); - - while (!run.Status.IsTerminal) - { - Thread.Sleep(1000); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - Assert.That(run.Usage?.TotalTokens, Is.GreaterThan(0)); - - PageableCollection runSteps = client.GetRunSteps(run); - Assert.That(runSteps.Count, Is.GreaterThan(1)); - Assert.Multiple(() => - { - Assert.That(runSteps.First().AssistantId, Is.EqualTo(assistant.Id)); - Assert.That(runSteps.First().ThreadId, Is.EqualTo(thread.Id)); - Assert.That(runSteps.First().RunId, Is.EqualTo(run.Id)); - Assert.That(runSteps.First().CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(runSteps.First().CompletedAt, Is.GreaterThan(s_2024)); - }); - RunStepDetails details = runSteps.First().Details; - Assert.That(details?.CreatedMessageId, Is.Not.Null.And.Not.Empty); - - string rawContent = runSteps.GetRawResponse().Content.ToString(); - details = runSteps.ElementAt(1).Details; - Assert.Multiple(() => - { - Assert.That(details?.ToolCalls.Count, Is.GreaterThan(0)); - Assert.That(details.ToolCalls[0].ToolKind, Is.EqualTo(RunStepToolCallKind.CodeInterpreter)); - Assert.That(details.ToolCalls[0].ToolCallId, Is.Not.Null.And.Not.Empty); - Assert.That(details.ToolCalls[0].CodeInterpreterInput, Is.Not.Null.And.Not.Empty); - Assert.That(details.ToolCalls[0].CodeInterpreterOutputs?.Count, Is.GreaterThan(0)); - Assert.That(details.ToolCalls[0].CodeInterpreterOutputs[0].ImageFileId, Is.Not.Null.And.Not.Empty); - }); - } + //[Test] + //public void BasicRunOperationsWork() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + // Validate(assistant); + // AssistantThread thread = client.CreateThread(); + // Validate(thread); + // PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + // Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + // ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + // Validate(message); + // ThreadRun run = client.CreateRun(thread.Id, assistant.Id); + // Validate(run); + // Assert.That(run.Status, Is.EqualTo(RunStatus.Queued)); + // Assert.That(run.CreatedAt, Is.GreaterThan(s_2024)); + // ThreadRun retrievedRun = client.GetRun(thread.Id, run.Id); + // Assert.That(retrievedRun.Id, Is.EqualTo(run.Id)); + // runsPage = client.GetRuns(thread).GetCurrentPage(); + // Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + // Assert.That(runsPage.Values[0].Id, Is.EqualTo(run.Id)); + + // PageResult messagesPage = client.GetMessages(thread).GetCurrentPage(); + // Assert.That(messagesPage.Values.Count, Is.GreaterThanOrEqualTo(1)); + // for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) + // { + // Thread.Sleep(500); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + // Assert.That(run.CompletedAt, Is.GreaterThan(s_2024)); + // Assert.That(run.RequiredActions.Count, Is.EqualTo(0)); + // Assert.That(run.AssistantId, Is.EqualTo(assistant.Id)); + // Assert.That(run.FailedAt, Is.Null); + // Assert.That(run.IncompleteDetails, Is.Null); + + // messagesPage = client.GetMessages(thread).GetCurrentPage(); + // Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + // Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + // Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + // Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + //} + + //[Test] + //public void BasicRunStepFunctionalityWorks() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-4o", new AssistantCreationOptions() + // { + // Tools = { new CodeInterpreterToolDefinition() }, + // Instructions = "You help the user with mathematical descriptions and visualizations.", + // }); + // Validate(assistant); + + // FileClient fileClient = new(); + // OpenAIFileInfo equationFile = fileClient.UploadFile( + // BinaryData.FromString(""" + // x,y + // 2,5 + // 7,14, + // 8,22 + // """).ToStream(), + // "text/csv", + // FileUploadPurpose.Assistants); + // Validate(equationFile); + + // AssistantThread thread = client.CreateThread(new ThreadCreationOptions() + // { + // InitialMessages = + // { + // "Describe the contents of any available tool resource file." + // + " Graph a linear regression and provide the coefficient of correlation." + // + " Explain any code executed to evaluate.", + // }, + // ToolResources = new() + // { + // CodeInterpreter = new() + // { + // FileIds = { equationFile.Id }, + // } + // } + // }); + // Validate(thread); + + // ThreadRun run = client.CreateRun(thread, assistant); + // Validate(run); + + // while (!run.Status.IsTerminal) + // { + // Thread.Sleep(1000); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + // Assert.That(run.Usage?.TotalTokens, Is.GreaterThan(0)); + + // PageCollection pages = client.GetRunSteps(run); + // PageResult firstPage = pages.GetCurrentPage(); + // RunStep firstStep = firstPage.Values[0]; + // RunStep secondStep = firstPage.Values[1]; + + // Assert.That(firstPage.Values.Count, Is.GreaterThan(1)); + // Assert.Multiple(() => + // { + // Assert.That(firstStep.AssistantId, Is.EqualTo(assistant.Id)); + // Assert.That(firstStep.ThreadId, Is.EqualTo(thread.Id)); + // Assert.That(firstStep.RunId, Is.EqualTo(run.Id)); + // Assert.That(firstStep.CreatedAt, Is.GreaterThan(s_2024)); + // Assert.That(firstStep.CompletedAt, Is.GreaterThan(s_2024)); + // }); + // RunStepDetails details = firstStep.Details; + // Assert.That(details?.CreatedMessageId, Is.Not.Null.And.Not.Empty); + + // string rawContent = firstPage.GetRawResponse().Content.ToString(); + + // details = secondStep.Details; + // Assert.Multiple(() => + // { + // Assert.That(details?.ToolCalls.Count, Is.GreaterThan(0)); + // Assert.That(details.ToolCalls[0].ToolKind, Is.EqualTo(RunStepToolCallKind.CodeInterpreter)); + // Assert.That(details.ToolCalls[0].ToolCallId, Is.Not.Null.And.Not.Empty); + // Assert.That(details.ToolCalls[0].CodeInterpreterInput, Is.Not.Null.And.Not.Empty); + // Assert.That(details.ToolCalls[0].CodeInterpreterOutputs?.Count, Is.GreaterThan(0)); + // Assert.That(details.ToolCalls[0].CodeInterpreterOutputs[0].ImageFileId, Is.Not.Null.And.Not.Empty); + // }); + //} + + //[Test] + //public void SettingResponseFormatWorks() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-4-turbo", new() + // { + // ResponseFormat = AssistantResponseFormat.JsonObject, + // }); + // Validate(assistant); + // Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); + // assistant = client.ModifyAssistant(assistant, new() + // { + // ResponseFormat = AssistantResponseFormat.Text, + // }); + // Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.Text)); + // AssistantThread thread = client.CreateThread(); + // Validate(thread); + // ThreadMessage message = client.CreateMessage(thread, MessageRole.User, ["Write some JSON for me!"]); + // Validate(message); + // ThreadRun run = client.CreateRun(thread, assistant, new() + // { + // ResponseFormat = AssistantResponseFormat.JsonObject, + // }); + // Validate(run); + // Assert.That(run.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); + //} + + //[Test] + //public void FunctionToolsWork() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + // { + // Tools = + // { + // new FunctionToolDefinition() + // { + // FunctionName = "get_favorite_food_for_day_of_week", + // Description = "gets the user's favorite food for a given day of the week, like Tuesday", + // Parameters = BinaryData.FromObjectAsJson(new + // { + // type = "object", + // properties = new + // { + // day_of_week = new + // { + // type = "string", + // description = "a day of the week, like Tuesday or Saturday", + // } + // } + // }), + // }, + // }, + // }); + // Validate(assistant); + // Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + // FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + // Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + // Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + // ThreadRun run = client.CreateThreadAndRun( + // assistant, + // new ThreadCreationOptions() + // { + // InitialMessages = { "What should I eat on Thursday?" }, + // }, + // new RunCreationOptions() + // { + // AdditionalInstructions = "Call provided tools when appropriate.", + // }); + // Validate(run); + + // for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) + // { + // Thread.Sleep(500); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.RequiresAction)); + // Assert.That(run.RequiredActions?.Count, Is.EqualTo(1)); + // Assert.That(run.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); + // Assert.That(run.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + // Assert.That(run.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); + + // run = client.SubmitToolOutputsToRun(run, [new(run.RequiredActions[0].ToolCallId, "tacos")]); + // Assert.That(run.Status.IsTerminal, Is.False); + + // for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) + // { + // Thread.Sleep(500); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + + // PageCollection messagePages = client.GetMessages(run.ThreadId, new MessageCollectionOptions() { Order = ListOrder.NewestFirst }); + // PageResult firstPage = messagePages.GetCurrentPage(); + // Assert.That(firstPage.Values.Count, Is.GreaterThan(1)); + // Assert.That(firstPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + // Assert.That(firstPage.Values[0].Content?[0], Is.Not.Null); + // Assert.That(firstPage.Values[0].Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); + //} + + //[Test] + //public async Task StreamingRunWorks() + //{ + // AssistantClient client = new(); + // Assistant assistant = await client.CreateAssistantAsync("gpt-3.5-turbo"); + // Validate(assistant); + + // AssistantThread thread = await client.CreateThreadAsync(new ThreadCreationOptions() + // { + // InitialMessages = { "Hello there, assistant! How are you today?", }, + // }); + // Validate(thread); + + // Stopwatch stopwatch = Stopwatch.StartNew(); + // void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); + + // AsyncCollectionResult streamingResult + // = client.CreateRunStreamingAsync(thread.Id, assistant.Id); + + // Print(">>> Connected <<<"); + + // await foreach (StreamingUpdate update in streamingResult) + // { + // string message = $"{update.UpdateKind} "; + // if (update is RunUpdate runUpdate) + // { + // message += $"at {update.UpdateKind switch + // { + // StreamingUpdateReason.RunCreated => runUpdate.Value.CreatedAt, + // StreamingUpdateReason.RunQueued => runUpdate.Value.StartedAt, + // StreamingUpdateReason.RunInProgress => runUpdate.Value.StartedAt, + // StreamingUpdateReason.RunCompleted => runUpdate.Value.CompletedAt, + // _ => "???", + // }}"; + // } + // if (update is MessageContentUpdate contentUpdate) + // { + // if (contentUpdate.Role.HasValue) + // { + // message += $"[{contentUpdate.Role}]"; + // } + // message += $"[{contentUpdate.MessageIndex}] {contentUpdate.Text}"; + // } + // Print(message); + // } + // Print(">>> Done <<<"); + //} + + //[TestCase] + //public async Task StreamingToolCall() + //{ + // AssistantClient client = GetTestClient(); + // FunctionToolDefinition getWeatherTool = new("get_current_weather", "Gets the user's current weather"); + // Assistant assistant = await client.CreateAssistantAsync("gpt-3.5-turbo", new() + // { + // Tools = { getWeatherTool } + // }); + // Validate(assistant); + + // Stopwatch stopwatch = Stopwatch.StartNew(); + // void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); + + // Print(" >>> Beginning call ... "); + // AsyncCollectionResult asyncResults = client.CreateThreadAndRunStreamingAsync( + // assistant, + // new() + // { + // InitialMessages = { "What should I wear outside right now?", }, + // }); + // Print(" >>> Starting enumeration ..."); + + // ThreadRun run = null; + + // do + // { + // run = null; + // List toolOutputs = []; + // await foreach (StreamingUpdate update in asyncResults) + // { + // string message = update.UpdateKind.ToString(); + + // if (update is RunUpdate runUpdate) + // { + // message += $" run_id:{runUpdate.Value.Id}"; + // run = runUpdate.Value; + // } + // if (update is RequiredActionUpdate requiredActionUpdate) + // { + // Assert.That(requiredActionUpdate.FunctionName, Is.EqualTo(getWeatherTool.FunctionName)); + // Assert.That(requiredActionUpdate.GetThreadRun().Status, Is.EqualTo(RunStatus.RequiresAction)); + // message += $" {requiredActionUpdate.FunctionName}"; + // toolOutputs.Add(new(requiredActionUpdate.ToolCallId, "warm and sunny")); + // } + // if (update is MessageContentUpdate contentUpdate) + // { + // message += $" {contentUpdate.Text}"; + // } + // Print(message); + // } + // if (toolOutputs.Count > 0) + // { + // asyncResults = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); + // } + // } while (run?.Status.IsTerminal == false); + //} + + //[Test] + //public void BasicFileSearchWorks() + //{ + // // First, we need to upload a simple test file. + // FileClient fileClient = new(); + // OpenAIFileInfo testFile = fileClient.UploadFile( + // BinaryData.FromString(""" + // This file describes the favorite foods of several people. + + // Summanus Ferdinand: tacos + // Tekakwitha Effie: pizza + // Filip Carola: cake + // """).ToStream(), + // "favorite_foods.txt", + // FileUploadPurpose.Assistants); + // Validate(testFile); + + // AssistantClient client = GetTestClient(); + + // // Create an assistant, using the creation helper to make a new vector store + // Assistant assistant = client.CreateAssistant("gpt-4-turbo", new() + // { + // Tools = { new FileSearchToolDefinition() }, + // ToolResources = new() + // { + // FileSearch = new() + // { + // NewVectorStores = + // { + // new VectorStoreCreationHelper([testFile.Id]), + // } + // } + // } + // }); + // Validate(assistant); + // Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); + // string createdVectorStoreId = assistant.ToolResources.FileSearch.VectorStoreIds[0]; + // _vectorStoreIdsToDelete.Add(createdVectorStoreId); + + // // Modify an assistant to use the existing vector store + // assistant = client.ModifyAssistant(assistant, new AssistantModificationOptions() + // { + // ToolResources = new() + // { + // FileSearch = new() + // { + // VectorStoreIds = { assistant.ToolResources.FileSearch.VectorStoreIds[0] }, + // }, + // }, + // }); + // Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); + // Assert.That(assistant.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); + + // // Create a thread with an override vector store + // AssistantThread thread = client.CreateThread(new ThreadCreationOptions() + // { + // InitialMessages = { "Using the files you have available, what's Filip's favorite food?" }, + // ToolResources = new() + // { + // FileSearch = new() + // { + // NewVectorStores = + // { + // new VectorStoreCreationHelper([testFile.Id]) + // } + // } + // } + // }); + // Validate(thread); + // Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); + // createdVectorStoreId = thread.ToolResources.FileSearch.VectorStoreIds[0]; + // _vectorStoreIdsToDelete.Add(createdVectorStoreId); + + // // Ensure that modifying the thread with an existing vector store works + // thread = client.ModifyThread(thread, new ThreadModificationOptions() + // { + // ToolResources = new() + // { + // FileSearch = new() + // { + // VectorStoreIds = { createdVectorStoreId }, + // } + // } + // }); + // Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); + // Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); + + // ThreadRun run = client.CreateRun(thread, assistant); + // Validate(run); + // do + // { + // Thread.Sleep(1000); + // run = client.GetRun(run); + // } while (run?.Status.IsTerminal == false); + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + + // IEnumerable messages = client.GetMessages(thread, new() { Order = ListOrder.NewestFirst }).GetAllValues(); + // int messageCount = 0; + // bool hasCake = false; + // foreach (ThreadMessage message in messages) + // { + // messageCount++; + + // foreach (MessageContent content in message.Content) + // { + // Console.WriteLine(content.Text); + // foreach (TextAnnotation annotation in content.TextAnnotations) + // { + // Console.WriteLine($" --> From file: {annotation.InputFileId}, replacement: {annotation.TextToReplace}"); + // } + + // if (!hasCake) + // { + // hasCake = content.Text.ToLower().Contains("cake"); + // } + // } + // } + // Assert.That(messageCount > 1); + // Assert.That(hasCake, Is.True); + //} [Test] - public void SettingResponseFormatWorks() + public async Task Pagination_CanEnumerateAssistants() { AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-4-turbo", new() - { - ResponseFormat = AssistantResponseFormat.JsonObject, - }); - Validate(assistant); - Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); - assistant = client.ModifyAssistant(assistant, new() - { - ResponseFormat = AssistantResponseFormat.Text, - }); - Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.Text)); - AssistantThread thread = client.CreateThread(); - Validate(thread); - ThreadMessage message = client.CreateMessage(thread, MessageRole.User, ["Write some JSON for me!"]); - Validate(message); - ThreadRun run = client.CreateRun(thread, assistant, new() - { - ResponseFormat = AssistantResponseFormat.JsonObject, - }); - Validate(run); - Assert.That(run.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); - } - [Test] - public void FunctionToolsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + // Create assistant collection + for (int i = 0; i < 10; i++) { - Tools = - { - new FunctionToolDefinition() - { - FunctionName = "get_favorite_food_for_day_of_week", - Description = "gets the user's favorite food for a given day of the week, like Tuesday", - Parameters = BinaryData.FromObjectAsJson(new - { - type = "object", - properties = new - { - day_of_week = new - { - type = "string", - description = "a day of the week, like Tuesday or Saturday", - } - } - }), - }, - }, - }); - Validate(assistant); - Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); - - FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; - Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); - - ThreadRun run = client.CreateThreadAndRun( - assistant, - new ThreadCreationOptions() - { - InitialMessages = { "What should I eat on Thursday?" }, - }, - new RunCreationOptions() + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() { - AdditionalInstructions = "Call provided tools when appropriate.", + Name = $"Test Assistant {i}", }); - Validate(run); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.RequiresAction)); - Assert.That(run.RequiredActions?.Count, Is.EqualTo(1)); - Assert.That(run.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); - Assert.That(run.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(run.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); - - run = client.SubmitToolOutputsToRun(run, [new(run.RequiredActions[0].ToolCallId, "tacos")]); - Assert.That(run.Status.IsTerminal, Is.False); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); + Validate(assistant); + Assert.That(assistant.Name, Is.EqualTo($"Test Assistant {i}")); } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - - PageableCollection messages = client.GetMessages(run.ThreadId, resultOrder: ListOrder.NewestFirst); - Assert.That(messages.Count, Is.GreaterThan(1)); - Assert.That(messages.First().Role, Is.EqualTo(MessageRole.Assistant)); - Assert.That(messages.First().Content?[0], Is.Not.Null); - Assert.That(messages.First().Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); - } - - [Test] - public async Task StreamingRunWorks() - { - AssistantClient client = new(); - Assistant assistant = await client.CreateAssistantAsync("gpt-3.5-turbo"); - Validate(assistant); - AssistantThread thread = await client.CreateThreadAsync(new ThreadCreationOptions() - { - InitialMessages = { "Hello there, assistant! How are you today?", }, - }); - Validate(thread); - - Stopwatch stopwatch = Stopwatch.StartNew(); - void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); - - AsyncResultCollection streamingResult - = client.CreateRunStreamingAsync(thread.Id, assistant.Id); + // Page through collection + int count = 0; + IAsyncEnumerable assistants = client.GetAssistantsAsync(new AssistantCollectionOptions() { Order = ListOrder.NewestFirst }).GetAllValuesAsync(); - Print(">>> Connected <<<"); + int lastIdSeen = int.MaxValue; - await foreach (StreamingUpdate update in streamingResult) + await foreach (Assistant assistant in assistants) { - string message = $"{update.UpdateKind} "; - if (update is RunUpdate runUpdate) + Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); + if (assistant.Name?.StartsWith("Test Assistant ") == true) { - message += $"at {update.UpdateKind switch - { - StreamingUpdateReason.RunCreated => runUpdate.Value.CreatedAt, - StreamingUpdateReason.RunQueued => runUpdate.Value.StartedAt, - StreamingUpdateReason.RunInProgress => runUpdate.Value.StartedAt, - StreamingUpdateReason.RunCompleted => runUpdate.Value.CompletedAt, - _ => "???", - }}"; + Assert.That(int.TryParse(assistant.Name["Test Assistant ".Length..], out int seenId), Is.True); + Assert.That(seenId, Is.LessThan(lastIdSeen)); + lastIdSeen = seenId; } - if (update is MessageContentUpdate contentUpdate) + count++; + if (lastIdSeen == 0 || count > 100) { - if (contentUpdate.Role.HasValue) - { - message += $"[{contentUpdate.Role}]"; - } - message += $"[{contentUpdate.MessageIndex}] {contentUpdate.Text}"; + break; } - Print(message); } - Print(">>> Done <<<"); + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); } - [TestCase] - public async Task StreamingToolCall() + [Test] + public async Task Pagination_CanPageThroughAssistantCollection() { AssistantClient client = GetTestClient(); - FunctionToolDefinition getWeatherTool = new("get_current_weather", "Gets the user's current weather"); - Assistant assistant = await client.CreateAssistantAsync("gpt-3.5-turbo", new() - { - Tools = { getWeatherTool } - }); - Validate(assistant); - Stopwatch stopwatch = Stopwatch.StartNew(); - void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); + // Create assistant collection + for (int i = 0; i < 10; i++) + { + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + { + Name = $"Test Assistant {i}" + }); + Validate(assistant); + Assert.That(assistant.Name, Is.EqualTo($"Test Assistant {i}")); + } - Print(" >>> Beginning call ... "); - AsyncResultCollection asyncResults = client.CreateThreadAndRunStreamingAsync( - assistant, - new() + // Page through collection + int count = 0; + int pageCount = 0; + AsyncPageCollection pages = client.GetAssistantsAsync( + new AssistantCollectionOptions() { - InitialMessages = { "What should I wear outside right now?", }, + Order = ListOrder.NewestFirst, + PageSize = 2 }); - Print(" >>> Starting enumeration ..."); - ThreadRun run = null; + int lastIdSeen = int.MaxValue; - do + await foreach (PageResult page in pages) { - run = null; - List toolOutputs = []; - await foreach (StreamingUpdate update in asyncResults) + foreach (Assistant assistant in page.Values) { - string message = update.UpdateKind.ToString(); - - if (update is RunUpdate runUpdate) - { - message += $" run_id:{runUpdate.Value.Id}"; - run = runUpdate.Value; - } - if (update is RequiredActionUpdate requiredActionUpdate) - { - Assert.That(requiredActionUpdate.FunctionName, Is.EqualTo(getWeatherTool.FunctionName)); - Assert.That(requiredActionUpdate.GetThreadRun().Status, Is.EqualTo(RunStatus.RequiresAction)); - message += $" {requiredActionUpdate.FunctionName}"; - toolOutputs.Add(new(requiredActionUpdate.ToolCallId, "warm and sunny")); - } - if (update is MessageContentUpdate contentUpdate) + Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); + if (assistant.Name?.StartsWith("Test Assistant ") == true) { - message += $" {contentUpdate.Text}"; + Assert.That(int.TryParse(assistant.Name["Test Assistant ".Length..], out int seenId), Is.True); + Assert.That(seenId, Is.LessThan(lastIdSeen)); + lastIdSeen = seenId; } - Print(message); + count++; } - if (toolOutputs.Count > 0) + + pageCount++; + if (lastIdSeen == 0 || count > 100) { - asyncResults = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); + break; } - } while (run?.Status.IsTerminal == false); + } + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); } [Test] - public void BasicFileSearchWorks() + public async Task Pagination_CanRehydrateAssistantPageCollectionFromBytes() { - // First, we need to upload a simple test file. - FileClient fileClient = new(); - OpenAIFileInfo testFile = fileClient.UploadFile( - BinaryData.FromString(""" - This file describes the favorite foods of several people. - - Summanus Ferdinand: tacos - Tekakwitha Effie: pizza - Filip Carola: cake - """).ToStream(), - "favorite_foods.txt", - FileUploadPurpose.Assistants); - Validate(testFile); - AssistantClient client = GetTestClient(); - // Create an assistant, using the creation helper to make a new vector store - Assistant assistant = client.CreateAssistant("gpt-4-turbo", new() + // Create assistant collection + for (int i = 0; i < 10; i++) { - Tools = { new FileSearchToolDefinition() }, - ToolResources = new() + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([testFile.Id]), - } - } - } - }); - Validate(assistant); - Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - string createdVectorStoreId = assistant.ToolResources.FileSearch.VectorStoreIds[0]; - _vectorStoreIdsToDelete.Add(createdVectorStoreId); + Name = $"Test Assistant {i}" + }); + Validate(assistant); + Assert.That(assistant.Name, Is.EqualTo($"Test Assistant {i}")); + } - // Modify an assistant to use the existing vector store - assistant = client.ModifyAssistant(assistant, new AssistantModificationOptions() - { - ToolResources = new() + AsyncPageCollection pages = client.GetAssistantsAsync( + new AssistantCollectionOptions() { - FileSearch = new() - { - VectorStoreIds = { assistant.ToolResources.FileSearch.VectorStoreIds[0] }, - }, - }, - }); - Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - Assert.That(assistant.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); + Order = ListOrder.NewestFirst, + PageSize = 2 + }); - // Create a thread with an override vector store - AssistantThread thread = client.CreateThread(new ThreadCreationOptions() - { - InitialMessages = { "Using the files you have available, what's Filip's favorite food?" }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([testFile.Id]) - } - } - } - }); - Validate(thread); - Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - createdVectorStoreId = thread.ToolResources.FileSearch.VectorStoreIds[0]; - _vectorStoreIdsToDelete.Add(createdVectorStoreId); + // Simulate rehydration of the collection + BinaryData rehydrationBytes = (await pages.GetCurrentPageAsync().ConfigureAwait(false)).PageToken.ToBytes(); + ContinuationToken rehydrationToken = ContinuationToken.FromBytes(rehydrationBytes); - // Ensure that modifying the thread with an existing vector store works - thread = client.ModifyThread(thread, new ThreadModificationOptions() + AsyncPageCollection rehydratedPages = client.GetAssistantsAsync(rehydrationToken); + + int count = 0; + int pageCount = 0; + int lastIdSeen = int.MaxValue; + + await foreach (PageResult page in rehydratedPages) { - ToolResources = new() + foreach (Assistant assistant in page.Values) { - FileSearch = new() + Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); + if (assistant.Name?.StartsWith("Test Assistant ") == true) { - VectorStoreIds = { createdVectorStoreId }, + Assert.That(int.TryParse(assistant.Name["Test Assistant ".Length..], out int seenId), Is.True); + Assert.That(seenId, Is.LessThan(lastIdSeen)); + lastIdSeen = seenId; } + count++; } - }); - Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); - - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); - do - { - Thread.Sleep(1000); - run = client.GetRun(run); - } while (run?.Status.IsTerminal == false); - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); - foreach (ThreadMessage message in messages) - { - foreach (MessageContent content in message.Content) + pageCount++; + if (lastIdSeen == 0 || count > 100) { - Console.WriteLine(content.Text); - foreach (TextAnnotation annotation in content.TextAnnotations) - { - Console.WriteLine($" --> From file: {annotation.InputFileId}, replacement: {annotation.TextToReplace}"); - } + break; } } - Assert.That(messages.Count() > 1); - Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); } [Test] - public async Task CanEnumerateAssistants() + public async Task Pagination_CanRehydrateAssistantPageCollectionFromPageToken() { AssistantClient client = GetTestClient(); @@ -624,28 +793,42 @@ public async Task CanEnumerateAssistants() { Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() { - Name = $"Test Assistant {i}", + Name = $"Test Assistant {i}" }); Validate(assistant); Assert.That(assistant.Name, Is.EqualTo($"Test Assistant {i}")); } - // Page through collection - int count = 0; - AsyncPageableCollection assistants = client.GetAssistantsAsync(ListOrder.NewestFirst); + AsyncPageCollection pages = client.GetAssistantsAsync( + new AssistantCollectionOptions() + { + Order = ListOrder.NewestFirst, + PageSize = 2 + }); + // Call the rehydration method, passing a typed OpenAIPageToken + PageResult firstPage = await pages.GetCurrentPageAsync().ConfigureAwait(false); + AsyncPageCollection rehydratedPages = client.GetAssistantsAsync(firstPage.PageToken); + + int count = 0; + int pageCount = 0; int lastIdSeen = int.MaxValue; - await foreach (Assistant assistant in assistants) + await foreach (PageResult page in rehydratedPages) { - Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); - if (assistant.Name?.StartsWith("Test Assistant ") == true) + foreach (Assistant assistant in page.Values) { - Assert.That(int.TryParse(assistant.Name["Test Assistant ".Length..], out int seenId), Is.True); - Assert.That(seenId, Is.LessThan(lastIdSeen)); - lastIdSeen = seenId; + Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); + if (assistant.Name?.StartsWith("Test Assistant ") == true) + { + Assert.That(int.TryParse(assistant.Name["Test Assistant ".Length..], out int seenId), Is.True); + Assert.That(seenId, Is.LessThan(lastIdSeen)); + lastIdSeen = seenId; + } + count++; } - count++; + + pageCount++; if (lastIdSeen == 0 || count > 100) { break; @@ -653,10 +836,11 @@ public async Task CanEnumerateAssistants() } Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); } [Test] - public async Task CanPageThroughAssistantCollection() + public async Task Pagination_CanCastAssistantPageCollectionToConvenienceFromProtocol() { AssistantClient client = GetTestClient(); @@ -671,17 +855,19 @@ public async Task CanPageThroughAssistantCollection() Assert.That(assistant.Name, Is.EqualTo($"Test Assistant {i}")); } - // Page through collection + // Call the protocol method + IAsyncEnumerable pages = client.GetAssistantsAsync(limit: 2, order: "desc", after: null, before: null, options: default); + + // Cast to the convenience type + AsyncPageCollection assistantPages = (AsyncPageCollection)pages; + int count = 0; int pageCount = 0; - AsyncPageableCollection assistants = client.GetAssistantsAsync(ListOrder.NewestFirst); - IAsyncEnumerable> pages = assistants.AsPages(pageSizeHint: 2); - int lastIdSeen = int.MaxValue; - await foreach (ResultPage page in pages) + await foreach (PageResult page in assistantPages) { - foreach (Assistant assistant in page) + foreach (Assistant assistant in page.Values) { Console.WriteLine($"[{count,3}] {assistant.Id} {assistant.CreatedAt:s} {assistant.Name}"); if (assistant.Name?.StartsWith("Test Assistant ") == true) @@ -704,6 +890,88 @@ public async Task CanPageThroughAssistantCollection() Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); } + //[Test] + //public void Pagination_CanRehydrateRunStepPageCollectionFromBytes() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-4o", new AssistantCreationOptions() + // { + // Tools = { new CodeInterpreterToolDefinition() }, + // Instructions = "You help the user with mathematical descriptions and visualizations.", + // }); + // Validate(assistant); + + // FileClient fileClient = new(); + // OpenAIFileInfo equationFile = fileClient.UploadFile( + // BinaryData.FromString(""" + // x,y + // 2,5 + // 7,14, + // 8,22 + // """).ToStream(), + // "text/csv", + // FileUploadPurpose.Assistants); + // Validate(equationFile); + + // AssistantThread thread = client.CreateThread(new ThreadCreationOptions() + // { + // InitialMessages = + // { + // "Describe the contents of any available tool resource file." + // + " Graph a linear regression and provide the coefficient of correlation." + // + " Explain any code executed to evaluate.", + // }, + // ToolResources = new() + // { + // CodeInterpreter = new() + // { + // FileIds = { equationFile.Id }, + // } + // } + // }); + // Validate(thread); + + // ThreadRun run = client.CreateRun(thread, assistant); + // Validate(run); + + // while (!run.Status.IsTerminal) + // { + // Thread.Sleep(1000); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + // Assert.That(run.Usage?.TotalTokens, Is.GreaterThan(0)); + + // PageCollection pages = client.GetRunSteps(run); + // IEnumerator> pageEnumerator = ((IEnumerable>)pages).GetEnumerator(); + + // // Simulate rehydration of the collection + // BinaryData rehydrationBytes = pages.GetCurrentPage().PageToken.ToBytes(); + // ContinuationToken rehydrationToken = ContinuationToken.FromBytes(rehydrationBytes); + + // PageCollection rehydratedPages = client.GetRunSteps(rehydrationToken); + // IEnumerator> rehydratedPageEnumerator = ((IEnumerable>)rehydratedPages).GetEnumerator(); + + // int pageCount = 0; + + // while (pageEnumerator.MoveNext() && rehydratedPageEnumerator.MoveNext()) + // { + // PageResult page = pageEnumerator.Current; + // PageResult rehydratedPage = rehydratedPageEnumerator.Current; + + // Assert.AreEqual(page.Values.Count, rehydratedPage.Values.Count); + + // for (int i = 0; i < page.Values.Count; i++) + // { + // Assert.AreEqual(page.Values[0].Id, rehydratedPage.Values[0].Id); + // } + + // pageCount++; + // } + + // Assert.That(pageCount, Is.GreaterThanOrEqualTo(1)); + //} + [Test] public async Task MessagesWithRoles() { @@ -723,7 +991,7 @@ public async Task MessagesWithRoles() async Task RefreshMessageListAsync() { messages.Clear(); - await foreach (ThreadMessage message in client.GetMessagesAsync(thread)) + await foreach (ThreadMessage message in client.GetMessagesAsync(thread).GetAllValuesAsync()) { messages.Add(message); } @@ -814,6 +1082,765 @@ public void RunStepDeserialization() Assert.That(deserializedRunStep.Details.ToolCalls[0].CodeInterpreterOutputs[0].Logs, Is.Not.Null.And.Not.Empty); } + #region LRO Tests + + [Test] + public void LRO_ProtocolOnly_Polling_CanWaitForThreadRunToComplete() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + string json = $"{{\"assistant_id\":\"{assistant.Id}\"}}"; + BinaryContent content = BinaryContent.Create(BinaryData.FromString(json)); + + ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread.Id, content); + + PipelineResponse response = runOperation.GetRawResponse(); + using JsonDocument createdJsonDoc = JsonDocument.Parse(response.Content); + string runId = createdJsonDoc.RootElement.GetProperty("id"u8).GetString()!; + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runId)); + + PageResult messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.GreaterThanOrEqualTo(1)); + + runOperation.Wait(); + + response = runOperation.GetRawResponse(); + using JsonDocument completedJsonDoc = JsonDocument.Parse(response.Content); + string status = completedJsonDoc.RootElement.GetProperty("status"u8).GetString()!; + + Assert.That(status, Is.EqualTo(RunStatus.Completed.ToString())); + Assert.That(runOperation.IsCompleted, Is.True); + + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + } + + [Test] + public async Task LRO_ProtocolOnly_Streaming_CanWaitForThreadRunToComplete() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + // Create streaming + string json = $"{{\"assistant_id\":\"{assistant.Id}\", \"stream\":true}}"; + BinaryContent content = BinaryContent.Create(BinaryData.FromString(json)); + RequestOptions options = new() { BufferResponse = false }; + + ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread.Id, content, options); + + // For streaming on protocol, if you call Wait, it will throw. + Assert.Throws(() => runOperation.Wait()); + + // Instead, callers must get the response stream and parse it. + PipelineResponse response = runOperation.GetRawResponse(); + IAsyncEnumerable> events = SseParser.Create( + response.ContentStream, + (_, bytes) => bytes.ToArray()).EnumerateAsync(); + + bool first = true; + string runId = default; + string status = default; + + PageResult messagesPage = default; + + await foreach (var sseItem in events) + { + if (BinaryData.FromBytes(sseItem.Data).ToString() == "[DONE]") + { + continue; + } + + using JsonDocument doc = JsonDocument.Parse(sseItem.Data); + + if (first) + { + Assert.That(sseItem.EventType, Is.EqualTo("thread.run.created")); + + runId = doc.RootElement.GetProperty("id"u8).GetString()!; + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runId)); + + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.GreaterThanOrEqualTo(1)); + + first = false; + } + + string prefix = sseItem.EventType.AsSpan().Slice(0, 11).ToString(); + string suffix = sseItem.EventType.AsSpan().Slice(11).ToString(); + if (prefix == "thread.run." && !suffix.Contains("step.")) + { + status = doc.RootElement.GetProperty("status"u8).GetString()!; + + // Note: the below doesn't work because 'created' isn't a valid status. + //Assert.That(suffix, Is.EqualTo(status)); + } + } + + Assert.That(status, Is.EqualTo(RunStatus.Completed.ToString())); + + // For streaming on protocol, if you read IsCompleted, it will throw. + Assert.Throws(() => { bool b = runOperation.IsCompleted; }); + + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + } + + [Test] + public async Task LRO_ProtocolOnly_Streaming_CanCancelThreadRun() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + string threadId = thread.Id; + string runId = default; + + // Create streaming + string json = $"{{\"assistant_id\":\"{assistant.Id}\", \"stream\":true}}"; + BinaryContent content = BinaryContent.Create(BinaryData.FromString(json)); + RequestOptions options = new() { BufferResponse = false }; + + ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread.Id, content, options); + + // Instead, callers must get the response stream and parse it. + PipelineResponse response = runOperation.GetRawResponse(); + IAsyncEnumerable> events = SseParser.Create( + response.ContentStream, + (_, bytes) => bytes.ToArray()).EnumerateAsync(); + + bool first = true; + string status = default; + + PageResult messagesPage = default; + + await foreach (var sseItem in events) + { + if (BinaryData.FromBytes(sseItem.Data).ToString() == "[DONE]") + { + continue; + } + + using JsonDocument doc = JsonDocument.Parse(sseItem.Data); + + if (first) + { + Assert.That(sseItem.EventType, Is.EqualTo("thread.run.created")); + + runId = doc.RootElement.GetProperty("id"u8).GetString()!; + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runId)); + + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.GreaterThanOrEqualTo(1)); + + first = false; + + // Cancel the run while reading the event stream + runOperation.CancelRun(threadId, runId, options: default); + } + + string prefix = sseItem.EventType.AsSpan().Slice(0, 11).ToString(); + string suffix = sseItem.EventType.AsSpan().Slice(11).ToString(); + if (prefix == "thread.run." && !suffix.Contains("step.")) + { + status = doc.RootElement.GetProperty("status"u8).GetString()!; + } + } + + Assert.That(status, Is.EqualTo(RunStatus.Cancelled.ToString())); + } + + [Test] + public void LRO_Convenience_Polling_CanWaitForThreadRunToComplete() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + // Create polling + ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread, assistant); + + Assert.That(runOperation.IsCompleted, Is.False); + Assert.That(runOperation.ThreadId, Is.EqualTo(thread.Id)); + Assert.That(runOperation.Id, Is.Not.Null); + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Queued)); + Assert.That(runOperation.Value, Is.Not.Null); + Assert.That(runOperation.Value.Id, Is.EqualTo(runOperation.Id)); + + // Wait for operation to complete. + runOperation.Wait(); + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runOperation.Id)); + + Assert.That(runOperation.IsCompleted, Is.True); + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + Assert.That(runOperation.Value.Status, Is.EqualTo(RunStatus.Completed)); + + PageResult messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + } + + [Test] + public void LRO_Convenience_Polling_CanSubmitToolUpdates_Wait() + { + AssistantClient client = GetTestClient(); + + #region Create Assistant with Tools + + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + { + Tools = + { + new FunctionToolDefinition() + { + FunctionName = "get_favorite_food_for_day_of_week", + Description = "gets the user's favorite food for a given day of the week, like Tuesday", + Parameters = BinaryData.FromObjectAsJson(new + { + type = "object", + properties = new + { + day_of_week = new + { + type = "string", + description = "a day of the week, like Tuesday or Saturday", + } + } + }), + }, + }, + }); + Validate(assistant); + Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + #endregion + + #region Create Thread + + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["What should I eat on Thursday?"]); + Validate(message); + + #endregion + + // Create run polling + ThreadRunOperation runOperation = client.CreateRun( + ReturnWhen.Started, + thread, assistant, + new RunCreationOptions() + { + AdditionalInstructions = "Call provided tools when appropriate.", + }); + + while (!runOperation.IsCompleted) + { + runOperation.Wait(); + + if (runOperation.Status == RunStatus.RequiresAction) + { + Assert.That(runOperation.Value.RequiredActions?.Count, Is.EqualTo(1)); + Assert.That(runOperation.Value.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Value.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(runOperation.Value.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Status?.IsTerminal, Is.False); + + IEnumerable outputs = new List { + new ToolOutput(runOperation.Value.RequiredActions[0].ToolCallId, "tacos") + }; + + runOperation.SubmitToolOutputsToRun(outputs); + } + } + + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + + PageCollection messagePages = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.NewestFirst }); + PageResult page = messagePages.GetCurrentPage(); + Assert.That(page.Values.Count, Is.GreaterThan(1)); + Assert.That(page.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(page.Values[0].Content?[0], Is.Not.Null); + Assert.That(page.Values[0].Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); + } + + [Test] + public void LRO_Convenience_Polling_CanSubmitToolUpdates_GetAllUpdates() + { + AssistantClient client = GetTestClient(); + + #region Create Assistant with Tools + + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + { + Tools = + { + new FunctionToolDefinition() + { + FunctionName = "get_favorite_food_for_day_of_week", + Description = "gets the user's favorite food for a given day of the week, like Tuesday", + Parameters = BinaryData.FromObjectAsJson(new + { + type = "object", + properties = new + { + day_of_week = new + { + type = "string", + description = "a day of the week, like Tuesday or Saturday", + } + } + }), + }, + }, + }); + Validate(assistant); + Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + #endregion + + #region Create Thread + + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["What should I eat on Thursday?"]); + Validate(message); + + #endregion + + // Create run polling + ThreadRunOperation runOperation = client.CreateRun( + ReturnWhen.Started, + thread, assistant, + new RunCreationOptions() + { + AdditionalInstructions = "Call provided tools when appropriate.", + }); + + IEnumerable updates = runOperation.GetUpdates(); + + foreach (ThreadRun update in updates) + { + if (update.Status == RunStatus.RequiresAction) + { + Assert.That(runOperation.Value.RequiredActions?.Count, Is.EqualTo(1)); + Assert.That(runOperation.Value.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Value.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(runOperation.Value.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Status?.IsTerminal, Is.False); + + IEnumerable outputs = new List { + new ToolOutput(runOperation.Value.RequiredActions[0].ToolCallId, "tacos") + }; + + runOperation.SubmitToolOutputsToRun(outputs); + } + } + + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + + PageCollection messagePages = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.NewestFirst }); + PageResult page = messagePages.GetCurrentPage(); + Assert.That(page.Values.Count, Is.GreaterThan(1)); + Assert.That(page.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(page.Values[0].Content?[0], Is.Not.Null); + Assert.That(page.Values[0].Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); + } + + [Test] + public void LRO_Convenience_Polling_CanRehydrateRunOperationFromPageToken() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + // Create polling + ThreadRunOperation runOperation = client.CreateRun(ReturnWhen.Started, thread, assistant); + + // Get the rehydration token + ContinuationToken rehydrationToken = runOperation.RehydrationToken; + + // Call the rehydration method + ThreadRunOperation rehydratedRunOperation = client.GetRun(rehydrationToken); + + // Validate operations are equivalent + Assert.That(runOperation.ThreadId, Is.EqualTo(rehydratedRunOperation.ThreadId)); + Assert.That(runOperation.Id, Is.EqualTo(rehydratedRunOperation.Id)); + + // Wait for both to complete + Task.WaitAll( + Task.Run(() => runOperation.Wait()), + Task.Run(() => rehydratedRunOperation.Wait())); + + Assert.That(runOperation.Status, Is.EqualTo(rehydratedRunOperation.Status)); + + // Validate that values from both are equivalent + PageCollection runStepPages = runOperation.GetRunSteps(); + PageCollection rehydratedRunStepPages = rehydratedRunOperation.GetRunSteps(); + + List runSteps = runStepPages.GetAllValues().ToList(); + List rehydratedRunSteps = rehydratedRunStepPages.GetAllValues().ToList(); + + for (int i = 0; i < runSteps.Count; i++) + { + Assert.AreEqual(runSteps[i].Id, rehydratedRunSteps[i].Id); + Assert.AreEqual(runSteps[i].Status, rehydratedRunSteps[i].Status); + } + + Assert.AreEqual(runSteps.Count, rehydratedRunSteps.Count); + Assert.That(runSteps.Count, Is.GreaterThan(0)); + } + + [Test] + public async Task LRO_Convenience_Streaming_CanWaitForThreadRunToComplete() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + // Create streaming + StreamingThreadRunOperation runOperation = client.CreateRunStreaming(thread, assistant); + + // Before the response stream has been enumerated, all the public properties + // should still be null. + Assert.That(runOperation.IsCompleted, Is.False); + Assert.That(runOperation.ThreadId, Is.Null); + Assert.That(runOperation.Id, Is.Null); + Assert.That(runOperation.Status, Is.Null); + Assert.That(runOperation.Value, Is.Null); + + // Wait for operation to complete, as implemented in streaming operation type. + await runOperation.WaitAsync(); + + // Validate that req/response operation work with streaming + IAsyncEnumerable steps = runOperation.GetRunStepsAsync().GetAllValuesAsync(); + Assert.That(await steps.CountAsync(), Is.GreaterThan(0)); + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runOperation.Id)); + + Assert.That(runOperation.IsCompleted, Is.True); + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + Assert.That(runOperation.Value.Status, Is.EqualTo(RunStatus.Completed)); + + PageResult messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + } + + [Test] + public async Task LRO_Convenience_Streaming_CanGetStreamingUpdates() + { + AssistantClient client = GetTestClient(); + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo"); + Validate(assistant); + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); + Validate(message); + + // Create streaming + StreamingThreadRunOperation runOperation = client.CreateRunStreaming(thread, assistant); + + // Before the response stream has been enumerated, all the public properties + // should still be null. + Assert.That(runOperation.IsCompleted, Is.False); + Assert.That(runOperation.ThreadId, Is.Null); + Assert.That(runOperation.Id, Is.Null); + Assert.That(runOperation.Status, Is.Null); + Assert.That(runOperation.Value, Is.Null); + + // Instead of calling Wait for the operation to complete, manually + // enumerate the updates from the update stream. + IAsyncEnumerable updates = runOperation.GetUpdatesStreamingAsync(); + await foreach (StreamingUpdate update in updates) + { + // TODO: we could print messages here, but not critical ATM. + } + + // TODO: add this back once conveniences are available + //ThreadRun retrievedRun = runOperation.GetRun(thread.Id, runOperation.RunId, options: default); + //Assert.That(retrievedRun.Id, Is.EqualTo(run.Id)); + + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(runOperation.Id)); + + Assert.That(runOperation.IsCompleted, Is.True); + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + Assert.That(runOperation.Value.Status, Is.EqualTo(RunStatus.Completed)); + Assert.That(runOperation.ThreadId, Is.EqualTo(thread.Id)); + + PageResult messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.Count, Is.EqualTo(2)); + + Assert.That(messagesPage.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messagesPage.Values[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messagesPage.Values[1].Id, Is.EqualTo(message.Id)); + } + + [Test] + public async Task LRO_Convenience_Streaming_CanSubmitToolUpdates_GetAllUpdates() + { + AssistantClient client = GetTestClient(); + + #region Create Assistant with Tools + + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + { + Tools = + { + new FunctionToolDefinition() + { + FunctionName = "get_favorite_food_for_day_of_week", + Description = "gets the user's favorite food for a given day of the week, like Tuesday", + Parameters = BinaryData.FromObjectAsJson(new + { + type = "object", + properties = new + { + day_of_week = new + { + type = "string", + description = "a day of the week, like Tuesday or Saturday", + } + } + }), + }, + }, + }); + Validate(assistant); + Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + #endregion + + #region Create Thread + + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["What should I eat on Thursday?"]); + Validate(message); + + #endregion + + // Create run streaming + StreamingThreadRunOperation runOperation = client.CreateRunStreaming(thread, assistant, + new RunCreationOptions() + { + AdditionalInstructions = "Call provided tools when appropriate.", + }); + + + IAsyncEnumerable updates = runOperation.GetUpdatesStreamingAsync(); + + await foreach (StreamingUpdate update in updates) + { + if (update is RunUpdate && + runOperation.Status == RunStatus.RequiresAction) + { + Assert.That(runOperation.Value.RequiredActions?.Count, Is.EqualTo(1)); + Assert.That(runOperation.Value.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Value.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(runOperation.Value.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Status?.IsTerminal, Is.False); + + IEnumerable outputs = new List { + new ToolOutput(runOperation.Value.RequiredActions[0].ToolCallId, "tacos") + }; + + runOperation.SubmitToolOutputsToRunStreaming(outputs); + } + } + + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + + PageCollection messagePages = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.NewestFirst }); + PageResult page = messagePages.GetCurrentPage(); + Assert.That(page.Values.Count, Is.GreaterThan(1)); + Assert.That(page.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(page.Values[0].Content?[0], Is.Not.Null); + Assert.That(page.Values[0].Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); + } + + [Test] + public async Task LRO_Convenience_Streaming_CanSubmitToolUpdates_Wait() + { + AssistantClient client = GetTestClient(); + + #region Create Assistant with Tools + + Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + { + Tools = + { + new FunctionToolDefinition() + { + FunctionName = "get_favorite_food_for_day_of_week", + Description = "gets the user's favorite food for a given day of the week, like Tuesday", + Parameters = BinaryData.FromObjectAsJson(new + { + type = "object", + properties = new + { + day_of_week = new + { + type = "string", + description = "a day of the week, like Tuesday or Saturday", + } + } + }), + }, + }, + }); + Validate(assistant); + Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + #endregion + + #region Create Thread + + AssistantThread thread = client.CreateThread(); + Validate(thread); + PageResult runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(0)); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["What should I eat on Thursday?"]); + Validate(message); + + #endregion + + // Create run streaming + StreamingThreadRunOperation runOperation = client.CreateRunStreaming(thread, assistant, + new RunCreationOptions() + { + AdditionalInstructions = "Call provided tools when appropriate.", + }); + + do + { + await runOperation.WaitAsync(); + if (runOperation.Status == RunStatus.RequiresAction) + { + Assert.That(runOperation.Value.RequiredActions?.Count, Is.EqualTo(1)); + Assert.That(runOperation.Value.RequiredActions[0].ToolCallId, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Value.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + Assert.That(runOperation.Value.RequiredActions[0].FunctionArguments, Is.Not.Null.And.Not.Empty); + Assert.That(runOperation.Status?.IsTerminal, Is.False); + + IEnumerable outputs = new List { + new ToolOutput(runOperation.Value.RequiredActions[0].ToolCallId, "tacos") + }; + + runOperation.SubmitToolOutputsToRunStreaming(outputs); + } + } + while (!runOperation.IsCompleted); + + Assert.That(runOperation.Status, Is.EqualTo(RunStatus.Completed)); + + PageCollection messagePages = client.GetMessages(runOperation.ThreadId, new MessageCollectionOptions() { Order = ListOrder.NewestFirst }); + PageResult page = messagePages.GetCurrentPage(); + Assert.That(page.Values.Count, Is.GreaterThan(1)); + Assert.That(page.Values[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(page.Values[0].Content?[0], Is.Not.Null); + Assert.That(page.Values[0].Content[0].Text.ToLowerInvariant(), Does.Contain("tacos")); + } + + #endregion + [OneTimeTearDown] protected void Cleanup() { @@ -885,6 +1912,11 @@ private void Validate(T target) { Assert.That(run?.Id, Is.Not.Null); } + //else if (target is ThreadRunOperation runOperation) + //{ + // Assert.That(runOperation?.ThreadId, Is.Not.Null); + // Assert.That(runOperation?.RunId, Is.Not.Null); + //} else if (target is OpenAIFileInfo file) { Assert.That(file?.Id, Is.Not.Null); diff --git a/tests/Assistants/VectorStoreTests.cs b/tests/Assistants/VectorStoreTests.cs index 3643068ea..8b959e08c 100644 --- a/tests/Assistants/VectorStoreTests.cs +++ b/tests/Assistants/VectorStoreTests.cs @@ -75,7 +75,7 @@ public void CanCreateGetAndDeleteVectorStores() Assert.That(deleted, Is.True); _vectorStoresToDelete.RemoveAt(_vectorStoresToDelete.Count - 1); - vectorStore = client.CreateVectorStore(new VectorStoreCreationOptions () + vectorStore = client.CreateVectorStore(new VectorStoreCreationOptions() { FileIds = testFiles.Select(file => file.Id).ToList() }); @@ -104,7 +104,7 @@ public void CanEnumerateVectorStores() int lastIdSeen = int.MaxValue; int count = 0; - foreach (VectorStore vectorStore in client.GetVectorStores(ListOrder.NewestFirst)) + foreach (VectorStore vectorStore in client.GetVectorStores(new VectorStoreCollectionOptions() { Order = ListOrder.NewestFirst }).GetAllValues()) { Assert.That(vectorStore.Id, Is.Not.Null); if (vectorStore.Name?.StartsWith("Test Vector Store ") == true) @@ -141,7 +141,7 @@ public async Task CanEnumerateVectorStoresAsync() int lastIdSeen = int.MaxValue; int count = 0; - await foreach (VectorStore vectorStore in client.GetVectorStoresAsync(ListOrder.NewestFirst)) + await foreach (VectorStore vectorStore in client.GetVectorStoresAsync(new VectorStoreCollectionOptions() { Order = ListOrder.NewestFirst }).GetAllValuesAsync()) { Assert.That(vectorStore.Id, Is.Not.Null); if (vectorStore.Name?.StartsWith("Test Vector Store ") == true) @@ -192,7 +192,7 @@ public void CanAssociateFiles() Thread.Sleep(1000); int count = 0; - foreach (VectorStoreFileAssociation association in client.GetFileAssociations(vectorStore)) + foreach (VectorStoreFileAssociation association in client.GetFileAssociations(vectorStore).GetAllValues()) { count++; Assert.That(association.FileId, Is.Not.EqualTo(files[0].Id)); @@ -201,6 +201,69 @@ public void CanAssociateFiles() Assert.That(count, Is.EqualTo(2)); } + [Test] + public void Pagination_CanRehydrateFileAssociationCollection() + { + VectorStoreClient client = GetTestClient(); + VectorStore vectorStore = client.CreateVectorStore(); + Validate(vectorStore); + + IReadOnlyList files = GetNewTestFiles(3); + + foreach (OpenAIFileInfo file in files) + { + VectorStoreFileAssociation association = client.AddFileToVectorStore(vectorStore, file); + Validate(association); + Assert.Multiple(() => + { + Assert.That(association.FileId, Is.EqualTo(file.Id)); + Assert.That(association.VectorStoreId, Is.EqualTo(vectorStore.Id)); + Assert.That(association.LastError, Is.Null); + Assert.That(association.CreatedAt, Is.GreaterThan(s_2024)); + Assert.That(association.Status, Is.EqualTo(VectorStoreFileAssociationStatus.InProgress)); + }); + } + + bool removed = client.RemoveFileFromStore(vectorStore, files[0]); + Assert.True(removed); + _associationsToRemove.RemoveAt(0); + + // Errata: removals aren't immediately reflected when requesting the list + Thread.Sleep(1000); + + PageCollection pages = client.GetFileAssociations(vectorStore); + IEnumerator> pageEnumerator = ((IEnumerable>)pages).GetEnumerator(); + + // Simulate rehydration of the collection + BinaryData rehydrationBytes = pages.GetCurrentPage().PageToken.ToBytes(); + ContinuationToken rehydrationToken = ContinuationToken.FromBytes(rehydrationBytes); + + PageCollection rehydratedPages = client.GetFileAssociations(rehydrationToken); + IEnumerator> rehydratedPageEnumerator = ((IEnumerable>)rehydratedPages).GetEnumerator(); + + int pageCount = 0; + + while (pageEnumerator.MoveNext() && rehydratedPageEnumerator.MoveNext()) + { + PageResult page = pageEnumerator.Current; + PageResult rehydratedPage = rehydratedPageEnumerator.Current; + + Assert.AreEqual(page.Values.Count, rehydratedPage.Values.Count); + + for (int i = 0; i < page.Values.Count; i++) + { + Assert.AreEqual(page.Values[0].FileId, rehydratedPage.Values[0].FileId); + Assert.AreEqual(page.Values[0].VectorStoreId, rehydratedPage.Values[0].VectorStoreId); + Assert.AreEqual(page.Values[0].CreatedAt, rehydratedPage.Values[0].CreatedAt); + Assert.AreEqual(page.Values[0].Size, rehydratedPage.Values[0].Size); + } + + pageCount++; + } + + Assert.That(pageCount, Is.GreaterThanOrEqualTo(1)); + } + [Test] public void CanUseBatchIngestion() { @@ -225,7 +288,7 @@ public void CanUseBatchIngestion() Thread.Sleep(500); } - foreach (VectorStoreFileAssociation association in client.GetFileAssociations(batchJob)) + foreach (VectorStoreFileAssociation association in client.GetFileAssociations(batchJob).GetAllValues()) { Assert.Multiple(() => { @@ -271,9 +334,9 @@ public async Task CanApplyChunkingStrategy(ChunkingStrategyKind strategyKind) Validate(vectorStore); Assert.That(vectorStore.FileCounts.Total, Is.EqualTo(5)); - AsyncPageableCollection associations = client.GetFileAssociationsAsync(vectorStore); + AsyncPageCollection associations = client.GetFileAssociationsAsync(vectorStore); - await foreach (VectorStoreFileAssociation association in associations) + await foreach (VectorStoreFileAssociation association in associations.GetAllValuesAsync()) { Assert.That(testFiles.Any(file => file.Id == association.FileId), Is.True); Assert.That(association.ChunkingStrategy, Is.InstanceOf()); diff --git a/tests/Chat/ChatClientTests.cs b/tests/Chat/ChatClientTests.cs index 1acc37da5..e8d3a6f6a 100644 --- a/tests/Chat/ChatClientTests.cs +++ b/tests/Chat/ChatClientTests.cs @@ -77,8 +77,8 @@ public void StreamingChat() TimeSpan? latestTokenReceiptTime = null; Stopwatch stopwatch = Stopwatch.StartNew(); - ResultCollection streamingResult = client.CompleteChatStreaming(messages); - Assert.That(streamingResult, Is.InstanceOf>()); + CollectionResult streamingResult = client.CompleteChatStreaming(messages); + Assert.That(streamingResult, Is.InstanceOf>()); int updateCount = 0; foreach (StreamingChatCompletionUpdate chatUpdate in streamingResult) @@ -109,8 +109,8 @@ public async Task StreamingChatAsync() TimeSpan? latestTokenReceiptTime = null; Stopwatch stopwatch = Stopwatch.StartNew(); - AsyncResultCollection streamingResult = client.CompleteChatStreamingAsync(messages); - Assert.That(streamingResult, Is.InstanceOf>()); + AsyncCollectionResult streamingResult = client.CompleteChatStreamingAsync(messages); + Assert.That(streamingResult, Is.InstanceOf>()); int updateCount = 0; ChatTokenUsage usage = null; @@ -338,7 +338,7 @@ public async Task TokenLogProbabilitiesStreaming(bool includeLogProbabilities) options = new(); } - AsyncResultCollection chatCompletionUpdates = client.CompleteChatStreamingAsync(messages, options); + AsyncCollectionResult chatCompletionUpdates = client.CompleteChatStreamingAsync(messages, options); Assert.That(chatCompletionUpdates, Is.Not.Null); await foreach (StreamingChatCompletionUpdate chatCompletionUpdate in chatCompletionUpdates) diff --git a/tests/OpenAI.Tests.csproj b/tests/OpenAI.Tests.csproj index b33b036ea..9c4038587 100644 --- a/tests/OpenAI.Tests.csproj +++ b/tests/OpenAI.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/tests/Utility/System.Net.ServerSentEvents.cs b/tests/Utility/System.Net.ServerSentEvents.cs new file mode 100644 index 000000000..ea0b4f818 --- /dev/null +++ b/tests/Utility/System.Net.ServerSentEvents.cs @@ -0,0 +1,623 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This file contains a source copy of: +// https://github.com/dotnet/runtime/tree/2bd15868f12ace7cee9999af61d5c130b2603f04/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents +// Once the System.Net.ServerSentEvents package is available, this file should be removed and replaced with a package reference. +// +// The only changes made to this code from the original are: +// - Enabled nullable reference types at file scope, and use a few null suppression operators to work around the lack of [NotNull] +// - Put into a single file for ease of management (it should not be edited in this repo). +// - Changed public types to be internal. +// - Removed a use of a [NotNull] attribute to assist in netstandard2.0 compilation. +// - Replaced a reference to a .resx string with an inline constant. + +#nullable enable + +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace System.Net.ServerSentEvents +{ + /// Represents a server-sent event. + /// Specifies the type of data payload in the event. + internal readonly struct SseItem + { + /// Initializes the server-sent event. + /// The event's payload. + /// The event's type. + public SseItem(T data, string eventType) + { + Data = data; + EventType = eventType; + } + + /// Gets the event's payload. + public T Data { get; } + + /// Gets the event's type. + public string EventType { get; } + } + + /// Encapsulates a method for parsing the bytes payload of a server-sent event. + /// Specifies the type of the return value of the parser. + /// The event's type. + /// The event's payload bytes. + /// The parsed . + internal delegate T SseItemParser(string eventType, ReadOnlySpan data); + + /// Provides a parser for parsing server-sent events. + internal static class SseParser + { + /// The default ("message") for an event that did not explicitly specify a type. + public const string EventTypeDefault = "message"; + + /// Creates a parser for parsing a of server-sent events into a sequence of values. + /// The stream containing the data to parse. + /// + /// The enumerable of strings, which may be enumerated synchronously or asynchronously. The strings + /// are decoded from the UTF8-encoded bytes of the payload of each event. + /// + /// is null. + /// + /// This overload has behavior equivalent to calling with a delegate + /// that decodes the data of each event using 's GetString method. + /// + public static SseParser Create(Stream sseStream) => + Create(sseStream, static (_, bytes) => Utf8GetString(bytes)); + + /// Creates a parser for parsing a of server-sent events into a sequence of values. + /// Specifies the type of data in each event. + /// The stream containing the data to parse. + /// The parser to use to transform each payload of bytes into a data element. + /// The enumerable, which may be enumerated synchronously or asynchronously. + /// is null. + /// is null. + public static SseParser Create(Stream sseStream, SseItemParser itemParser) => + new SseParser( + sseStream ?? throw new ArgumentNullException(nameof(sseStream)), + itemParser ?? throw new ArgumentNullException(nameof(itemParser))); + + /// Encoding.UTF8.GetString(bytes) + internal static string Utf8GetString(ReadOnlySpan bytes) + { +#if NET + return Encoding.UTF8.GetString(bytes); +#else + unsafe + { + fixed (byte* ptr = bytes) + { + return ptr is null ? + string.Empty : + Encoding.UTF8.GetString(ptr, bytes.Length); + } + } +#endif + } + } + + /// Provides a parser for server-sent events information. + /// Specifies the type of data parsed from an event. + internal sealed class SseParser + { + // For reference: + // Specification: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events + + /// Carriage Return. + private const byte CR = (byte)'\r'; + /// Line Feed. + private const byte LF = (byte)'\n'; + /// Carriage Return Line Feed. + private static ReadOnlySpan CRLF => "\r\n"u8; + + /// The default size of an ArrayPool buffer to rent. + /// Larger size used by default to minimize number of reads. Smaller size used in debug to stress growth/shifting logic. + private const int DefaultArrayPoolRentSize = +#if DEBUG + 16; +#else + 1024; +#endif + + /// The stream to be parsed. + private readonly Stream _stream; + /// The parser delegate used to transform bytes into a . + private readonly SseItemParser _itemParser; + + /// Indicates whether the enumerable has already been used for enumeration. + private int _used; + + /// Buffer, either empty or rented, containing the data being read from the stream while looking for the next line. + private byte[] _lineBuffer = []; + /// The starting offset of valid data in . + private int _lineOffset; + /// The length of valid data in , starting from . + private int _lineLength; + /// The index in where a newline ('\r', '\n', or "\r\n") was found. + private int _newlineIndex; + /// The index in of characters already checked for newlines. + /// + /// This is to avoid O(LineLength^2) behavior in the rare case where we have long lines that are built-up over multiple reads. + /// We want to avoid re-checking the same characters we've already checked over and over again. + /// + private int _lastSearchedForNewline; + /// Set when eof has been reached in the stream. + private bool _eof; + + /// Rented buffer containing buffered data for the next event. + private byte[]? _dataBuffer; + /// The length of valid data in , starting from index 0. + private int _dataLength; + /// Whether data has been appended to . + /// This can be different than != 0 if empty data was appended. + private bool _dataAppended; + + /// The event type for the next event. + private string _eventType = SseParser.EventTypeDefault; + + /// Initialize the enumerable. + /// The stream to parse. + /// The function to use to parse payload bytes into a . + internal SseParser(Stream stream, SseItemParser itemParser) + { + _stream = stream; + _itemParser = itemParser; + } + + /// Gets an enumerable of the server-sent events from this parser. + /// The parser has already been enumerated. Such an exception may propagate out of a call to . + public IEnumerable> Enumerate() + { + // Validate that the parser is only used for one enumeration. + ThrowIfNotFirstEnumeration(); + + // Rent a line buffer. This will grow as needed. The line buffer is what's passed to the stream, + // so we want it to be large enough to reduce the number of reads we need to do when data is + // arriving quickly. (In debug, we use a smaller buffer to stress the growth and shifting logic.) + _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); + try + { + // Spec: "Event streams in this format must always be encoded as UTF-8". + // Skip a UTF8 BOM if it exists at the beginning of the stream. (The BOM is defined as optional in the SSE grammar.) + while (FillLineBuffer() != 0 && _lineLength < Utf8Bom.Length) ; + SkipBomIfPresent(); + + // Process all events in the stream. + while (true) + { + // See if there's a complete line in data already read from the stream. Lines are permitted to + // end with CR, LF, or CRLF. Look for all of them and if we find one, process the line. However, + // if we only find a CR and it's at the end of the read data, don't process it now, as we want + // to process it together with an LF that might immediately follow, rather than treating them + // as two separate characters, in which case we'd incorrectly process the CR as a line by itself. + GetNextSearchOffsetAndLength(out int searchOffset, out int searchLength); + _newlineIndex = _lineBuffer.AsSpan(searchOffset, searchLength).IndexOfAny(CR, LF); + if (_newlineIndex >= 0) + { + _lastSearchedForNewline = -1; + _newlineIndex += searchOffset; + if (_lineBuffer[_newlineIndex] is LF || // the newline is LF + _newlineIndex - _lineOffset + 1 < _lineLength || // we must have CR and we have whatever comes after it + _eof) // if we get here, we know we have a CR at the end of the buffer, so it's definitely the whole newline if we've hit EOF + { + // Process the line. + if (ProcessLine(out SseItem sseItem, out int advance)) + { + yield return sseItem; + } + + // Move past the line. + _lineOffset += advance; + _lineLength -= advance; + continue; + } + } + else + { + // Record the last position searched for a newline. The next time we search, + // we'll search from here rather than from _lineOffset, in order to avoid searching + // the same characters again. + _lastSearchedForNewline = _lineOffset + _lineLength; + } + + // We've processed everything in the buffer we currently can, so if we've already read EOF, we're done. + if (_eof) + { + // Spec: "Once the end of the file is reached, any pending data must be discarded. (If the file ends in the middle of an + // event, before the final empty line, the incomplete event is not dispatched.)" + break; + } + + // Read more data into the buffer. + FillLineBuffer(); + } + } + finally + { + ArrayPool.Shared.Return(_lineBuffer); + if (_dataBuffer is not null) + { + ArrayPool.Shared.Return(_dataBuffer); + } + } + } + + /// Gets an asynchronous enumerable of the server-sent events from this parser. + /// The cancellation token to use to cancel the enumeration. + /// The parser has already been enumerated. Such an exception may propagate out of a call to . + /// The enumeration was canceled. Such an exception may propagate out of a call to . + public async IAsyncEnumerable> EnumerateAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Validate that the parser is only used for one enumeration. + ThrowIfNotFirstEnumeration(); + + // Rent a line buffer. This will grow as needed. The line buffer is what's passed to the stream, + // so we want it to be large enough to reduce the number of reads we need to do when data is + // arriving quickly. (In debug, we use a smaller buffer to stress the growth and shifting logic.) + _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); + try + { + // Spec: "Event streams in this format must always be encoded as UTF-8". + // Skip a UTF8 BOM if it exists at the beginning of the stream. (The BOM is defined as optional in the SSE grammar.) + while (await FillLineBufferAsync(cancellationToken).ConfigureAwait(false) != 0 && _lineLength < Utf8Bom.Length) ; + SkipBomIfPresent(); + + // Process all events in the stream. + while (true) + { + // See if there's a complete line in data already read from the stream. Lines are permitted to + // end with CR, LF, or CRLF. Look for all of them and if we find one, process the line. However, + // if we only find a CR and it's at the end of the read data, don't process it now, as we want + // to process it together with an LF that might immediately follow, rather than treating them + // as two separate characters, in which case we'd incorrectly process the CR as a line by itself. + GetNextSearchOffsetAndLength(out int searchOffset, out int searchLength); + _newlineIndex = _lineBuffer.AsSpan(searchOffset, searchLength).IndexOfAny(CR, LF); + if (_newlineIndex >= 0) + { + _lastSearchedForNewline = -1; + _newlineIndex += searchOffset; + if (_lineBuffer[_newlineIndex] is LF || // newline is LF + _newlineIndex - _lineOffset + 1 < _lineLength || // newline is CR, and we have whatever comes after it + _eof) // if we get here, we know we have a CR at the end of the buffer, so it's definitely the whole newline if we've hit EOF + { + // Process the line. + if (ProcessLine(out SseItem sseItem, out int advance)) + { + yield return sseItem; + } + + // Move past the line. + _lineOffset += advance; + _lineLength -= advance; + continue; + } + } + else + { + // Record the last position searched for a newline. The next time we search, + // we'll search from here rather than from _lineOffset, in order to avoid searching + // the same characters again. + _lastSearchedForNewline = searchOffset + searchLength; + } + + // We've processed everything in the buffer we currently can, so if we've already read EOF, we're done. + if (_eof) + { + // Spec: "Once the end of the file is reached, any pending data must be discarded. (If the file ends in the middle of an + // event, before the final empty line, the incomplete event is not dispatched.)" + break; + } + + // Read more data into the buffer. + await FillLineBufferAsync(cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(_lineBuffer); + if (_dataBuffer is not null) + { + ArrayPool.Shared.Return(_dataBuffer); + } + } + } + + /// Gets the next index and length with which to perform a newline search. + private void GetNextSearchOffsetAndLength(out int searchOffset, out int searchLength) + { + if (_lastSearchedForNewline > _lineOffset) + { + searchOffset = _lastSearchedForNewline; + searchLength = _lineLength - (_lastSearchedForNewline - _lineOffset); + } + else + { + searchOffset = _lineOffset; + searchLength = _lineLength; + } + + Debug.Assert(searchOffset >= _lineOffset, $"{searchOffset}, {_lineLength}"); + Debug.Assert(searchOffset <= _lineOffset + _lineLength, $"{searchOffset}, {_lineOffset}, {_lineLength}"); + Debug.Assert(searchOffset <= _lineBuffer.Length, $"{searchOffset}, {_lineBuffer.Length}"); + + Debug.Assert(searchLength >= 0, $"{searchLength}"); + Debug.Assert(searchLength <= _lineLength, $"{searchLength}, {_lineLength}"); + } + + private int GetNewLineLength() + { + Debug.Assert(_newlineIndex - _lineOffset < _lineLength, "Expected to be positioned at a non-empty newline"); + return _lineBuffer.AsSpan(_newlineIndex, _lineLength - (_newlineIndex - _lineOffset)).StartsWith(CRLF) ? 2 : 1; + } + + /// + /// If there's no room remaining in the line buffer, either shifts the contents + /// left or grows the buffer in order to make room for the next read. + /// + private void ShiftOrGrowLineBufferIfNecessary() + { + // If data we've read is butting up against the end of the buffer and + // it's not taking up the entire buffer, slide what's there down to + // the beginning, making room to read more data into the buffer (since + // there's no newline in the data that's there). Otherwise, if the whole + // buffer is full, grow the buffer to accommodate more data, since, again, + // what's there doesn't contain a newline and thus a line is longer than + // the current buffer accommodates. + if (_lineOffset + _lineLength == _lineBuffer.Length) + { + if (_lineOffset != 0) + { + _lineBuffer.AsSpan(_lineOffset, _lineLength).CopyTo(_lineBuffer); + if (_lastSearchedForNewline >= 0) + { + _lastSearchedForNewline -= _lineOffset; + } + _lineOffset = 0; + } + else if (_lineLength == _lineBuffer.Length) + { + GrowBuffer(ref _lineBuffer!, _lineBuffer.Length * 2); + } + } + } + + /// Processes a complete line from the SSE stream. + /// The parsed item if the method returns true. + /// How many characters to advance in the line buffer. + /// true if an SSE item was successfully parsed; otherwise, false. + private bool ProcessLine(out SseItem sseItem, out int advance) + { + ReadOnlySpan line = _lineBuffer.AsSpan(_lineOffset, _newlineIndex - _lineOffset); + + // Spec: "If the line is empty (a blank line) Dispatch the event" + if (line.IsEmpty) + { + advance = GetNewLineLength(); + + if (_dataAppended) + { + sseItem = new SseItem(_itemParser(_eventType, _dataBuffer.AsSpan(0, _dataLength)), _eventType); + _eventType = SseParser.EventTypeDefault; + _dataLength = 0; + _dataAppended = false; + return true; + } + + sseItem = default; + return false; + } + + // Find the colon separating the field name and value. + int colonPos = line.IndexOf((byte)':'); + ReadOnlySpan fieldName; + ReadOnlySpan fieldValue; + if (colonPos >= 0) + { + // Spec: "Collect the characters on the line before the first U+003A COLON character (:), and let field be that string." + fieldName = line.Slice(0, colonPos); + + // Spec: "Collect the characters on the line after the first U+003A COLON character (:), and let value be that string. + // If value starts with a U+0020 SPACE character, remove it from value." + fieldValue = line.Slice(colonPos + 1); + if (!fieldValue.IsEmpty && fieldValue[0] == (byte)' ') + { + fieldValue = fieldValue.Slice(1); + } + } + else + { + // Spec: "using the whole line as the field name, and the empty string as the field value." + fieldName = line; + fieldValue = []; + } + + if (fieldName.SequenceEqual("data"u8)) + { + // Spec: "Append the field value to the data buffer, then append a single U+000A LINE FEED (LF) character to the data buffer." + // Spec: "If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer." + + // If there's nothing currently in the data buffer and we can easily detect that this line is immediately followed by + // an empty line, we can optimize it to just handle the data directly from the line buffer, rather than first copying + // into the data buffer and dispatching from there. + if (!_dataAppended) + { + int newlineLength = GetNewLineLength(); + ReadOnlySpan remainder = _lineBuffer.AsSpan(_newlineIndex + newlineLength, _lineLength - line.Length - newlineLength); + if (!remainder.IsEmpty && + (remainder[0] is LF || (remainder[0] is CR && remainder.Length > 1))) + { + advance = line.Length + newlineLength + (remainder.StartsWith(CRLF) ? 2 : 1); + sseItem = new SseItem(_itemParser(_eventType, fieldValue), _eventType); + _eventType = SseParser.EventTypeDefault; + return true; + } + } + + // We need to copy the data from the data buffer to the line buffer. Make sure there's enough room. + if (_dataBuffer is null || _dataLength + _lineLength + 1 > _dataBuffer.Length) + { + GrowBuffer(ref _dataBuffer, _dataLength + _lineLength + 1); + } + + // Append a newline if there's already content in the buffer. + // Then copy the field value to the data buffer + if (_dataAppended) + { + _dataBuffer![_dataLength++] = LF; + } + fieldValue.CopyTo(_dataBuffer.AsSpan(_dataLength)); + _dataLength += fieldValue.Length; + _dataAppended = true; + } + else if (fieldName.SequenceEqual("event"u8)) + { + // Spec: "Set the event type buffer to field value." + _eventType = SseParser.Utf8GetString(fieldValue); + } + else if (fieldName.SequenceEqual("id"u8)) + { + // Spec: "If the field value does not contain U+0000 NULL, then set the last event ID buffer to the field value. Otherwise, ignore the field." + if (fieldValue.IndexOf((byte)'\0') < 0) + { + // Note that fieldValue might be empty, in which case LastEventId will naturally be reset to the empty string. This is per spec. + LastEventId = SseParser.Utf8GetString(fieldValue); + } + } + else if (fieldName.SequenceEqual("retry"u8)) + { + // Spec: "If the field value consists of only ASCII digits, then interpret the field value as an integer in base ten, + // and set the event stream's reconnection time to that integer. Otherwise, ignore the field." + if (long.TryParse( +#if NET7_0_OR_GREATER + fieldValue, +#else + SseParser.Utf8GetString(fieldValue), +#endif + NumberStyles.None, CultureInfo.InvariantCulture, out long milliseconds)) + { + ReconnectionInterval = TimeSpan.FromMilliseconds(milliseconds); + } + } + else + { + // We'll end up here if the line starts with a colon, producing an empty field name, or if the field name is otherwise unrecognized. + // Spec: "If the line starts with a U+003A COLON character (:) Ignore the line." + // Spec: "Otherwise, The field is ignored" + } + + advance = line.Length + GetNewLineLength(); + sseItem = default; + return false; + } + + /// Gets the last event ID. + /// This value is updated any time a new last event ID is parsed. It is not reset between SSE items. + public string LastEventId { get; private set; } = string.Empty; // Spec: "must be initialized to the empty string" + + /// Gets the reconnection interval. + /// + /// If no retry event was received, this defaults to , and it will only + /// ever be in that situation. If a client wishes to retry, the server-sent + /// events specification states that the interval may then be decided by the client implementation and should be a + /// few seconds. + /// + public TimeSpan ReconnectionInterval { get; private set; } = Timeout.InfiniteTimeSpan; + + /// Transitions the object to a used state, throwing if it's already been used. + private void ThrowIfNotFirstEnumeration() + { + if (Interlocked.Exchange(ref _used, 1) != 0) + { + throw new InvalidOperationException("The enumerable may be enumerated only once."); + } + } + + /// Reads data from the stream into the line buffer. + private int FillLineBuffer() + { + ShiftOrGrowLineBufferIfNecessary(); + + int offset = _lineOffset + _lineLength; + int bytesRead = _stream.Read( +#if NET + _lineBuffer.AsSpan(offset)); +#else + _lineBuffer, offset, _lineBuffer.Length - offset); +#endif + + if (bytesRead > 0) + { + _lineLength += bytesRead; + } + else + { + _eof = true; + bytesRead = 0; + } + + return bytesRead; + } + + /// Reads data asynchronously from the stream into the line buffer. + private async ValueTask FillLineBufferAsync(CancellationToken cancellationToken) + { + ShiftOrGrowLineBufferIfNecessary(); + + int offset = _lineOffset + _lineLength; + int bytesRead = await +#if NET + _stream.ReadAsync(_lineBuffer.AsMemory(offset), cancellationToken) +#else + new ValueTask(_stream.ReadAsync(_lineBuffer, offset, _lineBuffer.Length - offset, cancellationToken)) +#endif + .ConfigureAwait(false); + + if (bytesRead > 0) + { + _lineLength += bytesRead; + } + else + { + _eof = true; + bytesRead = 0; + } + + return bytesRead; + } + + /// Gets the UTF8 BOM. + private static ReadOnlySpan Utf8Bom => [0xEF, 0xBB, 0xBF]; + + /// Called at the beginning of processing to skip over an optional UTF8 byte order mark. + private void SkipBomIfPresent() + { + Debug.Assert(_lineOffset == 0, $"Expected _lineOffset == 0, got {_lineOffset}"); + + if (_lineBuffer.AsSpan(0, _lineLength).StartsWith(Utf8Bom)) + { + _lineOffset += 3; + _lineLength -= 3; + } + } + + /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. + private static void GrowBuffer(ref byte[]? buffer, int minimumLength) + { + byte[]? toReturn = buffer; + buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); + if (toReturn is not null) + { + Array.Copy(toReturn, buffer, toReturn.Length); + ArrayPool.Shared.Return(toReturn); + } + } + } +} \ No newline at end of file