This commit is contained in:
Shyju Krishnankutty
2026-01-12 17:26:36 -08:00
Unverified
parent 536364131d
commit 5090ec393e
11 changed files with 223 additions and 58 deletions
@@ -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<string, string> uppercaseFunc = s => s.ToUpperInvariant();
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
@@ -35,7 +36,9 @@ Func<string, string> 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.
@@ -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.
@@ -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<TaskOrchestrationContext>(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}.");
}
@@ -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<string>();
//}
//[Function("dafx-Orchestration")]
public static async Task<List<string>> RunWorkflowOrchestratorAsync(TaskOrchestrationContext taskOrchestrationContext)
public static async Task<List<string>> RunWorkflowOrchestratorAsync(string taskOrchestrationContext, FunctionContext functionsContext)
{
//ILogger logger = context.CreateReplaySafeLogger(nameof(Function));
//logger.LogInformation("Invoking RunWorkflowOrchestrator");
var logger = functionsContext.GetLogger("BuiltInFunctions");
var outputs = new List<string>();
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<DurableWorkflowRunner>();
//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}");
@@ -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<IFunctionMetadata> 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<string> { 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<string, ExecutorInfo> 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,
};
}
}
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
/// <summary>
/// Logging messages for <see cref="DurableWorkflowFunctionMetadataTransformer"/>.
/// </summary>
[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);
}
@@ -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<DurableWorkflowRunner> _logger;
public DurableWorkflowRunner(ILogger<DurableWorkflowRunner> 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;
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
/// <summary>
/// Logging messages for <see cref="DurableWorkflowRunner"/>.
/// </summary>
[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);
}
@@ -25,6 +25,7 @@ public static class FunctionsApplicationBuilderExtensions
var options = new DurableWorkflowOptions();
configure(options);
builder.Services.AddSingleton(options);
builder.Services.AddSingleton<DurableWorkflowRunner>();
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>();
return builder;
@@ -2,16 +2,36 @@
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
internal sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId)
/// <summary>
/// Represents information about an executor in a workflow, including its type and identifier.
/// </summary>
/// <param name="ExecutorType">The type identifier of the executor.</param>
/// <param name="ExecutorId">The unique identifier of the executor instance.</param>
public sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId)
{
/// <summary>
/// Determines whether this executor info matches a specific executor type by generic parameter.
/// </summary>
/// <typeparam name="T">The executor type to match against.</typeparam>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch<T>() where T : Executor =>
this.ExecutorType.IsMatch<T>()
&& this.ExecutorId == typeof(T).Name;
/// <summary>
/// Determines whether this executor info matches a given executor instance.
/// </summary>
/// <param name="executor">The executor instance to match against.</param>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch(Executor executor) =>
this.ExecutorType.IsMatch(executor.GetType())
&& this.ExecutorId == executor.Id;
/// <summary>
/// Determines whether this executor info matches a given executor binding.
/// </summary>
/// <param name="binding">The executor binding to match against.</param>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch(ExecutorBinding binding) =>
this.ExecutorType.IsMatch(binding.ExecutorType)
&& this.ExecutorId == binding.Id;
@@ -36,6 +36,18 @@ public class Workflow
);
}
/// <summary>
/// Gets information about all executors in the workflow, keyed by their ID.
/// </summary>
/// <returns>A dictionary mapping executor IDs to their <see cref="ExecutorInfo"/>.</returns>
public Dictionary<string, ExecutorInfo> ReflectExecutors()
{
return this.ExecutorBindings.Values.ToDictionary(
keySelector: binding => binding.Id,
elementSelector: RepresentationExtensions.ToExecutorInfo
);
}
internal Dictionary<string, RequestPort> Ports { get; init; } = [];
/// <summary>