diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
index 46ea8dea2246..4a2663a1f5f0 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
@@ -8,7 +8,7 @@ namespace Agents;
///
/// Demonstrate service selection for through setting service-id
/// on and also providing override
-/// when calling
+/// when calling
///
public class ChatCompletion_ServiceSelection(ITestOutputHelper output) : BaseAgentsTest(output)
{
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index 383b5df27385..a742d7ce6767 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -43,6 +43,48 @@ public abstract class Agent
///
public ILoggerFactory? LoggerFactory { get; init; }
+ ///
+ /// Invoke the agent with the provided message and arguments.
+ ///
+ /// The message to pass to the agent.
+ /// The conversation thread to continue with this invocation. If not provided, creates a new thread.
+ /// Optional arguments to pass to the agents's invocation, including any .
+ /// The containing services, plugins, and other state for use by the agent.
+ /// Optional parameters for agent invocation.
+ /// The to monitor for cancellation requests. The default is .
+ /// An async list of response items that each contain a and an .
+ ///
+ /// To continue this thread in the future, use an returned in one of the response items.
+ ///
+ public abstract IAsyncEnumerable> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Invoke the agent with the provided message and arguments.
+ ///
+ /// The message to pass to the agent.
+ /// The conversation thread to continue with this invocation. If not provided, creates a new thread.
+ /// Optional arguments to pass to the agents's invocation, including any .
+ /// The containing services, plugins, and other state for use by the agent.
+ /// Optional parameters for agent invocation.
+ /// The to monitor for cancellation requests. The default is .
+ /// An async list of response items that each contain a and an .
+ ///
+ /// To continue this thread in the future, use an returned in one of the response items.
+ ///
+ public abstract IAsyncEnumerable> InvokeStreamingAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default);
+
///
/// The associated with this .
///
@@ -96,4 +138,39 @@ public abstract class Agent
protected internal abstract Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken);
private ILogger? _logger;
+
+ ///
+ /// Ensures that the thread exists, is of the expected type, and is active, plus adds the provided message to the thread.
+ ///
+ /// The expected type of the thead.
+ /// The message to add to the thread once it is setup.
+ /// The thread to create if it's null, validate it's type if not null, and start if it is not active.
+ /// A callback to use to construct the thread if it's null.
+ /// The to monitor for cancellation requests. The default is .
+ /// An async task that completes once all update are complete.
+ ///
+ protected async Task EnsureThreadExistsWithMessageAsync(
+ ChatMessageContent message,
+ AgentThread? thread,
+ Func 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;
+ }
}
diff --git a/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs
new file mode 100644
index 000000000000..e93045a5544f
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Optional parameters for agent invocation.
+///
+public class AgentInvokeOptions
+{
+ ///
+ /// 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.
+ ///
+ public string AdditionalInstructions { get; init; } = string.Empty;
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentResponseItem.cs b/dotnet/src/Agents/Abstractions/AgentResponseItem.cs
new file mode 100644
index 000000000000..e6fca3c1438d
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentResponseItem.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Container class that holds a or and an .
+///
+public class AgentResponseItem
+{
+ private readonly TMessage _message;
+ private readonly AgentThread _thread;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chat message content.
+ /// The conversation thread associated with the response.
+ public AgentResponseItem(TMessage message, AgentThread thread)
+ {
+ Verify.NotNull(message);
+ Verify.NotNull(thread);
+
+ this._message = message;
+ this._thread = thread;
+ }
+
+ ///
+ /// Gets the chat message content.
+ ///
+ public TMessage Message => this._message;
+
+ ///
+ /// Gets the conversation thread associated with the response.
+ ///
+ public AgentThread Thread => this._thread;
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentThread.cs b/dotnet/src/Agents/Abstractions/AgentThread.cs
new file mode 100644
index 000000000000..3fc4a6bcd94a
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentThread.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Base abstraction for all Semantic Kernel agent threads.
+/// A thread represents a specific conversation with an agent.
+///
+///
+/// This class is used to manage the lifecycle of an agent thread.
+/// The thread can be not-start, started or ended.
+///
+public abstract class AgentThread
+{
+ ///
+ /// Gets the id of the current thread.
+ ///
+ public abstract string? Id { get; }
+
+ ///
+ /// Creates the thread and returns the thread id.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// The id of the new thread.
+ public abstract Task CreateAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Deletes the current thread.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// A task that completes when the thread has been ended.
+ public abstract Task DeleteAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// This method is called when a new message has been contributed to the chat by any participant.
+ ///
+ ///
+ /// Inheritors can use this method to update their context based on the new message.
+ ///
+ /// The new message.
+ /// The to monitor for cancellation requests. The default is .
+ /// A task that completes when the context has been updated.
+ public abstract Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
index 8cde6b5a9001..e71b7ff03f1a 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
@@ -44,6 +44,32 @@ public sealed class AggregatorAgent(Func chatProvider) : Agent
///
public AggregatorMode Mode { get; init; } = AggregatorMode.Flat;
+ ///
+ public override IAsyncEnumerable> 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();
+ }
+
+ ///
+ public override IAsyncEnumerable> 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();
+ }
+
///
///
/// Different instances will never share the same channel.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 912bd83778fe..0ee773037885 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -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;
@@ -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;
@@ -116,7 +118,7 @@ public Task AddChatMessageAsync(string threadId, ChatMessageContent message, Can
/// An asynchronous enumeration of messages.
public IAsyncEnumerable GetThreadMessagesAsync(string threadId, CancellationToken cancellationToken = default)
{
- return AgentThreadActions.GetMessagesAsync(this.Client, threadId, cancellationToken);
+ return AgentThreadActions.GetMessagesAsync(this.Client, threadId, null, cancellationToken);
}
///
@@ -139,6 +141,45 @@ public IAsyncEnumerable InvokeAsync(
return this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken);
}
+ ///
+ public override async IAsyncEnumerable> 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);
+ }
+ }
+
///
/// Invokes the assistant on the specified thread.
///
@@ -178,6 +219,52 @@ async IAsyncEnumerable InternalInvokeAsync()
}
}
+ ///
+ public async override IAsyncEnumerable> 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);
+ }
+ }
+
///
/// Invokes the assistant on the specified thread with streaming response.
///
@@ -276,7 +363,7 @@ protected override async Task 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);
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
new file mode 100644
index 000000000000..c5c0090ea170
--- /dev/null
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.SemanticKernel.Agents.AzureAI.Internal;
+
+namespace Microsoft.SemanticKernel.Agents.AzureAI;
+
+///
+/// Represents a conversation thread for an Azure AI agent.
+///
+public class AzureAIAgentThread : AgentThread
+{
+ private readonly AgentsClient _client;
+ private string? _id = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agents client to use for interacting with threads.
+ public AzureAIAgentThread(AgentsClient client)
+ {
+ Verify.NotNull(client);
+
+ this._client = client;
+ }
+
+ ///
+ /// Initializes a new instance of the class that resumes an existing thread.
+ ///
+ /// The agents client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public AzureAIAgentThread(AgentsClient client, string id)
+ {
+ Verify.NotNull(client);
+ Verify.NotNull(id);
+
+ this._client = client;
+ this._id = id;
+ }
+
+ ///
+ public override string? Id => this._id;
+
+ ///
+ public override async Task CreateAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is not null)
+ {
+ return this._id;
+ }
+
+ var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._id = assistantThreadResponse.Value.Id;
+
+ return assistantThreadResponse.Value.Id;
+ }
+
+ ///
+ public override async Task DeleteAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ }
+
+ await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
+ this._id = null;
+ }
+
+ ///
+ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ }
+
+ // If the message was generated by this agent, it is already in the thread and we shouldn't add it again.
+ if (newMessage.Metadata == null || !newMessage.Metadata.TryGetValue("ThreadId", out var messageThreadId) || !string.Equals(messageThreadId, this._id))
+ {
+ await AgentThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ }
+
+ return AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Agents/AzureAI/AzureAIChannel.cs b/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
index c3979e10bcb3..fa5991d4ace8 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
@@ -47,7 +47,7 @@ protected override IAsyncEnumerable InvokeStreaming
///
protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken)
{
- return AgentThreadActions.GetMessagesAsync(client, threadId, cancellationToken);
+ return AgentThreadActions.GetMessagesAsync(client, threadId, null, cancellationToken);
}
///
diff --git a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
index 167349b63d11..9eb6495b68f8 100644
--- a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
+++ b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
@@ -15,6 +15,7 @@
using Microsoft.SemanticKernel.Agents.Extensions;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.FunctionCalling;
+using AAIP = Azure.AI.Projects;
namespace Microsoft.SemanticKernel.Agents.AzureAI.Internal;
@@ -45,7 +46,7 @@ internal static class AgentThreadActions
/// The thread identifier
public static async Task CreateThreadAsync(AgentsClient client, CancellationToken cancellationToken = default)
{
- AgentThread thread = await client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ AAIP.AgentThread thread = await client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return thread.Id;
}
@@ -85,9 +86,10 @@ await client.CreateMessageAsync(
///
/// The assistant client
/// The thread identifier
+ /// The order to return messages in.
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
- public static async IAsyncEnumerable GetMessagesAsync(AgentsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken)
+ public static async IAsyncEnumerable GetMessagesAsync(AgentsClient client, string threadId, ListSortOrder? messageOrder, [EnumeratorCancellation] CancellationToken cancellationToken)
{
Dictionary agentNames = []; // Cache agent names by their identifier
@@ -95,7 +97,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Agents
PageableList? messages = null;
do
{
- messages = await client.GetMessagesAsync(threadId, runId: null, limit: null, ListSortOrder.Descending, after: lastId, before: null, cancellationToken).ConfigureAwait(false);
+ messages = await client.GetMessagesAsync(threadId, runId: null, limit: null, messageOrder ?? ListSortOrder.Descending, after: lastId, before: null, cancellationToken).ConfigureAwait(false);
foreach (ThreadMessage message in messages)
{
lastId = message.Id;
diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
index a8b3a2b9f4fa..d5cdf0e6255d 100644
--- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs
+++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
@@ -73,6 +73,32 @@ public static string CreateSessionId()
#region public methods
+ ///
+ public override IAsyncEnumerable> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ // TODO: Implement the InvokeAsync method for BedrockAgent.
+ throw new NotImplementedException();
+ }
+
+ ///
+ public override IAsyncEnumerable> InvokeStreamingAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ // TODO: Implement the InvokeStreamingAsync method for BedrockAgent.
+ throw new NotImplementedException();
+ }
+
///
/// Invoke the Bedrock agent with the given message.
///
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index ed3f1ce3d2c6..ddbe0bb5ac5c 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -57,6 +57,46 @@ public ChatCompletionAgent(
///
public AuthorRole InstructionsRole { get; init; } = AuthorRole.System;
+ ///
+ public override async IAsyncEnumerable> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync(
+ message,
+ thread,
+ () => new ChatHistoryAgentThread(),
+ cancellationToken).ConfigureAwait(false);
+
+ // Invoke Chat Completion with the updated chat history.
+ var chatHistory = new ChatHistory();
+ await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
+ {
+ chatHistory.Add(existingMessage);
+ }
+ string agentName = this.GetDisplayName();
+ var invokeResults = this.InternalInvokeAsync(
+ agentName,
+ chatHistory,
+ this.MergeArguments(arguments),
+ kernel ?? this.Kernel,
+ options?.AdditionalInstructions,
+ cancellationToken);
+
+ // Notify the thread of new messages and return them to the caller.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await chatHistoryAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, chatHistoryAgentThread);
+ }
+ }
+
///
public override IAsyncEnumerable InvokeAsync(
ChatHistory history,
@@ -68,8 +108,47 @@ public override IAsyncEnumerable InvokeAsync(
return ActivityExtensions.RunWithActivityAsync(
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description),
- () => this.InternalInvokeAsync(agentName, history, arguments, kernel, cancellationToken),
+ () => this.InternalInvokeAsync(agentName, history, arguments, kernel, null, cancellationToken),
+ cancellationToken);
+ }
+
+ ///
+ public override async IAsyncEnumerable> InvokeStreamingAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync(
+ message,
+ thread,
+ () => new ChatHistoryAgentThread(),
+ cancellationToken).ConfigureAwait(false);
+
+ // Invoke Chat Completion with the updated chat history.
+ var chatHistory = new ChatHistory();
+ await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
+ {
+ chatHistory.Add(existingMessage);
+ }
+ string agentName = this.GetDisplayName();
+ var invokeResults = this.InternalInvokeStreamingAsync(
+ agentName,
+ chatHistory,
+ (newMessage) => chatHistoryAgentThread.OnNewMessageAsync(newMessage),
+ this.MergeArguments(arguments),
+ kernel ?? this.Kernel,
+ options?.AdditionalInstructions,
cancellationToken);
+
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ yield return new(result, chatHistoryAgentThread);
+ }
}
///
@@ -83,7 +162,18 @@ public override IAsyncEnumerable InvokeStreamingAsy
return ActivityExtensions.RunWithActivityAsync(
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description),
- () => this.InternalInvokeStreamingAsync(agentName, history, arguments, kernel, cancellationToken),
+ () => this.InternalInvokeStreamingAsync(
+ agentName,
+ history,
+ (newMessage) =>
+ {
+ history.Add(newMessage);
+ return Task.CompletedTask;
+ },
+ arguments,
+ kernel,
+ null,
+ cancellationToken),
cancellationToken);
}
@@ -114,6 +204,7 @@ private async Task SetupAgentChatHistoryAsync(
IReadOnlyList history,
KernelArguments? arguments,
Kernel kernel,
+ string? additionalInstructions,
CancellationToken cancellationToken)
{
ChatHistory chat = [];
@@ -125,6 +216,11 @@ private async Task SetupAgentChatHistoryAsync(
chat.Add(new ChatMessageContent(this.InstructionsRole, instructions) { AuthorName = this.Name });
}
+ if (!string.IsNullOrWhiteSpace(additionalInstructions))
+ {
+ chat.Add(new ChatMessageContent(AuthorRole.System, additionalInstructions) { AuthorName = this.Name });
+ }
+
chat.AddRange(history);
return chat;
@@ -135,6 +231,7 @@ private async IAsyncEnumerable InternalInvokeAsync(
ChatHistory history,
KernelArguments? arguments = null,
Kernel? kernel = null,
+ string? additionalInstructions = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
kernel ??= this.Kernel;
@@ -142,7 +239,7 @@ private async IAsyncEnumerable InternalInvokeAsync(
(IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments);
- ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, cancellationToken).ConfigureAwait(false);
+ ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, additionalInstructions, cancellationToken).ConfigureAwait(false);
int messageCount = chat.Count;
@@ -180,8 +277,10 @@ await chatCompletionService.GetChatMessageContentsAsync(
private async IAsyncEnumerable InternalInvokeStreamingAsync(
string agentName,
ChatHistory history,
+ Func onNewMessage,
KernelArguments? arguments = null,
Kernel? kernel = null,
+ string? additionalInstructions = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
kernel ??= this.Kernel;
@@ -189,7 +288,7 @@ private async IAsyncEnumerable InternalInvokeStream
(IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments);
- ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, cancellationToken).ConfigureAwait(false);
+ ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, additionalInstructions, cancellationToken).ConfigureAwait(false);
int messageCount = chat.Count;
@@ -226,12 +325,14 @@ private async IAsyncEnumerable InternalInvokeStream
message.AuthorName = this.Name;
+ await onNewMessage(message).ConfigureAwait(false);
history.Add(message);
}
// Do not duplicate terminated function result to history
if (role != AuthorRole.Tool)
{
+ await onNewMessage(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name }).ConfigureAwait(false);
history.Add(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name });
}
}
diff --git a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
new file mode 100644
index 000000000000..cb13ead90e99
--- /dev/null
+++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Represents a conversation thread based on an instance of that is maanged inside this class.
+///
+public class ChatHistoryAgentThread : AgentThread
+{
+ private readonly ChatHistory _chatHistory = new();
+ private string? _id = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChatHistoryAgentThread()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class that resumes an existing thread.
+ ///
+ /// An existing chat history to base this thread on.
+ /// The id of the existing thread. If not provided, a new one will be generated.
+ public ChatHistoryAgentThread(ChatHistory chatHistory, string? id = null)
+ {
+ Verify.NotNull(chatHistory);
+ this._chatHistory = chatHistory;
+ this._id = id ?? Guid.NewGuid().ToString("N");
+ }
+
+ ///
+ public override string? Id => this._id;
+
+ ///
+ public override Task CreateAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is not null)
+ {
+ return Task.FromResult(this._id);
+ }
+
+ this._id = Guid.NewGuid().ToString("N");
+
+ return Task.FromResult(this._id);
+ }
+
+ ///
+ public override Task DeleteAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ }
+
+ this._chatHistory.Clear();
+ this._id = null;
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ }
+
+ this._chatHistory.Add(newMessage);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ }
+
+ return this._chatHistory.ToAsyncEnumerable();
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
index 64749cedff69..e1cb991b643e 100644
--- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
@@ -65,13 +65,14 @@ await client.CreateMessageAsync(
///
/// The assistant client
/// The thread identifier
+ /// The order to return messages in.
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
- public static async IAsyncEnumerable GetMessagesAsync(AssistantClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken)
+ public static async IAsyncEnumerable GetMessagesAsync(AssistantClient client, string threadId, MessageCollectionOrder? messageOrder, [EnumeratorCancellation] CancellationToken cancellationToken)
{
Dictionary agentNames = []; // Cache agent names by their identifier
- await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, new() { Order = MessageCollectionOrder.Descending }, cancellationToken).ConfigureAwait(false))
+ await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, new() { Order = messageOrder ?? MessageCollectionOrder.Descending }, cancellationToken).ConfigureAwait(false))
{
string? assistantName = null;
if (!string.IsNullOrWhiteSpace(message.AssistantId) &&
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index c8d300874c60..e16d130d6e46 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -336,7 +336,7 @@ public Task AddChatMessageAsync(string threadId, ChatMessageContent message, Can
/// An asynchronous enumeration of messages.
public IAsyncEnumerable GetThreadMessagesAsync(string threadId, CancellationToken cancellationToken = default)
{
- return AssistantThreadActions.GetMessagesAsync(this.Client, threadId, cancellationToken);
+ return AssistantThreadActions.GetMessagesAsync(this.Client, threadId, null, cancellationToken);
}
///
@@ -360,6 +360,45 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul
return this.IsDeleted;
}
+ ///
+ public override async IAsyncEnumerable> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync(
+ message,
+ thread,
+ () => new OpenAIAssistantAgentThread(this.Client),
+ cancellationToken).ConfigureAwait(false);
+
+ // Create options that include the additional instructions.
+ var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new RunCreationOptions()
+ {
+ AdditionalInstructions = options?.AdditionalInstructions,
+ };
+
+ // Invoke the Agent with the thread that we already added our message to.
+ var invokeResults = this.InvokeAsync(
+ openAIAssistantAgentThread.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 openAIAssistantAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, openAIAssistantAgentThread);
+ }
+ }
+
///
/// Invokes the assistant on the specified thread.
///
@@ -417,6 +456,52 @@ async IAsyncEnumerable InternalInvokeAsync()
}
}
+ ///
+ public async override IAsyncEnumerable> InvokeStreamingAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync(
+ message,
+ thread,
+ () => new OpenAIAssistantAgentThread(this.Client),
+ cancellationToken).ConfigureAwait(false);
+
+ // Create options that include the additional instructions.
+ var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new RunCreationOptions()
+ {
+ AdditionalInstructions = options?.AdditionalInstructions,
+ };
+
+ // Invoke the Agent with the thread that we already added our message to.
+ var newMessagesReceiver = new ChatHistory();
+ var invokeResults = this.InvokeStreamingAsync(
+ openAIAssistantAgentThread.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, openAIAssistantAgentThread);
+ }
+
+ // Notify the thread of any new messages that were assembled from the streaming response.
+ foreach (var newMessage in newMessagesReceiver)
+ {
+ await openAIAssistantAgentThread.OnNewMessageAsync(newMessage, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
///
/// Invokes the assistant on the specified thread with streaming response.
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
new file mode 100644
index 000000000000..f6be22a77c91
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using OpenAI.Assistants;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Represents a conversation thread for an Open AI Assistant agent.
+///
+public class OpenAIAssistantAgentThread : AgentThread
+{
+ private readonly AssistantClient _client;
+ private string? _id = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The assistant client to use for interacting with threads.
+ public OpenAIAssistantAgentThread(AssistantClient client)
+ {
+ Verify.NotNull(client);
+
+ this._client = client;
+ }
+
+ ///
+ /// Initializes a new instance of the class that resumes an existing thread.
+ ///
+ /// The assistant client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public OpenAIAssistantAgentThread(AssistantClient client, string id)
+ {
+ Verify.NotNull(client);
+ Verify.NotNull(id);
+
+ this._client = client;
+ this._id = id;
+ }
+
+ ///
+ public override string? Id => this._id;
+
+ ///
+ public override async Task CreateAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is not null)
+ {
+ return this._id;
+ }
+
+ var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._id = assistantThreadResponse.Value.Id;
+
+ return assistantThreadResponse.Value.Id;
+ }
+
+ ///
+ public override async Task DeleteAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ }
+
+ await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
+ this._id = null;
+ }
+
+ ///
+ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ }
+
+ // If the message was generated by this agent, it is already in the thread and we shouldn't add it again.
+ if (newMessage.Metadata == null || !newMessage.Metadata.TryGetValue("ThreadId", out var messageThreadId) || !string.Equals(messageThreadId, this._id))
+ {
+ await AssistantThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._id is null)
+ {
+ throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ }
+
+ return AssistantThreadActions.GetMessagesAsync(this._client, this._id!, MessageCollectionOrder.Ascending, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
index 4b91bac74178..39534df768da 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
@@ -52,7 +52,7 @@ protected override IAsyncEnumerable InvokeStreaming
///
protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken)
{
- return AssistantThreadActions.GetMessagesAsync(this._client, this._threadId, cancellationToken);
+ return AssistantThreadActions.GetMessagesAsync(this._client, this._threadId, null, cancellationToken);
}
///
diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs
index 7f242ff510a5..b88ba37a3aa1 100644
--- a/dotnet/src/Agents/UnitTests/MockAgent.cs
+++ b/dotnet/src/Agents/UnitTests/MockAgent.cs
@@ -19,6 +19,18 @@ internal sealed class MockAgent : ChatHistoryKernelAgent
public IReadOnlyList Response { get; set; } = [];
+ public override IAsyncEnumerable> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ this.InvokeCount++;
+ return this.Response.Select(x => new AgentResponseItem(x, thread!)).ToAsyncEnumerable();
+ }
+
public override IAsyncEnumerable InvokeAsync(
ChatHistory history,
KernelArguments? arguments = null,
@@ -30,6 +42,19 @@ public override IAsyncEnumerable InvokeAsync(
return this.Response.ToAsyncEnumerable();
}
+ ///
+ public override IAsyncEnumerable> InvokeStreamingAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ this.InvokeCount++;
+ return this.Response.Select(m => new AgentResponseItem(new StreamingChatMessageContent(m.Role, m.Content), thread!)).ToAsyncEnumerable();
+ }
+
public override IAsyncEnumerable InvokeStreamingAsync(
ChatHistory history,
KernelArguments? arguments = null,
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
new file mode 100644
index 000000000000..04b0b030ff04
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+///
+/// Base class for setting up and tearing down agents, to be used in tests.
+/// Each agent type should have its own derived class.
+///
+public abstract class AgentFixture : IAsyncLifetime
+{
+ public abstract Agent Agent { get; }
+
+ public abstract AgentThread AgentThread { get; }
+
+ public abstract Task GetChatHistory();
+
+ public abstract Task DeleteThread(AgentThread thread);
+
+ public abstract Task DisposeAsync();
+
+ public abstract Task InitializeAsync();
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
new file mode 100644
index 000000000000..e944d506af2a
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Azure.Identity;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.AzureAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SemanticKernel.IntegrationTests.TestSettings;
+using AAIP = Azure.AI.Projects;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+public class AzureAIAgentFixture : AgentFixture
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ private AAIP.AgentsClient? _agentsClient;
+ private AAIP.Agent? _aiAgent;
+ private AzureAIAgent? _agent;
+ private AzureAIAgentThread? _thread;
+
+ public override Agent Agent => this._agent!;
+
+ public override AgentThread AgentThread => this._thread!;
+
+ public override async Task GetChatHistory()
+ {
+ var chatHistory = new ChatHistory();
+ await foreach (var existingMessage in this._thread!.GetMessagesAsync().ConfigureAwait(false))
+ {
+ chatHistory.Add(existingMessage);
+ }
+ return chatHistory;
+ }
+
+ public override Task DeleteThread(AgentThread thread)
+ {
+ return this._agentsClient!.DeleteThreadAsync(thread.Id);
+ }
+
+ public override async Task DisposeAsync()
+ {
+ if (this._thread!.Id is not null)
+ {
+ await this._agentsClient!.DeleteThreadAsync(this._thread!.Id);
+ }
+
+ await this._agentsClient!.DeleteAgentAsync(this._aiAgent!.Id);
+ }
+
+ public override async Task InitializeAsync()
+ {
+ AzureAIConfiguration configuration = this._configuration.GetSection("AzureAI").Get()!;
+ var client = AzureAIAgent.CreateAzureAIClient(configuration.ConnectionString, new AzureCliCredential());
+ this._agentsClient = client.GetAgentsClient();
+
+ this._aiAgent =
+ await this._agentsClient.CreateAgentAsync(
+ configuration.ChatModelId,
+ name: "HelpfulAssistant",
+ description: "Helpful Assistant",
+ instructions: "You are a helpful assistant.");
+
+ var kernelBuilder = Kernel.CreateBuilder();
+ Kernel kernel = kernelBuilder.Build();
+
+ this._agent = new AzureAIAgent(this._aiAgent, this._agentsClient) { Kernel = kernel };
+ this._thread = new AzureAIAgentThread(this._agentsClient);
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentInvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentInvokeTests.cs
new file mode 100644
index 000000000000..5ee777fe67f5
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentInvokeTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+public class AzureAIAgentInvokeTests() : InvokeTests(() => new AzureAIAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
new file mode 100644
index 000000000000..f541b6cf7907
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Azure.Identity;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SemanticKernel.IntegrationTests.TestSettings;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+///
+/// Contains setup and teardown for the tests.
+///
+public class ChatCompletionAgentFixture : AgentFixture
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ private ChatCompletionAgent? _agent;
+ private ChatHistoryAgentThread? _thread;
+
+ public override Agent Agent => this._agent!;
+
+ public override AgentThread AgentThread => this._thread!;
+
+ public override async Task GetChatHistory()
+ {
+ var chatHistory = new ChatHistory();
+ await foreach (var existingMessage in this._thread!.GetMessagesAsync().ConfigureAwait(false))
+ {
+ chatHistory.Add(existingMessage);
+ }
+ return chatHistory;
+ }
+
+ public override Task DisposeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public override Task DeleteThread(AgentThread thread)
+ {
+ return Task.CompletedTask;
+ }
+
+ public override Task InitializeAsync()
+ {
+ AzureOpenAIConfiguration configuration = this._configuration.GetSection("AzureOpenAI").Get()!;
+
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddAzureOpenAIChatCompletion(
+ deploymentName: configuration.ChatDeploymentName!,
+ endpoint: configuration.Endpoint,
+ credentials: new AzureCliCredential());
+ Kernel kernel = kernelBuilder.Build();
+
+ this._agent = new ChatCompletionAgent()
+ {
+ Kernel = kernel,
+ Instructions = "You are a helpful assistant.",
+ };
+ this._thread = new ChatHistoryAgentThread();
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentInvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentInvokeTests.cs
new file mode 100644
index 000000000000..a234260e4620
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentInvokeTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+public class ChatCompletionAgentInvokeTests() : InvokeTests(() => new ChatCompletionAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
new file mode 100644
index 000000000000..1cee519982b4
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Agents;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+///
+/// Base test class for testing the method of agents.
+/// Each agent type should have its own derived class.
+///
+public abstract class InvokeTests(Func createAgentFixture) : IAsyncLifetime
+{
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private AgentFixture _agentFixture;
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+ protected AgentFixture Fixture => this._agentFixture;
+
+ [Fact]
+ public async Task InvokeReturnsResultAsync()
+ {
+ var agent = this.Fixture.Agent;
+ var asyncResults = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "What is the capital of France."), this.Fixture.AgentThread);
+ var results = await asyncResults.ToListAsync();
+ Assert.Single(results);
+
+ var firstResult = results.First();
+ Assert.Contains("Paris", firstResult.Message.Content);
+ Assert.NotNull(firstResult.Thread);
+ }
+
+ [Fact]
+ public async Task InvokeWithoutThreadCreatesThreadAsync()
+ {
+ var agent = this.Fixture.Agent;
+ var asyncResults = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "What is the capital of France."));
+ var results = await asyncResults.ToListAsync();
+ Assert.Single(results);
+
+ var firstResult = results.First();
+ Assert.Contains("Paris", firstResult.Message.Content);
+ Assert.NotNull(firstResult.Thread);
+
+ await this.Fixture.DeleteThread(firstResult.Thread);
+ }
+
+ [Fact]
+ public async Task ConversationMaintainsHistoryAsync()
+ {
+ var q1 = "What is the capital of France.";
+ var q2 = "What is the capital of Austria.";
+
+ var agent = this.Fixture.Agent;
+ var asyncResults1 = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, q1), this.Fixture.AgentThread);
+ var result1 = await asyncResults1.FirstAsync();
+ var asyncResults2 = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, q2), result1.Thread);
+ var result2 = await asyncResults2.FirstAsync();
+
+ Assert.Contains("Paris", result1.Message.Content);
+ Assert.Contains("Austria", result2.Message.Content);
+
+ var chatHistory = await this.Fixture.GetChatHistory();
+
+ Assert.Equal(4, chatHistory.Count);
+ Assert.Equal(2, chatHistory.Count(x => x.Role == AuthorRole.User));
+ Assert.Equal(2, chatHistory.Count(x => x.Role == AuthorRole.Assistant));
+ Assert.Equal(q1, chatHistory[0].Content);
+ Assert.Equal(q2, chatHistory[2].Content);
+ Assert.Contains("Paris", chatHistory[1].Content);
+ Assert.Contains("Vienna", chatHistory[3].Content);
+ }
+
+ public Task InitializeAsync()
+ {
+ this._agentFixture = createAgentFixture();
+ return this._agentFixture.InitializeAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return this._agentFixture.DisposeAsync();
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
new file mode 100644
index 000000000000..c62d913a4b0f
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Azure.Identity;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Assistants;
+using SemanticKernel.IntegrationTests.TestSettings;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+///
+/// Contains setup and teardown for the tests.
+///
+public class OpenAIAssistantAgentFixture : AgentFixture
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ private AssistantClient? _assistantClient;
+ private Assistant? _assistant;
+ private OpenAIAssistantAgent? _agent;
+ private OpenAIAssistantAgentThread? _thread;
+
+ public override Agent Agent => this._agent!;
+
+ public override AgentThread AgentThread => this._thread!;
+
+ public override async Task GetChatHistory()
+ {
+ var chatHistory = new ChatHistory();
+ await foreach (var existingMessage in this._thread!.GetMessagesAsync().ConfigureAwait(false))
+ {
+ chatHistory.Add(existingMessage);
+ }
+ return chatHistory;
+ }
+
+ public override async Task DisposeAsync()
+ {
+ if (this._thread!.Id is not null)
+ {
+ await this._assistantClient!.DeleteThreadAsync(this._thread!.Id);
+ }
+
+ await this._assistantClient!.DeleteAssistantAsync(this._assistant!.Id);
+ }
+
+ public override Task DeleteThread(AgentThread thread)
+ {
+ return this._assistantClient!.DeleteThreadAsync(thread.Id);
+ }
+
+ public override async Task InitializeAsync()
+ {
+ AzureOpenAIConfiguration configuration = this._configuration.GetSection("AzureOpenAI").Get()!;
+ var client = OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(configuration.Endpoint));
+ this._assistantClient = client.GetAssistantClient();
+
+ this._assistant =
+ await this._assistantClient.CreateAssistantAsync(
+ configuration.ModelId,
+ name: "HelpfulAssistant",
+ instructions: "You are a helpful assistant.");
+
+ var kernelBuilder = Kernel.CreateBuilder();
+ Kernel kernel = kernelBuilder.Build();
+
+ this._agent = new OpenAIAssistantAgent(this._assistant, this._assistantClient) { Kernel = kernel };
+ this._thread = new OpenAIAssistantAgentThread(this._assistantClient);
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentInvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentInvokeTests.cs
new file mode 100644
index 000000000000..31e63181d910
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentInvokeTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
+
+public class OpenAIAssistantAgentInvokeTests() : InvokeTests(() => new OpenAIAssistantAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index 917e2447fea3..cb21d20b7f4a 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -67,6 +67,7 @@
+
diff --git a/dotnet/src/IntegrationTests/TestSettings/AzureAIConfiguration.cs b/dotnet/src/IntegrationTests/TestSettings/AzureAIConfiguration.cs
new file mode 100644
index 000000000000..0e3993fafc0b
--- /dev/null
+++ b/dotnet/src/IntegrationTests/TestSettings/AzureAIConfiguration.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SemanticKernel.IntegrationTests.TestSettings;
+
+[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated",
+ Justification = "Configuration classes are instantiated through IConfiguration.")]
+internal sealed class AzureAIConfiguration(string connectionString, string chatModelId)
+{
+ public string ConnectionString { get; set; } = connectionString;
+
+ public string ChatModelId { get; set; } = chatModelId;
+}