1
1
using System . Runtime . CompilerServices ;
2
- using Cnblogs . DashScope . Sdk ;
2
+ using System . Text . Json ;
3
+ using Cnblogs . DashScope . Core ;
4
+ using Microsoft . Extensions . Logging ;
3
5
using Microsoft . SemanticKernel ;
4
6
using Microsoft . SemanticKernel . ChatCompletion ;
5
7
using Microsoft . SemanticKernel . Services ;
@@ -15,45 +17,132 @@ public sealed class DashScopeChatCompletionService : IChatCompletionService, ITe
15
17
private readonly IDashScopeClient _dashScopeClient ;
16
18
private readonly Dictionary < string , object ? > _attributes = new ( ) ;
17
19
private readonly string _modelId ;
20
+ private readonly ILogger < DashScopeChatCompletionService > _logger ;
18
21
19
22
/// <summary>
20
23
/// Creates a new DashScope chat completion service.
21
24
/// </summary>
22
25
/// <param name="modelId"></param>
23
26
/// <param name="dashScopeClient"></param>
24
- public DashScopeChatCompletionService ( string modelId , IDashScopeClient dashScopeClient )
27
+ /// <param name="logger"></param>
28
+ public DashScopeChatCompletionService (
29
+ string modelId ,
30
+ IDashScopeClient dashScopeClient ,
31
+ ILogger < DashScopeChatCompletionService > logger )
25
32
{
26
33
_dashScopeClient = dashScopeClient ;
27
34
_modelId = modelId ;
35
+ _logger = logger ;
28
36
_attributes . Add ( AIServiceExtensions . ModelIdKey , _modelId ) ;
29
37
}
30
38
31
39
/// <inheritdoc />
32
40
public async Task < IReadOnlyList < ChatMessageContent > > GetChatMessageContentsAsync (
33
- ChatHistory chatHistory ,
41
+ ChatHistory chat ,
34
42
PromptExecutionSettings ? executionSettings = null ,
35
43
Kernel ? kernel = null ,
36
44
CancellationToken cancellationToken = default )
37
45
{
38
- var chatMessages = chatHistory . ToChatMessages ( ) ;
39
46
var chatParameters = DashScopePromptExecutionSettings . FromPromptExecutionSettings ( executionSettings ) ;
40
47
chatParameters ??= new DashScopePromptExecutionSettings ( ) ;
41
48
chatParameters . IncrementalOutput = false ;
42
49
chatParameters . ResultFormat = ResultFormats . Message ;
43
- var response = await _dashScopeClient . GetTextCompletionAsync (
44
- new ModelRequest < TextGenerationInput , ITextGenerationParameters >
50
+ chatParameters . ToolCallBehavior ? . ConfigureOptions ( kernel , chatParameters ) ;
51
+
52
+ var autoInvoke = kernel is not null && chatParameters . ToolCallBehavior ? . MaximumAutoInvokeAttempts > 0 ;
53
+ for ( var it = 1 ; ; it ++ )
54
+ {
55
+ var response = await _dashScopeClient . GetTextCompletionAsync (
56
+ new ModelRequest < TextGenerationInput , ITextGenerationParameters >
57
+ {
58
+ Input = new TextGenerationInput { Messages = chat . ToChatMessages ( ) } ,
59
+ Model = string . IsNullOrEmpty ( chatParameters . ModelId ) ? _modelId : chatParameters . ModelId ,
60
+ Parameters = chatParameters
61
+ } ,
62
+ cancellationToken ) ;
63
+ CaptureTokenUsage ( response . Usage ) ;
64
+ EnsureChoiceExists ( response . Output . Choices ) ;
65
+ var message = response . Output . Choices ! [ 0 ] . Message ;
66
+ var chatMessageContent = new DashScopeChatMessageContent (
67
+ new AuthorRole ( message . Role ) ,
68
+ message . Content ,
69
+ name : null ,
70
+ toolCalls : message . ToolCalls ,
71
+ metadata : response . ToMetaData ( ) ) ;
72
+ if ( autoInvoke == false || message . ToolCalls is null )
45
73
{
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 ] ;
74
+ // no needs to invoke tool
75
+ return [ chatMessageContent ] ;
76
+ }
77
+
78
+ LogToolCalls ( message . ToolCalls ) ;
79
+ chat . Add ( chatMessageContent ) ;
80
+
81
+ foreach ( var call in message . ToolCalls )
82
+ {
83
+ if ( call . Type is not ToolTypes . Function || call . Function is null )
84
+ {
85
+ AddResponseMessage ( chat , null , "Error: Tool call was not a function call." , call . Id ) ;
86
+ continue ;
87
+ }
88
+
89
+ // ensure not calling function that was not included in request list.
90
+ if ( chatParameters . Tools ? . Any (
91
+ x => string . Equals ( x . Function ? . Name , call . Function . Name , StringComparison . OrdinalIgnoreCase ) )
92
+ != true )
93
+ {
94
+ AddResponseMessage (
95
+ chat ,
96
+ null ,
97
+ "Error: Function call requests for a function that wasn't defined." ,
98
+ call . Id ) ;
99
+ continue ;
100
+ }
101
+
102
+ object ? callResult ;
103
+ try
104
+ {
105
+ if ( kernel ! . Plugins . TryGetKernelFunctionAndArguments (
106
+ call . Function ,
107
+ out var kernelFunction ,
108
+ out var kernelArguments )
109
+ == false )
110
+ {
111
+ AddResponseMessage ( chat , null , "Error: Requested function could not be found." , call . Id ) ;
112
+ continue ;
113
+ }
114
+
115
+ var functionResult = await kernelFunction . InvokeAsync ( kernel , kernelArguments , cancellationToken ) ;
116
+ callResult = functionResult . GetValue < object > ( ) ?? string . Empty ;
117
+ }
118
+ catch ( JsonException )
119
+ {
120
+ AddResponseMessage ( chat , null , "Error: Function call arguments were invalid JSON." , call . Id ) ;
121
+ continue ;
122
+ }
123
+ catch ( Exception )
124
+ {
125
+ AddResponseMessage ( chat , null , "Error: Exception while invoking function. {e.Message}" , call . Id ) ;
126
+ continue ;
127
+ }
128
+
129
+ var stringResult = ProcessFunctionResult ( callResult , chatParameters . ToolCallBehavior ) ;
130
+ AddResponseMessage ( chat , stringResult , null , call . Id ) ;
131
+ }
132
+
133
+ chatParameters . Tools ? . Clear ( ) ;
134
+ chatParameters . ToolCallBehavior ? . ConfigureOptions ( kernel , chatParameters ) ;
135
+ if ( it >= chatParameters . ToolCallBehavior ! . MaximumAutoInvokeAttempts )
136
+ {
137
+ autoInvoke = false ;
138
+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
139
+ {
140
+ _logger . LogDebug (
141
+ "Maximum auto-invoke ({MaximumAutoInvoke}) reached" ,
142
+ chatParameters . ToolCallBehavior ! . MaximumAutoInvokeAttempts ) ;
143
+ }
144
+ }
145
+ }
57
146
}
58
147
59
148
/// <inheritdoc />
@@ -68,6 +157,7 @@ public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessa
68
157
var parameters = DashScopePromptExecutionSettings . FromPromptExecutionSettings ( executionSettings ) ;
69
158
parameters . IncrementalOutput = true ;
70
159
parameters . ResultFormat = ResultFormats . Message ;
160
+ parameters . ToolCallBehavior ? . ConfigureOptions ( kernel , parameters ) ;
71
161
var responses = _dashScopeClient . GetTextCompletionStreamAsync (
72
162
new ModelRequest < TextGenerationInput , ITextGenerationParameters >
73
163
{
@@ -141,4 +231,88 @@ public async IAsyncEnumerable<StreamingTextContent> GetStreamingTextContentsAsyn
141
231
metadata : response . ToMetaData ( ) ) ;
142
232
}
143
233
}
234
+
235
+ private void CaptureTokenUsage ( TextGenerationTokenUsage ? usage )
236
+ {
237
+ if ( usage is null )
238
+ {
239
+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
240
+ {
241
+ _logger . LogDebug ( "Usage info is not available" ) ;
242
+ }
243
+
244
+ return ;
245
+ }
246
+
247
+ if ( _logger . IsEnabled ( LogLevel . Information ) )
248
+ {
249
+ _logger . LogInformation (
250
+ "Input tokens: {InputTokens}. Output tokens: {CompletionTokens}. Total tokens: {TotalTokens}" ,
251
+ usage . InputTokens ,
252
+ usage . OutputTokens ,
253
+ usage . TotalTokens ) ;
254
+ }
255
+ }
256
+
257
+ private void LogToolCalls ( IReadOnlyCollection < ToolCall > ? calls )
258
+ {
259
+ if ( calls is null )
260
+ {
261
+ return ;
262
+ }
263
+
264
+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
265
+ {
266
+ _logger . LogDebug ( "Tool requests: {Requests}" , calls . Count ) ;
267
+ }
268
+
269
+ if ( _logger . IsEnabled ( LogLevel . Trace ) )
270
+ {
271
+ _logger . LogTrace (
272
+ "Function call requests: {Requests}" ,
273
+ string . Join ( ", " , calls . Select ( ftc => $ "{ ftc . Function ? . Name } ({ ftc . Function ? . Arguments } )") ) ) ;
274
+ }
275
+ }
276
+
277
+ private void AddResponseMessage ( ChatHistory chat , string ? result , string ? errorMessage , string ? toolId )
278
+ {
279
+ // Log any error
280
+ if ( errorMessage is not null && _logger . IsEnabled ( LogLevel . Debug ) )
281
+ {
282
+ _logger . LogDebug ( "Failed to handle tool request ({ToolId}). {Error}" , toolId , errorMessage ) ;
283
+ }
284
+
285
+ // Add the tool response message to both the chat options and to the chat history.
286
+ result ??= errorMessage ?? string . Empty ;
287
+ chat . Add ( new DashScopeChatMessageContent ( AuthorRole . Tool , result , name : toolId ) ) ;
288
+ }
289
+
290
+ private static void EnsureChoiceExists ( List < TextGenerationChoice > ? choices )
291
+ {
292
+ if ( choices is null || choices . Count == 0 )
293
+ {
294
+ throw new KernelException ( "No choice was returned from model" ) ;
295
+ }
296
+ }
297
+
298
+ private static string ProcessFunctionResult ( object functionResult , ToolCallBehavior ? toolCallBehavior )
299
+ {
300
+ if ( functionResult is string stringResult )
301
+ {
302
+ return stringResult ;
303
+ }
304
+
305
+ // This is an optimization to use ChatMessageContent content directly
306
+ // without unnecessary serialization of the whole message content class.
307
+ if ( functionResult is ChatMessageContent chatMessageContent )
308
+ {
309
+ return chatMessageContent . ToString ( ) ;
310
+ }
311
+
312
+ // For polymorphic serialization of unknown in advance child classes of the KernelContent class,
313
+ // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property.
314
+ // For more details about the polymorphic serialization, see the article at:
315
+ // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0
316
+ return JsonSerializer . Serialize ( functionResult , toolCallBehavior ? . ToolCallResultSerializerOptions ) ;
317
+ }
144
318
}
0 commit comments