mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Run Response ADR & Updates (#104)
* Add ADR for different run response options * Add another option to the list. * Update agno non-streaming with further clarification * Add another option * Adding optional includeUpdates option * Adding Pros/Cons for each option * Make pros/cons a list * Add some thoughts on structured outputs and custom AIContent types * Update design doc to clarify primary and secondary better and split out custom response types with it's own options * Add structured outputs competitive comparison and suggestion * Address PR comments. * Remove AgentRunFinishReason until we can find a good use case for it. * Add finish reason to list of excluded properties. * Add custom agent run response types. Usage to follow. * Update Agent run response types * Add additional code coverage * Remove onIntermediateMessage since it is unecessary with the new response approach. * Add AgentId to response. * Rename ParseAsStructuredOutput to Deserialize * Update decision doc. * Fix formatting. * Update CopilotStudio to return new response types * Address PR comment Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
@@ -64,7 +64,7 @@ public abstract class AgentActor : OrchestrationActor
|
||||
/// <remarks>
|
||||
/// Override this method to customize the invocation of the agent.
|
||||
/// </remarks>
|
||||
protected virtual Task InvokeAsync(
|
||||
protected virtual Task<AgentRunResponse> InvokeAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentRunOptions options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
@@ -84,7 +84,7 @@ public abstract class AgentActor : OrchestrationActor
|
||||
/// <remarks>
|
||||
/// Override this method to customize the invocation of the agent.
|
||||
/// </remarks>
|
||||
protected virtual IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
|
||||
protected virtual IAsyncEnumerable<AgentRunResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
|
||||
this.Agent.RunStreamingAsync(
|
||||
messages,
|
||||
this.Thread,
|
||||
@@ -111,54 +111,46 @@ public abstract class AgentActor : OrchestrationActor
|
||||
{
|
||||
this.Context.Cancellation.ThrowIfCancellationRequested();
|
||||
|
||||
List<ChatMessage>? responseMessages = [];
|
||||
ChatResponse response = new(responseMessages);
|
||||
|
||||
AgentRunOptions options =
|
||||
new()
|
||||
{
|
||||
OnIntermediateMessages = HandleMessage,
|
||||
};
|
||||
AgentRunOptions options = new();
|
||||
|
||||
if (this.Context.StreamingResponseCallback == null)
|
||||
{
|
||||
// No need to utilize streaming if no callback is provided
|
||||
await this.InvokeAsync([.. input], options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
IAsyncEnumerable<ChatResponseUpdate> streamedResponses = this.InvokeStreamingAsync([.. input], options, cancellationToken);
|
||||
ChatResponseUpdate? lastStreamedResponse = null;
|
||||
await foreach (ChatResponseUpdate streamedResponse in streamedResponses.ConfigureAwait(false))
|
||||
{
|
||||
this.Context.Cancellation.ThrowIfCancellationRequested();
|
||||
|
||||
await HandleStreamedMessage(lastStreamedResponse, isFinal: false).ConfigureAwait(false);
|
||||
|
||||
lastStreamedResponse = streamedResponse;
|
||||
}
|
||||
|
||||
await HandleStreamedMessage(lastStreamedResponse, isFinal: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response.Messages.Last();
|
||||
|
||||
async Task HandleMessage(IReadOnlyCollection<ChatMessage> messages)
|
||||
{
|
||||
responseMessages?.AddRange(messages);
|
||||
AgentRunResponse response = await this.InvokeAsync([.. input], options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this.Context.ResponseCallback is not null)
|
||||
{
|
||||
await this.Context.ResponseCallback.Invoke(messages).ConfigureAwait(false);
|
||||
await this.Context.ResponseCallback.Invoke(response.Messages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response.Messages.Last();
|
||||
}
|
||||
|
||||
async ValueTask HandleStreamedMessage(ChatResponseUpdate? streamedResponse, bool isFinal)
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> streamedResponses = this.InvokeStreamingAsync([.. input], options, cancellationToken);
|
||||
AgentRunResponseUpdate? lastStreamedResponse = null;
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
await foreach (AgentRunResponseUpdate streamedResponse in streamedResponses.ConfigureAwait(false))
|
||||
{
|
||||
this.Context.Cancellation.ThrowIfCancellationRequested();
|
||||
|
||||
await HandleStreamedMessage(lastStreamedResponse, isFinal: false).ConfigureAwait(false);
|
||||
|
||||
lastStreamedResponse = streamedResponse;
|
||||
}
|
||||
|
||||
return updates.ToAgentRunResponse().Messages.Last();
|
||||
|
||||
async ValueTask HandleStreamedMessage(AgentRunResponseUpdate? streamedResponse, bool isFinal)
|
||||
{
|
||||
if (this.Context.StreamingResponseCallback != null && streamedResponse != null)
|
||||
{
|
||||
await this.Context.StreamingResponseCallback.Invoke(streamedResponse, isFinal).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (streamedResponse != null)
|
||||
{
|
||||
updates.Add(streamedResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public delegate ValueTask OrchestrationResponseCallback(IEnumerable<ChatMessage>
|
||||
/// </summary>
|
||||
/// <param name="response">The agent response</param>
|
||||
/// <param name="isFinal">Indicates if streamed content is final chunk of the message.</param>
|
||||
public delegate ValueTask OrchestrationStreamingCallback(ChatResponseUpdate response, bool isFinal);
|
||||
public delegate ValueTask OrchestrationStreamingCallback(AgentRunResponseUpdate response, bool isFinal);
|
||||
|
||||
/// <summary>
|
||||
/// Called when human interaction is requested.
|
||||
|
||||
@@ -60,7 +60,7 @@ internal sealed class HandoffActor : AgentActor
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task InvokeAsync(
|
||||
protected override Task<AgentRunResponse> InvokeAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentRunOptions options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
@@ -72,7 +72,7 @@ internal sealed class HandoffActor : AgentActor
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
|
||||
this._chatAgent.RunStreamingAsync(
|
||||
messages,
|
||||
this.Thread,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#if NET
|
||||
using System;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#if NET
|
||||
using System.Runtime.CompilerServices;
|
||||
#else
|
||||
using System.Text;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
// TODO: Consolidate with same internal class in Microsoft.Extensions.AI.Abstractions when both are available in the same repository.
|
||||
/// <summary>Internal extensions for working with <see cref="AIContent"/>.</summary>
|
||||
internal static class AIContentExtensions
|
||||
{
|
||||
/// <summary>Concatenates the text of all <see cref="TextContent"/> instances in the list.</summary>
|
||||
public static string ConcatText(this IEnumerable<AIContent> contents)
|
||||
{
|
||||
if (contents is IList<AIContent> list)
|
||||
{
|
||||
int count = list.Count;
|
||||
switch (count)
|
||||
{
|
||||
case 0:
|
||||
return string.Empty;
|
||||
|
||||
case 1:
|
||||
return (list[0] as TextContent)?.Text ?? string.Empty;
|
||||
|
||||
default:
|
||||
#if NET
|
||||
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (list[i] is TextContent text)
|
||||
{
|
||||
builder.AppendLiteral(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToStringAndClear();
|
||||
#else
|
||||
StringBuilder builder = new();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (list[i] is TextContent text)
|
||||
{
|
||||
builder.Append(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return string.Concat(contents.OfType<TextContent>());
|
||||
}
|
||||
|
||||
/// <summary>Concatenates the <see cref="ChatMessage.Text"/> of all <see cref="ChatMessage"/> instances in the list.</summary>
|
||||
/// <remarks>A newline separator is added between each non-empty piece of text.</remarks>
|
||||
public static string ConcatText(this IList<ChatMessage> messages)
|
||||
{
|
||||
int count = messages.Count;
|
||||
switch (count)
|
||||
{
|
||||
case 0:
|
||||
return string.Empty;
|
||||
|
||||
case 1:
|
||||
return messages[0].Text;
|
||||
|
||||
default:
|
||||
#if NET
|
||||
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
|
||||
bool needsSeparator = false;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string text = messages[i].Text;
|
||||
if (text.Length > 0)
|
||||
{
|
||||
if (needsSeparator)
|
||||
{
|
||||
builder.AppendLiteral(Environment.NewLine);
|
||||
}
|
||||
|
||||
builder.AppendLiteral(text);
|
||||
|
||||
needsSeparator = true;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToStringAndClear();
|
||||
#else
|
||||
StringBuilder builder = new();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string text = messages[i].Text;
|
||||
if (text.Length > 0)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.Append(text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public Task<ChatResponse> RunAsync(
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -69,11 +69,11 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
/// <remarks>
|
||||
/// The provided message string will be treated as a user message.
|
||||
/// </remarks>
|
||||
public Task<ChatResponse> RunAsync(
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
string message,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -91,8 +91,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public Task<ChatResponse> RunAsync(
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
ChatMessage message,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -110,8 +110,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public abstract Task<ChatResponse> RunAsync(
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -123,8 +123,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
public IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
/// <returns>An async list of response items that each contain a <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -139,11 +139,11 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
/// <returns>An async list of response items that each contain a <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The provided message string will be treated as a user message.
|
||||
/// </remarks>
|
||||
public IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
string message,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -161,8 +161,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
public IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
/// <returns>An async list of response items that each contain a <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
ChatMessage message,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -180,8 +180,8 @@ public abstract class Agent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
public abstract IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
/// <returns>An async list of response items that each contain a <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
@@ -26,17 +23,5 @@ public class AgentRunOptions
|
||||
public AgentRunOptions(AgentRunOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
this.OnIntermediateMessages = options.OnIntermediateMessages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a function to be called when a complete new message is generated by the agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This callback is particularly useful in cases where the caller wants to receive complete messages
|
||||
/// when invoking the agent with streaming.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Func<IReadOnlyCollection<ChatMessage>, Task>? OnIntermediateMessages { get; set; } = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>Represents the response to an Agent run request.</summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AgentRunResponse"/> provides one or more response messages and metadata about the response.
|
||||
/// A typical response will contain a single message, however a response may contain multiple messages
|
||||
/// in a variety of scenarios. For example, if the agent internally invokes functions or tools, performs
|
||||
/// RAG retrievals or has other complex logic, a single run by the agent may produce many messages showing
|
||||
/// the intermediate progress that the agent made towards producing the agent result.
|
||||
/// </remarks>
|
||||
public class AgentRunResponse
|
||||
{
|
||||
private static readonly JsonReaderOptions s_allowMultipleValuesJsonReaderOptions = new()
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
AllowMultipleValues = true
|
||||
#endif
|
||||
};
|
||||
|
||||
/// <summary>The response messages.</summary>
|
||||
private IList<ChatMessage>? _messages;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponse"/> class.</summary>
|
||||
public AgentRunResponse()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponse"/> class.</summary>
|
||||
/// <param name="message">The response message.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
public AgentRunResponse(ChatMessage message)
|
||||
{
|
||||
_ = Throw.IfNull(message);
|
||||
|
||||
this.Messages.Add(message);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponse"/> class.</summary>
|
||||
/// <param name="messages">The response messages.</param>
|
||||
public AgentRunResponse(IList<ChatMessage>? messages)
|
||||
{
|
||||
this._messages = messages;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the agent response messages.</summary>
|
||||
[AllowNull]
|
||||
public IList<ChatMessage> Messages
|
||||
{
|
||||
get => this._messages ??= new List<ChatMessage>(1);
|
||||
set => this._messages = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets the text of the response.</summary>
|
||||
/// <remarks>
|
||||
/// This property concatenates the <see cref="ChatMessage.Text"/> of all <see cref="ChatMessage"/>
|
||||
/// instances in <see cref="Messages"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string Text => this._messages?.ConcatText() ?? string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the ID of the agent that produced the response.</summary>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the agent response.</summary>
|
||||
public string? ResponseId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a timestamp for the run response.</summary>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets usage details for the run response.</summary>
|
||||
/// <remarks>
|
||||
/// Where the agent run response is produced via many model invocations, this
|
||||
/// usage is an aggregation of the usage for all these model invocations.
|
||||
/// </remarks>
|
||||
public UsageDetails? Usage { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the raw representation of the run response from an underlying implementation.</summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="AgentRunResponse"/> is created to represent some underlying object from another object
|
||||
/// model, this property can be used to store that original object. This can be useful for debugging or
|
||||
/// for enabling a consumer to access the underlying object model if needed.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public object? RawRepresentation { get; set; }
|
||||
|
||||
/// <summary>Gets or sets any additional properties associated with the run response.</summary>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => this.Text;
|
||||
|
||||
/// <summary>Creates an array of <see cref="AgentRunResponseUpdate" /> instances that represent this <see cref="AgentRunResponse" />.</summary>
|
||||
/// <returns>An array of <see cref="AgentRunResponseUpdate" /> instances that may be used to represent this <see cref="AgentRunResponse" />.</returns>
|
||||
public AgentRunResponseUpdate[] ToAgentRunResponseUpdates()
|
||||
{
|
||||
AgentRunResponseUpdate? extra = null;
|
||||
if (this.AdditionalProperties is not null || this.Usage is not null)
|
||||
{
|
||||
extra = new AgentRunResponseUpdate
|
||||
{
|
||||
AdditionalProperties = this.AdditionalProperties
|
||||
};
|
||||
|
||||
if (this.Usage is { } usage)
|
||||
{
|
||||
extra.Contents.Add(new UsageContent(usage));
|
||||
}
|
||||
}
|
||||
|
||||
int messageCount = this._messages?.Count ?? 0;
|
||||
var updates = new AgentRunResponseUpdate[messageCount + (extra is not null ? 1 : 0)];
|
||||
|
||||
int i;
|
||||
for (i = 0; i < messageCount; i++)
|
||||
{
|
||||
ChatMessage message = this._messages![i];
|
||||
updates[i] = new AgentRunResponseUpdate
|
||||
{
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
Contents = message.Contents,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
Role = message.Role,
|
||||
|
||||
AgentId = this.AgentId,
|
||||
ResponseId = this.ResponseId,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = this.CreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (extra is not null)
|
||||
{
|
||||
updates[i] = extra;
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
// TODO: Add overloads without serializer options.
|
||||
/// <summary>
|
||||
/// Deserializes the response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <returns>The result as the requested type.</returns>
|
||||
/// <exception cref="InvalidOperationException">The result is not parsable into the requested type.</exception>
|
||||
public T Deserialize<T>(JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
var structuredOutput = this.GetResultCore<T>(serializerOptions, out var failureReason);
|
||||
return failureReason switch
|
||||
{
|
||||
FailureReason.ResultDidNotContainJson => throw new InvalidOperationException("The response did not contain JSON to be deserialized."),
|
||||
FailureReason.DeserializationProducedNull => throw new InvalidOperationException("The deserialized response is null."),
|
||||
_ => structuredOutput!,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to deserialize response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="structuredOutput">The parsed structured output.</param>
|
||||
/// <returns><see langword="true" /> if parsing was successful; otherwise, <see langword="false" />.</returns>
|
||||
public bool TryDeserialize<T>(JsonSerializerOptions serializerOptions, [NotNullWhen(true)] out T? structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
structuredOutput = this.GetResultCore<T>(serializerOptions, out var failureReason);
|
||||
return failureReason is null;
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch
|
||||
{
|
||||
structuredOutput = default;
|
||||
return false;
|
||||
}
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
}
|
||||
|
||||
private static T? DeserializeFirstTopLevelObject<T>(string json, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
// We need to deserialize only the first top-level object as a workaround for a common LLM backend
|
||||
// issue. GPT 3.5 Turbo commonly returns multiple top-level objects after doing a function call.
|
||||
// See https://community.openai.com/t/2-json-objects-returned-when-using-function-calling-and-json-mode/574348
|
||||
var utf8ByteLength = Encoding.UTF8.GetByteCount(json);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(utf8ByteLength);
|
||||
try
|
||||
{
|
||||
var utf8SpanLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
|
||||
var utf8Span = new ReadOnlySpan<byte>(buffer, 0, utf8SpanLength);
|
||||
var reader = new Utf8JsonReader(utf8Span, s_allowMultipleValuesJsonReaderOptions);
|
||||
return JsonSerializer.Deserialize(ref reader, typeInfo);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private T? GetResultCore<T>(JsonSerializerOptions serializerOptions, out FailureReason? failureReason)
|
||||
{
|
||||
var json = this.Text;
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
failureReason = FailureReason.ResultDidNotContainJson;
|
||||
return default;
|
||||
}
|
||||
|
||||
T? deserialized = default;
|
||||
|
||||
// If there's an exception here, we want it to propagate, since the Result property is meant to throw directly
|
||||
|
||||
deserialized = DeserializeFirstTopLevelObject(json!, (JsonTypeInfo<T>)serializerOptions.GetTypeInfo(typeof(T)));
|
||||
|
||||
if (deserialized is null)
|
||||
{
|
||||
failureReason = FailureReason.DeserializationProducedNull;
|
||||
return default;
|
||||
}
|
||||
|
||||
failureReason = default;
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
private enum FailureReason
|
||||
{
|
||||
ResultDidNotContainJson,
|
||||
DeserializationProducedNull
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single streaming response chunk from an <see cref="Agent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="AgentRunResponseUpdate"/> is so named because it represents updates
|
||||
/// that layer on each other to form a single agent response. Conceptually, this combines the roles of
|
||||
/// <see cref="AgentRunResponse"/> and <see cref="ChatMessage"/> in streaming output.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The relationship between <see cref="AgentRunResponse"/> and <see cref="AgentRunResponseUpdate"/> is
|
||||
/// codified in the <see cref="AgentRunResponseUpdateExtensions.ToAgentRunResponseAsync"/> and
|
||||
/// <see cref="AgentRunResponse.ToAgentRunResponseUpdates"/>, which enable bidirectional conversions
|
||||
/// between the two. Note, however, that the provided conversions may be lossy, for example if multiple
|
||||
/// updates all have different <see cref="RawRepresentation"/> objects whereas there's only one slot for
|
||||
/// such an object available in <see cref="AgentRunResponse.RawRepresentation"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("[{Role}] {ContentForDebuggerDisplay}{EllipsesForDebuggerDisplay,nq}")]
|
||||
public class AgentRunResponseUpdate
|
||||
{
|
||||
/// <summary>The response update content items.</summary>
|
||||
private IList<AIContent>? _contents;
|
||||
|
||||
/// <summary>The name of the author of the update.</summary>
|
||||
private string? _authorName;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponseUpdate"/> class.</summary>
|
||||
[JsonConstructor]
|
||||
public AgentRunResponseUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponseUpdate"/> class.</summary>
|
||||
/// <param name="role">The role of the author of the update.</param>
|
||||
/// <param name="content">The text content of the update.</param>
|
||||
public AgentRunResponseUpdate(ChatRole? role, string? content)
|
||||
: this(role, content is null ? null : [new TextContent(content)])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentRunResponseUpdate"/> class.</summary>
|
||||
/// <param name="role">The role of the author of the update.</param>
|
||||
/// <param name="contents">The contents of the update.</param>
|
||||
public AgentRunResponseUpdate(ChatRole? role, IList<AIContent>? contents)
|
||||
{
|
||||
this.Role = role;
|
||||
this._contents = contents;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the name of the author of the response update.</summary>
|
||||
public string? AuthorName
|
||||
{
|
||||
get => this._authorName;
|
||||
set => this._authorName = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the role of the author of the response update.</summary>
|
||||
public ChatRole? Role { get; set; }
|
||||
|
||||
/// <summary>Gets the text of this update.</summary>
|
||||
/// <remarks>
|
||||
/// This property concatenates the text of all <see cref="TextContent"/> objects in <see cref="Contents"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string Text => this._contents is not null ? this._contents.ConcatText() : string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the agent run response update content items.</summary>
|
||||
[AllowNull]
|
||||
public IList<AIContent> Contents
|
||||
{
|
||||
get => this._contents ??= [];
|
||||
set => this._contents = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the raw representation of the response update from an underlying implementation.</summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="AgentRunResponseUpdate"/> is created to represent some underlying object from another object
|
||||
/// model, this property can be used to store that original object. This can be useful for debugging or
|
||||
/// for enabling a consumer to access the underlying object model if needed.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public object? RawRepresentation { get; set; }
|
||||
|
||||
/// <summary>Gets or sets additional properties for the update.</summary>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the agent that produced the response.</summary>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the response of which this update is a part.</summary>
|
||||
public string? ResponseId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the message of which this update is a part.</summary>
|
||||
/// <remarks>
|
||||
/// A single streaming response may be composed of multiple messages, each of which may be represented
|
||||
/// by multiple updates. This property is used to group those updates together into messages.
|
||||
///
|
||||
/// Some providers may consider streaming responses to be a single message, and in that case
|
||||
/// the value of this property may be the same as the response ID.
|
||||
///
|
||||
/// This value is used when <see cref="AgentRunResponseUpdateExtensions.ToAgentRunResponseAsync(IAsyncEnumerable{AgentRunResponseUpdate}, System.Threading.CancellationToken)"/>
|
||||
/// groups <see cref="AgentRunResponseUpdate"/> instances into <see cref="AgentRunResponse"/> instances.
|
||||
/// The value must be unique to each call to the underlying provider, and must be shared by
|
||||
/// all updates that are part of the same logical message within a streaming response.
|
||||
/// </remarks>
|
||||
public string? MessageId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a timestamp for the response update.</summary>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.Text;
|
||||
|
||||
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private AIContent? ContentForDebuggerDisplay => this._contents is { Count: > 0 } ? this._contents[0] : null;
|
||||
|
||||
/// <summary>Gets an indication for the debugger display of whether there's more content.</summary>
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string EllipsesForDebuggerDisplay => this._contents is { Count: > 1 } ? ", ..." : string.Empty;
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for working with <see cref="AgentRunResponseUpdate"/> instances.
|
||||
/// </summary>
|
||||
public static class AgentRunResponseUpdateExtensions
|
||||
{
|
||||
/// <summary>Combines <see cref="AgentRunResponseUpdate"/> instances into a single <see cref="AgentRunResponse"/>.</summary>
|
||||
/// <param name="updates">The updates to be combined.</param>
|
||||
/// <returns>The combined <see cref="AgentRunResponse"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentRunResponse"/>, the method will attempt to reconstruct
|
||||
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentRunResponseUpdate.MessageId"/> to determine
|
||||
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
|
||||
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
|
||||
/// </remarks>
|
||||
public static AgentRunResponse ToAgentRunResponse(
|
||||
this IEnumerable<AgentRunResponseUpdate> updates)
|
||||
{
|
||||
_ = Throw.IfNull(updates);
|
||||
|
||||
AgentRunResponse response = new();
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
ProcessUpdate(update, response);
|
||||
}
|
||||
|
||||
FinalizeResponse(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>Combines <see cref="AgentRunResponseUpdate"/> instances into a single <see cref="AgentRunResponse"/>.</summary>
|
||||
/// <param name="updates">The updates to be combined.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The combined <see cref="AgentRunResponse"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentRunResponse"/>, the method will attempt to reconstruct
|
||||
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentRunResponseUpdate.MessageId"/> to determine
|
||||
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
|
||||
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
|
||||
/// </remarks>
|
||||
public static Task<AgentRunResponse> ToAgentRunResponseAsync(
|
||||
this IAsyncEnumerable<AgentRunResponseUpdate> updates,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(updates);
|
||||
|
||||
return ToAgentRunResponseAsync(updates, cancellationToken);
|
||||
|
||||
static async Task<AgentRunResponse> ToAgentRunResponseAsync(
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> updates,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AgentRunResponse response = new();
|
||||
|
||||
await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
ProcessUpdate(update, response);
|
||||
}
|
||||
|
||||
FinalizeResponse(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
|
||||
internal static void CoalesceTextContent(List<AIContent> contents)
|
||||
{
|
||||
Coalesce<TextContent>(contents, static text => new(text));
|
||||
Coalesce<TextReasoningContent>(contents, static text => new(text));
|
||||
|
||||
// This implementation relies on TContent's ToString returning its exact text.
|
||||
static void Coalesce<TContent>(List<AIContent> contents, Func<string, TContent> fromText)
|
||||
where TContent : AIContent
|
||||
{
|
||||
StringBuilder? coalescedText = null;
|
||||
|
||||
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
|
||||
int start = 0;
|
||||
while (start < contents.Count - 1)
|
||||
{
|
||||
// We need at least two TextContents in a row to be able to coalesce.
|
||||
if (contents[start] is not TContent firstText)
|
||||
{
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contents[start + 1] is not TContent secondText)
|
||||
{
|
||||
start += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append the text from those nodes and continue appending subsequent TextContents until we run out.
|
||||
// We null out nodes as their text is appended so that we can later remove them all in one O(N) operation.
|
||||
coalescedText ??= new();
|
||||
_ = coalescedText.Clear().Append(firstText).Append(secondText);
|
||||
contents[start + 1] = null!;
|
||||
int i = start + 2;
|
||||
for (; i < contents.Count && contents[i] is TContent next; i++)
|
||||
{
|
||||
_ = coalescedText.Append(next);
|
||||
contents[i] = null!;
|
||||
}
|
||||
|
||||
// Store the replacement node. We inherit the properties of the first text node. We don't
|
||||
// currently propagate additional properties from the subsequent nodes. If we ever need to,
|
||||
// we can add that here.
|
||||
var newContent = fromText(coalescedText.ToString());
|
||||
contents[start] = newContent;
|
||||
newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone();
|
||||
|
||||
start = i;
|
||||
}
|
||||
|
||||
// Remove all of the null slots left over from the coalescing process.
|
||||
_ = contents.RemoveAll(u => u is null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Finalizes the <paramref name="response"/> object.</summary>
|
||||
private static void FinalizeResponse(AgentRunResponse response)
|
||||
{
|
||||
int count = response.Messages.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CoalesceTextContent((List<AIContent>)response.Messages[i].Contents);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Processes the <see cref="AgentRunResponseUpdate"/>, incorporating its contents into <paramref name="response"/>.</summary>
|
||||
/// <param name="update">The update to process.</param>
|
||||
/// <param name="response">The <see cref="AgentRunResponse"/> object that should be updated based on <paramref name="update"/>.</param>
|
||||
private static void ProcessUpdate(AgentRunResponseUpdate update, AgentRunResponse response)
|
||||
{
|
||||
// If there is no message created yet, or if the last update we saw had a different
|
||||
// message ID than the newest update, create a new message.
|
||||
ChatMessage message;
|
||||
var isNewMessage = false;
|
||||
if (response.Messages.Count == 0)
|
||||
{
|
||||
isNewMessage = true;
|
||||
}
|
||||
else if (update.MessageId is { Length: > 0 } updateMessageId
|
||||
&& response.Messages[response.Messages.Count - 1].MessageId is string lastMessageId
|
||||
&& updateMessageId != lastMessageId)
|
||||
{
|
||||
isNewMessage = true;
|
||||
}
|
||||
|
||||
if (isNewMessage)
|
||||
{
|
||||
message = new ChatMessage(ChatRole.Assistant, []);
|
||||
response.Messages.Add(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = response.Messages[response.Messages.Count - 1];
|
||||
}
|
||||
|
||||
// Some members on AgentRunResponseUpdate map to members of ChatMessage.
|
||||
// Incorporate those into the latest message; in cases where the message
|
||||
// stores a single value, prefer the latest update's value over anything
|
||||
// stored in the message.
|
||||
if (update.AuthorName is not null)
|
||||
{
|
||||
message.AuthorName = update.AuthorName;
|
||||
}
|
||||
|
||||
if (update.Role is ChatRole role)
|
||||
{
|
||||
message.Role = role;
|
||||
}
|
||||
|
||||
if (update.MessageId is { Length: > 0 })
|
||||
{
|
||||
// Note that this must come after the message checks earlier, as they depend
|
||||
// on this value for change detection.
|
||||
message.MessageId = update.MessageId;
|
||||
}
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
// Usage content is treated specially and propagated to the response's Usage.
|
||||
case UsageContent usage:
|
||||
(response.Usage ??= new()).Add(usage.Details);
|
||||
break;
|
||||
|
||||
default:
|
||||
message.Contents.Add(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Other members on a AgentRunResponseUpdate map to members of the AgentRunResponse.
|
||||
// Update the response object with those, preferring the values from later updates.
|
||||
|
||||
if (update.AgentId is { Length: > 0 })
|
||||
{
|
||||
response.AgentId = update.AgentId;
|
||||
}
|
||||
|
||||
if (update.ResponseId is { Length: > 0 })
|
||||
{
|
||||
response.ResponseId = update.ResponseId;
|
||||
}
|
||||
|
||||
if (update.CreatedAt is not null)
|
||||
{
|
||||
response.CreatedAt = update.CreatedAt;
|
||||
}
|
||||
|
||||
if (update.AdditionalProperties is not null)
|
||||
{
|
||||
if (response.AdditionalProperties is null)
|
||||
{
|
||||
response.AdditionalProperties = new(update.AdditionalProperties);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in update.AdditionalProperties)
|
||||
{
|
||||
response.AdditionalProperties[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Microsoft.Extensions.AI.Agents.CopilotStudio;
|
||||
/// </summary>
|
||||
internal static class ActivityProcessor
|
||||
{
|
||||
public static async IAsyncEnumerable<(ChatMessage message, bool reasoning)> ProcessActivityAsync(IAsyncEnumerable<IActivity> activities, bool streaming, ILogger logger)
|
||||
public static async IAsyncEnumerable<ChatMessage> ProcessActivityAsync(IAsyncEnumerable<IActivity> activities, bool streaming, ILogger logger)
|
||||
{
|
||||
await foreach (IActivity activity in activities.ConfigureAwait(false))
|
||||
{
|
||||
@@ -27,13 +27,13 @@ internal static class ActivityProcessor
|
||||
// pick from a list of actions.
|
||||
// The activity text doesn't make sense without the actions, as the message
|
||||
// is often instructing the user to pick from the provided list of actions.
|
||||
yield return (CreateChatMessageFromActivity(activity, [new TextContent(activity.Text)]), false);
|
||||
yield return CreateChatMessageFromActivity(activity, [new TextContent(activity.Text)]);
|
||||
break;
|
||||
case "typing":
|
||||
case "event":
|
||||
// TODO: Revisit usage of TextReasoningContent here, to evaluate whether all are really reasoning
|
||||
// or whether simply an AIContent base type would be more appropriate.
|
||||
yield return (CreateChatMessageFromActivity(activity, [new TextReasoningContent(activity.Text)]), true);
|
||||
yield return CreateChatMessageFromActivity(activity, [new TextReasoningContent(activity.Text)]);
|
||||
break;
|
||||
default:
|
||||
logger.LogWarning("Unknown activity type '{ActivityType}' received.", activity.Type);
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CopilotStudioAgent : Agent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> RunAsync(
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -63,39 +63,24 @@ public class CopilotStudioAgent : Agent
|
||||
// Invoke the Copilot Studio agent with the provided messages.
|
||||
string question = string.Join("\n", messages.Select(m => m.Text));
|
||||
var responseMessages = ActivityProcessor.ProcessActivityAsync(this.Client.AskQuestionAsync(question, copilotStudioAgentThread.Id, cancellationToken), streaming: false, this._logger);
|
||||
|
||||
// Enumerate the response messages
|
||||
var responseMessagesList = new List<ChatMessage>();
|
||||
await foreach ((ChatMessage message, bool reasoning) in responseMessages.ConfigureAwait(false))
|
||||
await foreach (var message in responseMessages.ConfigureAwait(false))
|
||||
{
|
||||
// If the message is a reasoning message, return it as part of the intermediate messages
|
||||
// instead of the final response.
|
||||
if (reasoning)
|
||||
{
|
||||
if (options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages.Invoke([message]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the message to the list
|
||||
responseMessagesList.Add(message);
|
||||
}
|
||||
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
return new ChatResponse(responseMessagesList)
|
||||
return new AgentRunResponse(responseMessagesList)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = responseMessagesList.LastOrDefault()?.MessageId,
|
||||
ConversationId = copilotStudioAgentThread.Id,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -116,25 +101,19 @@ public class CopilotStudioAgent : Agent
|
||||
var responseMessages = ActivityProcessor.ProcessActivityAsync(this.Client.AskQuestionAsync(question, copilotStudioAgentThread.Id, cancellationToken), streaming: true, this._logger);
|
||||
|
||||
// Enumerate the response messages
|
||||
await foreach ((ChatMessage message, bool reasoning) in responseMessages.ConfigureAwait(false))
|
||||
await foreach (ChatMessage message in responseMessages.ConfigureAwait(false))
|
||||
{
|
||||
// If the message is a reasoning message, return it as part of the intermediate messages.
|
||||
if (reasoning && options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages.Invoke([message]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
yield return new ChatResponseUpdate(message.Role, message.Contents)
|
||||
yield return new AgentRunResponseUpdate(message.Role, message.Contents)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
ResponseId = message.MessageId,
|
||||
MessageId = message.MessageId,
|
||||
ConversationId = copilotStudioAgentThread.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class ChatClientAgent : Agent
|
||||
internal ChatOptions? ChatOptions => this._agentOptions?.ChatOptions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> RunAsync(
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -103,16 +103,12 @@ public sealed class ChatClientAgent : Agent
|
||||
var chatResponseMessages = chatResponse.Messages as IReadOnlyCollection<ChatMessage> ?? chatResponse.Messages.ToArray();
|
||||
|
||||
await this.NotifyThreadOfNewMessagesAsync(chatClientThread, chatResponseMessages, cancellationToken).ConfigureAwait(false);
|
||||
if (options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages(chatResponseMessages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return chatResponse;
|
||||
return chatResponse.ToAgentRunResponse(this.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -145,7 +141,7 @@ public sealed class ChatClientAgent : Agent
|
||||
{
|
||||
responseUpdates.Add(update);
|
||||
update.AuthorName ??= agentName;
|
||||
yield return update;
|
||||
yield return update.ToAgentRunResponseUpdate(this.Id);
|
||||
}
|
||||
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
@@ -162,10 +158,6 @@ public sealed class ChatClientAgent : Agent
|
||||
await this.NotifyThreadOfNewMessagesAsync(chatClientThread, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.NotifyThreadOfNewMessagesAsync(chatClientThread, chatResponseMessages, cancellationToken).ConfigureAwait(false);
|
||||
if (options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages(chatResponseMessages).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
+7
-7
@@ -21,8 +21,8 @@ public static class ChatClientAgentExtensions
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public static Task<ChatResponse> RunAsync(
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public static Task<AgentRunResponse> RunAsync(
|
||||
this ChatClientAgent agent,
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
@@ -45,8 +45,8 @@ public static class ChatClientAgentExtensions
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public static Task<ChatResponse> RunAsync(
|
||||
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public static Task<AgentRunResponse> RunAsync(
|
||||
this ChatClientAgent agent,
|
||||
string prompt,
|
||||
AgentThread? thread = null,
|
||||
@@ -69,7 +69,7 @@ public static class ChatClientAgentExtensions
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public static IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
public static IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
this ChatClientAgent agent,
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
@@ -92,8 +92,8 @@ public static class ChatClientAgentExtensions
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async enumerable of <see cref="ChatResponseUpdate"/> items for streaming the response.</returns>
|
||||
public static IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
/// <returns>An async enumerable of <see cref="AgentRunResponseUpdate"/> items for streaming the response.</returns>
|
||||
public static IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
this ChatClientAgent agent,
|
||||
string prompt,
|
||||
AgentThread? thread = null,
|
||||
|
||||
@@ -14,7 +14,6 @@ internal sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
/// <param name="chatOptions">Optional chat options to pass to the agent's invocation.</param>
|
||||
internal ChatClientAgentRunOptions(AgentRunOptions? source = null, ChatOptions? chatOptions = null)
|
||||
{
|
||||
this.OnIntermediateMessages = source?.OnIntermediateMessages;
|
||||
this.ChatOptions = chatOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ChatResponse"/> and <see cref="ChatResponseUpdate"/>.
|
||||
/// </summary>
|
||||
internal static class ChatResponseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ChatResponse"/> instance to an <see cref="AgentRunResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="chatResponse">The <see cref="ChatResponse"/> to convert. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="agentId">The ID of the agent that generated the response. Cannot be <see langword="null"/>.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="AgentRunResponse"/> containing the messages, metadata, and additional properties from the
|
||||
/// specified <see cref="ChatResponse"/>.
|
||||
/// </returns>
|
||||
public static AgentRunResponse ToAgentRunResponse(this ChatResponse chatResponse, string agentId)
|
||||
{
|
||||
_ = Throw.IfNull(chatResponse);
|
||||
_ = Throw.IfNullOrWhitespace(agentId);
|
||||
|
||||
return new AgentRunResponse(chatResponse.Messages)
|
||||
{
|
||||
AgentId = agentId,
|
||||
ResponseId = chatResponse.ResponseId,
|
||||
CreatedAt = chatResponse.CreatedAt,
|
||||
Usage = chatResponse.Usage,
|
||||
RawRepresentation = chatResponse,
|
||||
AdditionalProperties = chatResponse.AdditionalProperties
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ChatResponseUpdate"/> instance to an <see cref="AgentRunResponseUpdate"/>.
|
||||
/// </summary>
|
||||
/// <param name="chatResponseUpdate">The <see cref="ChatResponseUpdate"/> to convert. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="agentId">The ID of the agent that generated the response. Cannot be <see langword="null"/>.</param>
|
||||
/// <returns>An <see cref="AgentRunResponseUpdate"/> containing the properties from the specified <see cref="ChatResponseUpdate"/>.</returns>
|
||||
public static AgentRunResponseUpdate ToAgentRunResponseUpdate(this ChatResponseUpdate chatResponseUpdate, string agentId)
|
||||
{
|
||||
_ = Throw.IfNull(chatResponseUpdate);
|
||||
|
||||
return new()
|
||||
{
|
||||
AgentId = agentId,
|
||||
Role = chatResponseUpdate.Role,
|
||||
AuthorName = chatResponseUpdate.AuthorName,
|
||||
Contents = chatResponseUpdate.Contents,
|
||||
MessageId = chatResponseUpdate.MessageId,
|
||||
ResponseId = chatResponseUpdate.ResponseId,
|
||||
CreatedAt = chatResponseUpdate.CreatedAt,
|
||||
RawRepresentation = chatResponseUpdate,
|
||||
AdditionalProperties = chatResponseUpdate.AdditionalProperties
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Samples;
|
||||
@@ -87,29 +88,29 @@ public abstract class BaseSample : TextWriter
|
||||
/// Processes and writes the latest agent chat response to the console, including metadata and content details.
|
||||
/// </summary>
|
||||
/// <remarks>This method formats and outputs the most recent message from the provided <see
|
||||
/// cref="ChatResponse"/> object. It includes the message role, author name (if available), text content, and
|
||||
/// cref="AgentRunResponse"/> object. It includes the message role, author name (if available), text content, and
|
||||
/// additional content such as images, function calls, and function results. Usage statistics, including token
|
||||
/// counts, are also displayed.</remarks>
|
||||
/// <param name="chatResponse">The <see cref="ChatResponse"/> object containing the chat messages and usage data.</param>
|
||||
/// <param name="response">The <see cref="AgentRunResponse"/> object containing the chat messages and usage data.</param>
|
||||
/// <param name="printUsage">The flag to indicate whether to print usage information. Defaults to <see langword="true"/>.</param>
|
||||
protected void WriteResponseOutput(ChatResponse chatResponse, bool? printUsage = true)
|
||||
protected void WriteResponseOutput(AgentRunResponse response, bool? printUsage = true)
|
||||
{
|
||||
if (chatResponse.Messages.Count == 0)
|
||||
if (response.Messages.Count == 0)
|
||||
{
|
||||
// If there are no messages, we can skip writing the message.
|
||||
return;
|
||||
}
|
||||
|
||||
var message = chatResponse.Messages.Last();
|
||||
var message = response.Messages.Last();
|
||||
this.WriteMessageOutput(message);
|
||||
|
||||
WriteUsage();
|
||||
|
||||
void WriteUsage()
|
||||
{
|
||||
if (!(printUsage ?? true) || chatResponse.Usage is null) { return; }
|
||||
if (!(printUsage ?? true) || response.Usage is null) { return; }
|
||||
|
||||
UsageDetails usageDetails = chatResponse.Usage;
|
||||
UsageDetails usageDetails = response.Usage;
|
||||
|
||||
Console.WriteLine($" [Usage] Tokens: {usageDetails.TotalTokenCount}, Input: {usageDetails.InputTokenCount}, Output: {usageDetails.OutputTokenCount}");
|
||||
}
|
||||
@@ -151,11 +152,11 @@ public abstract class BaseSample : TextWriter
|
||||
/// Writes the streaming agent response updates to the console.
|
||||
/// </summary>
|
||||
/// <remarks>This method formats and outputs the most recent message from the provided <see
|
||||
/// cref="ChatResponseUpdate"/> object. It includes the message role, author name (if available), text content, and
|
||||
/// cref="AgentRunResponseUpdate"/> object. It includes the message role, author name (if available), text content, and
|
||||
/// additional content such as images, function calls, and function results. Usage statistics, including token
|
||||
/// counts, are also displayed.</remarks>
|
||||
/// <param name="update">The <see cref="ChatResponseUpdate"/> object containing the chat messages and usage data.</param>
|
||||
protected void WriteAgentOutput(ChatResponseUpdate update)
|
||||
/// <param name="update">The <see cref="AgentRunResponseUpdate"/> object containing the chat messages and usage data.</param>
|
||||
protected void WriteAgentOutput(AgentRunResponseUpdate update)
|
||||
{
|
||||
if (update.Contents.Count == 0)
|
||||
{
|
||||
|
||||
@@ -74,7 +74,7 @@ public abstract class OrchestrationSample : BaseSample
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the provided chat response messages to the console or test output, including role and author information.
|
||||
/// Writes the provided messages to the console or test output, including role and author information.
|
||||
/// </summary>
|
||||
/// <param name="response">An enumerable of <see cref="ChatMessage"/> objects to write.</param>
|
||||
protected static void WriteResponse(IEnumerable<ChatMessage> response)
|
||||
@@ -89,15 +89,15 @@ public abstract class OrchestrationSample : BaseSample
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the streamed chat response updates to the console or test output, including role and author information.
|
||||
/// Writes the streamed agent run response updates to the console or test output, including role and author information.
|
||||
/// </summary>
|
||||
/// <param name="streamedResponses">An enumerable of <see cref="ChatResponseUpdate"/> objects representing streamed responses.</param>
|
||||
protected static void WriteStreamedResponse(IEnumerable<ChatResponseUpdate> streamedResponses)
|
||||
/// <param name="streamedResponses">An enumerable of <see cref="AgentRunResponseUpdate"/> objects representing streamed responses.</param>
|
||||
protected static void WriteStreamedResponse(IEnumerable<AgentRunResponseUpdate> streamedResponses)
|
||||
{
|
||||
string? authorName = null;
|
||||
ChatRole? authorRole = null;
|
||||
StringBuilder builder = new();
|
||||
foreach (ChatResponseUpdate response in streamedResponses)
|
||||
foreach (AgentRunResponseUpdate response in streamedResponses)
|
||||
{
|
||||
authorName ??= response.AuthorName;
|
||||
authorRole ??= response.Role;
|
||||
@@ -122,7 +122,7 @@ public abstract class OrchestrationSample : BaseSample
|
||||
/// <summary>
|
||||
/// Gets the list of streamed response updates received so far.
|
||||
/// </summary>
|
||||
public List<ChatResponseUpdate> StreamedResponses { get; } = [];
|
||||
public List<AgentRunResponseUpdate> StreamedResponses { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of chat messages representing the conversation history.
|
||||
@@ -142,12 +142,12 @@ public abstract class OrchestrationSample : BaseSample
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback to handle a streamed chat response update, adding it to the list and writing output if final.
|
||||
/// Callback to handle a streamed agent run response update, adding it to the list and writing output if final.
|
||||
/// </summary>
|
||||
/// <param name="streamedResponse">The <see cref="ChatResponseUpdate"/> to process.</param>
|
||||
/// <param name="streamedResponse">The <see cref="AgentRunResponseUpdate"/> to process.</param>
|
||||
/// <param name="isFinal">Indicates whether this is the final update in the stream.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public ValueTask StreamingResultCallback(ChatResponseUpdate streamedResponse, bool isFinal)
|
||||
public ValueTask StreamingResultCallback(AgentRunResponseUpdate streamedResponse, bool isFinal)
|
||||
{
|
||||
this.StreamedResponses.Add(streamedResponse);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user