From 40b9d2fe9e5e4fac013f26abc2987f3abdd99c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Mon, 18 Mar 2024 15:05:13 +0800 Subject: [PATCH 1/2] feat: support auto invoke functions for non-stream chat --- .../DashScopeTextEmbeddingGenerator.cs | 11 +- .../DashScopeTextGenerator.cs | 2 +- .../DependencyInjector.cs | 2 +- .../KernelMemory.DashScope.csproj | 4 +- .../DashScopeChatCompletionService.cs | 211 +++++++++++++-- .../DashScopeChatMessageContent.cs | 27 ++ .../DashScopeMapper.cs | 13 +- .../DashScopePromptExecutionSettings.cs | 34 ++- .../DashScopeServiceCollectionExtensions.cs | 13 +- ...DashScopeTextEmbeddingGenerationService.cs | 10 +- .../FunctionDefinition.cs | 32 +++ .../FunctionDefinitionConvertor.cs | 103 +++++++ .../SemanticKernel.DashScope.csproj | 4 +- .../ToolCallBehavior.cs | 121 +++++++++ .../KernelMemory.DashScope.UnitTests/Cases.cs | 2 +- .../DashScopeTextEmbeddingGeneratorTests.cs | 2 +- .../DashScopeTextGeneratorTests.cs | 2 +- .../DependencyInjectorTests.cs | 2 +- .../KernelMemory.DashScope.UnitTests.csproj | 4 +- .../Cases.cs | 88 +++++- .../ChatCompletionTests.cs | 254 +++++++++++++++++- .../MockLoggerFactory.cs | 15 ++ .../SemanticKernel.DashScope.UnitTest.csproj | 2 +- .../ServiceCollectionExtensionsTests.cs | 1 + .../TextCompletionTests.cs | 27 +- .../TextEmbeddingTests.cs | 2 +- 26 files changed, 927 insertions(+), 61 deletions(-) create mode 100644 src/SemanticKernel.DashScope/DashScopeChatMessageContent.cs create mode 100644 src/SemanticKernel.DashScope/FunctionDefinition.cs create mode 100644 src/SemanticKernel.DashScope/FunctionDefinitionConvertor.cs create mode 100644 src/SemanticKernel.DashScope/ToolCallBehavior.cs create mode 100644 test/SemanticKernel.DashScope.UnitTest/MockLoggerFactory.cs diff --git a/src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs b/src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs index 1c14842..d76009a 100644 --- a/src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs +++ b/src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs @@ -1,5 +1,4 @@ -using Cnblogs.DashScope.Sdk; -using Cnblogs.DashScope.Sdk.TextEmbedding; +using Cnblogs.DashScope.Core; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI; @@ -30,7 +29,13 @@ public async Task GenerateEmbeddingAsync( string text, CancellationToken cancellationToken = new()) { - var result = await dashScopeClient.GetTextEmbeddingsAsync(modelId, [text], null, cancellationToken); + var result = await dashScopeClient.GetEmbeddingsAsync( + new ModelRequest + { + Input = new TextEmbeddingInput { Texts = [text] }, + Model = modelId + }, + cancellationToken); return result.Output.Embeddings[0].Embedding; } diff --git a/src/KernelMemory.DashScope/DashScopeTextGenerator.cs b/src/KernelMemory.DashScope/DashScopeTextGenerator.cs index 14c692c..8951593 100644 --- a/src/KernelMemory.DashScope/DashScopeTextGenerator.cs +++ b/src/KernelMemory.DashScope/DashScopeTextGenerator.cs @@ -1,5 +1,5 @@ using System.Runtime.CompilerServices; -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Microsoft.Extensions.Logging; using Microsoft.KernelMemory.AI; using Microsoft.KernelMemory.Diagnostics; diff --git a/src/KernelMemory.DashScope/DependencyInjector.cs b/src/KernelMemory.DashScope/DependencyInjector.cs index 281edef..dcaf7bf 100644 --- a/src/KernelMemory.DashScope/DependencyInjector.cs +++ b/src/KernelMemory.DashScope/DependencyInjector.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.KernelMemory.AI.DashScope; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/src/KernelMemory.DashScope/KernelMemory.DashScope.csproj b/src/KernelMemory.DashScope/KernelMemory.DashScope.csproj index 2d0f75c..d3c8d5d 100644 --- a/src/KernelMemory.DashScope/KernelMemory.DashScope.csproj +++ b/src/KernelMemory.DashScope/KernelMemory.DashScope.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs b/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs index 6cdbd81..1f56701 100644 --- a/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs +++ b/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs @@ -1,5 +1,8 @@ -using System.Runtime.CompilerServices; -using Cnblogs.DashScope.Sdk; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Cnblogs.DashScope.Core; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Services; @@ -15,45 +18,132 @@ public sealed class DashScopeChatCompletionService : IChatCompletionService, ITe private readonly IDashScopeClient _dashScopeClient; private readonly Dictionary _attributes = new(); private readonly string _modelId; + private readonly ILogger _logger; /// /// Creates a new DashScope chat completion service. /// /// /// - public DashScopeChatCompletionService(string modelId, IDashScopeClient dashScopeClient) + /// + public DashScopeChatCompletionService( + string modelId, + IDashScopeClient dashScopeClient, + ILogger logger) { _dashScopeClient = dashScopeClient; _modelId = modelId; + _logger = logger; _attributes.Add(AIServiceExtensions.ModelIdKey, _modelId); } /// public async Task> GetChatMessageContentsAsync( - ChatHistory chatHistory, + ChatHistory chat, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) { - var chatMessages = chatHistory.ToChatMessages(); var chatParameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); chatParameters ??= new DashScopePromptExecutionSettings(); chatParameters.IncrementalOutput = false; chatParameters.ResultFormat = ResultFormats.Message; - var response = await _dashScopeClient.GetTextCompletionAsync( - new ModelRequest + chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters); + + var autoInvoke = kernel is not null && chatParameters.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0; + for (var it = 1;; it++) + { + var response = await _dashScopeClient.GetTextCompletionAsync( + new ModelRequest + { + Input = new TextGenerationInput { Messages = chat.ToChatMessages() }, + Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId, + Parameters = chatParameters + }, + cancellationToken); + CaptureTokenUsage(response.Usage); + EnsureChoiceExists(response.Output.Choices); + var message = response.Output.Choices![0].Message; + var chatMessageContent = new DashScopeChatMessageContent( + new AuthorRole(message.Role), + message.Content, + name: null, + toolCalls: message.ToolCalls, + metadata: response.ToMetaData()); + if (autoInvoke == false || message.ToolCalls is null) { - Input = new TextGenerationInput { Messages = chatMessages }, - Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId, - Parameters = chatParameters - }, - cancellationToken); - var message = response.Output.Choices![0].Message; - var chatMessageContent = new ChatMessageContent( - new AuthorRole(message.Role), - message.Content, - metadata: response.ToMetaData()); - return [chatMessageContent]; + // no needs to invoke tool + return [chatMessageContent]; + } + + LogToolCalls(message.ToolCalls); + chat.Add(chatMessageContent); + + foreach (var call in message.ToolCalls) + { + if (call.Type is not ToolTypes.Function || call.Function is null) + { + AddResponseMessage(chat, null, "Error: Tool call was not a function call.", call.Id); + continue; + } + + // ensure not calling function that was not included in request list. + if (chatParameters.Tools?.Any( + x => string.Equals(x.Function?.Name, call.Function.Name, StringComparison.OrdinalIgnoreCase)) + != true) + { + AddResponseMessage( + chat, + null, + "Error: Function call requests for a function that wasn't defined.", + call.Id); + continue; + } + + object? callResult; + try + { + if (kernel!.Plugins.TryGetKernelFunctionAndArguments( + call.Function, + out var kernelFunction, + out var kernelArguments) + == false) + { + AddResponseMessage(chat, null, "Error: Requested function could not be found.", call.Id); + continue; + } + + var functionResult = await kernelFunction.InvokeAsync(kernel, kernelArguments, cancellationToken); + callResult = functionResult.GetValue() ?? string.Empty; + } + catch (JsonException) + { + AddResponseMessage(chat, null, "Error: Function call arguments were invalid JSON.", call.Id); + continue; + } + catch (Exception) + { + AddResponseMessage(chat, null, "Error: Exception while invoking function. {e.Message}", call.Id); + continue; + } + + var stringResult = ProcessFunctionResult(callResult, chatParameters.ToolCallBehavior); + AddResponseMessage(chat, stringResult, null, call.Id); + } + + chatParameters.Tools?.Clear(); + chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters); + if (it >= chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Maximum auto-invoke ({MaximumAutoInvoke}) reached", + chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + } } /// @@ -68,6 +158,7 @@ public async IAsyncEnumerable GetStreamingChatMessa var parameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings); parameters.IncrementalOutput = true; parameters.ResultFormat = ResultFormats.Message; + parameters.ToolCallBehavior?.ConfigureOptions(kernel, parameters); var responses = _dashScopeClient.GetTextCompletionStreamAsync( new ModelRequest { @@ -141,4 +232,88 @@ public async IAsyncEnumerable GetStreamingTextContentsAsyn metadata: response.ToMetaData()); } } + + private void CaptureTokenUsage(TextGenerationTokenUsage? usage) + { + if (usage is null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Usage info is not available"); + } + + return; + } + + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation( + "Input tokens: {InputTokens}. Output tokens: {CompletionTokens}. Total tokens: {TotalTokens}", + usage.InputTokens, + usage.OutputTokens, + usage.TotalTokens); + } + } + + private void LogToolCalls(IReadOnlyCollection? calls) + { + if (calls is null) + { + return; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Tool requests: {Requests}", calls.Count); + } + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace( + "Function call requests: {Requests}", + string.Join(", ", calls.Select(ftc => $"{ftc.Function?.Name}({ftc.Function?.Arguments})"))); + } + } + + private void AddResponseMessage(ChatHistory chat, string? result, string? errorMessage, string? toolId) + { + // Log any error + if (errorMessage is not null && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolId, errorMessage); + } + + // Add the tool response message to both the chat options and to the chat history. + result ??= errorMessage ?? string.Empty; + chat.Add(new DashScopeChatMessageContent(AuthorRole.Tool, result, name: toolId)); + } + + private static void EnsureChoiceExists(List? choices) + { + if (choices is null || choices.Count == 0) + { + throw new KernelException("No choice was returned from model"); + } + } + + private static string ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) + { + if (functionResult is string stringResult) + { + return stringResult; + } + + // This is an optimization to use ChatMessageContent content directly + // without unnecessary serialization of the whole message content class. + if (functionResult is ChatMessageContent chatMessageContent) + { + return chatMessageContent.ToString(); + } + + // For polymorphic serialization of unknown in advance child classes of the KernelContent class, + // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. + // For more details about the polymorphic serialization, see the article at: + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 + return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); + } } diff --git a/src/SemanticKernel.DashScope/DashScopeChatMessageContent.cs b/src/SemanticKernel.DashScope/DashScopeChatMessageContent.cs new file mode 100644 index 0000000..d9180e4 --- /dev/null +++ b/src/SemanticKernel.DashScope/DashScopeChatMessageContent.cs @@ -0,0 +1,27 @@ +using Cnblogs.DashScope.Core; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Cnblogs.SemanticKernel.Connectors.DashScope; + +/// +/// DashScope specialized message content +/// +public class DashScopeChatMessageContent( + AuthorRole role, + string content, + Dictionary? metadata = null, + string? name = null, + List? toolCalls = null) + : ChatMessageContent(role, content, metadata: metadata) +{ + /// + /// The name of tool if role is tool. + /// + public string? Name { get; } = name; + + /// + /// Optional tool calls. + /// + public List? ToolCalls { get; } = toolCalls; +} diff --git a/src/SemanticKernel.DashScope/DashScopeMapper.cs b/src/SemanticKernel.DashScope/DashScopeMapper.cs index c179171..6f3d983 100644 --- a/src/SemanticKernel.DashScope/DashScopeMapper.cs +++ b/src/SemanticKernel.DashScope/DashScopeMapper.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Microsoft.SemanticKernel.ChatCompletion; namespace Cnblogs.SemanticKernel.Connectors.DashScope; @@ -7,7 +7,16 @@ internal static class DashScopeMapper { public static List ToChatMessages(this ChatHistory history) { - return history.Select(x => new ChatMessage(x.Role.Label, x.Content ?? string.Empty)).ToList(); + return history.Select( + x => + { + if (x is DashScopeChatMessageContent d) + { + return new ChatMessage(x.Role.Label, x.Content ?? string.Empty, d.Name, ToolCalls: d.ToolCalls); + } + + return new ChatMessage(x.Role.Label, x.Content ?? string.Empty); + }).ToList(); } public static Dictionary? ToMetaData( diff --git a/src/SemanticKernel.DashScope/DashScopePromptExecutionSettings.cs b/src/SemanticKernel.DashScope/DashScopePromptExecutionSettings.cs index a0b9f87..ae448bd 100644 --- a/src/SemanticKernel.DashScope/DashScopePromptExecutionSettings.cs +++ b/src/SemanticKernel.DashScope/DashScopePromptExecutionSettings.cs @@ -1,7 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; namespace Cnblogs.SemanticKernel.Connectors.DashScope; @@ -40,6 +41,37 @@ public class DashScopePromptExecutionSettings : PromptExecutionSettings, ITextGe /// public bool? EnableSearch { get; set; } + /// + public List? Tools { get; internal set; } + + /// + /// Gets or sets the behavior for how tool calls are handled. + /// + /// + /// + /// To disable all tool calling, set the property to null (the default). + /// + /// To allow the model to request one of any number of functions, set the property to an + /// instance returned from , called with + /// a list of the functions available. + /// + /// + /// To allow the model to request one of any of the functions in the supplied , + /// set the property to if the client should simply + /// send the information about the functions and not handle the response in any special manner, or + /// if the client should attempt to automatically + /// invoke the function and send the result back to the service. + /// + /// + /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service + /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to + /// resolve that function from the functions available in the , and if found, rather + /// than returning the response back to the caller, it will handle the request automatically, invoking + /// the function, and sending back the result. The intermediate messages will be retained in the + /// if an instance was provided. + /// + public ToolCallBehavior? ToolCallBehavior { get; set; } + [return: NotNullIfNotNull(nameof(settings))] internal static DashScopePromptExecutionSettings? FromPromptExecutionSettings(PromptExecutionSettings? settings) { diff --git a/src/SemanticKernel.DashScope/DashScopeServiceCollectionExtensions.cs b/src/SemanticKernel.DashScope/DashScopeServiceCollectionExtensions.cs index c4e5043..f27c30d 100644 --- a/src/SemanticKernel.DashScope/DashScopeServiceCollectionExtensions.cs +++ b/src/SemanticKernel.DashScope/DashScopeServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.SemanticKernel.Connectors.DashScope; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; @@ -91,10 +92,16 @@ public static IServiceCollection AddDashScopeChatCompletion( { services.AddKeyedSingleton( serviceId, - (_, _) => new DashScopeChatCompletionService(modelId, new DashScopeClient(apiKey))); + (sp, _) => new DashScopeChatCompletionService( + modelId, + new DashScopeClient(apiKey), + sp.GetRequiredService>())); return services.AddKeyedSingleton( serviceId, - (_, _) => new DashScopeChatCompletionService(modelId, new DashScopeClient(apiKey))); + (sp, _) => new DashScopeChatCompletionService( + modelId, + new DashScopeClient(apiKey), + sp.GetRequiredService>())); } #endregion diff --git a/src/SemanticKernel.DashScope/DashScopeTextEmbeddingGenerationService.cs b/src/SemanticKernel.DashScope/DashScopeTextEmbeddingGenerationService.cs index 10ce78a..9cfe9fc 100644 --- a/src/SemanticKernel.DashScope/DashScopeTextEmbeddingGenerationService.cs +++ b/src/SemanticKernel.DashScope/DashScopeTextEmbeddingGenerationService.cs @@ -1,5 +1,4 @@ -using Cnblogs.DashScope.Sdk; -using Cnblogs.DashScope.Sdk.TextEmbedding; +using Cnblogs.DashScope.Core; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; @@ -34,7 +33,12 @@ public async Task>> GenerateEmbeddingsAsync( CancellationToken cancellationToken = new()) { var result = new List>(data.Count); - var embeddings = await _client.GetTextEmbeddingsAsync(_modelId, data, null, cancellationToken); + var embeddings = await _client.GetEmbeddingsAsync( + new ModelRequest + { + Model = _modelId, Input = new TextEmbeddingInput { Texts = data } + }, + cancellationToken); if (embeddings.Output.Embeddings.Count != data.Count) { throw new KernelException( diff --git a/src/SemanticKernel.DashScope/FunctionDefinition.cs b/src/SemanticKernel.DashScope/FunctionDefinition.cs new file mode 100644 index 0000000..b4238ee --- /dev/null +++ b/src/SemanticKernel.DashScope/FunctionDefinition.cs @@ -0,0 +1,32 @@ +using Cnblogs.DashScope.Core; +using Microsoft.SemanticKernel; + +namespace Cnblogs.SemanticKernel.Connectors.DashScope; + +/// +/// Function definition for model to use. +/// +public record FunctionDefinition : IFunctionDefinition +{ + /// + /// Creates a new function definition. + /// + /// The name of this function. + /// The description of this function. + /// Parameter map of this function. + public FunctionDefinition(string Name, string Description, KernelJsonSchema? Parameters) + { + this.Description = Description; + this.Name = Name; + this.Parameters = Parameters; + } + + /// + public string Name { get; init; } + + /// + public string Description { get; init; } + + /// + public object? Parameters { get; init; } +} diff --git a/src/SemanticKernel.DashScope/FunctionDefinitionConvertor.cs b/src/SemanticKernel.DashScope/FunctionDefinitionConvertor.cs new file mode 100644 index 0000000..62f7899 --- /dev/null +++ b/src/SemanticKernel.DashScope/FunctionDefinitionConvertor.cs @@ -0,0 +1,103 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Cnblogs.DashScope.Core; +using Json.Schema; +using Json.Schema.Generation; +using Microsoft.SemanticKernel; + +namespace Cnblogs.SemanticKernel.Connectors.DashScope; + +/// +/// Convertors from to +/// +internal static class FunctionDefinitionConvertor +{ + private static readonly KernelJsonSchema DefaultSchemaForTypelessParameter = + KernelJsonSchema.Parse("{\"type\":\"string\"}"); + + private const char FunctionNameSeparator = '-'; + + public static FunctionDefinition ToFunctionDefinition(this KernelFunctionMetadata metadata) + { + var required = new List(); + var properties = new Dictionary(); + foreach (var parameter in metadata.Parameters) + { + properties.Add( + parameter.Name, + parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); + if (parameter.IsRequired) + { + required.Add(parameter.Name); + } + } + + var schema = KernelJsonSchema.Parse( + JsonSerializer.Serialize( + new + { + type = "object", + required, + properties + })); + + var qualifiedName = string.IsNullOrEmpty(metadata.PluginName) + ? metadata.Name + : string.Join(FunctionNameSeparator, metadata.PluginName, metadata.Name); + return new FunctionDefinition(qualifiedName, metadata.Description, schema); + } + + public static bool TryGetKernelFunctionAndArguments( + this KernelPluginCollection collection, + FunctionCall functionCall, + [NotNullWhen(true)] out KernelFunction? function, + out KernelArguments? arguments) + { + var qualifiedName = functionCall.Name.AsSpan(); + var separatorIndex = qualifiedName.IndexOf(FunctionNameSeparator); + string? pluginName = null; + var functionName = functionCall.Name; + if (separatorIndex > 0) + { + pluginName = qualifiedName[..separatorIndex].Trim().ToString(); + functionName = qualifiedName[(separatorIndex + 1)..].Trim().ToString(); + } + + arguments = null; + if (collection.TryGetFunction(pluginName, functionName, out function) == false) + { + return false; + } + + if (string.IsNullOrEmpty(functionCall.Arguments)) + { + return true; + } + + var dic = JsonSerializer.Deserialize>(functionCall.Arguments)!; + arguments = new KernelArguments(); + foreach (var parameter in dic) + { + arguments[parameter.Key] = parameter.Value?.ToString(); + } + + return true; + } + + private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) + { + // If there's a description, incorporate it. + if (!string.IsNullOrWhiteSpace(description)) + { + return KernelJsonSchema.Parse( + JsonSerializer.Serialize( + new JsonSchemaBuilder() + .FromType(typeof(string)) + .Description(description) + .Build())); + } + + // Otherwise, we can use a cached schema for a string with no description. + return DefaultSchemaForTypelessParameter; + } +} diff --git a/src/SemanticKernel.DashScope/SemanticKernel.DashScope.csproj b/src/SemanticKernel.DashScope/SemanticKernel.DashScope.csproj index f9e50bf..30a6860 100644 --- a/src/SemanticKernel.DashScope/SemanticKernel.DashScope.csproj +++ b/src/SemanticKernel.DashScope/SemanticKernel.DashScope.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/src/SemanticKernel.DashScope/ToolCallBehavior.cs b/src/SemanticKernel.DashScope/ToolCallBehavior.cs new file mode 100644 index 0000000..5c5bbbf --- /dev/null +++ b/src/SemanticKernel.DashScope/ToolCallBehavior.cs @@ -0,0 +1,121 @@ +using System.Text.Json; +using Cnblogs.DashScope.Core; +using Microsoft.SemanticKernel; + +namespace Cnblogs.SemanticKernel.Connectors.DashScope; + +/// +/// Represents a behavior for DashScope tool calls. +/// +public abstract class ToolCallBehavior +{ + /// + /// The default maximum number of tool-call auto-invokes that can be made in a single request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled (e.g. will behave like )). + /// This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. It is currently hardcoded, but in the future it could + /// be made configurable by the developer. Other configuration is also possible in the future, + /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure + /// to find the requested function, failure to invoke the function, etc.), with behaviors for + /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call + /// support, where the model can request multiple tools in a single response, it is significantly + /// less likely that this limit is reached, as most of the time only a single request is needed. + /// + private const int DefaultMaximumAutoInvokeAttempts = 5; + + internal ToolCallBehavior(bool autoInvoke) + { + MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; + } + + /// + /// Options to control tool call result serialization behavior. + /// + public JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } + + internal int MaximumAutoInvokeAttempts { get; } + + /// Configures the with any tools this provides. + /// The used for the operation. This can be queried to determine what tools to provide into the . + /// The destination to configure. + internal abstract void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options); + + /// Gets an instance that will provide the specified list of functions to the model. + /// The functions that should be made available to the model. + /// true to attempt to automatically handle function call requests; otherwise, false. + /// + /// The that may be set into + /// to indicate that the specified functions should be made available to the model. + /// + public static ToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke) + { + return new EnabledFunctions(functions, autoInvoke); + } + + /// + /// Gets an instance that will provide all of the 's plugins' function information. + /// Function call requests from the model will be propagated back to the caller. + /// + /// + /// If no is available, no function information will be provided to the model. + /// + public static ToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); + + /// + /// Gets an instance that will both provide all of the 's plugins' function information + /// to the model and attempt to automatically handle any function call requests. + /// + /// + /// When successful, tool call requests from the model become an implementation detail, with the service + /// handling invoking any requested functions and supplying the results back to the model. + /// If no is available, no function information will be provided to the model. + /// + public static ToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); + + private sealed class EnabledFunctions(IEnumerable functions, bool autoInvoke = false) + : ToolCallBehavior(autoInvoke) + { + /// + internal override void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options) + { + // If no kernel is provided, we don't have any tools to provide. + if (kernel is null) + { + return; + } + + // Provide all functions from the kernel. + if (functions.Any()) + { + options.Tools = functions + .Select(x => new ToolDefinition(ToolTypes.Function, x.ToFunctionDefinition())) + .ToList(); + } + } + } + + private sealed class KernelFunctions(bool autoInvoke = false) : ToolCallBehavior(autoInvoke) + { + /// + internal override void ConfigureOptions(Kernel? kernel, DashScopePromptExecutionSettings options) + { + // If no kernel is provided, we don't have any tools to provide. + if (kernel is null) + { + return; + } + + // Provide all functions from the kernel. + var functions = kernel.Plugins.GetFunctionsMetadata(); + if (functions.Count > 0) + { + options.Tools = functions + .Select(x => new ToolDefinition(ToolTypes.Function, x.ToFunctionDefinition())) + .ToList(); + } + } + } +} diff --git a/test/KernelMemory.DashScope.UnitTests/Cases.cs b/test/KernelMemory.DashScope.UnitTests/Cases.cs index b010ab3..2c18f1c 100644 --- a/test/KernelMemory.DashScope.UnitTests/Cases.cs +++ b/test/KernelMemory.DashScope.UnitTests/Cases.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.KernelMemory.AI.DashScope; using Microsoft.KernelMemory.AI; diff --git a/test/KernelMemory.DashScope.UnitTests/DashScopeTextEmbeddingGeneratorTests.cs b/test/KernelMemory.DashScope.UnitTests/DashScopeTextEmbeddingGeneratorTests.cs index e184b92..6b790a0 100644 --- a/test/KernelMemory.DashScope.UnitTests/DashScopeTextEmbeddingGeneratorTests.cs +++ b/test/KernelMemory.DashScope.UnitTests/DashScopeTextEmbeddingGeneratorTests.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.KernelMemory.AI.DashScope; using FluentAssertions; using NSubstitute; diff --git a/test/KernelMemory.DashScope.UnitTests/DashScopeTextGeneratorTests.cs b/test/KernelMemory.DashScope.UnitTests/DashScopeTextGeneratorTests.cs index ea24626..fcc9dc1 100644 --- a/test/KernelMemory.DashScope.UnitTests/DashScopeTextGeneratorTests.cs +++ b/test/KernelMemory.DashScope.UnitTests/DashScopeTextGeneratorTests.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.KernelMemory.AI.DashScope; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; diff --git a/test/KernelMemory.DashScope.UnitTests/DependencyInjectorTests.cs b/test/KernelMemory.DashScope.UnitTests/DependencyInjectorTests.cs index c0417c6..3f4d446 100644 --- a/test/KernelMemory.DashScope.UnitTests/DependencyInjectorTests.cs +++ b/test/KernelMemory.DashScope.UnitTests/DependencyInjectorTests.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.KernelMemory.AI.DashScope; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/test/KernelMemory.DashScope.UnitTests/KernelMemory.DashScope.UnitTests.csproj b/test/KernelMemory.DashScope.UnitTests/KernelMemory.DashScope.UnitTests.csproj index 63b6017..a360ca5 100644 --- a/test/KernelMemory.DashScope.UnitTests/KernelMemory.DashScope.UnitTests.csproj +++ b/test/KernelMemory.DashScope.UnitTests/KernelMemory.DashScope.UnitTests.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/test/SemanticKernel.DashScope.UnitTest/Cases.cs b/test/SemanticKernel.DashScope.UnitTest/Cases.cs index 95228ea..93eb1be 100644 --- a/test/SemanticKernel.DashScope.UnitTest/Cases.cs +++ b/test/SemanticKernel.DashScope.UnitTest/Cases.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -19,7 +19,7 @@ public static class Cases public static readonly IConfiguration Configuration = new ConfigurationBuilder().AddInMemoryCollection( - new Dictionary() + new Dictionary { { "dashScope:apiKey", ApiKey }, { "dashScope:chatCompletionModelId", ModelId }, @@ -53,6 +53,90 @@ public static class Cases Usage = new(3) }; + public static KernelFunction NormalFunction(Action method) + => KernelFunctionFactory.CreateFromMethod( + (string location) => + { + method(); + return "Weather"; + }, + "GetCurrentWeather"); + + public static KernelFunction AlterFunction(Action method) + => KernelFunctionFactory.CreateFromMethod( + (string location) => + { + method(); + return "Weather"; + }, + "GetCurrentWeatherAlter"); + + public static KernelPlugin Plugin(params KernelFunction[] functions) + => KernelPluginFactory.CreateFromFunctions("MyPlugin", functions); + + public static ModelResponse ErrToolCallResponse( + KernelFunction[] functions, + string toolType = "function", + string pluginName = "MyPlugin", + string paramBody = "{\"location\": \"LA\"}") + => new() + { + Output = new() + { + Choices = + [ + new() + { + FinishReason = "tool_call", + Message = new( + "assistant", + string.Empty, + ToolCalls: functions.Select( + (f, i) => new ToolCall( + $"{i}", + toolType, + new($"{pluginName}-{f.Name}", paramBody))).ToList()) + } + ] + }, + Usage = new() + { + InputTokens = 10, + OutputTokens = 30, + TotalTokens = 40 + } + }; + + public static ModelResponse + ToolCallResponse(params KernelFunction[] functions) + => new() + { + Output = new() + { + Choices = + [ + new() + { + FinishReason = "tool_call", + Message = new( + "assistant", + string.Empty, + ToolCalls: functions.Select( + f => new ToolCall( + "0", + "function", + new($"MyPlugin-{f.Name}", "{\"location\": \"LA\"}"))).ToList()) + } + ] + }, + Usage = new() + { + InputTokens = 10, + OutputTokens = 30, + TotalTokens = 40 + } + }; + public static readonly ModelResponse ChatGenerationResponse = new() { Output = new() diff --git a/test/SemanticKernel.DashScope.UnitTest/ChatCompletionTests.cs b/test/SemanticKernel.DashScope.UnitTest/ChatCompletionTests.cs index 090447a..a54bf3e 100644 --- a/test/SemanticKernel.DashScope.UnitTest/ChatCompletionTests.cs +++ b/test/SemanticKernel.DashScope.UnitTest/ChatCompletionTests.cs @@ -1,8 +1,11 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.SemanticKernel.Connectors.DashScope; using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; using NSubstitute; +using NSubstitute.Core; using NSubstitute.Extensions; namespace SemanticKernel.DashScope.UnitTest; @@ -18,7 +21,10 @@ public async Task ChatCompletion_Normal_SuccessAsync(PromptExecutionSettings? se dashScopeClient.Configure() .GetTextCompletionAsync(Arg.Any>()) .Returns(Task.FromResult(Cases.ChatGenerationResponse)); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); // Act var response = await service.GetChatMessageContentsAsync(Cases.ChatHistory, settings); @@ -39,6 +45,229 @@ await dashScopeClient.Received().GetTextCompletionAsync( ]); } + [Fact] + public async Task ChatCompletion_ToolCalling_SuccessAsync() + { + // Arrange + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function = Cases.NormalFunction(() => functionCallCount++); + kernel.Plugins.Add(Cases.Plugin(function)); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns(Task.FromResult(Cases.ToolCallResponse(function)), Task.FromResult(Cases.ChatGenerationResponse)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + var history = new ChatHistory(); + + // Act + var response = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(1); + response.Should().HaveCount(1); // model response + history.Should().HaveCount(2); // tool response + model response + } + + [Fact] + public async Task ChatCompletion_MaximumToolCallingCount_SuccessAsync() + { + // Arrange + const int maximumAutoInvokeTime = 5; + const int autoInvokeResponsesCount = 6; + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function = Cases.NormalFunction(() => functionCallCount++); + kernel.Plugins.Add(Cases.Plugin(function)); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns( + Task.FromResult(Cases.ToolCallResponse(function)), + Enumerable.Range(0, autoInvokeResponsesCount - 1) + .Select(_ => Task.FromResult(Cases.ToolCallResponse(function))).ToArray()); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(maximumAutoInvokeTime, "tool can only be invoked below maximum auto invoke time"); + } + + [Fact] + public async Task ChatCompletion_ToolTypeIsNotFunction_SkipAsync() + { + // Arrange + const string nonFunctionToolType = "search"; + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function = Cases.NormalFunction(() => functionCallCount++); + kernel.Plugins.Add(Cases.Plugin(function)); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns( + Task.FromResult(Cases.ErrToolCallResponse([function], toolType: nonFunctionToolType)), + Task.FromResult(Cases.ChatGenerationResponse)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(0, "Tool type can only be function"); + } + + [Fact] + public async Task ChatCompletion_FunctionCallWithMalformedJson_SkipAsync() + { + // Arrange + const string malFormedJson = "invalid json"; + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function = Cases.NormalFunction(() => functionCallCount++); + kernel.Plugins.Add(Cases.Plugin(function)); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns( + Task.FromResult(Cases.ErrToolCallResponse([function], paramBody: malFormedJson)), + Task.FromResult(Cases.ChatGenerationResponse)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(0, "malformed json should be skipped"); + history.Should().HaveCount(2, "error message should be added to chat history"); + } + + [Fact] + public async Task ChatCompletion_FunctionThrowException_SkipAsync() + { + // Arrange + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function1 = Cases.NormalFunction(() => throw new InvalidOperationException()); + var function2 = Cases.AlterFunction(() => functionCallCount++); + kernel.Plugins.Add(Cases.Plugin(function1, function2)); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns( + Task.FromResult(Cases.ToolCallResponse(function1, function2)), + Task.FromResult(Cases.ChatGenerationResponse)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(1, "interrupted function call should be skipped"); + history.Should().HaveCount(3, "interrupted function call error message should be added to chat history"); + } + + [Fact] + public async Task ChatCompletion_FunctionDoesNotExists_SkipAsync() + { + // Arrange + var functionCallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function = Cases.NormalFunction(() => functionCallCount++); + var plugin = Cases.Plugin(function); + + // not adds function to kernel + // kernel.Plugins.Add(plugin); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns( + Task.FromResult(Cases.ToolCallResponse(function)), + Task.FromResult(Cases.ChatGenerationResponse)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.EnableFunctions(plugin.GetFunctionsMetadata(), autoInvoke: true) + }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + functionCallCount.Should().Be(0, "Should not call function that not exists in kernel"); + } + + [Fact] + public async Task ChatCompletion_CallingNotProvidedFunction_SkipAsync() + { + // Arrange + var function1CallCount = 0; + var function2CallCount = 0; + var kernel = Kernel.CreateBuilder().Build(); + var function1 = Cases.NormalFunction(() => function1CallCount++); + var function2 = Cases.AlterFunction(() => function2CallCount++); + kernel.Plugins.Add(Cases.Plugin(function1, function2)); + + var responseCallingFunction2 = Cases.ToolCallResponse(function2); + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionAsync(Arg.Any>()) + .Returns(Task.FromResult(responseCallingFunction2)); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = + new DashScopePromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.EnableFunctions([function1.Metadata], autoInvoke: true) + }; + var history = new ChatHistory(); + + // Act + _ = await service.GetChatMessageContentsAsync(history, settings, kernel); + + // Assert + function1CallCount.Should().Be(0, "can not invoke tools that was not provided in request"); + function2CallCount.Should().Be(0, "tools that not presented in response should not be called"); + } + [Fact] public async Task ChatCompletion_CustomModel_SuccessAsync() { @@ -47,8 +276,11 @@ public async Task ChatCompletion_CustomModel_SuccessAsync() dashScopeClient.Configure() .GetTextCompletionAsync(Arg.Any>()) .Returns(Task.FromResult(Cases.ChatGenerationResponse)); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); - var settings = new DashScopePromptExecutionSettings() { ModelId = Cases.ModelIdAlter }; + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; // Act _ = await service.GetChatMessageContentsAsync(Cases.ChatHistory, settings); @@ -68,7 +300,10 @@ public async Task ChatCompletionStream_Normal_SuccessAsync(PromptExecutionSettin dashScopeClient.Configure() .GetTextCompletionStreamAsync(Arg.Any>()) .Returns(list.ToAsyncEnumerable()); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); // Act var response = await service.GetStreamingChatMessageContentsAsync(Cases.ChatHistory, settings).ToListAsync(); @@ -98,8 +333,11 @@ public async Task ChatCompletionStream_CustomModel_SuccessAsync() dashScopeClient.Configure() .GetTextCompletionStreamAsync(Arg.Any>()) .Returns(list.ToAsyncEnumerable()); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); - var settings = new DashScopePromptExecutionSettings() { ModelId = Cases.ModelIdAlter }; + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + MockLoggerFactory.MockLogger()); + var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; // Act _ = await service.GetStreamingChatMessageContentsAsync(Cases.ChatHistory, settings).ToListAsync(); @@ -114,6 +352,6 @@ public static TheoryData Settings { null, new DashScopePromptExecutionSettings { Seed = 1000 }, - new PromptExecutionSettings { ExtensionData = new Dictionary() { { "seed", 1000 } } } + new PromptExecutionSettings { ExtensionData = new Dictionary { { "seed", 1000 } } } }; } diff --git a/test/SemanticKernel.DashScope.UnitTest/MockLoggerFactory.cs b/test/SemanticKernel.DashScope.UnitTest/MockLoggerFactory.cs new file mode 100644 index 0000000..48e86c2 --- /dev/null +++ b/test/SemanticKernel.DashScope.UnitTest/MockLoggerFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Extensions; + +namespace SemanticKernel.DashScope.UnitTest; + +public static class MockLoggerFactory +{ + public static ILogger MockLogger() + { + var logger = Substitute.For>(); + logger.Configure().IsEnabled(Arg.Any()).Returns(true); + return logger; + } +} diff --git a/test/SemanticKernel.DashScope.UnitTest/SemanticKernel.DashScope.UnitTest.csproj b/test/SemanticKernel.DashScope.UnitTest/SemanticKernel.DashScope.UnitTest.csproj index 5861fd0..f84832f 100644 --- a/test/SemanticKernel.DashScope.UnitTest/SemanticKernel.DashScope.UnitTest.csproj +++ b/test/SemanticKernel.DashScope.UnitTest/SemanticKernel.DashScope.UnitTest.csproj @@ -1,6 +1,6 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/SemanticKernel.DashScope.UnitTest/ServiceCollectionExtensionsTests.cs b/test/SemanticKernel.DashScope.UnitTest/ServiceCollectionExtensionsTests.cs index 1354562..27f7afd 100644 --- a/test/SemanticKernel.DashScope.UnitTest/ServiceCollectionExtensionsTests.cs +++ b/test/SemanticKernel.DashScope.UnitTest/ServiceCollectionExtensionsTests.cs @@ -17,6 +17,7 @@ public void ServiceCollectionExtension_AddChatService_AddTextAndChatService(Init { // Arrange var builder = Kernel.CreateBuilder(); + builder.Services.AddLogging(); // Act _ = type switch diff --git a/test/SemanticKernel.DashScope.UnitTest/TextCompletionTests.cs b/test/SemanticKernel.DashScope.UnitTest/TextCompletionTests.cs index a473a96..890b702 100644 --- a/test/SemanticKernel.DashScope.UnitTest/TextCompletionTests.cs +++ b/test/SemanticKernel.DashScope.UnitTest/TextCompletionTests.cs @@ -1,6 +1,7 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.SemanticKernel.Connectors.DashScope; using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using NSubstitute; using NSubstitute.Extensions; @@ -18,7 +19,10 @@ public async Task GetTextContent_Normal_SuccessAsync(PromptExecutionSettings? se dashScopeClient.Configure() .GetTextCompletionAsync(Arg.Any>()) .Returns(Task.FromResult(Cases.TextGenerationResponse)); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + NullLogger.Instance); // Act var response = await service.GetTextContentsAsync(Cases.Prompt, settings); @@ -47,8 +51,11 @@ public async Task GetTextContent_OverrideModelId_SuccessAsync() dashScopeClient.Configure() .GetTextCompletionAsync(Arg.Any>()) .Returns(Task.FromResult(Cases.TextGenerationResponse)); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); - var settings = new DashScopePromptExecutionSettings() { ModelId = Cases.ModelIdAlter }; + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + NullLogger.Instance); + var settings = new DashScopePromptExecutionSettings { ModelId = Cases.ModelIdAlter }; // Act _ = await service.GetTextContentsAsync(Cases.Prompt, settings); @@ -68,7 +75,10 @@ public async Task GetTextContentStream_Normal_SuccessAsync(PromptExecutionSettin dashScopeClient.Configure() .GetTextCompletionStreamAsync(Arg.Any>()) .Returns(list.ToAsyncEnumerable()); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + NullLogger.Instance); // Act var response = await service.GetStreamingTextContentsAsync(Cases.Prompt, settings).ToListAsync(); @@ -98,7 +108,10 @@ public async Task GetTextContentStream_OverrideModelId_SuccessAsync() dashScopeClient.Configure() .GetTextCompletionStreamAsync(Arg.Any>()) .Returns(list.ToAsyncEnumerable()); - var service = new DashScopeChatCompletionService(Cases.ModelId, dashScopeClient); + var service = new DashScopeChatCompletionService( + Cases.ModelId, + dashScopeClient, + NullLogger.Instance); var settings = new PromptExecutionSettings { ModelId = Cases.ModelIdAlter }; // Act @@ -114,6 +127,6 @@ public static TheoryData Settings { null, new DashScopePromptExecutionSettings { Seed = 1000 }, - new PromptExecutionSettings { ExtensionData = new Dictionary() { { "seed", 1000 } } } + new PromptExecutionSettings { ExtensionData = new Dictionary { { "seed", 1000 } } } }; } diff --git a/test/SemanticKernel.DashScope.UnitTest/TextEmbeddingTests.cs b/test/SemanticKernel.DashScope.UnitTest/TextEmbeddingTests.cs index 7155c6b..991bf5a 100644 --- a/test/SemanticKernel.DashScope.UnitTest/TextEmbeddingTests.cs +++ b/test/SemanticKernel.DashScope.UnitTest/TextEmbeddingTests.cs @@ -1,4 +1,4 @@ -using Cnblogs.DashScope.Sdk; +using Cnblogs.DashScope.Core; using Cnblogs.SemanticKernel.Connectors.DashScope; using FluentAssertions; using NSubstitute; From 041318fd6014bafc9c8525e7d5f0cb54ff11fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Mon, 18 Mar 2024 18:12:33 +0800 Subject: [PATCH 2/2] chore: code cleanup --- src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs b/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs index 1f56701..ff85f4d 100644 --- a/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs +++ b/src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Text.Json; using Cnblogs.DashScope.Core; using Microsoft.Extensions.Logging;