diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7846a1811d9a..9ccb88b1ab0f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -50,6 +50,7 @@ + diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs index e96f1272b32f..4568c79753d6 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs @@ -2,16 +2,22 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.ChatCompletion; /// /// Class sponsor that holds extension methods for interface. /// -public static class ChatCompletionServiceExtensions +public static partial class ChatCompletionServiceExtensions { /// /// Get chat multiple chat message content choices for the prompt and settings. @@ -110,4 +116,614 @@ public static IAsyncEnumerable GetStreamingChatMess return chatCompletionService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); } + + /// Creates an for the specified . + /// The chat completion service to be represented as a chat client. + /// + /// The . If the is an , the + /// will be returned. Otherwise, a new will be created that wraps . + /// + [Experimental("SKEXP0001")] + public static IChatClient AsChatClient(this IChatCompletionService service) + { + Verify.NotNull(service); + + return service is IChatClient chatClient ? + chatClient : + new ChatCompletionServiceChatClient(service); + } + + // TODO: We can also add AsChatClient overloads for ITextToImageService, ITextToAudioService, IImageToTextService, and IAudioToTextService. + // All of these operate on the same input and output content types already supported by the more general IChatClient interface. + // Decide if we want to. + + /// Creates an for the specified . + /// The chat client to be represented as a chat completion service. + /// An optional that can be used to resolve services to use in the instance. + /// + /// The . If is an , will + /// be returned. Otherwise, a new will be created that wraps . + /// + [Experimental("SKEXP0001")] + public static IChatCompletionService AsChatCompletionService(this IChatClient client, IServiceProvider? serviceProvider = null) + { + Verify.NotNull(client); + + return client is IChatCompletionService chatCompletionService ? + chatCompletionService : + new ChatClientChatCompletionService(client, serviceProvider); + } + + /// Provides an implementation of around an arbitrary . + private sealed class ChatCompletionServiceChatClient : IChatClient + { + /// The wrapped . + private readonly IChatCompletionService _chatCompletionService; + + /// Initializes the for . + public ChatCompletionServiceChatClient(IChatCompletionService chatCompletionService) + { + Verify.NotNull(chatCompletionService); + + this._chatCompletionService = chatCompletionService; + + this.Metadata = new ChatClientMetadata( + chatCompletionService.GetType().Name, + chatCompletionService.GetEndpoint() is string endpoint ? new Uri(endpoint) : null, + chatCompletionService.GetModelId()); + } + + /// + public ChatClientMetadata Metadata { get; } + + /// + public async Task CompleteAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(chatMessages); + + var response = await this._chatCompletionService.GetChatMessageContentAsync( + new ChatHistory(chatMessages.Select(ToChatMessageContent)), + ToPromptExecutionSettings(options), + kernel: null, + cancellationToken).ConfigureAwait(false); + + return new(ToChatMessage(response)) + { + ModelId = response.ModelId, + RawRepresentation = response.InnerContent, + }; + } + + /// + public async IAsyncEnumerable CompleteStreamingAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(chatMessages); + + await foreach (var update in this._chatCompletionService.GetStreamingChatMessageContentsAsync( + new ChatHistory(chatMessages.Select(ToChatMessageContent)), + ToPromptExecutionSettings(options), + kernel: null, + cancellationToken).ConfigureAwait(false)) + { + yield return ToStreamingChatCompletionUpdate(update); + } + } + + /// + public void Dispose() + { + (this._chatCompletionService as IDisposable)?.Dispose(); + } + + /// + public TService? GetService(object? key = null) where TService : class + { + return + typeof(TService) == typeof(IChatClient) ? (TService)(object)this : + this._chatCompletionService as TService; + } + } + + /// Provides an implementation of around an . + private sealed class ChatClientChatCompletionService : IChatCompletionService + { + /// The wrapped . + private readonly IChatClient _chatClient; + + /// Initializes the for . + public ChatClientChatCompletionService(IChatClient chatClient, IServiceProvider? serviceProvider) + { + Verify.NotNull(chatClient); + + // Store the client. + this._chatClient = chatClient; + + // Initialize the attributes. + var attrs = new Dictionary(); + this.Attributes = new ReadOnlyDictionary(attrs); + + var metadata = chatClient.Metadata; + if (metadata.ProviderUri is not null) + { + attrs[AIServiceExtensions.EndpointKey] = metadata.ProviderUri.ToString(); + } + if (metadata.ModelId is not null) + { + attrs[AIServiceExtensions.ModelIdKey] = metadata.ModelId; + } + } + + /// + public IReadOnlyDictionary Attributes { get; } + + /// + public async Task> GetChatMessageContentsAsync( + ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(chatHistory); + + var completion = await this._chatClient.CompleteAsync( + chatHistory.Select(ToChatMessage).ToList(), + ToChatOptions(executionSettings, kernel), + cancellationToken).ConfigureAwait(false); + + return completion.Choices.Select(ToChatMessageContent).ToList(); + } + + /// + public async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(chatHistory); + + await foreach (var update in this._chatClient.CompleteStreamingAsync( + chatHistory.Select(ToChatMessage).ToList(), + ToChatOptions(executionSettings, kernel), + cancellationToken).ConfigureAwait(false)) + { + yield return ToStreamingChatMessageContent(update); + } + } + } + + /// Converts a pair of and to a . + private static ChatOptions? ToChatOptions(PromptExecutionSettings? settings, Kernel? kernel) + { + if (settings is null && kernel is null) + { + return null; + } + + ChatOptions options = new(); + + if (settings is not null) + { + if (settings.GetType() != typeof(PromptExecutionSettings)) + { + // If the settings are of a derived type, serialize it and deserialize it as the base in order to try + // to get the derived strongly-typed properties to show up in the loosely-typed properties dictionary. + settings = JsonSerializer.Deserialize( + JsonSerializer.Serialize(settings, AbstractionsJsonContext.Default.PromptExecutionSettings), + AbstractionsJsonContext.Default.PromptExecutionSettings); + } + + if (settings!.ExtensionData is IDictionary extensionData) + { + foreach (var entry in extensionData) + { + if (entry.Key.Equals("temperature", StringComparison.OrdinalIgnoreCase) && + entry.Value is float temperature) + { + options.Temperature = temperature; + } + else if (entry.Key.Equals("top_p", StringComparison.OrdinalIgnoreCase) && + entry.Value is float top_p) + { + options.TopP = top_p; + } + else if (entry.Key.Equals("max_tokens", StringComparison.OrdinalIgnoreCase) && + entry.Value is int maxTokens) + { + options.MaxOutputTokens = maxTokens; + } + else if (entry.Key.Equals("stop_sequences", StringComparison.OrdinalIgnoreCase) && + entry.Value is IList stopSequences) + { + options.StopSequences = stopSequences; + } + else if (entry.Key.Equals("frequency_penalty", StringComparison.OrdinalIgnoreCase) && + entry.Value is float frequencyPenalty) + { + options.FrequencyPenalty = frequencyPenalty; + } + else if (entry.Key.Equals("presence_penalty", StringComparison.OrdinalIgnoreCase) && + entry.Value is float presence_penalty) + { + options.PresencePenalty = presence_penalty; + } + else if (entry.Key.Equals("response_format", StringComparison.OrdinalIgnoreCase) && + entry.Value is { } responseFormat) + { + options.ResponseFormat = responseFormat switch + { + "text" => ChatResponseFormat.Text, + "json" => ChatResponseFormat.Json, + JsonElement e => ChatResponseFormat.ForJsonSchema(e.ToString()), + _ => null, + }; + } + else + { + (options.AdditionalProperties ??= [])[entry.Key] = entry.Value; + } + } + } + + if (settings.FunctionChoiceBehavior?.GetConfiguration(new([]) { Kernel = kernel }).Functions is { Count: > 0 } functions) + { + options.ToolMode = settings.FunctionChoiceBehavior is RequiredFunctionChoiceBehavior ? ChatToolMode.RequireAny : ChatToolMode.Auto; + options.Tools = functions.Select(f => f.AsAIFunction(kernel)).Cast().ToList(); + } + } + + return options; + } + + /// Converts a to a . + private static PromptExecutionSettings? ToPromptExecutionSettings(ChatOptions? options) + { + if (options is null) + { + return null; + } + + PromptExecutionSettings settings = new() + { + ExtensionData = new Dictionary(StringComparer.OrdinalIgnoreCase) + }; + + // Transfer over all strongly-typed members of ChatOptions. We do not know the exact name the derived PromptExecutionSettings + // will pick for these options, so we just use the most common choice for each. (We could make this more exact by having an + // IPromptExecutionSettingsFactory interface with a method like `PromptExecutionSettings Create(ChatOptions options)`; that + // interface could then optionally be implemented by an IChatCompletionService, and this implementation could just ask the + // chat completion service to produce the PromptExecutionSettings it wants. But, this is already a problem + // with PromptExecutionSettings, regardless of ChatOptions... someone creating a PES without knowing what backend is being + // used has to guess at the names to use.) + + if (options.Temperature is not null) + { + settings.ExtensionData["temperature"] = options.Temperature.Value; + } + + if (options.MaxOutputTokens is not null) + { + settings.ExtensionData["max_tokens"] = options.MaxOutputTokens.Value; + } + + if (options.FrequencyPenalty is not null) + { + settings.ExtensionData["frequency_penalty"] = options.FrequencyPenalty.Value; + } + + if (options.PresencePenalty is not null) + { + settings.ExtensionData["presence_penalty"] = options.PresencePenalty.Value; + } + + if (options.StopSequences is not null) + { + settings.ExtensionData["stop_sequences"] = options.StopSequences; + } + + if (options.TopP is not null) + { + settings.ExtensionData["top_p"] = options.TopP.Value; + } + + if (options.TopK is not null) + { + settings.ExtensionData["top_k"] = options.TopK.Value; + } + + if (options.ResponseFormat is not null) + { + if (options.ResponseFormat is ChatResponseFormatText) + { + settings.ExtensionData["response_format"] = "text"; + } + else if (options.ResponseFormat is ChatResponseFormatJson json) + { + // There's no established way to pass response format with a schema via PromptExecutionSettings. + // For now, we'll use a JsonElement as the value. + settings.ExtensionData["response_format"] = json.Schema is not null ? + JsonSerializer.Deserialize(json.Schema, AbstractionsJsonContext.Default.JsonElement) : + "json"; + } + } + + // Transfer over loosely-typed members of ChatOptions. + + if (options.AdditionalProperties is not null) + { + foreach (var kvp in options.AdditionalProperties) + { + if (kvp.Value is not null) + { + settings.ExtensionData[kvp.Key] = kvp.Value; + } + } + } + + // Transfer over tools. For IChatClient, we do not want automatic invocation, as that's a concern left up to + // components like FunctionInvocationChatClient. As such, based on the tool mode, we map to the appropriate + // FunctionChoiceBehavior, but always with autoInvoke: false. + + if (options.Tools is { Count: > 0 }) + { + var functions = options.Tools.OfType().Select(f => new AIFunctionKernelFunction(f)); + settings.FunctionChoiceBehavior = + options.ToolMode is null or AutoChatToolMode ? FunctionChoiceBehavior.Auto(functions, autoInvoke: false) : + options.ToolMode is RequiredChatToolMode { RequiredFunctionName: null } ? FunctionChoiceBehavior.Required(functions, autoInvoke: false) : + options.ToolMode is RequiredChatToolMode { RequiredFunctionName: string functionName } ? FunctionChoiceBehavior.Required(functions.Where(f => f.Name == functionName), autoInvoke: false) : + null; + } + + return settings; + } + + /// Converts a to a . + /// This conversion should not be necessary once SK eventually adopts the shared content types. + private static ChatMessage ToChatMessage(ChatMessageContent content) + { + ChatMessage message = new(); + + foreach (var item in content.Items) + { + AIContent? aiContent = null; + switch (item) + { + case Microsoft.SemanticKernel.TextContent tc: + aiContent = new Microsoft.Extensions.AI.TextContent(tc.Text); + break; + + case Microsoft.SemanticKernel.ImageContent ic: + aiContent = + ic.DataUri is not null ? new Microsoft.Extensions.AI.ImageContent(ic.DataUri, ic.MimeType) : + ic.Uri is not null ? new Microsoft.Extensions.AI.ImageContent(ic.Uri, ic.MimeType) : + null; + break; + + case Microsoft.SemanticKernel.AudioContent ac: + aiContent = + ac.DataUri is not null ? new Microsoft.Extensions.AI.AudioContent(ac.DataUri, ac.MimeType) : + ac.Uri is not null ? new Microsoft.Extensions.AI.AudioContent(ac.Uri, ac.MimeType) : + null; + break; + + case Microsoft.SemanticKernel.BinaryContent bc: + aiContent = + bc.DataUri is not null ? new Microsoft.Extensions.AI.DataContent(bc.DataUri, bc.MimeType) : + bc.Uri is not null ? new Microsoft.Extensions.AI.DataContent(bc.Uri, bc.MimeType) : + null; + break; + + case Microsoft.SemanticKernel.FunctionCallContent fcc: + aiContent = new Microsoft.Extensions.AI.FunctionCallContent(fcc.Id ?? string.Empty, fcc.FunctionName, fcc.Arguments); + break; + + case Microsoft.SemanticKernel.FunctionResultContent frc: + aiContent = new Microsoft.Extensions.AI.FunctionResultContent(frc.CallId ?? string.Empty, frc.FunctionName ?? string.Empty, frc.Result); + break; + } + + if (aiContent is not null) + { + aiContent.RawRepresentation = content.InnerContent; + aiContent.AdditionalProperties = content.Metadata is not null ? new(content.Metadata) : null; + + message.Contents.Add(aiContent); + } + } + + return message; + } + + /// Converts a to a . + /// This conversion should not be necessary once SK eventually adopts the shared content types. + private static ChatMessageContent ToChatMessageContent(ChatMessage message) + { + ChatMessageContent result = new() + { + InnerContent = message, + AuthorName = message.AuthorName, + Role = new AuthorRole(message.Role.Value), + Metadata = message.AdditionalProperties + }; + + foreach (AIContent content in message.Contents) + { + KernelContent? resultContent = null; + switch (content) + { + case Microsoft.Extensions.AI.TextContent tc: + resultContent = new Microsoft.SemanticKernel.TextContent(tc.Text); + break; + + case Microsoft.Extensions.AI.ImageContent ic: + resultContent = ic.ContainsData ? + new Microsoft.SemanticKernel.ImageContent(ic.Uri) : + new Microsoft.SemanticKernel.ImageContent(new Uri(ic.Uri)); + break; + + case Microsoft.Extensions.AI.AudioContent ac: + resultContent = ac.ContainsData ? + new Microsoft.SemanticKernel.AudioContent(ac.Uri) : + new Microsoft.SemanticKernel.AudioContent(new Uri(ac.Uri)); + break; + + case Microsoft.Extensions.AI.DataContent dc: + resultContent = dc.ContainsData ? + new Microsoft.SemanticKernel.BinaryContent(dc.Uri) : + new Microsoft.SemanticKernel.BinaryContent(new Uri(dc.Uri)); + break; + + case Microsoft.Extensions.AI.FunctionCallContent fcc: + resultContent = new Microsoft.SemanticKernel.FunctionCallContent(fcc.Name, null, fcc.CallId, fcc.Arguments is not null ? new(fcc.Arguments) : null); + break; + + case Microsoft.Extensions.AI.FunctionResultContent frc: + resultContent = new Microsoft.SemanticKernel.FunctionResultContent(frc.Name, null, frc.CallId, frc.Result); + break; + } + + if (resultContent is not null) + { + resultContent.Metadata = content.AdditionalProperties; + resultContent.InnerContent = content.RawRepresentation; + + result.Items.Add(resultContent); + } + } + + return result; + } + + /// Converts a to a . + /// This conversion should not be necessary once SK eventually adopts the shared content types. + private static StreamingChatCompletionUpdate ToStreamingChatCompletionUpdate(StreamingChatMessageContent content) + { + StreamingChatCompletionUpdate update = new() + { + AdditionalProperties = content.Metadata is not null ? new AdditionalPropertiesDictionary(content.Metadata) : null, + AuthorName = content.AuthorName, + ChoiceIndex = content.ChoiceIndex, + ModelId = content.ModelId, + Role = content.Role is not null ? new ChatRole(content.Role.Value.Label) : null, + }; + + foreach (var item in content.Items) + { + AIContent? aiContent = null; + switch (item) + { + case Microsoft.SemanticKernel.StreamingTextContent tc: + aiContent = new Microsoft.Extensions.AI.TextContent(tc.Text); + break; + + case Microsoft.SemanticKernel.StreamingFunctionCallUpdateContent fcc: + aiContent = new Microsoft.Extensions.AI.FunctionCallContent( + fcc.CallId ?? string.Empty, + fcc.Name ?? string.Empty, + fcc.Arguments is not null ? JsonSerializer.Deserialize>(fcc.Arguments, AbstractionsJsonContext.Default.IDictionaryStringObject!) : null); + break; + } + + if (aiContent is not null) + { + aiContent.RawRepresentation = content; + + update.Contents.Add(aiContent); + } + } + + return update; + } + + /// Converts a to a . + /// This conversion should not be necessary once SK eventually adopts the shared content types. + private static StreamingChatMessageContent ToStreamingChatMessageContent(StreamingChatCompletionUpdate update) + { + StreamingChatMessageContent content = new( + update.Role is not null ? new AuthorRole(update.Role.Value.Value) : null, + null, + update.RawRepresentation, + update.ChoiceIndex, + metadata: update.AdditionalProperties) + { + ModelId = update.ModelId + }; + + foreach (AIContent item in update.Contents) + { + StreamingKernelContent? resultContent = + item is Microsoft.Extensions.AI.TextContent tc ? new Microsoft.SemanticKernel.StreamingTextContent(tc.Text) : + item is Microsoft.Extensions.AI.FunctionCallContent fcc ? + new Microsoft.SemanticKernel.StreamingFunctionCallUpdateContent(fcc.CallId, fcc.Name, fcc.Arguments is not null ? + JsonSerializer.Serialize(fcc.Arguments!, AbstractionsJsonContext.Default.IDictionaryStringObject!) : + null) : + null; + + if (resultContent is not null) + { + content.Items.Add(resultContent); + } + } + + return content; + } + + /// Provides a that wraps an . + /// + /// The implementation should largely be unused, other than for its . The implementation of + /// only manufactures these to pass along to the underlying + /// with autoInvoke:false, which means the + /// implementation shouldn't be invoking these functions at all. As such, the and + /// methods both unconditionally throw, even though they could be implemented. + /// + private sealed class AIFunctionKernelFunction : KernelFunction + { + private readonly AIFunction _aiFunction; + + public AIFunctionKernelFunction(AIFunction aiFunction) : + base(aiFunction.Metadata.Name, + aiFunction.Metadata.Description, + aiFunction.Metadata.Parameters.Select(p => new KernelParameterMetadata(p.Name, AbstractionsJsonContext.Default.Options) + { + Description = p.Description, + DefaultValue = p.DefaultValue, + IsRequired = p.IsRequired, + ParameterType = p.ParameterType, + Schema = + p.Schema is JsonElement je ? new KernelJsonSchema(je) : + p.Schema is string s ? new KernelJsonSchema(JsonSerializer.Deserialize(s, AbstractionsJsonContext.Default.JsonElement)) : + null, + }).ToList(), + AbstractionsJsonContext.Default.Options, + new KernelReturnParameterMetadata(AbstractionsJsonContext.Default.Options) + { + Description = aiFunction.Metadata.ReturnParameter.Description, + ParameterType = aiFunction.Metadata.ReturnParameter.ParameterType, + Schema = + aiFunction.Metadata.ReturnParameter.Schema is JsonElement je ? new KernelJsonSchema(je) : + aiFunction.Metadata.ReturnParameter.Schema is string s ? new KernelJsonSchema(JsonSerializer.Deserialize(s, AbstractionsJsonContext.Default.JsonElement)) : + null, + }) + { + this._aiFunction = aiFunction; + } + + private AIFunctionKernelFunction(AIFunctionKernelFunction other, string pluginName) : + base(other.Name, pluginName, other.Description, other.Metadata.Parameters, AbstractionsJsonContext.Default.Options, other.Metadata.ReturnParameter) + { + this._aiFunction = other._aiFunction; + } + + public override KernelFunction Clone(string pluginName) + { + Verify.NotNullOrWhiteSpace(pluginName); + return new AIFunctionKernelFunction(this, pluginName); + } + + protected override ValueTask InvokeCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken) + { + // This should never be invoked, as instances are always passed with autoInvoke:false. + throw new NotSupportedException(); + } + + protected override IAsyncEnumerable InvokeStreamingCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken) + { + // This should never be invoked, as instances are always passed with autoInvoke:false. + throw new NotSupportedException(); + } + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs index c09e9a79463d..4195c992bf7a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs @@ -1,10 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Embeddings; @@ -24,7 +28,6 @@ public static class EmbeddingGenerationExtensions /// The containing services, plugins, and other state for use throughout the operation. /// Cancellation token /// A list of embedding structs representing the input . - [Experimental("SKEXP0001")] public static async Task> GenerateEmbeddingAsync( this IEmbeddingGenerationService generator, TValue value, @@ -35,4 +38,125 @@ public static async Task> GenerateEmbeddingAsyncCreates an for the specified . + /// The embedding generation service to be represented as an embedding generator. + /// + /// The . If the is an , + /// the will be returned. Otherwise, a new will be created that wraps the . + /// + public static IEmbeddingGenerator> AsEmbeddingGenerator( + this IEmbeddingGenerationService service) + where TEmbedding : unmanaged + { + Verify.NotNull(service); + + return service is IEmbeddingGenerator> embeddingGenerator ? + embeddingGenerator : + new EmbeddingGenerationServiceEmbeddingGenerator(service); + } + + /// Creates an for the specified . + /// The embedding generator to be represented as an embedding generation service. + /// An optional that can be used to resolve services to use in the instance. + /// + /// The . If the is an , + /// the will be returned. Otherwise, a new will be created that wraps the . + /// + public static IEmbeddingGenerationService AsEmbeddingGenerationService( + this IEmbeddingGenerator> generator, + IServiceProvider? serviceProvider = null) + where TEmbedding : unmanaged + { + Verify.NotNull(generator); + + return generator is IEmbeddingGenerationService service ? + service : + new EmbeddingGeneratorEmbeddingGenerationService(generator, serviceProvider); + } + + /// Provides an implementation of around an . + private sealed class EmbeddingGenerationServiceEmbeddingGenerator : IEmbeddingGenerator> + where TEmbedding : unmanaged + { + /// The wrapped + private readonly IEmbeddingGenerationService _service; + + /// Initializes the for . + public EmbeddingGenerationServiceEmbeddingGenerator(IEmbeddingGenerationService service) + { + this._service = service; + this.Metadata = new EmbeddingGeneratorMetadata( + service.GetType().Name, + service.GetEndpoint() is string endpoint ? new Uri(endpoint) : null, + service.GetModelId()); + } + + /// + public EmbeddingGeneratorMetadata Metadata { get; } + + /// + public void Dispose() + { + (this._service as IDisposable)?.Dispose(); + } + + /// + public async Task>> GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + IList> result = await this._service.GenerateEmbeddingsAsync(values.ToList(), kernel: null, cancellationToken).ConfigureAwait(false); + return new(result.Select(e => new Embedding(e))); + } + + /// + public TService? GetService(object? key = null) where TService : class + { + return + typeof(TService) == typeof(IEmbeddingGenerator>) ? (TService)(object)this : + this._service as TService; + } + } + + /// Provides an implementation of around an . + private sealed class EmbeddingGeneratorEmbeddingGenerationService : IEmbeddingGenerationService + where TEmbedding : unmanaged + { + /// The wrapped + private readonly IEmbeddingGenerator> _generator; + + /// Initializes the for . + public EmbeddingGeneratorEmbeddingGenerationService( + IEmbeddingGenerator> generator, IServiceProvider? serviceProvider) + { + // Store the generator. + this._generator = generator; + + // Initialize the attributes. + var attrs = new Dictionary(); + this.Attributes = new ReadOnlyDictionary(attrs); + + var metadata = generator.Metadata; + if (metadata.ProviderUri is not null) + { + attrs[AIServiceExtensions.EndpointKey] = metadata.ProviderUri.ToString(); + } + if (metadata.ModelId is not null) + { + attrs[AIServiceExtensions.ModelIdKey] = metadata.ModelId; + } + } + + /// + public IReadOnlyDictionary Attributes { get; } + + /// + public async Task>> GenerateEmbeddingsAsync(IList data, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(data); + + var embeddings = await this._generator.GenerateAsync(data, cancellationToken: cancellationToken).ConfigureAwait(false); + + return embeddings.Select(e => e.Vector).ToList(); + } + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs b/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs new file mode 100644 index 000000000000..bc4fa6440444 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.SemanticKernel; + +[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] +[JsonSerializable(typeof(IDictionary))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(PromptExecutionSettings))] +internal sealed partial class AbstractionsJsonContext : JsonSerializerContext +{ + /// Gets the singleton used as the default in JSON serialization operations. + private static readonly JsonSerializerOptions s_defaultToolJsonOptions = CreateDefaultToolJsonOptions(); + + /// Gets JSON type information for the specified type. + /// + /// This first tries to get the type information from , + /// falling back to if it can't. + /// + public static JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? firstOptions) + { + return firstOptions?.TryGetTypeInfo(type, out JsonTypeInfo? info) is true ? + info : + s_defaultToolJsonOptions.GetTypeInfo(type); + } + + /// Creates the default to use for serialization-related operations. + [UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")] + private static JsonSerializerOptions CreateDefaultToolJsonOptions() + { + // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, + // and we want to be flexible in terms of what can be put into the various collections in the object model. + // Otherwise, use the source-generated options to enable trimming and Native AOT. + + if (JsonSerializer.IsReflectionEnabledByDefault) + { + // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext above. + JsonSerializerOptions options = new(JsonSerializerDefaults.Web) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + }; + + options.MakeReadOnly(); + return options; + } + + return Default.Options; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index 3c4a1df1bc3d..bab2fca0f92e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -12,6 +12,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; @@ -537,4 +538,72 @@ private void LogFunctionResult(ILogger logger, FunctionResult functionResult) logger.LogFunctionResultValue(functionResult); } } + + /// Creates an for this . + /// + /// The instance to pass to the when it's invoked as part of the 's invocation. + /// + /// An instance of that, when invoked, will in turn invoke the current . + [Experimental("SKEXP0001")] + public AIFunction AsAIFunction(Kernel? kernel = null) + { + return new KernelAIFunction(this, kernel); + } + + /// An wrapper around a . + private sealed class KernelAIFunction : AIFunction + { + private readonly KernelFunction _kernelFunction; + private readonly Kernel? _kernel; + + public KernelAIFunction(KernelFunction kernelFunction, Kernel? kernel) + { + this._kernelFunction = kernelFunction; + this._kernel = kernel; + + string name = string.IsNullOrWhiteSpace(kernelFunction.PluginName) ? + kernelFunction.Name : + $"{kernelFunction.PluginName}_{kernelFunction.Name}"; + + this.Metadata = new AIFunctionMetadata(name) + { + Description = kernelFunction.Description, + + Parameters = kernelFunction.Metadata.Parameters.Select(p => new AIFunctionParameterMetadata(p.Name) + { + Description = p.Description, + ParameterType = p.ParameterType, + IsRequired = p.IsRequired, + HasDefaultValue = p.DefaultValue is not null, + DefaultValue = p.DefaultValue, + Schema = p.Schema?.RootElement, + }).ToList(), + + ReturnParameter = new AIFunctionReturnParameterMetadata() + { + Description = kernelFunction.Metadata.ReturnParameter.Description, + ParameterType = kernelFunction.Metadata.ReturnParameter.ParameterType, + Schema = kernelFunction.Metadata.ReturnParameter.Schema?.RootElement, + }, + }; + } + + public override AIFunctionMetadata Metadata { get; } + + protected override async Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) + { + Verify.NotNull(arguments); + + KernelArguments args = []; + foreach (var argument in arguments) + { + args[argument.Key] = argument.Value; + } + + var functionResult = await this._kernelFunction.InvokeAsync(this._kernel ?? new(), args, cancellationToken).ConfigureAwait(false); + return functionResult.Value is object value ? JsonSerializer.SerializeToElement( + value, + AbstractionsJsonContext.GetTypeInfo(value.GetType(), this._kernelFunction.JsonSerializerOptions)) : null; + } + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelPlugin.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelPlugin.cs index 9ba7e2db8d75..30227246b21b 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelPlugin.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelPlugin.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Microsoft.Extensions.AI; #pragma warning disable CA1716 // Identifiers should not match keywords @@ -92,6 +93,17 @@ public IList GetFunctionsMetadata() /// public abstract IEnumerator GetEnumerator(); + /// Produces an for every in this plugin. + /// An enumerable of instances, one for each in this plugin. + [Experimental("SKEXP0001")] + public IEnumerable AsAIFunctions() + { + foreach (KernelFunction function in this) + { + yield return function.AsAIFunction(); + } + } + /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); diff --git a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj index 0b2cf11c45ee..6fbec9539af9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj +++ b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj @@ -4,7 +4,7 @@ Microsoft.SemanticKernel.Abstractions Microsoft.SemanticKernel net8.0;netstandard2.0 - $(NoWarn);SKEXP0001;NU5104;SKEXP0120 + $(NoWarn);NU5104;SKEXP0001;NU5104;SKEXP0120 true true @@ -29,6 +29,7 @@ + diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs new file mode 100644 index 000000000000..f118233e0bba --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; +using Xunit; + +namespace SemanticKernel.UnitTests.AI; + +public class ServiceConversionExtensionsTests +{ + [Fact] + public void InvalidArgumentsThrow() + { + Assert.Throws("service", () => ChatCompletionServiceExtensions.AsChatClient(null!)); + Assert.Throws("client", () => ChatCompletionServiceExtensions.AsChatCompletionService(null!)); + + Assert.Throws("service", () => EmbeddingGenerationExtensions.AsEmbeddingGenerator(null!)); + Assert.Throws("generator", () => EmbeddingGenerationExtensions.AsEmbeddingGenerationService(null!)); + } +}