From 588e0bc0b2f6508152ebac58d1db293ace4953ec Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 21 Jan 2026 10:49:10 -0800 Subject: [PATCH] WIP --- .../AzureFunctions/09_Workflow/Program.cs | 15 +++- .../AzureFunctions/09_Workflow/demo.http | 2 +- .../10_WorkflowConcurrent/Program.cs | 9 +- .../AgentEntity.cs | 3 +- .../ServiceCollectionExtensions.cs | 32 +++++-- .../BuiltInFunctions.cs | 37 ++++++++ ...DurableAgentFunctionMetadataTransformer.cs | 8 +- .../DurableAgentsOptionsExtensions.cs | 13 ++- .../DurableOptionsExtensions.cs | 11 ++- ...ableWorkflowFunctionMetadataTransformer.cs | 51 +++++++---- ...WorkflowFunctionMetadataTransformerLogs.cs | 18 ++++ .../DurableWorkflowOptionsExtensions.cs | 85 +++++++++++++++++++ .../FunctionsWorkflowOptions.cs | 17 ++++ 13 files changed, 258 insertions(+), 43 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowOptionsExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsWorkflowOptions.cs diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs index db166d184d..06a84a05cf 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs +++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs @@ -20,22 +20,31 @@ Func orderParserFunc = input => }; var orderParserExecutor = orderParserFunc.BindAsExecutor("OrderParser"); +Func cancelOrderFunc = input => $"Cancelled order:{input}"; +var orderArchiver = orderParserFunc.BindAsExecutor("OrderArchiver"); + OrderLookupExecutor orderLookupExecutor = new(); OrderEnricherExecutor orderEnricherExeecutor = new(); PaymentProcessorExecutor paymentProcessorExecutor = new(); -Workflow workflow = new WorkflowBuilder(orderParserExecutor) +Workflow processOrder = new WorkflowBuilder(orderParserExecutor) .WithName("FulfillOrder") .WithDescription("Looks up an order by ID and run payment processing") .AddEdge(orderParserExecutor, orderLookupExecutor) .AddEdge(orderLookupExecutor, orderEnricherExeecutor) .AddEdge(orderEnricherExeecutor, paymentProcessorExecutor) - .WithOutputFrom(paymentProcessorExecutor) + .Build(); + +Workflow cancelOrder = new WorkflowBuilder(orderParserExecutor) + .WithName("CancelOrder") + .WithDescription("Cancel an order") + .AddEdge(orderParserExecutor, orderLookupExecutor) + .AddEdge(orderLookupExecutor, orderArchiver) .Build(); var host = FunctionsApplication.CreateBuilder(args) .ConfigureFunctionsWebApplication() - .ConfigureDurableOptions(options => options.Workflows.AddWorkflow(workflow)) + .ConfigureDurableOptions(options => options.Workflows.AddWorkflow([processOrder, cancelOrder])) .Build(); host.Run(); diff --git a/dotnet/samples/AzureFunctions/09_Workflow/demo.http b/dotnet/samples/AzureFunctions/09_Workflow/demo.http index 0c23d3c7ca..7aa11dad84 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/demo.http +++ b/dotnet/samples/AzureFunctions/09_Workflow/demo.http @@ -8,7 +8,7 @@ Content-Type: text/plain QWERTY80853 ### Look up a short order id -POST {{authority}}/api/workflows/FulfillOrder/run +POST {{authority}}/api/workflows/CancelOrder/run Content-Type: text/plain 12345 diff --git a/dotnet/samples/AzureFunctions/10_WorkflowConcurrent/Program.cs b/dotnet/samples/AzureFunctions/10_WorkflowConcurrent/Program.cs index 004b9c9553..948f750038 100644 --- a/dotnet/samples/AzureFunctions/10_WorkflowConcurrent/Program.cs +++ b/dotnet/samples/AzureFunctions/10_WorkflowConcurrent/Program.cs @@ -43,7 +43,8 @@ var workflow = new WorkflowBuilder(startExecutor) FunctionsApplication.CreateBuilder(args) .ConfigureFunctionsWebApplication() .ConfigureDurableOptions(options => -{ - // Configure workflows - options.Workflows.AddWorkflow(workflow); -}).Build().Run(); + { + // Configure workflows + options.Workflows.AddWorkflow(workflow, enableMcpToolTrigger: true); + }) + .Build().Run(); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index ec4ba3acf6..11f74a4a61 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -216,8 +216,7 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella private AIAgent GetAgent(AgentSessionId sessionId) { - IReadOnlyDictionary> agents = - this._services.GetRequiredService>>(); + IReadOnlyDictionary> agents = this._options.GetAgentFactories(); if (!agents.TryGetValue(sessionId.Name, out Func? agentFactory)) { throw new InvalidOperationException($"Agent '{sessionId.Name}' not found"); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs index 79d44924ca..a1ac7723a7 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Agents.AI.DurableTask; @@ -80,23 +81,40 @@ public static class ServiceCollectionExtensions DurableAgentsOptions options = new(); configure(options); - IReadOnlyDictionary> agents = options.GetAgentFactories(); + IReadOnlyDictionary> newAgents = options.GetAgentFactories(); - // The agent dictionary contains the real agent factories, which is used by the agent entities. - services.AddSingleton(agents); + // Check if we already have DurableAgentsOptions registered and merge with it + ServiceDescriptor? existingOptionsDescriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(DurableAgentsOptions)); - // Register the options so AgentEntity can access TTL configuration - services.AddSingleton(options); + if (existingOptionsDescriptor?.ImplementationInstance is DurableAgentsOptions existingOptions) + { + // Merge new agents into the existing options + foreach (KeyValuePair> agent in newAgents) + { + if (!existingOptions.ContainsAgent(agent.Key)) + { + existingOptions.AddAIAgentFactory(agent.Key, agent.Value, options.GetTimeToLive(agent.Key)); + } + } + + options = existingOptions; + } + else + { + // Register the options so AgentEntity can access configuration + services.AddSingleton(options); + } // The keyed services are used to resolve durable agent *proxy* instances for external clients. - foreach (var factory in agents) + foreach (var factory in newAgents) { services.AddKeyedSingleton(factory.Key, (sp, _) => factory.Value(sp).AsDurableAgentProxy(sp)); } // A custom data converter is needed because the default chat client uses camel case for JSON properties, // which is not the default behavior for the Durable Task SDK. - services.AddSingleton(); + services.TryAddSingleton(); return options; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index 21601406fa..e486c4a1cd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -19,12 +19,14 @@ internal static class BuiltInFunctions { internal const string HttpPrefix = "http-"; internal const string McpToolPrefix = "mcptool-"; + internal const string WorkflowMcpToolPrefix = "mcptool-workflow-"; 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 InvokeWorkflowActivityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowActivityAsync)}"; internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}"; + internal static readonly string RunWorkflowMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowMcpToolAsync)}"; #pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - Azure Functions does not use single-file publishing internal static readonly string ScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location); @@ -220,6 +222,41 @@ internal static class BuiltInFunctions return agentResponse.Text; } + /// + /// Runs a workflow via MCP tool trigger. + /// + public static async Task RunWorkflowMcpToolAsync( + [McpToolTrigger("BuiltInWorkflowMcpTool")] ToolInvocationContext context, + [DurableClient] DurableTaskClient client, + FunctionContext functionContext) + { + if (context.Arguments is null) + { + throw new ArgumentException("MCP Tool invocation is missing required arguments."); + } + + if (!context.Arguments.TryGetValue("input", out object? inputObj) || inputObj is not string input) + { + throw new ArgumentException("MCP Tool invocation is missing required 'input' argument of type string."); + } + + // Extract workflow name from the MCP tool name (format: mcptool-workflow-{workflowName}) + string workflowName = context.Name; + string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName); + + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + orchestrationFunctionName, + new DurableWorkflowRunRequest { WorkflowName = workflowName, Input = input }); + + // Wait for the orchestration to complete and return the result + OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs: true, + cancellation: functionContext.CancellationToken); + + return metadata?.ReadOutputAs(); + } + #pragma warning disable DURTASK001 // Durable analyzer complained public static Task WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input) { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs index f626db2a90..7653296b1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs @@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions; internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadataTransformer { private readonly ILogger _logger; - private readonly IReadOnlyDictionary> _agents; + private readonly DurableAgentsOptions _agentOptions; private readonly IServiceProvider _serviceProvider; private readonly IFunctionsAgentOptionsProvider _functionsAgentOptionsProvider; @@ -22,12 +22,12 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat #pragma warning restore IL3000 public DurableAgentFunctionMetadataTransformer( - IReadOnlyDictionary> agents, + DurableAgentsOptions agentOptions, ILogger logger, IServiceProvider serviceProvider, IFunctionsAgentOptionsProvider functionsAgentOptionsProvider) { - this._agents = agents ?? throw new ArgumentNullException(nameof(agents)); + this._agentOptions = agentOptions ?? throw new ArgumentNullException(nameof(agentOptions)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); this._serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); this._functionsAgentOptionsProvider = functionsAgentOptionsProvider ?? throw new ArgumentNullException(nameof(functionsAgentOptionsProvider)); @@ -39,7 +39,7 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat { this._logger.LogTransformingFunctionMetadata(original.Count); - foreach (KeyValuePair> kvp in this._agents) + foreach (KeyValuePair> kvp in this._agentOptions.GetAgentFactories()) { string agentName = kvp.Key; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentsOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentsOptionsExtensions.cs index d3a2fed7a6..4ed6016d0a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentsOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentsOptionsExtensions.cs @@ -141,12 +141,17 @@ public static class DurableAgentsOptionsExtensions ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(factory); - FunctionsAgentOptions agentOptions = new(); - agentOptions.HttpTrigger.IsEnabled = enableHttpTrigger; - agentOptions.McpToolTrigger.IsEnabled = enableMcpToolTrigger; + // Check if agent options already exist (e.g., from a previous ConfigureDurableAgents call) + // If so, preserve the existing options instead of overwriting them + if (!s_agentOptions.ContainsKey(name)) + { + FunctionsAgentOptions agentOptions = new(); + agentOptions.HttpTrigger.IsEnabled = enableHttpTrigger; + agentOptions.McpToolTrigger.IsEnabled = enableMcpToolTrigger; + s_agentOptions[name] = agentOptions; + } options.AddAIAgentFactory(name, factory, timeToLive); - s_agentOptions[name] = agentOptions; return options; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs index 30b7a4dc55..b5abd14103 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs @@ -53,16 +53,23 @@ public static class DurableOptionsExtensions private static void RegisterServices(FunctionsApplicationBuilder builder, DurableOptions options) { builder.Services.TryAddSingleton(options); - builder.Services.TryAddSingleton(options.Agents); // backward compatibility. can be removed in future. + builder.Services.TryAddSingleton(options.Agents); builder.RegisterCoreAgentServices(); } private static void ConfigureAgents(FunctionsApplicationBuilder builder, DurableOptions options) { + // Only configure agents if there are any agent factories registered in DurableOptions + IReadOnlyDictionary> agentFactories = options.Agents.GetAgentFactories(); + if (agentFactories.Count == 0) + { + return; + } + builder.Services.ConfigureDurableAgents(agentOpts => { - foreach (KeyValuePair> agentFactory in options.Agents.GetAgentFactories()) + foreach (KeyValuePair> agentFactory in agentFactories) { bool isWorkflowOnly = options.Agents.IsWorkflowOnly(agentFactory.Key); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs index d231e2550c..8ef3919f5a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs @@ -25,6 +25,9 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta { this._logger.LogTransformStart(original.Count); + // Track registered function names to avoid duplicates when the same executor is used in multiple workflows + HashSet registeredFunctionNames = new(); + foreach (var workflow in this._options.Workflows) { this._logger.LogAddingWorkflowFunction(workflow.Key); @@ -38,9 +41,17 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta this._logger.LogAddingHttpTrigger(workflow.Key); original.Add(CreateHttpTrigger(workflow.Key, $"workflows/{workflow.Key}/run")); + // Check if MCP tool trigger is enabled for this workflow + if (DurableWorkflowOptionsExtensions.TryGetWorkflowOptions(workflow.Key, out FunctionsWorkflowOptions? workflowOptions) && + workflowOptions?.McpToolTrigger.IsEnabled == true) + { + this._logger.LogAddingMcpToolTrigger(workflow.Key); + original.Add(CreateMcpToolTrigger(workflow.Key, workflow.Value.Description)); + } + // 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 }; + HashSet executorIds = new() { workflow.Value.StartExecutorId }; var reflectedEdges = workflow.Value.ReflectEdges(); foreach (var (sourceId, edgeSet) in reflectedEdges) @@ -64,6 +75,13 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta string executorName = WorkflowNamingHelper.GetExecutorName(executorId); string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName); + // Skip if this function has already been registered by another workflow + if (!registeredFunctionNames.Add(functionName)) + { + this._logger.LogSkippingDuplicateFunction(functionName, workflow.Key); + continue; + } + // Check if the executor type is an agent-related type if (WorkflowHelper.IsAgentExecutorType(executorInfo.ExecutorType)) { @@ -132,19 +150,20 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta }; } - //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, - // }; - //} + private static DefaultFunctionMetadata CreateMcpToolTrigger(string workflowName, string? description) + { + return new DefaultFunctionMetadata + { + Name = $"{BuiltInFunctions.WorkflowMcpToolPrefix}{workflowName}", + Language = "dotnet-isolated", + RawBindings = + [ + $$"""{"name":"context","type":"mcpToolTrigger","direction":"In","toolName":"{{workflowName}}","description":"{{description ?? $"Run the {workflowName} workflow"}}","toolProperties":"[{\"propertyName\":\"input\",\"propertyType\":\"string\",\"description\":\"The input to the workflow.\",\"isRequired\":true,\"isArray\":false}]"}""", + """{"name":"input","type":"mcpToolProperty","direction":"In","propertyName":"input","description":"The input to the workflow","isRequired":true,"dataType":"String","propertyType":"string"}""", + """{"name":"client","type":"durableClient","direction":"In"}""" + ], + EntryPoint = BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint, + 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 index f7554a5652..aa6cfb1083 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformerLogs.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformerLogs.cs @@ -12,32 +12,50 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions; internal static partial class DurableWorkflowFunctionMetadataTransformerLogs { [LoggerMessage( + EventId = 200, 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( + EventId = 201, Level = LogLevel.Information, Message = "Adding durable workflow function for workflow: {WorkflowName}")] public static partial void LogAddingWorkflowFunction(this ILogger logger, string workflowName); [LoggerMessage( + EventId = 202, Level = LogLevel.Information, Message = "Adding HTTP trigger function for workflow: {WorkflowName}")] public static partial void LogAddingHttpTrigger(this ILogger logger, string workflowName); [LoggerMessage( + EventId = 203, 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( + EventId = 204, 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( + EventId = 205, + Level = LogLevel.Information, + Message = "Adding MCP tool trigger function for workflow: {WorkflowName}")] + public static partial void LogAddingMcpToolTrigger(this ILogger logger, string workflowName); + + [LoggerMessage( + EventId = 206, Level = LogLevel.Information, Message = "Transform finished. Updated function count: {FunctionCount}")] public static partial void LogTransformFinished(this ILogger logger, int functionCount); + + [LoggerMessage( + EventId = 207, + Level = LogLevel.Debug, + Message = "Skipping duplicate function registration: {FunctionName} (already registered by another workflow) in workflow: {WorkflowName}")] + public static partial void LogSkippingDuplicateFunction(this ILogger logger, string functionName, string workflowName); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowOptionsExtensions.cs new file mode 100644 index 0000000000..3f91e484b8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowOptionsExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.DurableTask; +using Microsoft.Agents.AI.Workflows; + +namespace Microsoft.Agents.AI.Hosting.AzureFunctions; + +/// +/// Provides extension methods for registering and configuring workflows in the context of the Azure Functions hosting environment. +/// +public static class DurableWorkflowOptionsExtensions +{ + // Registry of workflow options. + private static readonly Dictionary s_workflowOptions = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Adds a workflow to the specified instance and optionally configures + /// workflow-specific options. + /// + /// The instance to which the workflow will be added. + /// The workflow to add. The workflow's Name property must not be null or empty. + /// An optional delegate to configure workflow-specific options. If null, default options are used. + /// The updated instance containing the added workflow. + /// Thrown when or is null. + /// Thrown when the workflow does not have a valid name. + public static DurableWorkflowOptions AddWorkflow( + this DurableWorkflowOptions options, + Workflow workflow, + Action? configure) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(workflow); + + if (string.IsNullOrEmpty(workflow.Name)) + { + throw new ArgumentException("Workflow must have a valid Name property.", nameof(workflow)); + } + + // Initialize with default behavior (MCP trigger disabled) + FunctionsWorkflowOptions workflowOptions = new(); + configure?.Invoke(workflowOptions); + + options.AddWorkflow(workflow); + s_workflowOptions[workflow.Name] = workflowOptions; + + return options; + } + + /// + /// Adds a workflow to the specified instance and configures + /// trigger support for MCP tool invocations. + /// + /// The instance to which the workflow will be added. + /// The workflow to add. The workflow's Name property must not be null or empty. + /// true to enable an MCP tool trigger for the workflow; otherwise, false. + /// The updated instance with the specified workflow and trigger configuration applied. + /// Thrown when or is null. + /// Thrown when the workflow does not have a valid name. + public static DurableWorkflowOptions AddWorkflow( + this DurableWorkflowOptions options, + Workflow workflow, + bool enableMcpToolTrigger) + { + return AddWorkflow(options, workflow, workflowOptions => workflowOptions.McpToolTrigger.IsEnabled = enableMcpToolTrigger); + } + + /// + /// Tries to get the for a workflow by name. + /// + /// The name of the workflow. + /// When this method returns, contains the workflow options if found; otherwise, null. + /// true if the workflow options were found; otherwise, false. + internal static bool TryGetWorkflowOptions(string workflowName, out FunctionsWorkflowOptions? workflowOptions) + { + return s_workflowOptions.TryGetValue(workflowName, out workflowOptions); + } + + /// + /// Builds the workflow options used for dependency injection (read-only copy). + /// + internal static IReadOnlyDictionary GetWorkflowOptionsSnapshot() + { + return new Dictionary(s_workflowOptions, StringComparer.OrdinalIgnoreCase); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsWorkflowOptions.cs new file mode 100644 index 0000000000..32632ddc71 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsWorkflowOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.Hosting.AzureFunctions; + +/// +/// Provides configuration options for enabling and customizing function triggers for a workflow. +/// +public sealed class FunctionsWorkflowOptions +{ + /// + /// Gets or sets the options used to configure the MCP tool trigger behavior. + /// + /// + /// By default, MCP tool trigger is disabled for workflows. + /// + public McpToolTriggerOptions McpToolTrigger { get; set; } = new(false); +}