11// Copyright (c) Microsoft. All rights reserved.
22
33// Uncomment this to enable JSON checkpointing to the local file system.
4- #define CHECKPOINT_JSON
4+ // #define CHECKPOINT_JSON
55
66using System . Diagnostics ;
77using System . Reflection ;
8+ using System . Text . Json ;
89using Azure . AI . Agents . Persistent ;
910using Azure . Identity ;
1011using Microsoft . Agents . AI . Workflows ;
12+ #if CHECKPOINT_JSON
1113using Microsoft . Agents . AI . Workflows . Checkpointing ;
14+ #endif
1215using Microsoft . Agents . AI . Workflows . Declarative ;
1316using Microsoft . Agents . AI . Workflows . Declarative . Events ;
17+ using Microsoft . Agents . AI . Workflows . Declarative . Kit ;
1418using Microsoft . Extensions . AI ;
1519using Microsoft . Extensions . Configuration ;
1620
@@ -63,20 +67,21 @@ private async Task ExecuteAsync()
6367
6468#if CHECKPOINT_JSON
6569 // Use a file-system based JSON checkpoint store to persist checkpoints to disk.
66- DirectoryInfo checkpointFolder = Directory . CreateDirectory ( Path . Combine ( "." , $ "chk-{ DateTime . Now : YYmmdd-hhMMss -ff} ") ) ;
70+ DirectoryInfo checkpointFolder = Directory . CreateDirectory ( Path . Combine ( "." , $ "chk-{ DateTime . Now : yyMMdd-hhmmss -ff} ") ) ;
6771 CheckpointManager checkpointManager = CheckpointManager . CreateJson ( new FileSystemJsonCheckpointStore ( checkpointFolder ) ) ;
68- Checkpointed < StreamingRun > run = await InProcessExecution . StreamAsync ( workflow , input , checkpointManager ) ;
6972#else
7073 // Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
7174 CheckpointManager checkpointManager = CheckpointManager . CreateInMemory ( ) ;
7275#endif
7376
77+ Checkpointed < StreamingRun > run = await InProcessExecution . StreamAsync ( workflow , input , checkpointManager ) ;
78+
7479 bool isComplete = false ;
75- InputResponse ? response = null ;
80+ object ? response = null ;
7681 do
7782 {
78- ExternalRequest ? inputRequest = await this . MonitorAndDisposeWorkflowRunAsync ( run , response ) ;
79- if ( inputRequest is not null )
83+ ExternalRequest ? externalRequest = await this . MonitorAndDisposeWorkflowRunAsync ( run , response ) ;
84+ if ( externalRequest is not null )
8085 {
8186 Notify ( "\n WORKFLOW: Yield" ) ;
8287
@@ -86,7 +91,7 @@ private async Task ExecuteAsync()
8691 }
8792
8893 // Process the external request.
89- response = HandleExternalRequest ( inputRequest ) ;
94+ response = await this . HandleExternalRequestAsync ( externalRequest ) ;
9095
9196 // Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
9297 workflow = this . CreateWorkflow ( ) ;
@@ -107,11 +112,25 @@ private async Task ExecuteAsync()
107112 Notify ( "\n WORKFLOW: Done!\n " ) ;
108113 }
109114
115+ /// <summary>
116+ /// Create the workflow from the declarative YAML. Includes definition of the
117+ /// <see cref="DeclarativeWorkflowOptions" /> and the associated <see cref="WorkflowAgentProvider"/>.
118+ /// </summary>
119+ /// <remarks>
120+ /// The value assigned to <see cref="IncludeFunctions" /> controls on whether the function
121+ /// tools (<see cref="AIFunction"/>) initialized in the constructor are included for auto-invocation.
122+ /// </remarks>
110123 private Workflow CreateWorkflow ( )
111124 {
112125 // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
126+ AzureAgentProvider agentProvider = new ( this . FoundryEndpoint , new AzureCliCredential ( ) )
127+ {
128+ // Functions included here will be auto-executed by the framework.
129+ Functions = IncludeFunctions ? this . FunctionMap . Values : null ,
130+ } ;
131+
113132 DeclarativeWorkflowOptions options =
114- new ( new AzureAgentProvider ( this . FoundryEndpoint , new AzureCliCredential ( ) ) )
133+ new ( agentProvider )
115134 {
116135 Configuration = this . Configuration ,
117136 //ConversationId = null, // Assign to continue a conversation
@@ -121,8 +140,18 @@ private Workflow CreateWorkflow()
121140 return DeclarativeWorkflowBuilder . Build < string > ( this . WorkflowFile , options ) ;
122141 }
123142
143+ /// <summary>
144+ /// Configuration key used to identify the Foundry project endpoint.
145+ /// </summary>
124146 private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT" ;
125147
148+ /// <summary>
149+ /// Controls on whether the function tools (<see cref="AIFunction"/>) initialized
150+ /// in the constructor are included for auto-invocation.
151+ /// NOTE: By default, no functions exist as part of this sample.
152+ /// </summary>
153+ private const bool IncludeFunctions = true ;
154+
126155 private static Dictionary < string , string > NameCache { get ; } = [ ] ;
127156 private static HashSet < string > FileCache { get ; } = [ ] ;
128157
@@ -132,6 +161,7 @@ private Workflow CreateWorkflow()
132161 private PersistentAgentsClient FoundryClient { get ; }
133162 private IConfiguration Configuration { get ; }
134163 private CheckpointInfo ? LastCheckpoint { get ; set ; }
164+ private Dictionary < string , AIFunction > FunctionMap { get ; }
135165
136166 private Program ( string workflowFile , string ? workflowInput )
137167 {
@@ -142,12 +172,21 @@ private Program(string workflowFile, string? workflowInput)
142172
143173 this . FoundryEndpoint = this . Configuration [ ConfigKeyFoundryEndpoint ] ?? throw new InvalidOperationException ( $ "Undefined configuration setting: { ConfigKeyFoundryEndpoint } ") ;
144174 this . FoundryClient = new PersistentAgentsClient ( this . FoundryEndpoint , new AzureCliCredential ( ) ) ;
175+
176+ List < AIFunction > functions =
177+ [
178+ // Manually define any custom functions that may be required by agents within the workflow.
179+ // By default, this sample does not include any functions.
180+ //AIFunctionFactory.Create(),
181+ ] ;
182+ this . FunctionMap = functions . ToDictionary ( f => f . Name ) ;
145183 }
146184
147- private async Task < ExternalRequest ? > MonitorAndDisposeWorkflowRunAsync ( Checkpointed < StreamingRun > run , InputResponse ? response = null )
185+ private async Task < ExternalRequest ? > MonitorAndDisposeWorkflowRunAsync ( Checkpointed < StreamingRun > run , object ? response = null )
148186 {
149187 await using IAsyncDisposable disposeRun = run ;
150188
189+ bool hasStreamed = false ;
151190 string ? messageId = null ;
152191
153192 await foreach ( WorkflowEvent workflowEvent in run . Run . WatchStreamAsync ( ) )
@@ -211,11 +250,12 @@ private Program(string workflowFile, string? workflowInput)
211250 case AgentRunUpdateEvent streamEvent :
212251 if ( ! string . Equals ( messageId , streamEvent . Update . MessageId , StringComparison . Ordinal ) )
213252 {
253+ hasStreamed = false ;
214254 messageId = streamEvent . Update . MessageId ;
215255
216256 if ( messageId is not null )
217257 {
218- string ? agentId = streamEvent . Update . AuthorName ;
258+ string ? agentId = streamEvent . Update . AgentId ;
219259 if ( agentId is not null )
220260 {
221261 if ( ! NameCache . TryGetValue ( agentId , out string ? realName ) )
@@ -245,11 +285,18 @@ private Program(string workflowFile, string? workflowInput)
245285 await DownloadFileContentAsync ( Path . GetFileName ( messageUpdate . TextAnnotation ? . TextToReplace ?? "response.png" ) , content ) ;
246286 }
247287 break ;
288+ case RequiredActionUpdate actionUpdate :
289+ Console . ForegroundColor = ConsoleColor . White ;
290+ Console . Write ( $ "Calling tool: { actionUpdate . FunctionName } ") ;
291+ Console . ForegroundColor = ConsoleColor . DarkGray ;
292+ Console . WriteLine ( $ " [{ actionUpdate . ToolCallId } ]") ;
293+ break ;
248294 }
249295 try
250296 {
251297 Console . ResetColor ( ) ;
252- Console . Write ( streamEvent . Data ) ;
298+ Console . Write ( streamEvent . Update . Text ) ;
299+ hasStreamed |= ! string . IsNullOrEmpty ( streamEvent . Update . Text ) ;
253300 }
254301 finally
255302 {
@@ -260,7 +307,11 @@ private Program(string workflowFile, string? workflowInput)
260307 case AgentRunResponseEvent messageEvent :
261308 try
262309 {
263- Console . WriteLine ( ) ;
310+ if ( hasStreamed )
311+ {
312+ Console . WriteLine ( ) ;
313+ }
314+
264315 if ( messageEvent . Response . Usage is not null )
265316 {
266317 Console . ForegroundColor = ConsoleColor . DarkGray ;
@@ -277,14 +328,31 @@ private Program(string workflowFile, string? workflowInput)
277328
278329 return default ;
279330 }
280- private static InputResponse HandleExternalRequest ( ExternalRequest request )
331+
332+ /// <summary>
333+ /// Handle request for external input, either from a human or a function tool invocation.
334+ /// </summary>
335+ private async ValueTask < object > HandleExternalRequestAsync ( ExternalRequest request ) =>
336+ request . Data . TypeId . TypeName switch
337+ {
338+ // Request for human input
339+ _ when request . Data . TypeId . IsMatch < InputRequest > ( ) => HandleInputRequest ( request . DataAs < InputRequest > ( ) ! ) ,
340+ // Request for function tool invocation. (Only active when functions are defined and IncludeFunctions is true.)
341+ _ when request . Data . TypeId . IsMatch < AgentToolRequest > ( ) => await this . HandleToolRequestAsync ( request . DataAs < AgentToolRequest > ( ) ! ) ,
342+ // Unknown request type.
343+ _ => throw new InvalidOperationException ( $ "Unsupported external request type: { request . GetType ( ) . Name } .") ,
344+ } ;
345+
346+ /// <summary>
347+ /// Handle request for human input.
348+ /// </summary>
349+ private static InputResponse HandleInputRequest ( InputRequest request )
281350 {
282- InputRequest ? message = request . Data . As < InputRequest > ( ) ;
283351 string ? userInput ;
284352 do
285353 {
286354 Console . ForegroundColor = ConsoleColor . DarkGreen ;
287- Console . Write ( $ "\n { message ? . Prompt ?? "INPUT:" } ") ;
355+ Console . Write ( $ "\n { request . Prompt ?? "INPUT:" } ") ;
288356 Console . ForegroundColor = ConsoleColor . White ;
289357 userInput = Console . ReadLine ( ) ;
290358 }
@@ -293,6 +361,30 @@ private static InputResponse HandleExternalRequest(ExternalRequest request)
293361 return new InputResponse ( userInput ) ;
294362 }
295363
364+ /// <summary>
365+ /// Handle a function tool request by invoking the specified tools and returning the results.
366+ /// </summary>
367+ /// <remarks>
368+ /// This handler is only active when <see cref="IncludeFunctions"/> is set to true and
369+ /// one or more <see cref="AIFunction"/> instances are defined in the constructor.
370+ /// </remarks>
371+ private async ValueTask < AgentToolResponse > HandleToolRequestAsync ( AgentToolRequest request )
372+ {
373+ Task < FunctionResultContent > [ ] functionTasks = request . FunctionCalls . Select ( functionCall => InvokesToolAsync ( functionCall ) ) . ToArray ( ) ;
374+
375+ await Task . WhenAll ( functionTasks ) ;
376+
377+ return AgentToolResponse . Create ( request , functionTasks . Select ( task => task . Result ) ) ;
378+
379+ async Task < FunctionResultContent > InvokesToolAsync ( FunctionCallContent functionCall )
380+ {
381+ AIFunction functionTool = this . FunctionMap [ functionCall . Name ] ;
382+ AIFunctionArguments ? functionArguments = functionCall . Arguments is null ? null : new ( functionCall . Arguments . NormalizePortableValues ( ) ) ;
383+ object ? result = await functionTool . InvokeAsync ( functionArguments ) ;
384+ return new FunctionResultContent ( functionCall . CallId , JsonSerializer . Serialize ( result ) ) ;
385+ }
386+ }
387+
296388 private static string ? ParseWorkflowFile ( string [ ] args )
297389 {
298390 string ? workflowFile = args . FirstOrDefault ( ) ;
0 commit comments