mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
WIP
This commit is contained in:
@@ -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}");
|
||||
|
||||
+56
-32
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -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);
|
||||
}
|
||||
+1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user