diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step02_ConversationState.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step02_ConversationState.cs index 35d6c367ec97..15bb1134403d 100644 --- a/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step02_ConversationState.cs +++ b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step02_ConversationState.cs @@ -98,7 +98,6 @@ public async Task ManageConversationStateWithResponseApiAsync() await foreach (AgentResponseItem<ChatMessageContent> responseItem in responseItems) { agentThread = responseItem.Thread; - this.Output.WriteLine(agentThread.Id); WriteAgentChatMessage(responseItem.Message); } } diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 4a29c6e5de28..d0587839eaea 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -35,6 +35,7 @@ <ItemGroup> <ProjectReference Include="..\..\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" /> <ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" /> + <ProjectReference Include="..\Core\Agents.Core.csproj" /> </ItemGroup> <ItemGroup> diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs index 7212f9c8b22e..5c977ff8ccc9 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs @@ -14,7 +14,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// <summary> -/// Represents a <see cref="Agent"/> specialization based on Open AI Assistant / GPT. +/// Represents a <see cref="Agent"/> specialization based on OpenAI Response API. /// </summary> [ExcludeFromCodeCoverage] public sealed class OpenAIResponseAgent : Agent @@ -45,11 +45,7 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In { Verify.NotNull(messages); - var agentThread = await this.EnsureThreadExistsWithMessagesAsync( - messages, - thread, - () => new OpenAIResponseAgentThread(this.Client, this.StoreEnabled), - cancellationToken).ConfigureAwait(false); + var agentThread = await this.EnsureThreadExistsWithMessagesAsync(messages, thread, cancellationToken).ConfigureAwait(false); // Invoke responses with the updated chat history. var chatHistory = new ChatHistory(); @@ -97,10 +93,20 @@ protected override Task<AgentChannel> RestoreChannelAsync(string channelState, C } #region private + private async Task<AgentThread> EnsureThreadExistsWithMessagesAsync(ICollection<ChatMessageContent> messages, AgentThread? thread, CancellationToken cancellationToken) + { + if (this.StoreEnabled) + { + return await this.EnsureThreadExistsWithMessagesAsync(messages, thread, () => new OpenAIResponseAgentThread(this.Client), cancellationToken).ConfigureAwait(false); + } + + return await this.EnsureThreadExistsWithMessagesAsync(messages, thread, () => new ChatHistoryAgentThread(), cancellationToken).ConfigureAwait(false); + } + private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync( string? agentName, ChatHistory history, - OpenAIResponseAgentThread agentThread, + AgentThread agentThread, AgentInvokeOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -110,7 +116,7 @@ private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync( if (!this.StoreEnabled) { // Use the thread chat history - overrideHistory = [.. agentThread.ChatHistory, .. history]; + overrideHistory = [.. this.GetChatHistory(agentThread), .. history]; } var inputItems = overrideHistory.Select(c => c.ToResponseItem()); @@ -118,11 +124,11 @@ private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync( { EndUserId = this.GetDisplayName(), Instructions = $"{this.Instructions}\n{options?.AdditionalInstructions}", - StoredOutputEnabled = agentThread.StoreEnabled, + StoredOutputEnabled = this.StoreEnabled, }; - if (agentThread.StoreEnabled && agentThread.ResponseId != null) + if (this.StoreEnabled && agentThread.Id != null) { - creationOptions.PreviousResponseId = agentThread.ResponseId; + creationOptions.PreviousResponseId = agentThread.Id; } var clientResult = await this.Client.CreateResponseAsync(inputItems, creationOptions, cancellationToken).ConfigureAwait(false); @@ -130,8 +136,7 @@ private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync( if (this.StoreEnabled) { - // Update the response id - agentThread.ResponseId = response.Id; + this.UpdateResponseId(agentThread, response.Id); } var messages = response.OutputItems.Select(o => o.ToChatMessageContent()); @@ -143,5 +148,26 @@ private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync( yield return message; } } + + private ChatHistory GetChatHistory(AgentThread agentThread) + { + if (agentThread is ChatHistoryAgentThread chatHistoryAgentThread) + { + return chatHistoryAgentThread.ChatHistory; + } + + throw new InvalidOperationException("The agent thread is not a ChatHistoryAgentThread."); + } + + private void UpdateResponseId(AgentThread agentThread, string id) + { + if (agentThread is OpenAIResponseAgentThread openAIResponseAgentThread) + { + openAIResponseAgentThread.ResponseId = id; + return; + } + + throw new InvalidOperationException("The agent thread is not an OpenAIResponseAgentThread."); + } #endregion } diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs index feef7428e608..28445d8a069b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs @@ -6,65 +6,49 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Responses; namespace Microsoft.SemanticKernel.Agents.OpenAI; /// <summary> -/// Represents a conversation thread for an OpenAI responses-based agent. +/// Represents a conversation thread for an OpenAI Response API based agent when store is enabled. /// </summary> [ExcludeFromCodeCoverage] public sealed class OpenAIResponseAgentThread : AgentThread { private readonly OpenAIResponseClient _client; - private readonly ChatHistory _chatHistory = new(); private bool _isDeleted = false; /// <summary> /// Initializes a new instance of the <see cref="OpenAIResponseAgentThread"/> class. /// </summary> /// <param name="client">The agents client to use for interacting with responses.</param> - /// <param name="enableStore">Enable storing messages on the server.</param> - public OpenAIResponseAgentThread(OpenAIResponseClient client, bool enableStore = false) + public OpenAIResponseAgentThread(OpenAIResponseClient client) { Verify.NotNull(client); this._client = client; - this.StoreEnabled = enableStore; } /// <summary> /// Initializes a new instance of the <see cref="OpenAIResponseAgentThread"/> class that resumes an existing response. /// </summary> /// <param name="client">The agents client to use for interacting with responses.</param> - /// <param name="id">The ID of an existing response to resume.</param> - /// <param name="enableStore">Enable storing messages on the server.</param> - public OpenAIResponseAgentThread(OpenAIResponseClient client, string id, bool enableStore = false) + /// <param name="responseId">The ID of an existing response to resume.</param> + public OpenAIResponseAgentThread(OpenAIResponseClient client, string responseId) { Verify.NotNull(client); - Verify.NotNull(id); + Verify.NotNull(responseId); this._client = client; - this.ResponseId = id; - this.StoreEnabled = enableStore; + this.ResponseId = responseId; } - /// <summary> - /// Storing of messages is enabled. - /// </summary> - public bool StoreEnabled { get; private set; } = false; - /// <summary> /// The current response id. /// </summary> internal string? ResponseId { get; set; } - /// <summary> - /// The current chat history. - /// </summary> - internal ChatHistory ChatHistory => this._chatHistory; - /// <inheritdoc /> public override string? Id => this.ResponseId; @@ -93,7 +77,6 @@ protected override Task DeleteInternalAsync(CancellationToken cancellationToken throw new InvalidOperationException("This thread cannot be deleted, since it has not been created."); } - this._chatHistory.Clear(); this._isDeleted = true; return Task.CompletedTask; @@ -107,12 +90,6 @@ protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); } - // Keep track of locally - if (string.IsNullOrEmpty(this.ResponseId)) - { - this._chatHistory.Add(newMessage); - } - return Task.CompletedTask; } @@ -124,7 +101,7 @@ public async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync([EnumeratorCa throw new InvalidOperationException("This thread has been deleted and cannot be used anymore."); } - if (this.StoreEnabled && !string.IsNullOrEmpty(this.ResponseId)) + if (!string.IsNullOrEmpty(this.ResponseId)) { var options = new ResponseItemCollectionOptions(); var collectionResult = this._client.GetResponseInputItemsAsync(this.ResponseId, options, cancellationToken).ConfigureAwait(false); @@ -133,12 +110,7 @@ public async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync([EnumeratorCa yield return responseItem.ToChatMessageContent(); } } - else - { - foreach (var message in this._chatHistory) - { - yield return message; - } - } + + yield break; } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentThreadTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentThreadTests.cs index 3fd424351584..3c0eb684bd51 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentThreadTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentThreadTests.cs @@ -23,7 +23,7 @@ public void ConstructorShouldVerifyParams() // Arrange & Act & Assert Assert.Throws<ArgumentNullException>(() => new OpenAIResponseAgentThread(null!)); Assert.Throws<ArgumentNullException>(() => new OpenAIResponseAgentThread(null!, "threadId")); - Assert.Throws<ArgumentNullException>(() => new OpenAIResponseAgentThread(this.Client, id: null!)); + Assert.Throws<ArgumentNullException>(() => new OpenAIResponseAgentThread(this.Client, responseId: null!)); var agentThread = new OpenAIResponseAgentThread(this.Client); Assert.NotNull(agentThread); @@ -47,7 +47,7 @@ public void ConstructorForResumingThreadShouldUseParams() /// Verify <see cref="OpenAIResponseAgentThread.GetMessagesAsync(System.Threading.CancellationToken)"/> returned when store is disabled. /// </summary> [Fact] - public async Task VerifyGetMessagesWhenStoreDisabledAsync() + public async Task VerifyGetMessagesWhenThreadIsUnusedAsync() { // Arrange var thread = new OpenAIResponseAgentThread(this.Client); @@ -72,7 +72,7 @@ public async Task VerifyGetMessagesWhenStoreEnabledAsync() new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(MessagesResponse) } ); var responseId = "resp_67e8ff743ea08191b085bea42b4d83e809a3a922c4f4221b"; - var thread = new OpenAIResponseAgentThread(this.Client, id: responseId, enableStore: true); + var thread = new OpenAIResponseAgentThread(this.Client, responseId: responseId); // Act var messages = thread.GetMessagesAsync();