Skip to content

.Net: Common agent api review feedback 1 #11118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
63 changes: 49 additions & 14 deletions dotnet/src/Agents/Abstractions/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,35 @@ public abstract class Agent
/// </summary>
/// <param name="message">The message to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread.</param>
/// <param name="arguments">Optional arguments to pass to the agents's invocation, including any <see cref="PromptExecutionSettings"/>.</param>
/// <param name="kernel">The <see cref="Kernel"/> containing services, plugins, and other state for use by the agent.</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="ChatMessageContent"/> and an <see cref="AgentThread"/>.</returns>
/// <remarks>
/// To continue this thread in the future, use an <see cref="AgentThread"/> returned in one of the response items.
/// </remarks>
public abstract IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
public virtual IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.InvokeAsync(new[] { message }, thread, options, cancellationToken);
}

/// <summary>
/// Invoke the agent with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread.</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="ChatMessageContent"/> and an <see cref="AgentThread"/>.</returns>
/// <remarks>
/// To continue this thread in the future, use an <see cref="AgentThread"/> returned in one of the response items.
/// </remarks>
public abstract IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);

Expand All @@ -69,19 +85,35 @@ public abstract IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAs
/// </summary>
/// <param name="message">The message to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread.</param>
/// <param name="arguments">Optional arguments to pass to the agents's invocation, including any <see cref="PromptExecutionSettings"/>.</param>
/// <param name="kernel">The <see cref="Kernel"/> containing services, plugins, and other state for use by the agent.</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="ChatMessageContent"/> and an <see cref="AgentThread"/>.</returns>
/// <remarks>
/// To continue this thread in the future, use an <see cref="AgentThread"/> returned in one of the response items.
/// </remarks>
public abstract IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
public virtual IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.InvokeStreamingAsync(new[] { message }, thread, options, cancellationToken);
}

/// <summary>
/// Invoke the agent with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread.</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="ChatMessageContent"/> and an <see cref="AgentThread"/>.</returns>
/// <remarks>
/// To continue this thread in the future, use an <see cref="AgentThread"/> returned in one of the response items.
/// </remarks>
public abstract IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);

Expand Down Expand Up @@ -143,14 +175,14 @@ public abstract IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>>
/// Ensures that the thread exists, is of the expected type, and is active, plus adds the provided message to the thread.
/// </summary>
/// <typeparam name="TThreadType">The expected type of the thead.</typeparam>
/// <param name="message">The message to add to the thread once it is setup.</param>
/// <param name="messages">The messages to add to the thread once it is setup.</param>
/// <param name="thread">The thread to create if it's null, validate it's type if not null, and start if it is not active.</param>
/// <param name="constructThread">A callback to use to construct the thread if it's null.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An async task that completes once all update are complete.</returns>
/// <exception cref="KernelException"></exception>
protected async Task<TThreadType> EnsureThreadExistsWithMessageAsync<TThreadType>(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread,
Func<TThreadType> constructThread,
CancellationToken cancellationToken)
Expand All @@ -168,8 +200,11 @@ protected async Task<TThreadType> EnsureThreadExistsWithMessageAsync<TThreadType

await thread.CreateAsync(cancellationToken).ConfigureAwait(false);

// Notify the thread that a new message is available.
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
// Notify the thread that new messages are available.
foreach (var message in messages)
{
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
}

