From 8c8a8a768676459e7b735e2969645ae39452a00e Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 23 May 2024 10:05:32 -0700 Subject: [PATCH 1/8] WIP: implement GetRun LRO --- .dotnet/OpenAI.sln | 34 ++--- .../Assistants/AssistantRunOperation.cs | 125 ++++++++++++++++++ .dotnet/src/Custom/Common/LroHelpers.cs | 14 ++ .dotnet/src/OpenAI.csproj | 2 +- 4 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 .dotnet/src/Custom/Assistants/AssistantRunOperation.cs create mode 100644 .dotnet/src/Custom/Common/LroHelpers.cs diff --git a/.dotnet/OpenAI.sln b/.dotnet/OpenAI.sln index 5d1edd8c3..58b34adf2 100644 --- a/.dotnet/OpenAI.sln +++ b/.dotnet/OpenAI.sln @@ -1,10 +1,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29709.97 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI", "src\OpenAI.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI", "src\OpenAI.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Tests", "tests\OpenAI.Tests.csproj", "{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Tests", "tests\OpenAI.Tests.csproj", "{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ClientModel", "..\..\azure-sdk-for-net\sdk\core\System.ClientModel\src\System.ClientModel.csproj", "{2DAD1811-2A5E-4C60-80D1-B94533FD1B74}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,26 +14,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B0C276D1-2930-4887-B29A-D1A33E7009A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B0C276D1-2930-4887-B29A-D1A33E7009A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B0C276D1-2930-4887-B29A-D1A33E7009A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B0C276D1-2930-4887-B29A-D1A33E7009A2}.Release|Any CPU.Build.0 = Release|Any CPU - {8E9A77AC-792A-4432-8320-ACFD46730401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E9A77AC-792A-4432-8320-ACFD46730401}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E9A77AC-792A-4432-8320-ACFD46730401}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E9A77AC-792A-4432-8320-ACFD46730401}.Release|Any CPU.Build.0 = Release|Any CPU - {A4241C1F-A53D-474C-9E4E-075054407E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4241C1F-A53D-474C-9E4E-075054407E74}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4241C1F-A53D-474C-9E4E-075054407E74}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4241C1F-A53D-474C-9E4E-075054407E74}.Release|Any CPU.Build.0 = Release|Any CPU - {FA8BD3F1-8616-47B6-974C-7576CDF4717E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA8BD3F1-8616-47B6-974C-7576CDF4717E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA8BD3F1-8616-47B6-974C-7576CDF4717E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA8BD3F1-8616-47B6-974C-7576CDF4717E}.Release|Any CPU.Build.0 = Release|Any CPU - {85677AD3-C214-42FA-AE6E-49B956CAC8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85677AD3-C214-42FA-AE6E-49B956CAC8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85677AD3-C214-42FA-AE6E-49B956CAC8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85677AD3-C214-42FA-AE6E-49B956CAC8DC}.Release|Any CPU.Build.0 = Release|Any CPU {28FF4005-4467-4E36-92E7-DEA27DEB1519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28FF4005-4467-4E36-92E7-DEA27DEB1519}.Debug|Any CPU.Build.0 = Debug|Any CPU {28FF4005-4467-4E36-92E7-DEA27DEB1519}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -40,6 +22,10 @@ Global {1F1CD1D4-9932-4B73-99D8-C252A67D4B46}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F1CD1D4-9932-4B73-99D8-C252A67D4B46}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F1CD1D4-9932-4B73-99D8-C252A67D4B46}.Release|Any CPU.Build.0 = Release|Any CPU + {2DAD1811-2A5E-4C60-80D1-B94533FD1B74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DAD1811-2A5E-4C60-80D1-B94533FD1B74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DAD1811-2A5E-4C60-80D1-B94533FD1B74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DAD1811-2A5E-4C60-80D1-B94533FD1B74}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs new file mode 100644 index 000000000..37425ef6b --- /dev/null +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -0,0 +1,125 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI.Assistants; + +internal class AssistantRunOperation : ResultOperation +{ + private readonly string _threadId; + private readonly string _runId; + + private readonly Func> _getRun; + private readonly Func>> _getRunAsync; + + public AssistantRunOperation(ClientResult createResult, + Func> getRun, + Func>> getRunAsync) : + base(GetIdFromResult(createResult), GetResponseFromResult(createResult)) + { + _threadId = createResult.Value.ThreadId; + _runId = createResult.Value.Id; + + _getRun = getRun; + _getRunAsync = getRunAsync; + } + + private static string GetIdFromResult(ClientResult result) + { + // TODO: validate this is reversible -- i.e. it will parse + return $"{result.Value.ThreadId};{result.Value.Id}"; + } + + private static PipelineResponse GetResponseFromResult(ClientResult result) + => result.GetRawResponse(); + + public override PipelineResponse UpdateStatus() + { + if (HasCompleted) + { + return GetRawResponse(); + } + + ClientResult result = _getRun(); + + PipelineResponse response = result.GetRawResponse(); + Value = result.Value; + + if (result.Value.Status.IsTerminal) + { + HasCompleted = true; + } + + SetRawResponse(response); + + return response; + } + + public override async ValueTask UpdateStatusAsync() + { + if (HasCompleted) + { + return GetRawResponse(); + } + + ClientResult result = await _getRunAsync().ConfigureAwait(false); + + PipelineResponse response = result.GetRawResponse(); + Value = result.Value; + + if (result.Value.Status.IsTerminal) + { + HasCompleted = true; + } + + SetRawResponse(response); + + return response; + } + + public override ClientResult WaitForCompletion() + { + throw new NotImplementedException(); + } + + public override ClientResult WaitForCompletion(TimeSpan pollingInterval) + { + throw new NotImplementedException(); + } + + public override Task> WaitForCompletionAsync() + { + throw new NotImplementedException(); + } + + public override Task> WaitForCompletionAsync(TimeSpan pollingInterval) + { + throw new NotImplementedException(); + } + + public override PipelineResponse WaitForCompletionResponse(TimeSpan pollingInterval) + { + throw new NotImplementedException(); + } + + public override PipelineResponse WaitForCompletionResponse() + { + throw new NotImplementedException(); + } + + public override ValueTask WaitForCompletionResponseAsync(TimeSpan pollingInterval) + { + throw new NotImplementedException(); + } + + public override ValueTask WaitForCompletionResponseAsync() + { + throw new NotImplementedException(); + } +} diff --git a/.dotnet/src/Custom/Common/LroHelpers.cs b/.dotnet/src/Custom/Common/LroHelpers.cs new file mode 100644 index 000000000..a9e2dc1f1 --- /dev/null +++ b/.dotnet/src/Custom/Common/LroHelpers.cs @@ -0,0 +1,14 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenAI; + +internal class LroHelpers +{ + +} diff --git a/.dotnet/src/OpenAI.csproj b/.dotnet/src/OpenAI.csproj index 0f0e6e6f0..9e24a6295 100644 --- a/.dotnet/src/OpenAI.csproj +++ b/.dotnet/src/OpenAI.csproj @@ -10,7 +10,7 @@ False - + \ No newline at end of file From 282652e3cd471cb28533e23fa711aa3e9f7f1376 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 23 May 2024 10:33:03 -0700 Subject: [PATCH 2/8] complete implementation of LRO type --- .../Assistants/AssistantRunOperation.cs | 114 ++++++++++-------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index 37425ef6b..e1fecbc2d 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -15,14 +15,19 @@ internal class AssistantRunOperation : ResultOperation private readonly string _threadId; private readonly string _runId; - private readonly Func> _getRun; - private readonly Func>> _getRunAsync; + private readonly Func> _getRun; + private readonly Func>> _getRunAsync; + + private TimeSpan _pollingInterval = TimeSpan.FromSeconds(2); + private ClientResult _result; public AssistantRunOperation(ClientResult createResult, - Func> getRun, - Func>> getRunAsync) : + Func> getRun, + Func>> getRunAsync) : base(GetIdFromResult(createResult), GetResponseFromResult(createResult)) { + _result = createResult; + _threadId = createResult.Value.ThreadId; _runId = createResult.Value.Id; @@ -39,87 +44,96 @@ private static string GetIdFromResult(ClientResult result) private static PipelineResponse GetResponseFromResult(ClientResult result) => result.GetRawResponse(); - public override PipelineResponse UpdateStatus() + public override ClientResult UpdateStatus() { if (HasCompleted) { - return GetRawResponse(); + return _result; } - ClientResult result = _getRun(); + _result = _getRun(_threadId, _runId); + Value = _result.Value; - PipelineResponse response = result.GetRawResponse(); - Value = result.Value; - - if (result.Value.Status.IsTerminal) + if (_result.Value.Status.IsTerminal) { HasCompleted = true; } - SetRawResponse(response); + SetRawResponse(_result.GetRawResponse()); - return response; + return _result; } - public override async ValueTask UpdateStatusAsync() + public override async ValueTask UpdateStatusAsync() { if (HasCompleted) { - return GetRawResponse(); + return _result; } - ClientResult result = await _getRunAsync().ConfigureAwait(false); - - PipelineResponse response = result.GetRawResponse(); - Value = result.Value; + _result = await _getRunAsync(_threadId, _runId).ConfigureAwait(false); + Value = _result.Value; - if (result.Value.Status.IsTerminal) + if (_result.Value.Status.IsTerminal) { HasCompleted = true; } - SetRawResponse(response); + SetRawResponse(_result.GetRawResponse()); - return response; + return _result; } - public override ClientResult WaitForCompletion() - { - throw new NotImplementedException(); - } + public override ClientResult WaitForCompletion(CancellationToken cancellationToken = default) + => WaitForCompletion(_pollingInterval, cancellationToken); - public override ClientResult WaitForCompletion(TimeSpan pollingInterval) + public override ClientResult WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); - } + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); - public override Task> WaitForCompletionAsync() - { - throw new NotImplementedException(); - } + UpdateStatus(); - public override Task> WaitForCompletionAsync(TimeSpan pollingInterval) - { - throw new NotImplementedException(); - } + if (HasCompleted) + { + return _result; + } - public override PipelineResponse WaitForCompletionResponse(TimeSpan pollingInterval) - { - throw new NotImplementedException(); + cancellationToken.WaitHandle.WaitOne(_pollingInterval); + } } - public override PipelineResponse WaitForCompletionResponse() - { - throw new NotImplementedException(); - } + public override async Task> WaitForCompletionAsync(CancellationToken cancellationToken = default) + => await WaitForCompletionAsync(_pollingInterval, cancellationToken).ConfigureAwait(false); - public override ValueTask WaitForCompletionResponseAsync(TimeSpan pollingInterval) + public override async Task> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); - } + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); - public override ValueTask WaitForCompletionResponseAsync() - { - throw new NotImplementedException(); + await UpdateStatusAsync().ConfigureAwait(false); + + if (HasCompleted) + { + return _result; + } + + await Task.Delay(pollingInterval, cancellationToken).ConfigureAwait(false); + } } + + public override ClientResult WaitForCompletionResult(CancellationToken cancellationToken = default) + => WaitForCompletion(_pollingInterval, cancellationToken); + + public override ClientResult WaitForCompletionResult(TimeSpan pollingInterval, CancellationToken cancellationToken = default) + => WaitForCompletion(pollingInterval, cancellationToken); + + public override async ValueTask WaitForCompletionResultAsync(CancellationToken cancellationToken = default) + => await WaitForCompletionAsync(_pollingInterval, cancellationToken).ConfigureAwait(false); + + + public override async ValueTask WaitForCompletionResultAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) + => await WaitForCompletionAsync(pollingInterval, cancellationToken).ConfigureAwait(false); } From 8df1d2423c27970d61bc0db5d6235b80626f5986 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 23 May 2024 10:48:37 -0700 Subject: [PATCH 3/8] update tests; BasicRunOperationsWork passes --- .../Assistants/AssistantClient.Convenience.cs | 8 +- .../src/Custom/Assistants/AssistantClient.cs | 16 +- .../Assistants/AssistantRunOperation.cs | 5 + .../Assistants/Sample02_FunctionCalling.cs | 362 ++++++++--------- .../Sample02_FunctionCallingAsync.cs | 362 ++++++++--------- .../Assistants/Sample04_AllTheTools.cs | 384 +++++++++--------- .../Assistants/AssistantTests.cs | 196 +++++---- 7 files changed, 667 insertions(+), 666 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs b/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs index e1021bd2f..9a88b6857 100644 --- a/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs +++ b/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs @@ -215,7 +215,7 @@ public virtual ClientResult DeleteMessage(ThreadMessage message) /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual Task> CreateRunAsync(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) + public virtual Task> CreateRunAsync(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) => CreateRunAsync(thread?.Id, assistant?.Id, options); /// @@ -226,7 +226,7 @@ public virtual Task> CreateRunAsync(AssistantThread thre /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual ClientResult CreateRun(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) + public virtual ResultOperation CreateRun(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) => CreateRun(thread?.Id, assistant?.Id, options); /// @@ -346,7 +346,7 @@ public virtual PageableCollection GetRuns( /// /// The run to get a refreshed instance of. /// A new instance with updated information. - public virtual Task> GetRunAsync(ThreadRun run) + internal virtual Task> GetRunAsync(ThreadRun run) => GetRunAsync(run?.ThreadId, run?.Id); /// @@ -354,7 +354,7 @@ public virtual Task> GetRunAsync(ThreadRun run) /// /// The run to get a refreshed instance of. /// A new instance with updated information. - public virtual ClientResult GetRun(ThreadRun run) + internal virtual ClientResult GetRun(ThreadRun run) => GetRun(run?.ThreadId, run?.Id); /// diff --git a/.dotnet/src/Custom/Assistants/AssistantClient.cs b/.dotnet/src/Custom/Assistants/AssistantClient.cs index 15838592e..1a8147c9f 100644 --- a/.dotnet/src/Custom/Assistants/AssistantClient.cs +++ b/.dotnet/src/Custom/Assistants/AssistantClient.cs @@ -451,7 +451,7 @@ public virtual ClientResult DeleteMessage(string threadId, string messageI /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null) + public virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); @@ -461,7 +461,9 @@ public virtual async Task> CreateRunAsync(string threadI ClientResult protocolResult = await CreateRunAsync(threadId, options.ToBinaryContent(), null) .ConfigureAwait(false); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ClientResult createResult = CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + + return new AssistantRunOperation(createResult, GetRun, GetRunAsync); } /// @@ -472,7 +474,7 @@ public virtual async Task> CreateRunAsync(string threadI /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual ClientResult CreateRun(string threadId, string assistantId, RunCreationOptions options = null) + public virtual ResultOperation CreateRun(string threadId, string assistantId, RunCreationOptions options = null) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); @@ -481,7 +483,9 @@ public virtual ClientResult CreateRun(string threadId, string assista options.Stream = null; ClientResult protocolResult = CreateRun(threadId, options.ToBinaryContent(), null); - return CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + ClientResult createResult = CreateResultFromProtocol(protocolResult, ThreadRun.FromResponse); + + return new AssistantRunOperation(createResult, GetRun, GetRunAsync); } /// @@ -662,7 +666,7 @@ public virtual PageableCollection GetRuns( /// The ID of the thread to retrieve the run from. /// The ID of the run to retrieve. /// The existing instance. - public virtual async Task> GetRunAsync(string threadId, string runId) + internal virtual async Task> GetRunAsync(string threadId, string runId) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(runId, nameof(runId)); @@ -677,7 +681,7 @@ public virtual async Task> GetRunAsync(string threadId, /// The ID of the thread to retrieve the run from. /// The ID of the run to retrieve. /// The existing instance. - public virtual ClientResult GetRun(string threadId, string runId) + internal virtual ClientResult GetRun(string threadId, string runId) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(runId, nameof(runId)); diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index e1fecbc2d..dcff1e9f5 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -10,6 +10,8 @@ namespace OpenAI.Assistants; +// TODO: add hooks for cancel run? + internal class AssistantRunOperation : ResultOperation { private readonly string _threadId; @@ -27,6 +29,7 @@ public AssistantRunOperation(ClientResult createResult, base(GetIdFromResult(createResult), GetResponseFromResult(createResult)) { _result = createResult; + Value = _result.Value; _threadId = createResult.Value.ThreadId; _runId = createResult.Value.Id; @@ -52,6 +55,7 @@ public override ClientResult UpdateStatus() } _result = _getRun(_threadId, _runId); + Value = _result.Value; if (_result.Value.Status.IsTerminal) @@ -72,6 +76,7 @@ public override async ValueTask UpdateStatusAsync() } _result = await _getRunAsync(_threadId, _runId).ConfigureAwait(false); + Value = _result.Value; if (_result.Value.Status.IsTerminal) diff --git a/.dotnet/tests/Samples/Assistants/Sample02_FunctionCalling.cs b/.dotnet/tests/Samples/Assistants/Sample02_FunctionCalling.cs index 7d71e57bd..0e7bc13f5 100644 --- a/.dotnet/tests/Samples/Assistants/Sample02_FunctionCalling.cs +++ b/.dotnet/tests/Samples/Assistants/Sample02_FunctionCalling.cs @@ -11,185 +11,185 @@ namespace OpenAI.Samples; public partial class AssistantSamples { - [Test] - [Ignore("Compilation validation only")] - public void Sample02_FunctionCalling() - { - #region - string GetCurrentLocation() - { - // Call a location API here. - return "San Francisco"; - } - - const string GetCurrentLocationFunctionName = "get_current_location"; - - FunctionToolDefinition getLocationTool = new() - { - FunctionName = GetCurrentLocationFunctionName, - Description = "Get the user's current location" - }; - - string GetCurrentWeather(string location, string unit = "celsius") - { - // Call a weather API here. - return $"31 {unit}"; - } - - const string GetCurrentWeatherFunctionName = "get_current_weather"; - - FunctionToolDefinition getWeatherTool = new() - { - FunctionName = GetCurrentWeatherFunctionName, - Description = "Get the current weather in a given location", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. Boston, MA" - }, - "unit": { - "type": "string", - "enum": [ "celsius", "fahrenheit" ], - "description": "The temperature unit to use. Infer this from the specified location." - } - }, - "required": [ "location" ] - } - """), - }; - #endregion - - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - - #region - // Create an assistant that can call the function tools. - AssistantCreationOptions assistantOptions = new() - { - Name = "Sample: Function Calling", - Instructions = - "Don't make assumptions about what values to plug into functions." - + " Ask for clarification if a user request is ambiguous.", - Tools = { getLocationTool, getWeatherTool }, - }; - - Assistant assistant = client.CreateAssistant("gpt-4-turbo", assistantOptions); - #endregion - - #region - // Create a thread with an initial user message and run it. - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { new ThreadInitializationMessage(["What's the weather like today?"]), }, - }; - - ThreadRun run = client.CreateThreadAndRun(assistant.Id, threadOptions); - #endregion - - #region - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - run = client.GetRun(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case GetCurrentLocationFunctionName: - { - string toolResult = GetCurrentLocation(); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - case GetCurrentWeatherFunctionName: - { - // The arguments that the model wants to use to call the function are specified as a - // stringified JSON object based on the schema defined in the tool definition. Note that - // the model may hallucinate arguments too. Consequently, it is important to do the - // appropriate parsing and validation before calling the function. - using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); - bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); - bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); - - if (!hasLocation) - { - throw new ArgumentNullException(nameof(location), "The location argument is required."); - } - - string toolResult = hasUnit - ? GetCurrentWeather(location.GetString(), unit.GetString()) - : GetCurrentWeather(location.GetString()); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - PageableCollection messages - = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - } +// [Test] +// [Ignore("Compilation validation only")] +// public void Sample02_FunctionCalling() +// { +// #region +// string GetCurrentLocation() +// { +// // Call a location API here. +// return "San Francisco"; +// } + +// const string GetCurrentLocationFunctionName = "get_current_location"; + +// FunctionToolDefinition getLocationTool = new() +// { +// FunctionName = GetCurrentLocationFunctionName, +// Description = "Get the user's current location" +// }; + +// string GetCurrentWeather(string location, string unit = "celsius") +// { +// // Call a weather API here. +// return $"31 {unit}"; +// } + +// const string GetCurrentWeatherFunctionName = "get_current_weather"; + +// FunctionToolDefinition getWeatherTool = new() +// { +// FunctionName = GetCurrentWeatherFunctionName, +// Description = "Get the current weather in a given location", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g. Boston, MA" +// }, +// "unit": { +// "type": "string", +// "enum": [ "celsius", "fahrenheit" ], +// "description": "The temperature unit to use. Infer this from the specified location." +// } +// }, +// "required": [ "location" ] +// } +// """), +// }; +// #endregion + +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); + +// #region +// // Create an assistant that can call the function tools. +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Sample: Function Calling", +// Instructions = +// "Don't make assumptions about what values to plug into functions." +// + " Ask for clarification if a user request is ambiguous.", +// Tools = { getLocationTool, getWeatherTool }, +// }; + +// Assistant assistant = client.CreateAssistant("gpt-4-turbo", assistantOptions); +// #endregion + +// #region +// // Create a thread with an initial user message and run it. +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { new ThreadInitializationMessage(["What's the weather like today?"]), }, +// }; + +// ThreadRun run = client.CreateThreadAndRun(assistant.Id, threadOptions); +// #endregion + +// #region +// // Poll the run until it is no longer queued or in progress. +// while (!run.Status.IsTerminal) +// { +// Thread.Sleep(TimeSpan.FromSeconds(1)); +// run = client.GetRun(run.ThreadId, run.Id); + +// // If the run requires action, resolve them. +// if (run.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// foreach (RequiredAction action in run.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case GetCurrentLocationFunctionName: +// { +// string toolResult = GetCurrentLocation(); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// case GetCurrentWeatherFunctionName: +// { +// // The arguments that the model wants to use to call the function are specified as a +// // stringified JSON object based on the schema defined in the tool definition. Note that +// // the model may hallucinate arguments too. Consequently, it is important to do the +// // appropriate parsing and validation before calling the function. +// using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); +// bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); +// bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); + +// if (!hasLocation) +// { +// throw new ArgumentNullException(nameof(location), "The location argument is required."); +// } + +// string toolResult = hasUnit +// ? GetCurrentWeather(location.GetString(), unit.GetString()) +// : GetCurrentWeather(location.GetString()); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); +// } +// } +// #endregion + +// #region +// // With the run complete, list the messages and display their content +// if (run.Status == RunStatus.Completed) +// { +// PageableCollection messages +// = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); + +// foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// } +// else +// { +// throw new NotImplementedException(run.Status.ToString()); +// } +// #endregion +// } } diff --git a/.dotnet/tests/Samples/Assistants/Sample02_FunctionCallingAsync.cs b/.dotnet/tests/Samples/Assistants/Sample02_FunctionCallingAsync.cs index 04a8f6b1a..7ccc88768 100644 --- a/.dotnet/tests/Samples/Assistants/Sample02_FunctionCallingAsync.cs +++ b/.dotnet/tests/Samples/Assistants/Sample02_FunctionCallingAsync.cs @@ -11,185 +11,185 @@ namespace OpenAI.Samples; public partial class AssistantSamples { - [Test] - [Ignore("Compilation validation only")] - public async Task Sample02_FunctionCallingAsync() - { - #region - string GetCurrentLocation() - { - // Call a location API here. - return "San Francisco"; - } - - const string GetCurrentLocationFunctionName = "get_current_location"; - - FunctionToolDefinition getLocationTool = new() - { - FunctionName = GetCurrentLocationFunctionName, - Description = "Get the user's current location" - }; - - string GetCurrentWeather(string location, string unit = "celsius") - { - // Call a weather API here. - return $"31 {unit}"; - } - - const string GetCurrentWeatherFunctionName = "get_current_weather"; - - FunctionToolDefinition getWeatherTool = new() - { - FunctionName = GetCurrentWeatherFunctionName, - Description = "Get the current weather in a given location", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. Boston, MA" - }, - "unit": { - "type": "string", - "enum": [ "celsius", "fahrenheit" ], - "description": "The temperature unit to use. Infer this from the specified location." - } - }, - "required": [ "location" ] - } - """), - }; - #endregion - - // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. -#pragma warning disable OPENAI001 - AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); - - #region - // Create an assistant that can call the function tools. - AssistantCreationOptions assistantOptions = new() - { - Name = "Sample: Function Calling", - Instructions = - "Don't make assumptions about what values to plug into functions." - + " Ask for clarification if a user request is ambiguous.", - Tools = { getLocationTool, getWeatherTool }, - }; - - Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); - #endregion - - #region - // Create a thread with an initial user message and run it. - ThreadCreationOptions threadOptions = new() - { - InitialMessages = { new ThreadInitializationMessage(["What's the weather like today?"]), }, - }; - - ThreadRun run = await client.CreateThreadAndRunAsync(assistant.Id, threadOptions); - #endregion - - #region - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - await Task.Delay(TimeSpan.FromSeconds(1)); - run = await client.GetRunAsync(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case GetCurrentLocationFunctionName: - { - string toolResult = GetCurrentLocation(); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - case GetCurrentWeatherFunctionName: - { - // The arguments that the model wants to use to call the function are specified as a - // stringified JSON object based on the schema defined in the tool definition. Note that - // the model may hallucinate arguments too. Consequently, it is important to do the - // appropriate parsing and validation before calling the function. - using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); - bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); - bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); - - if (!hasLocation) - { - throw new ArgumentNullException(nameof(location), "The location argument is required."); - } - - string toolResult = hasUnit - ? GetCurrentWeather(location.GetString(), unit.GetString()) - : GetCurrentWeather(location.GetString()); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = await client.SubmitToolOutputsToRunAsync(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - AsyncPageableCollection messages - = client.GetMessagesAsync(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - await foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - } +// [Test] +// [Ignore("Compilation validation only")] +// public async Task Sample02_FunctionCallingAsync() +// { +// #region +// string GetCurrentLocation() +// { +// // Call a location API here. +// return "San Francisco"; +// } + +// const string GetCurrentLocationFunctionName = "get_current_location"; + +// FunctionToolDefinition getLocationTool = new() +// { +// FunctionName = GetCurrentLocationFunctionName, +// Description = "Get the user's current location" +// }; + +// string GetCurrentWeather(string location, string unit = "celsius") +// { +// // Call a weather API here. +// return $"31 {unit}"; +// } + +// const string GetCurrentWeatherFunctionName = "get_current_weather"; + +// FunctionToolDefinition getWeatherTool = new() +// { +// FunctionName = GetCurrentWeatherFunctionName, +// Description = "Get the current weather in a given location", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "location": { +// "type": "string", +// "description": "The city and state, e.g. Boston, MA" +// }, +// "unit": { +// "type": "string", +// "enum": [ "celsius", "fahrenheit" ], +// "description": "The temperature unit to use. Infer this from the specified location." +// } +// }, +// "required": [ "location" ] +// } +// """), +// }; +// #endregion + +// // Assistants is a beta API and subject to change; acknowledge its experimental status by suppressing the matching warning. +//#pragma warning disable OPENAI001 +// AssistantClient client = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY")); + +// #region +// // Create an assistant that can call the function tools. +// AssistantCreationOptions assistantOptions = new() +// { +// Name = "Sample: Function Calling", +// Instructions = +// "Don't make assumptions about what values to plug into functions." +// + " Ask for clarification if a user request is ambiguous.", +// Tools = { getLocationTool, getWeatherTool }, +// }; + +// Assistant assistant = await client.CreateAssistantAsync("gpt-4-turbo", assistantOptions); +// #endregion + +// #region +// // Create a thread with an initial user message and run it. +// ThreadCreationOptions threadOptions = new() +// { +// InitialMessages = { new ThreadInitializationMessage(["What's the weather like today?"]), }, +// }; + +// ThreadRun run = await client.CreateThreadAndRunAsync(assistant.Id, threadOptions); +// #endregion + +// #region +// // Poll the run until it is no longer queued or in progress. +// while (!run.Status.IsTerminal) +// { +// await Task.Delay(TimeSpan.FromSeconds(1)); +// run = await client.GetRunAsync(run.ThreadId, run.Id); + +// // If the run requires action, resolve them. +// if (run.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// foreach (RequiredAction action in run.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case GetCurrentLocationFunctionName: +// { +// string toolResult = GetCurrentLocation(); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// case GetCurrentWeatherFunctionName: +// { +// // The arguments that the model wants to use to call the function are specified as a +// // stringified JSON object based on the schema defined in the tool definition. Note that +// // the model may hallucinate arguments too. Consequently, it is important to do the +// // appropriate parsing and validation before calling the function. +// using JsonDocument argumentsJson = JsonDocument.Parse(action.FunctionArguments); +// bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); +// bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); + +// if (!hasLocation) +// { +// throw new ArgumentNullException(nameof(location), "The location argument is required."); +// } + +// string toolResult = hasUnit +// ? GetCurrentWeather(location.GetString(), unit.GetString()) +// : GetCurrentWeather(location.GetString()); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// run = await client.SubmitToolOutputsToRunAsync(run.ThreadId, run.Id, toolOutputs); +// } +// } +// #endregion + +// #region +// // With the run complete, list the messages and display their content +// if (run.Status == RunStatus.Completed) +// { +// AsyncPageableCollection messages +// = client.GetMessagesAsync(run.ThreadId, resultOrder: ListOrder.OldestFirst); + +// await foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// } +// else +// { +// throw new NotImplementedException(run.Status.ToString()); +// } +// #endregion +// } } diff --git a/.dotnet/tests/Samples/Assistants/Sample04_AllTheTools.cs b/.dotnet/tests/Samples/Assistants/Sample04_AllTheTools.cs index 4645e7fdc..87538a1d3 100644 --- a/.dotnet/tests/Samples/Assistants/Sample04_AllTheTools.cs +++ b/.dotnet/tests/Samples/Assistants/Sample04_AllTheTools.cs @@ -12,196 +12,196 @@ namespace OpenAI.Samples; public partial class AssistantSamples { - [Test] - [Ignore("Only verifying compilation")] - public void Sample04_AllTheTools() - { -#pragma warning disable OPENAI001 - - #region Define a function tool - static string GetNameOfFamilyMember(string relation) - => relation switch - { - { } when relation.Contains("father") => "John Doe", - { } when relation.Contains("mother") => "Jane Doe", - _ => throw new ArgumentException(relation, nameof(relation)) - }; - - FunctionToolDefinition getNameOfFamilyMemberTool = new() - { - FunctionName = nameof(GetNameOfFamilyMember), - Description = "Provided a family relation type like 'father' or 'mother', " - + "gets the name of the related person from the user.", - Parameters = BinaryData.FromString(""" - { - "type": "object", - "properties": { - "relation": { - "type": "string", - "description": "The relation to the user to query, e.g. 'mother' or 'father'" - } - }, - "required": [ "relation" ] - } - """), - }; - - #region Upload a mock file for use with file search - FileClient fileClient = new(); - OpenAIFileInfo favoriteNumberFile = fileClient.UploadFile( - BinaryData.FromString(""" - This file contains the favorite numbers for individuals. - - John Doe: 14 - Bob Doe: 32 - Jane Doe: 44 - """).ToStream(), - "favorite_numbers.txt", - FileUploadPurpose.Assistants); - #endregion - - #region Create an assistant with functions, file search, and code interpreter all enabled - AssistantClient client = new(); - Assistant assistant = client.CreateAssistant("gpt-4-turbo", new AssistantCreationOptions() - { - Instructions = "Use functions to resolve family relations into the names of people. Use file search to " - + " look up the favorite numbers of people. Use code interpreter to create graphs of lines.", - Tools = { getNameOfFamilyMemberTool, new FileSearchToolDefinition(), new CodeInterpreterToolDefinition() }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([favoriteNumberFile.Id]), - }, - }, - }, - }); - #endregion - - #region Create a new thread and start a run - AssistantThread thread = client.CreateThread(new ThreadCreationOptions() - { - InitialMessages = - { - new ThreadInitializationMessage( - [ - "Create a graph of a line with a slope that's my father's favorite number " - + "and an offset that's my mother's favorite number.", - "Include people's names in your response and cite where you found them." - ]), - }, - }); - - ThreadRun run = client.CreateRun(thread, assistant); - #endregion - - #region Complete the run, calling functions as needed - // Poll the run until it is no longer queued or in progress. - while (!run.Status.IsTerminal) - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - run = client.GetRun(run.ThreadId, run.Id); - - // If the run requires action, resolve them. - if (run.Status == RunStatus.RequiresAction) - { - List toolOutputs = []; - - foreach (RequiredAction action in run.RequiredActions) - { - switch (action.FunctionName) - { - case nameof(GetNameOfFamilyMember): - { - using JsonDocument argumentsDocument = JsonDocument.Parse(action.FunctionArguments); - string relation = argumentsDocument.RootElement.TryGetProperty("relation", out JsonElement relationProperty) - ? relationProperty.GetString() - : null; - string toolResult = GetNameOfFamilyMember(relation); - toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); - break; - } - - default: - { - // Handle other or unexpected calls. - throw new NotImplementedException(); - } - } - } - - // Submit the tool outputs to the assistant, which returns the run to the queued state. - run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); - } - } - #endregion - - #region - // With the run complete, list the messages and display their content - if (run.Status == RunStatus.Completed) - { - PageableCollection messages - = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); - - foreach (ThreadMessage message in messages) - { - Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); - foreach (MessageContent contentItem in message.Content) - { - Console.WriteLine($"{contentItem.Text}"); - - if (contentItem.ImageFileId is not null) - { - Console.WriteLine($" {contentItem.ImageFileId}"); - } - - // Include annotations, if any. - if (contentItem.TextAnnotations.Count > 0) - { - Console.WriteLine(); - foreach (TextAnnotation annotation in contentItem.TextAnnotations) - { - Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); - Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); - Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); - Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); - Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); - } - } - - } - Console.WriteLine(); - } - #endregion - - #region List run steps for details about tool calls - PageableCollection runSteps = client.GetRunSteps(run, resultOrder: ListOrder.OldestFirst); - foreach (RunStep step in runSteps) - { - Console.WriteLine($"Run step: {step.Status}"); - foreach (RunStepToolCall toolCall in step.Details.ToolCalls) - { - Console.WriteLine($" --> Tool call: {toolCall.ToolKind}"); - foreach (RunStepCodeInterpreterOutput output in toolCall.CodeInterpreterOutputs) - { - Console.WriteLine($" --> Output: {output.ImageFileId}"); - } - } - } - #endregion - } - else - { - throw new NotImplementedException(run.Status.ToString()); - } - #endregion - - #region Clean up any temporary resources that are no longer needed - _ = client.DeleteThread(thread); - _ = client.DeleteAssistant(assistant); - _ = fileClient.DeleteFile(favoriteNumberFile.Id); - #endregion - } +// [Test] +// [Ignore("Only verifying compilation")] +// public void Sample04_AllTheTools() +// { +//#pragma warning disable OPENAI001 + +// #region Define a function tool +// static string GetNameOfFamilyMember(string relation) +// => relation switch +// { +// { } when relation.Contains("father") => "John Doe", +// { } when relation.Contains("mother") => "Jane Doe", +// _ => throw new ArgumentException(relation, nameof(relation)) +// }; + +// FunctionToolDefinition getNameOfFamilyMemberTool = new() +// { +// FunctionName = nameof(GetNameOfFamilyMember), +// Description = "Provided a family relation type like 'father' or 'mother', " +// + "gets the name of the related person from the user.", +// Parameters = BinaryData.FromString(""" +// { +// "type": "object", +// "properties": { +// "relation": { +// "type": "string", +// "description": "The relation to the user to query, e.g. 'mother' or 'father'" +// } +// }, +// "required": [ "relation" ] +// } +// """), +// }; + +// #region Upload a mock file for use with file search +// FileClient fileClient = new(); +// OpenAIFileInfo favoriteNumberFile = fileClient.UploadFile( +// BinaryData.FromString(""" +// This file contains the favorite numbers for individuals. + +// John Doe: 14 +// Bob Doe: 32 +// Jane Doe: 44 +// """).ToStream(), +// "favorite_numbers.txt", +// FileUploadPurpose.Assistants); +// #endregion + +// #region Create an assistant with functions, file search, and code interpreter all enabled +// AssistantClient client = new(); +// Assistant assistant = client.CreateAssistant("gpt-4-turbo", new AssistantCreationOptions() +// { +// Instructions = "Use functions to resolve family relations into the names of people. Use file search to " +// + " look up the favorite numbers of people. Use code interpreter to create graphs of lines.", +// Tools = { getNameOfFamilyMemberTool, new FileSearchToolDefinition(), new CodeInterpreterToolDefinition() }, +// ToolResources = new() +// { +// FileSearch = new() +// { +// NewVectorStores = +// { +// new VectorStoreCreationHelper([favoriteNumberFile.Id]), +// }, +// }, +// }, +// }); +// #endregion + +// #region Create a new thread and start a run +// AssistantThread thread = client.CreateThread(new ThreadCreationOptions() +// { +// InitialMessages = +// { +// new ThreadInitializationMessage( +// [ +// "Create a graph of a line with a slope that's my father's favorite number " +// + "and an offset that's my mother's favorite number.", +// "Include people's names in your response and cite where you found them." +// ]), +// }, +// }); + +// ThreadRun run = client.CreateRun(thread, assistant); +// #endregion + +// #region Complete the run, calling functions as needed +// // Poll the run until it is no longer queued or in progress. +// while (!run.Status.IsTerminal) +// { +// Thread.Sleep(TimeSpan.FromSeconds(1)); +// run = client.GetRun(run.ThreadId, run.Id); + +// // If the run requires action, resolve them. +// if (run.Status == RunStatus.RequiresAction) +// { +// List toolOutputs = []; + +// foreach (RequiredAction action in run.RequiredActions) +// { +// switch (action.FunctionName) +// { +// case nameof(GetNameOfFamilyMember): +// { +// using JsonDocument argumentsDocument = JsonDocument.Parse(action.FunctionArguments); +// string relation = argumentsDocument.RootElement.TryGetProperty("relation", out JsonElement relationProperty) +// ? relationProperty.GetString() +// : null; +// string toolResult = GetNameOfFamilyMember(relation); +// toolOutputs.Add(new ToolOutput(action.ToolCallId, toolResult)); +// break; +// } + +// default: +// { +// // Handle other or unexpected calls. +// throw new NotImplementedException(); +// } +// } +// } + +// // Submit the tool outputs to the assistant, which returns the run to the queued state. +// run = client.SubmitToolOutputsToRun(run.ThreadId, run.Id, toolOutputs); +// } +// } +// #endregion + +// #region +// // With the run complete, list the messages and display their content +// if (run.Status == RunStatus.Completed) +// { +// PageableCollection messages +// = client.GetMessages(run.ThreadId, resultOrder: ListOrder.OldestFirst); + +// foreach (ThreadMessage message in messages) +// { +// Console.WriteLine($"[{message.Role.ToString().ToUpper()}]: "); +// foreach (MessageContent contentItem in message.Content) +// { +// Console.WriteLine($"{contentItem.Text}"); + +// if (contentItem.ImageFileId is not null) +// { +// Console.WriteLine($" {contentItem.ImageFileId}"); +// } + +// // Include annotations, if any. +// if (contentItem.TextAnnotations.Count > 0) +// { +// Console.WriteLine(); +// foreach (TextAnnotation annotation in contentItem.TextAnnotations) +// { +// Console.WriteLine($"* File ID used by file_search: {annotation.InputFileId}"); +// Console.WriteLine($"* file_search quote from file: {annotation.InputQuote}"); +// Console.WriteLine($"* File ID created by code_interpreter: {annotation.OutputFileId}"); +// Console.WriteLine($"* Text to replace: {annotation.TextToReplace}"); +// Console.WriteLine($"* Message content index range: {annotation.StartIndex}-{annotation.EndIndex}"); +// } +// } + +// } +// Console.WriteLine(); +// } +// #endregion + +// #region List run steps for details about tool calls +// PageableCollection runSteps = client.GetRunSteps(run, resultOrder: ListOrder.OldestFirst); +// foreach (RunStep step in runSteps) +// { +// Console.WriteLine($"Run step: {step.Status}"); +// foreach (RunStepToolCall toolCall in step.Details.ToolCalls) +// { +// Console.WriteLine($" --> Tool call: {toolCall.ToolKind}"); +// foreach (RunStepCodeInterpreterOutput output in toolCall.CodeInterpreterOutputs) +// { +// Console.WriteLine($" --> Output: {output.ImageFileId}"); +// } +// } +// } +// #endregion +// } +// else +// { +// throw new NotImplementedException(run.Status.ToString()); +// } +// #endregion + +// #region Clean up any temporary resources that are no longer needed +// _ = client.DeleteThread(thread); +// _ = client.DeleteAssistant(assistant); +// _ = fileClient.DeleteFile(favoriteNumberFile.Id); +// #endregion +// } } diff --git a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs index 4ed271349..698925132 100644 --- a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs +++ b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs @@ -182,23 +182,20 @@ public void BasicRunOperationsWork() Assert.That(runs.Count, Is.EqualTo(0)); ThreadMessage message = client.CreateMessage(thread.Id, ["Hello, assistant!"]); Validate(message); - ThreadRun run = client.CreateRun(thread.Id, assistant.Id); - Validate(run); - Assert.That(run.Status, Is.EqualTo(RunStatus.Queued)); - Assert.That(run.CreatedAt, Is.GreaterThan(s_2024)); - ThreadRun retrievedRun = client.GetRun(thread.Id, run.Id); - Assert.That(retrievedRun.Id, Is.EqualTo(run.Id)); + ResultOperation runOperation = client.CreateRun(thread.Id, assistant.Id); + Validate(runOperation.Value); + Assert.That(runOperation.Value.Status, Is.EqualTo(RunStatus.Queued)); + Assert.That(runOperation.Value.CreatedAt, Is.GreaterThan(s_2024)); + runs = client.GetRuns(thread); Assert.That(runs.Count, Is.EqualTo(1)); - Assert.That(runs.First().Id, Is.EqualTo(run.Id)); + Assert.That(runs.First().Id, Is.EqualTo(runOperation.Value.Id)); PageableCollection messages = client.GetMessages(thread); Assert.That(messages.Count, Is.GreaterThanOrEqualTo(1)); - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } + + ThreadRun run = runOperation.WaitForCompletion(); + Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); Assert.That(run.CompletedAt, Is.GreaterThan(s_2024)); Assert.That(run.RequiredActions.Count, Is.EqualTo(0)); @@ -231,14 +228,11 @@ public void BasicRunStepFunctionalityWorks() }); Validate(thread); - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); + ResultOperation runOperation = client.CreateRun(thread, assistant); + Validate(runOperation); + + ThreadRun run = runOperation.WaitForCompletion(); - 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)); @@ -286,87 +280,87 @@ public void SettingResponseFormatWorks() Validate(thread); ThreadMessage message = client.CreateMessage(thread, ["Write some JSON for me!"]); Validate(message); - ThreadRun run = client.CreateRun(thread, assistant, new() + ResultOperation runOperation = client.CreateRun(thread, assistant, new() { ResponseFormat = AssistantResponseFormat.JsonObject, }); - Validate(run); - Assert.That(run.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); + Validate(runOperation); + Assert.That(runOperation.Value.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); } - [Test] - public void FunctionToolsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() - { - Tools = - { - new FunctionToolDefinition() - { - FunctionName = "get_favorite_food_for_day_of_week", - Description = "gets the user's favorite food for a given day of the week, like Tuesday", - Parameters = BinaryData.FromObjectAsJson(new - { - type = "object", - properties = new - { - day_of_week = new - { - type = "string", - description = "a day of the week, like Tuesday or Saturday", - } - } - }), - }, - }, - }); - Validate(assistant); - Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); - - FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; - Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); - - ThreadRun run = client.CreateThreadAndRun( - assistant, - new ThreadCreationOptions() - { - InitialMessages = { new(["What should I eat on Thursday?"]) }, - }, - new RunCreationOptions() - { - AdditionalInstructions = "Call provided tools when appropriate.", - }); - Validate(run); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.RequiresAction)); - Assert.That(run.RequiredActions?.Count, Is.EqualTo(1)); - Assert.That(run.RequiredActions[0].ToolCallId, Is.Not.Null.Or.Empty); - Assert.That(run.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(run.RequiredActions[0].FunctionArguments, Is.Not.Null.Or.Empty); - - run = client.SubmitToolOutputsToRun(run, [new(run.RequiredActions[0].ToolCallId, "tacos")]); - Assert.That(run.Status.IsTerminal, Is.False); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - - 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, Does.Contain("tacos")); - } + //[Test] + //public void FunctionToolsWork() + //{ + // AssistantClient client = GetTestClient(); + // Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + // { + // Tools = + // { + // new FunctionToolDefinition() + // { + // FunctionName = "get_favorite_food_for_day_of_week", + // Description = "gets the user's favorite food for a given day of the week, like Tuesday", + // Parameters = BinaryData.FromObjectAsJson(new + // { + // type = "object", + // properties = new + // { + // day_of_week = new + // { + // type = "string", + // description = "a day of the week, like Tuesday or Saturday", + // } + // } + // }), + // }, + // }, + // }); + // Validate(assistant); + // Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); + + // FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; + // Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + // Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); + + // ThreadRun run = client.CreateThreadAndRun( + // assistant, + // new ThreadCreationOptions() + // { + // InitialMessages = { new(["What should I eat on Thursday?"]) }, + // }, + // new RunCreationOptions() + // { + // AdditionalInstructions = "Call provided tools when appropriate.", + // }); + // Validate(run); + + // for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) + // { + // Thread.Sleep(500); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.RequiresAction)); + // Assert.That(run.RequiredActions?.Count, Is.EqualTo(1)); + // Assert.That(run.RequiredActions[0].ToolCallId, Is.Not.Null.Or.Empty); + // Assert.That(run.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); + // Assert.That(run.RequiredActions[0].FunctionArguments, Is.Not.Null.Or.Empty); + + // run = client.SubmitToolOutputsToRun(run, [new(run.RequiredActions[0].ToolCallId, "tacos")]); + // Assert.That(run.Status.IsTerminal, Is.False); + + // for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) + // { + // Thread.Sleep(500); + // run = client.GetRun(run); + // } + // Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + + // 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, Does.Contain("tacos")); + //} [Test] public async Task StreamingRunWorks() @@ -561,13 +555,11 @@ This file describes the favorite foods of several people. Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); - do - { - Thread.Sleep(1000); - run = client.GetRun(run); - } while (run?.Status.IsTerminal == false); + ResultOperation runOperation = client.CreateRun(thread, assistant); + Validate(runOperation); + + ThreadRun run = runOperation.WaitForCompletion(); + Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); From 76f2f48b311291342b67d8734fdeaf5589daefc5 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 23 May 2024 14:59:17 -0700 Subject: [PATCH 4/8] nit changes from Azure.Core meeting discussion --- .../Assistants/AssistantRunOperation.cs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index dcff1e9f5..a6735e049 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -21,15 +21,15 @@ internal class AssistantRunOperation : ResultOperation private readonly Func>> _getRunAsync; private TimeSpan _pollingInterval = TimeSpan.FromSeconds(2); - private ClientResult _result; + private ClientResult _lastSeenResult; public AssistantRunOperation(ClientResult createResult, Func> getRun, Func>> getRunAsync) : base(GetIdFromResult(createResult), GetResponseFromResult(createResult)) { - _result = createResult; - Value = _result.Value; + _lastSeenResult = createResult; + Value = _lastSeenResult.Value; _threadId = createResult.Value.ThreadId; _runId = createResult.Value.Id; @@ -51,42 +51,44 @@ public override ClientResult UpdateStatus() { if (HasCompleted) { - return _result; + return _lastSeenResult; } - _result = _getRun(_threadId, _runId); + ClientResult result = _getRun(_threadId, _runId); - Value = _result.Value; + // Compute delta between result and _lastResult - if (_result.Value.Status.IsTerminal) + Value = _lastSeenResult.Value; + + if (_lastSeenResult.Value.Status.IsTerminal) { HasCompleted = true; } - SetRawResponse(_result.GetRawResponse()); + SetRawResponse(_lastSeenResult.GetRawResponse()); - return _result; + return _lastSeenResult; } public override async ValueTask UpdateStatusAsync() { if (HasCompleted) { - return _result; + return _lastSeenResult; } - _result = await _getRunAsync(_threadId, _runId).ConfigureAwait(false); + _lastSeenResult = await _getRunAsync(_threadId, _runId).ConfigureAwait(false); - Value = _result.Value; + Value = _lastSeenResult.Value; - if (_result.Value.Status.IsTerminal) + if (_lastSeenResult.Value.Status.IsTerminal) { HasCompleted = true; } - SetRawResponse(_result.GetRawResponse()); + SetRawResponse(_lastSeenResult.GetRawResponse()); - return _result; + return _lastSeenResult; } public override ClientResult WaitForCompletion(CancellationToken cancellationToken = default) @@ -102,7 +104,7 @@ public override ClientResult WaitForCompletion(TimeSpan pollingInterv if (HasCompleted) { - return _result; + return _lastSeenResult; } cancellationToken.WaitHandle.WaitOne(_pollingInterval); @@ -122,7 +124,7 @@ public override async Task> WaitForCompletionAsync(TimeS if (HasCompleted) { - return _result; + return _lastSeenResult; } await Task.Delay(pollingInterval, cancellationToken).ConfigureAwait(false); From 235c24d91a8103634d3c5c2f6ad9a94c0debc11a Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 24 May 2024 14:17:55 -0700 Subject: [PATCH 5/8] updates to match SCM APIs --- .dotnet/src/Custom/Assistants/AssistantRunOperation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index a6735e049..90de3a505 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -111,10 +111,10 @@ public override ClientResult WaitForCompletion(TimeSpan pollingInterv } } - public override async Task> WaitForCompletionAsync(CancellationToken cancellationToken = default) + public override async ValueTask> WaitForCompletionAsync(CancellationToken cancellationToken = default) => await WaitForCompletionAsync(_pollingInterval, cancellationToken).ConfigureAwait(false); - public override async Task> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) + public override async ValueTask> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) { while (true) { From ad42022d145dedca04bf4bf1011a10be8b506d00 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 24 May 2024 16:40:06 -0700 Subject: [PATCH 6/8] Move to StatusBasedOperation --- .../Assistants/AssistantClient.Convenience.cs | 4 +- .../src/Custom/Assistants/AssistantClient.cs | 4 +- .../Assistants/AssistantRunOperation.cs | 91 ++++++++++++----- .../Assistants/AssistantTests.cs | 99 +++++++++++++++++-- 4 files changed, 164 insertions(+), 34 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs b/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs index 9a88b6857..950b1fdf3 100644 --- a/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs +++ b/.dotnet/src/Custom/Assistants/AssistantClient.Convenience.cs @@ -215,7 +215,7 @@ public virtual ClientResult DeleteMessage(ThreadMessage message) /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual Task> CreateRunAsync(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) + public virtual Task> CreateRunAsync(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) => CreateRunAsync(thread?.Id, assistant?.Id, options); /// @@ -226,7 +226,7 @@ public virtual Task> CreateRunAsync(AssistantThread t /// The assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual ResultOperation CreateRun(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) + public virtual StatusBasedOperation CreateRun(AssistantThread thread, Assistant assistant, RunCreationOptions options = null) => CreateRun(thread?.Id, assistant?.Id, options); /// diff --git a/.dotnet/src/Custom/Assistants/AssistantClient.cs b/.dotnet/src/Custom/Assistants/AssistantClient.cs index 1a8147c9f..3a942739b 100644 --- a/.dotnet/src/Custom/Assistants/AssistantClient.cs +++ b/.dotnet/src/Custom/Assistants/AssistantClient.cs @@ -451,7 +451,7 @@ public virtual ClientResult DeleteMessage(string threadId, string messageI /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null) + public virtual async Task> CreateRunAsync(string threadId, string assistantId, RunCreationOptions options = null) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); @@ -474,7 +474,7 @@ public virtual async Task> CreateRunAsync(string thre /// The ID of the assistant that should be used when evaluating the thread. /// Additional options for the run. /// A new instance. - public virtual ResultOperation CreateRun(string threadId, string assistantId, RunCreationOptions options = null) + public virtual StatusBasedOperation CreateRun(string threadId, string assistantId, RunCreationOptions options = null) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); Argument.AssertNotNullOrEmpty(assistantId, nameof(assistantId)); diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index 90de3a505..a43f4c7e4 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -1,8 +1,6 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,21 +10,24 @@ namespace OpenAI.Assistants; // TODO: add hooks for cancel run? -internal class AssistantRunOperation : ResultOperation +internal class AssistantRunOperation : StatusBasedOperation { + private static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromSeconds(2); + private readonly string _threadId; private readonly string _runId; private readonly Func> _getRun; private readonly Func>> _getRunAsync; - private TimeSpan _pollingInterval = TimeSpan.FromSeconds(2); private ClientResult _lastSeenResult; - public AssistantRunOperation(ClientResult createResult, + private bool _statusChanged; + + public AssistantRunOperation(ClientResult createResult, Func> getRun, Func>> getRunAsync) : - base(GetIdFromResult(createResult), GetResponseFromResult(createResult)) + base(GetIdFromResult(createResult), createResult.Value.Status, GetResponseFromResult(createResult)) { _lastSeenResult = createResult; Value = _lastSeenResult.Value; @@ -56,18 +57,25 @@ public override ClientResult UpdateStatus() ClientResult result = _getRun(_threadId, _runId); - // Compute delta between result and _lastResult + // Compute delta between result and _lastSeenResult + if (_lastSeenResult.Value.Status != result.Value.Status) + { + Status = result.Value.Status; + _statusChanged = true; + } - Value = _lastSeenResult.Value; + _lastSeenResult = result; - if (_lastSeenResult.Value.Status.IsTerminal) + Value = result.Value; + + if (result.Value.Status.IsTerminal) { HasCompleted = true; } - SetRawResponse(_lastSeenResult.GetRawResponse()); + SetRawResponse(result.GetRawResponse()); - return _lastSeenResult; + return result; } public override async ValueTask UpdateStatusAsync() @@ -91,11 +99,10 @@ public override async ValueTask UpdateStatusAsync() return _lastSeenResult; } - public override ClientResult WaitForCompletion(CancellationToken cancellationToken = default) - => WaitForCompletion(_pollingInterval, cancellationToken); - - public override ClientResult WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken = default) + public override ClientResult WaitForCompletion(TimeSpan? pollingInterval = default, CancellationToken cancellationToken = default) { + pollingInterval ??= DefaultPollingInterval; + while (true) { cancellationToken.ThrowIfCancellationRequested(); @@ -107,15 +114,14 @@ public override ClientResult WaitForCompletion(TimeSpan pollingInterv return _lastSeenResult; } - cancellationToken.WaitHandle.WaitOne(_pollingInterval); + cancellationToken.WaitHandle.WaitOne(pollingInterval.Value); } } - public override async ValueTask> WaitForCompletionAsync(CancellationToken cancellationToken = default) - => await WaitForCompletionAsync(_pollingInterval, cancellationToken).ConfigureAwait(false); - - public override async ValueTask> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) + public override async ValueTask> WaitForCompletionAsync(TimeSpan? pollingInterval = default, CancellationToken cancellationToken = default) { + pollingInterval ??= DefaultPollingInterval; + while (true) { cancellationToken.ThrowIfCancellationRequested(); @@ -127,20 +133,59 @@ public override async ValueTask> WaitForCompletionAsync( return _lastSeenResult; } - await Task.Delay(pollingInterval, cancellationToken).ConfigureAwait(false); + await Task.Delay(pollingInterval.Value, cancellationToken).ConfigureAwait(false); } } public override ClientResult WaitForCompletionResult(CancellationToken cancellationToken = default) - => WaitForCompletion(_pollingInterval, cancellationToken); + => WaitForCompletion(DefaultPollingInterval, cancellationToken); public override ClientResult WaitForCompletionResult(TimeSpan pollingInterval, CancellationToken cancellationToken = default) => WaitForCompletion(pollingInterval, cancellationToken); public override async ValueTask WaitForCompletionResultAsync(CancellationToken cancellationToken = default) - => await WaitForCompletionAsync(_pollingInterval, cancellationToken).ConfigureAwait(false); + => await WaitForCompletionAsync(DefaultPollingInterval, cancellationToken).ConfigureAwait(false); public override async ValueTask WaitForCompletionResultAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) => await WaitForCompletionAsync(pollingInterval, cancellationToken).ConfigureAwait(false); + + public override async ValueTask> WaitForStatusUpdateAsync(TimeSpan? pollingInterval = null, CancellationToken cancellationToken = default) + { + pollingInterval ??= DefaultPollingInterval; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + ClientResult result = await UpdateStatusAsync().ConfigureAwait(false); + + if (_statusChanged) + { + (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); + return FromValue(tuple, result.GetRawResponse()); + } + + await Task.Delay(pollingInterval.Value, cancellationToken).ConfigureAwait(false); + } + } + + public override ClientResult<(RunStatus Status, ThreadRun? Value)> WaitForStatusUpdate(TimeSpan? pollingInterval = null, CancellationToken cancellationToken = default) + { + pollingInterval ??= DefaultPollingInterval; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + ClientResult result = UpdateStatus(); + + if (_statusChanged) + { + (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); + return FromValue(tuple, result.GetRawResponse()); + } + + cancellationToken.WaitHandle.WaitOne(pollingInterval.Value); + } + } } diff --git a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs index 698925132..a1056e727 100644 --- a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs +++ b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs @@ -182,7 +182,7 @@ public void BasicRunOperationsWork() Assert.That(runs.Count, Is.EqualTo(0)); ThreadMessage message = client.CreateMessage(thread.Id, ["Hello, assistant!"]); Validate(message); - ResultOperation runOperation = client.CreateRun(thread.Id, assistant.Id); + StatusBasedOperation runOperation = client.CreateRun(thread.Id, assistant.Id); Validate(runOperation.Value); Assert.That(runOperation.Value.Status, Is.EqualTo(RunStatus.Queued)); Assert.That(runOperation.Value.CreatedAt, Is.GreaterThan(s_2024)); @@ -195,7 +195,7 @@ public void BasicRunOperationsWork() Assert.That(messages.Count, Is.GreaterThanOrEqualTo(1)); ThreadRun run = runOperation.WaitForCompletion(); - + Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); Assert.That(run.CompletedAt, Is.GreaterThan(s_2024)); Assert.That(run.RequiredActions.Count, Is.EqualTo(0)); @@ -228,7 +228,7 @@ public void BasicRunStepFunctionalityWorks() }); Validate(thread); - ResultOperation runOperation = client.CreateRun(thread, assistant); + StatusBasedOperation runOperation = client.CreateRun(thread, assistant); Validate(runOperation); ThreadRun run = runOperation.WaitForCompletion(); @@ -280,7 +280,7 @@ public void SettingResponseFormatWorks() Validate(thread); ThreadMessage message = client.CreateMessage(thread, ["Write some JSON for me!"]); Validate(message); - ResultOperation runOperation = client.CreateRun(thread, assistant, new() + StatusBasedOperation runOperation = client.CreateRun(thread, assistant, new() { ResponseFormat = AssistantResponseFormat.JsonObject, }); @@ -555,11 +555,45 @@ This file describes the favorite foods of several people. Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); - ResultOperation runOperation = client.CreateRun(thread, assistant); + StatusBasedOperation runOperation = client.CreateRun(thread, assistant); Validate(runOperation); - ThreadRun run = runOperation.WaitForCompletion(); - + while (true) + { + (RunStatus Status, ThreadRun Value) update = runOperation.WaitForStatusUpdate(); + + if (update.Status == RunStatus.RequiresAction) + { + List outputs = new(); + + foreach (RequiredAction action in update.Value.RequiredActions) + { + string output = action.FunctionName switch + { + string s when s == getTemperatureTool.FunctionName + => GetTemperature(("Seattle", "Farenheit")), + string s when s == getRainProbabilityTool.FunctionName + => GetRainProbability("Seattle, WA"), + _ => throw new InvalidOperationException() + }; + + outputs.Add(new ToolOutput(action.ToolCallId, output)); + + } + + client.SubmitToolOutputsToRun(update.Value, outputs); + } + + if (update.Status.IsTerminal) + { + break; + } + } + + Assert.That(runOperation.Value, Is.Not.Null); + + ThreadRun run = runOperation.Value; + Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); @@ -578,6 +612,57 @@ This file describes the favorite foods of several people. Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); } + FunctionToolDefinition getTemperatureTool = new() + { + FunctionName = "get_current_temperature", + Description = "Gets the current temperature at a specific location.", + Parameters = BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g., San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["Celsius", "Fahrenheit"], + "description": "The temperature unit to use. Infer this from the user's location." + } + } + } + """), + }; + + FunctionToolDefinition getRainProbabilityTool = new() + { + FunctionName = "get_current_rain_probability", + Description = "Gets the current forecasted probability of rain at a specific location," + + " represented as a percent chance in the range of 0 to 100.", + Parameters = BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g., San Francisco, CA" + } + }, + "required": ["location"] + } + """), + }; + + private string GetTemperature((string City, string Unit) input) + { + return "57"; + } + + private string GetRainProbability(string location) + { + return "25%"; + } + [Test] public async Task CanEnumerateAssistants() { From 11db7180d421da342081b2a1c011709addbf538d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 24 May 2024 16:59:07 -0700 Subject: [PATCH 7/8] add separate test for require_action --- .../Assistants/AssistantTests.cs | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs index a1056e727..96955278c 100644 --- a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs +++ b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using static OpenAI.Tests.TestHelpers; @@ -556,11 +555,58 @@ This file describes the favorite foods of several people. Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); StatusBasedOperation runOperation = client.CreateRun(thread, assistant); - Validate(runOperation); + ThreadRun run = runOperation.WaitForCompletion(); + + Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); + + PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); + foreach (ThreadMessage message in messages) + { + foreach (MessageContent content in message.Content) + { + Console.WriteLine(content.Text); + foreach (TextAnnotation annotation in content.TextAnnotations) + { + Console.WriteLine($" --> From file: {annotation.InputFileId}, quote: {annotation.InputQuote}, replacement: {annotation.TextToReplace}"); + } + } + } + Assert.That(messages.Count() > 1); + Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); + } - while (true) + [Test] + public void RunStepsWithActionsWork() + { + AssistantClient client = GetTestClient(); + + // Create an assistant that can call the function tools. + AssistantCreationOptions assistantOptions = new() { - (RunStatus Status, ThreadRun Value) update = runOperation.WaitForStatusUpdate(); + Name = "Sample: Function Calling", + Instructions = + "Don't make assumptions about what values to plug into functions." + + " Ask for clarification if a user request is ambiguous.", + Tools = { getTemperatureTool, getRainProbabilityTool }, + }; + + Assistant assistant = client.CreateAssistant("gpt-4-turbo", assistantOptions); + Validate(assistant); + + // Create a thread with an override vector store + AssistantThread thread = client.CreateThread(); + ThreadMessage message = client.CreateMessage( + thread, + [ + "What's the weather in Seattle today and the likelihood it'll rain?" + ]); + + StatusBasedOperation runOperation = client.CreateRun(thread, assistant); + + (RunStatus Status, ThreadRun Value) update; + do + { + update = runOperation.WaitForStatusUpdate(); if (update.Status == RunStatus.RequiresAction) { @@ -583,12 +629,8 @@ This file describes the favorite foods of several people. client.SubmitToolOutputsToRun(update.Value, outputs); } - - if (update.Status.IsTerminal) - { - break; - } } + while (!update.Status.IsTerminal); Assert.That(runOperation.Value, Is.Not.Null); @@ -597,19 +639,8 @@ This file describes the favorite foods of several people. Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); - foreach (ThreadMessage message in messages) - { - foreach (MessageContent content in message.Content) - { - Console.WriteLine(content.Text); - foreach (TextAnnotation annotation in content.TextAnnotations) - { - Console.WriteLine($" --> From file: {annotation.InputFileId}, quote: {annotation.InputQuote}, replacement: {annotation.TextToReplace}"); - } - } - } Assert.That(messages.Count() > 1); - Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); + Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("weather")))); } FunctionToolDefinition getTemperatureTool = new() From 38bd485d2dbff4a64d3dc95b482a93d6cf0bdb98 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 28 May 2024 08:54:36 -0700 Subject: [PATCH 8/8] add pause and resume --- .../Assistants/AssistantRunOperation.cs | 55 +++++++++++++------ .../Assistants/AssistantTests.cs | 6 ++ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs index a43f4c7e4..7d706c1f3 100644 --- a/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs +++ b/.dotnet/src/Custom/Assistants/AssistantRunOperation.cs @@ -23,6 +23,7 @@ internal class AssistantRunOperation : StatusBasedOperation _lastSeenResult; private bool _statusChanged; + private bool _paused; public AssistantRunOperation(ClientResult createResult, Func> getRun, @@ -107,13 +108,18 @@ public override ClientResult WaitForCompletion(TimeSpan? pollingInter { cancellationToken.ThrowIfCancellationRequested(); - UpdateStatus(); - - if (HasCompleted) + if (!_paused) { - return _lastSeenResult; + UpdateStatus(); + + if (HasCompleted) + { + return _lastSeenResult; + } } + // TODO: note pollling interval logic may change for e.g. exponential + // backoff if the operation is paused. cancellationToken.WaitHandle.WaitOne(pollingInterval.Value); } } @@ -126,11 +132,14 @@ public override async ValueTask> WaitForCompletionAsync( { cancellationToken.ThrowIfCancellationRequested(); - await UpdateStatusAsync().ConfigureAwait(false); - - if (HasCompleted) + if (!_paused) { - return _lastSeenResult; + await UpdateStatusAsync().ConfigureAwait(false); + + if (HasCompleted) + { + return _lastSeenResult; + } } await Task.Delay(pollingInterval.Value, cancellationToken).ConfigureAwait(false); @@ -157,12 +166,15 @@ public override async ValueTask WaitForCompletionResultAsync(TimeS { cancellationToken.ThrowIfCancellationRequested(); - ClientResult result = await UpdateStatusAsync().ConfigureAwait(false); - - if (_statusChanged) + if (!_paused) { - (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); - return FromValue(tuple, result.GetRawResponse()); + ClientResult result = await UpdateStatusAsync().ConfigureAwait(false); + + if (_statusChanged) + { + (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); + return FromValue(tuple, result.GetRawResponse()); + } } await Task.Delay(pollingInterval.Value, cancellationToken).ConfigureAwait(false); @@ -177,15 +189,22 @@ public override async ValueTask WaitForCompletionResultAsync(TimeS { cancellationToken.ThrowIfCancellationRequested(); - ClientResult result = UpdateStatus(); - - if (_statusChanged) + if (!_paused) { - (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); - return FromValue(tuple, result.GetRawResponse()); + ClientResult result = UpdateStatus(); + + if (_statusChanged) + { + (RunStatus Status, ThreadRun? Value) tuple = new(_lastSeenResult.Value.Status, _lastSeenResult.Value); + return FromValue(tuple, result.GetRawResponse()); + } } cancellationToken.WaitHandle.WaitOne(pollingInterval.Value); } } + + public override void Pause() => _paused = true; + + public override void Resume() => _paused = false; } diff --git a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs index 96955278c..e64193738 100644 --- a/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs +++ b/.dotnet/tests/TestScenarios/Assistants/AssistantTests.cs @@ -610,6 +610,9 @@ public void RunStepsWithActionsWork() if (update.Status == RunStatus.RequiresAction) { + // Temporarily stop polling + runOperation.Pause(); + List outputs = new(); foreach (RequiredAction action in update.Value.RequiredActions) @@ -628,6 +631,9 @@ public void RunStepsWithActionsWork() } client.SubmitToolOutputsToRun(update.Value, outputs); + + // Start polling again + runOperation.Resume(); } } while (!update.Status.IsTerminal);