Skip to content

.Net: Add a common agent invoke api. #11069

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
merged 12 commits into from
Mar 21, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Agents;
/// <summary>
/// Demonstrate service selection for <see cref="ChatCompletionAgent"/> through setting service-id
/// on <see cref="KernelAgent.Arguments"/> and also providing override <see cref="KernelArguments"/>
/// when calling <see cref="ChatCompletionAgent.InvokeAsync"/>
/// when calling <see cref="ChatCompletionAgent.InvokeAsync(ChatHistory, KernelArguments?, Kernel?, CancellationToken)"/>
/// </summary>
public class ChatCompletion_ServiceSelection(ITestOutputHelper output) : BaseAgentsTest(output)
{
Expand Down
77 changes: 77 additions & 0 deletions dotnet/src/Agents/Abstractions/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ public abstract class Agent
/// </summary>
public ILoggerFactory? LoggerFactory { get; init; }

/// <summary>
/// Invoke the agent with the provided message and arguments.
/// </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(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Invoke the agent with the provided message and arguments.
/// </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(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>
/// The <see cref="ILogger"/> associated with this <see cref="Agent"/>.
/// </summary>
Expand Down Expand Up @@ -96,4 +138,39 @@ public abstract class Agent
protected internal abstract Task<AgentChannel> RestoreChannelAsync(string channelState, CancellationToken cancellationToken);

private ILogger? _logger;

/// <summary>
/// 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="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,
AgentThread? thread,
Func<TThreadType> constructThread,
CancellationToken cancellationToken)
where TThreadType : AgentThread
{
if (thread is null)
{
thread = constructThread();
}

if (thread is not TThreadType concreteThreadType)
{
throw new KernelException($"{this.GetType().Name} currently only supports agent threads of type {nameof(TThreadType)}.");
}

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

// Notify the thread that a new message is available.
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);

return concreteThreadType;
}
}
15 changes: 15 additions & 0 deletions dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.

namespace Microsoft.SemanticKernel.Agents;

/// <summary>
/// Optional parameters for agent invocation.
/// </summary>
public class AgentInvokeOptions
{
/// <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.
/// </summary>
public string AdditionalInstructions { get; init; } = string.Empty;
}
36 changes: 36 additions & 0 deletions dotnet/src/Agents/Abstractions/AgentResponseItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.

namespace Microsoft.SemanticKernel.Agents;

/// <summary>
/// Container class that holds a <see cref="ChatMessageContent"/> or <see cref="StreamingChatMessageContent"/> and an <see cref="AgentThread"/>.
/// </summary>
public class AgentResponseItem<TMessage>
{
private readonly TMessage _message;
private readonly AgentThread _thread;

/// <summary>
/// Initializes a new instance of the <see cref="AgentResponseItem{T}"/> class.
/// </summary>
/// <param name="message">The chat message content.</param>
/// <param name="thread">The conversation thread associated with the response.</param>
public AgentResponseItem(TMessage message, AgentThread thread)
{
Verify.NotNull(message);
Verify.NotNull(thread);

this._message = message;
this._thread = thread;
}

/// <summary>
/// Gets the chat message content.
/// </summary>
public TMessage Message => this._message;

/// <summary>
/// Gets the conversation thread associated with the response.
/// </summary>
public AgentThread Thread => this._thread;
}
47 changes: 47 additions & 0 deletions dotnet/src/Agents/Abstractions/AgentThread.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.SemanticKernel.Agents;

/// <summary>
/// Base abstraction for all Semantic Kernel agent threads.
/// A thread represents a specific conversation with an agent.
/// </summary>
/// <remarks>
/// This class is used to manage the lifecycle of an agent thread.
/// The thread can be not-start, started or ended.
/// </remarks>
public abstract class AgentThread
{
/// <summary>
/// Gets the id of the current thread.
/// </summary>
public abstract string? Id { get; }

/// <summary>
/// Creates the thread and returns the thread id.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The id of the new thread.</returns>
public abstract Task<string> CreateAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Deletes the current thread.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that completes when the thread has been ended.</returns>
public abstract Task DeleteAsync(CancellationToken cancellationToken = default);

/// <summary>
/// This method is called when a new message has been contributed to the chat by any participant.
/// </summary>
/// <remarks>
/// Inheritors can use this method to update their context based on the new message.
/// </remarks>
/// <param name="newMessage">The new message.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that completes when the context has been updated.</returns>
public abstract Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default);
}
26 changes: 26 additions & 0 deletions dotnet/src/Agents/Abstractions/AggregatorAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ public sealed class AggregatorAgent(Func<AgentChat> chatProvider) : Agent
/// </value>
public AggregatorMode Mode { get; init; } = AggregatorMode.Flat;

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
// TODO: Need to determine the correct approach here.
throw new NotImplementedException();
}

