From 8d06a62006f2d2aa980b49391dd6eccfa7a95d71 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 24 Jun 2024 11:57:41 -0700 Subject: [PATCH 01/18] Updated --- dotnet/src/Agents/Abstractions/Agent.cs | 16 ++++++++++++++-- dotnet/src/Agents/Abstractions/AgentChat.cs | 5 +---- .../src/Agents/Abstractions/AggregatorAgent.cs | 6 +++--- .../Agents/Abstractions/ChatHistoryChannel.cs | 2 +- .../Abstractions/ChatHistoryKernelAgent.cs | 11 ++++++++--- .../Agents/Abstractions/IChatHistoryHandler.cs | 3 --- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 7 +++---- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 6 +++--- dotnet/src/Agents/UnitTests/AgentChannelTests.cs | 3 +-- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 2 -- .../src/Agents/UnitTests/AggregatorAgentTests.cs | 3 +-- .../Agents/UnitTests/ChatHistoryChannelTests.cs | 3 +-- .../Agents/UnitTests/Core/AgentGroupChatTests.cs | 3 +-- .../UnitTests/Core/ChatCompletionAgentTests.cs | 3 +-- 14 files changed, 38 insertions(+), 35 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs index 4ebe3d1416cf..8af2de3b0869 100644 --- a/dotnet/src/Agents/Abstractions/Agent.cs +++ b/dotnet/src/Agents/Abstractions/Agent.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.SemanticKernel.Agents; @@ -36,6 +37,16 @@ public abstract class Agent /// public string? Name { get; init; } + /// + /// A for this . + /// + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; + + /// + /// The associated with this . + /// + protected ILogger Logger => this._logger ??= this.LoggerFactory.CreateLogger(this.GetType()); + /// /// Set of keys to establish channel affinity. Minimum expected key-set: /// @@ -53,12 +64,13 @@ public abstract class Agent /// /// Produce the an appropriate for the agent type. /// - /// An agent specific logger. /// The to monitor for cancellation requests. The default is . /// An appropriate for the agent type. /// /// Every agent conversation, or , will establish one or more /// objects according to the specific type. /// - protected internal abstract Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken); + protected internal abstract Task CreateChannelAsync(CancellationToken cancellationToken); + + private ILogger? _logger; } diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 26b51928c362..7e7dea00a805 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -256,10 +256,7 @@ async Task GetOrCreateChannelAsync() { this.Logger.LogDebug("[{MethodName}] Creating channel for {AgentType}: {AgentId}", nameof(InvokeAgentAsync), agent.GetType(), agent.Id); - // Creating an agent-typed logger for CreateChannelAsync - channel = await agent.CreateChannelAsync(this.LoggerFactory.CreateLogger(agent.GetType()), cancellationToken).ConfigureAwait(false); - // Creating an channel-typed logger for the channel - channel.Logger = this.LoggerFactory.CreateLogger(channel.GetType()); + channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false); this._agentChannels.Add(channelKey, channel); diff --git a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs index c236cd7a565a..00964fdc9e57 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs @@ -44,14 +44,14 @@ protected internal override IEnumerable GetChannelKeys() } /// - protected internal override Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken) + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) { - logger.LogDebug("[{MethodName}] Creating channel {ChannelType}", nameof(CreateChannelAsync), nameof(AggregatorChannel)); + this.Logger.LogDebug("[{MethodName}] Creating channel {ChannelType}", nameof(CreateChannelAsync), nameof(AggregatorChannel)); AgentChat chat = chatProvider.Invoke(); AggregatorChannel channel = new(chat); - logger.LogInformation("[{MethodName}] Created channel {ChannelType} ({ChannelMode}) with: {AgentChatType}", nameof(CreateChannelAsync), nameof(AggregatorChannel), this.Mode, chat.GetType()); + this.Logger.LogInformation("[{MethodName}] Created channel {ChannelType} ({ChannelMode}) with: {AgentChatType}", nameof(CreateChannelAsync), nameof(AggregatorChannel), this.Mode, chat.GetType()); return Task.FromResult(channel); } diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 281529bffd8e..3baeb934a52b 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -25,7 +25,7 @@ protected internal sealed override async IAsyncEnumerable In throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); } - await foreach (var message in historyHandler.InvokeAsync(this._history, this.Logger, cancellationToken).ConfigureAwait(false)) + await foreach (var message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) { this._history.Add(message); diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs b/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs index ee86a7af770e..315f7bc37cbc 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs @@ -18,14 +18,19 @@ protected internal sealed override IEnumerable GetChannelKeys() } /// - protected internal sealed override Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken) + protected internal sealed override Task CreateChannelAsync(CancellationToken cancellationToken) { - return Task.FromResult(new ChatHistoryChannel()); + ChatHistoryChannel channel = + new() + { + Logger = this.LoggerFactory.CreateLogger() + }; + + return Task.FromResult(channel); } /// public abstract IAsyncEnumerable InvokeAsync( IReadOnlyList history, - ILogger logger, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs b/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs index f377d38ba58e..13fedcd0d0cb 100644 --- a/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs +++ b/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Threading; -using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents; @@ -14,11 +13,9 @@ public interface IChatHistoryHandler /// Entry point for calling into an agent from a a . /// /// The chat history at the point the channel is created. - /// The logger associated with the /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. IAsyncEnumerable InvokeAsync( IReadOnlyList history, - ILogger logger, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index e8f9378e8a39..659c1a7c6313 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -24,7 +24,6 @@ public sealed class ChatCompletionAgent : ChatHistoryKernelAgent /// public override async IAsyncEnumerable InvokeAsync( IReadOnlyList history, - ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var chatCompletionService = this.Kernel.GetRequiredService(); @@ -38,7 +37,7 @@ public override async IAsyncEnumerable InvokeAsync( int messageCount = chat.Count; - logger.LogDebug("[{MethodName}] Invoking {ServiceType}.", nameof(InvokeAsync), chatCompletionService.GetType()); + this.Logger.LogDebug("[{MethodName}] Invoking {ServiceType}.", nameof(InvokeAsync), chatCompletionService.GetType()); IReadOnlyList messages = await chatCompletionService.GetChatMessageContentsAsync( @@ -47,9 +46,9 @@ await chatCompletionService.GetChatMessageContentsAsync( this.Kernel, cancellationToken).ConfigureAwait(false); - if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled + if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled { - logger.LogInformation("[{MethodName}] Invoked {ServiceType} with message count: {MessageCount}.", nameof(InvokeAsync), chatCompletionService.GetType(), messages.Count); + this.Logger.LogInformation("[{MethodName}] Invoked {ServiceType} with message count: {MessageCount}.", nameof(InvokeAsync), chatCompletionService.GetType(), messages.Count); } // Capture mutated messages related function calling / tools diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index ca016a5d97cb..f101aa9ffb83 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -204,13 +204,13 @@ protected override IEnumerable GetChannelKeys() } /// - protected override async Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken) + protected override async Task CreateChannelAsync(CancellationToken cancellationToken) { - logger.LogDebug("[{MethodName}] Creating assistant thread", nameof(CreateChannelAsync)); + this.Logger.LogDebug("[{MethodName}] Creating assistant thread", nameof(CreateChannelAsync)); AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); - logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); + this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); return new OpenAIAssistantChannel(this._client, thread.Id, this._config.Polling); } diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index 544bf946c332..7223b8d46805 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Xunit; @@ -68,7 +67,7 @@ private sealed class NextAgent : TestAgent; private class TestAgent : KernelAgent { - protected internal override Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken) + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index d3c61e4c0a85..bc8e2b42e29a 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; @@ -137,7 +136,6 @@ private sealed class TestAgent : ChatHistoryKernelAgent public override async IAsyncEnumerable InvokeAsync( IReadOnlyList history, - ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(0, cancellationToken); diff --git a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs index f544c1426526..0fb1d8817902 100644 --- a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; @@ -88,7 +87,7 @@ private static Mock CreateMockAgent() Mock agent = new(); ChatMessageContent[] messages = [new ChatMessageContent(AuthorRole.Assistant, "test agent")]; - agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable()); + agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable()); return agent; } diff --git a/dotnet/src/Agents/UnitTests/ChatHistoryChannelTests.cs b/dotnet/src/Agents/UnitTests/ChatHistoryChannelTests.cs index 40a83d739312..7ef624c61ab9 100644 --- a/dotnet/src/Agents/UnitTests/ChatHistoryChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/ChatHistoryChannelTests.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Xunit; @@ -30,7 +29,7 @@ public async Task VerifyAgentWithoutIChatHistoryHandlerAsync() private sealed class TestAgent : KernelAgent { - protected internal override Task CreateChannelAsync(ILogger logger, CancellationToken cancellationToken) + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs index 3948f4b46836..48b652491f53 100644 --- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; @@ -199,7 +198,7 @@ private static Mock CreateMockAgent() Mock agent = new(); ChatMessageContent[] messages = [new ChatMessageContent(AuthorRole.Assistant, "test")]; - agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable()); + agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable()); return agent; } diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs index e1c873598951..5357f0edbd11 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; @@ -60,7 +59,7 @@ public async Task VerifyChatCompletionAgentInvocationAsync() ExecutionSettings = new(), }; - var result = await agent.InvokeAsync([], NullLogger.Instance).ToArrayAsync(); + var result = await agent.InvokeAsync([]).ToArrayAsync(); Assert.Single(result); From 0b8f5a13408d89c763b15bdea987a645da7bb1f8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 24 Jun 2024 12:00:43 -0700 Subject: [PATCH 02/18] Outline --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f101aa9ffb83..be6e7b8b4066 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -162,6 +162,17 @@ public static async Task RetrieveAsync( }; } + /// + public async Task AddMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) + { + if (this.IsDeleted) + { + return; + } + + await this._client.CreateMessageAsync(threadId, message.Role.ToMessageRole(), message.Content, fileIds: null, metadata: null, cancellationToken).ConfigureAwait(false); // %%% STATIC + } + /// public async Task DeleteAsync(CancellationToken cancellationToken = default) { @@ -173,6 +184,21 @@ public async Task DeleteAsync(CancellationToken cancellationToken = default) this.IsDeleted = (await this._client.DeleteAssistantAsync(this.Id, cancellationToken).ConfigureAwait(false)).Value; } + /// + /// Entry point for calling into an agent from a a . %%% + /// + /// %%% + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + public IAsyncEnumerable InvokeAsync( + string threadId, + CancellationToken cancellationToken = default) + { + ChatMessageContent[] empty = []; + + return empty.ToAsyncEnumerable(); // %%% + } + /// protected override IEnumerable GetChannelKeys() { From f10eeee567a9aa4a0ea86b7c571c5857edd133a5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 24 Jun 2024 12:02:14 -0700 Subject: [PATCH 03/18] Sample --- dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs b/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs index 4b8b48c5ef87..d40465ea6d81 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs @@ -46,6 +46,7 @@ public async Task RunAsync() Instructions = ReviewerInstructions, Name = ReviewerName, Kernel = this.CreateKernelWithChatCompletion(), + LoggerFactory = this.LoggerFactory, }; ChatCompletionAgent agentWriter = @@ -54,6 +55,7 @@ public async Task RunAsync() Instructions = CopyWriterInstructions, Name = CopyWriterName, Kernel = this.CreateKernelWithChatCompletion(), + LoggerFactory = this.LoggerFactory, }; // Create a chat for agent interaction. From 5ac0a103ba8d698e74e8af94b79d5bc3953504ca Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 24 Jun 2024 12:30:09 -0700 Subject: [PATCH 04/18] Checkpoint --- .../Agents/OpenAI/OpenAIAssistantActions.cs | 505 ++++++++++++++++++ .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 22 +- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 66 +-- 3 files changed, 523 insertions(+), 70 deletions(-) create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs new file mode 100644 index 000000000000..80cae2600b3e --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs @@ -0,0 +1,505 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.AI.OpenAI.Assistants; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// A specialization for use with . +/// +internal static class OpenAIAssistantActions +{ + /*AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration*/ + private const string FunctionDelimiter = "-"; + + private static readonly HashSet s_messageRoles = + [ + AuthorRole.User, + AuthorRole.Assistant, + ]; + + private static readonly HashSet s_pollingStatuses = + [ + RunStatus.Queued, + RunStatus.InProgress, + RunStatus.Cancelling, + ]; + + private static readonly HashSet s_terminalStatuses = + [ + RunStatus.Expired, + RunStatus.Failed, + RunStatus.Cancelled, + ]; + + //private readonly Dictionary _agentTools = []; + + /// // %%% + public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) + { + if (!s_messageRoles.Contains(message.Role)) + { + throw new KernelException($"Invalid message role: {message.Role}"); + } + + if (string.IsNullOrWhiteSpace(message.Content)) + { + return; + } + + await client.CreateMessageAsync( + threadId, + message.Role.ToMessageRole(), + message.Content, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public static async IAsyncEnumerable GetMessagesAsync(AssistantsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) + { + Dictionary agentNames = []; // Cache agent names by their identifier + + PageableList messages; + + string? lastId = null; + do + { + messages = await client.GetMessagesAsync(threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false); + foreach (ThreadMessage message in messages) + { + AuthorRole role = new(message.Role.ToString()); + + string? assistantName = null; + if (!string.IsNullOrWhiteSpace(message.AssistantId) && + !agentNames.TryGetValue(message.AssistantId, out assistantName)) + { + Assistant assistant = await client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(assistant.Name)) + { + agentNames.Add(assistant.Id, assistant.Name); + } + } + + assistantName ??= message.AssistantId; + + foreach (MessageContent item in message.ContentItems) + { + ChatMessageContent? content = null; + + if (item is MessageTextContent contentMessage) + { + content = GenerateTextMessageContent(assistantName, role, contentMessage); + } + else if (item is MessageImageFileContent contentImage) + { + content = GenerateImageFileContent(assistantName, role, contentImage); + } + + if (content is not null) + { + yield return content; + } + } + + lastId = message.Id; + } + } + while (messages.HasMore); + } + + // /// // %%% + // public static async IAsyncEnumerable InvokeAsync( + // OpenAIAssistantAgent agent, + // [EnumeratorCancellation] CancellationToken cancellationToken) + // { + // if (agent.IsDeleted) + // { + // throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); + // } + + // if (!this._agentTools.TryGetValue(agent.Id, out ToolDefinition[]? tools)) + // { + // tools = [.. agent.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; + // this._agentTools.Add(agent.Id, tools); + // } + + // this.Logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); + + // CreateRunOptions options = + // new(agent.Id) + // { + // OverrideInstructions = agent.Instructions, + // OverrideTools = tools, + // }; + + // // Create run + // ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false); + + // this.Logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); + + // // Evaluate status and process steps and messages, as encountered. + // HashSet processedStepIds = []; + // Dictionary functionSteps = []; + + // do + // { + // // Poll run and steps until actionable + // PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); + + // // Is in terminal state? + // if (s_terminalStatuses.Contains(run.Status)) + // { + // throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); + // } + + // // Is tool action required? + // if (run.Status == RunStatus.RequiresAction) + // { + // this.Logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); + + // // Execute functions in parallel and post results at once. + // FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + // if (activeFunctionSteps.Length > 0) + // { + // // Emit function-call content + // yield return GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps); + + // // Invoke functions for each tool-step + // IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); + + // // Block for function results + // FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); + + // // Process tool output + // ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); + + // await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); + // } + + // if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled + // { + // this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run steps: {RunId}", nameof(InvokeAsync), activeFunctionSteps.Length, run.Id); + // } + // } + + // // Enumerate completed messages + // this.Logger.LogDebug("[{MethodName}] Processing run messages: {RunId}", nameof(InvokeAsync), run.Id); + + // IEnumerable completedStepsToProcess = + // steps + // .Where(s => s.CompletedAt.HasValue && !processedStepIds.Contains(s.Id)) + // .OrderBy(s => s.CreatedAt); + + // int messageCount = 0; + // foreach (RunStep completedStep in completedStepsToProcess) + // { + // if (completedStep.Type.Equals(RunStepType.ToolCalls)) + // { + // RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; + + // foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) + // { + // ChatMessageContent? content = null; + + // // Process code-interpreter content + // if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) + // { + // content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + // } + // // Process function result content + // else if (toolCall is RunStepFunctionToolCall toolFunction) + // { + // FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation + // content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); + // } + + // if (content is not null) + // { + // ++messageCount; + + // yield return content; + // } + // } + // } + // else if (completedStep.Type.Equals(RunStepType.MessageCreation)) + // { + // RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; + + // // Retrieve the message + // ThreadMessage? message = await this.RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); + + // if (message is not null) + // { + // AuthorRole role = new(message.Role.ToString()); + + // foreach (MessageContent itemContent in message.ContentItems) + // { + // ChatMessageContent? content = null; + + // // Process text content + // if (itemContent is MessageTextContent contentMessage) + // { + // content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); + // } + // // Process image content + // else if (itemContent is MessageImageFileContent contentImage) + // { + // content = GenerateImageFileContent(agent.GetName(), role, contentImage); + // } + + // if (content is not null) + // { + // ++messageCount; + + // yield return content; + // } + // } + // } + // } + + // processedStepIds.Add(completedStep.Id); + // } + + // if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled + // { + // this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run messages: {RunId}", nameof(InvokeAsync), messageCount, run.Id); + // } + // } + // while (RunStatus.Completed != run.Status); + + // this.Logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); + + // // Local function to assist in run polling (participates in method closure). + // async Task> PollRunStatusAsync() + // { + // this.Logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); + + // int count = 0; + + // do + // { + // // Reduce polling frequency after a couple attempts + // await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + // ++count; + + //#pragma warning disable CA1031 // Do not catch general exception types + // try + // { + // run = await client.GetRunAsync(threadId, run.Id, cancellationToken).ConfigureAwait(false); + // } + // catch + // { + // // Retry anyway.. + // } + //#pragma warning restore CA1031 // Do not catch general exception types + // } + // while (s_pollingStatuses.Contains(run.Status)); + + // this.Logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); + + // return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); + // } + + // // Local function to capture kernel function state for further processing (participates in method closure). + // IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) + // { + // if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) + // { + // foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) + // { + // var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); + + // KernelArguments functionArguments = []; + // if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) + // { + // Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; + // foreach (var argumentKvp in arguments) + // { + // functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); + // } + // } + + // var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); + + // functionSteps.Add(toolCall.Id, content); + + // yield return content; + // } + // } + // } + + // async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) + // { + // ThreadMessage? message = null; + + // bool retry = false; + // int count = 0; + // do + // { + // try + // { + // message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); + // } + // catch (RequestFailedException exception) + // { + // // Step has provided the message-id. Retry on of NotFound/404 exists. + // // Extremely rarely there might be a synchronization issue between the + // // assistant response and message-service. + // retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; + // } + + // if (retry) + // { + // await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + // } + + // ++count; + // } + // while (retry); + + // return message; + // } + // } + + private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) + { + string? fileId = null; + if (annotation is MessageTextFileCitationAnnotation citationAnnotation) + { + fileId = citationAnnotation.FileId; + } + else if (annotation is MessageTextFilePathAnnotation pathAnnotation) + { + fileId = pathAnnotation.FileId; + } + + return + new() + { + Quote = annotation.Text, + StartIndex = annotation.StartIndex, + EndIndex = annotation.EndIndex, + FileId = fileId, + }; + } + + private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageImageFileContent contentImage) + { + return + new ChatMessageContent( + role, + [ + new FileReferenceContent(contentImage.FileId) + ]) + { + AuthorName = agentName, + }; + } + + private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageTextContent contentMessage) + { + ChatMessageContent? messageContent = null; + + string textContent = contentMessage.Text.Trim(); + + if (!string.IsNullOrWhiteSpace(textContent)) + { + messageContent = + new(role, textContent) + { + AuthorName = agentName + }; + + foreach (MessageTextAnnotation annotation in contentMessage.Annotations) + { + messageContent.Items.Add(GenerateAnnotationContent(annotation)); + } + } + + return messageContent; + } + + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter) + { + return + new ChatMessageContent( + AuthorRole.Tool, + [ + new TextContent(contentCodeInterpreter.Input) + ]) + { + AuthorName = agentName, + }; + } + + private static ChatMessageContent GenerateFunctionCallContent(string agentName, FunctionCallContent[] functionSteps) + { + ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) + { + AuthorName = agentName + }; + + functionCallContent.Items.AddRange(functionSteps); + + return functionCallContent; + } + + private static ChatMessageContent GenerateFunctionResultContent(string agentName, FunctionCallContent functionStep, string result) + { + ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) + { + AuthorName = agentName + }; + + functionCallContent.Items.Add( + new FunctionResultContent( + functionStep.FunctionName, + functionStep.PluginName, + functionStep.Id, + result)); + + return functionCallContent; + } + + private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionSteps, CancellationToken cancellationToken) + { + Task[] functionTasks = new Task[functionSteps.Length]; + + for (int index = 0; index < functionSteps.Length; ++index) + { + functionTasks[index] = functionSteps[index].InvokeAsync(agent.Kernel, cancellationToken); + } + + return functionTasks; + } + + private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] functionResults) + { + ToolOutput[] toolOutputs = new ToolOutput[functionResults.Length]; + + for (int index = 0; index < functionResults.Length; ++index) + { + FunctionResultContent functionResult = functionResults[index]; + + object resultValue = (functionResult.Result as FunctionResult)?.GetValue() ?? string.Empty; + + if (resultValue is not string textResult) + { + textResult = JsonSerializer.Serialize(resultValue); + } + + toolOutputs[index] = new ToolOutput(functionResult.CallId, textResult!); + } + + return toolOutputs; + } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index be6e7b8b4066..4539ad734b05 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -163,14 +163,11 @@ public static async Task RetrieveAsync( } /// - public async Task AddMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) + public Task AddMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) { - if (this.IsDeleted) - { - return; - } + this.ThrowIfDeleted(); - await this._client.CreateMessageAsync(threadId, message.Role.ToMessageRole(), message.Content, fileIds: null, metadata: null, cancellationToken).ConfigureAwait(false); // %%% STATIC + return OpenAIAssistantActions.CreateMessageAsync(this._client, threadId, message, cancellationToken); } /// @@ -194,8 +191,11 @@ public IAsyncEnumerable InvokeAsync( string threadId, CancellationToken cancellationToken = default) { - ChatMessageContent[] empty = []; + this.ThrowIfDeleted(); + + //return OpenAIAssistantActions.InvokeAsync(this._client, threadId, this._config.Polling, cancellationToken); + ChatMessageContent[] empty = []; return empty.ToAsyncEnumerable(); // %%% } @@ -319,4 +319,12 @@ private static AssistantsClientOptions CreateClientOptions(OpenAIAssistantConfig return options; } + + private void ThrowIfDeleted() + { + if (this.IsDeleted) + { + throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {this.Id}."); + } + } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 0d8b20b5b931..1f210479e1d9 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -38,23 +38,13 @@ internal sealed class OpenAIAssistantChannel(AssistantsClient client, string thr private readonly AssistantsClient _client = client; private readonly string _threadId = threadId; private readonly Dictionary _agentTools = []; - private readonly Dictionary _agentNames = []; // Cache agent names by their identifier for GetHistoryAsync() /// protected override async Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken) { foreach (ChatMessageContent message in history) { - if (string.IsNullOrWhiteSpace(message.Content)) - { - continue; - } - - await this._client.CreateMessageAsync( - this._threadId, - message.Role.ToMessageRole(), - message.Content, - cancellationToken: cancellationToken).ConfigureAwait(false); + await OpenAIAssistantActions.CreateMessageAsync(this._client, this._threadId, message, cancellationToken).ConfigureAwait(false); } } @@ -74,11 +64,6 @@ protected override async IAsyncEnumerable InvokeAsync( this._agentTools.Add(agent.Id, tools); } - if (!this._agentNames.ContainsKey(agent.Id) && !string.IsNullOrWhiteSpace(agent.Name)) - { - this._agentNames.Add(agent.Id, agent.Name); - } - this.Logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, this._threadId); CreateRunOptions options = @@ -286,54 +271,9 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R } /// - protected override async IAsyncEnumerable GetHistoryAsync([EnumeratorCancellation] CancellationToken cancellationToken) + protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { - PageableList messages; - - string? lastId = null; - do - { - messages = await this._client.GetMessagesAsync(this._threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false); - foreach (ThreadMessage message in messages) - { - AuthorRole role = new(message.Role.ToString()); - - string? assistantName = null; - if (!string.IsNullOrWhiteSpace(message.AssistantId) && - !this._agentNames.TryGetValue(message.AssistantId, out assistantName)) - { - Assistant assistant = await this._client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(assistant.Name)) - { - this._agentNames.Add(assistant.Id, assistant.Name); - } - } - - assistantName ??= message.AssistantId; - - foreach (MessageContent item in message.ContentItems) - { - ChatMessageContent? content = null; - - if (item is MessageTextContent contentMessage) - { - content = GenerateTextMessageContent(assistantName, role, contentMessage); - } - else if (item is MessageImageFileContent contentImage) - { - content = GenerateImageFileContent(assistantName, role, contentImage); - } - - if (content is not null) - { - yield return content; - } - } - - lastId = message.Id; - } - } - while (messages.HasMore); + return OpenAIAssistantActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); } private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) From 4f9f2c5b18c85dd2edf5df6608b2666278c841c2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 24 Jun 2024 12:38:37 -0700 Subject: [PATCH 05/18] Checkpoint --- .../Agents/OpenAI/OpenAIAssistantActions.cs | 508 +++++++++--------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 21 +- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 409 +------------- 3 files changed, 266 insertions(+), 672 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs index 80cae2600b3e..2b5b4fd85d21 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs @@ -41,8 +41,6 @@ internal static class OpenAIAssistantActions RunStatus.Cancelled, ]; - //private readonly Dictionary _agentTools = []; - /// // %%% public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { @@ -116,259 +114,259 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist while (messages.HasMore); } - // /// // %%% - // public static async IAsyncEnumerable InvokeAsync( - // OpenAIAssistantAgent agent, - // [EnumeratorCancellation] CancellationToken cancellationToken) - // { - // if (agent.IsDeleted) - // { - // throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); - // } - - // if (!this._agentTools.TryGetValue(agent.Id, out ToolDefinition[]? tools)) - // { - // tools = [.. agent.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; - // this._agentTools.Add(agent.Id, tools); - // } - - // this.Logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); - - // CreateRunOptions options = - // new(agent.Id) - // { - // OverrideInstructions = agent.Instructions, - // OverrideTools = tools, - // }; - - // // Create run - // ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false); - - // this.Logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); - - // // Evaluate status and process steps and messages, as encountered. - // HashSet processedStepIds = []; - // Dictionary functionSteps = []; - - // do - // { - // // Poll run and steps until actionable - // PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); - - // // Is in terminal state? - // if (s_terminalStatuses.Contains(run.Status)) - // { - // throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); - // } - - // // Is tool action required? - // if (run.Status == RunStatus.RequiresAction) - // { - // this.Logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); - - // // Execute functions in parallel and post results at once. - // FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); - // if (activeFunctionSteps.Length > 0) - // { - // // Emit function-call content - // yield return GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps); - - // // Invoke functions for each tool-step - // IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); - - // // Block for function results - // FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); - - // // Process tool output - // ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - - // await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); - // } - - // if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled - // { - // this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run steps: {RunId}", nameof(InvokeAsync), activeFunctionSteps.Length, run.Id); - // } - // } - - // // Enumerate completed messages - // this.Logger.LogDebug("[{MethodName}] Processing run messages: {RunId}", nameof(InvokeAsync), run.Id); - - // IEnumerable completedStepsToProcess = - // steps - // .Where(s => s.CompletedAt.HasValue && !processedStepIds.Contains(s.Id)) - // .OrderBy(s => s.CreatedAt); - - // int messageCount = 0; - // foreach (RunStep completedStep in completedStepsToProcess) - // { - // if (completedStep.Type.Equals(RunStepType.ToolCalls)) - // { - // RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; - - // foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) - // { - // ChatMessageContent? content = null; - - // // Process code-interpreter content - // if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) - // { - // content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); - // } - // // Process function result content - // else if (toolCall is RunStepFunctionToolCall toolFunction) - // { - // FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation - // content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); - // } - - // if (content is not null) - // { - // ++messageCount; - - // yield return content; - // } - // } - // } - // else if (completedStep.Type.Equals(RunStepType.MessageCreation)) - // { - // RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; - - // // Retrieve the message - // ThreadMessage? message = await this.RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); - - // if (message is not null) - // { - // AuthorRole role = new(message.Role.ToString()); - - // foreach (MessageContent itemContent in message.ContentItems) - // { - // ChatMessageContent? content = null; - - // // Process text content - // if (itemContent is MessageTextContent contentMessage) - // { - // content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); - // } - // // Process image content - // else if (itemContent is MessageImageFileContent contentImage) - // { - // content = GenerateImageFileContent(agent.GetName(), role, contentImage); - // } - - // if (content is not null) - // { - // ++messageCount; - - // yield return content; - // } - // } - // } - // } - - // processedStepIds.Add(completedStep.Id); - // } - - // if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled - // { - // this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run messages: {RunId}", nameof(InvokeAsync), messageCount, run.Id); - // } - // } - // while (RunStatus.Completed != run.Status); - - // this.Logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); - - // // Local function to assist in run polling (participates in method closure). - // async Task> PollRunStatusAsync() - // { - // this.Logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); - - // int count = 0; - - // do - // { - // // Reduce polling frequency after a couple attempts - // await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); - // ++count; - - //#pragma warning disable CA1031 // Do not catch general exception types - // try - // { - // run = await client.GetRunAsync(threadId, run.Id, cancellationToken).ConfigureAwait(false); - // } - // catch - // { - // // Retry anyway.. - // } - //#pragma warning restore CA1031 // Do not catch general exception types - // } - // while (s_pollingStatuses.Contains(run.Status)); - - // this.Logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); - - // return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); - // } - - // // Local function to capture kernel function state for further processing (participates in method closure). - // IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) - // { - // if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) - // { - // foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) - // { - // var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); - - // KernelArguments functionArguments = []; - // if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) - // { - // Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; - // foreach (var argumentKvp in arguments) - // { - // functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); - // } - // } - - // var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); - - // functionSteps.Add(toolCall.Id, content); - - // yield return content; - // } - // } - // } - - // async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) - // { - // ThreadMessage? message = null; - - // bool retry = false; - // int count = 0; - // do - // { - // try - // { - // message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); - // } - // catch (RequestFailedException exception) - // { - // // Step has provided the message-id. Retry on of NotFound/404 exists. - // // Extremely rarely there might be a synchronization issue between the - // // assistant response and message-service. - // retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; - // } - - // if (retry) - // { - // await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); - // } - - // ++count; - // } - // while (retry); - - // return message; - // } - // } + /// // %%% + public static async IAsyncEnumerable InvokeAsync( + OpenAIAssistantAgent agent, + AssistantsClient client, + string threadId, + OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration, + ILogger logger, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (agent.IsDeleted) + { + throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); + } + + ToolDefinition[]? tools = [.. agent.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; + + logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); + + CreateRunOptions options = + new(agent.Id) + { + OverrideInstructions = agent.Instructions, + OverrideTools = tools, + }; + + // Create run + ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false); + + logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); + + // Evaluate status and process steps and messages, as encountered. + HashSet processedStepIds = []; + Dictionary functionSteps = []; + + do + { + // Poll run and steps until actionable + PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); + + // Is in terminal state? + if (s_terminalStatuses.Contains(run.Status)) + { + throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); + } + + // Is tool action required? + if (run.Status == RunStatus.RequiresAction) + { + logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); + + // Execute functions in parallel and post results at once. + FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + if (activeFunctionSteps.Length > 0) + { + // Emit function-call content + yield return GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps); + + // Invoke functions for each tool-step + IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); + + // Block for function results + FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); + + // Process tool output + ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); + + await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); + } + + if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled + { + logger.LogInformation("[{MethodName}] Processed #{MessageCount} run steps: {RunId}", nameof(InvokeAsync), activeFunctionSteps.Length, run.Id); + } + } + + // Enumerate completed messages + logger.LogDebug("[{MethodName}] Processing run messages: {RunId}", nameof(InvokeAsync), run.Id); + + IEnumerable completedStepsToProcess = + steps + .Where(s => s.CompletedAt.HasValue && !processedStepIds.Contains(s.Id)) + .OrderBy(s => s.CreatedAt); + + int messageCount = 0; + foreach (RunStep completedStep in completedStepsToProcess) + { + if (completedStep.Type.Equals(RunStepType.ToolCalls)) + { + RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; + + foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) + { + ChatMessageContent? content = null; + + // Process code-interpreter content + if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) + { + content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + } + // Process function result content + else if (toolCall is RunStepFunctionToolCall toolFunction) + { + FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation + content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); + } + + if (content is not null) + { + ++messageCount; + + yield return content; + } + } + } + else if (completedStep.Type.Equals(RunStepType.MessageCreation)) + { + RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; + + // Retrieve the message + ThreadMessage? message = await RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); + + if (message is not null) + { + AuthorRole role = new(message.Role.ToString()); + + foreach (MessageContent itemContent in message.ContentItems) + { + ChatMessageContent? content = null; + + // Process text content + if (itemContent is MessageTextContent contentMessage) + { + content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); + } + // Process image content + else if (itemContent is MessageImageFileContent contentImage) + { + content = GenerateImageFileContent(agent.GetName(), role, contentImage); + } + + if (content is not null) + { + ++messageCount; + + yield return content; + } + } + } + } + + processedStepIds.Add(completedStep.Id); + } + + if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled + { + logger.LogInformation("[{MethodName}] Processed #{MessageCount} run messages: {RunId}", nameof(InvokeAsync), messageCount, run.Id); + } + } + while (RunStatus.Completed != run.Status); + + logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); + + // Local function to assist in run polling (participates in method closure). + async Task> PollRunStatusAsync() + { + logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); + + int count = 0; + + do + { + // Reduce polling frequency after a couple attempts + await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + ++count; + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + run = await client.GetRunAsync(threadId, run.Id, cancellationToken).ConfigureAwait(false); + } + catch + { + // Retry anyway.. + } +#pragma warning restore CA1031 // Do not catch general exception types + } + while (s_pollingStatuses.Contains(run.Status)); + + logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); + + return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + // Local function to capture kernel function state for further processing (participates in method closure). + IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) + { + if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) + { + foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) + { + var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); + + KernelArguments functionArguments = []; + if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) + { + Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; + foreach (var argumentKvp in arguments) + { + functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); + } + } + + var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); + + functionSteps.Add(toolCall.Id, content); + + yield return content; + } + } + } + + async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) + { + ThreadMessage? message = null; + + bool retry = false; + int count = 0; + do + { + try + { + message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); + } + catch (RequestFailedException exception) + { + // Step has provided the message-id. Retry on of NotFound/404 exists. + // Extremely rarely there might be a synchronization issue between the + // assistant response and message-service. + retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; + } + + if (retry) + { + await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + } + + ++count; + } + while (retry); + + return message; + } + } private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) { diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 4539ad734b05..4fc1d957d5be 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -193,10 +193,7 @@ public IAsyncEnumerable InvokeAsync( { this.ThrowIfDeleted(); - //return OpenAIAssistantActions.InvokeAsync(this._client, threadId, this._config.Polling, cancellationToken); - - ChatMessageContent[] empty = []; - return empty.ToAsyncEnumerable(); // %%% + return OpenAIAssistantActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken); } /// @@ -241,6 +238,14 @@ protected override async Task CreateChannelAsync(CancellationToken return new OpenAIAssistantChannel(this._client, thread.Id, this._config.Polling); } + internal void ThrowIfDeleted() + { + if (this.IsDeleted) + { + throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {this.Id}."); + } + } + /// /// Initializes a new instance of the class. /// @@ -319,12 +324,4 @@ private static AssistantsClientOptions CreateClientOptions(OpenAIAssistantConfig return options; } - - private void ThrowIfDeleted() - { - if (this.IsDeleted) - { - throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {this.Id}."); - } - } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 1f210479e1d9..29cbc544ffb2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -1,15 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Azure; using Azure.AI.OpenAI.Assistants; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -19,25 +12,8 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; internal sealed class OpenAIAssistantChannel(AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) : AgentChannel { - private const string FunctionDelimiter = "-"; - - private static readonly HashSet s_pollingStatuses = - [ - RunStatus.Queued, - RunStatus.InProgress, - RunStatus.Cancelling, - ]; - - private static readonly HashSet s_terminalStatuses = - [ - RunStatus.Expired, - RunStatus.Failed, - RunStatus.Cancelled, - ]; - private readonly AssistantsClient _client = client; private readonly string _threadId = threadId; - private readonly Dictionary _agentTools = []; /// protected override async Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken) @@ -49,225 +25,13 @@ protected override async Task ReceiveAsync(IReadOnlyList his } /// - protected override async IAsyncEnumerable InvokeAsync( + protected override IAsyncEnumerable InvokeAsync( OpenAIAssistantAgent agent, - [EnumeratorCancellation] CancellationToken cancellationToken) + CancellationToken cancellationToken) { - if (agent.IsDeleted) - { - throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); - } - - if (!this._agentTools.TryGetValue(agent.Id, out ToolDefinition[]? tools)) - { - tools = [.. agent.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; - this._agentTools.Add(agent.Id, tools); - } - - this.Logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, this._threadId); - - CreateRunOptions options = - new(agent.Id) - { - OverrideInstructions = agent.Instructions, - OverrideTools = tools, - }; - - // Create run - ThreadRun run = await this._client.CreateRunAsync(this._threadId, options, cancellationToken).ConfigureAwait(false); - - this.Logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); - - // Evaluate status and process steps and messages, as encountered. - HashSet processedStepIds = []; - Dictionary functionSteps = []; - - do - { - // Poll run and steps until actionable - PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); - - // Is in terminal state? - if (s_terminalStatuses.Contains(run.Status)) - { - throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); - } - - // Is tool action required? - if (run.Status == RunStatus.RequiresAction) - { - this.Logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); - - // Execute functions in parallel and post results at once. - FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); - if (activeFunctionSteps.Length > 0) - { - // Emit function-call content - yield return GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps); - - // Invoke functions for each tool-step - IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); - - // Block for function results - FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); - - // Process tool output - ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - - await this._client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); - } - - if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled - { - this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run steps: {RunId}", nameof(InvokeAsync), activeFunctionSteps.Length, run.Id); - } - } - - // Enumerate completed messages - this.Logger.LogDebug("[{MethodName}] Processing run messages: {RunId}", nameof(InvokeAsync), run.Id); - - IEnumerable completedStepsToProcess = - steps - .Where(s => s.CompletedAt.HasValue && !processedStepIds.Contains(s.Id)) - .OrderBy(s => s.CreatedAt); - - int messageCount = 0; - foreach (RunStep completedStep in completedStepsToProcess) - { - if (completedStep.Type.Equals(RunStepType.ToolCalls)) - { - RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; - - foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) - { - ChatMessageContent? content = null; - - // Process code-interpreter content - if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) - { - content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); - } - // Process function result content - else if (toolCall is RunStepFunctionToolCall toolFunction) - { - FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation - content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); - } - - if (content is not null) - { - ++messageCount; + agent.ThrowIfDeleted(); - yield return content; - } - } - } - else if (completedStep.Type.Equals(RunStepType.MessageCreation)) - { - RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; - - // Retrieve the message - ThreadMessage? message = await this.RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); - - if (message is not null) - { - AuthorRole role = new(message.Role.ToString()); - - foreach (MessageContent itemContent in message.ContentItems) - { - ChatMessageContent? content = null; - - // Process text content - if (itemContent is MessageTextContent contentMessage) - { - content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); - } - // Process image content - else if (itemContent is MessageImageFileContent contentImage) - { - content = GenerateImageFileContent(agent.GetName(), role, contentImage); - } - - if (content is not null) - { - ++messageCount; - - yield return content; - } - } - } - } - - processedStepIds.Add(completedStep.Id); - } - - if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled - { - this.Logger.LogInformation("[{MethodName}] Processed #{MessageCount} run messages: {RunId}", nameof(InvokeAsync), messageCount, run.Id); - } - } - while (RunStatus.Completed != run.Status); - - this.Logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); - - // Local function to assist in run polling (participates in method closure). - async Task> PollRunStatusAsync() - { - this.Logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); - - int count = 0; - - do - { - // Reduce polling frequency after a couple attempts - await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); - ++count; - -#pragma warning disable CA1031 // Do not catch general exception types - try - { - run = await this._client.GetRunAsync(this._threadId, run.Id, cancellationToken).ConfigureAwait(false); - } - catch - { - // Retry anyway.. - } -#pragma warning restore CA1031 // Do not catch general exception types - } - while (s_pollingStatuses.Contains(run.Status)); - - this.Logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); - - return await this._client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - // Local function to capture kernel function state for further processing (participates in method closure). - IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) - { - if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) - { - foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) - { - var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); - - KernelArguments functionArguments = []; - if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) - { - Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; - foreach (var argumentKvp in arguments) - { - functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); - } - } - - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); - - functionSteps.Add(toolCall.Id, content); - - yield return content; - } - } - } + return OpenAIAssistantActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); // %%% CONFIG } /// @@ -275,169 +39,4 @@ protected override IAsyncEnumerable GetHistoryAsync(Cancella { return OpenAIAssistantActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); } - - private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) - { - string? fileId = null; - if (annotation is MessageTextFileCitationAnnotation citationAnnotation) - { - fileId = citationAnnotation.FileId; - } - else if (annotation is MessageTextFilePathAnnotation pathAnnotation) - { - fileId = pathAnnotation.FileId; - } - - return - new() - { - Quote = annotation.Text, - StartIndex = annotation.StartIndex, - EndIndex = annotation.EndIndex, - FileId = fileId, - }; - } - - private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageImageFileContent contentImage) - { - return - new ChatMessageContent( - role, - [ - new FileReferenceContent(contentImage.FileId) - ]) - { - AuthorName = agentName, - }; - } - - private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageTextContent contentMessage) - { - ChatMessageContent? messageContent = null; - - string textContent = contentMessage.Text.Trim(); - - if (!string.IsNullOrWhiteSpace(textContent)) - { - messageContent = - new(role, textContent) - { - AuthorName = agentName - }; - - foreach (MessageTextAnnotation annotation in contentMessage.Annotations) - { - messageContent.Items.Add(GenerateAnnotationContent(annotation)); - } - } - - return messageContent; - } - - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter) - { - return - new ChatMessageContent( - AuthorRole.Tool, - [ - new TextContent(contentCodeInterpreter.Input) - ]) - { - AuthorName = agentName, - }; - } - - private static ChatMessageContent GenerateFunctionCallContent(string agentName, FunctionCallContent[] functionSteps) - { - ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) - { - AuthorName = agentName - }; - - functionCallContent.Items.AddRange(functionSteps); - - return functionCallContent; - } - - private static ChatMessageContent GenerateFunctionResultContent(string agentName, FunctionCallContent functionStep, string result) - { - ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) - { - AuthorName = agentName - }; - - functionCallContent.Items.Add( - new FunctionResultContent( - functionStep.FunctionName, - functionStep.PluginName, - functionStep.Id, - result)); - - return functionCallContent; - } - - private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionSteps, CancellationToken cancellationToken) - { - Task[] functionTasks = new Task[functionSteps.Length]; - - for (int index = 0; index < functionSteps.Length; ++index) - { - functionTasks[index] = functionSteps[index].InvokeAsync(agent.Kernel, cancellationToken); - } - - return functionTasks; - } - - private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] functionResults) - { - ToolOutput[] toolOutputs = new ToolOutput[functionResults.Length]; - - for (int index = 0; index < functionResults.Length; ++index) - { - FunctionResultContent functionResult = functionResults[index]; - - object resultValue = (functionResult.Result as FunctionResult)?.GetValue() ?? string.Empty; - - if (resultValue is not string textResult) - { - textResult = JsonSerializer.Serialize(resultValue); - } - - toolOutputs[index] = new ToolOutput(functionResult.CallId, textResult!); - } - - return toolOutputs; - } - - private async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) - { - ThreadMessage? message = null; - - bool retry = false; - int count = 0; - do - { - try - { - message = await this._client.GetMessageAsync(this._threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); - } - catch (RequestFailedException exception) - { - // Step has provided the message-id. Retry on of NotFound/404 exists. - // Extremely rarely there might be a synchronization issue between the - // assistant response and message-service. - retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; - } - - if (retry) - { - await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); - } - - ++count; - } - while (retry); - - return message; - } } From a6c0c4a39f749801f7ab80199eb4e967f5a674c7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 07:49:20 -0700 Subject: [PATCH 06/18] Checkpoint --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 67 +++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 4fc1d957d5be..85ac6752083c 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -162,7 +162,55 @@ public static async Task RetrieveAsync( }; } - /// + /// + /// Create a new assistant thread. + /// + /// Configuration for accessing the Assistants API service, such as the api-key. + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public static async Task CreateThreadAsync( + OpenAIAssistantConfiguration config, + CancellationToken cancellationToken = default) + { + // Validate input + Verify.NotNull(config, nameof(config)); + + // Create the client + AssistantsClient client = CreateClient(config); + + AssistantThread thread = await client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + + return thread.Id; + } + + /// + /// Create a new assistant thread. + /// + /// The thread identifier + /// Configuration for accessing the Assistants API service, such as the api-key. + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public static async Task DeleteThreadAsync( + string threadId, + OpenAIAssistantConfiguration config, + CancellationToken cancellationToken = default) + { + // Validate input + Verify.NotNullOrWhiteSpace(threadId, nameof(threadId)); + Verify.NotNull(config, nameof(config)); + + // Create the client + AssistantsClient client = CreateClient(config); + + return await client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds a message to the specified thread. + /// + /// The thread identifier + /// A non-system message with which to append to the conversation. + /// The to monitor for cancellation requests. The default is . public Task AddMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); @@ -170,15 +218,22 @@ public Task AddMessageAsync(string threadId, ChatMessageContent message, Cancell return OpenAIAssistantActions.CreateMessageAsync(this._client, threadId, message, cancellationToken); } - /// - public async Task DeleteAsync(CancellationToken cancellationToken = default) + /// + /// Delete the assistant definition. + /// + /// + /// True if assistant definition has been deleted + /// + /// Assistant based agent will not be useable after deletion. + /// + public async Task DeleteAsync(CancellationToken cancellationToken = default) { - if (this.IsDeleted) + if (!this.IsDeleted) { - return; + this.IsDeleted = (await this._client.DeleteAssistantAsync(this.Id, cancellationToken).ConfigureAwait(false)).Value; } - this.IsDeleted = (await this._client.DeleteAssistantAsync(this.Id, cancellationToken).ConfigureAwait(false)).Value; + return this.IsDeleted; } /// From b78b24353b5b92d00722e355d5c0546dc6d09a3b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 07:53:52 -0700 Subject: [PATCH 07/18] Update sample --- dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs index 682c96001deb..c9ffcdac8a84 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs @@ -27,7 +27,7 @@ public async Task UseSingleChatCompletionAgentAsync() }; /// Create a chat for agent interaction. For more, . - AgentGroupChat chat = new(); + ChatHistory chat = new(); // Respond to user input await InvokeAgentAsync("Fortune favors the bold."); @@ -37,11 +37,11 @@ public async Task UseSingleChatCompletionAgentAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + chat.Add(new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync(agent)) + await foreach (var content in agent.InvokeAsync(chat)) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } From b5b3c7f5baf2cea85f195184fa394ccf75751eb4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 07:55:54 -0700 Subject: [PATCH 08/18] Merge fix --- dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs index 2b5b4fd85d21..26332dbefdc0 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs @@ -488,7 +488,7 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function { FunctionResultContent functionResult = functionResults[index]; - object resultValue = (functionResult.Result as FunctionResult)?.GetValue() ?? string.Empty; + object resultValue = functionResult.Result ?? string.Empty; if (resultValue is not string textResult) { From 6df6e1f8cfa91d30abcad65375e59edfdc496d5b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 08:18:59 -0700 Subject: [PATCH 09/18] Support thread access --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 85ac6752083c..70aa4b99a2e6 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -211,13 +211,26 @@ public static async Task DeleteThreadAsync( /// The thread identifier /// A non-system message with which to append to the conversation. /// The to monitor for cancellation requests. The default is . - public Task AddMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) + public Task AddThreadMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); return OpenAIAssistantActions.CreateMessageAsync(this._client, threadId, message, cancellationToken); } + /// + /// Gets messages for a specified thread. + /// + /// The thread identifier + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + public IAsyncEnumerable GetThreadMessagesAsync(string threadId, CancellationToken cancellationToken = default) + { + this.ThrowIfDeleted(); + + return OpenAIAssistantActions.GetMessagesAsync(this._client, threadId, cancellationToken); + } + /// /// Delete the assistant definition. /// From 7b53c5a1663df851b72009c656e7487adcf805c6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 08:19:11 -0700 Subject: [PATCH 10/18] Update assistant sample --- .../Step8_OpenAIAssistant.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 5f03ffb39c8f..0e7b080ea82e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -20,11 +20,13 @@ public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task UseSingleOpenAIAssistantAgentAsync() { + OpenAIAssistantConfiguration config = new(this.ApiKey, this.Endpoint); + // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config, new() { Instructions = HostInstructions, @@ -37,7 +39,7 @@ await OpenAIAssistantAgent.CreateAsync( agent.Kernel.Plugins.Add(plugin); // Create a chat for agent interaction. - var chat = new AgentGroupChat(); + string threadId = await OpenAIAssistantAgent.CreateThreadAsync(config); // Respond to user input try @@ -55,13 +57,16 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + await agent.AddMessageToThreadAsync(threadId, new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync(agent)) + await foreach (var content in agent.InvokeAsync(threadId)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + if (content.Role != AuthorRole.Tool) + { + Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + } } } } From 48ab517da35546cf28c68fa5bc87f7269e850306 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 08:21:48 -0700 Subject: [PATCH 11/18] Logger update --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 70aa4b99a2e6..e5d264c669c8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -303,7 +303,11 @@ protected override async Task CreateChannelAsync(CancellationToken this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); - return new OpenAIAssistantChannel(this._client, thread.Id, this._config.Polling); + return + new OpenAIAssistantChannel(this._client, thread.Id, this._config.Polling) + { + Logger = this.LoggerFactory.CreateLogger() + }; } internal void ThrowIfDeleted() From c5aa3e617f6979fecbe0f763bfd02b8cecc4f209 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 08:25:28 -0700 Subject: [PATCH 12/18] Consistent naming --- .../samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 0e7b080ea82e..546c07bf26d2 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -57,7 +57,7 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - await agent.AddMessageToThreadAsync(threadId, new ChatMessageContent(AuthorRole.User, input)); + await agent.AddChatMessageAsync(threadId, new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index e5d264c669c8..5dd0b7d85b58 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -211,7 +211,7 @@ public static async Task DeleteThreadAsync( /// The thread identifier /// A non-system message with which to append to the conversation. /// The to monitor for cancellation requests. The default is . - public Task AddThreadMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) + public Task AddChatMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); From 25b0f07a433651ec3dd61907527c422aafb6ae54 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 08:26:52 -0700 Subject: [PATCH 13/18] Add thread clean-up to sample --- .../samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs | 1 + dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 546c07bf26d2..642375ddbeab 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -51,6 +51,7 @@ await OpenAIAssistantAgent.CreateAsync( } finally { + await OpenAIAssistantAgent.DeleteThreadAsync(config, threadId); await agent.DeleteAsync(); } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 5dd0b7d85b58..ade799a6e263 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -186,13 +186,13 @@ public static async Task CreateThreadAsync( /// /// Create a new assistant thread. /// - /// The thread identifier /// Configuration for accessing the Assistants API service, such as the api-key. + /// The thread identifier /// The to monitor for cancellation requests. The default is . /// The thread identifier public static async Task DeleteThreadAsync( - string threadId, OpenAIAssistantConfiguration config, + string threadId, CancellationToken cancellationToken = default) { // Validate input From 57ba5174de0c4de10f7a74d75921443888adc826 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 10:36:18 -0700 Subject: [PATCH 14/18] Comment cleanup --- .../Agents/OpenAI/OpenAIAssistantActions.cs | 28 +++++++++++++++++-- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 +-- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs index 26332dbefdc0..f2dd07c904d5 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs @@ -41,7 +41,14 @@ internal static class OpenAIAssistantActions RunStatus.Cancelled, ]; - /// // %%% + /// + /// Create a message in the specified thread. + /// + /// The assistant client + /// The thread identifier + /// The message to add + /// The to monitor for cancellation requests. The default is . + /// if a system message is present, without taking any other action public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { if (!s_messageRoles.Contains(message.Role)) @@ -61,7 +68,13 @@ await client.CreateMessageAsync( cancellationToken: cancellationToken).ConfigureAwait(false); } - /// + /// + /// Retrieves the thread messages. + /// + /// The assistant client + /// The thread identifier + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. public static async IAsyncEnumerable GetMessagesAsync(AssistantsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) { Dictionary agentNames = []; // Cache agent names by their identifier @@ -114,7 +127,16 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist while (messages.HasMore); } - /// // %%% + /// + /// Invoke the assistant on the specified thread. + /// + /// The assistant agent to interact with the thread. + /// The assistant client + /// The thread identifier + /// Config to utilize when polling for run state. + /// The logger to utilize (might be agent or channel scoped) + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. public static async IAsyncEnumerable InvokeAsync( OpenAIAssistantAgent agent, AssistantsClient client, diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index ade799a6e263..832d5271f8dc 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -250,9 +250,9 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul } /// - /// Entry point for calling into an agent from a a . %%% + /// Invoke the assistant on the specified thread. /// - /// %%% + /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. public IAsyncEnumerable InvokeAsync( diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 29cbc544ffb2..16dcda68d298 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -31,7 +31,7 @@ protected override IAsyncEnumerable InvokeAsync( { agent.ThrowIfDeleted(); - return OpenAIAssistantActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); // %%% CONFIG + return OpenAIAssistantActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); } /// From b14244bf8d09247581ad8aad7ab6519e37f8f840 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 25 Jun 2024 10:46:19 -0700 Subject: [PATCH 15/18] Rename --- ...{OpenAIAssistantActions.cs => AssistantThreadActions.cs} | 4 ++-- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 6 +++--- dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename dotnet/src/Agents/OpenAI/{OpenAIAssistantActions.cs => AssistantThreadActions.cs} (99%) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs similarity index 99% rename from dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs rename to dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index f2dd07c904d5..37649844a230 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -14,9 +14,9 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// A specialization for use with . +/// Actions associated with an Open Assistant thread. /// -internal static class OpenAIAssistantActions +internal static class AssistantThreadActions { /*AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration*/ private const string FunctionDelimiter = "-"; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 832d5271f8dc..8b2a1a6c344b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -215,7 +215,7 @@ public Task AddChatMessageAsync(string threadId, ChatMessageContent message, Can { this.ThrowIfDeleted(); - return OpenAIAssistantActions.CreateMessageAsync(this._client, threadId, message, cancellationToken); + return AssistantThreadActions.CreateMessageAsync(this._client, threadId, message, cancellationToken); } /// @@ -228,7 +228,7 @@ public IAsyncEnumerable GetThreadMessagesAsync(string thread { this.ThrowIfDeleted(); - return OpenAIAssistantActions.GetMessagesAsync(this._client, threadId, cancellationToken); + return AssistantThreadActions.GetMessagesAsync(this._client, threadId, cancellationToken); } /// @@ -261,7 +261,7 @@ public IAsyncEnumerable InvokeAsync( { this.ThrowIfDeleted(); - return OpenAIAssistantActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken); } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 16dcda68d298..b84ef800ebd4 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -20,7 +20,7 @@ protected override async Task ReceiveAsync(IReadOnlyList his { foreach (ChatMessageContent message in history) { - await OpenAIAssistantActions.CreateMessageAsync(this._client, this._threadId, message, cancellationToken).ConfigureAwait(false); + await AssistantThreadActions.CreateMessageAsync(this._client, this._threadId, message, cancellationToken).ConfigureAwait(false); } } @@ -31,12 +31,12 @@ protected override IAsyncEnumerable InvokeAsync( { agent.ThrowIfDeleted(); - return OpenAIAssistantActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); } /// protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { - return OpenAIAssistantActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); + return AssistantThreadActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); } } From 07e82c7ca3a4e88c014066b62f7018e72ee2cfe4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 1 Jul 2024 13:25:22 -0700 Subject: [PATCH 16/18] Consistency --- .../Step8_OpenAIAssistant.cs | 4 ++-- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 23 ++++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 642375ddbeab..3bac17a4d5ad 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -39,7 +39,7 @@ await OpenAIAssistantAgent.CreateAsync( agent.Kernel.Plugins.Add(plugin); // Create a chat for agent interaction. - string threadId = await OpenAIAssistantAgent.CreateThreadAsync(config); + string threadId = await agent.CreateThreadAsync(); // Respond to user input try @@ -51,7 +51,7 @@ await OpenAIAssistantAgent.CreateAsync( } finally { - await OpenAIAssistantAgent.DeleteThreadAsync(config, threadId); + await agent.DeleteThreadAsync(threadId); await agent.DeleteAsync(); } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 8b2a1a6c344b..b46cdb013c18 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -165,20 +165,11 @@ public static async Task RetrieveAsync( /// /// Create a new assistant thread. /// - /// Configuration for accessing the Assistants API service, such as the api-key. /// The to monitor for cancellation requests. The default is . /// The thread identifier - public static async Task CreateThreadAsync( - OpenAIAssistantConfiguration config, - CancellationToken cancellationToken = default) + public async Task CreateThreadAsync(CancellationToken cancellationToken = default) { - // Validate input - Verify.NotNull(config, nameof(config)); - - // Create the client - AssistantsClient client = CreateClient(config); - - AssistantThread thread = await client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); return thread.Id; } @@ -186,23 +177,17 @@ public static async Task CreateThreadAsync( /// /// Create a new assistant thread. /// - /// Configuration for accessing the Assistants API service, such as the api-key. /// The thread identifier /// The to monitor for cancellation requests. The default is . /// The thread identifier - public static async Task DeleteThreadAsync( - OpenAIAssistantConfiguration config, + public async Task DeleteThreadAsync( string threadId, CancellationToken cancellationToken = default) { // Validate input Verify.NotNullOrWhiteSpace(threadId, nameof(threadId)); - Verify.NotNull(config, nameof(config)); - - // Create the client - AssistantsClient client = CreateClient(config); - return await client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false); + return await this._client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false); } /// From 3bf2b70c567da39e63832383fe0b13ef5b81d0c3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 1 Jul 2024 14:38:05 -0700 Subject: [PATCH 17/18] Sample clean-up --- .../GettingStartedWithAgents/Step8_OpenAIAssistant.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 3bac17a4d5ad..206ac840c08c 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c)c Microsoft. All rights reserved. using System.ComponentModel; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; @@ -20,13 +20,11 @@ public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task UseSingleOpenAIAssistantAgentAsync() { - OpenAIAssistantConfiguration config = new(this.ApiKey, this.Endpoint); - // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + config: new(this.ApiKey, this.Endpoint), new() { Instructions = HostInstructions, From 45592c7da09e6c61d0b089f2565c73a9ecc83fce Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 1 Jul 2024 14:44:31 -0700 Subject: [PATCH 18/18] Header fix --- .../samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 206ac840c08c..09afcfc44826 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -1,4 +1,4 @@ -// Copyright (c)c Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents;