mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Multiple fixes in Microsoft.Agents.Orchestration to make handoffs work end-to-end (#568)
* Multiple fixes in Microsoft.Agents.Orchestration: - Enable multi-turn (via RunAsync) interactions for all orchestrating agents - Fix tool calling in HandoffOrchestration (multiple issues) - Fixes for chat history serialization involving tool calls * GHCP PR feedback * Fix issue with returning old responses --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
@@ -61,10 +61,13 @@ public partial class ConcurrentOrchestration : OrchestratingAgent
|
||||
this.ResumeAsync(messages, new AgentRunResponse?[this.Agents.Count], context, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.ConcurrentState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.Messages, state.Completed, context, cancellationToken);
|
||||
|
||||
// Append the new messages to the checkpoint state
|
||||
List<ChatMessage> allMessages = [.. state.Messages, .. newMessages];
|
||||
return this.ResumeAsync(allMessages, state.Completed, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -51,10 +51,14 @@ public sealed partial class GroupChatOrchestration : OrchestratingAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.GroupChatState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.AllMessages, state.OriginalMessageCount, context, cancellationToken);
|
||||
|
||||
// Append the new messages to the checkpoint state
|
||||
List<ChatMessage> allMessages = [.. state.AllMessages, .. newMessages];
|
||||
|
||||
return this.ResumeAsync(allMessages, allMessages.Count, context, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<AgentRunResponse> ResumeAsync(
|
||||
|
||||
@@ -52,26 +52,28 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.HandoffState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
|
||||
AIAgent? nextAgent = null;
|
||||
foreach (var agent in this.Agents)
|
||||
if (state.NextAgent is null)
|
||||
{
|
||||
if (agent.Id == state.NextAgent)
|
||||
nextAgent = this._handoffs.InitialAgent;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextAgent = this.Agents.FirstOrDefault(a => a.Id == state.NextAgent);
|
||||
if (nextAgent is null)
|
||||
{
|
||||
nextAgent = agent;
|
||||
break;
|
||||
Throw.InvalidOperationException($"The next agent '{state.NextAgent}' is not defined in the orchestration.");
|
||||
}
|
||||
}
|
||||
|
||||
if (nextAgent is null)
|
||||
{
|
||||
Throw.InvalidOperationException($"The next agent '{state.NextAgent}' is not defined in the orchestration.");
|
||||
}
|
||||
// Append the new messages to the checkpoint state
|
||||
List<ChatMessage> allMessages = [.. state.AllMessages, .. newMessages];
|
||||
|
||||
return this.ResumeAsync(nextAgent, state.AllMessages, state.OriginalMessageCount, context, cancellationToken);
|
||||
return this.ResumeAsync(nextAgent, allMessages, allMessages.Count, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -144,7 +146,7 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
|
||||
private static void RemoveHandoffFunctionCalls(AgentRunResponse response, List<AITool> handoffTools)
|
||||
{
|
||||
HashSet<string>? removeToolNames = null;
|
||||
HashSet<string>? callIds = null;
|
||||
HashSet<string>? handoffCallIds = null;
|
||||
|
||||
foreach (var message in response.Messages)
|
||||
{
|
||||
@@ -153,23 +155,22 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
|
||||
if (message.Contents[i] is FunctionCallContent fcc)
|
||||
{
|
||||
removeToolNames ??= [.. handoffTools.Select(t => t.Name)];
|
||||
(callIds ??= new()).Add(fcc.CallId);
|
||||
|
||||
if (removeToolNames.Contains(fcc.Name))
|
||||
{
|
||||
(handoffCallIds ??= []).Add(fcc.CallId);
|
||||
message.Contents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callIds is not null)
|
||||
if (handoffCallIds is not null)
|
||||
{
|
||||
foreach (var message in response.Messages)
|
||||
{
|
||||
for (int i = message.Contents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (message.Contents[i] is FunctionResultContent frc && callIds.Contains(frc.CallId))
|
||||
if (message.Contents[i] is FunctionResultContent frc && handoffCallIds.Contains(frc.CallId))
|
||||
{
|
||||
message.Contents.RemoveAt(i);
|
||||
}
|
||||
@@ -215,7 +216,7 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
|
||||
|
||||
static void Terminate()
|
||||
{
|
||||
if (FunctionInvokingChatClient.CurrentContext is not { } ctx)
|
||||
if (NewFunctionInvokingChatClient.CurrentContext is not { } ctx)
|
||||
{
|
||||
throw new NotSupportedException($"The agent is not configured with a {nameof(FunctionInvokingChatClient)}. Cease execution.");
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public abstract partial class OrchestratingAgent : AIAgent
|
||||
JsonElement? checkpoint = await this.ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
Task<AgentRunResponse> completion = checkpoint is null ?
|
||||
this.RunCoreAsync(messages, context, cancellationToken) :
|
||||
this.ResumeCoreAsync(checkpoint.Value, context, cancellationToken);
|
||||
this.ResumeCoreAsync(checkpoint.Value, messages, context, cancellationToken);
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
@@ -157,9 +157,10 @@ public abstract partial class OrchestratingAgent : AIAgent
|
||||
/// Resumes processing of the orchestration.
|
||||
/// </summary>
|
||||
/// <param name="checkpointState">The last checkpoint state available from which to resume the operation.</param>
|
||||
/// <param name="newMessages">The new messages to be processed in addition to the checkpoint state.</param>
|
||||
/// <param name="context">The context for this operation.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
protected abstract Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken);
|
||||
protected abstract Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with input messages and respond with both streamed and regular messages.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
@@ -8,4 +9,5 @@ namespace Microsoft.Agents.Orchestration;
|
||||
[JsonSerializable(typeof(ConcurrentOrchestration.ConcurrentState))]
|
||||
[JsonSerializable(typeof(GroupChatOrchestration.GroupChatState))]
|
||||
[JsonSerializable(typeof(HandoffOrchestration.HandoffState))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class OrchestrationJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -32,10 +32,13 @@ public sealed partial class SequentialOrchestration : OrchestratingAgent
|
||||
this.ResumeAsync(0, messages, context, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.SequentialState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.Index, state.Messages, context, cancellationToken);
|
||||
|
||||
// Append the new messages to the checkpoint state
|
||||
List<ChatMessage> allMessages = [.. state.Messages, .. newMessages];
|
||||
return this.ResumeAsync(state.Index, allMessages, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -83,7 +83,7 @@ public class OrchestrationResultTests
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IReadOnlyCollection<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user