Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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‎

Lines changed: 8 additions & 3 deletions
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 2 additions & 2 deletions
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‎

Lines changed: 193 additions & 18 deletions
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
}
Lines changed: 27 additions & 0 deletions
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‎

Lines changed: 11 additions & 2 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /