@@ -205,6 +205,16 @@ public int MaximumConsecutiveErrorsPerRequest
205205 set => _maximumConsecutiveErrorsPerRequest = Throw . IfLessThan ( value , 0 ) ;
206206 }
207207
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+
208218 /// <summary>Gets or sets a delegate used to invoke <see cref="AIFunction"/> instances.</summary>
209219 /// <remarks>
210220 /// 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(
250260
251261 // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
252262 bool requiresFunctionInvocation =
253- options ? . Tools is { Count : > 0 } &&
263+ ( options ? . Tools is { Count : > 0 } || AdditionalTools is { Count : > 0 } ) &&
254264 iteration < MaximumIterationsPerRequest &&
255265 CopyFunctionCalls ( response . Messages , ref functionCallContents ) ;
256266
@@ -288,7 +298,7 @@ public override async Task<ChatResponse> GetResponseAsync(
288298
289299 // Add the responses from the function calls into the augmented history and also into the tracked
290300 // 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 ) ;
292302 responseMessages . AddRange ( modeAndMessages . MessagesAdded ) ;
293303 consecutiveErrorCount = modeAndMessages . NewConsecutiveErrorCount ;
294304
@@ -297,7 +307,7 @@ public override async Task<ChatResponse> GetResponseAsync(
297307 break ;
298308 }
299309
300- UpdateOptionsForNextIteration ( ref options ! , response . ConversationId ) ;
310+ UpdateOptionsForNextIteration ( ref options , response . ConversationId ) ;
301311 }
302312
303313 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
367377
368378 // If there are no tools to call, or for any other reason we should stop, return the response.
369379 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 } ) ||
371381 iteration >= _maximumIterationsPerRequest )
372382 {
373383 break ;
@@ -535,9 +545,16 @@ private static bool CopyFunctionCalls(
535545 return any ;
536546 }
537547
538- private static void UpdateOptionsForNextIteration ( ref ChatOptions options , string ? conversationId )
548+ private static void UpdateOptionsForNextIteration ( ref ChatOptions ? options , string ? conversationId )
539549 {
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 )
541558 {
542559 // We have to reset the tool mode to be non-required after the first iteration,
543560 // as otherwise we'll be in an infinite loop.
@@ -566,7 +583,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
566583 /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
567584 /// <returns>A value indicating how the caller should proceed.</returns>
568585 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 ,
570587 bool isStreaming , CancellationToken cancellationToken )
571588 {
572589 // 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)
695712 /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
696713 /// <returns>A value indicating how the caller should proceed.</returns>
697714 private async Task < FunctionInvocationResult > ProcessFunctionCallAsync (
698- List < ChatMessage > messages , ChatOptions options , List < FunctionCallContent > callContents ,
715+ List < ChatMessage > messages , ChatOptions ? options , List < FunctionCallContent > callContents ,
699716 int iteration , int functionCallIndex , bool captureExceptions , bool isStreaming , CancellationToken cancellationToken )
700717 {
701718 var callContent = callContents [ functionCallIndex ] ;
702719
703720 // 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 ) ;
705722 if ( aiFunction is null )
706723 {
707724 return new ( terminate : false , FunctionInvocationStatus . NotFound , callContent , result : null , exception : null ) ;
@@ -746,6 +763,23 @@ private async Task<FunctionInvocationResult> ProcessFunctionCallAsync(
746763 callContent ,
747764 result ,
748765 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+ }
749783 }
750784
751785 /// <summary>Creates one or more response messages for function invocation results.</summary>
0 commit comments