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 ;
3
6
using Microsoft . SemanticKernel ;
4
7
using Microsoft . SemanticKernel . ChatCompletion ;
5
8
using Microsoft . SemanticKernel . Services ;
@@ -15,45 +18,132 @@ public sealed class DashScopeChatCompletionService : IChatCompletionService, ITe
15
18
private readonly IDashScopeClient _dashScopeClient ;
16
19
private readonly Dictionary < string , object ? > _attributes = new ( ) ;
17
20
private readonly string _modelId ;
21
+ private readonly ILogger < DashScopeChatCompletionService > _logger ;
18
22
19
23
/// <summary>
20
24
/// Creates a new DashScope chat completion service.
21
25
/// </summary>
22
26
/// <param name="modelId"></param>
23
27
/// <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 )
25
33
{
26
34
_dashScopeClient = dashScopeClient ;
27
35
_modelId = modelId ;
36
+ _logger = logger ;
28
37
_attributes . Add ( AIServiceExtensions . ModelIdKey , _modelId ) ;
29
38
}
30
39
31
40
/// <inheritdoc />
32
41
public async Task < IReadOnlyList < ChatMessageContent > > GetChatMessageContentsAsync (
33
- ChatHistory chatHistory ,
42
+ ChatHistory chat ,
34
43
PromptExecutionSettings ? executionSettings = null ,
35
44
Kernel ? kernel = null ,
36
45
CancellationToken cancellationToken = default )
37
46
{
38
- var chatMessages = chatHistory . ToChatMessages ( ) ;
39
47
var chatParameters = DashScopePromptExecutionSettings . FromPromptExecutionSettings ( executionSettings ) ;
40
48
chatParameters ??= new DashScopePromptExecutionSettings ( ) ;
41
49
chatParameters . IncrementalOutput = false ;
42
50
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 )
45
74
{
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
+ }
57
147
}
58
148
59
149
/// <inheritdoc />
@@ -68,6 +158,7 @@ public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessa
68
158
var parameters = DashScopePromptExecutionSettings . FromPromptExecutionSettings ( executionSettings ) ;
69
159
parameters . IncrementalOutput = true ;
70
160
parameters . ResultFormat = ResultFormats . Message ;
161
+ parameters . ToolCallBehavior ? . ConfigureOptions ( kernel , parameters ) ;
71
162
var responses = _dashScopeClient . GetTextCompletionStreamAsync (
72
163
new ModelRequest < TextGenerationInput , ITextGenerationParameters >
73
164
{
@@ -141,4 +232,88 @@ public async IAsyncEnumerable<StreamingTextContent> GetStreamingTextContentsAsyn
141
232
metadata : response . ToMetaData ( ) ) ;
142
233
}
143
234
}
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
+ }
144
319
}
0 commit comments