diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs index 87ce302293..87120d9d49 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs +++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs @@ -27,6 +27,7 @@ const string JokerName = "Joker"; const string JokerInstructions = "You are good at telling jokes."; AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstructions, JokerName); +AIAgent agent2 = client.GetChatClient(deploymentName).CreateAIAgent("You are good at telling inspirational quotes.", "InspirationBot"); Func uppercaseFunc = s => s.ToUpperInvariant(); var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor"); @@ -35,7 +36,9 @@ Func reverseTextFunc = s => s.ToUpperInvariant(); var reverse = reverseTextFunc.BindAsExecutor("ReverseTextExecutor"); WorkflowBuilder builder = new(uppercase); -builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse); +builder.AddEdge(uppercase, agent2); +builder.AddEdge(agent2, reverse).WithOutputFrom(agent2); + var workflow = builder.WithName("MyTestWorkflow").Build(); // Configure the function app to host the AI agent. diff --git a/dotnet/samples/AzureFunctions/09_Workflow/demo.http b/dotnet/samples/AzureFunctions/09_Workflow/demo.http index 3b741adf31..9f4a088117 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/demo.http +++ b/dotnet/samples/AzureFunctions/09_Workflow/demo.http @@ -2,7 +2,7 @@ @authority=http://localhost:7071 ### Prompt the agent -POST {{authority}}/api/agents/Joker/run +POST {{authority}}/api/workflows/MyTestWorkflow/run Content-Type: text/plain Tell me a joke about a pirate. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs index dc1221d4b9..47c2599499 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs @@ -34,7 +34,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor if (taskOrechstrationContextBinding.IsCompletedSuccessfully) { var t = taskOrechstrationContextBinding.Result.Value; - context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(t!); + context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync("todo", context); } return; @@ -139,20 +139,6 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor context); return; } - if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint) - { - var triggerBinding = context.FunctionDefinition.InputBindings.Values.FirstOrDefault(b => b.Type == "orchestrationTrigger"); - var taskOrechstrationContextBinding = context.BindInputAsync(triggerBinding!); - - if (taskOrechstrationContextBinding.IsCompletedSuccessfully) - { - var t = taskOrechstrationContextBinding.Result.Value; - context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(t!); - } - - //context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(null); - return; - } throw new InvalidOperationException($"Unsupported function entry point '{context.FunctionDefinition.EntryPoint}' for 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 0463d4ecc9..de334f5332 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -11,6 +11,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.Hosting.AzureFunctions; @@ -49,16 +50,18 @@ internal static class BuiltInFunctions // return new List(); //} - //[Function("dafx-Orchestration")] - public static async Task> RunWorkflowOrchestratorAsync(TaskOrchestrationContext taskOrchestrationContext) + public static async Task> RunWorkflowOrchestratorAsync(string taskOrchestrationContext, FunctionContext functionsContext) { - //ILogger logger = context.CreateReplaySafeLogger(nameof(Function)); - //logger.LogInformation("Invoking RunWorkflowOrchestrator"); + var logger = functionsContext.GetLogger("BuiltInFunctions"); var outputs = new List(); + const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrtionContext + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Orchestrator {WorkflowName} is executing. Input: {Input}", WorkflowName, taskOrchestrationContext); + } - await Task.Delay(1); - outputs.Add("to do - call get executor result"); - + //var runner = functionsContext.InstanceServices.GetService(); + //await runner!.RunAsync(null, WorkflowName); return outputs; } @@ -95,7 +98,7 @@ internal static class BuiltInFunctions var workflowName = context.FunctionDefinition.Name.Replace("http", "dafx"); //string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow"); - string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("OrchFunction"); // dafx-MyTestWorkflow"); + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow"); //OrchFunction"); // dafx-MyTestWorkflow"); HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}.{instanceId}"); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs index 1e0690a7a2..3b4e541eec 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.DurableTask; +using Microsoft.Agents.AI.Workflows.Checkpointing; using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; using Microsoft.Extensions.Logging; @@ -21,29 +22,20 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta public void Transform(IList original) { - if (this._logger.IsEnabled(LogLevel.Information)) - { - this._logger.LogInformation("Transforming function metadata to add durable workflow functions. Initial function count: {FunctionCount}", original.Count); - } + this._logger.LogTransformStart(original.Count); foreach (var workflow in this._options.Workflows) { - if (this._logger.IsEnabled(LogLevel.Information)) - { - this._logger.LogInformation("Adding durable workflow function for workflow: {WorkflowName}", workflow.Key); - } + this._logger.LogAddingWorkflowFunction(workflow.Key); original.Add(CreateOrchestrationTrigger(workflow.Key)); - // We also want to create an HTTP trigge for this orchestration so users can start it via HTTP. - if (this._logger.IsEnabled(LogLevel.Information)) - { - this._logger.LogInformation("Adding HTTP trigger function for workflow: {WorkflowName}", workflow.Key); - var httpTriggerMetadata = CreateHttpTrigger(workflow.Key, $"workflows/{workflow.Key}/run"); - original.Add(httpTriggerMetadata); - } - // Create activity functions for each executor in the workflow - // Extract executor IDs from edges and start executor (since ExecutorBindings is internal) + // We also want to create an HTTP trigger for this orchestration so users can start it via HTTP. + this._logger.LogAddingHttpTrigger(workflow.Key); + original.Add(CreateHttpTrigger(workflow.Key, $"workflows/{workflow.Key}/run")); + + // Create activity/entity functions for each executor in the workflow based on their type + // Extract executor IDs from edges and start executor var executorIds = new HashSet { workflow.Value.StartExecutorId }; var reflectedEdges = workflow.Value.ReflectEdges(); @@ -59,23 +51,41 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta } } - foreach (var executorId in executorIds) - { - if (this._logger.IsEnabled(LogLevel.Information)) - { - this._logger.LogInformation( - "Adding activity function for executor: {ExecutorId} in workflow: {WorkflowName}", - executorId, - workflow.Key); - } + Dictionary executorInfos = workflow.Value.ReflectExecutors(); - original.Add(CreateActivityTrigger(workflow.Key, executorId)); + foreach (string executorId in executorIds) + { + if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo)) + { + string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}_{executorId}"; + + // Check if the executor type is an agent-related type + if (IsAgentExecutorType(executorInfo.ExecutorType)) + { + this._logger.LogAddingAgentEntityFunction(executorId, executorInfo.ExecutorType.TypeName, workflow.Key); + original.Add(CreateAgentTrigger(functionName)); + } + else + { + this._logger.LogAddingActivityFunction(executorId, executorInfo.ExecutorType.TypeName, workflow.Key); + original.Add(CreateActivityTrigger(functionName)); + } + } } } - if (this._logger.IsEnabled(LogLevel.Information)) + this._logger.LogTransformFinished(original.Count); + + static bool IsAgentExecutorType(TypeId executorType) { - this._logger.LogInformation("Transform finished. Updated function count: {FunctionCount}", original.Count); + // hack for now. In the future, the MAF type could expose something which can help with this. + // Check if the type name or assembly indicates it's an agent executor + // This includes AgentRunStreamingExecutor, AgentExecutor, ChatClientAgent wrappers, etc. + string typeName = executorType.TypeName; + string assemblyName = executorType.AssemblyName; + + return typeName.Contains("AIAgentHostExecutor", StringComparison.OrdinalIgnoreCase) && + assemblyName.Contains("Microsoft.Agents.AI", StringComparison.OrdinalIgnoreCase); } } @@ -113,10 +123,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta }; } - private static DefaultFunctionMetadata CreateActivityTrigger(string workflowName, string executorId) + private static DefaultFunctionMetadata CreateActivityTrigger(string functionName) { - string functionName = $"{AgentSessionId.ToEntityName(workflowName)}_{executorId}"; - return new DefaultFunctionMetadata() { Name = functionName, @@ -129,4 +137,20 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta ScriptFile = BuiltInFunctions.ScriptFile, }; } + + private static DefaultFunctionMetadata CreateAgentTrigger(string functionName) + { + return new DefaultFunctionMetadata() + { + Name = functionName, + Language = "dotnet-isolated", + RawBindings = + [ + """{"name":"encodedEntityRequest","type":"entityTrigger","direction":"In"}""", + """{"name":"client","type":"durableClient","direction":"In"}""" + ], + EntryPoint = BuiltInFunctions.RunAgentEntityFunctionEntryPoint, + ScriptFile = BuiltInFunctions.ScriptFile, + }; + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformerLogs.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformerLogs.cs new file mode 100644 index 0000000000..f7554a5652 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformerLogs.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.AzureFunctions; + +/// +/// Logging messages for . +/// +[ExcludeFromCodeCoverage] +internal static partial class DurableWorkflowFunctionMetadataTransformerLogs +{ + [LoggerMessage( + Level = LogLevel.Information, + Message = "Transforming function metadata to add durable workflow functions. Initial function count: {FunctionCount}")] + public static partial void LogTransformStart(this ILogger logger, int functionCount); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Adding durable workflow function for workflow: {WorkflowName}")] + public static partial void LogAddingWorkflowFunction(this ILogger logger, string workflowName); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Adding HTTP trigger function for workflow: {WorkflowName}")] + public static partial void LogAddingHttpTrigger(this ILogger logger, string workflowName); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Adding activity function for executor: {ExecutorId} (Type: {ExecutorType}) in workflow: {WorkflowName}")] + public static partial void LogAddingActivityFunction(this ILogger logger, string executorId, string executorType, string workflowName); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Adding agent entity function for executor: {ExecutorId} (Type: {ExecutorType}) in workflow: {WorkflowName}")] + public static partial void LogAddingAgentEntityFunction(this ILogger logger, string executorId, string executorType, string workflowName); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Transform finished. Updated function count: {FunctionCount}")] + public static partial void LogTransformFinished(this ILogger logger, int functionCount); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs new file mode 100644 index 0000000000..5a8c80c33d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunner.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.DurableTask; +using Microsoft.Agents.AI.Workflows; +using Microsoft.DurableTask; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.AzureFunctions; + +internal sealed class DurableWorkflowRunner +{ + private readonly DurableWorkflowOptions _options; + private readonly ILogger _logger; + public DurableWorkflowRunner(ILogger logger, DurableWorkflowOptions durableWorkflowOptions) + { + this._logger = logger; + this._options = durableWorkflowOptions; + } + + internal async Task RunAsync( + TaskOrchestrationContext taskOrchestrationContext, + string workflowName, + object? initialInput = null, + CancellationToken cancellationToken = default) + { + this._logger.LogAttemptingToRunWorkflow(workflowName); + + if (this._options.Workflows.TryGetValue(workflowName, out Workflow? wf)) + { + this._logger.LogRunningWorkflow(wf.Name); + + await this.RunExecutorsInWorkFlowAsync(taskOrchestrationContext, wf, initialInput, cancellationToken).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException($"Workflow '{workflowName}' not found."); + } + } + + private Task RunExecutorsInWorkFlowAsync( + TaskOrchestrationContext taskOrchestrationContext, + Workflow wf, + object? initialInput, + CancellationToken cancellationToken) + { + // Extract edeges and executors from the workflow and execute them in order/based on the pattern as Durable entities. + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs new file mode 100644 index 0000000000..fbec81f77d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowRunnerLogs.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.AzureFunctions; + +/// +/// Logging messages for . +/// +[ExcludeFromCodeCoverage] +internal static partial class DurableWorkflowRunnerLogs +{ + [LoggerMessage( + Level = LogLevel.Information, + Message = "Attempting to run workflow: {WorkflowName}")] + public static partial void LogAttemptingToRunWorkflow(this ILogger logger, string workflowName); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Running workflow: {WorkflowName}")] + public static partial void LogRunningWorkflow(this ILogger logger, string? workflowName); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs index c9f4a3bc4a..fbae490782 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs @@ -25,6 +25,7 @@ public static class FunctionsApplicationBuilderExtensions var options = new DurableWorkflowOptions(); configure(options); builder.Services.AddSingleton(options); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); return builder; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs index 6e019b4928..93fa110e99 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs @@ -2,16 +2,36 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing; -internal sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId) +/// +/// Represents information about an executor in a workflow, including its type and identifier. +/// +/// The type identifier of the executor. +/// The unique identifier of the executor instance. +public sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId) { + /// + /// Determines whether this executor info matches a specific executor type by generic parameter. + /// + /// The executor type to match against. + /// true if the executor type and ID match; otherwise, false. public bool IsMatch() where T : Executor => this.ExecutorType.IsMatch() && this.ExecutorId == typeof(T).Name; + /// + /// Determines whether this executor info matches a given executor instance. + /// + /// The executor instance to match against. + /// true if the executor type and ID match; otherwise, false. public bool IsMatch(Executor executor) => this.ExecutorType.IsMatch(executor.GetType()) && this.ExecutorId == executor.Id; + /// + /// Determines whether this executor info matches a given executor binding. + /// + /// The executor binding to match against. + /// true if the executor type and ID match; otherwise, false. public bool IsMatch(ExecutorBinding binding) => this.ExecutorType.IsMatch(binding.ExecutorType) && this.ExecutorId == binding.Id; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs index 8b77da7fc2..51ff5229f7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs @@ -36,6 +36,18 @@ public class Workflow ); } + /// + /// Gets information about all executors in the workflow, keyed by their ID. + /// + /// A dictionary mapping executor IDs to their . + public Dictionary ReflectExecutors() + { + return this.ExecutorBindings.Values.ToDictionary( + keySelector: binding => binding.Id, + elementSelector: RepresentationExtensions.ToExecutorInfo + ); + } + internal Dictionary Ports { get; init; } = []; ///