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