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..549ea3db0 100644 --- a/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs +++ b/examples/Assistants/Example01_RetrievalAugmentedGeneration.cs @@ -98,8 +98,9 @@ public void Example01_RetrievalAugmentedGeneration() } while (!threadRun.Status.IsTerminal); // Finally, we'll print out the full history for the thread that includes the augmented generation - PageableCollection messages - = assistantClient.GetMessages(threadRun.ThreadId, ListOrder.OldestFirst); + PageCollection messagePages + = assistantClient.GetMessages(threadRun.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); + IEnumerable messages = messagePages.GetAllValues(); foreach (ThreadMessage message in messages) { diff --git a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs index 6cde83f33..83b95fb0e 100644 --- a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs +++ b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs @@ -99,8 +99,9 @@ public async Task Example01_RetrievalAugmentedGenerationAsync() } while (!threadRun.Status.IsTerminal); // Finally, we'll print out the full history for the thread that includes the augmented generation - AsyncPageableCollection messages - = assistantClient.GetMessagesAsync(threadRun.ThreadId, ListOrder.OldestFirst); + AsyncPageCollection messagePages + = assistantClient.GetMessagesAsync(threadRun.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); + IAsyncEnumerable messages = messagePages.GetAllValuesAsync(); await foreach (ThreadMessage message in messages) { diff --git a/examples/Assistants/Example02_FunctionCalling.cs b/examples/Assistants/Example02_FunctionCalling.cs index a60491379..a0eb05637 100644 --- a/examples/Assistants/Example02_FunctionCalling.cs +++ b/examples/Assistants/Example02_FunctionCalling.cs @@ -151,8 +151,9 @@ string GetCurrentWeather(string location, string unit = "celsius") // 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); + PageCollection messagePages + = client.GetMessages(run.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); + IEnumerable messages = messagePages.GetAllValues(); foreach (ThreadMessage message in messages) { diff --git a/examples/Assistants/Example02_FunctionCallingAsync.cs b/examples/Assistants/Example02_FunctionCallingAsync.cs index 08bbd0bde..2ee924c56 100644 --- a/examples/Assistants/Example02_FunctionCallingAsync.cs +++ b/examples/Assistants/Example02_FunctionCallingAsync.cs @@ -151,8 +151,9 @@ string GetCurrentWeather(string location, string unit = "celsius") // 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); + AsyncPageCollection messagePages + = client.GetMessagesAsync(run.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); + IAsyncEnumerable messages = messagePages.GetAllValuesAsync(); await foreach (ThreadMessage message in messages) { diff --git a/examples/Assistants/Example02b_FunctionCallingStreaming.cs b/examples/Assistants/Example02b_FunctionCallingStreaming.cs index cb3bf7343..9c3e0adfc 100644 --- a/examples/Assistants/Example02b_FunctionCallingStreaming.cs +++ b/examples/Assistants/Example02b_FunctionCallingStreaming.cs @@ -91,8 +91,7 @@ public async Task Example02b_FunctionCallingStreaming() #endregion #region Step 3 - Initiate a streaming run - // TODO: replace this with finalized enumerable result pattern - AsyncResultCollection asyncUpdates + AsyncCollectionResult asyncUpdates = client.CreateRunStreamingAsync(thread, assistant); ThreadRun currentRun = null; diff --git a/examples/Assistants/Example03_ListAssistantsWithPagination.cs b/examples/Assistants/Example03_ListAssistantsWithPagination.cs index 1c49470c4..83776d7a1 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 assistantPages = client.GetAssistants(); + IEnumerable assistants = assistantPages.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..1f4d8218e 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 assistantPages = client.GetAssistantsAsync(); + IAsyncEnumerable assistants = assistantPages.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..3d55253d0 100644 --- a/examples/Assistants/Example04_AllTheTools.cs +++ b/examples/Assistants/Example04_AllTheTools.cs @@ -137,8 +137,9 @@ static string GetNameOfFamilyMember(string relation) // 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); + PageCollection messagePages + = client.GetMessages(run.ThreadId, new MessageCollectionOptions() { Order = ListOrder.OldestFirst }); + IEnumerable messages = messagePages.GetAllValues(); foreach (ThreadMessage message in messages) { @@ -171,8 +172,12 @@ PageableCollection messages #endregion #region List run steps for details about tool calls - PageableCollection runSteps = client.GetRunSteps(run, resultOrder: ListOrder.OldestFirst); - foreach (RunStep step in runSteps) + PageCollection runSteps = client.GetRunSteps( + run, new RunStepCollectionOptions() + { + Order = ListOrder.OldestFirst + }); + foreach (RunStep step in runSteps.GetAllValues()) { Console.WriteLine($"Run step: {step.Status}"); foreach (RunStepToolCall toolCall in step.Details.ToolCalls) diff --git a/examples/Assistants/Example05_AssistantsWithVision.cs b/examples/Assistants/Example05_AssistantsWithVision.cs index e73222261..4d10c84cf 100644 --- a/examples/Assistants/Example05_AssistantsWithVision.cs +++ b/examples/Assistants/Example05_AssistantsWithVision.cs @@ -44,7 +44,7 @@ public void Example05_AssistantsWithVision() } }); - ResultCollection streamingUpdates = assistantClient.CreateRunStreaming( + CollectionResult streamingUpdates = assistantClient.CreateRunStreaming( thread, assistant, new RunCreationOptions() diff --git a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs index 68e50ed50..3f79137e8 100644 --- a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs +++ b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs @@ -45,7 +45,7 @@ public async Task Example05_AssistantsWithVisionAsync() } }); - AsyncResultCollection streamingUpdates = assistantClient.CreateRunStreamingAsync( + AsyncCollectionResult streamingUpdates = assistantClient.CreateRunStreamingAsync( thread, assistant, new RunCreationOptions() 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..5eb15133d 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); } /// @@ -240,7 +239,7 @@ public virtual ClientResult CreateRun(AssistantThread thread, Assista /// 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( + public virtual AsyncCollectionResult CreateRunStreamingAsync( AssistantThread thread, Assistant assistant, RunCreationOptions options = null) @@ -253,7 +252,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 CollectionResult CreateRunStreaming( AssistantThread thread, Assistant assistant, RunCreationOptions options = null) @@ -291,7 +290,7 @@ public virtual ClientResult CreateThreadAndRun( /// 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( + public virtual AsyncCollectionResult CreateThreadAndRunStreamingAsync( Assistant assistant, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null) @@ -303,46 +302,46 @@ public virtual AsyncResultCollection CreateThreadAndRunStreamin /// 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 CollectionResult CreateThreadAndRunStreaming( Assistant assistant, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null) => CreateThreadAndRunStreaming(assistant?.Id, threadOptions, runOptions); /// - /// 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 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); + return GetRuns(thread.Id, options); } /// @@ -394,7 +393,7 @@ public virtual ClientResult SubmitToolOutputsToRun( /// /// The tool outputs, corresponding to instances from the run. /// - public virtual AsyncResultCollection SubmitToolOutputsToRunStreamingAsync( + public virtual AsyncCollectionResult SubmitToolOutputsToRunStreamingAsync( ThreadRun run, IEnumerable toolOutputs) => SubmitToolOutputsToRunStreamingAsync(run?.ThreadId, run?.Id, toolOutputs); @@ -406,7 +405,7 @@ public virtual AsyncResultCollection SubmitToolOutputsToRunStre /// /// The tool outputs, corresponding to instances from the run. /// - public virtual ResultCollection SubmitToolOutputsToRunStreaming( + public virtual CollectionResult SubmitToolOutputsToRunStreaming( ThreadRun run, IEnumerable toolOutputs) => SubmitToolOutputsToRunStreaming(run?.ThreadId, run?.Id, toolOutputs); @@ -428,38 +427,38 @@ public virtual ClientResult CancelRun(ThreadRun run) => CancelRun(run?.ThreadId, run?.Id); /// - /// Gets a collection of instances associated with a . + /// Gets a page collection holding 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( + /// 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 GetRunStepsAsync( ThreadRun run, - ListOrder? resultOrder = default) + RunStepCollectionOptions options = default) { Argument.AssertNotNull(run, nameof(run)); - return GetRunSteps(run.ThreadId, run.Id, resultOrder); + return GetRunStepsAsync(run.ThreadId, run.Id, options); } /// - /// Gets a collection of instances associated with a . + /// Gets a page collection holding 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( + /// 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 GetRunSteps( ThreadRun run, - ListOrder? resultOrder = default) + RunStepCollectionOptions options = default) { Argument.AssertNotNull(run, nameof(run)); - return GetRunStepsAsync(run.ThreadId, run.Id, resultOrder); + return GetRunSteps(run.ThreadId, run.Id, options); } } diff --git a/src/Custom/Assistants/AssistantClient.Protocol.cs b/src/Custom/Assistants/AssistantClient.Protocol.cs index e6912702e..7e6344b3c 100644 --- a/src/Custom/Assistants/AssistantClient.Protocol.cs +++ b/src/Custom/Assistants/AssistantClient.Protocol.cs @@ -1,6 +1,7 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Threading.Tasks; namespace OpenAI.Assistants; @@ -40,7 +41,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 +63,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 +93,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 +214,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) @@ -259,13 +322,75 @@ public virtual Task CreateRunAsync(string threadId, BinaryContent public virtual ClientResult CreateRun(string threadId, BinaryContent content, RequestOptions options = null) => _runSubClient.CreateRun(threadId, content, options); - /// - public virtual Task GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) - => _runSubClient.GetRunsAsync(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)); + + RunsPageEnumerator enumerator = new RunsPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.CreateAsync(enumerator); + } - /// - 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 IEnumerable GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); + + RunsPageEnumerator enumerator = new RunsPageEnumerator(_pipeline, _endpoint, threadId, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); + } /// public virtual Task GetRunAsync(string threadId, string runId, RequestOptions options) @@ -299,13 +424,79 @@ public virtual Task SubmitToolOutputsToRunAsync(string threadId, s public virtual ClientResult SubmitToolOutputsToRun(string threadId, string runId, BinaryContent content, RequestOptions options = null) => _runSubClient.SubmitToolOutputsToRun(threadId, runId, content, options); - /// - 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); + /// + /// [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)); - /// - 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); + RunStepsPageEnumerator enumerator = new RunStepsPageEnumerator(_pipeline, _endpoint, threadId, runId, limit, order, after, before, options); + return PageCollectionHelpers.Create(enumerator); + } /// public virtual Task GetRunStepAsync(string threadId, string runId, string stepId, RequestOptions options) diff --git a/src/Custom/Assistants/AssistantClient.cs b/src/Custom/Assistants/AssistantClient.cs index 155409142..9e47b4f0d 100644 --- a/src/Custom/Assistants/AssistantClient.cs +++ b/src/Custom/Assistants/AssistantClient.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using static OpenAI.InternalListHelpers; namespace OpenAI.Assistants; @@ -103,33 +102,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) { - 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.CreateAsync(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. + /// 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. - /// 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( + AssistantCollectionOptions options = default, + CancellationToken cancellationToken = default) { - return CreatePageable((continuationToken, pageSize) - => GetAssistants(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); + } + + /// + /// 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 GetAssistants( + 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.Create(enumerator); } /// @@ -289,7 +356,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 +403,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); } /// @@ -531,7 +666,7 @@ public virtual ClientResult CreateRun(string threadId, string assista /// 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( + public virtual AsyncCollectionResult CreateRunStreamingAsync( string threadId, string assistantId, RunCreationOptions options = null, @@ -559,7 +694,7 @@ await CreateRunAsync(threadId, options.ToBinaryContent(), cancellationToken.ToRe /// 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( + public virtual CollectionResult CreateRunStreaming( string threadId, string assistantId, RunCreationOptions options = null, @@ -626,7 +761,7 @@ public virtual ClientResult CreateThreadAndRun( /// 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( + public virtual AsyncCollectionResult CreateThreadAndRunStreamingAsync( string assistantId, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null, @@ -638,7 +773,7 @@ public virtual AsyncResultCollection CreateThreadAndRunStreamin runOptions.Stream = true; BinaryContent protocolContent = CreateThreadAndRunProtocolContent(assistantId, threadOptions, runOptions); - async Task getResultAsync() => + async Task getResultAsync() => await CreateThreadAndRunAsync(protocolContent, cancellationToken.ToRequestOptions(streaming: true)) .ConfigureAwait(false); @@ -652,7 +787,7 @@ await CreateThreadAndRunAsync(protocolContent, cancellationToken.ToRequestOption /// 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( + public virtual CollectionResult CreateThreadAndRunStreaming( string assistantId, ThreadCreationOptions threadOptions = null, RunCreationOptions runOptions = null, @@ -670,45 +805,113 @@ public virtual ResultCollection CreateThreadAndRunStreaming( } /// - /// Returns a collection of instances associated with an existing . + /// Gets a page collection holding instances associated with an existing . /// /// 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. - /// + /// Options describing the collection to return. /// 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( + /// 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, - ListOrder? resultOrder = default, + RunCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - return CreateAsyncPageable((continuationToken, pageSize) - => GetRunsAsync(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + RunsPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + 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 GetRunsAsync( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + 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); } /// - /// Returns a collection of instances associated with an existing . + /// Gets a page collection holding instances associated with an existing . /// /// 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. - /// + /// Options describing the collection to return. /// 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( + /// 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, - ListOrder? resultOrder = default, + RunCollectionOptions options = default, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); - return CreatePageable((continuationToken, pageSize) - => GetRuns(threadId, pageSize, resultOrder?.ToString(), continuationToken, null, cancellationToken.ToRequestOptions())); + RunsPageEnumerator 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 GetRuns( + ContinuationToken firstPageToken, + CancellationToken cancellationToken = default) + { + 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); } /// @@ -801,7 +1004,7 @@ public virtual ClientResult SubmitToolOutputsToRun( /// The tool outputs, corresponding to instances from the run. /// /// A token that can be used to cancel this method call. - public virtual AsyncResultCollection SubmitToolOutputsToRunStreamingAsync( + public virtual AsyncCollectionResult SubmitToolOutputsToRunStreamingAsync( string threadId, string runId, IEnumerable toolOutputs, @@ -829,7 +1032,7 @@ await SubmitToolOutputsToRunAsync(threadId, runId, content, cancellationToken.To /// The tool outputs, corresponding to instances from the run. /// /// A token that can be used to cancel this method call. - public virtual ResultCollection SubmitToolOutputsToRunStreaming( + public virtual CollectionResult SubmitToolOutputsToRunStreaming( string threadId, string runId, IEnumerable toolOutputs, @@ -879,51 +1082,123 @@ public virtual ClientResult CancelRun(string threadId, string runId, } /// - /// Gets a collection of instances associated with a . + /// Gets a page collection holding instances associated with a . /// /// 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. - /// + /// /// 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( + /// 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( string threadId, string runId, - ListOrder? resultOrder = default, + RunStepCollectionOptions options = default, 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())); + 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 collection of instances associated with a . + /// Gets a page collection holding instances associated with a . /// /// 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. - /// + /// /// 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 GetRunSteps( string threadId, string runId, - ListOrder? resultOrder = default, + RunStepCollectionOptions 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())); + RunStepsPageEnumerator enumerator = new(_pipeline, _endpoint, + threadId, + runId, + 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); } /// 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/AssistantsPageEnumerator.cs b/src/Custom/Assistants/AssistantsPageEnumerator.cs new file mode 100644 index 000000000..15dbcc2dc --- /dev/null +++ b/src/Custom/Assistants/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/Custom/Assistants/AssistantsPageToken.cs b/src/Custom/Assistants/AssistantsPageToken.cs new file mode 100644 index 000000000..251e8a421 --- /dev/null +++ b/src/Custom/Assistants/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/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..4ffef5d1b 100644 --- a/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs +++ b/src/Custom/Assistants/Internal/InternalAssistantRunClient.Protocol.cs @@ -121,76 +121,6 @@ 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. /// @@ -377,80 +307,6 @@ public virtual ClientResult SubmitToolOutputsToRun(string threadId, string runId } } - /// - /// [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. /// 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/MessagesPageEnumerator.cs b/src/Custom/Assistants/MessagesPageEnumerator.cs new file mode 100644 index 000000000..a4a262c33 --- /dev/null +++ b/src/Custom/Assistants/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/Custom/Assistants/MessagesPageToken.cs b/src/Custom/Assistants/MessagesPageToken.cs new file mode 100644 index 000000000..b11bca627 --- /dev/null +++ b/src/Custom/Assistants/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/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/RunStepsPageEnumerator.cs b/src/Custom/Assistants/RunStepsPageEnumerator.cs new file mode 100644 index 000000000..77660e761 --- /dev/null +++ b/src/Custom/Assistants/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/Custom/Assistants/RunStepsPageToken.cs b/src/Custom/Assistants/RunStepsPageToken.cs new file mode 100644 index 000000000..65f522815 --- /dev/null +++ b/src/Custom/Assistants/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/Custom/Assistants/RunsPageEnumerator.cs b/src/Custom/Assistants/RunsPageEnumerator.cs new file mode 100644 index 000000000..3f9ee113f --- /dev/null +++ b/src/Custom/Assistants/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/Custom/Assistants/RunsPageToken.cs b/src/Custom/Assistants/RunsPageToken.cs new file mode 100644 index 000000000..e019fe5d4 --- /dev/null +++ b/src/Custom/Assistants/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/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/Custom/VectorStores/VectorStoreFileBatchesPageEnumerator.cs b/src/Custom/VectorStores/VectorStoreFileBatchesPageEnumerator.cs new file mode 100644 index 000000000..cb1a78576 --- /dev/null +++ b/src/Custom/VectorStores/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/Custom/VectorStores/VectorStoreFileBatchesPageToken.cs b/src/Custom/VectorStores/VectorStoreFileBatchesPageToken.cs new file mode 100644 index 000000000..8db14097f --- /dev/null +++ b/src/Custom/VectorStores/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/Custom/VectorStores/VectorStoreFilesPageEnumerator.cs b/src/Custom/VectorStores/VectorStoreFilesPageEnumerator.cs new file mode 100644 index 000000000..d78384e31 --- /dev/null +++ b/src/Custom/VectorStores/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/Custom/VectorStores/VectorStoreFilesPageToken.cs b/src/Custom/VectorStores/VectorStoreFilesPageToken.cs new file mode 100644 index 000000000..ba7b1f9eb --- /dev/null +++ b/src/Custom/VectorStores/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/Custom/VectorStores/VectorStoresPageEnumerator.cs b/src/Custom/VectorStores/VectorStoresPageEnumerator.cs new file mode 100644 index 000000000..4a2aac725 --- /dev/null +++ b/src/Custom/VectorStores/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/Custom/VectorStores/VectorStoresPageToken.cs b/src/Custom/VectorStores/VectorStoresPageToken.cs new file mode 100644 index 000000000..bd4c29a2d --- /dev/null +++ b/src/Custom/VectorStores/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/src/OpenAI.csproj b/src/OpenAI.csproj index 056929393..9cbba7f62 100644 --- a/src/OpenAI.csproj +++ b/src/OpenAI.csproj @@ -71,6 +71,6 @@ - + diff --git a/src/Utility/PageCollectionHelpers.cs b/src/Utility/PageCollectionHelpers.cs new file mode 100644 index 000000000..f7863ce21 --- /dev/null +++ b/src/Utility/PageCollectionHelpers.cs @@ -0,0 +1,49 @@ +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); + + 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/Utility/PageEnumerator.cs b/src/Utility/PageEnumerator.cs new file mode 100644 index 000000000..6bf35810e --- /dev/null +++ b/src/Utility/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/Utility/PageResultEnumerator.cs b/src/Utility/PageResultEnumerator.cs new file mode 100644 index 000000000..27629b171 --- /dev/null +++ b/src/Utility/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/tests/Assistants/AssistantTests.cs b/tests/Assistants/AssistantTests.cs index 8cadaac82..73cbca4b9 100644 --- a/tests/Assistants/AssistantTests.cs +++ b/tests/Assistants/AssistantTests.cs @@ -97,7 +97,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!"); @@ -174,10 +175,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] @@ -205,16 +206,16 @@ 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")); + 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] @@ -225,8 +226,8 @@ public void BasicRunOperationsWork() Validate(assistant); AssistantThread thread = client.CreateThread(); Validate(thread); - PageableCollection runs = client.GetRuns(thread); - Assert.That(runs.Count, Is.EqualTo(0)); + 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); @@ -235,12 +236,12 @@ public void BasicRunOperationsWork() 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)); + runsPage = client.GetRuns(thread).GetCurrentPage(); + Assert.That(runsPage.Values.Count, Is.EqualTo(1)); + Assert.That(runsPage.Values[0].Id, Is.EqualTo(run.Id)); - PageableCollection messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.GreaterThanOrEqualTo(1)); + 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); @@ -253,12 +254,12 @@ public void BasicRunOperationsWork() Assert.That(run.FailedAt, Is.Null); Assert.That(run.IncompleteDetails, Is.Null); - messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.EqualTo(2)); + messagesPage = client.GetMessages(thread).GetCurrentPage(); + Assert.That(messagesPage.Values.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)); + 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] @@ -313,21 +314,26 @@ public void BasicRunStepFunctionalityWorks() 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)); + 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(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)); + 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 = runSteps.First().Details; + RunStepDetails details = firstStep.Details; Assert.That(details?.CreatedMessageId, Is.Not.Null.And.Not.Empty); - string rawContent = runSteps.GetRawResponse().Content.ToString(); - details = runSteps.ElementAt(1).Details; + string rawContent = firstPage.GetRawResponse().Content.ToString(); + + details = secondStep.Details; Assert.Multiple(() => { Assert.That(details?.ToolCalls.Count, Is.GreaterThan(0)); @@ -433,11 +439,12 @@ public void FunctionToolsWork() } 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")); + 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] @@ -456,7 +463,7 @@ public async Task StreamingRunWorks() Stopwatch stopwatch = Stopwatch.StartNew(); void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); - AsyncResultCollection streamingResult + AsyncCollectionResult streamingResult = client.CreateRunStreamingAsync(thread.Id, assistant.Id); Print(">>> Connected <<<"); @@ -503,7 +510,7 @@ public async Task StreamingToolCall() void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); Print(" >>> Beginning call ... "); - AsyncResultCollection asyncResults = client.CreateThreadAndRunStreamingAsync( + AsyncCollectionResult asyncResults = client.CreateThreadAndRunStreamingAsync( assistant, new() { @@ -642,9 +649,13 @@ This file describes the favorite foods of several people. } while (run?.Status.IsTerminal == false); Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); + 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); @@ -652,14 +663,19 @@ This file describes the favorite foods of several people. { Console.WriteLine($" --> From file: {annotation.InputFileId}, replacement: {annotation.TextToReplace}"); } + + if (!hasCake) + { + hasCake = content.Text.ToLower().Contains("cake"); + } } } - Assert.That(messages.Count() > 1); - Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); + Assert.That(messageCount > 1); + Assert.That(hasCake, Is.True); } [Test] - public async Task CanEnumerateAssistants() + public async Task Pagination_CanEnumerateAssistants() { AssistantClient client = GetTestClient(); @@ -676,7 +692,7 @@ public async Task CanEnumerateAssistants() // Page through collection int count = 0; - AsyncPageableCollection assistants = client.GetAssistantsAsync(ListOrder.NewestFirst); + IAsyncEnumerable assistants = client.GetAssistantsAsync(new AssistantCollectionOptions() { Order = ListOrder.NewestFirst }).GetAllValuesAsync(); int lastIdSeen = int.MaxValue; @@ -700,7 +716,7 @@ public async Task CanEnumerateAssistants() } [Test] - public async Task CanPageThroughAssistantCollection() + public async Task Pagination_CanPageThroughAssistantCollection() { AssistantClient client = GetTestClient(); @@ -718,14 +734,183 @@ public async Task CanPageThroughAssistantCollection() // Page through collection int count = 0; int pageCount = 0; - AsyncPageableCollection assistants = client.GetAssistantsAsync(ListOrder.NewestFirst); - IAsyncEnumerable> pages = assistants.AsPages(pageSizeHint: 2); + AsyncPageCollection pages = client.GetAssistantsAsync( + new AssistantCollectionOptions() + { + Order = ListOrder.NewestFirst, + PageSize = 2 + }); + + int lastIdSeen = int.MaxValue; + + await foreach (PageResult page in pages) + { + foreach (Assistant assistant in page.Values) + { + 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++; + } + + pageCount++; + if (lastIdSeen == 0 || count > 100) + { + break; + } + } + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); + } + + [Test] + public async Task Pagination_CanRehydrateAssistantPageCollectionFromBytes() + { + AssistantClient client = GetTestClient(); + + // 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}")); + } + + AsyncPageCollection pages = client.GetAssistantsAsync( + new AssistantCollectionOptions() + { + Order = ListOrder.NewestFirst, + PageSize = 2 + }); + + // Simulate rehydration of the collection + BinaryData rehydrationBytes = (await pages.GetCurrentPageAsync().ConfigureAwait(false)).PageToken.ToBytes(); + ContinuationToken rehydrationToken = ContinuationToken.FromBytes(rehydrationBytes); + + AsyncPageCollection rehydratedPages = client.GetAssistantsAsync(rehydrationToken); + + int count = 0; + int pageCount = 0; + int lastIdSeen = int.MaxValue; + + await foreach (PageResult page in rehydratedPages) + { + foreach (Assistant assistant in page.Values) + { + 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++; + } + + pageCount++; + if (lastIdSeen == 0 || count > 100) + { + break; + } + } + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); + } + + [Test] + public async Task Pagination_CanRehydrateAssistantPageCollectionFromPageToken() + { + AssistantClient client = GetTestClient(); + + // 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}")); + } + + 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 (PageResult page in rehydratedPages) + { + foreach (Assistant assistant in page.Values) + { + 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++; + } + pageCount++; + if (lastIdSeen == 0 || count > 100) + { + break; + } + } + + Assert.That(count, Is.GreaterThanOrEqualTo(10)); + Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); + } + + [Test] + public async Task Pagination_CanCastAssistantPageCollectionToConvenienceFromProtocol() + { + AssistantClient client = GetTestClient(); + + // 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}")); + } + + // 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; 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) @@ -748,6 +933,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() { @@ -767,7 +1034,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); } diff --git a/tests/Assistants/VectorStoreTests.cs b/tests/Assistants/VectorStoreTests.cs index 672afa41b..ea1d2b7ae 100644 --- a/tests/Assistants/VectorStoreTests.cs +++ b/tests/Assistants/VectorStoreTests.cs @@ -73,7 +73,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() }); @@ -102,7 +102,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) @@ -139,7 +139,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) @@ -190,7 +190,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)); @@ -199,6 +199,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() { @@ -223,7 +286,7 @@ public void CanUseBatchIngestion() Thread.Sleep(500); } - foreach (VectorStoreFileAssociation association in client.GetFileAssociations(batchJob)) + foreach (VectorStoreFileAssociation association in client.GetFileAssociations(batchJob).GetAllValues()) { Assert.Multiple(() => { @@ -269,9 +332,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/ChatTests.cs b/tests/Chat/ChatTests.cs index 81e55452e..5527c2d3d 100644 --- a/tests/Chat/ChatTests.cs +++ b/tests/Chat/ChatTests.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; @@ -277,7 +277,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)