mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
wip
This commit is contained in:
@@ -3,10 +3,14 @@
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="LocalNugetSource" value="D:\LocalNugetSource" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="LocalNugetSource">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
public static class OrchFunction
|
||||
{
|
||||
[Function(nameof(OrchFunction))]
|
||||
public static async Task<List<string>> RunOrchestratorAsync(
|
||||
[OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
ILogger logger = context.CreateReplaySafeLogger(nameof(OrchFunction));
|
||||
logger.LogInformation("Saying hello.");
|
||||
var outputs = new List<string>();
|
||||
|
||||
outputs.Add("Tokyo");
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
[Function("OrchFunction_HttpStart")]
|
||||
public static async Task<HttpResponseData> HttpStartAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client,
|
||||
FunctionContext executionContext)
|
||||
{
|
||||
// Function input comes from the request content.
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
nameof(OrchFunction));
|
||||
|
||||
// Returns an HTTP 202 response with an instance management payload.
|
||||
// See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration
|
||||
return await client.CreateCheckStatusResponseAsync(req, instanceId);
|
||||
}
|
||||
}
|
||||
@@ -41,16 +41,16 @@ builder.AddEdge(agent2, reverse).WithOutputFrom(agent2);
|
||||
|
||||
var workflow = builder.WithName("MyTestWorkflow").Build();
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
// 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(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.AddDurableWorkflows(options =>
|
||||
//.ConfigureDurableAgents(op => op.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.ConfigureDurableOptions(options =>
|
||||
{
|
||||
// Configure durable workflow options here if needed.
|
||||
options.AddWorkflow(workflow);
|
||||
// Configure workflows - agents referenced in workflows are automatically registered!
|
||||
options.Workflows.AddWorkflow(workflow);
|
||||
})
|
||||
.Build();
|
||||
app.Run();
|
||||
|
||||
@@ -11,7 +11,10 @@ public sealed class DurableAgentsOptions
|
||||
private readonly Dictionary<string, Func<IServiceProvider, AIAgent>> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, TimeSpan?> _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal DurableAgentsOptions()
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DurableAgentsOptions"/> class.
|
||||
/// </summary>
|
||||
public DurableAgentsOptions()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,25 @@ namespace Microsoft.Agents.AI.DurableTask;
|
||||
public sealed class DurableWorkflowOptions
|
||||
{
|
||||
private readonly Dictionary<string, Workflow> _workflows = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object? _parentOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DurableWorkflowOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parentOptions">Optional parent options container for accessing related configuration.</param>
|
||||
public DurableWorkflowOptions(object? parentOptions = null)
|
||||
{
|
||||
this._parentOptions = parentOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a workflow to the collection for processing or execution.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow instance to add. Cannot be null.</param>
|
||||
/// <remarks>
|
||||
/// When a workflow is added, any AI agent executors in the workflow will be automatically
|
||||
/// registered with the DurableAgentsOptions if it was provided during construction.
|
||||
/// </remarks>
|
||||
public void AddWorkflow(Workflow workflow)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
@@ -25,6 +39,13 @@ public sealed class DurableWorkflowOptions
|
||||
}
|
||||
|
||||
this._workflows[workflow.Name] = workflow;
|
||||
|
||||
// Register any agentic executors with DurableAgentsOptions if available through parent
|
||||
DurableAgentsOptions? agentOptions = this.TryGetAgentOptions();
|
||||
if (agentOptions is not null)
|
||||
{
|
||||
RegisterAgenticExecutors(workflow, agentOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -34,4 +55,46 @@ public sealed class DurableWorkflowOptions
|
||||
/// Changes to the underlying workflow collection are immediately visible through this property. Accessing a
|
||||
/// workflow by name that does not exist will result in a KeyNotFoundException.</remarks>
|
||||
public IReadOnlyDictionary<string, Workflow> Workflows => this._workflows;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075",
|
||||
Justification = "Reflection is used to access Agents property from parent options container for automatic agent registration.")]
|
||||
private DurableAgentsOptions? TryGetAgentOptions()
|
||||
{
|
||||
// Try to extract DurableAgentsOptions from the parent container
|
||||
// This uses reflection to access the Agents property if available
|
||||
if (this._parentOptions is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if parent has an Agents property (DurableOptions pattern)
|
||||
System.Reflection.PropertyInfo? agentsProperty = this._parentOptions.GetType()
|
||||
.GetProperty("Agents", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (agentsProperty?.PropertyType == typeof(DurableAgentsOptions))
|
||||
{
|
||||
return agentsProperty.GetValue(this._parentOptions) as DurableAgentsOptions;
|
||||
}
|
||||
|
||||
// If parent is directly DurableAgentsOptions (for backward compatibility)
|
||||
return this._parentOptions as DurableAgentsOptions;
|
||||
}
|
||||
|
||||
private static void RegisterAgenticExecutors(Workflow workflow, DurableAgentsOptions agentOptions)
|
||||
{
|
||||
// Use the public EnumerateAgentExecutors method to get all AIAgent instances
|
||||
foreach (AIAgent agent in workflow.EnumerateAgentExecutors())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Register the agent with DurableAgentsOptions
|
||||
agentOptions.AddAIAgent(agent);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Agent with this name is already registered, skip it
|
||||
// This is expected behavior when multiple workflows use the same agent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Azure.Functions.Worker.Invocation;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
@@ -26,17 +25,9 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
||||
IFunctionInputBindingFeature? functionInputBindingFeature = context.Features.Get<IFunctionInputBindingFeature>() ??
|
||||
throw new InvalidOperationException("Function input binding feature is not available on the current context.");
|
||||
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint)
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint)
|
||||
{
|
||||
var triggerBinding = context.FunctionDefinition.InputBindings.Values.FirstOrDefault(b => b.Type == "orchestrationTrigger");
|
||||
var taskOrechstrationContextBinding = context.BindInputAsync<TaskOrchestrationContext>(triggerBinding!);
|
||||
|
||||
if (taskOrechstrationContextBinding.IsCompletedSuccessfully)
|
||||
{
|
||||
var t = taskOrechstrationContextBinding.Result.Value;
|
||||
context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync("todo", context);
|
||||
}
|
||||
|
||||
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeWorkflowActivityAsync("aa", context);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,25 +36,17 @@ internal static class BuiltInFunctions
|
||||
[ActivityTrigger] string input,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
return Task.FromResult($"Hello from activity with input: {input}");
|
||||
string activityFunctionName = functionContext.FunctionDefinition.Name;
|
||||
|
||||
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService<DurableWorkflowRunner>();
|
||||
return runner.ExecuteActivityAsync(activityFunctionName, input, functionContext);
|
||||
}
|
||||
|
||||
//[Function("my-Orchestration")]
|
||||
//public static async Task<List<string>> RunOrchestrator1Async(
|
||||
//[OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
//{
|
||||
// ILogger logger = context.CreateReplaySafeLogger(nameof(Function));
|
||||
// logger.LogInformation("Saying hello.");
|
||||
|
||||
// // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
|
||||
// return new List<string>();
|
||||
//}
|
||||
|
||||
public static async Task<List<string>> RunWorkflowOrchestratorAsync(string taskOrchestrationContext, FunctionContext functionsContext)
|
||||
{
|
||||
var logger = functionsContext.GetLogger("BuiltInFunctions");
|
||||
var outputs = new List<string>();
|
||||
const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrtionContext
|
||||
const string WorkflowName = "MyTestWorkflow"; // to do: get from TaskOrchestrationContext
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("Orchestrator {WorkflowName} is executing. Input: {Input}", WorkflowName, taskOrchestrationContext);
|
||||
@@ -95,10 +87,10 @@ internal static class BuiltInFunctions
|
||||
FunctionContext context)
|
||||
{
|
||||
// to do: Retrieve the workflow and execute it.
|
||||
var workflowName = context.FunctionDefinition.Name.Replace("http", "dafx");
|
||||
|
||||
var workflowName = context.FunctionDefinition.Name.Replace("http-", "");
|
||||
var inputMessage = await req.ReadAsStringAsync();
|
||||
//string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow");
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow"); //OrchFunction"); // 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}");
|
||||
@@ -240,6 +232,26 @@ internal static class BuiltInFunctions
|
||||
return agentResponse.Text;
|
||||
}
|
||||
|
||||
#pragma warning disable DURTASK001 // Durable analyzer complained
|
||||
public static Task<List<string>> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DuableWorkflowRunRequest input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
ILogger logger = context.CreateReplaySafeLogger("BuiltInFunctions");
|
||||
logger.LogInformation("WorkflowRunnerOrchestrationAsync function called.");
|
||||
|
||||
FunctionContext? functionContext = context.GetFunctionContext();
|
||||
if (functionContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
|
||||
}
|
||||
|
||||
var workFlowName = input.WorkflowName;
|
||||
|
||||
return Task.FromResult(new List<string>() { workFlowName });
|
||||
}
|
||||
#pragma warning restore DURTASK001
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error response with the specified status code and error message.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to run a workflow in the Duable system.
|
||||
/// </summary>
|
||||
public sealed class DuableWorkflowRunRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the workflow.
|
||||
/// </summary>
|
||||
[JsonPropertyName("workflowName")]
|
||||
public string WorkflowName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the input string to be processed or analyzed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("input")]
|
||||
public string Input { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for durable features in Azure Functions.
|
||||
/// </summary>
|
||||
public sealed class DurableOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the configuration options for durable agents.
|
||||
/// </summary>
|
||||
public DurableAgentsOptions Agents { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration options for durable workflows.
|
||||
/// </summary>
|
||||
public DurableWorkflowOptions Workflows { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DurableOptions"/> class.
|
||||
/// </summary>
|
||||
internal DurableOptions()
|
||||
{
|
||||
this.Workflows = new DurableWorkflowOptions(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Worker;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring durable options (agents and workflows).
|
||||
/// </summary>
|
||||
public static class DurableOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures durable agents and workflows in a unified way.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Functions application builder.</param>
|
||||
/// <param name="configure">A delegate to configure the durable options.</param>
|
||||
/// <returns>The Functions application builder for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// This method provides a unified configuration point for both durable agents and workflows.
|
||||
/// Agents configured here will be automatically registered when referenced in workflows.
|
||||
/// </remarks>
|
||||
public static FunctionsApplicationBuilder ConfigureDurableOptions(
|
||||
this FunctionsApplicationBuilder builder,
|
||||
Action<DurableOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
DurableOptions options = new();
|
||||
configure(options);
|
||||
|
||||
// Register the unified DurableOptions for components that need both agents and workflows
|
||||
builder.Services.AddSingleton(options);
|
||||
|
||||
// Register AgentsOption for backward compatibility. Can be removed if needed, after syncing with team.
|
||||
builder.Services.AddSingleton(options.Agents);
|
||||
|
||||
// Configure agents using the existing infrastructure
|
||||
builder.Services.ConfigureDurableAgents(agentOpts =>
|
||||
{
|
||||
// Copy agent registrations from the unified options to the service-level options
|
||||
foreach (KeyValuePair<string, Func<IServiceProvider, AIAgent>> agentFactory in options.Agents.GetAgentFactories())
|
||||
{
|
||||
agentOpts.AddAIAgentFactory(
|
||||
agentFactory.Key,
|
||||
agentFactory.Value,
|
||||
options.Agents.GetTimeToLive(agentFactory.Key));
|
||||
}
|
||||
|
||||
// Copy TTL settings
|
||||
agentOpts.DefaultTimeToLive = options.Agents.DefaultTimeToLive;
|
||||
agentOpts.MinimumTimeToLiveSignalDelay = options.Agents.MinimumTimeToLiveSignalDelay;
|
||||
});
|
||||
|
||||
builder.Services.TryAddSingleton<IFunctionsAgentOptionsProvider>(_ =>
|
||||
new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
|
||||
|
||||
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableAgentFunctionMetadataTransformer>();
|
||||
|
||||
// Handling of built-in function execution for Agent HTTP, MCP tool, or Entity invocations.
|
||||
builder.UseWhen<BuiltInFunctionExecutionMiddleware>(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));
|
||||
builder.Services.AddSingleton<BuiltInFunctionExecutor>();
|
||||
|
||||
builder.Services.AddSingleton<DurableWorkflowRunner>();
|
||||
|
||||
builder.ConfigureDurableWorker().AddTasks(t => t.AddOrchestratorFunc<DuableWorkflowRunRequest, List<string>>(
|
||||
"WorkflowRunnerOrchestration",
|
||||
async (tc, inputBindingData) =>
|
||||
{
|
||||
FunctionContext? functionContext = tc.GetFunctionContext();
|
||||
if (functionContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
|
||||
}
|
||||
|
||||
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService<DurableWorkflowRunner>();
|
||||
return await runner.RunWorkflowOrchestrationAsync(tc, inputBindingData).ConfigureAwait(false);
|
||||
}));
|
||||
|
||||
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
+21
-19
@@ -12,10 +12,11 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
|
||||
private readonly ILogger<DurableWorkflowFunctionMetadataTransformer> _logger;
|
||||
private readonly DurableWorkflowOptions _options;
|
||||
|
||||
public DurableWorkflowFunctionMetadataTransformer(ILogger<DurableWorkflowFunctionMetadataTransformer> logger, DurableWorkflowOptions durableWorkflowOptions)
|
||||
public DurableWorkflowFunctionMetadataTransformer(ILogger<DurableWorkflowFunctionMetadataTransformer> logger, DurableOptions durableOptions)
|
||||
{
|
||||
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this._options = durableWorkflowOptions ?? throw new ArgumentNullException(nameof(durableWorkflowOptions));
|
||||
ArgumentNullException.ThrowIfNull(durableOptions);
|
||||
this._options = durableOptions.Workflows;
|
||||
}
|
||||
|
||||
public string Name => nameof(DurableWorkflowFunctionMetadataTransformer);
|
||||
@@ -28,7 +29,7 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
|
||||
{
|
||||
this._logger.LogAddingWorkflowFunction(workflow.Key);
|
||||
|
||||
original.Add(CreateOrchestrationTrigger(workflow.Key));
|
||||
//original.Add(CreateOrchestrationTrigger(workflow.Key));
|
||||
|
||||
// We also want to create an HTTP trigger for this orchestration so users can start it via HTTP.
|
||||
this._logger.LogAddingHttpTrigger(workflow.Key);
|
||||
@@ -57,7 +58,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
|
||||
{
|
||||
if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo))
|
||||
{
|
||||
string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}_{executorId}";
|
||||
// string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}-{executorId}"; //.Split("_")[0]
|
||||
string functionName = $"{AgentSessionId.ToEntityName(workflow.Key)}-{executorId.Split("_")[0]}"; //.Split("_")[0]
|
||||
|
||||
// Check if the executor type is an agent-related type
|
||||
if (IsAgentExecutorType(executorInfo.ExecutorType))
|
||||
@@ -106,22 +108,22 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
|
||||
};
|
||||
}
|
||||
|
||||
private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
|
||||
{
|
||||
return new DefaultFunctionMetadata()
|
||||
{
|
||||
Name = AgentSessionId.ToEntityName(name),
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
// """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
|
||||
"""{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
|
||||
//private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
|
||||
//{
|
||||
// return new DefaultFunctionMetadata()
|
||||
// {
|
||||
// Name = AgentSessionId.ToEntityName(name),
|
||||
// Language = "dotnet-isolated",
|
||||
// RawBindings =
|
||||
// [
|
||||
// // """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
|
||||
// """{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
|
||||
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
|
||||
ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
};
|
||||
}
|
||||
// ],
|
||||
// EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
|
||||
// ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
// };
|
||||
//}
|
||||
|
||||
private static DefaultFunctionMetadata CreateActivityTrigger(string functionName)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -11,40 +13,149 @@ internal sealed class DurableWorkflowRunner
|
||||
{
|
||||
private readonly DurableWorkflowOptions _options;
|
||||
private readonly ILogger<DurableWorkflowRunner> _logger;
|
||||
public DurableWorkflowRunner(ILogger<DurableWorkflowRunner> logger, DurableWorkflowOptions durableWorkflowOptions)
|
||||
|
||||
public DurableWorkflowRunner(ILogger<DurableWorkflowRunner> logger, DurableOptions durableOptions)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._options = durableWorkflowOptions;
|
||||
ArgumentNullException.ThrowIfNull(durableOptions);
|
||||
this._options = durableOptions.Workflows;
|
||||
}
|
||||
|
||||
internal async Task RunAsync(
|
||||
TaskOrchestrationContext taskOrchestrationContext,
|
||||
string workflowName,
|
||||
object? initialInput = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
internal async Task<List<string>> RunWorkflowOrchestrationAsync(TaskOrchestrationContext context, DuableWorkflowRunRequest input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
string workflowName = input.WorkflowName;
|
||||
|
||||
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
|
||||
if (!this._options.Workflows.TryGetValue(workflowName, out Workflow? wf))
|
||||
{
|
||||
throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
|
||||
}
|
||||
|
||||
this._logger.LogRunningWorkflow(wf.Name);
|
||||
|
||||
var result = await this.RunExecutorsInWorkFlowAsync(context, wf, input.Input).ConfigureAwait(false);
|
||||
|
||||
return [result];
|
||||
}
|
||||
|
||||
private Task RunExecutorsInWorkFlowAsync(
|
||||
private async Task<string> RunExecutorsInWorkFlowAsync(
|
||||
TaskOrchestrationContext taskOrchestrationContext,
|
||||
Workflow wf,
|
||||
object? initialInput,
|
||||
CancellationToken cancellationToken)
|
||||
Workflow workflow,
|
||||
string initialInput)
|
||||
{
|
||||
// Extract edeges and executors from the workflow and execute them in order/based on the pattern as Durable entities.
|
||||
List<string> executorResult = [];
|
||||
|
||||
return Task.CompletedTask;
|
||||
if (this._logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
foreach (WorkflowExecutorInfo executorInfo in WorkflowHelper.GetExecutorsFromWorkflowInOrder(workflow))
|
||||
{
|
||||
string triggerName = this.BuildTriggerName(workflow.Name!, executorInfo.ExecutorId);
|
||||
this._logger.LogInformation(
|
||||
" Scheduling executor '{ExecutorId}' (IsAgentic: {IsAgentic}) with trigger name '{TriggerName}'",
|
||||
executorInfo.ExecutorId,
|
||||
executorInfo.IsAgenticExecutor,
|
||||
triggerName);
|
||||
|
||||
string input = executorResult.Count == 0 ? initialInput : executorResult.Last()!;
|
||||
if (!executorInfo.IsAgenticExecutor)
|
||||
{
|
||||
var result = await taskOrchestrationContext.CallActivityAsync<string>(triggerName, input);
|
||||
executorResult.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
string AgentName = this.GetAgentNameFromExecutorId(workflow.Name!, executorInfo.ExecutorId);
|
||||
this._logger.LogInformation(
|
||||
" Invoking agentic executor '{ExecutorId}'",
|
||||
AgentName);
|
||||
DurableAIAgent agent = taskOrchestrationContext.GetAgent(AgentName);
|
||||
if (agent != null)
|
||||
{
|
||||
AgentThread destinationThread = agent.GetNewThread();
|
||||
var agentResponse = await agent.RunAsync(input, destinationThread);
|
||||
executorResult.Add(agentResponse.Text);
|
||||
this._logger.LogInformation(
|
||||
"Agentic executor '{ExecutorId}' completed with response: {AgentResponse}",
|
||||
AgentName,
|
||||
agentResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return final result
|
||||
return executorResult.Last();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private string GetAgentNameFromExecutorId(string workflowName, string executorId)
|
||||
{
|
||||
// Example: "InspirationBot_edaac621050849efb1a62805fa03d3f8"
|
||||
var parts = executorId.Split('_');
|
||||
return parts.Length > 0 ? parts[0] : executorId;
|
||||
}
|
||||
|
||||
private string BuildTriggerName(string workflowName, string executorName)
|
||||
{
|
||||
return $"dafx-{workflowName}-{executorName}";
|
||||
}
|
||||
|
||||
internal async Task<string> ExecuteActivityAsync(string activityFunctionName, string input, FunctionContext functionContext)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(activityFunctionName);
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
ArgumentNullException.ThrowIfNull(functionContext);
|
||||
|
||||
// Parse the activity function name to extract workflow name and executor name
|
||||
// Format: "dafx-{workflowName}-{executorName}"
|
||||
if (!activityFunctionName.StartsWith("dafx-", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"Activity function name '{activityFunctionName}' does not start with 'dafx-' prefix.");
|
||||
}
|
||||
|
||||
string nameWithoutPrefix = activityFunctionName["dafx-".Length..];
|
||||
string[] parts = nameWithoutPrefix.Split('-', 2);
|
||||
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
throw new InvalidOperationException($"Activity function name '{activityFunctionName}' is not in the expected format 'dafx-{{workflowName}}-{{executorName}}'.");
|
||||
}
|
||||
|
||||
string workflowName = parts[0];
|
||||
string executorName = parts[1];
|
||||
|
||||
this._logger.LogAttemptingToExecuteActivity(workflowName, executorName);
|
||||
|
||||
// Get the workflow
|
||||
if (!this._options.Workflows.TryGetValue(workflowName, out Workflow? workflow))
|
||||
{
|
||||
throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
|
||||
}
|
||||
|
||||
// Get the executor info
|
||||
Dictionary<string, ExecutorInfo> executorInfos = workflow.ReflectExecutors();
|
||||
|
||||
// Find the executor by matching the executor name (which may have a GUID suffix)
|
||||
KeyValuePair<string, ExecutorInfo> executorPair = executorInfos.FirstOrDefault(e =>
|
||||
e.Key.StartsWith(executorName + "_", StringComparison.Ordinal) ||
|
||||
string.Equals(e.Key, executorName, StringComparison.Ordinal));
|
||||
|
||||
if (executorPair.Key == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Executor '{executorName}' not found in workflow '{workflowName}'.");
|
||||
}
|
||||
|
||||
this._logger.LogExecutingActivity(executorPair.Key, executorPair.Value.ExecutorType.TypeName);
|
||||
|
||||
const string result = "Many types are internal.";
|
||||
|
||||
this._logger.LogActivityExecuted(executorPair.Key, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -20,4 +20,19 @@ internal static partial class DurableWorkflowRunnerLogs
|
||||
Level = LogLevel.Information,
|
||||
Message = "Running workflow: {WorkflowName}")]
|
||||
public static partial void LogRunningWorkflow(this ILogger logger, string? workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Attempting to execute activity in workflow '{WorkflowName}' for executor '{ExecutorName}'")]
|
||||
public static partial void LogAttemptingToExecuteActivity(this ILogger logger, string workflowName, string executorName);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Executing activity for executor '{ExecutorId}' of type '{ExecutorType}'")]
|
||||
public static partial void LogExecutingActivity(this ILogger logger, string executorId, string executorType);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Activity executed for executor '{ExecutorId}' with result: {Result}")]
|
||||
public static partial void LogActivityExecuted(this ILogger logger, string executorId, string result);
|
||||
}
|
||||
|
||||
+2
-18
@@ -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;
|
||||
@@ -14,23 +14,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
/// </summary>
|
||||
public static class FunctionsApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds support for durable workflows to the specified Functions application builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Functions application builder to configure with durable workflow capabilities.</param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns>The same instance of <see cref="FunctionsApplicationBuilder"/> to allow for method chaining.</returns>
|
||||
public static FunctionsApplicationBuilder AddDurableWorkflows(this FunctionsApplicationBuilder builder, Action<DurableWorkflowOptions> configure)
|
||||
{
|
||||
var options = new DurableWorkflowOptions();
|
||||
configure(options);
|
||||
builder.Services.AddSingleton(options);
|
||||
builder.Services.AddSingleton<DurableWorkflowRunner>();
|
||||
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to use durable agents with a builder pattern.
|
||||
/// </summary>
|
||||
@@ -57,6 +40,7 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
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));
|
||||
builder.Services.AddSingleton<BuiltInFunctionExecutor>();
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- CA2007: This rule should generally be suppressed in Durable Task libraries. Also, this is not library code. -->
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
<NoWarn>$(NoWarn);CA2007;AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an executor in the workflow with its metadata.
|
||||
/// </summary>
|
||||
/// <param name="ExecutorId">The unique identifier of the executor.</param>
|
||||
/// <param name="IsAgenticExecutor">Indicates whether this executor is an agentic executor.</param>
|
||||
internal sealed record WorkflowExecutorInfo(string ExecutorId, bool IsAgenticExecutor);
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a workflow instance and returns a list of executors with metadata in the order they should be executed.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow instance to analyze.</param>
|
||||
/// <returns>A list of executor information in topological order (execution order).</returns>
|
||||
public static List<WorkflowExecutorInfo> GetExecutorsFromWorkflowInOrder(Workflow workflow)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
|
||||
Dictionary<string, ExecutorInfo> executors = workflow.ReflectExecutors();
|
||||
Dictionary<string, HashSet<EdgeInfo>> edges = workflow.ReflectEdges();
|
||||
|
||||
// Build adjacency list and in-degree map
|
||||
Dictionary<string, List<string>> adjacencyList = new();
|
||||
Dictionary<string, int> inDegree = new();
|
||||
|
||||
// Initialize all executors with in-degree 0
|
||||
foreach (string executorId in executors.Keys)
|
||||
{
|
||||
adjacencyList[executorId] = new List<string>();
|
||||
inDegree[executorId] = 0;
|
||||
}
|
||||
|
||||
// Build the graph from edges
|
||||
foreach (KeyValuePair<string, HashSet<EdgeInfo>> edgeGroup in edges)
|
||||
{
|
||||
string sourceId = edgeGroup.Key;
|
||||
|
||||
foreach (EdgeInfo edge in edgeGroup.Value)
|
||||
{
|
||||
// For each sink (target) in this edge
|
||||
foreach (string sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
// Add edge from source to sink
|
||||
adjacencyList[sourceId].Add(sinkId);
|
||||
|
||||
// Increment in-degree of the sink
|
||||
if (inDegree.TryGetValue(sinkId, out int currentDegree))
|
||||
{
|
||||
inDegree[sinkId] = currentDegree + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform topological sort using Kahn's algorithm
|
||||
List<string> orderedExecutorIds = new();
|
||||
Queue<string> queue = new();
|
||||
|
||||
// Start with the workflow's starting executor
|
||||
queue.Enqueue(workflow.StartExecutorId);
|
||||
|
||||
// Also add any other executors with in-degree 0 (shouldn't be any if workflow is well-formed)
|
||||
foreach (KeyValuePair<string, int> kvp in inDegree)
|
||||
{
|
||||
if (kvp.Value == 0 && kvp.Key != workflow.StartExecutorId)
|
||||
{
|
||||
queue.Enqueue(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
string current = queue.Dequeue();
|
||||
orderedExecutorIds.Add(current);
|
||||
|
||||
// For each neighbor of the current executor
|
||||
foreach (string neighbor in adjacencyList[current])
|
||||
{
|
||||
inDegree[neighbor]--;
|
||||
|
||||
// If in-degree becomes 0, add to queue
|
||||
if (inDegree[neighbor] == 0)
|
||||
{
|
||||
queue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If result doesn't contain all executors, there might be a cycle or disconnected components
|
||||
if (orderedExecutorIds.Count != executors.Count)
|
||||
{
|
||||
// Add any remaining executors that weren't reached
|
||||
foreach (string executorId in executors.Keys)
|
||||
{
|
||||
if (!orderedExecutorIds.Contains(executorId))
|
||||
{
|
||||
orderedExecutorIds.Add(executorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to WorkflowExecutorInfo with agentic executor detection
|
||||
List<WorkflowExecutorInfo> result = new();
|
||||
foreach (string executorId in orderedExecutorIds)
|
||||
{
|
||||
if (executors.TryGetValue(executorId, out ExecutorInfo? executorInfo))
|
||||
{
|
||||
bool isAgentic = IsAgentExecutorType(executorInfo.ExecutorType);
|
||||
result.Add(new WorkflowExecutorInfo(executorId, isAgentic));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified executor type is an agentic executor.
|
||||
/// </summary>
|
||||
/// <param name="executorType">The executor type to check.</param>
|
||||
/// <returns><c>true</c> if the executor is an agentic executor; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsAgentExecutorType(TypeId executorType)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -204,4 +204,24 @@ public class Workflow
|
||||
.ConfigureAwait(false);
|
||||
return startExecutor.DescribeProtocol();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all AIAgent instances that are directly bound in this workflow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method only returns agents that are directly bound as executor values.
|
||||
/// It does not include agents created by factory functions, as those require
|
||||
/// an IServiceProvider to instantiate.
|
||||
/// </remarks>
|
||||
/// <returns>An enumerable collection of AIAgent instances found in the workflow.</returns>
|
||||
public IEnumerable<AIAgent> EnumerateAgentExecutors()
|
||||
{
|
||||
foreach (ExecutorBinding binding in this.ExecutorBindings.Values)
|
||||
{
|
||||
if (binding.RawValue is AIAgent agent)
|
||||
{
|
||||
yield return agent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user