diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs index aa4d41cd96..5a160e6781 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs +++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs @@ -26,7 +26,8 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey) // Set up an AI agent following the standard Microsoft Agent Framework pattern. const string JokerName = "Joker"; const string JokerInstructions = "You are good at telling jokes."; -const string AnalysisInstructions = @"You are a Customer Feedback Analyzer. Your task is to analyze customer survey responses and categorize them accurately. +const string AnalysisInstructions = """ +You are a Customer Feedback Analyzer. Your task is to analyze customer survey responses and categorize them accurately. INPUT: You will receive customer feedback text that may include a rating and comments. @@ -37,40 +38,37 @@ OUTPUT: Return ONLY ONE category from this list: - Support Incident Status CATEGORIZATION RULES: -- ""Bug Report"": Technical issues, errors, crashes, features not working as expected -- ""General Feedback"": Suggestions, compliments, general comments about the product or service -- ""Billing Question"": Payment issues, subscription inquiries, pricing questions, refund requests -- ""Support Incident Status"": Follow-ups on existing tickets, status inquiries about previous issues +- "Bug Report": Technical issues, errors, crashes, features not working as expected +- "General Feedback": Suggestions, compliments, general comments about the product or service +- "Billing Question": Payment issues, subscription inquiries, pricing questions, refund requests +- "Support Incident Status": Follow-ups on existing tickets, status inquiries about previous issues RESPONSE FORMAT: Return only the category name exactly as shown above, with no additional text or explanation. Examples: -- ""The app crashes when I try to export"" → Bug Report -- ""Love the new design! Great work"" → General Feedback -- ""Why was I charged twice this month?"" → Billing Question -- ""What's the status of ticket #12345?"" → Support Incident Status"; +- "The app crashes when I try to export" → Bug Report +- "Love the new design! Great work" → General Feedback +- "Why was I charged twice this month?" → Billing Question +- "What's the status of ticket #12345?" → Support Incident Status +"""; AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstructions, JokerName); -AIAgent agent2 = client.GetChatClient(deploymentName).CreateAIAgent(AnalysisInstructions, "FeedbackAnalysisBot"); +AIAgent surveyFeedbackAgent = client.GetChatClient(deploymentName).CreateAIAgent(AnalysisInstructions, "FeedbackAnalysisBot"); SurveyResponseParserExecutor surveyResponseParserExecutor = new(); ResponseRouterExecutor responseRouterExecutor = new(); WorkflowBuilder builder = new(surveyResponseParserExecutor); -builder.AddEdge(surveyResponseParserExecutor, agent2); -builder.AddEdge(agent2, responseRouterExecutor).WithOutputFrom(responseRouterExecutor); +builder.AddEdge(surveyResponseParserExecutor, surveyFeedbackAgent); +builder.AddEdge(surveyFeedbackAgent, responseRouterExecutor).WithOutputFrom(responseRouterExecutor); var workflow = builder.WithName("HandleSurveyResponse").Build(); // Configure the function app to host AI agents and workflows in a unified way. // This will automatically generate HTTP API endpoints for agents and workflows. -using IHost app = FunctionsApplication - .CreateBuilder(args) - .ConfigureFunctionsWebApplication() - //.ConfigureDurableAgents(op => op.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1))) - .ConfigureDurableOptions(options => - { - // Configure workflows - options.Workflows.AddWorkflow(workflow); - }) - .Build(); -app.Run(); +var functionBuilder = FunctionsApplication.CreateBuilder(args); +functionBuilder.ConfigureFunctionsWebApplication().ConfigureDurableOptions(options => +{ + // Configure workflows + options.Workflows.AddWorkflow(workflow); +}); +functionBuilder.Build().Run(); diff --git a/dotnet/samples/AzureFunctions/09_Workflow/demo.http b/dotnet/samples/AzureFunctions/09_Workflow/demo.http index a14218c2f7..d1a4ba0738 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/demo.http +++ b/dotnet/samples/AzureFunctions/09_Workflow/demo.http @@ -5,4 +5,4 @@ POST {{authority}}/api/workflows/HandleSurveyResponse/run Content-Type: text/plain -Rating: 5. Why was I charged $99 when my plan should be $49? I need a refund for the overcharge +Rating: 10. Why was I charged $99 when my plan should be $49? I need a refund for the overcharge diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs index c4f14a6da4..c1c8378be1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs @@ -68,7 +68,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor } } - if (durableTaskClient is null && context.FunctionDefinition.EntryPoint != BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint) + if (durableTaskClient is null) { // This is not expected to happen since all built-in functions are // expected to have a Durable Task client binding. @@ -117,11 +117,6 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint) { - //if (httpRequestData == null) - //{ - // throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}."); - //} - if (httpRequestData == null) { throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}."); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index e146425adf..150c1de6ff 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -23,7 +23,6 @@ internal static class BuiltInFunctions internal static readonly string RunAgentHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunAgentHttpAsync)}"; internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}"; internal static readonly string RunWorkflowOrechstrtationHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowOrechstrtationHttpTriggerAsync)}"; - internal static readonly string RunWorkflowOrechstrtationFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowOrchestratorAsync)}"; internal static readonly string InvokeWorkflowActivityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowActivityAsync)}"; internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}"; @@ -42,21 +41,6 @@ internal static class BuiltInFunctions return runner.ExecuteActivityAsync(activityFunctionName, input, functionContext); } - public static async Task> RunWorkflowOrchestratorAsync(string taskOrchestrationContext, FunctionContext functionsContext) - { - var logger = functionsContext.GetLogger("BuiltInFunctions"); - var outputs = new List(); - const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrationContext - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Orchestrator {WorkflowName} is executing. Input: {Input}", WorkflowName, taskOrchestrationContext); - } - - //var runner = functionsContext.InstanceServices.GetService(); - //await runner!.RunAsync(null, WorkflowName); - return outputs; - } - // Exposed as an entity trigger via AgentFunctionsProvider public static Task InvokeAgentAsync( [DurableClient] DurableTaskClient client, @@ -86,14 +70,12 @@ internal static class BuiltInFunctions [DurableClient] DurableTaskClient client, FunctionContext context) { - // to do: Retrieve the workflow and execute it. var workflowName = context.FunctionDefinition.Name.Replace("http-", ""); var inputMessage = await req.ReadAsStringAsync(); - //string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow"); string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("WorkflowRunnerOrchestration", new DuableWorkflowRunRequest { WorkflowName = workflowName, Input = inputMessage! }); //OrchFunction"); // dafx-MyTestWorkflow"); - HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}.{instanceId}"); + HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted); + await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}. Orchestration instanceId: {instanceId}"); return response; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs index 4a4b9b157c..cc7625f9e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.Hosting.AzureFunctions; @@ -68,7 +69,6 @@ public static class DurableOptionsExtensions builder.UseWhen(static context => string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) || - string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal)); @@ -85,9 +85,15 @@ public static class DurableOptionsExtensions { throw new InvalidOperationException("FunctionContext is not available in the orchestration context."); } - + var logger = tc.CreateReplaySafeLogger("WorkflowRunnerOrchestration"); DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService(); - return await runner.RunWorkflowOrchestrationAsync(tc, inputBindingData).ConfigureAwait(false); + var orchestrationResult = await runner.RunWorkflowOrchestrationAsync(tc, inputBindingData, logger); + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Durable workflow orchestration completed. Result:{Result}", string.Join(",", orchestrationResult)); + } + + return orchestrationResult; })); builder.Services.AddSingleton(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs index 52869fb473..5afa6778a0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs @@ -21,23 +21,23 @@ internal sealed class DurableWorkflowRunner this._options = durableOptions.Workflows; } - internal async Task> RunWorkflowOrchestrationAsync(TaskOrchestrationContext context, DuableWorkflowRunRequest input) + internal async Task> RunWorkflowOrchestrationAsync(TaskOrchestrationContext taskOrchestrationContext, DuableWorkflowRunRequest input, ILogger logger) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(taskOrchestrationContext); ArgumentNullException.ThrowIfNull(input); string workflowName = input.WorkflowName; - this._logger.LogAttemptingToRunWorkflow(workflowName); + logger.LogAttemptingToRunWorkflow(workflowName); if (!this._options.Workflows.TryGetValue(workflowName, out Workflow? wf)) { throw new InvalidOperationException($"Workflow '{workflowName}' not found."); } - this._logger.LogRunningWorkflow(wf.Name); + logger.LogRunningWorkflow(wf.Name); - var result = await this.RunExecutorsInWorkFlowAsync(context, wf, input.Input).ConfigureAwait(false); + var result = await this.RunExecutorsInWorkFlowAsync(taskOrchestrationContext, wf, input.Input, logger); return [result]; } @@ -45,16 +45,17 @@ internal sealed class DurableWorkflowRunner private async Task RunExecutorsInWorkFlowAsync( TaskOrchestrationContext taskOrchestrationContext, Workflow workflow, - string initialInput) + string initialInput, + ILogger logger) { List executorResult = []; - if (this._logger.IsEnabled(LogLevel.Information)) + if (logger.IsEnabled(LogLevel.Information)) { foreach (WorkflowExecutorInfo executorInfo in WorkflowHelper.GetExecutorsFromWorkflowInOrder(workflow)) { string triggerName = this.BuildTriggerName(workflow.Name!, executorInfo.ExecutorId); - this._logger.LogInformation( + logger.LogInformation( " Scheduling executor '{ExecutorId}' (IsAgentic: {IsAgentic}) with trigger name '{TriggerName}'", executorInfo.ExecutorId, executorInfo.IsAgenticExecutor, @@ -69,7 +70,7 @@ internal sealed class DurableWorkflowRunner else { string AgentName = this.GetAgentNameFromExecutorId(workflow.Name!, executorInfo.ExecutorId); - this._logger.LogInformation( + logger.LogInformation( " Invoking agentic executor '{ExecutorId}'", AgentName); DurableAIAgent agent = taskOrchestrationContext.GetAgent(AgentName); @@ -78,7 +79,7 @@ internal sealed class DurableWorkflowRunner AgentThread destinationThread = agent.GetNewThread(); var agentResponse = await agent.RunAsync(input, destinationThread); executorResult.Add(agentResponse.Text); - this._logger.LogInformation( + logger.LogInformation( "Agentic executor '{ExecutorId}' completed with response: {AgentResponse}", AgentName, agentResponse); @@ -162,7 +163,7 @@ internal sealed class DurableWorkflowRunner "activity-run", CancellationToken.None).ConfigureAwait(false); - // Create a minimal workflow context for the executor + // Create a minimal workflow taskOrchestrationContext for the executor MinimalActivityContext context = new(executorPair.Key); // Execute the executor with the input diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs index a6458873df..b9487ca507 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.DurableTask; using Microsoft.Azure.Functions.Worker.Builder; @@ -38,7 +38,6 @@ public static class FunctionsApplicationBuilderExtensions builder.UseWhen(static context => string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) || - string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal) || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));