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 InvokeFunctionCallAsync() - { - var function = this._kernel.GetAssistantTool(functionDetails.Name); - - var functionArguments = new KernelArguments(); - if (!string.IsNullOrWhiteSpace(functionDetails.Arguments)) - { - var arguments = JsonSerializer.Deserialize>(functionDetails.Arguments)!; - foreach (var argument in arguments) - { - functionArguments[argument.Key] = argument.Value.ToString(); - } - } - - var result = await function.InvokeAsync(this._kernel, functionArguments, cancellationToken).ConfigureAwait(false); - if (result.ValueType == typeof(AssistantResponse)) - { - return result.GetValue()!; - } - - return result.GetValue() ?? string.Empty; - } - } -} diff --git a/dotnet/src/Experimental/Assistants/Internal/ChatThread.cs b/dotnet/src/Experimental/Assistants/Internal/ChatThread.cs deleted file mode 100644 index 5a32741e561d..000000000000 --- a/dotnet/src/Experimental/Assistants/Internal/ChatThread.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -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 a thread that contains messages. -/// -internal sealed class ChatThread : IChatThread -{ - /// - public string Id { get; private set; } - - private readonly OpenAIRestContext _restContext; - private bool _isDeleted; - - /// - /// Create a new thread. - /// - /// A context for accessing OpenAI REST endpoint - /// A cancellation token - /// An initialized instance. - public static async Task CreateAsync(OpenAIRestContext restContext, CancellationToken cancellationToken = default) - { - // Common case is for failure exception to be raised by REST invocation. Null result is a logical possibility, but unlikely edge case. - var threadModel = await restContext.CreateThreadModelAsync(cancellationToken).ConfigureAwait(false); - - return new ChatThread(threadModel, messageListModel: null, restContext); - } - - /// - /// Retrieve an existing thread. - /// - /// A context for accessing OpenAI REST endpoint - /// The thread identifier - /// A cancellation token - /// An initialized instance. - public static async Task GetAsync(OpenAIRestContext restContext, string threadId, CancellationToken cancellationToken = default) - { - var threadModel = await restContext.GetThreadModelAsync(threadId, cancellationToken).ConfigureAwait(false); - var messageListModel = await restContext.GetMessagesAsync(threadId, cancellationToken).ConfigureAwait(false); - - return new ChatThread(threadModel, messageListModel, restContext); - } - - /// - public async Task AddUserMessageAsync(string message, CancellationToken cancellationToken = default) - { - this.ThrowIfDeleted(); - - var messageModel = - await this._restContext.CreateUserTextMessageAsync( - this.Id, - message, - cancellationToken).ConfigureAwait(false); - - return new ChatMessage(messageModel); - } - - /// - public IAsyncEnumerable InvokeAsync(IAssistant assistant, CancellationToken cancellationToken) - { - return this.InvokeAsync(assistant, string.Empty, cancellationToken); - } - - /// - public async IAsyncEnumerable InvokeAsync(IAssistant assistant, string userMessage, [EnumeratorCancellation] CancellationToken cancellationToken) - { - this.ThrowIfDeleted(); - - if (!string.IsNullOrWhiteSpace(userMessage)) - { - yield return await this.AddUserMessageAsync(userMessage, cancellationToken).ConfigureAwait(false); - } - - var tools = assistant.Plugins.SelectMany(p => p.Select(f => f.ToToolModel(p.Name))); - var runModel = await this._restContext.CreateRunAsync(this.Id, assistant.Id, assistant.Instructions, tools, cancellationToken).ConfigureAwait(false); - - var run = new ChatRun(runModel, assistant.Kernel, this._restContext); - var results = await run.GetResultAsync(cancellationToken).ConfigureAwait(false); - - var messages = await this._restContext.GetMessagesAsync(this.Id, results, cancellationToken).ConfigureAwait(false); - foreach (var message in messages) - { - yield return new ChatMessage(message); - } - } - - /// - /// Delete an existing thread. - /// - /// A cancellation token - public async Task DeleteAsync(CancellationToken cancellationToken) - { - if (this._isDeleted) - { - return; - } - - await this._restContext.DeleteThreadModelAsync(this.Id, cancellationToken).ConfigureAwait(false); - this._isDeleted = true; - } - - /// - /// Initializes a new instance of the class. - /// - private ChatThread( - ThreadModel threadModel, - ThreadMessageListModel? messageListModel, - OpenAIRestContext restContext) - { - this.Id = threadModel.Id; - this._restContext = restContext; - } - - private void ThrowIfDeleted() - { - if (this._isDeleted) - { - throw new AssistantException($"{nameof(ChatThread)}: {this.Id} has been deleted."); - } - } -} diff --git a/dotnet/src/Experimental/Assistants/Internal/OpenAIRestContext.cs b/dotnet/src/Experimental/Assistants/Internal/OpenAIRestContext.cs deleted file mode 100644 index 4474eab49c39..000000000000 --- a/dotnet/src/Experimental/Assistants/Internal/OpenAIRestContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal; - -/// -/// Placeholder context. -/// -internal sealed class OpenAIRestContext -{ - private static readonly HttpClient s_defaultOpenAIClient = new(); - /// - public string ApiKey { get; } - - /// - public HttpClient GetHttpClient() => this._clientFactory.Invoke(); - - private readonly Func _clientFactory; - - /// - /// Initializes a new instance of the class. - /// - public OpenAIRestContext(string apiKey, Func? clientFactory = null) - { - this._clientFactory = clientFactory ??= () => s_defaultOpenAIClient; - - this.ApiKey = apiKey; - } -} diff --git a/dotnet/src/Experimental/Assistants/Models/AssistantConfigurationModel.cs b/dotnet/src/Experimental/Assistants/Models/AssistantConfigurationModel.cs deleted file mode 100644 index 0eaa23d4260a..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/AssistantConfigurationModel.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using YamlDotNet.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// Represents a yaml configuration file for an assistant. -/// -internal sealed class AssistantConfigurationModel -{ - /// - /// The assistant name - /// - [YamlMember(Alias = "name")] - public string Name { get; set; } = string.Empty; - - /// - /// The assistant description - /// - [YamlMember(Alias = "description")] - public string Description { get; set; } = string.Empty; - - /// - /// The assistant instructions template - /// - [YamlMember(Alias = "instructions")] - public string Instructions { get; set; } = string.Empty; - - ///// - ///// The assistant instructions template - ///// - //[YamlMember(Alias = "template")] - //public string Template { get; set; } = string.Empty; - - ///// - ///// The assistant instruction template format. - ///// - //[YamlMember(Alias = "template_format")] - //public string TemplateFormat { get; set; } = string.Empty; - - ///// - ///// Describes the input variables for the template. - ///// - //[YamlMember(Alias = "input_variables")] - //public List InputVariables { get; set; } - - ///// - ///// Describes known valid models. - ///// - //[YamlMember(Alias = "execution_settings")] - //public List ExecutionSettings { get; set; } -} diff --git a/dotnet/src/Experimental/Assistants/Models/AssistantModel.cs b/dotnet/src/Experimental/Assistants/Models/AssistantModel.cs deleted file mode 100644 index afeec4612bd4..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/AssistantModel.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// list of run steps belonging to a run. -/// -internal sealed class AssistantListModel : OpenAIListModel -{ - // No specialization -} - -/// -/// Model of Assistant data returned from OpenAI -/// -internal sealed record AssistantModel -{ - /// - /// Identifier, which can be referenced in API endpoints - /// - [JsonPropertyName("id")] - public string Id { get; init; } = string.Empty; - - /// - /// Always "assistant" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; init; } = "assistant"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// Unix timestamp (in seconds) for when the assistant was created - /// - [JsonPropertyName("created_at")] - public long CreatedAt { get; init; } - - /// - /// Name of the assistant - /// - [JsonPropertyName("name")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Name { get; set; } - - /// - /// The description of the assistant - /// - [JsonPropertyName("description")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Description { get; set; } - - /// - /// ID of the model to use - /// - [JsonPropertyName("model")] - public string Model { get; set; } = string.Empty; - - /// - /// The system instructions that the assistant uses - /// - [JsonPropertyName("instructions")] - public string Instructions { get; set; } = string.Empty; - - /// - /// A list of tool enabled on the assistant - /// There can be a maximum of 128 tools per assistant. - /// - [JsonPropertyName("tools")] - public List Tools { get; init; } = new List(); - - /// - /// A list of file IDs attached to this assistant. - /// There can be a maximum of 20 files attached to the assistant. - /// - [JsonPropertyName("file_ids")] - public List FileIds { get; init; } = new List(); - - /// - /// Set of 16 key-value pairs that can be attached to an object. - /// This can be useful for storing additional information about the - /// object in a structured format. - /// Keys can be a maximum of 64 characters long and values can be a - /// maximum of 512 characters long. - /// - [JsonPropertyName("metadata")] - public Dictionary Metadata { get; init; } = new Dictionary(); -} diff --git a/dotnet/src/Experimental/Assistants/Models/OpenAIListModel.cs b/dotnet/src/Experimental/Assistants/Models/OpenAIListModel.cs deleted file mode 100644 index 8c6c70eb9441..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/OpenAIListModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// list of run steps belonging to a run. -/// -internal abstract class OpenAIListModel -{ - /// - /// Always "list" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; set; } = "list"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// List of steps. - /// - [JsonPropertyName("data")] - public List Data { get; set; } = new List(); - - /// - /// The identifier of the first data record. - /// - [JsonPropertyName("first_id")] - public string FirstId { get; set; } = string.Empty; - - /// - /// The identifier of the last data record. - /// - [JsonPropertyName("last_id")] - public string LastId { get; set; } = string.Empty; - - /// - /// Indicates of more pages of data exist. - /// - [JsonPropertyName("has_more")] - public bool HasMore { get; set; } -} diff --git a/dotnet/src/Experimental/Assistants/Models/OpenAIParameters.cs b/dotnet/src/Experimental/Assistants/Models/OpenAIParameters.cs deleted file mode 100644 index 36d3892e2a87..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/OpenAIParameters.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// Wrapper for parameter map. -/// -internal sealed class OpenAIParameters -{ - /// - /// Empty parameter set. - /// - public static readonly OpenAIParameters Empty = new(); - - /// - /// Always "object" - /// - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - /// - /// Set of parameters. - /// - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = new(); - - /// - /// Set of parameters. - /// - [JsonPropertyName("required")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? Required { get; set; } -} - -/// -/// Wrapper for parameter definition. -/// -internal sealed class OpenAIParameter -{ - /// - /// The parameter type. - /// - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - /// - /// The parameter description. - /// - [JsonPropertyName("description")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Description { get; set; } -} diff --git a/dotnet/src/Experimental/Assistants/Models/ThreadMessageModel.cs b/dotnet/src/Experimental/Assistants/Models/ThreadMessageModel.cs deleted file mode 100644 index 05efab92ed87..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ThreadMessageModel.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 -#pragma warning disable CA1852 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// list of run steps belonging to a run. -/// -internal sealed class ThreadMessageListModel : OpenAIListModel -{ - // No specialization -} - -/// -/// Represents a message within a thread. -/// -internal sealed class ThreadMessageModel -{ - /// - /// Identifier, which can be referenced in API endpoints. - /// - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// - /// Always "thread.message" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; set; } = "thread.message"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// Unix timestamp (in seconds) for when the message was created. - /// - [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } - - /// - /// The thread ID that this message belongs to. - /// - [JsonPropertyName("thread_id")] - public string ThreadId { get; set; } = string.Empty; - - /// - /// The entity that produced the message. One of "user" or "assistant". - /// - [JsonPropertyName("role")] - public string Role { get; set; } = string.Empty; - - /// - /// The content of the message in array of text and/or images. - /// - [JsonPropertyName("content")] - public List Content { get; set; } = new List(); - - /// - /// A list of file IDs that the assistant should use. - /// - [JsonPropertyName("file_ids")] - public List FileIds { get; set; } = new List(); - - /// - /// If applicable, the ID of the assistant that authored this message. - /// - [JsonPropertyName("assistant_id")] - public string AssistantId { get; set; } = string.Empty; - - /// - /// If applicable, the ID of the run associated with the authoring of this message. - /// - [JsonPropertyName("run_id")] - public string RunId { get; set; } = string.Empty; - - /// - /// Set of 16 key-value pairs that can be attached to an object. - /// This can be useful for storing additional information about the - /// object in a structured format. Keys can be a maximum of 64 - /// characters long and values can be a maximum of 512 characters long. - /// - [JsonPropertyName("metadata")] - public Dictionary Metadata { get; set; } = new Dictionary(); - - /// - /// Representa contents within a message. - /// - public sealed class ContentModel - { - /// - /// Type of content. - /// - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - /// - /// Text context. - /// - [JsonPropertyName("text")] - public TextContentModel? Text { get; set; } - } - - /// - /// Text content. - /// - public sealed class TextContentModel - { - /// - /// The text itself. - /// - [JsonPropertyName("value")] - public string Value { get; set; } = string.Empty; - - /// - /// Any annotations on the text. - /// - [JsonPropertyName("annotations")] - public List Annotations { get; set; } = new List(); - } -} diff --git a/dotnet/src/Experimental/Assistants/Models/ThreadModel.cs b/dotnet/src/Experimental/Assistants/Models/ThreadModel.cs deleted file mode 100644 index 1dd73daa4f02..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ThreadModel.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// Model of Thread data returned from OpenAI -/// -internal sealed class ThreadModel -{ - /// - /// Identifier, which can be referenced in API endpoints. - /// - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// - /// Always "thread" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; set; } = "thread"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// The Unix timestamp (in seconds) for when the thread was created. - /// - [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } - - /// - /// Set of 16 key-value pairs that can be attached to an object. - /// This can be useful for storing additional information about the - /// object in a structured format. Keys can be a maximum of 64 - /// characters long and values can be a maximum of 512 characters long. - /// - [JsonPropertyName("metadata")] - public Dictionary Metadata { get; set; } = new Dictionary(); -} diff --git a/dotnet/src/Experimental/Assistants/Models/ThreadRunModel.cs b/dotnet/src/Experimental/Assistants/Models/ThreadRunModel.cs deleted file mode 100644 index 062963a029ad..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ThreadRunModel.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// Represents an execution run on a thread. -/// -internal sealed class ThreadRunModel -{ - /// - /// Identifier, which can be referenced in API endpoints. - /// - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// - /// Always "thread.run" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; set; } = "thread.run"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// Unix timestamp (in seconds) for when the run was created. - /// - [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } - - /// - /// ID of the assistant used for execution of this run. - /// - [JsonPropertyName("assistant_id")] - public string AssistantId { get; set; } = string.Empty; - - /// - /// ID of the thread that was executed on as a part of this run. - /// - [JsonPropertyName("thread_id")] - public string ThreadId { get; set; } = string.Empty; - - /// - /// The status of the run, which can be one of: - /// queued, in_progress, requires_action, cancelling, cancelled, failed, completed, or expired. - /// - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - /// - /// Unix timestamp (in seconds) for when the run was started. - /// - [JsonPropertyName("started_at")] - public long? StartedAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run will expire. - /// - [JsonPropertyName("expires_at")] - public long? ExpiresAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run was cancelled. - /// - [JsonPropertyName("cancelled_at")] - public long? CancelledAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run failed. - /// - [JsonPropertyName("failed_at")] - public long? FailedAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run was completed. - /// - [JsonPropertyName("completed_at")] - public long? CompletedAt { get; set; } - - /// - /// The last error associated with this run. Will be null if there are no errors. - /// - [JsonPropertyName("last_error")] - public ErrorModel? LastError { get; set; } - - /// - /// The model that the assistant used for this run. - /// - [JsonPropertyName("model")] - public string Model { get; set; } = string.Empty; - - /// - /// The instructions that the assistant used for this run. - /// - [JsonPropertyName("instructions")] - public string Instructions { get; set; } = string.Empty; - - /// - /// The list of tools that the assistant used for this run. - /// - [JsonPropertyName("tools")] - public List Tools { get; set; } = new List(); - - /// - /// The list of File IDs the assistant used for this run. - /// - [JsonPropertyName("file_ids")] - public List FileIds { get; set; } = new List(); - - /// - /// Set of 16 key-value pairs that can be attached to an object. - /// This can be useful for storing additional information about the - /// object in a structured format. Keys can be a maximum of 64 - /// characters long and values can be a maximum of 512 characters long. - /// - [JsonPropertyName("metadata")] - public Dictionary Metadata { get; set; } = new Dictionary(); - - /// - /// Run error information. - /// - public sealed class ErrorModel - { - /// - /// Error code. - /// - [JsonPropertyName("code")] - public string Code { get; set; } = string.Empty; - - /// - /// Error message. - /// - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; - } -} diff --git a/dotnet/src/Experimental/Assistants/Models/ThreadRunStepModel.cs b/dotnet/src/Experimental/Assistants/Models/ThreadRunStepModel.cs deleted file mode 100644 index c94b65632733..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ThreadRunStepModel.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 - -using System; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// list of run steps belonging to a run. -/// -internal sealed class ThreadRunStepListModel : OpenAIListModel -{ - // No specialization -} - -/// -/// Step in a run on a thread. -/// -internal sealed class ThreadRunStepModel -{ - /// - /// Identifier of the run step, which can be referenced in API endpoints. - /// - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// - /// Always "thread.run.step" - /// - [JsonPropertyName("object")] -#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema - public string Object { get; set; } = "thread.run.step"; -#pragma warning restore CA1720 // Identifier contains type name - - /// - /// Unix timestamp (in seconds) for when the run step was created. - /// - [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } - - /// - /// The ID of the run to which the run step belongs. - /// - [JsonPropertyName("run_id")] - public string RunId { get; set; } = string.Empty; - - /// - /// ID of the assistant associated with the run step. - /// - [JsonPropertyName("assistant_id")] - public string AssistantId { get; set; } = string.Empty; - - /// - /// The ID of the thread to which the run and run step belongs. - /// - [JsonPropertyName("thread_id")] - public string ThreadId { get; set; } = string.Empty; - - /// - /// The type of run step, which can be either message_creation or tool_calls. - /// - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - /// - /// The status of the run step, which can be one of: - /// in_progress, cancelled, failed, completed, or expired. - /// - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - /// - /// Unix timestamp (in seconds) for when the run step was cancelled. - /// - [JsonPropertyName("cancelled_at")] - public long? CancelledAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run step completed. - /// - [JsonPropertyName("completed_at")] - public long? CompletedAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run step expired. - /// A step is considered expired if the parent run is expired. - /// - [JsonPropertyName("expired_at")] - public long? ExpiredAt { get; set; } - - /// - /// Unix timestamp (in seconds) for when the run step failed. - /// - [JsonPropertyName("failed_at")] - public long? FailedAt { get; set; } - - /// - /// The last error associated with this run step. Will be null if there are no errors. - /// - [JsonPropertyName("last_error")] - public string LastError { get; set; } = string.Empty; - - /// - /// The details of the run step. - /// - [JsonPropertyName("step_details")] - public StepDetailsModel StepDetails { get; set; } = StepDetailsModel.Empty; - - /// - /// Details of a run step. - /// - public sealed class StepDetailsModel - { - /// - /// Empty definition - /// - public static StepDetailsModel Empty = new(); - - /// - /// Type of detail. - /// - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - /// - /// Details of the message creation by the run step. - /// - [JsonPropertyName("message_creation")] - public MessageCreationDetailsModel? MessageCreation { get; set; } - - /// - /// Details of tool calls. - /// - [JsonPropertyName("tool_calls")] - public ToolCallsDetailsModel[] ToolCalls { get; set; } = Array.Empty(); - } - - /// - /// Message creation details. - /// - public sealed class MessageCreationDetailsModel - { - /// - /// ID of the message that was created by this run step. - /// - [JsonPropertyName("message_id")] - public string MessageId { get; set; } = string.Empty; - } - - /// - /// Tool call details. - /// - public sealed class ToolCallsDetailsModel - { - /// - /// ID of the tool call. - /// - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// - /// The type of tool call. - /// - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - /// - /// The definition of the function that was called. - /// - [JsonPropertyName("function")] - public FunctionDetailsModel Function { get; set; } = FunctionDetailsModel.Empty; - } - - /// - /// Function call details. - /// - public sealed class FunctionDetailsModel - { - /// - /// Empty definition - /// - public static FunctionDetailsModel Empty = new(); - - /// - /// The name of the function. - /// - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// - /// The arguments passed to the function. - /// - [JsonPropertyName("arguments")] - public string Arguments { get; set; } = string.Empty; - - /// - /// The output of the function. - /// This will be null if the outputs have not been submitted yet. - /// - [JsonPropertyName("output")] - public string Output { get; set; } = string.Empty; - } -} diff --git a/dotnet/src/Experimental/Assistants/Models/ToolModel.cs b/dotnet/src/Experimental/Assistants/Models/ToolModel.cs deleted file mode 100644 index 6e13c0e9b5fd..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ToolModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -/// -/// Tool entry -/// -internal sealed record ToolModel -{ - /// - /// Type of tool to have at assistant's disposition - /// - [JsonPropertyName("type")] - public string Type { get; init; } = string.Empty; - - /// - /// The function definition for Type = 'function'. - /// - [JsonPropertyName("function")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public FunctionModel? Function { get; init; } - - /// - /// Defines the function when ToolModel.Type == 'function'. - /// - public sealed record FunctionModel - { - /// - /// The function name. - /// - [JsonPropertyName("name")] - public string Name { get; init; } = string.Empty; - - /// - /// The function description. - /// - [JsonPropertyName("description")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Description { get; init; } - - /// - /// The function description. - /// - [JsonPropertyName("parameters")] - public OpenAIParameters Parameters { get; init; } = OpenAIParameters.Empty; - } -} diff --git a/dotnet/src/Experimental/Assistants/Models/ToolResultModel.cs b/dotnet/src/Experimental/Assistants/Models/ToolResultModel.cs deleted file mode 100644 index f7f9e4a0c310..000000000000 --- a/dotnet/src/Experimental/Assistants/Models/ToolResultModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1812 -#pragma warning disable CA1852 - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Experimental.Assistants.Models; - -internal sealed class ToolResultModel -{ - private static readonly object s_placeholder = new(); - - /// - /// The tool call identifier. - /// - [JsonPropertyName("tool_call_id")] - public string CallId { get; set; } = string.Empty; - - /// - /// The tool output - /// - [JsonPropertyName("output")] - public object Output { get; set; } = s_placeholder; -} diff --git a/dotnet/src/InternalUtilities/src/Http/HttpResponseStream.cs b/dotnet/src/InternalUtilities/src/Http/HttpResponseStream.cs new file mode 100644 index 000000000000..3903b716dd88 --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Http/HttpResponseStream.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; + +namespace Microsoft.SemanticKernel.Http; + +/// +/// Associate a response stream with its parent response for parity in life-cycle management. +/// +[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "This class is an internal utility.")] +internal sealed class HttpResponseStream : Stream +{ + private readonly Stream _stream; + private readonly HttpResponseMessage _response; + + public override bool CanRead => this._stream.CanRead; + + public override bool CanSeek => this._stream.CanSeek; + + public override bool CanWrite => this._stream.CanWrite; + + public override long Length => this._stream.Length; + + public override long Position { get => this._stream.Position; set => this._stream.Position = value; } + + public override void Flush() + { + this._stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return this._stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return this._stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + this._stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + this._stream.Write(buffer, offset, count); + } + + public HttpResponseStream(Stream stream, HttpResponseMessage response) + { + this._stream = stream; + this._response = response; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + this._stream.Dispose(); + this._response.Dispose(); + } + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/BinaryContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/BinaryContent.cs new file mode 100644 index 000000000000..776b49e80b0b --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/BinaryContent.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel; + +/// +/// Provides access to binary content. +/// +public class BinaryContent : KernelContent +{ + private readonly Func>? _streamProvider; + private readonly BinaryData? _content; + + /// + /// Initializes a new instance of the class. + /// + /// The binary content + /// The model ID used to generate the content + /// Inner content + /// Additional metadata + public BinaryContent( + BinaryData content, + string? modelId = null, + object? innerContent = null, + IReadOnlyDictionary? metadata = null) + : base(innerContent, modelId, metadata) + { + Verify.NotNull(content, nameof(content)); + + this._content = content; + } + + /// + /// Initializes a new instance of the class. + /// + /// The asynchronous stream provider. + /// The model ID used to generate the content + /// Inner content + /// Additional metadata + /// + /// The is accessed and disposed as part of either the + /// the or + /// accessor methods. + /// + public BinaryContent( + Func> streamProvider, + string? modelId = null, + object? innerContent = null, + IReadOnlyDictionary? metadata = null) + : base(innerContent, modelId, metadata) + { + Verify.NotNull(streamProvider, nameof(streamProvider)); + + this._streamProvider = streamProvider; + } + + /// + /// Access the content stream. + /// + /// + /// Caller responsible for disposal. + /// + public async Task GetStreamAsync() + { + if (this._streamProvider != null) + { + return await this._streamProvider.Invoke().ConfigureAwait(false); + } + + if (this._content != null) + { + return this._content.ToStream(); + } + + throw new KernelException("Null content"); + } + + /// + /// The content stream + /// + public async Task GetContentAsync() + { + if (this._streamProvider != null) + { + using var stream = await this._streamProvider.Invoke().ConfigureAwait(false); + return await BinaryData.FromStreamAsync(stream).ConfigureAwait(false); + } + + if (this._content != null) + { + return this._content; + } + + throw new KernelException("Null content"); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj index 8cd6926c20fb..25368a6afe92 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj +++ b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj @@ -23,6 +23,7 @@ +