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 ;
36using Microsoft . SemanticKernel ;
47using Microsoft . SemanticKernel . ChatCompletion ;
58using 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}
0 commit comments