@@ -2692,4 +2692,215 @@ public async Task RespectsChatOptionsToolsModificationsByFunctionTool_ReplaceWit
26922692
26932693 Assert . Equal ( 2 , callCount ) ;
26942694 }
2695+
2696+ [ Fact ]
2697+ public async Task LogsFunctionNotFound ( )
2698+ {
2699+ var collector = new FakeLogCollector ( ) ;
2700+ ServiceCollection c = new ( ) ;
2701+ c . AddLogging ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2702+
2703+ var options = new ChatOptions
2704+ {
2705+ Tools = [ AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ]
2706+ } ;
2707+
2708+ List < ChatMessage > plan =
2709+ [
2710+ new ChatMessage ( ChatRole . User , "hello" ) ,
2711+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "UnknownFunc" ) ] ) ,
2712+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "callId1" , result : "Error: Requested function \" UnknownFunc\" not found." ) ] ) ,
2713+ new ChatMessage ( ChatRole . Assistant , "world" ) ,
2714+ ] ;
2715+
2716+ Func < ChatClientBuilder , ChatClientBuilder > configure = b =>
2717+ b . Use ( ( c , services ) => new FunctionInvokingChatClient ( c , services . GetRequiredService < ILoggerFactory > ( ) ) ) ;
2718+
2719+ await InvokeAndAssertAsync ( options , plan , null , configure , c . BuildServiceProvider ( ) ) ;
2720+
2721+ var logs = collector . GetSnapshot ( ) ;
2722+ Assert . Contains ( logs , e => e . Message . Contains ( "Function 'UnknownFunc' not found" ) && e . Level == LogLevel . Warning ) ;
2723+ }
2724+
2725+ [ Fact ]
2726+ public async Task LogsNonInvocableFunction ( )
2727+ {
2728+ var collector = new FakeLogCollector ( ) ;
2729+ ServiceCollection c = new ( ) ;
2730+ c . AddLogging ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2731+
2732+ var declarationOnly = AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) . AsDeclarationOnly ( ) ;
2733+ var options = new ChatOptions
2734+ {
2735+ Tools = [ declarationOnly ]
2736+ } ;
2737+
2738+ List < ChatMessage > plan =
2739+ [
2740+ new ChatMessage ( ChatRole . User , "hello" ) ,
2741+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ,
2742+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "callId1" , result : "Should not be produced" ) ] ) ,
2743+ new ChatMessage ( ChatRole . Assistant , "world" ) ,
2744+ ] ;
2745+
2746+ List < ChatMessage > expected = plan . Take ( 2 ) . ToList ( ) ;
2747+
2748+ Func < ChatClientBuilder , ChatClientBuilder > configure = b =>
2749+ b . Use ( ( c , services ) => new FunctionInvokingChatClient ( c , services . GetRequiredService < ILoggerFactory > ( ) ) ) ;
2750+
2751+ await InvokeAndAssertAsync ( options , plan , expected , configure , c . BuildServiceProvider ( ) ) ;
2752+
2753+ var logs = collector . GetSnapshot ( ) ;
2754+ Assert . Contains ( logs , e => e . Message . Contains ( "Function 'Func1' is not invocable (declaration only)" ) && e . Level == LogLevel . Debug ) ;
2755+ }
2756+
2757+ [ Fact ]
2758+ public async Task LogsFunctionRequestedTermination ( )
2759+ {
2760+ var collector = new FakeLogCollector ( ) ;
2761+ ServiceCollection c = new ( ) ;
2762+ c . AddLogging ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2763+
2764+ var options = new ChatOptions
2765+ {
2766+ Tools = [ AIFunctionFactory . Create ( ( ) =>
2767+ {
2768+ var context = FunctionInvokingChatClient . CurrentContext ! ;
2769+ context . Terminate = true ;
2770+ return "Terminated" ;
2771+ } , "TerminatingFunc" ) ]
2772+ } ;
2773+
2774+ List < ChatMessage > plan =
2775+ [
2776+ new ChatMessage ( ChatRole . User , "hello" ) ,
2777+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "TerminatingFunc" ) ] ) ,
2778+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "callId1" , result : "Terminated" ) ] ) ,
2779+ ] ;
2780+
2781+ Func < ChatClientBuilder , ChatClientBuilder > configure = b =>
2782+ b . Use ( ( c , services ) => new FunctionInvokingChatClient ( c , services . GetRequiredService < ILoggerFactory > ( ) ) ) ;
2783+
2784+ await InvokeAndAssertAsync ( options , plan , null , configure , c . BuildServiceProvider ( ) ) ;
2785+
2786+ var logs = collector . GetSnapshot ( ) ;
2787+ Assert . Contains ( logs , e => e . Message . Contains ( "Function 'TerminatingFunc' requested termination of the processing loop" ) && e . Level == LogLevel . Debug ) ;
2788+ }
2789+
2790+ [ Fact ]
2791+ public async Task LogsFunctionRequiresApproval ( )
2792+ {
2793+ var collector = new FakeLogCollector ( ) ;
2794+ ServiceCollection c = new ( ) ;
2795+ c . AddLogging ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2796+
2797+ var approvalFunc = new ApprovalRequiredAIFunction ( AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ) ;
2798+ var options = new ChatOptions
2799+ {
2800+ Tools = [ approvalFunc ]
2801+ } ;
2802+
2803+ List < ChatMessage > plan =
2804+ [
2805+ new ChatMessage ( ChatRole . User , "hello" ) ,
2806+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ,
2807+ ] ;
2808+
2809+ // Expected output includes the user message and the approval request
2810+ List < ChatMessage > expected =
2811+ [
2812+ new ChatMessage ( ChatRole . User , "hello" ) ,
2813+ new ChatMessage ( ChatRole . Assistant ,
2814+ [
2815+ new FunctionApprovalRequestContent ( "callId1" , new FunctionCallContent ( "callId1" , "Func1" ) )
2816+ ] )
2817+ ] ;
2818+
2819+ Func < ChatClientBuilder , ChatClientBuilder > configure = b =>
2820+ b . Use ( ( c , services ) => new FunctionInvokingChatClient ( c , services . GetRequiredService < ILoggerFactory > ( ) ) ) ;
2821+
2822+ await InvokeAndAssertAsync ( options , plan , expected , configure , c . BuildServiceProvider ( ) ) ;
2823+
2824+ var logs = collector . GetSnapshot ( ) ;
2825+ Assert . Contains ( logs , e => e . Message . Contains ( "Function 'Func1' requires approval" ) && e . Level == LogLevel . Debug ) ;
2826+ }
2827+
2828+ [ Fact ]
2829+ public async Task LogsProcessingApprovalResponse ( )
2830+ {
2831+ var collector = new FakeLogCollector ( ) ;
2832+ using var loggerFactory = LoggerFactory . Create ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2833+
2834+ var approvalFunc = new ApprovalRequiredAIFunction ( AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ) ;
2835+
2836+ using var innerClient = new TestChatClient
2837+ {
2838+ GetResponseAsyncCallback = ( messages , opts , ct ) =>
2839+ Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "world" ) ) )
2840+ } ;
2841+
2842+ using var client = new FunctionInvokingChatClient ( innerClient , loggerFactory ) ;
2843+
2844+ var options = new ChatOptions { Tools = [ approvalFunc ] } ;
2845+
2846+ var messages = new List < ChatMessage >
2847+ {
2848+ new ChatMessage ( ChatRole . User , "hello" ) ,
2849+ new ChatMessage ( ChatRole . Assistant ,
2850+ [
2851+ new FunctionApprovalRequestContent ( "callId1" , new FunctionCallContent ( "callId1" , "Func1" ) )
2852+ ] ) ,
2853+ new ChatMessage ( ChatRole . User ,
2854+ [
2855+ new FunctionApprovalResponseContent ( "callId1" , true , new FunctionCallContent ( "callId1" , "Func1" ) )
2856+ ] )
2857+ } ;
2858+
2859+ await client . GetResponseAsync ( messages , options ) ;
2860+
2861+ var logs = collector . GetSnapshot ( ) ;
2862+ Assert . Contains ( logs , e => e . Message . Contains ( "Processing approval response for 'Func1'. Approved: True" ) && e . Level == LogLevel . Debug ) ;
2863+ }
2864+
2865+ [ Fact ]
2866+ public async Task LogsFunctionRejected ( )
2867+ {
2868+ var collector = new FakeLogCollector ( ) ;
2869+ using var loggerFactory = LoggerFactory . Create ( b => b . AddProvider ( new FakeLoggerProvider ( collector ) ) . SetMinimumLevel ( LogLevel . Debug ) ) ;
2870+
2871+ var approvalFunc = new ApprovalRequiredAIFunction ( AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ) ;
2872+
2873+ using var innerClient = new TestChatClient
2874+ {
2875+ GetResponseAsyncCallback = ( messages , opts , ct ) =>
2876+ Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "world" ) ) )
2877+ } ;
2878+
2879+ using var client = new FunctionInvokingChatClient ( innerClient , loggerFactory ) ;
2880+
2881+ var options = new ChatOptions { Tools = [ approvalFunc ] } ;
2882+
2883+ var messages = new List < ChatMessage >
2884+ {
2885+ new ChatMessage ( ChatRole . User , "hello" ) ,
2886+ new ChatMessage ( ChatRole . Assistant ,
2887+ [
2888+ new FunctionApprovalRequestContent ( "callId1" , new FunctionCallContent ( "callId1" , "Func1" ) )
2889+ ] ) ,
2890+ new ChatMessage ( ChatRole . User ,
2891+ [
2892+ new FunctionApprovalResponseContent ( "callId1" , false , new FunctionCallContent ( "callId1" , "Func1" ) ) { Reason = "User denied" }
2893+ ] )
2894+ } ;
2895+
2896+ await client . GetResponseAsync ( messages , options ) ;
2897+
2898+ var logs = collector . GetSnapshot ( ) ;
2899+ Assert . Contains ( logs , e => e . Message . Contains ( "Function 'Func1' was rejected. Reason: User denied" ) && e . Level == LogLevel . Debug ) ;
2900+ }
2901+
2902+ // Note: LogMaxConsecutiveErrorsExceeded is exercised by the existing
2903+ // ContinuesWithFailingCallsUntilMaximumConsecutiveErrors test which triggers
2904+ // the threshold condition. The logging call is at line 1078 and will execute
2905+ // when MaximumConsecutiveErrorsPerRequest is exceeded.
26952906}
0 commit comments