diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 98e06b7449b1..948f961ff5de 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/dotnet/docs/EXPERIMENTS.md b/dotnet/docs/EXPERIMENTS.md
index 50f99e702de2..f98d36a7a4c3 100644
--- a/dotnet/docs/EXPERIMENTS.md
+++ b/dotnet/docs/EXPERIMENTS.md
@@ -24,6 +24,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
- SKEXP0012: OpenAI image service
- SKEXP0013: OpenAI parameters
- SKEXP0014: OpenAI chat history extension
+- SKEXP0015: OpenAI file service
## Memory connectors
diff --git a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
index b5a924e72b66..76e051bb58bb 100644
--- a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
@@ -58,12 +58,7 @@ public class Example74_FlowOrchestrator : BaseTest
");
[Fact(Skip = "Can take more than 1 minute")]
- public Task RunAsync()
- {
- return RunExampleAsync();
- }
-
- private async Task RunExampleAsync()
+ public async Task RunAsync()
{
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
@@ -76,18 +71,18 @@ private async Task RunExampleAsync()
FlowOrchestrator orchestrator = new(
GetKernelBuilder(LoggerFactory),
- await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
+ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()),
plugins,
config: GetOrchestratorConfig());
var sessionId = Guid.NewGuid().ToString();
WriteLine("*****************************************************");
- WriteLine("Executing " + nameof(RunExampleAsync));
+ WriteLine("Executing " + nameof(RunAsync));
Stopwatch sw = new();
sw.Start();
WriteLine("Flow: " + s_flow.Name);
var question = s_flow.Steps.First().Goal;
- var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
+ var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question);
WriteLine("Question: " + question);
WriteLine("Answer: " + result.Metadata!["answer"]);
@@ -105,7 +100,7 @@ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(
foreach (var t in userInputs)
{
WriteLine($"User: {t}");
- result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
+ result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t);
var responses = result.GetValue>()!;
foreach (var response in responses)
{
diff --git a/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs b/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
index 9d5959b6952d..f3814d7fc778 100644
--- a/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
@@ -4,7 +4,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Experimental.Agents;
+using Resources;
using Xunit;
using Xunit.Abstractions;
@@ -80,29 +83,27 @@ public async Task RunRetrievalToolAsync()
return;
}
- // REQUIRED:
- //
- // Use `curl` to upload document prior to running example and assign the
- // identifier to `fileId`.
- //
- // Powershell:
- // curl https://api.openai.com/v1/files `
- // -H "Authorization: Bearer $Env:OPENAI_APIKEY" `
- // -F purpose="assistants" `
- // -F file="@Resources/travelinfo.txt"
+ var kernel = Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build();
+ var fileService = kernel.GetRequiredService();
+ var result =
+ await fileService.UploadContentAsync(
+ new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream("travelinfo.txt")!)),
+ new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants));
- var fileId = "";
+ var fileId = result.Id;
var defaultAgent =
- await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
- .BuildAsync();
+ Track(
+ await new AgentBuilder()
+ .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .BuildAsync());
var retrievalAgent =
- await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
- .WithRetrieval(fileId)
- .BuildAsync();
+ Track(
+ await new AgentBuilder()
+ .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithRetrieval(fileId)
+ .BuildAsync());
try
{
@@ -115,7 +116,7 @@ await ChatAsync(
}
finally
{
- await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()));
+ await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId)));
}
}
diff --git a/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs b/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs
new file mode 100644
index 000000000000..f878a7486ac1
--- /dev/null
+++ b/dotnet/samples/KernelSyntaxExamples/Example79_OpenAIFiles.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Resources;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Examples;
+
+// ReSharper disable once InconsistentNaming
+///
+/// Showcase usage of Open AI file-service.
+///
+public sealed class Example79_OpenAIFiles : BaseTest
+{
+ private const string ResourceFileName = "30-user-context.txt";
+
+ ///
+ /// Show how to utilize OpenAI file-service.
+ ///
+ [Fact]
+ public async Task RunFileLifecycleAsync()
+ {
+ this.WriteLine("======== OpenAI File-Service ========");
+
+ if (TestConfiguration.OpenAI.ApiKey == null)
+ {
+ this.WriteLine("OpenAI apiKey not found. Skipping example.");
+ return;
+ }
+
+ // Initialize file-service
+ var kernel =
+ Kernel.CreateBuilder()
+ .AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey)
+ .Build();
+
+ var fileService = kernel.GetRequiredService();
+
+ // Upload file
+ var fileContent = new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream(ResourceFileName)!));
+ var fileReference =
+ await fileService.UploadContentAsync(
+ fileContent,
+ new OpenAIFileUploadExecutionSettings(ResourceFileName, OpenAIFilePurpose.Assistants));
+
+ WriteLine("SOURCE:");
+ WriteLine($"# Name: {fileReference.FileName}");
+ WriteLine("# Content:");
+ WriteLine(await fileContent.GetContentAsync());
+
+ try
+ {
+ // Retrieve file metadata for validation.
+ var copyReference = await fileService.GetFileAsync(fileReference.Id);
+ Assert.Equal(fileReference.Id, copyReference.Id);
+ WriteLine("REFERENCE:");
+ WriteLine($"# ID: {fileReference.Id}");
+ WriteLine($"# Name: {fileReference.FileName}");
+ WriteLine($"# Purpose: {fileReference.Purpose}");
+ }
+ finally
+ {
+ // Remove file
+ await fileService.DeleteFileAsync(fileReference.Id);
+ }
+ }
+
+ public Example79_OpenAIFiles(ITestOutputHelper output) : base(output) { }
+}
diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
index 3ca6bfe9e7ec..4755342c698e 100644
--- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
+++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
@@ -10,7 +10,7 @@
true
false
- CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102
+ CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102
Library
diff --git a/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs b/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
index 34f386783538..3b8a588479c8 100644
--- a/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Resources/EmbeddedResource.cs
@@ -23,13 +23,15 @@ internal static class EmbeddedResource
internal static string Read(string fileName)
{
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
- Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
- if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
+ Assembly assembly =
+ typeof(EmbeddedResource).GetTypeInfo().Assembly ??
+ throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
var resourceName = $"{s_namespace}." + fileName;
- using Stream? resource = assembly.GetManifestResourceStream(resourceName);
- if (resource == null) { throw new ConfigurationException($"{resourceName} resource not found"); }
+ using Stream resource =
+ assembly.GetManifestResourceStream(resourceName) ??
+ throw new ConfigurationException($"{resourceName} resource not found");
// Return the resource content, in text format.
using var reader = new StreamReader(resource);
@@ -39,8 +41,9 @@ internal static string Read(string fileName)
internal static Stream? ReadStream(string fileName)
{
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
- Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
- if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
+ Assembly assembly =
+ typeof(EmbeddedResource).GetTypeInfo().Assembly ??
+ throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
var resourceName = $"{s_namespace}." + fileName;
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs
new file mode 100644
index 000000000000..bf647296a06c
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Defines the purpose associated with the uploaded file.
+///
+[Experimental("SKEXP0015")]
+public enum OpenAIFilePurpose
+{
+ ///
+ /// File to be used by assistants for model processing.
+ ///
+ Assistants,
+
+ ///
+ /// File to be used by fine-tuning jobs.
+ ///
+ FineTune,
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs
new file mode 100644
index 000000000000..de2f37e79df8
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// References an uploaded file by id.
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileReference
+{
+ ///
+ /// The file identifier.
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// The timestamp the file was uploaded.s
+ ///
+ public DateTime CreatedTimestamp { get; set; }
+
+ ///
+ /// The name of the file.s
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// Describes the associated purpose of the file.
+ ///
+ public OpenAIFilePurpose Purpose { get; set; }
+
+ ///
+ /// The file size, in bytes.
+ ///
+ public int SizeInBytes { get; set; }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
new file mode 100644
index 000000000000..ac76d8af2901
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs
@@ -0,0 +1,265 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel.Http;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// File service access for OpenAI: https://api.openai.com/v1/files
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileService
+{
+ private const string OpenAIApiEndpoint = "https://api.openai.com/v1/";
+ private const string OpenAIApiRouteFiles = "files";
+
+ private readonly string _apiKey;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private readonly Uri _serviceUri;
+ private readonly string? _organization;
+
+ ///
+ /// Create an instance of the OpenAI chat completion connector
+ ///
+ /// OpenAI API Key
+ /// OpenAI Organization Id (usually optional)
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAIFileService(
+ string apiKey,
+ string? organization = null,
+ HttpClient? httpClient = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Verify.NotNull(apiKey, nameof(apiKey));
+
+ this._apiKey = apiKey;
+ this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance;
+ this._httpClient = HttpClientProvider.GetHttpClient(httpClient);
+ this._serviceUri = new Uri(this._httpClient.BaseAddress ?? new Uri(OpenAIApiEndpoint), OpenAIApiRouteFiles);
+ this._organization = organization;
+ }
+
+ ///
+ /// Remove a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ public async Task DeleteFileAsync(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ await this.ExecuteDeleteRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Retrieve the file content from a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ /// The file content as
+ ///
+ /// Files uploaded with do not support content retrieval.
+ ///
+ public BinaryContent GetFileContent(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ return new BinaryContent(() => this.StreamGetRequestAsync($"{this._serviceUri}/{id}/content", cancellationToken));
+ }
+
+ ///
+ /// Retrieve metadata for a previously uploaded file.
+ ///
+ /// The uploaded file identifier.
+ /// The to monitor for cancellation requests. The default is .
+ /// Thet metadata associated with the specified file identifier.
+ public async Task GetFileAsync(string id, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(id, nameof(id));
+
+ var result = await this.ExecuteGetRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
+
+ return this.ConvertFileReference(result);
+ }
+
+ ///
+ /// Retrieve metadata for all previously uploaded files.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// Thet metadata of all uploaded files.
+ public async Task> GetFilesAsync(CancellationToken cancellationToken = default)
+ {
+ var result = await this.ExecuteGetRequestAsync(this._serviceUri.ToString(), cancellationToken).ConfigureAwait(false);
+
+ return result.Data.Select(r => this.ConvertFileReference(r)).ToArray();
+ }
+
+ ///
+ /// Upload a file.
+ ///
+ /// The file content as
+ /// The upload settings
+ /// The to monitor for cancellation requests. The default is .
+ /// The file metadata.
+ public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(settings, nameof(settings));
+
+ using var formData = new MultipartFormDataContent();
+ using var contentPurpose = new StringContent(this.ConvertPurpose(settings.Purpose));
+ using var contentStream = await fileContent.GetStreamAsync().ConfigureAwait(false);
+ using var contentFile = new StreamContent(contentStream);
+ formData.Add(contentPurpose, "purpose");
+ formData.Add(contentFile, "file", settings.FileName);
+
+ var result = await this.ExecutePostRequestAsync(this._serviceUri.ToString(), formData, cancellationToken).ConfigureAwait(false);
+
+ return this.ConvertFileReference(result);
+ }
+
+ private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateDeleteRequest(url);
+ this.AddRequestHeaders(request);
+ using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task ExecuteGetRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateGetRequest(url);
+ this.AddRequestHeaders(request);
+ using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
+
+ var model = JsonSerializer.Deserialize(body);
+
+ return
+ model ??
+ throw new KernelException($"Unexpected response from {url}")
+ {
+ Data = { { "ResponseData", body } },
+ };
+ }
+
+ private async Task StreamGetRequestAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = HttpRequest.CreateGetRequest(url);
+ this.AddRequestHeaders(request);
+ var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ return
+ new HttpResponseStream(
+ await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false),
+ response);
+ }
+ catch
+ {
+ response.Dispose();
+ throw;
+ }
+ }
+
+ private async Task ExecutePostRequestAsync(string url, HttpContent payload, CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = payload };
+ this.AddRequestHeaders(request);
+ using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
+
+ var model = JsonSerializer.Deserialize(body);
+
+ return
+ model ??
+ throw new KernelException($"Unexpected response from {url}")
+ {
+ Data = { { "ResponseData", body } },
+ };
+ }
+
+ private void AddRequestHeaders(HttpRequestMessage request)
+ {
+ request.Headers.Add("User-Agent", HttpHeaderValues.UserAgent);
+ request.Headers.Add("Authorization", $"Bearer {this._apiKey}");
+
+ if (!string.IsNullOrEmpty(this._organization))
+ {
+ this._httpClient.DefaultRequestHeaders.Add(OpenAIClientCore.OrganizationKey, this._organization);
+ }
+ }
+
+ private OpenAIFileReference ConvertFileReference(FileInfo result)
+ {
+ return
+ new OpenAIFileReference
+ {
+ Id = result.Id,
+ FileName = result.FileName,
+ CreatedTimestamp = DateTimeOffset.FromUnixTimeSeconds(result.CreatedAt).UtcDateTime,
+ SizeInBytes = result.Bytes ?? 0,
+ Purpose = this.ConvertPurpose(result.Purpose),
+ };
+ }
+
+ private OpenAIFilePurpose ConvertPurpose(string purpose) =>
+ purpose.ToUpperInvariant() switch
+ {
+ "ASSISTANTS" => OpenAIFilePurpose.Assistants,
+ "FINE-TUNE" => OpenAIFilePurpose.FineTune,
+ _ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
+ };
+
+ private string ConvertPurpose(OpenAIFilePurpose purpose) =>
+ purpose switch
+ {
+ OpenAIFilePurpose.Assistants => "assistants",
+ OpenAIFilePurpose.FineTune => "fine-tune",
+ _ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
+ };
+
+ private class FileInfoList
+ {
+ [JsonPropertyName("data")]
+ public FileInfo[] Data { get; set; } = Array.Empty();
+
+ [JsonPropertyName("object")]
+ public string Object { get; set; } = "list";
+ }
+
+ private class FileInfo
+ {
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("object")]
+ public string Object { get; set; } = "file";
+
+ [JsonPropertyName("bytes")]
+ public int? Bytes { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public long CreatedAt { get; set; }
+
+ [JsonPropertyName("filename")]
+ public string FileName { get; set; } = string.Empty;
+
+ [JsonPropertyName("purpose")]
+ public string Purpose { get; set; } = string.Empty;
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs
new file mode 100644
index 000000000000..8b042835be38
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Execution serttings associated with Open AI file upload .
+///
+[Experimental("SKEXP0015")]
+public sealed class OpenAIFileUploadExecutionSettings
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file name
+ /// The file purpose
+ public OpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose)
+ {
+ Verify.NotNull(fileName, nameof(fileName));
+
+ this.FileName = fileName;
+ this.Purpose = purpose;
+ }
+
+ ///
+ /// The file name.
+ ///
+ public string FileName { get; }
+
+ ///
+ /// The file purpose.
+ ///
+ public OpenAIFilePurpose Purpose { get; }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
index aa1b9c383c4f..d6343ef0b4cf 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs
@@ -1178,6 +1178,68 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se
#endregion
+ #region Files
+
+ ///
+ /// Add the OpenAI file service to the list
+ ///
+ /// The instance to augment.
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// The HttpClient to use with this service.
+ /// The same instance as .
+ [Experimental("SKEXP0015")]
+ public static IKernelBuilder AddOpenAIFiles(
+ this IKernelBuilder builder,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ HttpClient? httpClient = null)
+ {
+ Verify.NotNull(builder);
+ Verify.NotNullOrWhiteSpace(apiKey);
+
+ builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAIFileService(
+ apiKey,
+ orgId,
+ HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
+ serviceProvider.GetService()));
+
+ return builder;
+ }
+
+ ///
+ /// Add the OpenAI file service to the list
+ ///
+ /// The instance to augment.
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0015")]
+ public static IServiceCollection AddOpenAIFiles(
+ this IServiceCollection services,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+ Verify.NotNullOrWhiteSpace(apiKey);
+
+ services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAIFileService(
+ apiKey,
+ orgId,
+ HttpClientProvider.GetHttpClient(serviceProvider),
+ serviceProvider.GetService()));
+
+ return services;
+ }
+
+ #endregion
+
private static OpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) =>
new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient));
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
index a541b834ba4f..3ca1ee09ce52 100644
--- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
@@ -10,7 +10,7 @@
enable
disable
false
- CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052
+ CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs
new file mode 100644
index 000000000000..9af2f2a33477
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs
@@ -0,0 +1,266 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Connectors.UnitTests.OpenAI.Files;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAIFileServiceTests : IDisposable
+{
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly HttpClient _httpClient;
+ private readonly Mock _mockLoggerFactory;
+
+ public OpenAIFileServiceTests()
+ {
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+ this._mockLoggerFactory = new Mock();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorWorksCorrectly(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var service = includeLoggerFactory ?
+ new OpenAIFileService("api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) :
+ new OpenAIFileService("api-key", "organization");
+
+ // Assert
+ Assert.NotNull(service);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task DeleteFileWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "test.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id"));
+ }
+ else
+ {
+ await service.DeleteFileAsync("file-id");
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetFileWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "file.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.GetFileAsync("file-id"));
+ }
+ else
+ {
+ var file = await service.GetFileAsync("file-id");
+ Assert.NotNull(file);
+ Assert.NotEqual(string.Empty, file.Id);
+ Assert.NotEqual(string.Empty, file.FileName);
+ Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp);
+ Assert.NotEqual(0, file.SizeInBytes);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetFilesWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "data": [
+ {
+ "id": "123",
+ "filename": "file1.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ },
+ {
+ "id": "456",
+ "filename": "file2.txt",
+ "purpose": "assistants",
+ "bytes": 999,
+ "created_at": 1677610606
+ }
+ ]
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.GetFilesAsync());
+ }
+ else
+ {
+ var files = (await service.GetFilesAsync()).ToArray();
+ Assert.NotNull(files);
+ Assert.NotEmpty(files);
+ }
+ }
+
+ [Fact]
+ public async Task GetFileContentWorksCorrectlyAsync()
+ {
+ // Arrange
+ var data = BinaryData.FromString("Hello AI!");
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ this._messageHandlerStub.ResponseToReturn =
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new ByteArrayContent(data.ToArray())
+ };
+
+ // Act & Assert
+ var content = service.GetFileContent("file-id");
+ var result = await content.GetContentAsync();
+ Assert.Equal(data.ToArray(), result.ToArray());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task UploadContentWorksCorrectlyAsync(bool isFailedRequest)
+ {
+ // Arrange
+ var service = new OpenAIFileService("api-key", "organization", this._httpClient);
+ using var response =
+ isFailedRequest ?
+ this.CreateFailedResponse() :
+ this.CreateSuccessResponse(
+ """
+ {
+ "id": "123",
+ "filename": "test.txt",
+ "purpose": "assistants",
+ "bytes": 120000,
+ "created_at": 1677610602
+ }
+ """);
+ this._messageHandlerStub.ResponseToReturn = response;
+
+ var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants);
+
+ await using var stream = new MemoryStream();
+ await using (var writer = new StreamWriter(stream, leaveOpen: true))
+ {
+ await writer.WriteLineAsync("test");
+ await writer.FlushAsync();
+ }
+
+ stream.Position = 0;
+
+ var content = new BinaryContent(() => Task.FromResult(stream));
+
+ // Act & Assert
+ if (isFailedRequest)
+ {
+ await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings));
+ }
+ else
+ {
+ var file = await service.UploadContentAsync(content, settings);
+ Assert.NotNull(file);
+ Assert.NotEqual(string.Empty, file.Id);
+ Assert.NotEqual(string.Empty, file.FileName);
+ Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp);
+ Assert.NotEqual(0, file.SizeInBytes);
+ }
+ }
+
+ private HttpResponseMessage CreateSuccessResponse(string payload)
+ {
+ return
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content =
+ new StringContent(
+ payload,
+ Encoding.UTF8,
+ "application/json")
+ };
+ }
+
+ private HttpResponseMessage CreateFailedResponse(string? payload = null)
+ {
+ return
+ new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
+ {
+ Content =
+ string.IsNullOrEmpty(payload) ?
+ null :
+ new StringContent(
+ payload,
+ Encoding.UTF8,
+ "application/json")
+ };
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj b/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj
deleted file mode 100644
index bdd40950b402..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Experimental.Assistants.UnitTests.csproj
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- SemanticKernel.Experimental.Assistants.UnitTests
- SemanticKernel.Experimental.Assistants.UnitTests
- net6.0
- LatestMajor
- true
- enable
- disable
- false
- CS1591;SKEXP0101
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
-
-
- all
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
-
\ No newline at end of file
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs
deleted file mode 100644
index d30e849edbcc..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelExtensionTests.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class KernelExtensionTests
-{
- private const string TwoPartToolName = "Fake-Bogus";
-
- [Fact]
- public static void InvokeTwoPartTool()
- {
- //Arrange
- var function = KernelFunctionFactory.CreateFromMethod(() => { }, functionName: "Bogus");
-
- var kernel = new Kernel();
- kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("Fake", "Fake functions", new[] { function }));
-
- //Act
- var tool = kernel.GetAssistantTool(TwoPartToolName);
-
- //Assert
- Assert.NotNull(tool);
- Assert.Equal("Bogus", tool.Name);
- }
-
- [Theory]
- [InlineData("Bogus")]
- [InlineData("i-am-not-valid")]
- public static void InvokeInvalidSinglePartTool(string toolName)
- {
- //Arrange
- var kernel = new Kernel();
-
- //Act & Assert
- Assert.Throws(() => kernel.GetAssistantTool(toolName));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs
deleted file mode 100644
index 576ce977667a..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/KernelFunctionExtensionTests.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class KernelFunctionExtensionTests
-{
- private const string ToolName = "Bogus";
- private const string PluginName = "Fake";
-
- [Fact]
- public static void GetTwoPartName()
- {
- var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName);
-
- string qualifiedName = function.GetQualifiedName(PluginName);
-
- Assert.Equal($"{PluginName}-{ToolName}", qualifiedName);
- }
-
- [Fact]
- public static void GetToolModelFromFunction()
- {
- const string FunctionDescription = "Bogus description";
- const string RequiredParamName = "required";
- const string OptionalParamName = "optional";
-
- var requiredParam = new KernelParameterMetadata("required") { IsRequired = true };
- var optionalParam = new KernelParameterMetadata("optional");
- var parameters = new List { requiredParam, optionalParam };
- var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName, FunctionDescription, parameters);
-
- var toolModel = function.ToToolModel(PluginName);
- var properties = toolModel.Function?.Parameters.Properties;
- var required = toolModel.Function?.Parameters.Required;
-
- Assert.Equal("function", toolModel.Type);
- Assert.Equal($"{PluginName}-{ToolName}", toolModel.Function?.Name);
- Assert.Equal(FunctionDescription, toolModel.Function?.Description);
- Assert.Equal(2, properties?.Count);
- Assert.True(properties?.ContainsKey(RequiredParamName));
- Assert.True(properties?.ContainsKey(OptionalParamName));
- Assert.Equal(1, required?.Count ?? 0);
- Assert.True(required?.Contains(RequiredParamName) ?? false);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs
deleted file mode 100644
index 87a12e538646..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.AssistantTests.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsAssistantTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestAssistantId = "assistantId";
-
- private readonly AssistantModel _assistantModel = new();
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsAssistantTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateAssistantModelAsync()
- {
- await this._restContext.CreateAssistantModelAsync(this._assistantModel).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseAssistantUrl);
- }
-
- [Fact]
- public async Task GetAssistantModelAsync()
- {
- await this._restContext.GetAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
- }
-
- [Fact]
- public async Task ListAssistantModelsAsync()
- {
- await this._restContext.ListAssistantModelsAsync(10, false, "20").ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, $"{OpenAIRestExtensions.BaseAssistantUrl}?limit=10&order=desc&after=20");
- }
-
- [Fact]
- public async Task DeleteAssistantModelAsync()
- {
- await this._restContext.DeleteAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs
deleted file mode 100644
index 4168daace491..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.MessagesTests.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsMessagesTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestThreadId = "threadId";
- private const string TestMessageId = "msgId";
- private const string TestContent = "Blah blah";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsMessagesTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateMessageModelAsync()
- {
- await this._restContext.CreateUserTextMessageAsync(TestThreadId, TestContent).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetMessageModelAsync()
- {
- await this._restContext.GetMessageAsync(TestThreadId, TestMessageId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId, TestMessageId));
- }
-
- [Fact]
- public async Task GetMessageModelsAsync()
- {
- await this._restContext.GetMessagesAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetSpecificMessageModelsAsync()
- {
- var messageIDs = new string[] { "1", "2", "3" };
-
- await this._restContext.GetMessagesAsync(TestThreadId, messageIDs).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, messageIDs.Length);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs
deleted file mode 100644
index 41ef1ed510ad..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.RunTests.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsRunTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestAssistantId = "assistantId";
- private const string TestThreadId = "threadId";
- private const string TestRunId = "runId";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsRunTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateRunAsync()
- {
- await this._restContext.CreateRunAsync(TestThreadId, TestAssistantId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId));
- }
-
- [Fact]
- public async Task GetRunAsync()
- {
- await this._restContext.GetRunAsync(TestThreadId, TestRunId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId, TestRunId));
- }
-
- [Fact]
- public async Task GetRunStepsAsync()
- {
- await this._restContext.GetRunStepsAsync(TestThreadId, TestRunId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunStepsUrl(TestThreadId, TestRunId));
- }
-
- [Fact]
- public async Task AddToolOutputsAsync()
- {
- var toolResults = Array.Empty();
-
- await this._restContext.AddToolOutputsAsync(TestThreadId, TestRunId, toolResults).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunToolOutput(TestThreadId, TestRunId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs b/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs
deleted file mode 100644
index 042eafee48aa..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Extensions/OpenAIRestExtensions.ThreadTests.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Moq;
-using Moq.Protected;
-using Xunit;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-[Trait("Category", "Unit Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class OpenAIRestExtensionsThreadTests
-{
- private const string BogusApiKey = "bogus";
- private const string TestThreadId = "threadId";
-
- private readonly OpenAIRestContext _restContext;
- private readonly Mock _mockHttpMessageHandler = new();
-
- public OpenAIRestExtensionsThreadTests()
- {
- this._mockHttpMessageHandler
- .Protected()
- .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
- .ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
- this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
- }
-
- [Fact]
- public async Task CreateThreadModelAsync()
- {
- await this._restContext.CreateThreadModelAsync().ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseThreadUrl);
- }
-
- [Fact]
- public async Task GetThreadModelAsync()
- {
- await this._restContext.GetThreadModelAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
- }
-
- [Fact]
- public async Task DeleteThreadModelAsync()
- {
- await this._restContext.DeleteThreadModelAsync(TestThreadId).ConfigureAwait(true);
-
- this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs
deleted file mode 100644
index 11e32b85effe..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/AssistantHarness.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating assistants.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class AssistantHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public AssistantHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantLifecycleAsync()
- {
- var assistant =
- await AssistantBuilder.NewAsync(
- apiKey: TestConfig.OpenAIApiKey,
- model: TestConfig.SupportedGpt35TurboModel,
- instructions: "say something funny",
- name: "Fred",
- description: "test assistant").ConfigureAwait(true);
-
- this.DumpAssistant(assistant);
-
- var copy =
- await AssistantBuilder.GetAssistantAsync(
- apiKey: TestConfig.OpenAIApiKey,
- assistantId: assistant.Id).ConfigureAwait(true);
-
- this.DumpAssistant(copy);
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantDefinitionAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/PoetAssistant.yaml")
- .BuildAsync()
- .ConfigureAwait(true);
-
- this.DumpAssistant(assistant);
-
- var copy =
- await AssistantBuilder.GetAssistantAsync(
- apiKey: TestConfig.OpenAIApiKey,
- assistantId: assistant.Id).ConfigureAwait(true);
-
- this.DumpAssistant(copy);
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantListAsync()
- {
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
- foreach (var assistant in assistants)
- {
- this.DumpAssistant(assistant);
- }
- }
-
- ///
- /// Verify creation and retrieval of assistant.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyAssistantDeleteAsync()
- {
- var names =
- new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Fred",
- "Barney",
- "DeleteMe",
- "Poet",
- "Math Tutor",
- };
-
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
- foreach (var assistant in assistants)
- {
- if (!string.IsNullOrWhiteSpace(assistant.Name) && names.Contains(assistant.Name))
- {
- this._output.WriteLine($"Removing: {assistant.Name} - {assistant.Id}");
- await context.DeleteAssistantModelAsync(assistant.Id).ConfigureAwait(true);
- }
- }
- }
-
- private void DumpAssistant(AssistantModel assistant)
- {
- this._output.WriteLine($"# {assistant.Id}");
- this._output.WriteLine($"# {assistant.Model}");
- this._output.WriteLine($"# {assistant.Instructions}");
- this._output.WriteLine($"# {assistant.Name}");
- this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
- }
-
- private void DumpAssistant(IAssistant assistant)
- {
- this._output.WriteLine($"# {assistant.Id}");
- this._output.WriteLine($"# {assistant.Model}");
- this._output.WriteLine($"# {assistant.Instructions}");
- this._output.WriteLine($"# {assistant.Name}");
- this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs
deleted file mode 100644
index b2d9c1403b8d..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/RunHarness.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Xunit;
-using Xunit.Abstractions;
-
-#pragma warning disable CA1812 // Uninstantiated internal types
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating runs.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class RunHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public RunHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyRunLifecycleAsync()
- {
- var assistant =
- await AssistantBuilder.NewAsync(
- apiKey: TestConfig.OpenAIApiKey,
- model: TestConfig.SupportedGpt35TurboModel,
- instructions: "say something funny",
- name: "Fred",
- description: "funny assistant").ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "I was on my way to the store this morning and...",
- "That was great! Tell me another.").ConfigureAwait(true);
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyRunFromDefinitionAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/PoetAssistant.yaml")
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "Eggs are yummy and beautiful geometric gems.",
- "It rains a lot in Seattle.").ConfigureAwait(true);
- }
-
- ///
- /// Verify creation of run.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyFunctionLifecycleAsync()
- {
- var gamePlugin = KernelPluginFactory.CreateFromType();
-
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .FromTemplatePath("Templates/GameAssistant.yaml")
- .WithPlugin(gamePlugin)
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- await this.ChatAsync(
- thread,
- assistant,
- "What is the question for the guessing game?",
- "Is it 'RED'?",
- "What is the answer?").ConfigureAwait(true);
- }
-
- private async Task ChatAsync(IChatThread thread, IAssistant assistant, params string[] messages)
- {
- foreach (var message in messages)
- {
- var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
- this.LogMessage(messageUser);
-
- var assistantMessages = await thread.InvokeAsync(assistant).ToArrayAsync().ConfigureAwait(true);
- this.LogMessages(assistantMessages);
- }
- }
-
- private void LogMessages(IEnumerable messages)
- {
- foreach (var message in messages)
- {
- this.LogMessage(message);
- }
- }
-
- private void LogMessage(IChatMessage message)
- {
- this._output.WriteLine($"# {message.Id}");
- this._output.WriteLine($"# {message.Content}");
- this._output.WriteLine($"# {message.Role}");
- this._output.WriteLine($"# {message.AssistantId}");
- }
-
- private sealed class GuessingGame
- {
- ///
- /// Get the question
- ///
- [KernelFunction, Description("Get the guessing game question")]
- public string GetQuestion() => "What color am I thinking of?";
-
- ///
- /// Get the answer
- ///
- [KernelFunction, Description("Get the answer to the guessing game question.")]
- public string GetAnswer() => "Blue";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs b/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs
deleted file mode 100644
index 0546bbd43a30..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Integration/ThreadHarness.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-#define DISABLEHOST // Comment line to enable
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Experimental.Assistants;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
-
-///
-/// Dev harness for manipulating threads.
-///
-///
-/// Comment out DISABLEHOST definition to enable tests.
-/// Not enabled by default.
-///
-[Trait("Category", "Integration Tests")]
-[Trait("Feature", "Assistant")]
-public sealed class ThreadHarness
-{
-#if DISABLEHOST
- private const string SkipReason = "Harness only for local/dev environment";
-#else
- private const string SkipReason = null;
-#endif
-
- private readonly ITestOutputHelper _output;
-
- ///
- /// Test constructor.
- ///
- public ThreadHarness(ITestOutputHelper output)
- {
- this._output = output;
- }
-
- ///
- /// Verify creation and retrieval of thread.
- ///
- [Fact(Skip = SkipReason)]
- public async Task VerifyThreadLifecycleAsync()
- {
- var assistant =
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
- .WithName("DeleteMe")
- .BuildAsync()
- .ConfigureAwait(true);
-
- var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
-
- Assert.NotNull(thread.Id);
-
- this._output.WriteLine($"# {thread.Id}");
-
- var message = await thread.AddUserMessageAsync("I'm so confused!").ConfigureAwait(true);
- Assert.NotNull(message);
-
- this._output.WriteLine($"# {message.Id}");
-
- var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
- var copy = await context.GetThreadModelAsync(thread.Id).ConfigureAwait(true);
-
- await context.DeleteThreadModelAsync(thread.Id).ConfigureAwait(true);
-
- await Assert.ThrowsAsync(() => context.GetThreadModelAsync(thread.Id)).ConfigureAwait(true);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs b/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs
deleted file mode 100644
index 5f2e147e7375..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/MockExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Net.Http;
-using System.Threading;
-using Moq;
-using Moq.Protected;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-internal static class MockExtensions
-{
- public static void VerifyMock(this Mock mockHandler, HttpMethod method, int times, string? uri = null)
- {
- mockHandler.Protected().Verify(
- "SendAsync",
- Times.Exactly(times),
- ItExpr.Is(req => req.Method == method && (uri == null || req.RequestUri == new Uri(uri))),
- ItExpr.IsAny());
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml b/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml
deleted file mode 100644
index dce10488e8a2..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Templates/GameAssistant.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: Fred
-instructions: |
- Run a guessing game where the user tries to guess the answer to a question but don't tell them the answer unless they give up by asking for the answer.
-
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml b/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml
deleted file mode 100644
index 7e356ddd61f7..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/Templates/PoetAssistant.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: Poet
-instructions: |
- Compose a sonnet inspired by the user input.
-description: You are a poet that composes poems based on user input.
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs b/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs
deleted file mode 100644
index 30263aa7737c..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/TestConfig.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Reflection;
-using Microsoft.Extensions.Configuration;
-using Xunit.Sdk;
-
-namespace SemanticKernel.Experimental.Assistants.UnitTests;
-
-internal static class TestConfig
-{
- public const string SupportedGpt35TurboModel = "gpt-3.5-turbo-1106";
-
- public static IConfiguration Configuration { get; } = CreateConfiguration();
-
- public static string OpenAIApiKey =>
- TestConfig.Configuration.GetValue("OpenAIApiKey") ??
- throw new TestClassException("Missing OpenAI APIKey.");
-
- private static IConfiguration CreateConfiguration()
- {
- return
- new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .AddJsonFile("testsettings.json")
- .AddJsonFile("testsettings.development.json", optional: true)
- .AddUserSecrets(Assembly.GetExecutingAssembly())
- .Build();
- }
-}
diff --git a/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json b/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json
deleted file mode 100644
index d456a389e0f9..000000000000
--- a/dotnet/src/Experimental/Assistants.UnitTests/testsettings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "OpenAIApiKey": ""
-}
diff --git a/dotnet/src/Experimental/Assistants/AssemblyInfo.cs b/dotnet/src/Experimental/Assistants/AssemblyInfo.cs
deleted file mode 100644
index 951ee2d58289..000000000000
--- a/dotnet/src/Experimental/Assistants/AssemblyInfo.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Diagnostics.CodeAnalysis;
-
-// This assembly is currently experimental.
-[assembly: Experimental("SKEXP0101")]
diff --git a/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs b/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs
deleted file mode 100644
index 8b52b43bb5c9..000000000000
--- a/dotnet/src/Experimental/Assistants/AssistantBuilder.Static.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Context for interacting with OpenAI REST API.
-///
-public partial class AssistantBuilder
-{
- ///
- /// Create a new assistant.
- ///
- /// The OpenAI API key
- /// The assistant chat model (required)
- /// The assistant instructions (required)
- /// The assistant name (optional)
- /// The assistant description(optional)
- /// The requested .
- public static async Task NewAsync(
- string apiKey,
- string model,
- string instructions,
- string? name = null,
- string? description = null)
- {
- return
- await new AssistantBuilder()
- .WithOpenAIChatCompletion(model, apiKey)
- .WithInstructions(instructions)
- .WithName(name)
- .WithDescription(description)
- .BuildAsync().ConfigureAwait(false);
- }
-
- ///
- /// Retrieve an existing assistant, by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant identifier
- /// Plugins to initialize as assistant tools
- /// A cancellation token
- /// An initialized instance.
- public static async Task GetAssistantAsync(
- string apiKey,
- string assistantId,
- IEnumerable? plugins = null,
- CancellationToken cancellationToken = default)
- {
- var restContext = new OpenAIRestContext(apiKey);
- var resultModel = await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false);
-
- return new Assistant(resultModel, restContext, plugins);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantBuilder.cs b/dotnet/src/Experimental/Assistants/AssistantBuilder.cs
deleted file mode 100644
index 1dc37b4ac126..000000000000
--- a/dotnet/src/Experimental/Assistants/AssistantBuilder.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-using YamlDotNet.Serialization;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Fluent builder for initializing an instance.
-///
-public partial class AssistantBuilder
-{
- private readonly AssistantModel _model;
- private readonly KernelPluginCollection _plugins;
-
- private string? _apiKey;
- private Func? _httpClientProvider;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public AssistantBuilder()
- {
- this._model = new AssistantModel();
- this._plugins = new KernelPluginCollection();
- }
-
- ///
- /// Create a instance.
- ///
- /// A cancellation token
- /// A new instance.
- public async Task BuildAsync(CancellationToken cancellationToken = default)
- {
- if (string.IsNullOrWhiteSpace(this._model.Model))
- {
- throw new AssistantException("Model must be defined for assistant.");
- }
-
- if (string.IsNullOrWhiteSpace(this._apiKey))
- {
- throw new AssistantException("ApiKey must be provided for assistant.");
- }
-
- return
- await Assistant.CreateAsync(
- new OpenAIRestContext(this._apiKey!, this._httpClientProvider),
- this._model,
- this._plugins,
- cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Define the OpenAI chat completion service (required).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithOpenAIChatCompletion(string model, string apiKey)
- {
- this._apiKey = apiKey;
- this._model.Model = model;
-
- return this;
- }
-
- ///
- /// Create a new assistant from a yaml formatted string.
- ///
- /// YAML assistant definition.
- /// instance for fluid expression.
- public AssistantBuilder FromTemplate(string template)
- {
- var deserializer = new DeserializerBuilder().Build();
-
- var assistantKernelModel = deserializer.Deserialize(template);
-
- return
- this
- .WithInstructions(assistantKernelModel.Instructions.Trim())
- .WithName(assistantKernelModel.Name.Trim())
- .WithDescription(assistantKernelModel.Description.Trim());
- }
-
- ///
- /// Create a new assistant from a yaml template.
- ///
- /// Path to a configuration file.
- /// instance for fluid expression.
- public AssistantBuilder FromTemplatePath(string templatePath)
- {
- var yamlContent = File.ReadAllText(templatePath);
-
- return this.FromTemplate(yamlContent);
- }
-
- ///
- /// Provide an httpclient (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithHttpClient(HttpClient httpClient)
- {
- this._httpClientProvider ??= () => httpClient;
-
- return this;
- }
-
- ///
- /// Define the assistant description (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithDescription(string? description)
- {
- this._model.Description = description;
-
- return this;
- }
-
- ///
- /// Define the assistant instructions (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithInstructions(string instructions)
- {
- this._model.Instructions = instructions;
-
- return this;
- }
-
- ///
- /// Define the assistant metadata (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithMetadata(string key, object value)
- {
- this._model.Metadata[key] = value;
-
- return this;
- }
-
- ///
- /// Define the assistant metadata (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithMetadata(IDictionary metadata)
- {
- foreach (var kvp in metadata)
- {
- this._model.Metadata[kvp.Key] = kvp.Value;
- }
-
- return this;
- }
-
- ///
- /// Define the assistant name (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithName(string? name)
- {
- this._model.Name = name;
-
- return this;
- }
-
- ///
- /// Define functions associated with assistant instance (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithPlugin(KernelPlugin? plugin)
- {
- if (plugin != null)
- {
- this._plugins.Add(plugin);
- }
-
- return this;
- }
-
- ///
- /// Define functions associated with assistant instance (optional).
- ///
- /// instance for fluid expression.
- public AssistantBuilder WithPlugins(IEnumerable plugins)
- {
- this._plugins.AddRange(plugins);
-
- return this;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantPlugin.cs b/dotnet/src/Experimental/Assistants/AssistantPlugin.cs
deleted file mode 100644
index 26d9cfae88f7..000000000000
--- a/dotnet/src/Experimental/Assistants/AssistantPlugin.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Specialization of for
-///
-public abstract class AssistantPlugin : KernelPlugin
-{
- ///
- protected AssistantPlugin(string name, string? description = null)
- : base(name, description)
- {
- // No specialization...
- }
-
- internal abstract Assistant Assistant { get; }
-
- ///
- /// Invoke plugin with user input
- ///
- /// The user input
- /// A cancel token
- /// The assistant response
- public async Task InvokeAsync(string input, CancellationToken cancellationToken = default)
- {
- var args = new KernelArguments { { "input", input } };
- var result = await this.First().InvokeAsync(this.Assistant.Kernel, args, cancellationToken).ConfigureAwait(false);
- var response = result.GetValue()!;
-
- return response.Message;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/AssistantResponse.cs b/dotnet/src/Experimental/Assistants/AssistantResponse.cs
deleted file mode 100644
index c92ce3662fb2..000000000000
--- a/dotnet/src/Experimental/Assistants/AssistantResponse.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text.Json.Serialization;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Response from assistant when called as a .
-///
-public class AssistantResponse
-{
- ///
- /// The thread-id for the assistant conversation.
- ///
- [JsonPropertyName("thread_id")]
- public string ThreadId { get; set; } = string.Empty;
-
- ///
- /// The assistant response.
- ///
- [JsonPropertyName("response")]
- public string Message { get; set; } = string.Empty;
-
- ///
- /// Instructions from assistant on next steps.
- ///
- [JsonPropertyName("system_instructions")]
- public string Instructions { get; set; } = string.Empty;
-}
diff --git a/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs b/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs
deleted file mode 100644
index 2f3057b11543..000000000000
--- a/dotnet/src/Experimental/Assistants/Exceptions/AssistantException.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-
-///
-/// Assistant specific .
-///
-public class AssistantException : KernelException
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public AssistantException()
- {
- }
-
- ///
- /// Initializes a new instance of the class with a specified error message.
- ///
- /// The error message that explains the reason for the exception.
- public AssistantException(string? message) : base(message)
- {
- }
-
- ///
- /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
- ///
- /// The error message that explains the reason for the exception.
- /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
- public AssistantException(string? message, Exception? innerException) : base(message, innerException)
- {
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj b/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj
deleted file mode 100644
index 2edcf111ad0f..000000000000
--- a/dotnet/src/Experimental/Assistants/Experimental.Assistants.csproj
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Microsoft.SemanticKernel.Experimental.Assistants
- Microsoft.SemanticKernel.Experimental.Assistants
- netstandard2.0
- alpha
- Latest
-
-
-
-
-
- Semantic Kernel Assistants
- Semantic Kernel Assistants
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs
deleted file mode 100644
index d508b01f9517..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
-
-internal static class AssistantsKernelExtensions
-{
- ///
- /// Retrieve a kernel function based on the tool name.
- ///
- public static KernelFunction GetAssistantTool(this Kernel kernel, string toolName)
- {
- string[] nameParts = toolName.Split('-');
- return nameParts.Length switch
- {
- 2 => kernel.Plugins.GetFunction(nameParts[0], nameParts[1]),
- _ => throw new AssistantException($"Unknown tool: {toolName}"),
- };
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs
deleted file mode 100644
index cc56d692896a..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/AssistantsKernelFunctionExtensions.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Json.More;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-internal static class AssistantsKernelFunctionExtensions
-{
- ///
- /// Produce a fully qualified toolname.
- ///
- public static string GetQualifiedName(this KernelFunction function, string pluginName)
- {
- return $"{pluginName}-{function.Name}";
- }
-
- ///
- /// Convert to an OpenAI tool model.
- ///
- /// The source function
- /// The plugin name
- /// An OpenAI tool model
- public static ToolModel ToToolModel(this KernelFunction function, string pluginName)
- {
- var metadata = function.Metadata;
- var required = new List(metadata.Parameters.Count);
- var properties =
- metadata.Parameters.ToDictionary(
- p => p.Name,
- p =>
- {
- if (p.IsRequired)
- {
- required.Add(p.Name);
- }
-
- return
- new OpenAIParameter
- {
- Type = ConvertType(p.ParameterType),
- Description = p.Description,
- };
- });
-
- var payload =
- new ToolModel
- {
- Type = "function",
- Function =
- new()
- {
- Name = function.GetQualifiedName(pluginName),
- Description = function.Description,
- Parameters =
- new OpenAIParameters
- {
- Properties = properties,
- Required = required,
- },
- },
- };
-
- return payload;
- }
-
- private static string ConvertType(Type? type)
- {
- if (type == null || type == typeof(string))
- {
- return "string";
- }
-
- if (type.IsNumber())
- {
- return "number";
- }
-
- if (type.IsEnum)
- {
- return "enum";
- }
-
- return type.Name;
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs
deleted file mode 100644
index 361f1249f0c6..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Assistant.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Web;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for assistants.
-///
-internal static partial class OpenAIRestExtensions
-{
- internal const string BaseAssistantUrl = $"{BaseUrl}/assistants";
-
- ///
- /// Create a new assistant.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant definition
- /// A cancellation token
- /// An assistant definition
- public static Task CreateAssistantModelAsync(
- this OpenAIRestContext context,
- AssistantModel model,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- model = model.Model,
- name = model.Name,
- description = model.Description,
- instructions = model.Instructions,
- tools = model.Tools,
- file_ids = model.FileIds,
- metadata = model.Metadata,
- };
-
- return
- context.ExecutePostAsync(
- BaseAssistantUrl,
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an assistant by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant identifier
- /// A cancellation token
- /// An assistant definition
- public static Task GetAssistantModelAsync(
- this OpenAIRestContext context,
- string assistantId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetAssistantUrl(assistantId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all assistants.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A limit on the number of objects to be returned.
- /// Limit can range between 1 and 100, and the default is 20.
- /// Set to true to sort by ascending created_at timestamp
- /// instead of descending.
- /// A cursor for use in pagination. This 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. This 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.
- /// List of retrieved Assistants
- /// A cancellation token
- /// An enumeration of assistant definitions
- public static async Task> ListAssistantModelsAsync(
- this OpenAIRestContext context,
- int limit = 20,
- bool ascending = false,
- string? after = null,
- string? before = null,
- CancellationToken cancellationToken = default)
- {
- var query = HttpUtility.ParseQueryString(string.Empty);
- query["limit"] = limit.ToString(CultureInfo.InvariantCulture);
- query["order"] = ascending ? "asc" : "desc";
- if (!string.IsNullOrWhiteSpace(after))
- {
- query["after"] = after;
- }
- if (!string.IsNullOrWhiteSpace(before))
- {
- query["before"] = before;
- }
-
- string requestUrl = string.Join("?", BaseAssistantUrl, query.ToString());
-
- var result =
- await context.ExecuteGetAsync(
- requestUrl,
- cancellationToken).ConfigureAwait(false);
-
- return result.Data;
- }
-
- ///
- /// Delete an existing assistant
- ///
- /// A context for accessing OpenAI REST endpoint
- /// Identifier of assistant to delete
- /// A cancellation token
- public static Task DeleteAssistantModelAsync(
- this OpenAIRestContext context,
- string id,
- CancellationToken cancellationToken = default)
- {
- return context.ExecuteDeleteAsync(GetAssistantUrl(id), cancellationToken);
- }
-
- internal static string GetAssistantUrl(string assistantId)
- {
- return $"{BaseAssistantUrl}/{assistantId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs
deleted file mode 100644
index 61d426907ba8..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Messages.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for thread messages.
-///
-internal static partial class OpenAIRestExtensions
-{
- ///
- /// Create a new message.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The message text
- /// A cancellation token
- /// A message definition
- public static Task CreateUserTextMessageAsync(
- this OpenAIRestContext context,
- string threadId,
- string content,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- role = AuthorRole.User.Label,
- content,
- };
-
- return
- context.ExecutePostAsync(
- GetMessagesUrl(threadId),
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an message by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The message identifier
- /// A cancellation token
- /// A message definition
- public static Task GetMessageAsync(
- this OpenAIRestContext context,
- string threadId,
- string messageId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId, messageId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all thread messages.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// A cancellation token
- /// A message list definition
- public static Task GetMessagesAsync(
- this OpenAIRestContext context,
- string threadId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId),
- cancellationToken);
- }
-
- ///
- /// Retrieve all thread messages.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// The set of message identifiers to retrieve
- /// A cancellation token
- /// A message list definition
- public static async Task> GetMessagesAsync(
- this OpenAIRestContext context,
- string threadId,
- IEnumerable messageIds,
- CancellationToken cancellationToken = default)
- {
- var tasks =
- messageIds.Select(
- id =>
- context.ExecuteGetAsync(
- GetMessagesUrl(threadId, id),
- cancellationToken)).ToArray();
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- return tasks.Select(t => t.Result).ToArray();
- }
-
- internal static string GetMessagesUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}/messages";
- }
-
- internal static string GetMessagesUrl(string threadId, string messageId)
- {
- return $"{BaseThreadUrl}/{threadId}/messages/{messageId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs
deleted file mode 100644
index 989ca93a8ae0..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Run.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for thread runs.
-///
-internal static partial class OpenAIRestExtensions
-{
- ///
- /// Create a new run.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// The assistant identifier
- /// Optional instruction override
- /// The assistant tools
- /// A cancellation token
- /// A run definition
- public static Task CreateRunAsync(
- this OpenAIRestContext context,
- string threadId,
- string assistantId,
- string? instructions = null,
- IEnumerable? tools = null,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- assistant_id = assistantId,
- instructions,
- tools,
- };
-
- return
- context.ExecutePostAsync(
- GetRunUrl(threadId),
- payload,
- cancellationToken);
- }
-
- ///
- /// Retrieve an run by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// A run identifier
- /// A cancellation token
- /// A run definition
- public static Task GetRunAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetRunUrl(threadId, runId),
- cancellationToken);
- }
-
- ///
- /// Retrieve run steps by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// A run identifier
- /// A cancellation token
- /// A set of run steps
- public static Task GetRunStepsAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetRunStepsUrl(threadId, runId),
- cancellationToken);
- }
-
- ///
- /// Add a function result for a run.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A thread identifier
- /// The run identifier
- /// The function/tool results.
- /// A cancellation token
- /// A run definition
- public static Task AddToolOutputsAsync(
- this OpenAIRestContext context,
- string threadId,
- string runId,
- IEnumerable results,
- CancellationToken cancellationToken = default)
- {
- var payload =
- new
- {
- tool_outputs = results
- };
-
- return
- context.ExecutePostAsync(
- GetRunToolOutput(threadId, runId),
- payload,
- cancellationToken);
- }
-
- internal static string GetRunUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs";
- }
-
- internal static string GetRunUrl(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}";
- }
-
- internal static string GetRunStepsUrl(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}/steps";
- }
-
- internal static string GetRunToolOutput(string threadId, string runId)
- {
- return $"{BaseThreadUrl}/{threadId}/runs/{runId}/submit_tool_outputs";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs
deleted file mode 100644
index 711b0e25e881..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.Thread.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Supported OpenAI REST API actions for threads.
-///
-internal static partial class OpenAIRestExtensions
-{
- internal const string BaseThreadUrl = $"{BaseUrl}/threads";
-
- ///
- /// Create a new thread.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// A cancellation token
- /// A thread definition
- public static Task CreateThreadModelAsync(
- this OpenAIRestContext context,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecutePostAsync(
- BaseThreadUrl,
- cancellationToken);
- }
-
- ///
- /// Retrieve an thread by identifier.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The thread identifier
- /// A cancellation token
- /// A thread definition
- public static Task GetThreadModelAsync(
- this OpenAIRestContext context,
- string threadId,
- CancellationToken cancellationToken = default)
- {
- return
- context.ExecuteGetAsync(
- GetThreadUrl(threadId),
- cancellationToken);
- }
-
- ///
- /// Delete an existing thread.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// Identifier of thread to delete
- /// A cancellation token
- public static Task DeleteThreadModelAsync(
- this OpenAIRestContext context,
- string id,
- CancellationToken cancellationToken = default)
- {
- return context.ExecuteDeleteAsync(GetThreadUrl(id), cancellationToken);
- }
-
- internal static string GetThreadUrl(string threadId)
- {
- return $"{BaseThreadUrl}/{threadId}";
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs
deleted file mode 100644
index e98198311a6b..000000000000
--- a/dotnet/src/Experimental/Assistants/Extensions/OpenAIRestExtensions.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-internal static partial class OpenAIRestExtensions
-{
- private const string BaseUrl = "https://api.openai.com/v1";
- private const string HeaderNameOpenAIAssistant = "OpenAI-Beta";
- private const string HeaderNameAuthorization = "Authorization";
- private const string HeaderOpenAIValueAssistant = "assistants=v1";
-
- private static async Task ExecuteGetAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreateGetRequest(url);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
-
- string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-
- // Common case is for failure exception to be raised by REST invocation.
- // Null result is a logical possibility, but unlikely edge case.
- // Might occur due to model alignment issues over time.
- return
- JsonSerializer.Deserialize(responseBody) ??
- throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
- }
-
- private static Task ExecutePostAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- return context.ExecutePostAsync(url, payload: null, cancellationToken);
- }
-
- private static async Task ExecutePostAsync(
- this OpenAIRestContext context,
- string url,
- object? payload,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreatePostRequest(url, payload);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
-
- string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- return
- JsonSerializer.Deserialize(responseBody) ??
- throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
- }
-
- private static async Task ExecuteDeleteAsync(
- this OpenAIRestContext context,
- string url,
- CancellationToken cancellationToken = default)
- {
- using var request = HttpRequest.CreateDeleteRequest(url);
-
- request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
- request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
-
- using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/IAssistant.cs b/dotnet/src/Experimental/Assistants/IAssistant.cs
deleted file mode 100644
index 5530e33ec887..000000000000
--- a/dotnet/src/Experimental/Assistants/IAssistant.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents an assistant that can call the model and use tools.
-///
-public interface IAssistant
-{
- ///
- /// The assistant identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// Always "assistant"
- ///
-#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning disable CA1716 // Identifiers should not match keywords
- string Object { get; }
-#pragma warning restore CA1716 // Identifiers should not match keywords
-#pragma warning restore CA1720 // Identifier contains type name
-
- ///
- /// Unix timestamp (in seconds) for when the assistant was created
- ///
- long CreatedAt { get; }
-
- ///
- /// Name of the assistant
- ///
- string? Name { get; }
-
- ///
- /// The description of the assistant
- ///
- string? Description { get; }
-
- ///
- /// ID of the model to use
- ///
- string Model { get; }
-
- ///
- /// The system instructions that the assistant uses
- ///
- string Instructions { get; }
-
- ///
- /// A semantic-kernel instance associated with the assistant.
- ///
- internal Kernel Kernel { get; }
-
- ///
- /// Tools defined for run execution.
- ///
- public KernelPluginCollection Plugins { get; }
-
- ///
- /// Expose the assistant as a plugin.
- ///
- public AssistantPlugin AsPlugin();
-
- ///
- /// Creates a new assistant chat thread.
- ///
- /// A cancellation token
- Task NewThreadAsync(CancellationToken cancellationToken = default);
-
- ///
- /// Gets an existing assistant chat thread.
- ///
- /// The id of the existing chat thread.
- /// A cancellation token
- Task GetThreadAsync(string id, CancellationToken cancellationToken = default);
-
- ///
- /// Deletes an existing assistant chat thread.
- ///
- /// The id of the existing chat thread. Allows for null-fallthrough to simplify caller patterns.
- /// A cancellation token
- Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default);
-
- ///
- /// Delete current assistant. Terminal state - Unable to perform any
- /// subsequent actions.
- ///
- /// A cancellation token
- Task DeleteAsync(CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs b/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs
deleted file mode 100644
index 26ebecc32df2..000000000000
--- a/dotnet/src/Experimental/Assistants/IAssistantExtensions.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using System.Threading;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Convenience actions for .
-///
-public static class IAssistantExtensions
-{
- ///
- /// Invoke assistant with user input
- ///
- /// the assistant
- /// the user input
- /// a cancel token
- /// chat messages
- public static async IAsyncEnumerable InvokeAsync(
- this IAssistant assistant,
- string input,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- IChatThread thread = await assistant.NewThreadAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- await foreach (var message in thread.InvokeAsync(assistant, input, cancellationToken))
- {
- yield return message;
- }
- }
- finally
- {
- await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/IChatMessage.cs b/dotnet/src/Experimental/Assistants/IChatMessage.cs
deleted file mode 100644
index 05e0e51c60b0..000000000000
--- a/dotnet/src/Experimental/Assistants/IChatMessage.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.ObjectModel;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents a message that is part of an assistant thread.
-///
-public interface IChatMessage
-{
- ///
- /// The message identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// The id of the assistant associated with the a message where role = "assistant", otherwise null.
- ///
- string? AssistantId { get; }
-
- ///
- /// The chat message content.
- ///
- string Content { get; }
-
- ///
- /// The role associated with the chat message.
- ///
- string Role { get; }
-
- ///
- /// Properties associated with the message.
- ///
- ReadOnlyDictionary Properties { get; }
-}
diff --git a/dotnet/src/Experimental/Assistants/IChatThread.cs b/dotnet/src/Experimental/Assistants/IChatThread.cs
deleted file mode 100644
index cd29324db872..000000000000
--- a/dotnet/src/Experimental/Assistants/IChatThread.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants;
-
-///
-/// Represents a thread that contains messages.
-///
-public interface IChatThread
-{
- ///
- /// The thread identifier (which can be referenced in API endpoints).
- ///
- string Id { get; }
-
- ///
- /// Add a textual user message to the thread.
- ///
- /// The user message
- /// A cancellation token
- ///
- Task AddUserMessageAsync(string message, CancellationToken cancellationToken = default);
-
- ///
- /// Advance the thread with the specified assistant.
- ///
- /// An assistant instance.
- /// A cancellation token
- /// The resulting assistant message(s)
- IAsyncEnumerable InvokeAsync(IAssistant assistant, CancellationToken cancellationToken = default);
-
- ///
- /// Advance the thread with the specified assistant.
- ///
- /// An assistant instance.
- /// The user message
- /// A cancellation token
- /// The resulting assistant message(s)
- IAsyncEnumerable InvokeAsync(IAssistant assistant, string userMessage, CancellationToken cancellationToken = default);
-
- ///
- /// Delete current thread. Terminal state - Unable to perform any
- /// subsequent actions.
- ///
- /// A cancellation token
- Task DeleteAsync(CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Experimental/Assistants/Internal/Assistant.cs b/dotnet/src/Experimental/Assistants/Internal/Assistant.cs
deleted file mode 100644
index 5d8b1585e39d..000000000000
--- a/dotnet/src/Experimental/Assistants/Internal/Assistant.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-///
-/// Represents an assistant that can call the model and use tools.
-///
-internal sealed class Assistant : IAssistant
-{
- ///
- public string Id => this._model.Id;
-
- ///
- public Kernel Kernel { get; }
-
- ///
- public KernelPluginCollection Plugins => this.Kernel.Plugins;
-
- ///
-#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning disable CA1716 // Identifiers should not match keywords
- public string Object => this._model.Object;
-#pragma warning restore CA1720 // Identifier contains type name - We don't control the schema
-#pragma warning restore CA1716 // Identifiers should not match keywords
-
- ///
- public long CreatedAt => this._model.CreatedAt;
-
- ///
- public string? Name => this._model.Name;
-
- ///
- public string? Description => this._model.Description;
-
- ///
- public string Model => this._model.Model;
-
- ///
- public string Instructions => this._model.Instructions;
-
- private static readonly Regex s_removeInvalidCharsRegex = new("[^0-9A-Za-z-]");
-
- private readonly OpenAIRestContext _restContext;
- private readonly AssistantModel _model;
-
- private AssistantPlugin? _assistantPlugin;
- private bool _isDeleted;
-
- ///
- /// Create a new assistant.
- ///
- /// A context for accessing OpenAI REST endpoint
- /// The assistant definition
- /// Plugins to initialize as assistant tools
- /// A cancellation token
- /// An initialized instance.
- public static async Task CreateAsync(
- OpenAIRestContext restContext,
- AssistantModel assistantModel,
- IEnumerable? plugins = null,
- CancellationToken cancellationToken = default)
- {
- var resultModel = await restContext.CreateAssistantModelAsync(assistantModel, cancellationToken).ConfigureAwait(false);
-
- return new Assistant(resultModel, restContext, plugins);
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal Assistant(
- AssistantModel model,
- OpenAIRestContext restContext,
- IEnumerable? plugins = null)
- {
- this._model = model;
- this._restContext = restContext;
-
- IKernelBuilder builder = Kernel.CreateBuilder();
- ;
- this.Kernel =
- Kernel
- .CreateBuilder()
- .AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey)
- .Build();
-
- if (plugins is not null)
- {
- this.Kernel.Plugins.AddRange(plugins);
- }
- }
-
- public AssistantPlugin AsPlugin() => this._assistantPlugin ??= this.DefinePlugin();
-
- ///
- public Task NewThreadAsync(CancellationToken cancellationToken = default)
- {
- this.ThrowIfDeleted();
-
- return ChatThread.CreateAsync(this._restContext, cancellationToken);
- }
-
- ///
- public Task GetThreadAsync(string id, CancellationToken cancellationToken = default)
- {
- this.ThrowIfDeleted();
-
- return ChatThread.GetAsync(this._restContext, id, cancellationToken);
- }
-
- ///
- public async Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default)
- {
- if (string.IsNullOrWhiteSpace(id))
- {
- return;
- }
-
- await this._restContext.DeleteThreadModelAsync(id!, cancellationToken).ConfigureAwait(false);
- }
-
- ///
- public async Task DeleteAsync(CancellationToken cancellationToken = default)
- {
- if (this._isDeleted)
- {
- return;
- }
-
- await this._restContext.DeleteAssistantModelAsync(this.Id, cancellationToken).ConfigureAwait(false);
- this._isDeleted = true;
- }
-
- ///
- /// Marshal thread run through interface.
- ///
- /// The user input
- /// A cancellation token.
- /// An assistant response (
- private async Task AskAsync(
- [Description("The user message provided to the assistant.")]
- string input,
- CancellationToken cancellationToken = default)
- {
- var thread = await this.NewThreadAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- await thread.AddUserMessageAsync(input, cancellationToken).ConfigureAwait(false);
-
- var messages = await thread.InvokeAsync(this, cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false);
- var response =
- new AssistantResponse
- {
- ThreadId = thread.Id,
- Message = string.Concat(messages.Select(m => m.Content)),
- };
-
- return response;
- }
- finally
- {
- await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
- }
- }
-
- private AssistantPluginImpl DefinePlugin()
- {
- var functionAsk = KernelFunctionFactory.CreateFromMethod(this.AskAsync, description: this.Description);
-
- return new AssistantPluginImpl(this, functionAsk);
- }
-
- private void ThrowIfDeleted()
- {
- if (this._isDeleted)
- {
- throw new AssistantException($"{nameof(Assistant)}: {this.Id} has been deleted.");
- }
- }
-
- private sealed class AssistantPluginImpl : AssistantPlugin
- {
- public KernelFunction FunctionAsk { get; }
-
- internal override Assistant Assistant { get; }
-
- public override int FunctionCount => 1;
-
- private static readonly string s_functionName = nameof(Assistant.AskAsync).Substring(0, nameof(Assistant.AskAsync).Length - 5);
-
- public AssistantPluginImpl(Assistant assistant, KernelFunction functionAsk)
- : base(s_removeInvalidCharsRegex.Replace(assistant.Name ?? assistant.Id, string.Empty),
- assistant.Description ?? assistant.Instructions)
- {
- this.Assistant = assistant;
- this.FunctionAsk = functionAsk;
- }
-
- public override IEnumerator GetEnumerator()
- {
- yield return this.FunctionAsk;
- }
-
- public override bool TryGetFunction(string name, [NotNullWhen(true)] out KernelFunction? function)
- {
- function = null;
-
- if (s_functionName.Equals(name, StringComparison.OrdinalIgnoreCase))
- {
- function = this.FunctionAsk;
- }
-
- return function != null;
- }
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs b/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs
deleted file mode 100644
index 8842f4a916a4..000000000000
--- a/dotnet/src/Experimental/Assistants/Internal/ChatMessage.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-///
-/// Represents a message that is part of an assistant thread.
-///
-internal sealed class ChatMessage : IChatMessage
-{
- ///
- public string Id { get; }
-
- ///
- public string? AssistantId { get; }
-
- ///
- public string Content { get; }
-
- ///
- public string Role { get; }
-
- ///
- public ReadOnlyDictionary Properties { get; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal ChatMessage(ThreadMessageModel model)
- {
- var content = (IEnumerable)model.Content;
- var text = content.First().Text?.Value ?? string.Empty;
-
- this.Id = model.Id;
- this.AssistantId = string.IsNullOrWhiteSpace(model.AssistantId) ? null : model.AssistantId;
- this.Role = model.Role;
- this.Content = text;
- this.Properties = new ReadOnlyDictionary(model.Metadata);
- }
-}
diff --git a/dotnet/src/Experimental/Assistants/Internal/ChatRun.cs b/dotnet/src/Experimental/Assistants/Internal/ChatRun.cs
deleted file mode 100644
index 1c7e9db0609c..000000000000
--- a/dotnet/src/Experimental/Assistants/Internal/ChatRun.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
-using Microsoft.SemanticKernel.Experimental.Assistants.Models;
-
-namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
-
-///
-/// Represents an execution run on a thread.
-///
-internal sealed class ChatRun
-{
- ///
- public string Id => this._model.Id;
-
- ///
- public string AssistantId => this._model.AssistantId;
-
- ///
- public string ThreadId => this._model.ThreadId;
-
- private const string ActionState = "requires_action";
- private const string FailedState = "failed";
- private const string CompletedState = "completed";
- private static readonly TimeSpan s_pollingInterval = TimeSpan.FromMilliseconds(500);
- private static readonly TimeSpan s_pollingBackoff = TimeSpan.FromSeconds(1);
-
- private static readonly HashSet s_pollingStates =
- new(StringComparer.OrdinalIgnoreCase)
- {
- "queued",
- "in_progress",
- };
-
- private readonly OpenAIRestContext _restContext;
- private readonly Kernel _kernel;
-
- private ThreadRunModel _model;
-
- ///
- public async Task> GetResultAsync(CancellationToken cancellationToken = default)
- {
- // Poll until actionable
- await PollRunStatus().ConfigureAwait(false);
-
- // Retrieve steps
- var steps = await this._restContext.GetRunStepsAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
-
- do
- {
- // Is tool action required?
- if (ActionState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase))
- {
- // Execute functions in parallel and post results at once.
- var tasks = steps.Data.SelectMany(step => this.ExecuteStep(step, cancellationToken)).ToArray();
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- var results = tasks.Select(t => t.Result).ToArray();
- await this._restContext.AddToolOutputsAsync(this.ThreadId, this.Id, results, cancellationToken).ConfigureAwait(false);
-
- // Refresh run as it goes back into pending state after posting function results.
- await PollRunStatus(force: true).ConfigureAwait(false);
-
- // Refresh steps to retrieve additional messages.
- steps = await this._restContext.GetRunStepsAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
- }
-
- // Did fail?
- if (FailedState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase))
- {
- throw new AssistantException($"Unexpected failure processing run: {this.Id}: {this._model.LastError?.Message ?? "Unknown"}");
- }
- }
- while (!CompletedState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase));
-
- var messageIds =
- steps.Data
- .Where(s => s.StepDetails.MessageCreation != null)
- .Select(s => s.StepDetails.MessageCreation!.MessageId)
- .ToArray();
-
- return messageIds;
-
- async Task PollRunStatus(bool force = false)
- {
- int count = 0;
-
- // Ignore model status when forced.
- while (force || s_pollingStates.Contains(this._model.Status))
- {
- if (!force)
- {
- // Reduce polling frequency after a couple attempts
- await Task.Delay(count >= 2 ? s_pollingInterval : s_pollingBackoff, cancellationToken).ConfigureAwait(false);
- ++count;
- }
-
- force = false;
-
- try
- {
- this._model = await this._restContext.GetRunAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception exception) when (!exception.IsCriticalException())
- {
- // Retry anyway..
- }
- }
- }
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal ChatRun(
- ThreadRunModel model,
- Kernel kernel,
- OpenAIRestContext restContext)
- {
- this._model = model;
- this._kernel = kernel;
- this._restContext = restContext;
- }
-
- private IEnumerable> ExecuteStep(ThreadRunStepModel step, CancellationToken cancellationToken)
- {
- // Process all of the steps that require action
- if (step.Status == "in_progress" && step.StepDetails.Type == "tool_calls")
- {
- foreach (var toolCall in step.StepDetails.ToolCalls)
- {
- // Run function
- yield return this.ProcessFunctionStepAsync(toolCall.Id, toolCall.Function, cancellationToken);
- }
- }
- }
-
- private async Task ProcessFunctionStepAsync(string callId, ThreadRunStepModel.FunctionDetailsModel functionDetails, CancellationToken cancellationToken)
- {
- var result = await InvokeFunctionCallAsync().ConfigureAwait(false);
- var toolResult = result as string;
- if (toolResult == null)
- {
- toolResult = JsonSerializer.Serialize(result);
- }
-
- return
- new ToolResultModel
- {
- CallId = callId,
- Output = toolResult!,
- };
-
- async Task