Skip to content

Commit 40b9d2f

Browse files
committed
feat: support auto invoke functions for non-stream chat
1 parent b2d1e95 commit 40b9d2f

26 files changed

+927
-61
lines changed

src/KernelMemory.DashScope/DashScopeTextEmbeddingGenerator.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using Cnblogs.DashScope.Sdk;
2-
using Cnblogs.DashScope.Sdk.TextEmbedding;
1+
using Cnblogs.DashScope.Core;
32
using Microsoft.KernelMemory;
43
using Microsoft.KernelMemory.AI;
54

@@ -30,7 +29,13 @@ public async Task<Embedding> GenerateEmbeddingAsync(
3029
string text,
3130
CancellationToken cancellationToken = new())
3231
{
33-
var result = await dashScopeClient.GetTextEmbeddingsAsync(modelId, [text], null, cancellationToken);
32+
var result = await dashScopeClient.GetEmbeddingsAsync(
33+
new ModelRequest<TextEmbeddingInput, ITextEmbeddingParameters>
34+
{
35+
Input = new TextEmbeddingInput { Texts = [text] },
36+
Model = modelId
37+
},
38+
cancellationToken);
3439
return result.Output.Embeddings[0].Embedding;
3540
}
3641

src/KernelMemory.DashScope/DashScopeTextGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using System.Runtime.CompilerServices;
2-
using Cnblogs.DashScope.Sdk;
2+
using Cnblogs.DashScope.Core;
33
using Microsoft.Extensions.Logging;
44
using Microsoft.KernelMemory.AI;
55
using Microsoft.KernelMemory.Diagnostics;

src/KernelMemory.DashScope/DependencyInjector.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Cnblogs.DashScope.Sdk;
1+
using Cnblogs.DashScope.Core;
22
using Cnblogs.KernelMemory.AI.DashScope;
33
using Microsoft.Extensions.Configuration;
44
using Microsoft.Extensions.DependencyInjection;

src/KernelMemory.DashScope/KernelMemory.DashScope.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
<ItemGroup>
2020
<PackageReference Include="Microsoft.DeepDev.TokenizerLib" Version="1.3.3" />
21-
<PackageReference Include="Microsoft.KernelMemory.Abstractions" Version="0.32.240307.1"/>
22-
<PackageReference Include="Cnblogs.DashScope.Sdk" Version="0.0.3"/>
21+
<PackageReference Include="Microsoft.KernelMemory.Abstractions" Version="0.34.240313.1" />
22+
<PackageReference Include="Cnblogs.DashScope.Core" Version="0.2.0" />
2323
</ItemGroup>
2424

2525
<ItemGroup>

src/SemanticKernel.DashScope/DashScopeChatCompletionService.cs

+193-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
using System.Runtime.CompilerServices;
2-
using Cnblogs.DashScope.Sdk;
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.CompilerServices;
3+
using System.Text.Json;
4+
using Cnblogs.DashScope.Core;
5+
using Microsoft.Extensions.Logging;
36
using Microsoft.SemanticKernel;
47
using Microsoft.SemanticKernel.ChatCompletion;
58
using Microsoft.SemanticKernel.Services;
@@ -15,45 +18,132 @@ public sealed class DashScopeChatCompletionService : IChatCompletionService, ITe
1518
private readonly IDashScopeClient _dashScopeClient;
1619
private readonly Dictionary<string, object?> _attributes = new();
1720
private readonly string _modelId;
21+
private readonly ILogger<DashScopeChatCompletionService> _logger;
1822

1923
/// <summary>
2024
/// Creates a new DashScope chat completion service.
2125
/// </summary>
2226
/// <param name="modelId"></param>
2327
/// <param name="dashScopeClient"></param>
24-
public DashScopeChatCompletionService(string modelId, IDashScopeClient dashScopeClient)
28+
/// <param name="logger"></param>
29+
public DashScopeChatCompletionService(
30+
string modelId,
31+
IDashScopeClient dashScopeClient,
32+
ILogger<DashScopeChatCompletionService> logger)
2533
{
2634
_dashScopeClient = dashScopeClient;
2735
_modelId = modelId;
36+
_logger = logger;
2837
_attributes.Add(AIServiceExtensions.ModelIdKey, _modelId);
2938
}
3039

