@@ -205,6 +205,16 @@ public int MaximumConsecutiveErrorsPerRequest
205
205
set => _maximumConsecutiveErrorsPerRequest = Throw . IfLessThan ( value , 0 ) ;
206
206
}
207
207
208
+ /// <summary>Gets or sets a collection of additional tools the client is able to invoke.</summary>
209
+ /// <remarks>
210
+ /// These will not impact the requests sent by the <see cref="FunctionInvokingChatClient"/>, which will pass through the
211
+ /// <see cref="ChatOptions.Tools" /> unmodified. However, if the inner client requests the invocation of a tool
212
+ /// that was not in <see cref="ChatOptions.Tools" />, this <see cref="AdditionalTools"/> collection will also be consulted
213
+ /// to look for a corresponding tool to invoke. This is useful when the service may have been pre-configured to be aware
214
+ /// of certain tools that aren't also sent on each individual request.
215
+ /// </remarks>
216
+ public IList < AITool > ? AdditionalTools { get ; set ; }
217
+
208
218
/// <summary>Gets or sets a delegate used to invoke <see cref="AIFunction"/> instances.</summary>
209
219
/// <remarks>
210
220
/// By default, the protected <see cref="InvokeFunctionAsync"/> method is called for each <see cref="AIFunction"/> to be invoked,
@@ -250,7 +260,7 @@ public override async Task<ChatResponse> GetResponseAsync(
250
260
251
261
// Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
252
262
bool requiresFunctionInvocation =
253
- options ? . Tools is { Count : > 0 } &&
263
+ ( options ? . Tools is { Count : > 0 } || AdditionalTools is { Count : > 0 } ) &&
254
264
iteration < MaximumIterationsPerRequest &&
255
265
CopyFunctionCalls ( response . Messages , ref functionCallContents ) ;
256
266
@@ -288,7 +298,7 @@ public override async Task<ChatResponse> GetResponseAsync(
288
298
289
299
// Add the responses from the function calls into the augmented history and also into the tracked
290
300
// list of response messages.
291
- var modeAndMessages = await ProcessFunctionCallsAsync ( augmentedHistory , options ! , functionCallContents ! , iteration , consecutiveErrorCount , isStreaming : false , cancellationToken ) ;
301
+ var modeAndMessages = await ProcessFunctionCallsAsync ( augmentedHistory , options , functionCallContents ! , iteration , consecutiveErrorCount , isStreaming : false , cancellationToken ) ;
292
302
responseMessages . AddRange ( modeAndMessages . MessagesAdded ) ;
293
303
consecutiveErrorCount = modeAndMessages . NewConsecutiveErrorCount ;
294
304
@@ -297,7 +307,7 @@ public override async Task<ChatResponse> GetResponseAsync(
297
307
break ;
298
308
}
299
309
300
- UpdateOptionsForNextIteration ( ref options ! , response . ConversationId ) ;
310
+ UpdateOptionsForNextIteration ( ref options , response . ConversationId ) ;
301
311
}
302
312
303
313
Debug . Assert ( responseMessages is not null , "Expected to only be here if we have response messages." ) ;
@@ -367,7 +377,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
367
377
368
378
// If there are no tools to call, or for any other reason we should stop, return the response.
369
379
if ( functionCallContents is not { Count : > 0 } ||
370
- options ? . Tools is not { Count : > 0 } ||
380
+ ( options ? . Tools is not { Count : > 0 } && AdditionalTools is not { Count : > 0 } ) ||
371
381
iteration >= _maximumIterationsPerRequest )
372
382
{
373
383
break ;
@@ -535,9 +545,16 @@ private static bool CopyFunctionCalls(
535
545
return any ;
536
546
}
537
547
538
- private static void UpdateOptionsForNextIteration ( ref ChatOptions options , string ? conversationId )
548
+ private static void UpdateOptionsForNextIteration ( ref ChatOptions ? options , string ? conversationId )
539
549
{
540
- if ( options . ToolMode is RequiredChatToolMode )
550
+ if ( options is null )
551
+ {
552
+ if ( conversationId is not null )
553
+ {
554
+ options = new ( ) { ConversationId = conversationId } ;
555
+ }
556
+ }
557
+ else if ( options . ToolMode is RequiredChatToolMode )
541
558
{
542
559
// We have to reset the tool mode to be non-required after the first iteration,
543
560
// as otherwise we'll be in an infinite loop.
@@ -566,7 +583,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
566
583
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
567
584
/// <returns>A value indicating how the caller should proceed.</returns>
568
585
private async Task < ( bool ShouldTerminate , int NewConsecutiveErrorCount , IList < ChatMessage > MessagesAdded ) > ProcessFunctionCallsAsync (
569
- List < ChatMessage > messages , ChatOptions options , List < FunctionCallContent > functionCallContents , int iteration , int consecutiveErrorCount ,
586
+ List < ChatMessage > messages , ChatOptions ? options , List < FunctionCallContent > functionCallContents , int iteration , int consecutiveErrorCount ,
570
587
bool isStreaming , CancellationToken cancellationToken )
571
588
{
572
589
// We must add a response for every tool call, regardless of whether we successfully executed it or not.
@@ -695,13 +712,13 @@ private void ThrowIfNoFunctionResultsAdded(IList<ChatMessage>? messages)
695
712
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
696
713
/// <returns>A value indicating how the caller should proceed.</returns>
697
714
private async Task < FunctionInvocationResult > ProcessFunctionCallAsync (
698
- List < ChatMessage > messages , ChatOptions options , List < FunctionCallContent > callContents ,
715
+ List < ChatMessage > messages , ChatOptions ? options , List < FunctionCallContent > callContents ,
699
716
int iteration , int functionCallIndex , bool captureExceptions , bool isStreaming , CancellationToken cancellationToken )
700
717
{
701
718
var callContent = callContents [ functionCallIndex ] ;
702
719
703
720
// Look up the AIFunction for the function call. If the requested function isn't available, send back an error.
704
- AIFunction ? aiFunction = options . Tools ! . OfType < AIFunction > ( ) . FirstOrDefault ( t => t . Name == callContent . Name ) ;
721
+ AIFunction ? aiFunction = FindAIFunction ( options ? . Tools , callContent . Name ) ?? FindAIFunction ( AdditionalTools , callContent . Name ) ;
705
722
if ( aiFunction is null )
706
723
{
707
724
return new ( terminate : false , FunctionInvocationStatus . NotFound , callContent , result : null , exception : null ) ;
@@ -746,6 +763,23 @@ private async Task<FunctionInvocationResult> ProcessFunctionCallAsync(
746
763
callContent ,
747
764
result ,
748
765
exception : null ) ;
766
+
767
+ static AIFunction ? FindAIFunction ( IList < AITool > ? tools , string functionName )
768
+ {
769
+ if ( tools is not null )
770
+ {
771
+ int count = tools . Count ;
772
+ for ( int i = 0 ; i < count ; i ++ )
773
+ {
774
+ if ( tools [ i ] is AIFunction function && function . Name == functionName )
775
+ {
776
+ return function ;
777
+ }
778
+ }
779
+ }
780
+
781
+ return null ;
782
+ }
749
783
}
750
784
751
785
/// <summary>Creates one or more response messages for function invocation results.</summary>
0 commit comments