diff --git a/docs/decisions/0021-aiservice-metadata.md b/docs/decisions/0021-aiservice-metadata.md new file mode 100644 index 000000000000..70822b1e82c4 --- /dev/null +++ b/docs/decisions/0021-aiservice-metadata.md @@ -0,0 +1,157 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: {proposed} +date: {2023-11-10} +deciders: SergeyMenshykh, markwallace, rbarreto, dmytrostruk +consulted: +informed: +--- +# Add AI Service Metadata + +## Context and Problem Statement + +Developers need to be able to know more information about the `IAIService` that will be used to execute a semantic function or a plan. +Some examples of why they need this information: + +1. As an SK developer I want to write a `IAIServiceSelector` which allows me to select the OpenAI service to used based on the configured model id so that I can select the optimum (could eb cheapest) model to use based on the prompt I am executing. +2. As an SK developer I want to write a pre-invocation hook which will compute the token size of a prompt before the prompt is sent to the LLM, so that I can determine the optimum `IAIService` to use. The library I am using to compute the token size of the prompt requires the model id. + +Current implementation of `IAIService` is empty. + +```csharp +public interface IAIService +{ +} +``` + +We can retrieve `IAIService` instances using `T IKernel.GetService(string? name = null) where T : IAIService;` i.e., by service type and name (aka service id). +The concrete instance of an `IAIService` can have different attributes depending on the service provider e.g. Azure OpenAI has a deployment name and OpenAI services have a model id. + +Consider the following code snippet: + +```csharp +IKernel kernel = new KernelBuilder() + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureChatCompletionService( + deploymentName: chatDeploymentName, + endpoint: endpoint, + serviceId: "AzureOpenAIChat", + apiKey: apiKey) + .WithOpenAIChatCompletionService( + modelId: openAIModelId, + serviceId: "OpenAIChat", + apiKey: openAIApiKey) + .Build(); + +var service = kernel.GetService("OpenAIChat"); +``` + +For Azure OpenAI we create the service with a deployment name. This is an arbitrary name specified by the person who deployed the AI model e.g. it could be `eastus-gpt-4` or `foo-bar`. +For OpenAI we create the service with a model id. This must match one of the deployed OpenAI models. + +From the perspective of a prompt creator using OpenAI, they will typically tune their prompts based on the model. So when the prompt is executed we need to be able to retrieve the service using the model id. As shown in the code snippet above the `IKernel` only supports retrieving an `IAService` instance by id. Additionally the `IChatCompletion` is a generic interface so it doesn't contain any properties which provide information about a specific connector instance. + +## Decision Drivers + +* We need a mechanism to store generic metadata for an `IAIService` instance. + * It will be the responsibility of the concrete `IAIService` instance to store the metadata that is relevant e.g., model id for OpenAI and HuggingFace AI services. +* We need to be able to iterate over the available `IAIService` instances. + +## Considered Options + +* Option #1 + * Extend `IAIService` to include the following properties: + * `string? ModelId { get; }` which returns the model id. It will be the responsibility of each `IAIService` implementation to populate this with the appropriate value. + * `IReadOnlyDictionary Attributes { get; }` which returns the attributes as a readonly dictionary. It will be the responsibility of each `IAIService` implementation to populate this with the appropriate metadata. + * Extend `INamedServiceProvider` to include this method `ICollection GetServices() where T : TService;` + * Extend `OpenAIKernelBuilderExtensions` so that `WithAzureXXX` methods will include a `modelId` property if a specific model can be targeted. +* Option #2 + * Extend `IAIService` to include the following method: + * `T? GetAttributes() where T : AIServiceAttributes;` which returns an instance of `AIServiceAttributes`. It will be the responsibility of each `IAIService` implementation to define it's own service attributes class and populate this with the appropriate values. + * Extend `INamedServiceProvider` to include this method `ICollection GetServices() where T : TService;` + * Extend `OpenAIKernelBuilderExtensions` so that `WithAzureXXX` methods will include a `modelId` property if a specific model can be targeted. +* Option #3 +* Option #2 + * Extend `IAIService` to include the following properties: + * `public IReadOnlyDictionary Attributes => this.InternalAttributes;` which returns a read only dictionary. It will be the responsibility of each `IAIService` implementation to define it's own service attributes class and populate this with the appropriate values. + * `ModelId` + * `Endpoint` + * `ApiVersion` + * Extend `INamedServiceProvider` to include this method `ICollection GetServices() where T : TService;` + * Extend `OpenAIKernelBuilderExtensions` so that `WithAzureXXX` methods will include a `modelId` property if a specific model can be targeted. + +These options would be used as follows: + +As an SK developer I want to write a custom `IAIServiceSelector` which will select an AI service based on the model id because I want to restrict which LLM is used. +In the sample below the service selector implementation looks for the first service that is a GPT3 model. + +### Option 1 + +``` csharp +public class Gpt3xAIServiceSelector : IAIServiceSelector +{ + public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + { + var services = serviceProvider.GetServices(); + foreach (var service in services) + { + if (!string.IsNullOrEmpty(service.ModelId) && service.ModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Selected model: {service.ModelId}"); + return (service, new OpenAIRequestSettings()); + } + } + + throw new SKException("Unable to find AI service for GPT 3.x."); + } +} +``` + +## Option 2 + +``` csharp +public class Gpt3xAIServiceSelector : IAIServiceSelector +{ + public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + { + var services = serviceProvider.GetServices(); + foreach (var service in services) + { + var serviceModelId = service.GetAttributes()?.ModelId; + if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Selected model: {serviceModelId}"); + return (service, new OpenAIRequestSettings()); + } + } + + throw new SKException("Unable to find AI service for GPT 3.x."); + } +} +``` + +## Option 3 + +```csharp +public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService +{ + var services = serviceProvider.GetServices(); + foreach (var service in services) + { + var serviceModelId = service.GetModelId(); + var serviceOrganization = service.GetAttribute(OpenAIServiceAttributes.OrganizationKey); + var serviceDeploymentName = service.GetAttribute(AzureOpenAIServiceAttributes.DeploymentNameKey); + if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Selected model: {serviceModelId}"); + return (service, new OpenAIRequestSettings()); + } + } + + throw new SKException("Unable to find AI service for GPT 3.x."); +} +``` + +## Decision Outcome + +Chosen option: Option 1, because it's a simple implementation and allows easy iteration over all possible attributes. diff --git a/dotnet/samples/KernelSyntaxExamples/Example12_SequentialPlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example12_SequentialPlanner.cs index d72b9e5f3487..db4bcdd4157c 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example12_SequentialPlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example12_SequentialPlanner.cs @@ -300,7 +300,7 @@ private static SemanticTextMemory InitializeMemory() var memoryStorage = new VolatileMemoryStore(); var textEmbeddingGenerator = new AzureOpenAITextEmbeddingGeneration( - modelId: TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, + deploymentName: TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, endpoint: TestConfiguration.AzureOpenAIEmbeddings.Endpoint, apiKey: TestConfiguration.AzureOpenAIEmbeddings.ApiKey); diff --git a/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs b/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs index e5f6405f7824..e0077418362b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs @@ -29,8 +29,14 @@ */ public class MyTextCompletionService : ITextCompletion { + public string? ModelId { get; private set; } + + public IReadOnlyDictionary Attributes => new Dictionary(); + public Task> GetCompletionsAsync(string text, AIRequestSettings? requestSettings, CancellationToken cancellationToken = default) { + this.ModelId = requestSettings?.ModelId; + return Task.FromResult>(new List { new MyTextCompletionStreamingResult() diff --git a/dotnet/samples/KernelSyntaxExamples/Example34_CustomChatModel.cs b/dotnet/samples/KernelSyntaxExamples/Example34_CustomChatModel.cs index c9e0c399d2de..752385346771 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example34_CustomChatModel.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example34_CustomChatModel.cs @@ -20,6 +20,10 @@ */ public sealed class MyChatCompletionService : IChatCompletion { + public string? ModelId { get; private set; } + + public IReadOnlyDictionary Attributes => new Dictionary(); + public ChatHistory CreateNewChat(string? instructions = null) { var chatHistory = new MyChatHistory(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs index 9ca201e6f38f..f7cc55a3ccb1 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs @@ -73,7 +73,7 @@ public static Task RunAsync() var loggerFactory = NullLoggerFactory.Instance; var memoryStorage = new VolatileMemoryStore(); var textEmbeddingGenerator = new AzureOpenAITextEmbeddingGeneration( - modelId: azureOpenAIEmbeddingDeployment, + deploymentName: azureOpenAIEmbeddingDeployment, endpoint: azureOpenAIEndpoint, apiKey: azureOpenAIKey, loggerFactory: loggerFactory); @@ -88,11 +88,11 @@ public static Task RunAsync() using var httpClient = new HttpClient(httpHandler); var aiServices = new AIServiceCollection(); ITextCompletion Factory() => new AzureOpenAIChatCompletion( - modelId: azureOpenAIChatCompletionDeployment, + deploymentName: azureOpenAIChatCompletionDeployment, endpoint: azureOpenAIEndpoint, apiKey: azureOpenAIKey, - httpClient, - loggerFactory); + httpClient: httpClient, + loggerFactory: loggerFactory); aiServices.SetService("foo", Factory); IAIServiceProvider aiServiceProvider = aiServices.Build(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs b/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs index 9d414c2c59de..ff4e6710d1c4 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs @@ -62,7 +62,7 @@ public static async Task RunAsync() var kernel = new KernelBuilder() .WithLoggerFactory(loggerFactory) .WithAIService(TestConfiguration.AzureOpenAI.ChatDeploymentName, (loggerFactory) => - new AzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.ChatDeploymentName, openAIClient, loggerFactory)) + new AzureOpenAIChatCompletion(deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, openAIClient: openAIClient, loggerFactory: loggerFactory)) .Build(); // Load semantic plugin defined with prompt templates diff --git a/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs index 6415d4b62c27..9700b775fffa 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.TemplateEngine; using RepoUtils; // ReSharper disable once InconsistentNaming @@ -16,11 +18,12 @@ public static async Task RunAsync() { Console.WriteLine("======== Example61_MultipleLLMs ========"); - string apiKey = TestConfiguration.AzureOpenAI.ApiKey; - string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string endpoint = TestConfiguration.AzureOpenAI.Endpoint; + string azureApiKey = TestConfiguration.AzureOpenAI.ApiKey; + string azureDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; + string azureModelId = TestConfiguration.AzureOpenAI.ChatModelId; + string azureEndpoint = TestConfiguration.AzureOpenAI.Endpoint; - if (apiKey == null || chatDeploymentName == null || endpoint == null) + if (azureApiKey == null || azureDeploymentName == null || azureEndpoint == null) { Console.WriteLine("AzureOpenAI endpoint, apiKey, or deploymentName not found. Skipping example."); return; @@ -38,23 +41,25 @@ public static async Task RunAsync() IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureOpenAIChatCompletionService( - deploymentName: chatDeploymentName, - endpoint: endpoint, + deploymentName: azureDeploymentName, + endpoint: azureEndpoint, serviceId: "AzureOpenAIChat", - apiKey: apiKey) + modelId: azureModelId, + apiKey: azureApiKey) .WithOpenAIChatCompletionService( modelId: openAIModelId, serviceId: "OpenAIChat", apiKey: openAIApiKey) .Build(); - await RunSemanticFunctionAsync(kernel, "AzureOpenAIChat"); - await RunSemanticFunctionAsync(kernel, "OpenAIChat"); + await RunByServiceIdAsync(kernel, "AzureOpenAIChat"); + await RunByModelIdAsync(kernel, openAIModelId); + await RunByFirstModelIdAsync(kernel, "gpt-4-1106-preview", azureModelId, openAIModelId); } - public static async Task RunSemanticFunctionAsync(IKernel kernel, string serviceId) + public static async Task RunByServiceIdAsync(IKernel kernel, string serviceId) { - Console.WriteLine($"======== {serviceId} ========"); + Console.WriteLine($"======== Service Id: {serviceId} ========"); var prompt = "Hello AI, what can you do for me?"; @@ -66,4 +71,41 @@ public static async Task RunSemanticFunctionAsync(IKernel kernel, string service }); Console.WriteLine(result.GetValue()); } + + public static async Task RunByModelIdAsync(IKernel kernel, string modelId) + { + Console.WriteLine($"======== Model Id: {modelId} ========"); + + var prompt = "Hello AI, what can you do for me?"; + + var result = await kernel.InvokeSemanticFunctionAsync( + prompt, + requestSettings: new AIRequestSettings() + { + ModelId = modelId + }); + Console.WriteLine(result.GetValue()); + } + + public static async Task RunByFirstModelIdAsync(IKernel kernel, params string[] modelIds) + { + Console.WriteLine($"======== Model Ids: {string.Join(", ", modelIds)} ========"); + + var prompt = "Hello AI, what can you do for me?"; + + var modelSettings = new List(); + foreach (var modelId in modelIds) + { + modelSettings.Add(new AIRequestSettings() { ModelId = modelId }); + } + var promptTemplateConfig = new PromptTemplateConfig() { ModelSettings = modelSettings }; + + var skfunction = kernel.RegisterSemanticFunction( + "HelloAI", + prompt, + promptTemplateConfig); + + var result = await kernel.RunAsync(skfunction); + Console.WriteLine(result.GetValue()); + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs index c4f620366c77..ade2db537a07 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI; @@ -9,7 +8,6 @@ using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TemplateEngine; using RepoUtils; // ReSharper disable once InconsistentNaming @@ -20,13 +18,14 @@ public static class Example62_CustomAIServiceSelector /// public static async Task RunAsync() { - Console.WriteLine("======== Example61_CustomAIServiceSelector ========"); + Console.WriteLine("======== Example62_CustomAIServiceSelector ========"); - string apiKey = TestConfiguration.AzureOpenAI.ApiKey; - string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string endpoint = TestConfiguration.AzureOpenAI.Endpoint; + string azureApiKey = TestConfiguration.AzureOpenAI.ApiKey; + string azureDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; + string azureModelId = TestConfiguration.AzureOpenAI.ChatModelId; + string azureEndpoint = TestConfiguration.AzureOpenAI.Endpoint; - if (apiKey == null || chatDeploymentName == null || endpoint == null) + if (azureApiKey == null || azureDeploymentName == null || azureModelId == null || azureEndpoint == null) { Console.WriteLine("AzureOpenAI endpoint, apiKey, or deploymentName not found. Skipping example."); return; @@ -41,72 +40,49 @@ public static async Task RunAsync() return; } - IKernel kernel = new KernelBuilder() + var kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureOpenAIChatCompletionService( - deploymentName: chatDeploymentName, - endpoint: endpoint, + deploymentName: azureDeploymentName, + endpoint: azureEndpoint, serviceId: "AzureOpenAIChat", - apiKey: apiKey) + modelId: azureModelId, + apiKey: azureApiKey, + setAsDefault: true) .WithOpenAIChatCompletionService( modelId: openAIModelId, serviceId: "OpenAIChat", apiKey: openAIApiKey) - .WithAIServiceSelector(new ByModelIdAIServiceSelector(openAIModelId)) + // Use the custom AI service selector to select the GPT 3.x model + .WithAIServiceSelector(new Gpt3xAIServiceSelector()) .Build(); - var modelSettings = new List - { - new OpenAIRequestSettings() { ServiceId = "AzureOpenAIChat", ModelId = "" }, - new OpenAIRequestSettings() { ServiceId = "OpenAIChat", ModelId = openAIModelId } - }; - - await RunSemanticFunctionAsync(kernel, "Hello AI, what can you do for me?", modelSettings); - } - - public static async Task RunSemanticFunctionAsync(IKernel kernel, string prompt, List modelSettings) - { - Console.WriteLine($"======== {prompt} ========"); - - var promptTemplateConfig = new PromptTemplateConfig() { ModelSettings = modelSettings }; - - var skfunction = kernel.RegisterSemanticFunction( - "MyFunction", - prompt, - promptTemplateConfig); - - var result = await kernel.RunAsync(skfunction); + var prompt = "Hello AI, what can you do for me?"; + var result = await kernel.InvokeSemanticFunctionAsync(prompt); Console.WriteLine(result.GetValue()); } -} - -public class ByModelIdAIServiceSelector : IAIServiceSelector -{ - private readonly string _openAIModelId; - - public ByModelIdAIServiceSelector(string openAIModelId) - { - this._openAIModelId = openAIModelId; - } - public (T?, AIRequestSettings?) SelectAIService(SKContext context, ISKFunction skfunction) where T : IAIService + /// + /// Custom AI service selector that selects the GPT 3.x model + /// + private sealed class Gpt3xAIServiceSelector : IAIServiceSelector { - foreach (var model in skfunction.ModelSettings) + public (T?, AIRequestSettings?) SelectAIService(SKContext context, ISKFunction skfunction) where T : IAIService { - if (model is OpenAIRequestSettings openAIModel) + var services = context.ServiceProvider.GetServices(); + foreach (var service in services) { - if (openAIModel.ModelId == this._openAIModelId) + // Find the first service that has a model id that starts with "gpt-3" + var serviceModelId = service.GetModelId(); + var endpoint = service.GetEndpoint(); + if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) { - var service = context.ServiceProvider.GetService(openAIModel.ServiceId); - if (service is not null) - { - Console.WriteLine($"======== Selected service: {openAIModel.ServiceId} {openAIModel.ModelId} ========"); - return (service, model); - } + Console.WriteLine($"Selected model: {serviceModelId} {endpoint}"); + return (service, new OpenAIRequestSettings()); } } - } - throw new SKException("Unable to find AI service to handled request."); + throw new SKException("Unable to find AI service for GPT 3.x."); + } } } diff --git a/dotnet/samples/KernelSyntaxExamples/README.md b/dotnet/samples/KernelSyntaxExamples/README.md index 26e95a78a215..fcbc8ceaa810 100644 --- a/dotnet/samples/KernelSyntaxExamples/README.md +++ b/dotnet/samples/KernelSyntaxExamples/README.md @@ -56,7 +56,9 @@ dotnet user-secrets set "OpenAI:ApiKey" "..." dotnet user-secrets set "AzureOpenAI:ServiceId" "..." dotnet user-secrets set "AzureOpenAI:DeploymentName" "..." +dotnet user-secrets set "AzureOpenAI:ModelId" "..." dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "..." +dotnet user-secrets set "AzureOpenAI:ChatModelId" "..." dotnet user-secrets set "AzureOpenAI:Endpoint" "https://... .openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "..." diff --git a/dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs b/dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs index 1bc49c179b42..9b7e62af9d43 100644 --- a/dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs +++ b/dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs @@ -68,7 +68,9 @@ public class AzureOpenAIConfig { public string ServiceId { get; set; } public string DeploymentName { get; set; } + public string ModelId { get; set; } public string ChatDeploymentName { get; set; } + public string ChatModelId { get; set; } public string Endpoint { get; set; } public string ApiKey { get; set; } } diff --git a/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextCompletion/HuggingFaceTextCompletion.cs b/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextCompletion/HuggingFaceTextCompletion.cs index e57690f76f98..168e4e0d8bb4 100644 --- a/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextCompletion/HuggingFaceTextCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextCompletion/HuggingFaceTextCompletion.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.HuggingFace.TextCompletion; @@ -25,6 +26,7 @@ public sealed class HuggingFaceTextCompletion : ITextCompletion private readonly string? _endpoint; private readonly HttpClient _httpClient; private readonly string? _apiKey; + private readonly Dictionary _attributes = new(); /// /// Initializes a new instance of the class. @@ -37,8 +39,10 @@ public HuggingFaceTextCompletion(Uri endpoint, string model) Verify.NotNull(endpoint); Verify.NotNullOrWhiteSpace(model); - this._endpoint = endpoint.AbsoluteUri; this._model = model; + this._endpoint = endpoint.AbsoluteUri; + this._attributes.Add(IAIServiceExtensions.ModelIdKey, this._model); + this._attributes.Add(IAIServiceExtensions.EndpointKey, this._endpoint); this._httpClient = new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); } @@ -60,8 +64,13 @@ public HuggingFaceTextCompletion(string model, string? apiKey = null, HttpClient this._apiKey = apiKey; this._httpClient = httpClient ?? new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); this._endpoint = endpoint; + this._attributes.Add(IAIServiceExtensions.ModelIdKey, this._model); + this._attributes.Add(IAIServiceExtensions.EndpointKey, this._endpoint ?? HuggingFaceApiEndpoint); } + /// + public IReadOnlyDictionary Attributes => this._attributes; + /// [Obsolete("Streaming capability is not supported, use GetCompletionsAsync instead")] public IAsyncEnumerable GetStreamingCompletionsAsync( diff --git a/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextEmbedding/HuggingFaceTextEmbeddingGeneration.cs b/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextEmbedding/HuggingFaceTextEmbeddingGeneration.cs index 103212317b7c..1d6c19907558 100644 --- a/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextEmbedding/HuggingFaceTextEmbeddingGeneration.cs +++ b/dotnet/src/Connectors/Connectors.AI.HuggingFace/TextEmbedding/HuggingFaceTextEmbeddingGeneration.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel.AI.Embeddings; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.HuggingFace.TextEmbedding; @@ -22,6 +23,7 @@ public sealed class HuggingFaceTextEmbeddingGeneration : ITextEmbeddingGeneratio private readonly string _model; private readonly string? _endpoint; private readonly HttpClient _httpClient; + private readonly Dictionary _attributes = new(); /// /// Initializes a new instance of the class. @@ -34,9 +36,10 @@ public HuggingFaceTextEmbeddingGeneration(Uri endpoint, string model) Verify.NotNull(endpoint); Verify.NotNullOrWhiteSpace(model); - this._endpoint = endpoint.AbsoluteUri; this._model = model; - + this._endpoint = endpoint.AbsoluteUri; + this._attributes.Add(IAIServiceExtensions.ModelIdKey, this._model); + this._attributes.Add(IAIServiceExtensions.EndpointKey, this._endpoint); this._httpClient = new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); } @@ -52,7 +55,8 @@ public HuggingFaceTextEmbeddingGeneration(string model, string endpoint) this._model = model; this._endpoint = endpoint; - + this._attributes.Add(IAIServiceExtensions.ModelIdKey, this._model); + this._attributes.Add(IAIServiceExtensions.EndpointKey, this._endpoint); this._httpClient = new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); } @@ -70,6 +74,8 @@ public HuggingFaceTextEmbeddingGeneration(string model, HttpClient httpClient, s this._model = model; this._endpoint = endpoint; this._httpClient = httpClient; + this._attributes.Add(IAIServiceExtensions.ModelIdKey, this._model); + this._attributes.Add(IAIServiceExtensions.EndpointKey, this._endpoint ?? this._httpClient.BaseAddress.ToString()); if (httpClient.BaseAddress == null && string.IsNullOrEmpty(endpoint)) { @@ -77,6 +83,9 @@ public HuggingFaceTextEmbeddingGeneration(string model, HttpClient httpClient, s } } + /// + public IReadOnlyDictionary Attributes => this._attributes; + /// public async Task>> GenerateEmbeddingsAsync(IList data, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/AzureOpenAIClientBase.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/AzureOpenAIClientBase.cs index 71f193e032c3..c7d9cc9dabd8 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/AzureOpenAIClientBase.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/AzureOpenAIClientBase.cs @@ -9,6 +9,7 @@ using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; /// @@ -16,6 +17,11 @@ namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; /// public abstract class AzureOpenAIClientBase : ClientBase { + /// + /// Key used to store the deployment name in the dictionary. + /// + public const string DeploymentNameKey = "DeploymentName"; + /// /// OpenAI / Azure OpenAI Client /// @@ -24,51 +30,51 @@ public abstract class AzureOpenAIClientBase : ClientBase /// /// Initializes a new instance of the class using API Key authentication. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. private protected AzureOpenAIClientBase( - string modelId, + string deploymentName, string endpoint, string apiKey, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) { - Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); Verify.NotNullOrWhiteSpace(apiKey); var options = GetClientOptions(httpClient); - this.ModelId = modelId; + this.DeploymentOrModelName = deploymentName; this.Client = new OpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey), options); } /// /// Initializes a new instance of the class supporting AAD authentication. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. private protected AzureOpenAIClientBase( - string modelId, + string deploymentName, string endpoint, TokenCredential credential, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) { - Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); var options = GetClientOptions(httpClient); - this.ModelId = modelId; + this.DeploymentOrModelName = deploymentName; this.Client = new OpenAIClient(new Uri(endpoint), credential, options); } @@ -77,19 +83,21 @@ private protected AzureOpenAIClientBase( /// Note: instances created this way might not have the default diagnostics settings, /// it's up to the caller to configure the client. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . /// The to use for logging. If null, no logging will be performed. private protected AzureOpenAIClientBase( - string modelId, + string deploymentName, OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) : base(loggerFactory) { - Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNull(openAIClient); - this.ModelId = modelId; + this.DeploymentOrModelName = deploymentName; this.Client = openAIClient; + + this.AddAttribute(DeploymentNameKey, deploymentName); } /// @@ -123,6 +131,6 @@ private static OpenAIClientOptions GetClientOptions(HttpClient? httpClient) /// Caller member name. Populated automatically by runtime. private protected void LogActionDetails([CallerMemberName] string? callerMemberName = default) { - this.Logger.LogInformation("Action: {Action}. Azure OpenAI Deployment Name: {DeploymentName}.", callerMemberName, this.ModelId); + this.Logger.LogInformation("Action: {Action}. Azure OpenAI Deployment Name: {DeploymentName}.", callerMemberName, this.DeploymentOrModelName); } } diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/ClientBase.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/ClientBase.cs index 4121793f99c3..35dae4906177 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/ClientBase.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/ClientBase.cs @@ -41,7 +41,7 @@ private protected ClientBase(ILoggerFactory? loggerFactory = null) /// /// Model Id or Deployment Name /// - private protected string ModelId { get; set; } = string.Empty; + private protected string DeploymentOrModelName { get; set; } = string.Empty; /// /// OpenAI / Azure OpenAI Client @@ -53,6 +53,11 @@ private protected ClientBase(ILoggerFactory? loggerFactory = null) /// private protected ILogger Logger { get; set; } + /// + /// Storage for AI service attributes. + /// + private protected Dictionary InternalAttributes = new(); + /// /// Instance of for metrics. /// @@ -100,7 +105,7 @@ private protected async Task> InternalGetTextResultsA var options = CreateCompletionsOptions(text, textRequestSettings); Response? response = await RunRequestAsync?>( - () => this.Client.GetCompletionsAsync(this.ModelId, options, cancellationToken)).ConfigureAwait(false); + () => this.Client.GetCompletionsAsync(this.DeploymentOrModelName, options, cancellationToken)).ConfigureAwait(false); if (response is null) { @@ -138,7 +143,7 @@ private protected async IAsyncEnumerable InternalGetTextStr var options = CreateCompletionsOptions(text, textRequestSettings); Response? response = await RunRequestAsync>( - () => this.Client.GetCompletionsStreamingAsync(this.ModelId, options, cancellationToken)).ConfigureAwait(false); + () => this.Client.GetCompletionsStreamingAsync(this.DeploymentOrModelName, options, cancellationToken)).ConfigureAwait(false); using StreamingCompletions streamingChatCompletions = response.Value; await foreach (StreamingChoice choice in streamingChatCompletions.GetChoicesStreaming(cancellationToken)) @@ -163,7 +168,7 @@ private protected async Task>> InternalGetEmbeddings var options = new EmbeddingsOptions(text); Response? response = await RunRequestAsync?>( - () => this.Client.GetEmbeddingsAsync(this.ModelId, options, cancellationToken)).ConfigureAwait(false); + () => this.Client.GetEmbeddingsAsync(this.DeploymentOrModelName, options, cancellationToken)).ConfigureAwait(false); if (response is null) { @@ -202,7 +207,7 @@ private protected async Task> InternalGetChatResultsA var chatOptions = CreateChatCompletionsOptions(chatRequestSettings, chat); Response? response = await RunRequestAsync?>( - () => this.Client.GetChatCompletionsAsync(this.ModelId, chatOptions, cancellationToken)).ConfigureAwait(false); + () => this.Client.GetChatCompletionsAsync(this.DeploymentOrModelName, chatOptions, cancellationToken)).ConfigureAwait(false); if (response is null) { @@ -242,7 +247,7 @@ private protected async IAsyncEnumerable InternalGetChatSt var options = CreateChatCompletionsOptions(chatRequestSettings, chat); Response? response = await RunRequestAsync>( - () => this.Client.GetChatCompletionsStreamingAsync(this.ModelId, options, cancellationToken)).ConfigureAwait(false); + () => this.Client.GetChatCompletionsStreamingAsync(this.DeploymentOrModelName, options, cancellationToken)).ConfigureAwait(false); if (response is null) { @@ -302,6 +307,14 @@ private protected async IAsyncEnumerable InternalGetChatSt } } + private protected void AddAttribute(string key, string? value) + { + if (!string.IsNullOrEmpty(value)) + { + this.InternalAttributes.Add(key, value!); + } + } + private static OpenAIChatHistory PrepareChatHistory(string text, AIRequestSettings? requestSettings, out OpenAIRequestSettings settings) { settings = OpenAIRequestSettings.FromRequestSettings(requestSettings); diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/OpenAIClientBase.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/OpenAIClientBase.cs index ccd33cb90a4e..ebb3764da94a 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/OpenAIClientBase.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/OpenAIClientBase.cs @@ -7,6 +7,7 @@ using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; @@ -15,6 +16,11 @@ namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; /// public abstract class OpenAIClientBase : ClientBase { + /// + /// Attribute name used to store the orhanization in the dictionary. + /// + public const string OrganizationKey = "Organization"; + /// /// OpenAI / Azure OpenAI Client /// @@ -38,7 +44,7 @@ private protected OpenAIClientBase( Verify.NotNullOrWhiteSpace(modelId); Verify.NotNullOrWhiteSpace(apiKey); - this.ModelId = modelId; + this.DeploymentOrModelName = modelId; var options = GetClientOptions(httpClient); @@ -66,7 +72,7 @@ private protected OpenAIClientBase( Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(openAIClient); - this.ModelId = modelId; + this.DeploymentOrModelName = modelId; this.Client = openAIClient; } @@ -76,7 +82,7 @@ private protected OpenAIClientBase( /// Caller member name. Populated automatically by runtime. private protected void LogActionDetails([CallerMemberName] string? callerMemberName = default) { - this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.ModelId); + this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.DeploymentOrModelName); } /// diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/AzureOpenAIChatCompletion.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/AzureOpenAIChatCompletion.cs index e79e226ab118..c40a039c965c 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/AzureOpenAIChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/AzureOpenAIChatCompletion.cs @@ -11,6 +11,7 @@ using Microsoft.SemanticKernel.AI.ChatCompletion; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion; @@ -23,50 +24,62 @@ public sealed class AzureOpenAIChatCompletion : AzureOpenAIClientBase, IChatComp /// /// Create an instance of the connector with API key auth. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletion( - string modelId, + string deploymentName, string endpoint, string apiKey, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, apiKey, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, apiKey, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } /// /// Create an instance of the connector with AAD auth. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletion( - string modelId, + string deploymentName, string endpoint, TokenCredential credentials, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, credentials, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, credentials, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } /// /// Creates a new client instance using the specified . /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletion( - string modelId, + string deploymentName, OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) : base(modelId, openAIClient, loggerFactory) + string? modelId = null, + ILoggerFactory? loggerFactory = null) : base(deploymentName, openAIClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// public Task> GetChatCompletionsAsync( ChatHistory chat, diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/OpenAIChatCompletion.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/OpenAIChatCompletion.cs index a54acfd2fd89..ce84c8f4b22f 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/OpenAIChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletion/OpenAIChatCompletion.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel.AI.ChatCompletion; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion; @@ -34,6 +35,8 @@ public OpenAIChatCompletion( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) : base(modelId, apiKey, organization, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); + this.AddAttribute(OrganizationKey, organization!); } /// @@ -47,8 +50,12 @@ public OpenAIChatCompletion( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) : base(modelId, openAIClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// public Task> GetChatCompletionsAsync( ChatHistory chat, diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithData.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithData.cs index 2c561d3aedc1..83d38df59595 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithData.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithData.cs @@ -16,6 +16,7 @@ using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletionWithData; @@ -43,8 +44,12 @@ public AzureOpenAIChatCompletionWithData( this._httpClient = httpClient ?? new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); this._logger = loggerFactory is not null ? loggerFactory.CreateLogger(this.GetType()) : NullLogger.Instance; + this._attributes.Add(IAIServiceExtensions.ModelIdKey, config.CompletionModelId); } + /// + public IReadOnlyDictionary Attributes => this._attributes; + /// public ChatHistory CreateNewChat(string? instructions = null) { @@ -121,7 +126,7 @@ public async IAsyncEnumerable GetStreamingCompletionsAsync private readonly HttpClient _httpClient; private readonly ILogger _logger; - + private readonly Dictionary _attributes = new(); private void ValidateConfig(AzureOpenAIChatCompletionWithDataConfig config) { Verify.NotNull(config); diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/CustomClient/OpenAIClientBase.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/CustomClient/OpenAIClientBase.cs index 1a098ceac4e3..fb280ae4ae00 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/CustomClient/OpenAIClientBase.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/CustomClient/OpenAIClientBase.cs @@ -12,6 +12,7 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ImageGeneration; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient; @@ -19,6 +20,11 @@ namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient; /// Base type for OpenAI clients. public abstract class OpenAIClientBase { + /// + /// Key used to store the organizarion in the dictionary. + /// + public const string OrganizationKey = "Organization"; + /// /// Initializes a new instance of the class. /// @@ -30,6 +36,11 @@ private protected OpenAIClientBase(HttpClient? httpClient, ILoggerFactory? logge this._logger = loggerFactory is not null ? loggerFactory.CreateLogger(this.GetType()) : NullLogger.Instance; } + /// + /// Storage for AI service attributes. + /// + private protected Dictionary InternalAttributes = new(); + /// Adds headers to use for OpenAI HTTP requests. private protected virtual void AddRequestHeaders(HttpRequestMessage request) { @@ -75,6 +86,19 @@ private protected async Task> ExecuteImageGenerationRequestAsync( return result.Images.Select(extractResponseFunc).ToList(); } + /// + /// Add attribute to the internal attribute dictionary if the value is not null or empty. + /// + /// Attribute key + /// Attribute value + private protected void AddAttribute(string key, string? value) + { + if (!string.IsNullOrEmpty(value)) + { + this.InternalAttributes.Add(key, value!); + } + } + #region private ================================================================================ /// diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/AzureOpenAIImageGeneration.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/AzureOpenAIImageGeneration.cs index dc167ae05210..5b25f44ac881 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/AzureOpenAIImageGeneration.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/AzureOpenAIImageGeneration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; @@ -9,6 +10,7 @@ using Microsoft.SemanticKernel.AI.ImageGeneration; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.ImageGeneration; @@ -68,6 +70,7 @@ public AzureOpenAIImageGeneration(string endpoint, string apiKey, HttpClient? ht this._apiKey = apiKey; this._maxRetryCount = maxRetryCount; this._apiVersion = apiVersion; + this.AddAttribute(IAIServiceExtensions.EndpointKey, endpoint); } /// @@ -96,8 +99,13 @@ public AzureOpenAIImageGeneration(string apiKey, HttpClient httpClient, string? this._apiKey = apiKey; this._maxRetryCount = maxRetryCount; this._apiVersion = apiVersion; + this.AddAttribute(IAIServiceExtensions.EndpointKey, endpoint); + this.AddAttribute(IAIServiceExtensions.ApiVersionKey, apiVersion); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// public async Task GenerateImageAsync(string description, int width, int height, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/OpenAIImageGeneration.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/OpenAIImageGeneration.cs index 7bd0c1bc2199..359ef19b4cb4 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/OpenAIImageGeneration.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/ImageGeneration/OpenAIImageGeneration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Threading; @@ -49,8 +50,13 @@ public OpenAIImageGeneration( Verify.NotNullOrWhiteSpace(apiKey); this._authorizationHeaderValue = $"Bearer {apiKey}"; this._organizationHeaderValue = organization; + + this.AddAttribute(OrganizationKey, organization!); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// Adds headers to use for OpenAI HTTP requests. private protected override void AddRequestHeaders(HttpRequestMessage request) { diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs index adb93afed226..b47bbacde451 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs @@ -39,6 +39,7 @@ public static class OpenAIKernelBuilderExtensions /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -47,13 +48,14 @@ public static KernelBuilder WithAzureTextCompletionService(this KernelBuilder bu string endpoint, string apiKey, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { builder.WithAIService(serviceId, (loggerFactory, httpHandlerFactory) => { var client = CreateAzureOpenAIClient(loggerFactory, httpHandlerFactory, deploymentName, endpoint, new AzureKeyCredential(apiKey), httpClient); - return new AzureTextCompletion(deploymentName, client, loggerFactory); + return new AzureTextCompletion(deploymentName, client, modelId, loggerFactory); }, setAsDefault); return builder; @@ -68,6 +70,7 @@ public static KernelBuilder WithAzureTextCompletionService(this KernelBuilder bu /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -76,13 +79,14 @@ public static KernelBuilder WithAzureTextCompletionService(this KernelBuilder bu string endpoint, TokenCredential credentials, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { builder.WithAIService(serviceId, (loggerFactory, httpHandlerFactory) => { var client = CreateAzureOpenAIClient(loggerFactory, httpHandlerFactory, deploymentName, endpoint, credentials, httpClient); - return new AzureTextCompletion(deploymentName, client, loggerFactory); + return new AzureTextCompletion(deploymentName, client, modelId, loggerFactory); }, setAsDefault); return builder; @@ -96,18 +100,21 @@ public static KernelBuilder WithAzureTextCompletionService(this KernelBuilder bu /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether the service should be the default for its type. /// Self instance public static KernelBuilder WithAzureTextCompletionService(this KernelBuilder builder, string deploymentName, OpenAIClient openAIClient, string? serviceId = null, + string? modelId = null, bool setAsDefault = false) { builder.WithAIService(serviceId, (loggerFactory) => new AzureTextCompletion( deploymentName, openAIClient, + modelId, loggerFactory), setAsDefault); @@ -158,6 +165,7 @@ public static KernelBuilder WithOpenAITextCompletionService(this KernelBuilder b /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -166,6 +174,7 @@ public static KernelBuilder WithAzureOpenAITextEmbeddingGenerationService(this K string endpoint, string apiKey, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -174,6 +183,7 @@ public static KernelBuilder WithAzureOpenAITextEmbeddingGenerationService(this K deploymentName, endpoint, apiKey, + modelId, HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory), loggerFactory), setAsDefault); @@ -189,6 +199,7 @@ public static KernelBuilder WithAzureOpenAITextEmbeddingGenerationService(this K /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -197,6 +208,7 @@ public static KernelBuilder WithAzureOpenAITextEmbeddingGenerationService(this K string endpoint, TokenCredential credential, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -205,6 +217,7 @@ public static KernelBuilder WithAzureOpenAITextEmbeddingGenerationService(this K deploymentName, endpoint, credential, + modelId, HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory), loggerFactory), setAsDefault); @@ -256,6 +269,7 @@ public static KernelBuilder WithOpenAITextEmbeddingGenerationService(this Kernel /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Whether to use the service also for text completion, if supported /// A local identifier for the given AI service + /// Model identifier /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -265,6 +279,7 @@ public static KernelBuilder WithAzureOpenAIChatCompletionService(this KernelBuil string apiKey, bool alsoAsTextCompletion = true, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -272,7 +287,7 @@ AzureOpenAIChatCompletion Factory(ILoggerFactory loggerFactory, IDelegatingHandl { OpenAIClient client = CreateAzureOpenAIClient(loggerFactory, httpHandlerFactory, deploymentName, endpoint, new AzureKeyCredential(apiKey), httpClient); - return new(deploymentName, client, loggerFactory); + return new(deploymentName, client, modelId, loggerFactory); }; builder.WithAIService(serviceId, Factory, setAsDefault); @@ -296,6 +311,7 @@ AzureOpenAIChatCompletion Factory(ILoggerFactory loggerFactory, IDelegatingHandl /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// Whether to use the service also for text completion, if supported /// A local identifier for the given AI service + /// Model identifier /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -305,6 +321,7 @@ public static KernelBuilder WithAzureOpenAIChatCompletionService(this KernelBuil TokenCredential credentials, bool alsoAsTextCompletion = true, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -312,7 +329,7 @@ AzureOpenAIChatCompletion Factory(ILoggerFactory loggerFactory, IDelegatingHandl { OpenAIClient client = CreateAzureOpenAIClient(loggerFactory, httpHandlerFactory, deploymentName, endpoint, credentials, httpClient); - return new(deploymentName, client, loggerFactory); + return new(deploymentName, client, modelId, loggerFactory); }; builder.WithAIService(serviceId, Factory, setAsDefault); @@ -408,6 +425,7 @@ public static KernelBuilder WithOpenAIChatCompletionService(this KernelBuilder b /// Custom for HTTP requests. /// Whether to use the service also for text completion, if supported /// A local identifier for the given AI service + /// Model identifier /// Whether the service should be the default for its type. /// Self instance public static KernelBuilder WithAzureOpenAIChatCompletionService(this KernelBuilder builder, @@ -415,11 +433,12 @@ public static KernelBuilder WithAzureOpenAIChatCompletionService(this KernelBuil OpenAIClient openAIClient, bool alsoAsTextCompletion = true, string? serviceId = null, + string? modelId = null, bool setAsDefault = false) { AzureOpenAIChatCompletion Factory(ILoggerFactory loggerFactory) { - return new(deploymentName, openAIClient, loggerFactory); + return new(deploymentName, openAIClient, modelId, loggerFactory); }; builder.WithAIService(serviceId, Factory, setAsDefault); diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIMemoryBuilderExtensions.cs index f993831b8e35..6fe4e3ed8ee8 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIMemoryBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIMemoryBuilderExtensions.cs @@ -21,6 +21,7 @@ public static class OpenAIMemoryBuilderExtensions /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// A local identifier for the given AI service + /// Model identifier /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -30,6 +31,7 @@ public static MemoryBuilder WithAzureOpenAITextEmbeddingGenerationService( string endpoint, string apiKey, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -38,6 +40,7 @@ public static MemoryBuilder WithAzureOpenAITextEmbeddingGenerationService( deploymentName, endpoint, apiKey, + modelId, HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory), loggerFactory)); @@ -53,6 +56,7 @@ public static MemoryBuilder WithAzureOpenAITextEmbeddingGenerationService( /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// A local identifier for the given AI service + /// Model identifier /// Whether the service should be the default for its type. /// Custom for HTTP requests. /// Self instance @@ -62,6 +66,7 @@ public static MemoryBuilder WithAzureOpenAITextEmbeddingGenerationService( string endpoint, TokenCredential credential, string? serviceId = null, + string? modelId = null, bool setAsDefault = false, HttpClient? httpClient = null) { @@ -70,6 +75,7 @@ public static MemoryBuilder WithAzureOpenAITextEmbeddingGenerationService( deploymentName, endpoint, credential, + modelId, HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory), loggerFactory)); diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/AzureTextCompletion.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/AzureTextCompletion.cs index 2a549835a04a..99e1faded63d 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/AzureTextCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/AzureTextCompletion.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion; @@ -22,50 +23,62 @@ public sealed class AzureTextCompletion : AzureOpenAIClientBase, ITextCompletion /// /// Creates a new AzureTextCompletion client instance using API Key auth /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureTextCompletion( - string modelId, + string deploymentName, string endpoint, string apiKey, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, apiKey, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, apiKey, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } /// /// Creates a new AzureTextCompletion client instance supporting AAD auth /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureTextCompletion( - string modelId, + string deploymentName, string endpoint, TokenCredential credential, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, credential, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, credential, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } /// /// Creates a new AzureTextCompletion client instance using the specified OpenAIClient /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. public AzureTextCompletion( - string modelId, + string deploymentName, OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) : base(modelId, openAIClient, loggerFactory) + string? modelId = null, + ILoggerFactory? loggerFactory = null) : base(deploymentName, openAIClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// public IAsyncEnumerable GetStreamingCompletionsAsync( string text, diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/OpenAITextCompletion.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/OpenAITextCompletion.cs index 9394be351f4d..a49f3128b958 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/OpenAITextCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextCompletion/OpenAITextCompletion.cs @@ -8,6 +8,7 @@ using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion; @@ -33,8 +34,13 @@ public OpenAITextCompletion( ILoggerFactory? loggerFactory = null ) : base(modelId, apiKey, organization, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); + this.AddAttribute(OrganizationKey, organization!); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// public IAsyncEnumerable GetStreamingCompletionsAsync( string text, diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGeneration.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGeneration.cs index c3ca536c8aed..cb759e88c589 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGeneration.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGeneration.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AI.Embeddings; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; @@ -20,37 +21,46 @@ public sealed class AzureOpenAITextEmbeddingGeneration : AzureOpenAIClientBase, /// /// Creates a new client instance using API Key auth. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureOpenAITextEmbeddingGeneration( - string modelId, + string deploymentName, string endpoint, string apiKey, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, apiKey, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, apiKey, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } /// /// Creates a new client instance supporting AAD auth. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public AzureOpenAITextEmbeddingGeneration( - string modelId, + string deploymentName, string endpoint, TokenCredential credential, + string? modelId = null, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) : base(modelId, endpoint, credential, httpClient, loggerFactory) + ILoggerFactory? loggerFactory = null) : base(deploymentName, endpoint, credential, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// /// Generates an embedding from the given . /// diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/OpenAITextEmbeddingGeneration.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/OpenAITextEmbeddingGeneration.cs index 1d19b0b546d5..af409f0bd4f5 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/OpenAITextEmbeddingGeneration.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/TextEmbedding/OpenAITextEmbeddingGeneration.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AI.Embeddings; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; @@ -32,8 +33,12 @@ public OpenAITextEmbeddingGeneration( ILoggerFactory? loggerFactory = null ) : base(modelId, apiKey, organization, httpClient, loggerFactory) { + this.AddAttribute(IAIServiceExtensions.ModelIdKey, modelId); } + /// + public IReadOnlyDictionary Attributes => this.InternalAttributes; + /// /// Generates an embedding from the given . /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Kusto/KustoMemoryRecord.cs b/dotnet/src/Connectors/Connectors.Memory.Kusto/KustoMemoryRecord.cs index a7e8783f06f1..d0f423a51ce3 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Kusto/KustoMemoryRecord.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Kusto/KustoMemoryRecord.cs @@ -19,7 +19,7 @@ public sealed class KustoMemoryRecord public string Key { get; set; } /// - /// Metadata associated with memory entity. + /// Attributes associated with memory entity. /// public MemoryRecordMetadata Metadata { get; set; } @@ -44,7 +44,7 @@ public KustoMemoryRecord(MemoryRecord record) : this(record.Key, record.Metadata /// Initializes a new instance of the class. /// /// Entity key. - /// Metadata associated with memory entity. + /// Attributes associated with memory entity. /// Source content embedding. /// Optional timestamp. public KustoMemoryRecord(string key, MemoryRecordMetadata metadata, ReadOnlyMemory embedding, DateTimeOffset? timestamp = null) diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresMemoryEntry.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresMemoryEntry.cs index a7429b44c157..5b019bc3edfd 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresMemoryEntry.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresMemoryEntry.cs @@ -16,7 +16,7 @@ public record struct PostgresMemoryEntry public string Key { get; set; } /// - /// Metadata as a string. + /// Attributes as a string. /// public string MetadataString { get; set; } diff --git a/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs b/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs index 8f4d4d22f25c..50c4b2568fdc 100644 --- a/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs +++ b/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs @@ -69,6 +69,10 @@ public void Dispose() private sealed class RedirectTextCompletion : ITextCompletion { + public string? ModelId => null; + + public IReadOnlyDictionary Attributes => new Dictionary(); + Task> ITextCompletion.GetCompletionsAsync(string text, AIRequestSettings? requestSettings, CancellationToken cancellationToken) { return Task.FromResult>(new List { new RedirectTextCompletionResult(text) }); diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/IAIService.cs b/dotnet/src/SemanticKernel.Abstractions/Services/IAIService.cs index 54085c6f5b4b..05e9619f909d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Services/IAIService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Services/IAIService.cs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; namespace Microsoft.SemanticKernel.Services; /// /// Represents an empty interface for AI services. /// -[SuppressMessage("Design", "CA1040:Avoid empty interfaces")] public interface IAIService { + /// + /// Gets the AI service attributes. + /// + IReadOnlyDictionary Attributes { get; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceExtensions.cs new file mode 100644 index 000000000000..26ab85185afa --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Services; + +/// +/// Extension methods for . +/// +public static class IAIServiceExtensions +{ + /// + /// Key used to store the model identifier in the dictionary. + /// + public const string ModelIdKey = "ModelId"; + + /// + /// Key used to store the endpoint key in the dictionary. + /// + public const string EndpointKey = "Endpoint"; + + /// + /// Key used to store the API version in the dictionary. + /// + public const string ApiVersionKey = "ApiVersion"; + + /// + /// Gets the model identifier. + /// + /// + /// + public static string? GetModelId(this IAIService service) + { + return service.GetAttribute(ModelIdKey); + } + + /// + /// Gets the endpoint. + /// + /// + /// + public static string? GetEndpoint(this IAIService service) + { + return service.GetAttribute(EndpointKey); + } + + /// + /// Gets the API version. + /// + /// + /// + public static string? GetApiVersion(this IAIService service) + { + return service.GetAttribute(ApiVersionKey); + } + + /// + /// Gets the specified attribute. + /// + /// + /// + /// + public static string? GetAttribute(this IAIService service, string key) + { + return service.Attributes?.TryGetValue(key, out var value) == true ? value as string : null; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/INamedServiceProvider.cs b/dotnet/src/SemanticKernel.Abstractions/Services/INamedServiceProvider.cs index 2cb9263f8b86..ed003cd2e360 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Services/INamedServiceProvider.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Services/INamedServiceProvider.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; + namespace Microsoft.SemanticKernel.Services; /// @@ -15,4 +17,11 @@ public interface INamedServiceProvider /// The name of the service, or null for the default service. /// The service instance, or null if not found. T? GetService(string? name = null) where T : TService; + + /// + /// Gets all services of the specified type, or an empty collection of none are found. + /// + /// The type of the service. + /// Collection of services of the specified type, or an empty collection of none are found + ICollection GetServices() where T : TService; } diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index fa9dec0266d6..fb2e617c5177 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Events; -using Microsoft.SemanticKernel.Functions; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; diff --git a/dotnet/src/SemanticKernel.Core/Services/AIServiceSelectorBase.cs b/dotnet/src/SemanticKernel.Core/Services/AIServiceSelectorBase.cs new file mode 100644 index 000000000000..e4eb0540856e --- /dev/null +++ b/dotnet/src/SemanticKernel.Core/Services/AIServiceSelectorBase.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace - Using the main namespace +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel; +#pragma warning restore IDE0130 + +/// +/// Base class for implementing . +/// +public abstract class AIServiceSelectorBase : IAIServiceSelector +{ + /// + public (T?, AIRequestSettings?) SelectAIService(SKContext context, ISKFunction skfunction) where T : IAIService + { + var services = context.ServiceProvider.GetServices(); + foreach (var service in services) + { + var result = this.SelectAIService(context, skfunction, service); + if (result is not null) + { + return ((T?, AIRequestSettings?))result; + } + } + + throw new SKException($"Valid service of type {typeof(T)} not found."); + } + + /// + /// Return the AI service and requesting settings if the specified provider is the valid choice. + /// + /// + /// + /// + /// Instance of + /// + protected abstract (T?, AIRequestSettings?)? SelectAIService(SKContext context, ISKFunction skfunction, T service) where T : IAIService; +} diff --git a/dotnet/src/SemanticKernel.Core/Services/NamedServiceProvider.cs b/dotnet/src/SemanticKernel.Core/Services/NamedServiceProvider.cs index ba5c903e4cd2..8036fae2a785 100644 --- a/dotnet/src/SemanticKernel.Core/Services/NamedServiceProvider.cs +++ b/dotnet/src/SemanticKernel.Core/Services/NamedServiceProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.SemanticKernel.Services; @@ -56,6 +57,33 @@ public NamedServiceProvider( return null; } + /// + public ICollection GetServices() where T : TService + { + if (typeof(T) == typeof(TService)) + { + return this.GetAllServices(); + } + + if (this._services.TryGetValue(typeof(T), out var namedServices)) + { + return namedServices.Values.Select(f => f.Invoke()).Cast().ToList(); + } + + return Array.Empty(); + } + + private HashSet GetAllServices() + { + HashSet services = new(); + foreach (var namedServices in this._services.Values) + { + services.UnionWith(namedServices.Values.Select(f => f.Invoke()).Cast()); + } + + return services; + } + private Func? GetServiceFactory(string? name = null) where T : TService { // Get the nested dictionary for the service type diff --git a/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs b/dotnet/src/SemanticKernel.Core/Services/OrderedIAIServiceSelector.cs similarity index 72% rename from dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs rename to dotnet/src/SemanticKernel.Core/Services/OrderedIAIServiceSelector.cs index db739fbdb607..3eaf239bd1c3 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs +++ b/dotnet/src/SemanticKernel.Core/Services/OrderedIAIServiceSelector.cs @@ -4,9 +4,8 @@ using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.Services; -namespace Microsoft.SemanticKernel.Functions; +namespace Microsoft.SemanticKernel.Services; /// /// Implementation of that selects the AI service based on the order of the model settings. @@ -40,6 +39,14 @@ internal class OrderedIAIServiceSelector : IAIServiceSelector return (service, model); } } + else if (!string.IsNullOrEmpty(model.ModelId)) + { + var service = this.GetServiceByModelId(serviceProvider, model.ModelId!); + if (service is not null) + { + return (service, model); + } + } else { // First request settings with empty or null service id is the default @@ -60,4 +67,19 @@ internal class OrderedIAIServiceSelector : IAIServiceSelector var names = string.Join("|", modelSettings.Select(model => model.ServiceId).ToArray()); throw new SKException($"Service of type {typeof(T)} and name {names ?? ""} not registered."); } + + private T? GetServiceByModelId(IAIServiceProvider serviceProvider, string modelId) where T : IAIService + { + var services = serviceProvider.GetServices(); + foreach (var service in services) + { + string? serviceModelId = service.GetModelId(); + if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId == modelId) + { + return service; + } + } + + return default; + } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs index 4796660c87ed..a18adcf5cb37 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs @@ -8,7 +8,6 @@ using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Diagnostics; -using Microsoft.SemanticKernel.Functions; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TemplateEngine; using Xunit; @@ -199,10 +198,17 @@ public void ItGetsAIServiceConfigurationByOrder(string[] serviceIds, string expe #region private private sealed class AIService : IAIService { + public IReadOnlyDictionary Attributes => new Dictionary(); + + public string? ModelId { get; } } private sealed class TextCompletion : ITextCompletion { + public IReadOnlyDictionary Attributes => new Dictionary(); + + public string? ModelId { get; } + public Task> GetCompletionsAsync(string text, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) { throw new NotImplementedException(); diff --git a/dotnet/src/SemanticKernel.UnitTests/Services/ServiceRegistryTests.cs b/dotnet/src/SemanticKernel.UnitTests/Services/ServiceRegistryTests.cs index 346d402bbccc..394b977d42ff 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Services/ServiceRegistryTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Services/ServiceRegistryTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using Microsoft.SemanticKernel.Services; using Xunit; @@ -215,5 +216,8 @@ public void ItReturnsFalseIfTryGetServiceWithInvalidName() // A test service implementation private sealed class TestService : IAIService { + public string? ModelId { get; } + + public IReadOnlyDictionary Attributes => new Dictionary(); } }