3140
/// <inheritdoc />
3241
public async Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(
33-
ChatHistory chatHistory,
42+
ChatHistory chat,
3443
PromptExecutionSettings? executionSettings = null,
3544
Kernel? kernel = null,
3645
CancellationToken cancellationToken = default)
3746
{
38-
var chatMessages = chatHistory.ToChatMessages();
3947
var chatParameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings);
4048
chatParameters ??= new DashScopePromptExecutionSettings();
4149
chatParameters.IncrementalOutput = false;
4250
chatParameters.ResultFormat = ResultFormats.Message;
43-
var response = await _dashScopeClient.GetTextCompletionAsync(
44-
new ModelRequest<TextGenerationInput, ITextGenerationParameters>
51+
chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters);
52+
53+
var autoInvoke = kernel is not null && chatParameters.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0;
54+
for (var it = 1;; it++)
55+
{
56+
var response = await _dashScopeClient.GetTextCompletionAsync(
57+
new ModelRequest<TextGenerationInput, ITextGenerationParameters>
58+
{
59+
Input = new TextGenerationInput { Messages = chat.ToChatMessages() },
60+
Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId,
61+
Parameters = chatParameters
62+
},
63+
cancellationToken);
64+
CaptureTokenUsage(response.Usage);
65+
EnsureChoiceExists(response.Output.Choices);
66+
var message = response.Output.Choices![0].Message;
67+
var chatMessageContent = new DashScopeChatMessageContent(
68+
new AuthorRole(message.Role),
69+
message.Content,
70+
name: null,
71+
toolCalls: message.ToolCalls,
72+
metadata: response.ToMetaData());
73+
if (autoInvoke == false || message.ToolCalls is null)
4574
{
46-
Input = new TextGenerationInput { Messages = chatMessages },
47-
Model = string.IsNullOrEmpty(chatParameters.ModelId) ? _modelId : chatParameters.ModelId,
48-
Parameters = chatParameters
49-
},
50-
cancellationToken);
51-
var message = response.Output.Choices![0].Message;
52-
var chatMessageContent = new ChatMessageContent(
53-
new AuthorRole(message.Role),
54-
message.Content,
55-
metadata: response.ToMetaData());
56-
return [chatMessageContent];
75+
// no needs to invoke tool
76+
return [chatMessageContent];
77+
}
78+
79+
LogToolCalls(message.ToolCalls);
80+
chat.Add(chatMessageContent);
81+
82+
foreach (var call in message.ToolCalls)
83+
{
84+
if (call.Type is not ToolTypes.Function || call.Function is null)
85+
{
86+
AddResponseMessage(chat, null, "Error: Tool call was not a function call.", call.Id);
87+
continue;
88+
}
89+
90+
// ensure not calling function that was not included in request list.
91+
if (chatParameters.Tools?.Any(
92+
x => string.Equals(x.Function?.Name, call.Function.Name, StringComparison.OrdinalIgnoreCase))
93+
!= true)
94+
{
95+
AddResponseMessage(
96+
chat,
97+
null,
98+
"Error: Function call requests for a function that wasn't defined.",
99+
call.Id);
100+
continue;
101+
}
102+
103+
object? callResult;
104+
try
105+
{
106+
if (kernel!.Plugins.TryGetKernelFunctionAndArguments(
107+
call.Function,
108+
out var kernelFunction,
109+
out var kernelArguments)
110+
== false)
111+
{
112+
AddResponseMessage(chat, null, "Error: Requested function could not be found.", call.Id);
113+
continue;
114+
}
115+
116+
var functionResult = await kernelFunction.InvokeAsync(kernel, kernelArguments, cancellationToken);
117+
callResult = functionResult.GetValue<object>() ?? string.Empty;
118+
}
119+
catch (JsonException)
120+
{
121+
AddResponseMessage(chat, null, "Error: Function call arguments were invalid JSON.", call.Id);
122+
continue;
123+
}
124+
catch (Exception)
125+
{
126+
AddResponseMessage(chat, null, "Error: Exception while invoking function. {e.Message}", call.Id);
127+
continue;
128+
}
129+
130+
var stringResult = ProcessFunctionResult(callResult, chatParameters.ToolCallBehavior);
131+
AddResponseMessage(chat, stringResult, null, call.Id);
132+
}
133+
134+
chatParameters.Tools?.Clear();
135+
chatParameters.ToolCallBehavior?.ConfigureOptions(kernel, chatParameters);
136+
if (it >= chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts)
137+
{
138+
autoInvoke = false;
139+
if (_logger.IsEnabled(LogLevel.Debug))
140+
{
141+
_logger.LogDebug(
142+
"Maximum auto-invoke ({MaximumAutoInvoke}) reached",
143+
chatParameters.ToolCallBehavior!.MaximumAutoInvokeAttempts);
144+
}
145+
}
146+
}
57147
}
58148

