diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7016e18d98d8..3a3deaaf59fc 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -48,7 +48,7 @@ - + @@ -68,10 +68,10 @@ - - - - + + + + diff --git a/dotnet/samples/Concepts/ChatCompletion/HybridCompletion_Fallback.cs b/dotnet/samples/Concepts/ChatCompletion/HybridCompletion_Fallback.cs index cdcd4027b56a..188eee708135 100644 --- a/dotnet/samples/Concepts/ChatCompletion/HybridCompletion_Fallback.cs +++ b/dotnet/samples/Concepts/ChatCompletion/HybridCompletion_Fallback.cs @@ -39,9 +39,9 @@ public async Task FallbackToAvailableModelAsync() // Create a fallback chat client that will fallback to the available chat client when unavailable chat client fails IChatClient fallbackChatClient = new FallbackChatClient([unavailableChatClient, availableChatClient]); - ChatOptions chatOptions = new() { Tools = [AIFunctionFactory.Create(GetWeather, new AIFunctionFactoryCreateOptions { Name = "GetWeather" })] }; + ChatOptions chatOptions = new() { Tools = [AIFunctionFactory.Create(GetWeather)] }; - var result = await fallbackChatClient.CompleteAsync("Do I need an umbrella?", chatOptions); + var result = await fallbackChatClient.GetResponseAsync("Do I need an umbrella?", chatOptions); Output.WriteLine(result); @@ -64,9 +64,9 @@ public async Task FallbackToAvailableModelStreamingAsync() // Create a fallback chat client that will fallback to the available chat client when unavailable chat client fails IChatClient fallbackChatClient = new FallbackChatClient([unavailableChatClient, availableChatClient]); - ChatOptions chatOptions = new() { Tools = [AIFunctionFactory.Create(GetWeather, new AIFunctionFactoryCreateOptions { Name = "GetWeather" })] }; + ChatOptions chatOptions = new() { Tools = [AIFunctionFactory.Create(GetWeather)] }; - var result = fallbackChatClient.CompleteStreamingAsync("Do I need an umbrella?", chatOptions); + var result = fallbackChatClient.GetStreamingResponseAsync("Do I need an umbrella?", chatOptions); await foreach (var update in result) { @@ -151,7 +151,7 @@ public FallbackChatClient(IList chatClients) public ChatClientMetadata Metadata => new(); /// - public async Task CompleteAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) + public async Task GetResponseAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) { for (int i = 0; i < this._chatClients.Count; i++) { @@ -159,7 +159,7 @@ public FallbackChatClient(IList chatClients) try { - return await chatClient.CompleteAsync(chatMessages, options, cancellationToken).ConfigureAwait(false); + return await chatClient.GetResponseAsync(chatMessages, options, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -177,15 +177,15 @@ public FallbackChatClient(IList chatClients) } /// - public async IAsyncEnumerable CompleteStreamingAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetStreamingResponseAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { for (int i = 0; i < this._chatClients.Count; i++) { var chatClient = this._chatClients.ElementAt(i); - IAsyncEnumerable completionStream = chatClient.CompleteStreamingAsync(chatMessages, options, cancellationToken); + IAsyncEnumerable completionStream = chatClient.GetStreamingResponseAsync(chatMessages, options, cancellationToken); - ConfiguredCancelableAsyncEnumerable.Enumerator enumerator = completionStream.ConfigureAwait(false).GetAsyncEnumerator(); + ConfiguredCancelableAsyncEnumerable.Enumerator enumerator = completionStream.ConfigureAwait(false).GetAsyncEnumerator(); try { diff --git a/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Services/AzureAIInferenceChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Services/AzureAIInferenceChatCompletionServiceTests.cs index a8447d4838a3..6faef5ab9a11 100644 --- a/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Services/AzureAIInferenceChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Services/AzureAIInferenceChatCompletionServiceTests.cs @@ -249,7 +249,7 @@ public async Task GetChatMessageInResponseFormatsAsync(string formatType, string format = JsonSerializer.Deserialize(formatValue); break; case "ChatResponseFormat": - format = formatValue == "text" ? new ChatCompletionsResponseFormatText() : new ChatCompletionsResponseFormatJSON(); + format = formatValue == "text" ? new ChatCompletionsResponseFormatText() : new ChatCompletionsResponseFormatJsonObject(); break; } diff --git a/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs index 3146cb94fb78..c6e9dd5d503e 100644 --- a/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs @@ -136,7 +136,7 @@ public int? MaxTokens /// Note that to enable JSON mode, some AI models may also require you to instruct the model to produce JSON /// via a system or user message. /// Please note is the base class. According to the scenario, a derived class of the base class might need to be assigned here, or this property needs to be casted to one of the possible derived classes. - /// The available derived classes include and . + /// The available derived classes include and . /// [JsonPropertyName("response_format")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs index 2a175afb348d..7ab32b31b869 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -12,8 +14,7 @@ namespace Microsoft.SemanticKernel.ChatCompletion; /// 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 +/// 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. @@ -23,28 +24,15 @@ internal 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, + base(aiFunction.Name, + aiFunction.Description, + MapParameterMetadata(aiFunction), + aiFunction.JsonSerializerOptions, 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, + Description = aiFunction.UnderlyingMethod?.ReturnParameter.GetCustomAttribute()?.Description, + ParameterType = aiFunction.UnderlyingMethod?.ReturnParameter.ParameterType, + Schema = new KernelJsonSchema(AIJsonUtilities.CreateJsonSchema(aiFunction.UnderlyingMethod?.ReturnParameter.ParameterType)), }) { this._aiFunction = aiFunction; @@ -73,4 +61,30 @@ protected override IAsyncEnumerable InvokeStreamingCoreAsync(K // This should never be invoked, as instances are always passed with autoInvoke:false. throw new NotSupportedException(); } + + private static IReadOnlyList MapParameterMetadata(AIFunction aiFunction) + { + if (!aiFunction.JsonSchema.TryGetProperty("properties", out JsonElement properties)) + { + return Array.Empty(); + } + + List kernelParams = []; + var parameterInfos = aiFunction.UnderlyingMethod?.GetParameters().ToDictionary(p => p.Name!, StringComparer.Ordinal); + foreach (var param in properties.EnumerateObject()) + { + ParameterInfo? paramInfo = null; + parameterInfos?.TryGetValue(param.Name, out paramInfo); + kernelParams.Add(new(param.Name, aiFunction.JsonSerializerOptions) + { + Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null, + DefaultValue = param.Value.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue : null, + IsRequired = param.Value.TryGetProperty("required", out JsonElement required) && required.GetBoolean(), + ParameterType = paramInfo?.ParameterType, + Schema = param.Value.TryGetProperty("schema", out JsonElement schema) ? new KernelJsonSchema(schema) : null, + }); + } + + return kernelParams; + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs index 7447b230ec63..419dca381015 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs @@ -34,12 +34,12 @@ public ChatClientChatCompletionService(IChatClient chatClient, IServiceProvider? var attrs = new Dictionary(); this.Attributes = new ReadOnlyDictionary(attrs); - var metadata = chatClient.Metadata; - if (metadata.ProviderUri is not null) + var metadata = chatClient.GetService(); + if (metadata?.ProviderUri is not null) { attrs[AIServiceExtensions.EndpointKey] = metadata.ProviderUri.ToString(); } - if (metadata.ModelId is not null) + if (metadata?.ModelId is not null) { attrs[AIServiceExtensions.ModelIdKey] = metadata.ModelId; } @@ -57,7 +57,7 @@ public async Task> GetChatMessageContentsAsync var messageList = ChatCompletionServiceExtensions.ToChatMessageList(chatHistory); var currentSize = messageList.Count; - var completion = await this._chatClient.CompleteAsync( + var completion = await this._chatClient.GetResponseAsync( messageList, ToChatOptions(executionSettings, kernel), cancellationToken).ConfigureAwait(false); @@ -76,7 +76,7 @@ public async IAsyncEnumerable GetStreamingChatMessa { Verify.NotNull(chatHistory); - await foreach (var update in this._chatClient.CompleteStreamingAsync( + await foreach (var update in this._chatClient.GetStreamingResponseAsync( ChatCompletionServiceExtensions.ToChatMessageList(chatHistory), ToChatOptions(executionSettings, kernel), cancellationToken).ConfigureAwait(false)) @@ -158,13 +158,19 @@ public async IAsyncEnumerable GetStreamingChatMessa else if (entry.Key.Equals("response_format", StringComparison.OrdinalIgnoreCase) && entry.Value is { } responseFormat) { - options.ResponseFormat = responseFormat switch + if (TryConvert(responseFormat, out string? responseFormatString)) { - "text" => ChatResponseFormat.Text, - "json_object" => ChatResponseFormat.Json, - JsonElement e => ChatResponseFormat.ForJsonSchema(e), - _ => null, - }; + options.ResponseFormat = responseFormatString switch + { + "text" => ChatResponseFormat.Text, + "json_object" => ChatResponseFormat.Json, + _ => null, + }; + } + else + { + options.ResponseFormat = responseFormat is JsonElement e ? ChatResponseFormat.ForJsonSchema(e) : null; + } } else { @@ -268,9 +274,9 @@ static bool TryConvert(object? value, [NotNullWhen(true)] out T? result) } } - /// Converts a to a . + /// Converts a to a . /// This conversion should not be necessary once SK eventually adopts the shared content types. - private static StreamingChatMessageContent ToStreamingChatMessageContent(StreamingChatCompletionUpdate update) + private static StreamingChatMessageContent ToStreamingChatMessageContent(ChatResponseUpdate update) { StreamingChatMessageContent content = new( update.Role is not null ? new AuthorRole(update.Role.Value.Value) : null, diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs index 308dbc64e183..862239ccd505 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs @@ -35,7 +35,7 @@ public ChatCompletionServiceChatClient(IChatCompletionService chatCompletionServ public ChatClientMetadata Metadata { get; } /// - public async Task CompleteAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) + public async Task GetResponseAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) { Verify.NotNull(chatMessages); @@ -53,7 +53,7 @@ public ChatCompletionServiceChatClient(IChatCompletionService chatCompletionServ } /// - public async IAsyncEnumerable CompleteStreamingAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetStreamingResponseAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Verify.NotNull(chatMessages); @@ -82,6 +82,7 @@ public void Dispose() serviceKey is not null ? null : serviceType.IsInstanceOfType(this) ? this : serviceType.IsInstanceOfType(this._chatCompletionService) ? this._chatCompletionService : + serviceType.IsInstanceOfType(this.Metadata) ? this.Metadata : null; } @@ -191,11 +192,11 @@ public void Dispose() return settings; } - /// Converts a to a . + /// Converts a to a . /// This conversion should not be necessary once SK eventually adopts the shared content types. - private static StreamingChatCompletionUpdate ToStreamingChatCompletionUpdate(StreamingChatMessageContent content) + private static ChatResponseUpdate ToStreamingChatCompletionUpdate(StreamingChatMessageContent content) { - StreamingChatCompletionUpdate update = new() + ChatResponseUpdate update = new() { AdditionalProperties = content.Metadata is not null ? new AdditionalPropertiesDictionary(content.Metadata) : null, AuthorName = content.AuthorName, diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs index ef8b0b56c7f9..cf5834725700 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.cs @@ -169,15 +169,15 @@ internal static ChatMessage ToChatMessage(ChatMessageContent content) 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) : + ic.DataUri is not null ? new Microsoft.Extensions.AI.DataContent(ic.DataUri, ic.MimeType ?? "image/*") : + ic.Uri is not null ? new Microsoft.Extensions.AI.DataContent(ic.Uri, ic.MimeType ?? "image/*") : 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) : + ac.DataUri is not null ? new Microsoft.Extensions.AI.DataContent(ac.DataUri, ac.MimeType ?? "audio/*") : + ac.Uri is not null ? new Microsoft.Extensions.AI.DataContent(ac.Uri, ac.MimeType ?? "audio/*") : null; break; @@ -193,7 +193,7 @@ internal static ChatMessage ToChatMessage(ChatMessageContent content) break; case Microsoft.SemanticKernel.FunctionResultContent frc: - aiContent = new Microsoft.Extensions.AI.FunctionResultContent(frc.CallId ?? string.Empty, frc.FunctionName ?? string.Empty, frc.Result); + aiContent = new Microsoft.Extensions.AI.FunctionResultContent(frc.CallId ?? string.Empty, frc.Result); break; } @@ -211,13 +211,13 @@ internal static ChatMessage ToChatMessage(ChatMessageContent content) /// Converts a to a . /// This conversion should not be necessary once SK eventually adopts the shared content types. - internal static ChatMessageContent ToChatMessageContent(ChatMessage message, Microsoft.Extensions.AI.ChatCompletion? completion = null) + internal static ChatMessageContent ToChatMessageContent(ChatMessage message, Microsoft.Extensions.AI.ChatResponse? response = null) { ChatMessageContent result = new() { - ModelId = completion?.ModelId, + ModelId = response?.ModelId, AuthorName = message.AuthorName, - InnerContent = completion?.RawRepresentation ?? message.RawRepresentation, + InnerContent = response?.RawRepresentation ?? message.RawRepresentation, Metadata = message.AdditionalProperties, Role = new AuthorRole(message.Role.Value), }; @@ -231,20 +231,20 @@ internal static ChatMessageContent ToChatMessageContent(ChatMessage message, Mic 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)); + case Microsoft.Extensions.AI.DataContent dc when dc.MediaTypeStartsWith("image/"): + resultContent = dc.Data is not null ? + new Microsoft.SemanticKernel.ImageContent(dc.Uri) : + new Microsoft.SemanticKernel.ImageContent(new Uri(dc.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)); + case Microsoft.Extensions.AI.DataContent dc when dc.MediaTypeStartsWith("audio/"): + resultContent = dc.Data is not null ? + new Microsoft.SemanticKernel.AudioContent(dc.Uri) : + new Microsoft.SemanticKernel.AudioContent(new Uri(dc.Uri)); break; case Microsoft.Extensions.AI.DataContent dc: - resultContent = dc.ContainsData ? + resultContent = dc.Data is not null ? new Microsoft.SemanticKernel.BinaryContent(dc.Uri) : new Microsoft.SemanticKernel.BinaryContent(new Uri(dc.Uri)); break; @@ -254,7 +254,7 @@ internal static ChatMessageContent ToChatMessageContent(ChatMessage message, Mic break; case Microsoft.Extensions.AI.FunctionResultContent frc: - resultContent = new Microsoft.SemanticKernel.FunctionResultContent(frc.Name, null, frc.CallId, frc.Result); + resultContent = new Microsoft.SemanticKernel.FunctionResultContent(callId: frc.CallId, result: frc.Result); break; } @@ -262,7 +262,7 @@ internal static ChatMessageContent ToChatMessageContent(ChatMessage message, Mic { resultContent.Metadata = content.AdditionalProperties; resultContent.InnerContent = content.RawRepresentation; - resultContent.ModelId = completion?.ModelId; + resultContent.ModelId = response?.ModelId; result.Items.Add(resultContent); } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs index c060c3f0d523..96f1dd0252dd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs @@ -132,6 +132,7 @@ public async Task>> GenerateAsync(IEnu serviceKey is not null ? null : serviceType.IsInstanceOfType(this) ? this : serviceType.IsInstanceOfType(this._service) ? this._service : + serviceType.IsInstanceOfType(this.Metadata) ? this.Metadata : null; } } @@ -154,12 +155,12 @@ public EmbeddingGeneratorEmbeddingGenerationService( var attrs = new Dictionary(); this.Attributes = new ReadOnlyDictionary(attrs); - var metadata = generator.Metadata; - if (metadata.ProviderUri is not null) + var metadata = (EmbeddingGeneratorMetadata?)generator.GetService(typeof(EmbeddingGeneratorMetadata)); + if (metadata?.ProviderUri is not null) { attrs[AIServiceExtensions.EndpointKey] = metadata.ProviderUri.ToString(); } - if (metadata.ModelId is not null) + if (metadata?.ModelId is not null) { attrs[AIServiceExtensions.ModelIdKey] = metadata.ModelId; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs b/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs index 29caab93da9a..736710ab146c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AbstractionsJsonContext.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.SemanticKernel.Functions; namespace Microsoft.SemanticKernel; @@ -15,6 +16,7 @@ namespace Microsoft.SemanticKernel; WriteIndented = true)] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(KernelFunctionSchemaModel))] [JsonSerializable(typeof(PromptExecutionSettings))] // types commonly used as values in settings dictionaries [JsonSerializable(typeof(string))] diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index cc2d260b48a7..fddac8f48282 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions; namespace Microsoft.SemanticKernel; @@ -517,6 +518,7 @@ public AIFunction AsAIFunction(Kernel? kernel = null) /// An wrapper around a . private sealed class KernelAIFunction : AIFunction { + private static readonly JsonElement s_defaultSchema = JsonDocument.Parse("{}").RootElement; private readonly KernelFunction _kernelFunction; private readonly Kernel? _kernel; @@ -524,37 +526,17 @@ public KernelAIFunction(KernelFunction kernelFunction, Kernel? kernel) { this._kernelFunction = kernelFunction; this._kernel = kernel; - - string name = string.IsNullOrWhiteSpace(kernelFunction.PluginName) ? + this.Name = string.IsNullOrWhiteSpace(kernelFunction.PluginName) ? kernelFunction.Name : $"{kernelFunction.PluginName}-{kernelFunction.Name}"; - this.Metadata = new AIFunctionMetadata(name) - { - Description = kernelFunction.Description, - - JsonSerializerOptions = kernelFunction.JsonSerializerOptions, - - 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, - }, - }; + this.JsonSchema = BuildFunctionSchema(kernelFunction); } - public override AIFunctionMetadata Metadata { get; } + public override string Name { get; } + public override JsonElement JsonSchema { get; } + public override string Description => this._kernelFunction.Description; + public override JsonSerializerOptions JsonSerializerOptions => this._kernelFunction.JsonSerializerOptions ?? base.JsonSerializerOptions; protected override async Task InvokeCoreAsync( IEnumerable> arguments, CancellationToken cancellationToken) @@ -576,5 +558,25 @@ public KernelAIFunction(KernelFunction kernelFunction, Kernel? kernel) JsonSerializer.SerializeToElement(value, AbstractionsJsonContext.GetTypeInfo(value.GetType(), this._kernelFunction.JsonSerializerOptions)) : null; } + + private static JsonElement BuildFunctionSchema(KernelFunction function) + { + KernelFunctionSchemaModel schemaModel = new() + { + Type = "object", + Description = function.Description, + }; + + foreach (var parameter in function.Metadata.Parameters) + { + schemaModel.Properties[parameter.Name] = parameter.Schema?.RootElement ?? s_defaultSchema; + if (parameter.IsRequired) + { + (schemaModel.Required ??= []).Add(parameter.Name); + } + } + + return JsonSerializer.SerializeToElement(schemaModel, AbstractionsJsonContext.Default.KernelFunctionSchemaModel); + } } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionSchemaModel.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionSchemaModel.cs new file mode 100644 index 000000000000..e7460f9773af --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionSchemaModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Functions; + +internal sealed class KernelFunctionSchemaModel +{ + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("condition"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Description { get; set; } + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + + [JsonPropertyName("required"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Required { get; set; } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs index cce73a65510f..556799ecc85e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs @@ -41,10 +41,10 @@ public void AsEmbeddingGeneratorMetadataReturnsExpectedData() }.AsEmbeddingGenerator(); Assert.NotNull(generator); - Assert.NotNull(generator.Metadata); - Assert.Equal(nameof(TestEmbeddingGenerationService), generator.Metadata.ProviderName); - Assert.Equal("examplemodel", generator.Metadata.ModelId); - Assert.Equal("https://example.com/", generator.Metadata.ProviderUri?.ToString()); + var metadata = Assert.IsType(generator.GetService(typeof(EmbeddingGeneratorMetadata))); + Assert.Equal(nameof(TestEmbeddingGenerationService), metadata.ProviderName); + Assert.Equal("examplemodel", metadata.ModelId); + Assert.Equal("https://example.com/", metadata.ProviderUri?.ToString()); } [Fact] @@ -75,10 +75,10 @@ public void AsChatClientMetadataReturnsExpectedData() }.AsChatClient(); Assert.NotNull(client); - Assert.NotNull(client.Metadata); - Assert.Equal(nameof(TestChatCompletionService), client.Metadata.ProviderName); - Assert.Equal("examplemodel", client.Metadata.ModelId); - Assert.Equal("https://example.com/", client.Metadata.ProviderUri?.ToString()); + var metadata = Assert.IsType(client.GetService(typeof(ChatClientMetadata))); + Assert.Equal(nameof(TestChatCompletionService), metadata.ProviderName); + Assert.Equal("examplemodel", metadata.ModelId); + Assert.Equal("https://example.com/", metadata.ProviderUri?.ToString()); } [Fact] @@ -151,15 +151,15 @@ public async Task AsChatClientNonStreamingContentConvertedAsExpected() }, }.AsChatClient(); - Microsoft.Extensions.AI.ChatCompletion result = await client.CompleteAsync([ + Microsoft.Extensions.AI.ChatResponse result = await client.GetResponseAsync([ new(ChatRole.System, [ new Microsoft.Extensions.AI.TextContent("some text"), - new Microsoft.Extensions.AI.ImageContent("http://imageurl"), + new Microsoft.Extensions.AI.DataContent("http://imageurl", mediaType: "image/jpeg"), ]), new(ChatRole.User, [ - new Microsoft.Extensions.AI.AudioContent("http://audiourl"), + new Microsoft.Extensions.AI.DataContent("http://audiourl", mediaType: "audio/mpeg"), new Microsoft.Extensions.AI.TextContent("some other text"), ]), new(ChatRole.Assistant, @@ -168,7 +168,7 @@ public async Task AsChatClientNonStreamingContentConvertedAsExpected() ]), new(ChatRole.Tool, [ - new Microsoft.Extensions.AI.FunctionResultContent("call123", "FunctionName", 42), + new Microsoft.Extensions.AI.FunctionResultContent("call123", 42), ]), ], new ChatOptions() { @@ -211,7 +211,7 @@ public async Task AsChatClientNonStreamingContentConvertedAsExpected() var frc = Assert.IsType(actualChatHistory[3].Items[0]); Assert.Equal("call123", frc.CallId); - Assert.Equal("FunctionName", frc.FunctionName); + Assert.Null(frc.FunctionName); Assert.Equal(42, frc.Result); Assert.NotNull(actualSettings); @@ -244,19 +244,19 @@ public async Task AsChatClientNonStreamingResponseFormatHandled() List messages = [new(ChatRole.User, "hi")]; - await client.CompleteAsync(messages); + await client.GetResponseAsync(messages); oaiSettings = JsonSerializer.Deserialize(JsonSerializer.Serialize(actualSettings)); Assert.Null(oaiSettings); - await client.CompleteAsync(messages, new() { ResponseFormat = ChatResponseFormat.Text }); + await client.GetResponseAsync(messages, new() { ResponseFormat = ChatResponseFormat.Text }); oaiSettings = JsonSerializer.Deserialize(JsonSerializer.Serialize(actualSettings)); Assert.Equal("text", oaiSettings?.ResponseFormat?.ToString()); - await client.CompleteAsync(messages, new() { ResponseFormat = ChatResponseFormat.Json }); + await client.GetResponseAsync(messages, new() { ResponseFormat = ChatResponseFormat.Json }); oaiSettings = JsonSerializer.Deserialize(JsonSerializer.Serialize(actualSettings)); Assert.Equal("json_object", oaiSettings?.ResponseFormat?.ToString()); - await client.CompleteAsync(messages, new() { ResponseFormat = ChatResponseFormat.ForJsonSchema(JsonSerializer.Deserialize(""" + await client.GetResponseAsync(messages, new() { ResponseFormat = ChatResponseFormat.ForJsonSchema(JsonSerializer.Deserialize(""" {"type": "string"} """)) }); oaiSettings = JsonSerializer.Deserialize(JsonSerializer.Serialize(actualSettings)); @@ -289,7 +289,7 @@ public async Task AsChatClientNonStreamingToolsPropagated(ChatToolMode mode) List messages = [new(ChatRole.User, "hi")]; - await client.CompleteAsync(messages, new() + await client.GetResponseAsync(messages, new() { Tools = [new NopAIFunction("AIFunc1"), new NopAIFunction("AIFunc2")], ToolMode = mode, @@ -335,8 +335,7 @@ public async Task AsChatClientNonStreamingToolsPropagated(ChatToolMode mode) private sealed class NopAIFunction(string name) : AIFunction { - public override AIFunctionMetadata Metadata => new(name); - + public override string Name => name; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { throw new FormatException(); @@ -362,15 +361,15 @@ public async Task AsChatClientStreamingContentConvertedAsExpected() }, }.AsChatClient(); - List result = await client.CompleteStreamingAsync([ + List result = await client.GetStreamingResponseAsync([ new(ChatRole.System, [ new Microsoft.Extensions.AI.TextContent("some text"), - new Microsoft.Extensions.AI.ImageContent("http://imageurl"), + new Microsoft.Extensions.AI.DataContent("http://imageurl", "image/jpeg"), ]), new(ChatRole.User, [ - new Microsoft.Extensions.AI.AudioContent("http://audiourl"), + new Microsoft.Extensions.AI.DataContent("http://audiourl", "audio/mpeg"), new Microsoft.Extensions.AI.TextContent("some other text"), ]), new(ChatRole.Assistant, @@ -379,7 +378,7 @@ public async Task AsChatClientStreamingContentConvertedAsExpected() ]), new(ChatRole.Tool, [ - new Microsoft.Extensions.AI.FunctionResultContent("call123", "FunctionName", 42), + new Microsoft.Extensions.AI.FunctionResultContent("call123", 42), ]), ], new ChatOptions() { @@ -423,7 +422,7 @@ public async Task AsChatClientStreamingContentConvertedAsExpected() var frc = Assert.IsType(actualChatHistory[3].Items[0]); Assert.Equal("call123", frc.CallId); - Assert.Equal("FunctionName", frc.FunctionName); + Assert.Null(frc.FunctionName); Assert.Equal(42, frc.Result); Assert.NotNull(actualSettings); @@ -451,7 +450,7 @@ public async Task AsChatCompletionServiceNonStreamingContentConvertedAsExpected( await Task.Yield(); actualChatHistory = messages; actualOptions = options; - return new Microsoft.Extensions.AI.ChatCompletion(new ChatMessage() { Text = "the result" }); + return new Microsoft.Extensions.AI.ChatResponse(new ChatMessage() { Text = "the result" }); }, }; @@ -505,8 +504,8 @@ public async Task AsChatCompletionServiceNonStreamingContentConvertedAsExpected( Assert.Single(actualChatHistory[3].Contents); Assert.Equal("some text", Assert.IsType(actualChatHistory[0].Contents[0]).Text); - Assert.Equal("http://imageurl/", Assert.IsType(actualChatHistory[0].Contents[1]).Uri?.ToString()); - Assert.Equal("http://audiourl/", Assert.IsType(actualChatHistory[1].Contents[0]).Uri?.ToString()); + Assert.Equal("http://imageurl/", Assert.IsType(actualChatHistory[0].Contents[1]).Uri?.ToString()); + Assert.Equal("http://audiourl/", Assert.IsType(actualChatHistory[1].Contents[0]).Uri?.ToString()); Assert.Equal("some other text", Assert.IsType(actualChatHistory[1].Contents[1]).Text); var fcc = Assert.IsType(actualChatHistory[2].Contents[0]); @@ -516,7 +515,6 @@ public async Task AsChatCompletionServiceNonStreamingContentConvertedAsExpected( var frc = Assert.IsType(actualChatHistory[3].Contents[0]); Assert.Equal("call123", frc.CallId); - Assert.Equal("FunctionName", frc.Name); Assert.Equal(42, frc.Result); Assert.NotNull(actualOptions); @@ -542,7 +540,7 @@ public async Task AsChatCompletionServiceStreamingContentConvertedAsExpected() { actualChatHistory = messages; actualOptions = options; - return new List() + return new List() { new() { Role = ChatRole.Assistant, Text = "the result" } }.ToAsyncEnumerable(); @@ -600,8 +598,8 @@ public async Task AsChatCompletionServiceStreamingContentConvertedAsExpected() Assert.Single(actualChatHistory[3].Contents); Assert.Equal("some text", Assert.IsType(actualChatHistory[0].Contents[0]).Text); - Assert.Equal("http://imageurl/", Assert.IsType(actualChatHistory[0].Contents[1]).Uri?.ToString()); - Assert.Equal("http://audiourl/", Assert.IsType(actualChatHistory[1].Contents[0]).Uri?.ToString()); + Assert.Equal("http://imageurl/", Assert.IsType(actualChatHistory[0].Contents[1]).Uri?.ToString()); + Assert.Equal("http://audiourl/", Assert.IsType(actualChatHistory[1].Contents[0]).Uri?.ToString()); Assert.Equal("some other text", Assert.IsType(actualChatHistory[1].Contents[1]).Text); var fcc = Assert.IsType(actualChatHistory[2].Contents[0]); @@ -611,7 +609,6 @@ public async Task AsChatCompletionServiceStreamingContentConvertedAsExpected() var frc = Assert.IsType(actualChatHistory[3].Contents[0]); Assert.Equal("call123", frc.CallId); - Assert.Equal("FunctionName", frc.Name); Assert.Equal(42, frc.Result); Assert.NotNull(actualOptions); @@ -652,18 +649,18 @@ private sealed class TestChatClient : IChatClient { public ChatClientMetadata Metadata { get; set; } = new(); - public Func, ChatOptions?, CancellationToken, Task>? CompleteAsyncDelegate { get; set; } + public Func, ChatOptions?, CancellationToken, Task>? CompleteAsyncDelegate { get; set; } - public Func, ChatOptions?, CancellationToken, IAsyncEnumerable>? CompleteStreamingAsyncDelegate { get; set; } + public Func, ChatOptions?, CancellationToken, IAsyncEnumerable>? CompleteStreamingAsyncDelegate { get; set; } - public Task CompleteAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) + public Task GetResponseAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) { return this.CompleteAsyncDelegate != null ? this.CompleteAsyncDelegate(chatMessages, options, cancellationToken) : throw new NotImplementedException(); } - public IAsyncEnumerable CompleteStreamingAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetStreamingResponseAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) { return this.CompleteStreamingAsyncDelegate != null ? this.CompleteStreamingAsyncDelegate(chatMessages, options, cancellationToken) @@ -674,7 +671,7 @@ public void Dispose() { } public object? GetService(Type serviceType, object? serviceKey = null) { - return null; + return serviceType == typeof(ChatClientMetadata) ? this.Metadata : null; } } @@ -709,7 +706,7 @@ public Task>> GenerateAsync(IEnumerable