From f67a35fe71f9121c10461ef668a97eefb28266b0 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Wed, 19 Mar 2025 19:52:26 +0000
Subject: [PATCH 01/10] Add a common agent invoke api.
---
.../AgentInvokeResponseAsyncEnumerable.cs | 39 ++++++++
dotnet/src/Agents/Abstractions/AgentThread.cs | 52 ++++++++++
.../IAgentInvokeResponseAsyncEnumerable.cs | 17 ++++
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 52 +++++++++-
.../src/Agents/AzureAI/AzureAIAgentThread.cs | 94 +++++++++++++++++++
.../AzureAI/Internal/AgentThreadActions.cs | 3 +-
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 59 +++++++++++-
.../src/Agents/Core/ChatHistoryAgentThread.cs | 83 ++++++++++++++++
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 47 ++++++++++
.../OpenAI/OpenAIAssistantAgentThread.cs | 94 +++++++++++++++++++
10 files changed, 534 insertions(+), 6 deletions(-)
create mode 100644 dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
create mode 100644 dotnet/src/Agents/Abstractions/AgentThread.cs
create mode 100644 dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
create mode 100644 dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
create mode 100644 dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
diff --git a/dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs b/dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
new file mode 100644
index 000000000000..99b74705f7c2
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Represents a response from an agent invocation.
+///
+/// The type of data returned by the response.
+public class AgentInvokeResponseAsyncEnumerable : IAgentInvokeResponseAsyncEnumerable
+{
+ private readonly IAsyncEnumerable _asyncEnumerable;
+ private readonly AgentThread _thread;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The internal that will be combined with additional invoke specific response information.
+ /// The conversation thread associated with the response.
+ public AgentInvokeResponseAsyncEnumerable(IAsyncEnumerable asyncEnumerable, AgentThread thread)
+ {
+ Verify.NotNull(asyncEnumerable);
+ Verify.NotNull(thread);
+
+ this._asyncEnumerable = asyncEnumerable;
+ this._thread = thread;
+ }
+
+ ///
+ public AgentThread Thread => this._thread;
+
+ ///
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ return this._asyncEnumerable.GetAsyncEnumerator(cancellationToken);
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentThread.cs b/dotnet/src/Agents/Abstractions/AgentThread.cs
new file mode 100644
index 000000000000..e718cb941e8b
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentThread.cs
@@ -0,0 +1,52 @@
+// 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 a value indicating whether the thread is currently active.
+ ///
+ public abstract bool IsActive { get; }
+
+ ///
+ /// Gets the id of the current thread.
+ ///
+ public abstract string? ThreadId { get; }
+
+ ///
+ /// Starts 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 StartThreadAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Ends 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 EndThreadAsync(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/IAgentInvokeResponseAsyncEnumerable.cs b/dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
new file mode 100644
index 000000000000..ba1775289830
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Represents a response from an agent invocation.
+///
+/// The type of data returned by the response.
+public interface IAgentInvokeResponseAsyncEnumerable : IAsyncEnumerable
+{
+ ///
+ /// Gets the thread associated with this response.
+ ///
+ AgentThread Thread { get; }
+}
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 912bd83778fe..8052a9969af2 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
@@ -9,6 +11,7 @@
using Microsoft.SemanticKernel.Agents.Extensions;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
+using AAIP = Azure.AI.Projects;
namespace Microsoft.SemanticKernel.Agents.AzureAI;
@@ -139,6 +142,53 @@ public IAsyncEnumerable InvokeAsync(
return this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken);
}
+ ///
+ public async Task> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ string? additionalInstructions = null,
+ CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ if (thread is null)
+ {
+ thread = new AzureAIAgentThread(this.Client);
+ }
+
+ if (thread is not AzureAIAgentThread)
+ {
+ throw new KernelException($"{nameof(AzureAIAgent)} currently only supports agent threads of type {nameof(AzureAIAgentThread)}.");
+ }
+
+ if (!thread.IsActive)
+ {
+ await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ // Notify the thread that a new message is availble.
+ await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
+
+ // Create options that include the additional instructions.
+ var options = string.IsNullOrWhiteSpace(additionalInstructions) ? null : new AzureAIInvocationOptions()
+ {
+ AdditionalInstructions = additionalInstructions,
+ };
+
+ // Invoke the Agent with the thread that we already added our message to.
+ var invokeResults = this.InvokeAsync(thread.ThreadId!, options, arguments, kernel, cancellationToken);
+
+ // Notify the thread of any new messages returned by the agent.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
+ }
+
///
/// Invokes the assistant on the specified thread.
///
@@ -276,7 +326,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..25a95f3bc3e6
--- /dev/null
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+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 bool _isActive = false;
+ private string? _threadId = 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.
+ ///
+ /// The agents client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public AzureAIAgentThread(AgentsClient client, string threadId)
+ {
+ Verify.NotNull(client);
+ Verify.NotNull(threadId);
+
+ this._client = client;
+ this._isActive = true;
+ this._threadId = threadId;
+ }
+
+ ///
+ public override bool IsActive => this._isActive;
+
+ ///
+ public override string? ThreadId => this._threadId;
+
+ ///
+ public override async Task StartThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._isActive)
+ {
+ throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ }
+
+ var assitantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._threadId = assitantThreadResponse.Value.Id;
+ this._isActive = true;
+
+ return assitantThreadResponse.Value.Id;
+ }
+
+ ///
+ public override async Task EndThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ }
+
+ await this._client.DeleteThreadAsync(this._threadId, cancellationToken).ConfigureAwait(false);
+ this._isActive = false;
+ this._threadId = null;
+ }
+
+ ///
+ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ }
+
+ // 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._threadId))
+ {
+ await AgentThreadActions.CreateMessageAsync(this._client, this._threadId!, newMessage, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
index 167349b63d11..70cb5bffa97a 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;
}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index ed3f1ce3d2c6..ded6cbdd8f67 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -57,6 +57,49 @@ public ChatCompletionAgent(
///
public AuthorRole InstructionsRole { get; init; } = AuthorRole.System;
+ ///
+ public async Task> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ string? additionalInstructions = null,
+ CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ if (thread == null)
+ {
+ thread = new ChatHistoryAgentThread();
+ }
+
+ if (thread is not ChatHistoryAgentThread chatHistoryAgentThread)
+ {
+ throw new KernelException($"{nameof(ChatCompletionAgent)} currently only supports agent threads of type {nameof(ChatHistoryAgentThread)}.");
+ }
+
+ if (!thread.IsActive)
+ {
+ await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ // Notify the thread that a new message is availble and get the updated chat history.
+ await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
+ var chatHistory = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
+
+ // Invoke Chat Completion with the updated chat history.
+ string agentName = this.GetDisplayName();
+ var invokeResults = this.InternalInvokeAsync(agentName, chatHistory, arguments, kernel, additionalInstructions, cancellationToken);
+
+ // Notify the thread of any new messages returned by chat completion.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
+ }
+
///
public override IAsyncEnumerable InvokeAsync(
ChatHistory history,
@@ -68,7 +111,7 @@ 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);
}
@@ -83,7 +126,7 @@ 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, arguments, kernel, null, cancellationToken),
cancellationToken);
}
@@ -114,6 +157,7 @@ private async Task SetupAgentChatHistoryAsync(
IReadOnlyList history,
KernelArguments? arguments,
Kernel kernel,
+ string? additionalInstructions,
CancellationToken cancellationToken)
{
ChatHistory chat = [];
@@ -125,6 +169,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 +184,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 +192,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;
@@ -182,6 +232,7 @@ private async IAsyncEnumerable InternalInvokeStream
ChatHistory history,
KernelArguments? arguments = null,
Kernel? kernel = null,
+ string? additionalInstructions = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
kernel ??= this.Kernel;
@@ -189,7 +240,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;
diff --git a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
new file mode 100644
index 000000000000..c0c59925faa6
--- /dev/null
+++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+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 bool _isActive = false;
+ private string? _threadId = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChatHistoryAgentThread()
+ {
+ }
+
+ ///
+ public override bool IsActive => this._isActive;
+
+ ///
+ public override string? ThreadId => this._threadId;
+
+ ///
+ public override Task StartThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._isActive)
+ {
+ throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ }
+
+ this._threadId = Guid.NewGuid().ToString("N");
+ this._isActive = true;
+
+ return Task.FromResult(this._threadId);
+ }
+
+ ///
+ public override Task EndThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ }
+
+ this._chatHistory.Clear();
+ this._threadId = null;
+ this._isActive = false;
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ }
+
+ this._chatHistory.Add(newMessage);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task RetrieveCurrentChatHistoryAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("The chat history for this thread cannot be retrieved, since the thread is not currently active.");
+ }
+
+ return Task.FromResult(this._chatHistory);
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index c8d300874c60..82bb280a4d15 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -360,6 +360,53 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul
return this.IsDeleted;
}
+ ///
+ public async Task> InvokeAsync(
+ ChatMessageContent message,
+ AgentThread? thread = null,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ string? additionalInstructions = null,
+ CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(message);
+
+ if (thread is null)
+ {
+ thread = new OpenAIAssistantAgentThread(this.Client);
+ }
+
+ if (thread is not OpenAIAssistantAgentThread)
+ {
+ throw new KernelException($"{nameof(OpenAIAssistantAgent)} currently only supports agent threads of type {nameof(OpenAIAssistantAgentThread)}.");
+ }
+
+ if (!thread.IsActive)
+ {
+ await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ // Notify the thread that a new message is availble.
+ await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
+
+ // Create options that include the additional instructions.
+ var options = string.IsNullOrWhiteSpace(additionalInstructions) ? null : new RunCreationOptions()
+ {
+ AdditionalInstructions = additionalInstructions,
+ };
+
+ // Invoke the Agent with the thread that we already added our message to.
+ var invokeResults = this.InvokeAsync(thread.ThreadId!, options, arguments, kernel, cancellationToken);
+
+ // Notify the thread of any new messages returned by the agent.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
+ }
+
///
/// Invokes the assistant on the specified thread.
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
new file mode 100644
index 000000000000..336c0e53f37a
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+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 bool _isActive = false;
+ private string? _threadId = 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.
+ ///
+ /// The assistant client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public OpenAIAssistantAgentThread(AssistantClient client, string threadId)
+ {
+ Verify.NotNull(client);
+ Verify.NotNull(threadId);
+
+ this._client = client;
+ this._isActive = true;
+ this._threadId = threadId;
+ }
+
+ ///
+ public override bool IsActive => this._isActive;
+
+ ///
+ public override string? ThreadId => this._threadId;
+
+ ///
+ public override async Task StartThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._isActive)
+ {
+ throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ }
+
+ var assitantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._threadId = assitantThreadResponse.Value.Id;
+ this._isActive = true;
+
+ return assitantThreadResponse.Value.Id;
+ }
+
+ ///
+ public override async Task EndThreadAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ }
+
+ await this._client.DeleteThreadAsync(this._threadId, cancellationToken).ConfigureAwait(false);
+ this._threadId = null;
+ this._threadId = null;
+ }
+
+ ///
+ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ }
+
+ // 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._threadId))
+ {
+ await AssistantThreadActions.CreateMessageAsync(this._client, this._threadId!, newMessage, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
From f13b5703567c43efe99b2c19a2fe40905a402640 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Wed, 19 Mar 2025 19:57:37 +0000
Subject: [PATCH 02/10] Fix typos and formatting.
---
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 4 +---
dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs | 6 +++---
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 2 +-
dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +-
dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs | 6 +++---
5 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 8052a9969af2..800ae0427a1d 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
using System.Collections.Generic;
-using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
@@ -168,7 +166,7 @@ public async Task> Invok
await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
}
- // Notify the thread that a new message is availble.
+ // Notify the thread that a new message is available.
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
// Create options that include the additional instructions.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
index 25a95f3bc3e6..9fd1007b1b9a 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -57,11 +57,11 @@ public override async Task StartThreadAsync(CancellationToken cancellati
throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
}
- var assitantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- this._threadId = assitantThreadResponse.Value.Id;
+ var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._threadId = assistantThreadResponse.Value.Id;
this._isActive = true;
- return assitantThreadResponse.Value.Id;
+ return assistantThreadResponse.Value.Id;
}
///
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index ded6cbdd8f67..82b6a2368751 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -83,7 +83,7 @@ public async Task> Invok
await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
}
- // Notify the thread that a new message is availble and get the updated chat history.
+ // Notify the thread that a new message is available and get the updated chat history.
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
var chatHistory = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 82bb280a4d15..c42ceb18fecd 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -386,7 +386,7 @@ public async Task> Invok
await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
}
- // Notify the thread that a new message is availble.
+ // Notify the thread that a new message is available.
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
// Create options that include the additional instructions.
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
index 336c0e53f37a..9494ea94e9a9 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -57,11 +57,11 @@ public override async Task StartThreadAsync(CancellationToken cancellati
throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
}
- var assitantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- this._threadId = assitantThreadResponse.Value.Id;
+ var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._threadId = assistantThreadResponse.Value.Id;
this._isActive = true;
- return assitantThreadResponse.Value.Id;
+ return assistantThreadResponse.Value.Id;
}
///
From af82b818d64e096cba0f5f4f60fa8260b5ea5c60 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Wed, 19 Mar 2025 21:54:03 +0000
Subject: [PATCH 03/10] Fix ambiguous reference in xml docs
---
.../samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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)
{
From 3e0802f2a9b1c85433bdeec8a46d25cbe6722886 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Thu, 20 Mar 2025 20:13:07 +0000
Subject: [PATCH 04/10] Adding InvokeAsync signature to Agent class and
updating it with new options and new response type.
---
dotnet/src/Agents/Abstractions/Agent.cs | 21 ++++++++++
.../Agents/Abstractions/AgentInvokeOptions.cs | 15 +++++++
.../AgentInvokeResponseAsyncEnumerable.cs | 39 -------------------
.../Agents/Abstractions/AgentResponseItem.cs | 36 +++++++++++++++++
.../Agents/Abstractions/AggregatorAgent.cs | 13 +++++++
.../IAgentInvokeResponseAsyncEnumerable.cs | 17 --------
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 18 ++++-----
dotnet/src/Agents/Bedrock/BedrockAgent.cs | 13 +++++++
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 13 +++----
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 31 ++++++++++-----
dotnet/src/Agents/UnitTests/MockAgent.cs | 12 ++++++
11 files changed, 147 insertions(+), 81 deletions(-)
create mode 100644 dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs
delete mode 100644 dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
create mode 100644 dotnet/src/Agents/Abstractions/AgentResponseItem.cs
delete mode 100644 dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index 383b5df27385..1b78385e1ff0 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -43,6 +43,27 @@ 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);
+
///
/// The associated with this .
///
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/AgentInvokeResponseAsyncEnumerable.cs b/dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
deleted file mode 100644
index 99b74705f7c2..000000000000
--- a/dotnet/src/Agents/Abstractions/AgentInvokeResponseAsyncEnumerable.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-
-namespace Microsoft.SemanticKernel.Agents;
-
-///
-/// Represents a response from an agent invocation.
-///
-/// The type of data returned by the response.
-public class AgentInvokeResponseAsyncEnumerable : IAgentInvokeResponseAsyncEnumerable
-{
- private readonly IAsyncEnumerable _asyncEnumerable;
- private readonly AgentThread _thread;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The internal that will be combined with additional invoke specific response information.
- /// The conversation thread associated with the response.
- public AgentInvokeResponseAsyncEnumerable(IAsyncEnumerable asyncEnumerable, AgentThread thread)
- {
- Verify.NotNull(asyncEnumerable);
- Verify.NotNull(thread);
-
- this._asyncEnumerable = asyncEnumerable;
- this._thread = thread;
- }
-
- ///
- public AgentThread Thread => this._thread;
-
- ///
- public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
- {
- return this._asyncEnumerable.GetAsyncEnumerator(cancellationToken);
- }
-}
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/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
index 8cde6b5a9001..57e6036304c5 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
@@ -44,6 +44,19 @@ 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 corrrect approach here.
+ throw new NotImplementedException();
+ }
+
///
///
/// Different instances will never share the same channel.
diff --git a/dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs b/dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
deleted file mode 100644
index ba1775289830..000000000000
--- a/dotnet/src/Agents/Abstractions/IAgentInvokeResponseAsyncEnumerable.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-
-namespace Microsoft.SemanticKernel.Agents;
-
-///
-/// Represents a response from an agent invocation.
-///
-/// The type of data returned by the response.
-public interface IAgentInvokeResponseAsyncEnumerable : IAsyncEnumerable
-{
- ///
- /// Gets the thread associated with this response.
- ///
- AgentThread Thread { get; }
-}
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 800ae0427a1d..e0763e129e8d 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;
@@ -141,13 +142,13 @@ public IAsyncEnumerable InvokeAsync(
}
///
- public async Task> InvokeAsync(
+ public override async IAsyncEnumerable> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
- string? additionalInstructions = null,
- CancellationToken cancellationToken = default)
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Verify.NotNull(message);
@@ -170,21 +171,20 @@ public async Task> Invok
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
// Create options that include the additional instructions.
- var options = string.IsNullOrWhiteSpace(additionalInstructions) ? null : new AzureAIInvocationOptions()
+ var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new AzureAIInvocationOptions()
{
- AdditionalInstructions = additionalInstructions,
+ AdditionalInstructions = options?.AdditionalInstructions,
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.ThreadId!, options, arguments, kernel, cancellationToken);
+ var invokeResults = this.InvokeAsync(thread.ThreadId!, internalOptions, arguments, kernel, cancellationToken);
- // Notify the thread of any new messages returned by the agent.
+ // Notify the thread of new messages and return them to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
{
await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, thread);
}
-
- return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
}
///
diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
index a8b3a2b9f4fa..bcc44cadda29 100644
--- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs
+++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
@@ -73,6 +73,19 @@ 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();
+ }
+
///
/// 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 82b6a2368751..268da1c63a42 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -58,13 +58,13 @@ public ChatCompletionAgent(
public AuthorRole InstructionsRole { get; init; } = AuthorRole.System;
///
- public async Task> InvokeAsync(
+ public override async IAsyncEnumerable> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
- string? additionalInstructions = null,
- CancellationToken cancellationToken = default)
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Verify.NotNull(message);
@@ -89,15 +89,14 @@ public async Task> Invok
// Invoke Chat Completion with the updated chat history.
string agentName = this.GetDisplayName();
- var invokeResults = this.InternalInvokeAsync(agentName, chatHistory, arguments, kernel, additionalInstructions, cancellationToken);
+ var invokeResults = this.InternalInvokeAsync(agentName, chatHistory, arguments, kernel, options?.AdditionalInstructions, cancellationToken);
- // Notify the thread of any new messages returned by chat completion.
+ // Notify the thread of new messages and return them to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
{
await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, thread);
}
-
- return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
}
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index c42ceb18fecd..54d6e2b0e75f 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -361,13 +361,13 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul
}
///
- public async Task> InvokeAsync(
+ public override async IAsyncEnumerable> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
KernelArguments? arguments = null,
Kernel? kernel = null,
- string? additionalInstructions = null,
- CancellationToken cancellationToken = default)
+ AgentInvokeOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Verify.NotNull(message);
@@ -390,21 +390,23 @@ public async Task> Invok
await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
// Create options that include the additional instructions.
- var options = string.IsNullOrWhiteSpace(additionalInstructions) ? null : new RunCreationOptions()
+ var internalOptions = string.IsNullOrWhiteSpace(options?.AdditionalInstructions) ? null : new RunCreationOptions()
{
- AdditionalInstructions = additionalInstructions,
+ AdditionalInstructions = options?.AdditionalInstructions,
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.ThreadId!, options, arguments, kernel, cancellationToken);
+ var invokeResults = this.InvokeAsync(thread.ThreadId!, internalOptions, arguments, kernel, cancellationToken);
- // Notify the thread of any new messages returned by the agent.
+ // Process messages in the background.
+ var processNewMessagesTask = ProcessNewMessagesAsync(thread, invokeResults, cancellationToken);
+
+ // Notify the thread of new messages and return them to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
{
await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, thread);
}
-
- return new AgentInvokeResponseAsyncEnumerable(invokeResults, thread);
}
///
@@ -601,4 +603,15 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod
ExecutionOptions = options,
};
}
+
+ private static async Task ProcessNewMessagesAsync(AgentThread thread, IAsyncEnumerable invokeResults, CancellationToken cancellationToken)
+ {
+ // Notify the thread of any new messages returned by the agent.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ }
+
+ return thread;
+ }
}
diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs
index 7f242ff510a5..8a580cd376b4 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,
From b5448798a26082c6f898d7777f18b9190ade3df5 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 08:56:03 +0000
Subject: [PATCH 05/10] Naming update related to PR feedback
---
dotnet/src/Agents/Abstractions/AgentThread.cs | 6 ++--
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 4 +--
.../src/Agents/AzureAI/AzureAIAgentThread.cs | 28 ++++++++---------
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 2 +-
.../src/Agents/Core/ChatHistoryAgentThread.cs | 27 ++++++++++++-----
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 +--
.../OpenAI/OpenAIAssistantAgentThread.cs | 30 +++++++++----------
7 files changed, 57 insertions(+), 44 deletions(-)
diff --git a/dotnet/src/Agents/Abstractions/AgentThread.cs b/dotnet/src/Agents/Abstractions/AgentThread.cs
index e718cb941e8b..c700642de18a 100644
--- a/dotnet/src/Agents/Abstractions/AgentThread.cs
+++ b/dotnet/src/Agents/Abstractions/AgentThread.cs
@@ -23,21 +23,21 @@ public abstract class AgentThread
///
/// Gets the id of the current thread.
///
- public abstract string? ThreadId { get; }
+ public abstract string? Id { get; }
///
/// Starts 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 StartThreadAsync(CancellationToken cancellationToken = default);
+ public abstract Task StartAsync(CancellationToken cancellationToken = default);
///
/// Ends 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 EndThreadAsync(CancellationToken cancellationToken = default);
+ public abstract Task EndAsync(CancellationToken cancellationToken = default);
///
/// This method is called when a new message has been contributed to the chat by any participant.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index e0763e129e8d..d7a1b4494406 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -164,7 +164,7 @@ public override async IAsyncEnumerable> In
if (!thread.IsActive)
{
- await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ await thread.StartAsync(cancellationToken).ConfigureAwait(false);
}
// Notify the thread that a new message is available.
@@ -177,7 +177,7 @@ public override async IAsyncEnumerable> In
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.ThreadId!, internalOptions, arguments, kernel, cancellationToken);
+ var invokeResults = this.InvokeAsync(thread.Id!, internalOptions, arguments, kernel, cancellationToken);
// Notify the thread of new messages and return them to the caller.
await foreach (var result in invokeResults.ConfigureAwait(false))
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
index 9fd1007b1b9a..cf3e805b77e5 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -15,7 +15,7 @@ public class AzureAIAgentThread : AgentThread
{
private readonly AgentsClient _client;
private bool _isActive = false;
- private string? _threadId = null;
+ private string? _id = null;
///
/// Initializes a new instance of the class.
@@ -29,28 +29,28 @@ public AzureAIAgentThread(AgentsClient client)
}
///
- /// Initializes a new instance of the class.
+ /// 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 threadId)
+ /// The ID of an existing thread to resume.
+ public AzureAIAgentThread(AgentsClient client, string id)
{
Verify.NotNull(client);
- Verify.NotNull(threadId);
+ Verify.NotNull(id);
this._client = client;
this._isActive = true;
- this._threadId = threadId;
+ this._id = id;
}
///
public override bool IsActive => this._isActive;
///
- public override string? ThreadId => this._threadId;
+ public override string? Id => this._id;
///
- public override async Task StartThreadAsync(CancellationToken cancellationToken = default)
+ public override async Task StartAsync(CancellationToken cancellationToken = default)
{
if (this._isActive)
{
@@ -58,23 +58,23 @@ public override async Task StartThreadAsync(CancellationToken cancellati
}
var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- this._threadId = assistantThreadResponse.Value.Id;
+ this._id = assistantThreadResponse.Value.Id;
this._isActive = true;
return assistantThreadResponse.Value.Id;
}
///
- public override async Task EndThreadAsync(CancellationToken cancellationToken = default)
+ public override async Task EndAsync(CancellationToken cancellationToken = default)
{
if (!this._isActive)
{
throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
}
- await this._client.DeleteThreadAsync(this._threadId, cancellationToken).ConfigureAwait(false);
+ await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
this._isActive = false;
- this._threadId = null;
+ this._id = null;
}
///
@@ -86,9 +86,9 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
}
// 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._threadId))
+ if (newMessage.Metadata == null || !newMessage.Metadata.TryGetValue("ThreadId", out var messageThreadId) || !string.Equals(messageThreadId, this._id))
{
- await AgentThreadActions.CreateMessageAsync(this._client, this._threadId!, newMessage, cancellationToken).ConfigureAwait(false);
+ await AgentThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
}
}
}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 268da1c63a42..ad27055d45a9 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -80,7 +80,7 @@ public override async IAsyncEnumerable> In
if (!thread.IsActive)
{
- await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ await thread.StartAsync(cancellationToken).ConfigureAwait(false);
}
// Notify the thread that a new message is available and get the updated chat history.
diff --git a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
index c0c59925faa6..bfd8b94e0695 100644
--- a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
+++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
@@ -14,7 +14,7 @@ public class ChatHistoryAgentThread : AgentThread
{
private readonly ChatHistory _chatHistory = new();
private bool _isActive = false;
- private string? _threadId = null;
+ private string? _id = null;
///
/// Initializes a new instance of the class.
@@ -23,28 +23,41 @@ 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._isActive = true;
+ this._id = id ?? Guid.NewGuid().ToString("N");
+ }
+
///
public override bool IsActive => this._isActive;
///
- public override string? ThreadId => this._threadId;
+ public override string? Id => this._id;
///
- public override Task StartThreadAsync(CancellationToken cancellationToken = default)
+ public override Task StartAsync(CancellationToken cancellationToken = default)
{
if (this._isActive)
{
throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
}
- this._threadId = Guid.NewGuid().ToString("N");
+ this._id = Guid.NewGuid().ToString("N");
this._isActive = true;
- return Task.FromResult(this._threadId);
+ return Task.FromResult(this._id);
}
///
- public override Task EndThreadAsync(CancellationToken cancellationToken = default)
+ public override Task EndAsync(CancellationToken cancellationToken = default)
{
if (!this._isActive)
{
@@ -52,7 +65,7 @@ public override Task EndThreadAsync(CancellationToken cancellationToken = defaul
}
this._chatHistory.Clear();
- this._threadId = null;
+ this._id = null;
this._isActive = false;
return Task.CompletedTask;
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 54d6e2b0e75f..cb67870d5064 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -383,7 +383,7 @@ public override async IAsyncEnumerable> In
if (!thread.IsActive)
{
- await thread.StartThreadAsync(cancellationToken).ConfigureAwait(false);
+ await thread.StartAsync(cancellationToken).ConfigureAwait(false);
}
// Notify the thread that a new message is available.
@@ -396,7 +396,7 @@ public override async IAsyncEnumerable> In
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.ThreadId!, internalOptions, arguments, kernel, cancellationToken);
+ var invokeResults = this.InvokeAsync(thread.Id!, internalOptions, arguments, kernel, cancellationToken);
// Process messages in the background.
var processNewMessagesTask = ProcessNewMessagesAsync(thread, invokeResults, cancellationToken);
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
index 9494ea94e9a9..18a9af0149a5 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -15,7 +15,7 @@ public class OpenAIAssistantAgentThread : AgentThread
{
private readonly AssistantClient _client;
private bool _isActive = false;
- private string? _threadId = null;
+ private string? _id = null;
///
/// Initializes a new instance of the class.
@@ -29,28 +29,28 @@ public OpenAIAssistantAgentThread(AssistantClient client)
}
///
- /// Initializes a new instance of the class.
+ /// 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 threadId)
+ /// The ID of an existing thread to resume.
+ public OpenAIAssistantAgentThread(AssistantClient client, string id)
{
Verify.NotNull(client);
- Verify.NotNull(threadId);
+ Verify.NotNull(id);
this._client = client;
this._isActive = true;
- this._threadId = threadId;
+ this._id = id;
}
///
public override bool IsActive => this._isActive;
///
- public override string? ThreadId => this._threadId;
+ public override string? Id => this._id;
///
- public override async Task StartThreadAsync(CancellationToken cancellationToken = default)
+ public override async Task StartAsync(CancellationToken cancellationToken = default)
{
if (this._isActive)
{
@@ -58,23 +58,23 @@ public override async Task StartThreadAsync(CancellationToken cancellati
}
var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- this._threadId = assistantThreadResponse.Value.Id;
+ this._id = assistantThreadResponse.Value.Id;
this._isActive = true;
return assistantThreadResponse.Value.Id;
}
///
- public override async Task EndThreadAsync(CancellationToken cancellationToken = default)
+ public override async Task EndAsync(CancellationToken cancellationToken = default)
{
if (!this._isActive)
{
throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
}
- await this._client.DeleteThreadAsync(this._threadId, cancellationToken).ConfigureAwait(false);
- this._threadId = null;
- this._threadId = null;
+ await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
+ this._id = null;
+ this._id = null;
}
///
@@ -86,9 +86,9 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
}
// 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._threadId))
+ if (newMessage.Metadata == null || !newMessage.Metadata.TryGetValue("ThreadId", out var messageThreadId) || !string.Equals(messageThreadId, this._id))
{
- await AssistantThreadActions.CreateMessageAsync(this._client, this._threadId!, newMessage, cancellationToken).ConfigureAwait(false);
+ await AssistantThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
}
}
}
From cc99661685d8395e106b2e45e19324b671e3e255 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 11:13:37 +0000
Subject: [PATCH 06/10] Add streaming method and implementations Fix kernel and
arguments override capability Add helper for common thread validation
---
dotnet/src/Agents/Abstractions/Agent.cs | 60 ++++++++++++
.../Agents/Abstractions/AggregatorAgent.cs | 13 +++
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 79 ++++++++++++----
dotnet/src/Agents/Bedrock/BedrockAgent.cs | 13 +++
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 87 ++++++++++++-----
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 93 ++++++++++++-------
dotnet/src/Agents/UnitTests/MockAgent.cs | 13 +++
7 files changed, 282 insertions(+), 76 deletions(-)
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index 1b78385e1ff0..6d99f2396811 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -64,6 +64,27 @@ public abstract IAsyncEnumerable> InvokeAs
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 .
///
@@ -117,4 +138,43 @@ public abstract IAsyncEnumerable> InvokeAs
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 type of the agent.
+ /// 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)}.");
+ }
+
+ if (!thread.IsActive)
+ {
+ await thread.StartAsync(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/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
index 57e6036304c5..a081d54a1c36 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
@@ -57,6 +57,19 @@ public override IAsyncEnumerable> InvokeAs
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 corrrect 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 d7a1b4494406..b27edd358c43 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -152,23 +152,11 @@ public override async IAsyncEnumerable> In
{
Verify.NotNull(message);
- if (thread is null)
- {
- thread = new AzureAIAgentThread(this.Client);
- }
-
- if (thread is not AzureAIAgentThread)
- {
- throw new KernelException($"{nameof(AzureAIAgent)} currently only supports agent threads of type {nameof(AzureAIAgentThread)}.");
- }
-
- if (!thread.IsActive)
- {
- await thread.StartAsync(cancellationToken).ConfigureAwait(false);
- }
-
- // Notify the thread that a new message is available.
- await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
+ 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()
@@ -177,13 +165,18 @@ public override async IAsyncEnumerable> In
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.Id!, internalOptions, arguments, kernel, cancellationToken);
+ 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 thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
- yield return new(result, thread);
+ await azureAIAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, azureAIAgentThread);
}
}
@@ -226,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.
///
diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
index bcc44cadda29..d5cdf0e6255d 100644
--- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs
+++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
@@ -86,6 +86,19 @@ public override IAsyncEnumerable> InvokeAs
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 ad27055d45a9..2ad0051c19bb 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -68,34 +68,28 @@ public override async IAsyncEnumerable> In
{
Verify.NotNull(message);
- if (thread == null)
- {
- thread = new ChatHistoryAgentThread();
- }
-
- if (thread is not ChatHistoryAgentThread chatHistoryAgentThread)
- {
- throw new KernelException($"{nameof(ChatCompletionAgent)} currently only supports agent threads of type {nameof(ChatHistoryAgentThread)}.");
- }
-
- if (!thread.IsActive)
- {
- await thread.StartAsync(cancellationToken).ConfigureAwait(false);
- }
-
- // Notify the thread that a new message is available and get the updated chat history.
- await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
- var chatHistory = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
+ var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync(
+ message,
+ thread,
+ () => new ChatHistoryAgentThread(),
+ cancellationToken).ConfigureAwait(false);
// Invoke Chat Completion with the updated chat history.
+ var chatHistory = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
string agentName = this.GetDisplayName();
- var invokeResults = this.InternalInvokeAsync(agentName, chatHistory, arguments, kernel, options?.AdditionalInstructions, cancellationToken);
+ 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 thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
- yield return new(result, thread);
+ await chatHistoryAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, chatHistoryAgentThread);
}
}
@@ -114,6 +108,41 @@ public override IAsyncEnumerable InvokeAsync(
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 = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
+ 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);
+ }
+ }
+
///
public override IAsyncEnumerable InvokeStreamingAsync(
ChatHistory history,
@@ -125,7 +154,18 @@ public override IAsyncEnumerable InvokeStreamingAsy
return ActivityExtensions.RunWithActivityAsync(
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description),
- () => this.InternalInvokeStreamingAsync(agentName, history, arguments, kernel, null, cancellationToken),
+ () => this.InternalInvokeStreamingAsync(
+ agentName,
+ history,
+ (newMessage) =>
+ {
+ history.Add(newMessage);
+ return Task.CompletedTask;
+ },
+ arguments,
+ kernel,
+ null,
+ cancellationToken),
cancellationToken);
}
@@ -229,6 +269,7 @@ await chatCompletionService.GetChatMessageContentsAsync(
private async IAsyncEnumerable InternalInvokeStreamingAsync(
string agentName,
ChatHistory history,
+ Func onNewMessage,
KernelArguments? arguments = null,
Kernel? kernel = null,
string? additionalInstructions = null,
@@ -276,12 +317,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/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index cb67870d5064..45852e9ff6db 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -371,23 +371,11 @@ public override async IAsyncEnumerable> In
{
Verify.NotNull(message);
- if (thread is null)
- {
- thread = new OpenAIAssistantAgentThread(this.Client);
- }
-
- if (thread is not OpenAIAssistantAgentThread)
- {
- throw new KernelException($"{nameof(OpenAIAssistantAgent)} currently only supports agent threads of type {nameof(OpenAIAssistantAgentThread)}.");
- }
-
- if (!thread.IsActive)
- {
- await thread.StartAsync(cancellationToken).ConfigureAwait(false);
- }
-
- // Notify the thread that a new message is available.
- await thread.OnNewMessageAsync(message, cancellationToken).ConfigureAwait(false);
+ 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()
@@ -396,16 +384,18 @@ public override async IAsyncEnumerable> In
};
// Invoke the Agent with the thread that we already added our message to.
- var invokeResults = this.InvokeAsync(thread.Id!, internalOptions, arguments, kernel, cancellationToken);
-
- // Process messages in the background.
- var processNewMessagesTask = ProcessNewMessagesAsync(thread, invokeResults, cancellationToken);
+ 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 thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
- yield return new(result, thread);
+ await openAIAssistantAgentThread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, openAIAssistantAgentThread);
}
}
@@ -466,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.
///
@@ -603,15 +639,4 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod
ExecutionOptions = options,
};
}
-
- private static async Task ProcessNewMessagesAsync(AgentThread thread, IAsyncEnumerable invokeResults, CancellationToken cancellationToken)
- {
- // Notify the thread of any new messages returned by the agent.
- await foreach (var result in invokeResults.ConfigureAwait(false))
- {
- await thread.OnNewMessageAsync(result, cancellationToken).ConfigureAwait(false);
- }
-
- return thread;
- }
}
diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs
index 8a580cd376b4..b88ba37a3aa1 100644
--- a/dotnet/src/Agents/UnitTests/MockAgent.cs
+++ b/dotnet/src/Agents/UnitTests/MockAgent.cs
@@ -42,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,
From 68b5b60bbd4c00c0d0ac8e18084a83e1f8a1f358 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 16:04:07 +0000
Subject: [PATCH 07/10] Add integration tests for ChatCompletion and
AzureOpenAIAssistant
---
dotnet/src/Agents/Abstractions/Agent.cs | 1 -
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 2 +-
.../src/Agents/AzureAI/AzureAIAgentThread.cs | 12 +++
dotnet/src/Agents/AzureAI/AzureAIChannel.cs | 2 +-
.../AzureAI/Internal/AgentThreadActions.cs | 5 +-
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 12 ++-
.../src/Agents/Core/ChatHistoryAgentThread.cs | 6 +-
.../OpenAI/Internal/AssistantThreadActions.cs | 5 +-
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +-
.../OpenAI/OpenAIAssistantAgentThread.cs | 14 ++-
.../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +-
.../AgentFixture.cs | 27 ++++++
.../ChatCompletionAgentFixture.cs | 72 +++++++++++++++
.../ChatCompletionAgentInvokeTests.cs | 7 ++
.../CommonInterfaceConformance/InvokeTests.cs | 89 +++++++++++++++++++
.../OpenAIAssistantAgentFixture.cs | 80 +++++++++++++++++
.../OpenAIAssistantAgentInvokeTests.cs | 7 ++
17 files changed, 331 insertions(+), 14 deletions(-)
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentInvokeTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentInvokeTests.cs
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index 6d99f2396811..d5f8c3f631f2 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -143,7 +143,6 @@ public abstract IAsyncEnumerable>
/// 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 type of the agent.
/// 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.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index b27edd358c43..0ee773037885 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -118,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);
}
///
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
index cf3e805b77e5..cfde3d6da8ea 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
@@ -91,4 +92,15 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
await AgentThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
}
}
+
+ ///
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread is not currently active.");
+ }
+
+ 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 70cb5bffa97a..9eb6495b68f8 100644
--- a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
+++ b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
@@ -86,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
@@ -96,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/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 2ad0051c19bb..ddbe0bb5ac5c 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -75,7 +75,11 @@ public override async IAsyncEnumerable> In
cancellationToken).ConfigureAwait(false);
// Invoke Chat Completion with the updated chat history.
- var chatHistory = await chatHistoryAgentThread.RetrieveCurrentChatHistoryAsync(cancellationToken).ConfigureAwait(false);
+ 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,
@@ -126,7 +130,11 @@ public override async IAsyncEnumerable
- public Task RetrieveCurrentChatHistoryAsync(CancellationToken cancellationToken = default)
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
{
if (!this._isActive)
{
throw new InvalidOperationException("The chat history for this thread cannot be retrieved, since the thread is not currently active.");
}
- return Task.FromResult(this._chatHistory);
+ 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 45852e9ff6db..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);
}
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
index 18a9af0149a5..67c46a616c12 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -1,6 +1,7 @@
// 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;
@@ -73,7 +74,7 @@ public override async Task EndAsync(CancellationToken cancellationToken = defaul
}
await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
- this._id = null;
+ this._isActive = false;
this._id = null;
}
@@ -91,4 +92,15 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
await AssistantThreadActions.CreateMessageAsync(this._client, this._id!, newMessage, cancellationToken).ConfigureAwait(false);
}
}
+
+ ///
+ public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (!this._isActive)
+ {
+ throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread is not currently active.");
+ }
+
+ 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/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/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..ba143e72cb4b
--- /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!.IsActive)
+ {
+ 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())
+{
+}
From 04a6f76d7eeb1bdf974c1b6a5461f2ad1aedc93b Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 17:17:01 +0000
Subject: [PATCH 08/10] Add integration tests for Azure AI Agent.
---
.../AzureAIAgentFixture.cs | 77 +++++++++++++++++++
.../AzureAIAgentInvokeTests.cs | 7 ++
.../IntegrationTests/IntegrationTests.csproj | 1 +
.../TestSettings/AzureAIConfiguration.cs | 14 ++++
4 files changed, 99 insertions(+)
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentInvokeTests.cs
create mode 100644 dotnet/src/IntegrationTests/TestSettings/AzureAIConfiguration.cs
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
new file mode 100644
index 000000000000..7680918f521e
--- /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!.IsActive)
+ {
+ 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/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index 9f4e835df255..a5ea9e436469 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -68,6 +68,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;
+}
From baba29a1febd4b6bc554b465105e0831bbbb734b Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 19:14:00 +0000
Subject: [PATCH 09/10] Remove IsActive and rename Start and End
---
dotnet/src/Agents/Abstractions/Agent.cs | 5 +---
dotnet/src/Agents/Abstractions/AgentThread.cs | 13 +++------
.../src/Agents/AzureAI/AzureAIAgentThread.cs | 27 +++++++------------
.../src/Agents/Core/ChatHistoryAgentThread.cs | 27 +++++++------------
.../OpenAI/OpenAIAssistantAgentThread.cs | 27 +++++++------------
.../AzureAIAgentFixture.cs | 2 +-
.../OpenAIAssistantAgentFixture.cs | 2 +-
7 files changed, 37 insertions(+), 66 deletions(-)
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index d5f8c3f631f2..a742d7ce6767 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -166,10 +166,7 @@ protected async Task EnsureThreadExistsWithMessageAsync
public abstract class AgentThread
{
- ///
- /// Gets a value indicating whether the thread is currently active.
- ///
- public abstract bool IsActive { get; }
-
///
/// Gets the id of the current thread.
///
public abstract string? Id { get; }
///
- /// Starts the thread and returns the thread id.
+ /// 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 StartAsync(CancellationToken cancellationToken = default);
+ public abstract Task CreateAsync(CancellationToken cancellationToken = default);
///
- /// Ends the current thread.
+ /// 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 EndAsync(CancellationToken cancellationToken = default);
+ public abstract Task DeleteAsync(CancellationToken cancellationToken = default);
///
/// This method is called when a new message has been contributed to the chat by any participant.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
index cfde3d6da8ea..c5c0090ea170 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentThread.cs
@@ -15,7 +15,6 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI;
public class AzureAIAgentThread : AgentThread
{
private readonly AgentsClient _client;
- private bool _isActive = false;
private string? _id = null;
///
@@ -40,50 +39,44 @@ public AzureAIAgentThread(AgentsClient client, string id)
Verify.NotNull(id);
this._client = client;
- this._isActive = true;
this._id = id;
}
- ///
- public override bool IsActive => this._isActive;
-
///
public override string? Id => this._id;
///
- public override async Task StartAsync(CancellationToken cancellationToken = default)
+ public override async Task CreateAsync(CancellationToken cancellationToken = default)
{
- if (this._isActive)
+ if (this._id is not null)
{
- throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ return this._id;
}
var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
this._id = assistantThreadResponse.Value.Id;
- this._isActive = true;
return assistantThreadResponse.Value.Id;
}
///
- public override async Task EndAsync(CancellationToken cancellationToken = default)
+ public override async Task DeleteAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
}
await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
- this._isActive = false;
this._id = null;
}
///
public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ 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.
@@ -96,9 +89,9 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
///
public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread is not currently active.");
+ 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/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
index 6cea56034d17..cb13ead90e99 100644
--- a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
+++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
@@ -15,7 +15,6 @@ namespace Microsoft.SemanticKernel.Agents;
public class ChatHistoryAgentThread : AgentThread
{
private readonly ChatHistory _chatHistory = new();
- private bool _isActive = false;
private string? _id = null;
///
@@ -34,41 +33,35 @@ public ChatHistoryAgentThread(ChatHistory chatHistory, string? id = null)
{
Verify.NotNull(chatHistory);
this._chatHistory = chatHistory;
- this._isActive = true;
this._id = id ?? Guid.NewGuid().ToString("N");
}
- ///
- public override bool IsActive => this._isActive;
-
///
public override string? Id => this._id;
///
- public override Task StartAsync(CancellationToken cancellationToken = default)
+ public override Task CreateAsync(CancellationToken cancellationToken = default)
{
- if (this._isActive)
+ if (this._id is not null)
{
- throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ return Task.FromResult(this._id);
}
this._id = Guid.NewGuid().ToString("N");
- this._isActive = true;
return Task.FromResult(this._id);
}
///
- public override Task EndAsync(CancellationToken cancellationToken = default)
+ public override Task DeleteAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
}
this._chatHistory.Clear();
this._id = null;
- this._isActive = false;
return Task.CompletedTask;
}
@@ -76,9 +69,9 @@ public override Task EndAsync(CancellationToken cancellationToken = default)
///
public override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
}
this._chatHistory.Add(newMessage);
@@ -88,9 +81,9 @@ public override Task OnNewMessageAsync(ChatMessageContent newMessage, Cancellati
///
public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("The chat history for this thread cannot be retrieved, since the thread is not currently active.");
+ 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/OpenAIAssistantAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
index 67c46a616c12..f6be22a77c91 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentThread.cs
@@ -15,7 +15,6 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI;
public class OpenAIAssistantAgentThread : AgentThread
{
private readonly AssistantClient _client;
- private bool _isActive = false;
private string? _id = null;
///
@@ -40,50 +39,44 @@ public OpenAIAssistantAgentThread(AssistantClient client, string id)
Verify.NotNull(id);
this._client = client;
- this._isActive = true;
this._id = id;
}
- ///
- public override bool IsActive => this._isActive;
-
///
public override string? Id => this._id;
///
- public override async Task StartAsync(CancellationToken cancellationToken = default)
+ public override async Task CreateAsync(CancellationToken cancellationToken = default)
{
- if (this._isActive)
+ if (this._id is not null)
{
- throw new InvalidOperationException("You cannot start this thread, since the thread is already active.");
+ return this._id;
}
var assistantThreadResponse = await this._client.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
this._id = assistantThreadResponse.Value.Id;
- this._isActive = true;
return assistantThreadResponse.Value.Id;
}
///
- public override async Task EndAsync(CancellationToken cancellationToken = default)
+ public override async Task DeleteAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since the thread is not currently active.");
+ throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
}
await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
- this._isActive = false;
this._id = null;
}
///
public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread is not currently active.");
+ 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.
@@ -96,9 +89,9 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
///
public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
{
- if (!this._isActive)
+ if (this._id is null)
{
- throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread is not currently active.");
+ 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/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
index 7680918f521e..e944d506af2a 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
@@ -47,7 +47,7 @@ public override Task DeleteThread(AgentThread thread)
public override async Task DisposeAsync()
{
- if (this._thread!.IsActive)
+ if (this._thread!.Id is not null)
{
await this._agentsClient!.DeleteThreadAsync(this._thread!.Id);
}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
index ba143e72cb4b..c62d913a4b0f 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
@@ -46,7 +46,7 @@ public override async Task GetChatHistory()
public override async Task DisposeAsync()
{
- if (this._thread!.IsActive)
+ if (this._thread!.Id is not null)
{
await this._assistantClient!.DeleteThreadAsync(this._thread!.Id);
}
From 0c2217ee7176f1ed28121ba1a6df83af95649eee Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 21 Mar 2025 19:20:47 +0000
Subject: [PATCH 10/10] Fix spelling error.
---
dotnet/src/Agents/Abstractions/AggregatorAgent.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
index a081d54a1c36..e71b7ff03f1a 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
@@ -53,7 +53,7 @@ public override IAsyncEnumerable> InvokeAs
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
- // TODO: Need to determine the corrrect approach here.
+ // TODO: Need to determine the correct approach here.
throw new NotImplementedException();
}
@@ -66,7 +66,7 @@ public override IAsyncEnumerable>
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
- // TODO: Need to determine the corrrect approach here.
+ // TODO: Need to determine the correct approach here.
throw new NotImplementedException();
}