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