return concreteThreadType;
}
Expand Down
10 changes: 10 additions & 0 deletions dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ namespace Microsoft.SemanticKernel.Agents;
/// </summary>
public class AgentInvokeOptions
{
/// <summary>
/// Gets or sets optional arguments to pass to the agent's invocation, including any <see cref="PromptExecutionSettings"/>
/// </summary>
public KernelArguments? KernelArguments { get; init; } = null;

/// <summary>
/// Gets or sets the <see cref="Kernel"/> containing services, plugins, and other state for use by the agent
/// </summary>
public Kernel? Kernel { get; init; } = null;

/// <summary>
/// Gets or sets any instructions, in addition to those that were provided to the agent
/// initially, that need to be added to the prompt for this invocation only.
Expand Down
8 changes: 2 additions & 6 deletions dotnet/src/Agents/Abstractions/AggregatorAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,8 @@ public sealed class AggregatorAgent(Func<AgentChat> chatProvider) : Agent

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
Expand All @@ -59,10 +57,8 @@ public override IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAs

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
Expand Down
24 changes: 10 additions & 14 deletions dotnet/src/Agents/AzureAI/AzureAIAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,15 @@ public IAsyncEnumerable<ChatMessageContent> InvokeAsync(

/// <inheritdoc/>
public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Verify.NotNull(message);
Verify.NotNull(messages);

var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
message,
messages,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
Expand All @@ -168,8 +166,8 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
var invokeResults = this.InvokeAsync(
azureAIAgentThread.Id!,
internalOptions,
this.MergeArguments(arguments),
kernel ?? this.Kernel,
this.MergeArguments(options?.KernelArguments),
options?.Kernel ?? this.Kernel,
cancellationToken);

// Notify the thread of new messages and return them to the caller.
Expand Down Expand Up @@ -221,17 +219,15 @@ async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync()

/// <inheritdoc/>
public async override IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Verify.NotNull(message);
Verify.NotNull(messages);

var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
message,
messages,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
Expand All @@ -247,8 +243,8 @@ public async override IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
var invokeResults = this.InvokeStreamingAsync(
azureAIAgentThread.Id!,
internalOptions,
this.MergeArguments(arguments),
kernel ?? this.Kernel,
this.MergeArguments(options?.KernelArguments),
options?.Kernel ?? this.Kernel,
newMessagesReceiver,
cancellationToken);

Expand Down
50 changes: 42 additions & 8 deletions dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.AI.Projects;
using Microsoft.SemanticKernel.Agents.AzureAI.Internal;

Expand All @@ -12,10 +14,11 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI;
/// <summary>
/// Represents a conversation thread for an Azure AI agent.
/// </summary>
public class AzureAIAgentThread : AgentThread
public sealed class AzureAIAgentThread : AgentThread
{
private readonly AgentsClient _client;
private string? _id = null;
private bool _isDeleted = false;

/// <summary>
/// Initializes a new instance of the <see cref="AzureAIAgentThread"/> class.
Expand Down Expand Up @@ -48,6 +51,11 @@ public AzureAIAgentThread(AgentsClient client, string id)
/// <inheritdoc />
public override async Task<string> CreateAsync(CancellationToken cancellationToken = default)
{
if (this._isDeleted)
{
throw new InvalidOperationException("This thread has been deleted and cannot be recreated.");
}

if (this._id is not null)
{
return this._id;
Expand All @@ -62,21 +70,39 @@ public override async Task<string> CreateAsync(CancellationToken cancellationTok
/// <inheritdoc />
public override async Task DeleteAsync(CancellationToken cancellationToken = default)
{
if (this._isDeleted)
{
return;
}

if (this._id is null)
{
throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
throw new InvalidOperationException("This thread cannot be deleted, since it has not been created.");
}

await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
this._id = null;
try
{
await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
// Do nothing, since the thread was already deleted.
}

this._isDeleted = true;
}

/// <inheritdoc />
public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
if (this._isDeleted)
{
throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
}

if (this._id is null)
{
throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}

// If the message was generated by this agent, it is already in the thread and we shouldn't add it again.
Expand All @@ -87,13 +113,21 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
}

/// <inheritdoc />
public IAsyncEnumerable<ChatMessageContent> GetMessagesAsync(CancellationToken cancellationToken = default)
public async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (this._isDeleted)
{
throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
}

if (this._id is null)
{
throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}

return AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken);
await foreach (var message in AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken).ConfigureAwait(false))
{
yield return message;
}
}
}
8 changes: 2 additions & 6 deletions dotnet/src/Agents/Bedrock/BedrockAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,8 @@ public static string CreateSessionId()

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
Expand All @@ -88,10 +86,8 @@ public override IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAs

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ChatMessageContent message,
ICollection<ChatMessageContent> messages,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
Expand Down
Loading
Loading