59149
/// <inheritdoc />
@@ -68,6 +158,7 @@ public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessa
68158
var parameters = DashScopePromptExecutionSettings.FromPromptExecutionSettings(executionSettings);
69159
parameters.IncrementalOutput = true;
70160
parameters.ResultFormat = ResultFormats.Message;
161+
parameters.ToolCallBehavior?.ConfigureOptions(kernel, parameters);
71162
var responses = _dashScopeClient.GetTextCompletionStreamAsync(
72163
new ModelRequest<TextGenerationInput, ITextGenerationParameters>
73164
{
@@ -141,4 +232,88 @@ public async IAsyncEnumerable<StreamingTextContent> GetStreamingTextContentsAsyn
141232
metadata: response.ToMetaData());
142233
}
143234
}
235+
236+
private void CaptureTokenUsage(TextGenerationTokenUsage? usage)
237+
{
238+
if (usage is null)
239+
{
240+
if (_logger.IsEnabled(LogLevel.Debug))
241+
{
242+
_logger.LogDebug("Usage info is not available");
243+
}
244+
245+
return;
246+
}
247+
248+
if (_logger.IsEnabled(LogLevel.Information))
249+
{
250+
_logger.LogInformation(
251+
"Input tokens: {InputTokens}. Output tokens: {CompletionTokens}. Total tokens: {TotalTokens}",
252+
usage.InputTokens,
253+
usage.OutputTokens,
254+
usage.TotalTokens);
255+
}
256+
}
257+
258+
private void LogToolCalls(IReadOnlyCollection<ToolCall>? calls)
259+
{
260+
if (calls is null)
261+
{
262+
return;
263+
}
264+
265+
if (_logger.IsEnabled(LogLevel.Debug))
266+
{
267+
_logger.LogDebug("Tool requests: {Requests}", calls.Count);
268+
}
269+
270+
if (_logger.IsEnabled(LogLevel.Trace))
271+
{
272+
_logger.LogTrace(
273+
"Function call requests: {Requests}",
274+
string.Join(", ", calls.Select(ftc => $"{ftc.Function?.Name}({ftc.Function?.Arguments})")));
275+
}
276+
}
277+
278+
private void AddResponseMessage(ChatHistory chat, string? result, string? errorMessage, string? toolId)
279+
{
280+
// Log any error
281+
if (errorMessage is not null && _logger.IsEnabled(LogLevel.Debug))
282+
{
283+
_logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolId, errorMessage);
284+
}
285+
286+
// Add the tool response message to both the chat options and to the chat history.
287+
result ??= errorMessage ?? string.Empty;
288+
chat.Add(new DashScopeChatMessageContent(AuthorRole.Tool, result, name: toolId));
289+
}
290+
291+
private static void EnsureChoiceExists(List<TextGenerationChoice>? choices)
292+
{
293+
if (choices is null || choices.Count == 0)
294+
{
295+
throw new KernelException("No choice was returned from model");
296+
}
297+
}
298+
299+
private static string ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior)
300+
{
301+
if (functionResult is string stringResult)
302+
{
303+
return stringResult;
304+
}
305+
306+
// This is an optimization to use ChatMessageContent content directly
307+
// without unnecessary serialization of the whole message content class.
308+
if (functionResult is ChatMessageContent chatMessageContent)
309+
{
310+
return chatMessageContent.ToString();
311+
}
312+
313+
// For polymorphic serialization of unknown in advance child classes of the KernelContent class,
314+
// a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property.
315+
// For more details about the polymorphic serialization, see the article at:
316+
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0
317+
return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions);
318+
}
144319
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Cnblogs.DashScope.Core;
2+
using Microsoft.SemanticKernel;
3+
using Microsoft.SemanticKernel.ChatCompletion;
4+
5+
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
6+
7+
/// <summary>
8+
/// DashScope specialized message content
9+
/// </summary>
10+
public class DashScopeChatMessageContent(
11+
AuthorRole role,
12+
string content,
13+
Dictionary<string, object?>? metadata = null,
14+
string? name = null,
15+
List<ToolCall>? toolCalls = null)
16+
: ChatMessageContent(role, content, metadata: metadata)
17+
{
18+
/// <summary>
19+
/// The name of tool if role is tool.
20+
/// </summary>
21+
public string? Name { get; } = name;
22+
23+
/// <summary>
24+
/// Optional tool calls.
25+
/// </summary>
26+
public List<ToolCall>? ToolCalls { get; } = toolCalls;
27+
}

src/SemanticKernel.DashScope/DashScopeMapper.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Cnblogs.DashScope.Sdk;
1+
using Cnblogs.DashScope.Core;
22
using Microsoft.SemanticKernel.ChatCompletion;
33

44
namespace Cnblogs.SemanticKernel.Connectors.DashScope;
@@ -7,7 +7,16 @@ internal static class DashScopeMapper
77
{
88
public static List<ChatMessage> ToChatMessages(this ChatHistory history)
99
{
10-
return history.Select(x => new ChatMessage(x.Role.Label, x.Content ?? string.Empty)).ToList();
10+
return history.Select(
11+
x =>
12+
{
13+
if (x is DashScopeChatMessageContent d)
14+
{
15+
return new ChatMessage(x.Role.Label, x.Content ?? string.Empty, d.Name, ToolCalls: d.ToolCalls);
16+
}
17+
18+
return new ChatMessage(x.Role.Label, x.Content ?? string.Empty);
19+
}).ToList();
1120
}
1221

1322
public static Dictionary<string, object?>? ToMetaData<TOutput, TUsage>(

0 commit comments

Comments
 (0)