// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Specialized;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
///
/// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow.
///
public sealed class HandoffsWorkflowBuilder
{
internal const string FunctionPrefix = "handoff_to_";
private readonly AIAgent _initialAgent;
private readonly Dictionary> _targets = [];
private readonly HashSet _allAgents = new(AIAgentIDEqualityComparer.Instance);
private HandoffToolCallFilteringBehavior _toolCallFilteringBehavior = HandoffToolCallFilteringBehavior.HandoffOnly;
///
/// Initializes a new instance of the class with no handoff relationships.
///
/// The first agent to be invoked (prior to any handoff).
internal HandoffsWorkflowBuilder(AIAgent initialAgent)
{
this._initialAgent = initialAgent;
this._allAgents.Add(initialAgent);
}
///
/// Gets or sets additional instructions to provide to an agent that has handoffs about how and when to perform them.
///
///
/// By default, simple instructions are included. This may be set to to avoid including
/// any additional instructions, or may be customized to provide more specific guidance.
///
public string? HandoffInstructions { get; private set; } = DefaultHandoffInstructions;
private const string DefaultHandoffInstructions =
$"""
You are one agent in a multi-agent system. You can hand off the conversation to another agent if appropriate. Handoffs are achieved
by calling a handoff function, named in the form `{FunctionPrefix}`; the description of the function provides details on the
target agent of that handoff. Handoffs between agents are handled seamlessly in the background; never mention or narrate these handoffs
in your conversation with the user.
""";
///
/// Sets additional instructions to provide to an agent that has handoffs about how and when to
/// perform them.
///
/// The instructions to provide, or to restore the default instructions.
public HandoffsWorkflowBuilder WithHandoffInstructions(string? instructions)
{
this.HandoffInstructions = instructions ?? DefaultHandoffInstructions;
return this;
}
///
/// Sets the behavior for filtering and contents from
/// s flowing through the handoff workflow. Defaults to .
///
/// The filtering behavior to apply.
public HandoffsWorkflowBuilder WithToolCallFilteringBehavior(HandoffToolCallFilteringBehavior behavior)
{
this._toolCallFilteringBehavior = behavior;
return this;
}
///
/// Adds handoff relationships from a source agent to one or more target agents.
///
/// The source agent.
/// The target agents to add as handoff targets for the source agent.
/// The updated instance.
/// The handoff reason for each target in is derived from that agent's description or name.
public HandoffsWorkflowBuilder WithHandoffs(AIAgent from, IEnumerable to)
{
Throw.IfNull(from);
Throw.IfNull(to);
foreach (var target in to)
{
if (target is null)
{
Throw.ArgumentNullException(nameof(to), "One or more target agents are null.");
}
this.WithHandoff(from, target);
}
return this;
}
///
/// Adds handoff relationships from one or more sources agent to a target agent.
///
/// The source agents.
/// The target agent to add as a handoff target for each source agent.
///
/// The reason the should hand off to the .
/// If , the reason is derived from 's description or name.
///
/// The updated instance.
public HandoffsWorkflowBuilder WithHandoffs(IEnumerable from, AIAgent to, string? handoffReason = null)
{
Throw.IfNull(from);
Throw.IfNull(to);
foreach (var source in from)
{
if (source is null)
{
Throw.ArgumentNullException(nameof(from), "One or more source agents are null.");
}
this.WithHandoff(source, to, handoffReason);
}
return this;
}
///
/// Adds a handoff relationship from a source agent to a target agent with a custom handoff reason.
///
/// The source agent.
/// The target agent.
///
/// The reason the should hand off to the .
/// If , the reason is derived from 's description or name.
///
/// The updated instance.
public HandoffsWorkflowBuilder WithHandoff(AIAgent from, AIAgent to, string? handoffReason = null)
{
Throw.IfNull(from);
Throw.IfNull(to);
this._allAgents.Add(from);
this._allAgents.Add(to);
if (!this._targets.TryGetValue(from, out var handoffs))
{
this._targets[from] = handoffs = [];
}
if (string.IsNullOrWhiteSpace(handoffReason))
{
handoffReason = to.Description ?? to.Name ?? (to as ChatClientAgent)?.Instructions;
if (string.IsNullOrWhiteSpace(handoffReason))
{
Throw.ArgumentException(
nameof(to),
$"The provided target agent '{to.Name ?? to.Id}' has no description, name, or instructions, and no handoff description has been provided. " +
"At least one of these is required to register a handoff so that the appropriate target agent can be chosen.");
}
}
if (!handoffs.Add(new(to, handoffReason)))
{
Throw.InvalidOperationException($"A handoff from agent '{from.Name ?? from.Id}' to agent '{to.Name ?? to.Id}' has already been registered.");
}
return this;
}
///
/// Builds a composed of agents that operate via handoffs, with the next
/// agent to process messages selected by the current agent.
///
/// The workflow built based on the handoffs in the builder.
public Workflow Build()
{
HandoffsStartExecutor start = new();
HandoffsEndExecutor end = new();
WorkflowBuilder builder = new(start);
HandoffAgentExecutorOptions options = new(this.HandoffInstructions, this._toolCallFilteringBehavior);
// Create an AgentExecutor for each again.
Dictionary executors = this._allAgents.ToDictionary(a => a.Id, a => new HandoffAgentExecutor(a, options));
// Connect the start executor to the initial agent.
builder.AddEdge(start, executors[this._initialAgent.Id]);
// Initialize each executor with its handoff targets to the other executors.
foreach (var agent in this._allAgents)
{
executors[agent.Id].Initialize(builder, end, executors,
this._targets.TryGetValue(agent, out HashSet? targets) ? targets : []);
}
// Build the workflow.
return builder.WithOutputFrom(end).Build();
}
}