diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs index a742d7ce6767..1dc1461f122f 100644 --- a/dotnet/src/Agents/Abstractions/Agent.cs +++ b/dotnet/src/Agents/Abstractions/Agent.cs @@ -48,19 +48,35 @@ public abstract class Agent /// /// 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( + public virtual IAsyncEnumerable> InvokeAsync( ChatMessageContent message, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, + AgentInvokeOptions? options = null, + CancellationToken cancellationToken = default) + { + return this.InvokeAsync(new[] { message }, thread, options, cancellationToken); + } + + /// + /// Invoke the agent with the provided message and arguments. + /// + /// The messages to pass to the agent. + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// 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( + ICollection messages, + AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default); @@ -69,19 +85,35 @@ public abstract IAsyncEnumerable> InvokeAs /// /// 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( + public virtual IAsyncEnumerable> InvokeStreamingAsync( ChatMessageContent message, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, + AgentInvokeOptions? options = null, + CancellationToken cancellationToken = default) + { + return this.InvokeStreamingAsync(new[] { message }, thread, options, cancellationToken); + } + + /// + /// Invoke the agent with the provided message and arguments. + /// + /// The messages to pass to the agent. + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// 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( + ICollection messages, + AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default); @@ -143,14 +175,14 @@ 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 message to add to the thread once it is setup. + /// The messages 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, + ICollection messages, AgentThread? thread, Func constructThread, CancellationToken cancellationToken) @@ -168,8 +200,11 @@ protected async Task EnsureThreadExistsWithMessageAsync public class AgentInvokeOptions { + /// + /// Gets or sets optional arguments to pass to the agent's invocation, including any + /// + public KernelArguments? KernelArguments { get; init; } = null; + + /// + /// Gets or sets the containing services, plugins, and other state for use by the agent + /// + public Kernel? Kernel { get; init; } = null; + /// /// 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. diff --git a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs index e71b7ff03f1a..d37cfd0590dc 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs @@ -46,10 +46,8 @@ public sealed class AggregatorAgent(Func chatProvider) : Agent /// public override IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { @@ -59,10 +57,8 @@ public override IAsyncEnumerable> InvokeAs /// public override IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs index 0ee773037885..548fa1bea0f0 100644 --- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs +++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs @@ -143,17 +143,15 @@ public IAsyncEnumerable InvokeAsync( /// public override async IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new AzureAIAgentThread(this.Client), cancellationToken).ConfigureAwait(false); @@ -168,8 +166,8 @@ public override async IAsyncEnumerable> In var invokeResults = this.InvokeAsync( azureAIAgentThread.Id!, internalOptions, - this.MergeArguments(arguments), - kernel ?? this.Kernel, + this.MergeArguments(options?.KernelArguments), + options?.Kernel ?? this.Kernel, cancellationToken); // Notify the thread of new messages and return them to the caller. @@ -221,17 +219,15 @@ async IAsyncEnumerable InternalInvokeAsync() /// public async override IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new AzureAIAgentThread(this.Client), cancellationToken).ConfigureAwait(false); @@ -247,8 +243,8 @@ public async override IAsyncEnumerable /// Represents a conversation thread for an Azure AI agent. /// -public class AzureAIAgentThread : AgentThread +public sealed class AzureAIAgentThread : AgentThread { private readonly AgentsClient _client; private string? _id = null; + private bool _isDeleted = false; /// /// Initializes a new instance of the class. @@ -48,6 +51,11 @@ public AzureAIAgentThread(AgentsClient client, string id) /// public override async Task CreateAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be recreated."); + } + if (this._id is not null) { return this._id; @@ -62,21 +70,39 @@ public override async Task CreateAsync(CancellationToken cancellationTok /// public override async Task DeleteAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + return; + } + if (this._id is null) { - throw new InvalidOperationException("This thread cannot be ended, since it has not been started."); + throw new InvalidOperationException("This thread cannot be deleted, since it has not been created."); } - await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false); - this._id = null; + try + { + await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Do nothing, since the thread was already deleted. + } + + this._isDeleted = true; } /// public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } // If the message was generated by this agent, it is already in the thread and we shouldn't add it again. @@ -87,13 +113,21 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc } /// - public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } - return AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken); + await foreach (var message in AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken).ConfigureAwait(false)) + { + yield return message; + } } } diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs index d5cdf0e6255d..a3762f00bcb1 100644 --- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs +++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs @@ -75,10 +75,8 @@ public static string CreateSessionId() /// public override IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { @@ -88,10 +86,8 @@ public override IAsyncEnumerable> InvokeAs /// public override IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index ddbe0bb5ac5c..271770646632 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -59,17 +59,15 @@ public ChatCompletionAgent( /// public override async IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new ChatHistoryAgentThread(), cancellationToken).ConfigureAwait(false); @@ -84,8 +82,8 @@ public override async IAsyncEnumerable> In var invokeResults = this.InternalInvokeAsync( agentName, chatHistory, - this.MergeArguments(arguments), - kernel ?? this.Kernel, + this.MergeArguments(options?.KernelArguments), + options?.Kernel ?? this.Kernel, options?.AdditionalInstructions, cancellationToken); @@ -114,17 +112,15 @@ public override IAsyncEnumerable InvokeAsync( /// public override async IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new ChatHistoryAgentThread(), cancellationToken).ConfigureAwait(false); @@ -140,8 +136,8 @@ public override async IAsyncEnumerable chatHistoryAgentThread.OnNewMessageAsync(newMessage), - this.MergeArguments(arguments), - kernel ?? this.Kernel, + this.MergeArguments(options?.KernelArguments), + options?.Kernel ?? this.Kernel, options?.AdditionalInstructions, cancellationToken); diff --git a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs index cb13ead90e99..889cae3b1f92 100644 --- a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs +++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; @@ -12,10 +12,11 @@ namespace Microsoft.SemanticKernel.Agents; /// /// Represents a conversation thread based on an instance of that is maanged inside this class. /// -public class ChatHistoryAgentThread : AgentThread +public sealed class ChatHistoryAgentThread : AgentThread { private readonly ChatHistory _chatHistory = new(); private string? _id = null; + private bool _isDeleted = false; /// /// Initializes a new instance of the class. @@ -42,6 +43,11 @@ public ChatHistoryAgentThread(ChatHistory chatHistory, string? id = null) /// public override Task CreateAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be recreated."); + } + if (this._id is not null) { return Task.FromResult(this._id); @@ -55,37 +61,54 @@ public override Task CreateAsync(CancellationToken cancellationToken = d /// public override Task DeleteAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + return Task.CompletedTask; + } + if (this._id is null) { - throw new InvalidOperationException("This thread cannot be ended, since it has not been started."); + throw new InvalidOperationException("This thread cannot be deleted, since it has not been created."); } this._chatHistory.Clear(); - this._id = null; + this._isDeleted = true; return Task.CompletedTask; } /// - public override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) + public async override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } this._chatHistory.Add(newMessage); - return Task.CompletedTask; } /// - public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } - return this._chatHistory.ToAsyncEnumerable(); + foreach (var message in this._chatHistory) + { + yield return message; + } } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index e16d130d6e46..48aba8edf415 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -362,17 +362,15 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// public override async IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new OpenAIAssistantAgentThread(this.Client), cancellationToken).ConfigureAwait(false); @@ -387,8 +385,8 @@ public override async IAsyncEnumerable> In var invokeResults = this.InvokeAsync( openAIAssistantAgentThread.Id!, internalOptions, - this.MergeArguments(arguments), - kernel ?? this.Kernel, + this.MergeArguments(options?.KernelArguments), + options?.Kernel ?? this.Kernel, cancellationToken); // Notify the thread of new messages and return them to the caller. @@ -458,17 +456,15 @@ async IAsyncEnumerable InternalInvokeAsync() /// public async override IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(message); + Verify.NotNull(messages); var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync( - message, + messages, thread, () => new OpenAIAssistantAgentThread(this.Client), cancellationToken).ConfigureAwait(false); @@ -484,8 +480,8 @@ public async override IAsyncEnumerable /// Represents a conversation thread for an Open AI Assistant agent. /// -public class OpenAIAssistantAgentThread : AgentThread +public sealed class OpenAIAssistantAgentThread : AgentThread { private readonly AssistantClient _client; private string? _id = null; + private bool _isDeleted = false; /// /// Initializes a new instance of the class. @@ -48,6 +50,11 @@ public OpenAIAssistantAgentThread(AssistantClient client, string id) /// public override async Task CreateAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be recreated."); + } + if (this._id is not null) { return this._id; @@ -62,21 +69,32 @@ public override async Task CreateAsync(CancellationToken cancellationTok /// public override async Task DeleteAsync(CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + return; + } + if (this._id is null) { - throw new InvalidOperationException("This thread cannot be ended, since it has not been started."); + throw new InvalidOperationException("This thread cannot be deleted, since it has not been created."); } await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false); - this._id = null; + + this._isDeleted = true; } /// public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } // If the message was generated by this agent, it is already in the thread and we shouldn't add it again. @@ -87,13 +105,21 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc } /// - public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { + if (this._isDeleted) + { + throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); + } + if (this._id is null) { - throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started."); + await this.CreateAsync(cancellationToken).ConfigureAwait(false); } - return AssistantThreadActions.GetMessagesAsync(this._client, this._id!, MessageCollectionOrder.Ascending, cancellationToken); + await foreach (var message in AssistantThreadActions.GetMessagesAsync(this._client, this._id!, MessageCollectionOrder.Ascending, cancellationToken).ConfigureAwait(false)) + { + yield return message; + } } } diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index b88ba37a3aa1..72bb3ed30509 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -20,10 +20,8 @@ internal sealed class MockAgent : ChatHistoryKernelAgent public IReadOnlyList Response { get; set; } = []; public override IAsyncEnumerable> InvokeAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { @@ -44,10 +42,8 @@ public override IAsyncEnumerable InvokeAsync( /// public override IAsyncEnumerable> InvokeStreamingAsync( - ChatMessageContent message, + ICollection messages, AgentThread? thread = null, - KernelArguments? arguments = null, - Kernel? kernel = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs new file mode 100644 index 000000000000..1b71751eb35e --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance; + +public abstract class AgentThreadTests(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 DeletingThreadTwiceDoesNotThrowAsync() + { + await this.Fixture.AgentThread.CreateAsync(); + + await this.Fixture.AgentThread.DeleteAsync(); + await this.Fixture.AgentThread.DeleteAsync(); + } + + [Fact] + public async Task UsingThreadAfterDeleteThrowsAsync() + { + await this.Fixture.AgentThread.CreateAsync(); + await this.Fixture.AgentThread.DeleteAsync(); + + await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.CreateAsync()); + await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.OnNewMessageAsync(new ChatMessageContent())); + } + + [Fact] + public async Task DeleteThreadBeforeCreateThrowsAsync() + { + await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.DeleteAsync()); + } + + [Fact] + public async Task UsingThreadbeforeCreateCreatesAsync() + { + await this.Fixture.AgentThread.OnNewMessageAsync(new ChatMessageContent()); + Assert.NotNull(this.Fixture.AgentThread.Id); + } + + 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/AgentThreadConformance/AzureAIAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AzureAIAgentThreadTests.cs new file mode 100644 index 000000000000..db2c20495580 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AzureAIAgentThreadTests.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance; + +public class AzureAIAgentThreadTests() : AgentThreadTests(() => new AzureAIAgentFixture()) +{ +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs new file mode 100644 index 000000000000..6c1b29269a98 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance; + +public class ChatCompletionAgentThreadTests() : AgentThreadTests(() => new ChatCompletionAgentFixture()) +{ +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs new file mode 100644 index 000000000000..63aadf8aa54c --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance; + +public class OpenAIAssistantAgentThreadTests() : AgentThreadTests(() => new OpenAIAssistantAgentFixture()) +{ +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs index e944d506af2a..9d61ff73e866 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; +using Azure; using Azure.Identity; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; @@ -49,7 +50,13 @@ public override async Task DisposeAsync() { if (this._thread!.Id is not null) { - await this._agentsClient!.DeleteThreadAsync(this._thread!.Id); + try + { + await this._agentsClient!.DeleteThreadAsync(this._thread!.Id); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + } } await this._agentsClient!.DeleteAgentAsync(this._aiAgent!.Id); diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs index 1cee519982b4..3b1afcd796b9 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs @@ -11,7 +11,7 @@ namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; /// -/// Base test class for testing the method of agents. +/// 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 diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs index c62d913a4b0f..9fdce790f7f5 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel; using System.Threading.Tasks; using Azure.Identity; using Microsoft.Extensions.Configuration; @@ -48,7 +49,13 @@ public override async Task DisposeAsync() { if (this._thread!.Id is not null) { - await this._assistantClient!.DeleteThreadAsync(this._thread!.Id); + try + { + await this._assistantClient!.DeleteThreadAsync(this._thread!.Id); + } + catch (ClientResultException ex) when (ex.Status == 404) + { + } } await this._assistantClient!.DeleteAssistantAsync(this._assistant!.Id);