.NET Workflows - Support agent level function invocation for declarative workflow (#1442)

* Checkpoint

* Checkpoint

* Checkpoint

* Good

* Namespace

* Namespace

* Dun

* Async Test

* AgentId

* Portable pattern

* Portable2

* Portable3

* Respond to comments

* Namespace

* Function call selection

* ToHashSet

* ToHashSet

* Updated

* Parameter name

* Final

* Tests
This commit is contained in:
Chris
2025-10-14 20:42:15 +00:00
committed by GitHub
parent a4039134de
commit a6b6937b94
25 changed files with 902 additions and 80 deletions
@@ -1,10 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Bot.ObjectModel.Abstractions;
@@ -16,23 +19,67 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
DeclarativeActionExecutor<InvokeAzureAgent>(model, state)
{
public static class Steps
{
public static string Input(string id) => $"{id}_{nameof(Input)}";
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
}
// Input is requested by a message other than ActionExecutorResult.
public static bool RequiresInput(object? message) => message is not ActionExecutorResult;
private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}");
private AzureAgentInput? AgentInput => this.Model.Input;
private AzureAgentOutput? AgentOutput => this.Model.Output;
protected override bool EmitResultEvent => false;
protected override bool IsDiscreteAction => false;
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
await this.InvokeAgentAsync(context, this.GetInputMessages(), cancellationToken).ConfigureAwait(false);
return default;
}
public ValueTask ResumeAsync(IWorkflowContext context, AgentToolResponse message, CancellationToken cancellationToken) =>
this.InvokeAgentAsync(context, [message.FunctionResults.ToChatMessage()], cancellationToken);
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
{
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
private async ValueTask InvokeAgentAsync(IWorkflowContext context, IEnumerable<ChatMessage>? messages, CancellationToken cancellationToken)
{
string? conversationId = this.GetConversationId();
string agentName = this.GetAgentName();
string? additionalInstructions = this.GetAdditionalInstructions();
bool autoSend = this.GetAutoSendValue();
IEnumerable<ChatMessage>? inputMessages = this.GetInputMessages();
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, inputMessages, cancellationToken).ConfigureAwait(false);
bool isComplete = true;
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, messages, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(agentResponse.Text))
{
// Identify function calls that have no associated result.
List<FunctionCallContent> functionCalls = this.GetOrphanedFunctionCalls(agentResponse);
isComplete = functionCalls.Count == 0;
if (!isComplete)
{
AgentToolRequest toolRequest = new(agentName, functionCalls);
await context.SendMessageAsync(toolRequest, targetId: null, cancellationToken).ConfigureAwait(false);
}
}
if (isComplete)
{
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
}
await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false);
return default;
}
private IEnumerable<ChatMessage>? GetInputMessages()
@@ -48,6 +95,28 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
return userInput?.ToChatMessages();
}
private List<FunctionCallContent> GetOrphanedFunctionCalls(AgentRunResponse agentResponse)
{
HashSet<string> functionResultIds =
[.. agentResponse.Messages
.SelectMany(
m =>
m.Contents
.OfType<FunctionResultContent>()
.Select(functionCall => functionCall.CallId))];
List<FunctionCallContent> functionCalls = [];
foreach (FunctionCallContent functionCall in agentResponse.Messages.SelectMany(m => m.Contents.OfType<FunctionCallContent>()))
{
if (!functionResultIds.Contains(functionCall.CallId))
{
functionCalls.Add(functionCall);
}
}
return functionCalls;
}
private string? GetConversationId()
{
if (this.Model.ConversationId is null)