Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ namespace Microsoft.Agents.AI.Workflows;
/// </summary>
public sealed class HandoffsWorkflowBuilder
{
internal const string FunctionPrefix = "handoff_to_";
/// <summary>
/// The prefix for function calls that trigger handoffs to other agents; the full name is then `{FunctionPrefix}&lt;agent_id&gt;`,
/// where `&lt;agent_id&gt;` is the ID of the target agent to hand off to.
/// </summary>
public const string FunctionPrefix = "handoff_to_";

private readonly AIAgent _initialAgent;
private readonly Dictionary<AIAgent, HashSet<HandoffTarget>> _targets = [];
private readonly HashSet<AIAgent> _allAgents = new(AIAgentIDEqualityComparer.Instance);

private bool _emitAgentResponseEvents;
private bool _emitAgentResponseUpdateEvents;
private HandoffToolCallFilteringBehavior _toolCallFilteringBehavior = HandoffToolCallFilteringBehavior.HandoffOnly;

/// <summary>
Expand Down Expand Up @@ -47,16 +55,43 @@ in your conversation with the user.
""";

/// <summary>
/// Sets additional instructions to provide to an agent that has handoffs about how and when to
/// perform them.
/// Sets instructions to provide to each agent that has handoffs about how and when perform them.
/// </summary>
/// <remarks>
/// In the vast majority of cases, the <see cref="DefaultHandoffInstructions"/> will be sufficient, and there will be no need to customize.
/// If you do provide alternate instructions, remember to explain the mechanics of the handoff function tool call, using see
/// <see cref="FunctionPrefix"/> constant.
/// </remarks>
/// <param name="instructions">The instructions to provide, or <see langword="null"/> to restore the default instructions.</param>
public HandoffsWorkflowBuilder WithHandoffInstructions(string? instructions)
{
this.HandoffInstructions = instructions ?? DefaultHandoffInstructions;
return this;
}

/// <summary>
/// Sets a value indicating whether agent streaming update events should be emitted during execution.
/// If <see langword="null"/>, the value will be taken from the <see cref="TurnToken"/>
/// </summary>
/// <param name="emitAgentResponseUpdateEvents"></param>
/// <returns></returns>
public HandoffsWorkflowBuilder EmitAgentResponseUpdateEvents(bool emitAgentResponseUpdateEvents)
{
this._emitAgentResponseUpdateEvents = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not the emitAgentResponseUpdateEvents parameter to be assigned to the _emitAgentResponseUpdateEvents field? Same for the EmitAgentResponseEvents property below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good catch!

return this;
}

/// <summary>
/// Sets a value indicating whether aggregated agent response events should be emitted during execution.
/// </summary>
/// <param name="emitAgentResponseEvents"></param>
/// <returns></returns>
public HandoffsWorkflowBuilder EmitAgentResponseEvents(bool emitAgentResponseEvents)
{
this._emitAgentResponseEvents = true;
return this;
}

/// <summary>
/// Sets the behavior for filtering <see cref="FunctionCallContent"/> and <see cref="ChatRole.Tool"/> contents from
/// <see cref="ChatMessage"/>s flowing through the handoff workflow. Defaults to <see cref="HandoffToolCallFilteringBehavior.HandoffOnly"/>.
Expand Down Expand Up @@ -175,7 +210,10 @@ public Workflow Build()
HandoffsEndExecutor end = new();
WorkflowBuilder builder = new(start);

HandoffAgentExecutorOptions options = new(this.HandoffInstructions, this._toolCallFilteringBehavior);
HandoffAgentExecutorOptions options = new(this.HandoffInstructions,
this._emitAgentResponseEvents,
this._emitAgentResponseUpdateEvents,
this._toolCallFilteringBehavior);

// Create an AgentExecutor for each again.
Dictionary<string, HandoffAgentExecutor> executors = this._allAgents.ToDictionary(a => a.Id, a => new HandoffAgentExecutor(a, options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ namespace Microsoft.Agents.AI.Workflows.Specialized;

internal record AIAgentHostState(JsonElement? ThreadState, bool? CurrentTurnEmitEvents);

internal static class TurnExtensions
{
public static bool ShouldEmitStreamingEvents(this TurnToken token, bool? agentSetting)
=> token.EmitEvents ?? agentSetting ?? false;

public static bool ShouldEmitStreamingEvents(bool? turnTokenSetting, bool? agentSetting)
=> turnTokenSetting ?? agentSetting ?? false;
}

internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
{
private readonly AIAgent _agent;
Expand Down Expand Up @@ -88,9 +97,6 @@ private ValueTask HandleFunctionResultAsync(
return this.ContinueTurnAsync(implicitTurnMessages, context, this._currentTurnEmitEvents ?? false, cancellationToken);
}

public bool ShouldEmitStreamingEvents(bool? emitEvents)
=> emitEvents ?? this._options.EmitAgentUpdateEvents ?? false;

private async ValueTask<AgentSession> EnsureSessionAsync(IWorkflowContext context, CancellationToken cancellationToken) =>
this._session ??= await this._agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -159,7 +165,10 @@ await context.SendMessageAsync(response.Messages is List<ChatMessage> list ? lis
}

protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
=> this.ContinueTurnAsync(messages, context, this.ShouldEmitStreamingEvents(emitEvents), cancellationToken);
=> this.ContinueTurnAsync(messages,
context,
TurnExtensions.ShouldEmitStreamingEvents(turnTokenSetting: emitEvents, this._options.EmitAgentUpdateEvents),
cancellationToken);

private async ValueTask<AgentResponse> InvokeAgentAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, bool emitEvents, CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ namespace Microsoft.Agents.AI.Workflows.Specialized;

internal sealed class HandoffAgentExecutorOptions
{
public HandoffAgentExecutorOptions(string? handoffInstructions, HandoffToolCallFilteringBehavior toolCallFilteringBehavior)
public HandoffAgentExecutorOptions(string? handoffInstructions, bool emitAgentResponseEvents, bool? emitAgentResponseUpdateEvents, HandoffToolCallFilteringBehavior toolCallFilteringBehavior)
{
this.HandoffInstructions = handoffInstructions;
this.EmitAgentResponseEvents = emitAgentResponseEvents;
this.EmitAgentResponseUpdateEvents = emitAgentResponseUpdateEvents;
this.ToolCallFilteringBehavior = toolCallFilteringBehavior;
}

public string? HandoffInstructions { get; set; }

public bool EmitAgentResponseEvents { get; set; }

public bool? EmitAgentResponseUpdateEvents { get; set; }

public HandoffToolCallFilteringBehavior ToolCallFilteringBehavior { get; set; } = HandoffToolCallFilteringBehavior.HandoffOnly;
}

Expand Down Expand Up @@ -250,7 +256,14 @@ await AddUpdateAsync(
}
}

allMessages.AddRange(updates.ToAgentResponse().Messages);
AgentResponse agentResponse = updates.ToAgentResponse();

if (options.EmitAgentResponseEvents)
{
await context.YieldOutputAsync(agentResponse, cancellationToken).ConfigureAwait(false);
}

allMessages.AddRange(agentResponse.Messages);

roleChanges.ResetUserToAssistantForChangedRoles();

Expand All @@ -259,7 +272,7 @@ await AddUpdateAsync(
async Task AddUpdateAsync(AgentResponseUpdate update, CancellationToken cancellationToken)
{
updates.Add(update);
if (message.TurnToken.EmitEvents is true)
if (message.TurnToken.ShouldEmitStreamingEvents(options.EmitAgentResponseUpdateEvents))
{
await context.YieldOutputAsync(update, cancellationToken).ConfigureAwait(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal void AddMessages(AgentSession session, params IEnumerable<ChatMessage>
=> this._sessionState.GetOrInitializeState(session).Messages.AddRange(messages);

protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(this._sessionState.GetOrInitializeState(context.Session).Messages);
=> new(this._sessionState.GetOrInitializeState(context.Session).Messages.AsReadOnly());

protected override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
Expand All @@ -62,6 +62,12 @@ public IEnumerable<ChatMessage> GetFromBookmark(AgentSession session)
}
}

public IEnumerable<ChatMessage> GetAllMessages(AgentSession session)
{
var state = this._sessionState.GetOrInitializeState(session);
return state.Messages.AsReadOnly();
}

public void UpdateBookmark(AgentSession session)
{
var state = this._sessionState.GetOrInitializeState(session);
Expand Down
17 changes: 14 additions & 3 deletions dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,17 @@ Task<AgentResponse> RunCoreAsync(
MessageMerger merger = new();

await foreach (AgentResponseUpdate update in workflowSession.InvokeStageAsync(cancellationToken)
.ConfigureAwait(false)
.WithCancellation(cancellationToken))
.ConfigureAwait(false)
.WithCancellation(cancellationToken))
{
merger.AddUpdate(update);
}

return merger.ComputeMerged(workflowSession.LastResponseId!, this.Id, this.Name);
AgentResponse response = merger.ComputeMerged(workflowSession.LastResponseId!, this.Id, this.Name);
workflowSession.ChatHistoryProvider.AddMessages(workflowSession, response.Messages);
workflowSession.ChatHistoryProvider.UpdateBookmark(workflowSession);

return response;
}

protected override async
Expand All @@ -138,11 +142,18 @@ IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
await this.ValidateWorkflowAsync().ConfigureAwait(false);

WorkflowSession workflowSession = await this.UpdateSessionAsync(messages, session, cancellationToken).ConfigureAwait(false);
MessageMerger merger = new();

await foreach (AgentResponseUpdate update in workflowSession.InvokeStageAsync(cancellationToken)
.ConfigureAwait(false)
.WithCancellation(cancellationToken))
{
merger.AddUpdate(update);
yield return update;
}

AgentResponse response = merger.ComputeMerged(workflowSession.LastResponseId!, this.Id, this.Name);
workflowSession.ChatHistoryProvider.AddMessages(workflowSession, response.Messages);
workflowSession.ChatHistoryProvider.UpdateBookmark(workflowSession);
}
}
Loading
Loading