/// <inheritdoc/>
public override IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> InvokeStreamingAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
// TODO: Need to determine the correct approach here.
throw new NotImplementedException();
}

/// <inheritdoc/>
/// <remarks>
/// Different <see cref="AggregatorAgent"/> instances will never share the same channel.
Expand Down
91 changes: 89 additions & 2 deletions dotnet/src/Agents/AzureAI/AzureAIAgent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
Expand All @@ -9,6 +10,7 @@
using Microsoft.SemanticKernel.Agents.Extensions;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using AAIP = Azure.AI.Projects;

namespace Microsoft.SemanticKernel.Agents.AzureAI;

Expand Down Expand Up @@ -116,7 +118,7 @@ public Task AddChatMessageAsync(string threadId, ChatMessageContent message, Can
/// <returns>An asynchronous enumeration of messages.</returns>
public IAsyncEnumerable<ChatMessageContent> GetThreadMessagesAsync(string threadId, CancellationToken cancellationToken = default)
{
return AgentThreadActions.GetMessagesAsync(this.Client, threadId, cancellationToken);
return AgentThreadActions.GetMessagesAsync(this.Client, threadId, null, cancellationToken);
}

/// <summary>
Expand All @@ -139,6 +141,45 @@ public IAsyncEnumerable<ChatMessageContent> InvokeAsync(
return this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken);
}

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

var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
message,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);

// Create options that include the additional instructions.
var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new AzureAIInvocationOptions()
{
AdditionalInstructions = options?.AdditionalInstructions,
};

// Invoke the Agent with the thread that we already added our message to.
var invokeResults = this.InvokeAsync(
azureAIAgentThread.Id!,
internalOptions,
this.MergeArguments(arguments),
kernel ?? this.Kernel,
cancellationToken);

// Notify the thread of new messages and return them to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
{
await azureAIAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
yield return new(result, azureAIAgentThread);
}
}

/// <summary>
/// Invokes the assistant on the specified thread.
/// </summary>
Expand Down Expand Up @@ -178,6 +219,52 @@ async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync()
}
}

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

var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
message,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);

// Create options that include the additional instructions.
var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new AzureAIInvocationOptions()
{
AdditionalInstructions = options?.AdditionalInstructions,
};

// Invoke the Agent with the thread that we already added our message to.
var newMessagesReceiver = new ChatHistory();
var invokeResults = this.InvokeStreamingAsync(
azureAIAgentThread.Id!,
internalOptions,
this.MergeArguments(arguments),
kernel ?? this.Kernel,
newMessagesReceiver,
cancellationToken);

// Return the chunks to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
{
yield return new(result, azureAIAgentThread);
}

// Notify the thread of any new messages that were assembled from the streaming response.
foreach (var newMessage in newMessagesReceiver)
{
await azureAIAgentThread.OnNewMessageAsync(newMessage, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Invokes the assistant on the specified thread with streaming response.
/// </summary>
Expand Down Expand Up @@ -276,7 +363,7 @@ protected override async Task<AgentChannel> RestoreChannelAsync(string channelSt

this.Logger.LogAzureAIAgentRestoringChannel(nameof(RestoreChannelAsync), nameof(AzureAIChannel), threadId);

AgentThread thread = await this.Client.GetThreadAsync(threadId, cancellationToken).ConfigureAwait(false);
AAIP.AgentThread thread = await this.Client.GetThreadAsync(threadId, cancellationToken).ConfigureAwait(false);

this.Logger.LogAzureAIAgentRestoredChannel(nameof(RestoreChannelAsync), nameof(AzureAIChannel), threadId);

Expand Down
Loading
Loading