mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
49d69b3bf5
* Expose workflow as MCP Tool * Expose workflow as MCP Tool * Cleanup * PR feedback fixes * update changelog to include PR numner * Improvements to error handling. * Adding a sample project demonstrating how to setup Agents and Workflows together. * Ensure duplicate agent registrations are properly handled.
215 lines
9.1 KiB
C#
215 lines
9.1 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using Microsoft.Azure.Functions.Worker;
|
|
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.Client;
|
|
|
|
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
|
|
|
/// <summary>
|
|
/// This implementation of function executor handles invocations using the built-in static methods for agent HTTP and entity functions.
|
|
/// </summary>
|
|
/// <remarks>By default, the Azure Functions worker generates function executor and that executor is used for function invocations.
|
|
/// But for the dummy HTTP function we create for agents (by augmenting the metadata), that executor will not have the code to handle that function since the entrypoint is a built-in static method.
|
|
/// </remarks>
|
|
internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
|
{
|
|
public async ValueTask ExecuteAsync(FunctionContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
|
|
// Orchestration triggers use a different input binding mechanism than other triggers.
|
|
// The encoded orchestrator state is retrieved via BindInputAsync on the orchestration trigger binding,
|
|
// not through IFunctionInputBindingFeature. Handle this case first to avoid unnecessary binding work.
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint)
|
|
{
|
|
await ExecuteOrchestrationAsync(context);
|
|
return;
|
|
}
|
|
|
|
// Acquire the input binding feature (fail fast if missing rather than null-forgiving operator).
|
|
IFunctionInputBindingFeature? functionInputBindingFeature = context.Features.Get<IFunctionInputBindingFeature>() ??
|
|
throw new InvalidOperationException("Function input binding feature is not available on the current context.");
|
|
|
|
FunctionInputBindingResult? inputBindingResults = await functionInputBindingFeature.BindFunctionInputAsync(context);
|
|
if (inputBindingResults is not { Values: { } values })
|
|
{
|
|
throw new InvalidOperationException($"Function input binding failed for the invocation {context.InvocationId}");
|
|
}
|
|
|
|
HttpRequestData? httpRequestData = null;
|
|
string? encodedEntityRequest = null;
|
|
DurableTaskClient? durableTaskClient = null;
|
|
ToolInvocationContext? mcpToolInvocationContext = null;
|
|
|
|
foreach (var binding in values)
|
|
{
|
|
switch (binding)
|
|
{
|
|
case HttpRequestData request:
|
|
httpRequestData = request;
|
|
break;
|
|
case string entityRequest:
|
|
encodedEntityRequest = entityRequest;
|
|
break;
|
|
case DurableTaskClient client:
|
|
durableTaskClient = client;
|
|
break;
|
|
case ToolInvocationContext toolContext:
|
|
mcpToolInvocationContext = toolContext;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (durableTaskClient is null)
|
|
{
|
|
// This is not expected to happen since all built-in functions (other than orchestration triggers)
|
|
// are expected to have a Durable Task client binding.
|
|
throw new InvalidOperationException($"Durable Task client binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint)
|
|
{
|
|
if (httpRequestData == null)
|
|
{
|
|
throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestrationHttpTriggerAsync(
|
|
httpRequestData,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.GetWorkflowStatusHttpFunctionEntryPoint)
|
|
{
|
|
if (httpRequestData == null)
|
|
{
|
|
throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.GetWorkflowStatusAsync(
|
|
httpRequestData,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RespondToWorkflowHttpFunctionEntryPoint)
|
|
{
|
|
if (httpRequestData == null)
|
|
{
|
|
throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.RespondToWorkflowAsync(
|
|
httpRequestData,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint)
|
|
{
|
|
if (encodedEntityRequest is null)
|
|
{
|
|
throw new InvalidOperationException($"Activity trigger input binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeWorkflowActivityAsync(
|
|
encodedEntityRequest,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunAgentHttpFunctionEntryPoint)
|
|
{
|
|
if (httpRequestData == null)
|
|
{
|
|
throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.RunAgentHttpAsync(
|
|
httpRequestData,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunAgentEntityFunctionEntryPoint)
|
|
{
|
|
if (encodedEntityRequest is null)
|
|
{
|
|
throw new InvalidOperationException($"Task entity dispatcher binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeAgentAsync(
|
|
durableTaskClient,
|
|
encodedEntityRequest,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint)
|
|
{
|
|
if (mcpToolInvocationContext is null)
|
|
{
|
|
throw new InvalidOperationException($"MCP tool invocation context binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value =
|
|
await BuiltInFunctions.RunMcpToolAsync(mcpToolInvocationContext, durableTaskClient, context);
|
|
return;
|
|
}
|
|
|
|
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint)
|
|
{
|
|
if (mcpToolInvocationContext is null)
|
|
{
|
|
throw new InvalidOperationException($"MCP tool invocation context binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowMcpToolAsync(
|
|
mcpToolInvocationContext,
|
|
durableTaskClient,
|
|
context);
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException($"Unsupported function entry point '{context.FunctionDefinition.EntryPoint}' for invocation {context.InvocationId}.");
|
|
}
|
|
|
|
private static async ValueTask ExecuteOrchestrationAsync(FunctionContext context)
|
|
{
|
|
BindingMetadata? orchestrationBinding = null;
|
|
foreach (BindingMetadata binding in context.FunctionDefinition.InputBindings.Values)
|
|
{
|
|
if (string.Equals(binding.Type, "orchestrationTrigger", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
orchestrationBinding = binding;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (orchestrationBinding is null)
|
|
{
|
|
throw new InvalidOperationException($"Orchestration trigger binding is missing for the invocation {context.InvocationId}.");
|
|
}
|
|
|
|
InputBindingData<object> triggerInputData = await context.BindInputAsync<object>(orchestrationBinding);
|
|
if (triggerInputData?.Value is not string encodedOrchestratorState)
|
|
{
|
|
throw new InvalidOperationException($"Orchestration history state was either missing from the input or not a string value for invocation {context.InvocationId}.");
|
|
}
|
|
|
|
context.GetInvocationResult().Value = BuiltInFunctions.RunWorkflowOrchestration(
|
|
encodedOrchestratorState,
|
|
context);
|
|
}
|
|
}
|