From 9b4a2c1d49a4c318ca6792a91783ac157fb4338f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 30 Jul 2024 20:35:54 +0200 Subject: [PATCH] Added agent architecture & first agent --- app/MindWork AI Studio/Agents/AgentBase.cs | 109 ++++++++++++++++++ .../Agents/AgentTextContentCleaner.cs | 94 +++++++++++++++ app/MindWork AI Studio/Agents/IAgent.cs | 55 +++++++++ app/MindWork AI Studio/Agents/Type.cs | 27 +++++ app/MindWork AI Studio/Chat/ChatRole.cs | 1 + app/MindWork AI Studio/Program.cs | 2 + .../Provider/Anthropic/ProviderAnthropic.cs | 1 + .../Provider/Fireworks/ProviderFireworks.cs | 1 + .../Provider/Mistral/ProviderMistral.cs | 1 + .../Provider/OpenAI/ProviderOpenAI.cs | 1 + .../Provider/SelfHosted/ProviderSelfHosted.cs | 1 + .../wwwroot/changelog/v0.8.6.md | 5 +- 12 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 app/MindWork AI Studio/Agents/AgentBase.cs create mode 100644 app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs create mode 100644 app/MindWork AI Studio/Agents/IAgent.cs create mode 100644 app/MindWork AI Studio/Agents/Type.cs diff --git a/app/MindWork AI Studio/Agents/AgentBase.cs b/app/MindWork AI Studio/Agents/AgentBase.cs new file mode 100644 index 00000000..191ca271 --- /dev/null +++ b/app/MindWork AI Studio/Agents/AgentBase.cs @@ -0,0 +1,109 @@ +using AIStudio.Chat; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Tools; + +// ReSharper disable MemberCanBePrivate.Global + +namespace AIStudio.Agents; + +public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent +{ + protected SettingsManager SettingsManager { get; init; } = settingsManager; + + protected IJSRuntime JsRuntime { get; init; } = jsRuntime; + + protected ThreadSafeRandom RNG { get; init; } = rng; + + /// + /// Represents the type or category of this agent. + /// + protected abstract Type Type { get; } + + /// + /// The name of the agent. + /// + public abstract string Id { get; } + + /// + /// The agent's job description. Will be used for the system prompt. + /// + protected abstract string JobDescription { get; } + + /// + /// Represents the system prompt provided for the agent. + /// + protected abstract string SystemPrompt(string additionalData); + + #region Implementation of IAgent + + public abstract AIStudio.Settings.Provider? ProviderSettings { get; set; } + + public abstract Task ProcessContext(ChatThread chatThread, IDictionary additionalData); + + public abstract Task ProcessInput(ContentBlock input, IDictionary additionalData); + + public abstract Task MadeDecision(ContentBlock input); + + public abstract IReadOnlyCollection GetContext(); + + public abstract IReadOnlyCollection GetAnswers(); + + #endregion + + protected ChatThread CreateChatThread(string systemPrompt) => new() + { + WorkspaceId = Guid.Empty, + ChatId = Guid.NewGuid(), + Name = string.Empty, + Seed = this.RNG.Next(), + SystemPrompt = systemPrompt, + Blocks = [], + }; + + protected DateTimeOffset AddUserRequest(ChatThread thread, string request) + { + var time = DateTimeOffset.Now; + thread.Blocks.Add(new ContentBlock + { + Time = time, + ContentType = ContentType.TEXT, + Role = ChatRole.USER, + Content = new ContentText + { + Text = request, + }, + }); + + return time; + } + + protected async Task AddAIResponseAsync(ChatThread thread, DateTimeOffset time) + { + if(this.ProviderSettings is null) + return; + + var providerSettings = this.ProviderSettings.Value; + var aiText = new ContentText + { + // We have to wait for the remote + // for the content stream: + InitialRemoteWait = true, + }; + + var resultingContentBlock = new ContentBlock + { + Time = time, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = aiText, + }; + + thread.Blocks.Add(resultingContentBlock); + + // Use the selected provider to get the AI response. + // By awaiting this line, we wait for the entire + // content to be streamed. + await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs new file mode 100644 index 00000000..83fe48ff --- /dev/null +++ b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs @@ -0,0 +1,94 @@ +using AIStudio.Chat; +using AIStudio.Settings; +using AIStudio.Tools; + +namespace AIStudio.Agents; + +public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng) +{ + private static readonly ContentBlock EMPTY_BLOCK = new() + { + Content = null, + ContentType = ContentType.NONE, + Role = ChatRole.AGENT, + Time = DateTimeOffset.UtcNow, + }; + + private readonly List context = new(); + private readonly List answers = new(); + + #region Overrides of AgentBase + + public override Settings.Provider? ProviderSettings { get; set; } + + protected override Type Type => Type.SYSTEM; + + public override string Id => "Text Content Cleaner"; + + protected override string JobDescription => + """ + You receive a Markdown document as input. Your goal is to identify the main content of the document + and return it including Markdown formatting. Remove areas that do not belong to the main part of the + document. For a blog article, return only the text of the article with its formatting. For a scientific + paper, only the contents of the paper. Delete elements of navigation, advertisements, HTML artifacts, + cookie banners, etc. If the content contains images, these images remain. The same applies to links. + Ensure that links and images are present as valid Markdown: + + - Syntax of links: [link text](URL) + - Syntax of images: ![alt text](URL) + + If you find relative links or images with relative paths, correct them to absolute paths. For this + purpose, here is the source URL: + """; + + protected override string SystemPrompt(string additionalData) => $"{this.JobDescription} `{additionalData}`."; + + /// + public override async Task ProcessContext(ChatThread chatThread, IDictionary additionalData) + { + // We process the last block of the chat thread. Then, we add the result + // to the chat thread as the last block: + var answer = await this.ProcessInput(chatThread.Blocks[^1], additionalData); + chatThread.Blocks.Add(answer); + + this.context.Clear(); + this.context.AddRange(chatThread.Blocks); + + return chatThread; + } + + // + public override async Task ProcessInput(ContentBlock input, IDictionary additionalData) + { + if (input.Content is not ContentText text) + return EMPTY_BLOCK; + + if(text.InitialRemoteWait || text.IsStreaming) + return EMPTY_BLOCK; + + if(string.IsNullOrWhiteSpace(text.Text)) + return EMPTY_BLOCK; + + if(!additionalData.TryGetValue("sourceURL", out var sourceURL) || string.IsNullOrWhiteSpace(sourceURL)) + return EMPTY_BLOCK; + + var thread = this.CreateChatThread(this.SystemPrompt(sourceURL)); + var time = this.AddUserRequest(thread, text.Text); + await this.AddAIResponseAsync(thread, time); + + var answer = thread.Blocks[^1]; + this.answers.Add(answer); + return answer; + } + + // + public override Task MadeDecision(ContentBlock input) => Task.FromResult(true); + + // + public override IReadOnlyCollection GetContext() => this.context; + + // + public override IReadOnlyCollection GetAnswers() => this.answers; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/IAgent.cs b/app/MindWork AI Studio/Agents/IAgent.cs new file mode 100644 index 00000000..e101f2ef --- /dev/null +++ b/app/MindWork AI Studio/Agents/IAgent.cs @@ -0,0 +1,55 @@ +using AIStudio.Chat; + +namespace AIStudio.Agents; + +public interface IAgent +{ + /// + /// Gets the name of the agent. + /// + public string Id { get; } + + /// + /// The provider to use for this agent. + /// + public AIStudio.Settings.Provider? ProviderSettings { get; set; } + + /// + /// Processes a chat thread (i.e., context) and returns the updated thread. + /// + /// The chat thread to process. The thread is the context for the agent. + /// Additional data to use for processing the chat thread. + /// The updated chat thread. The last content block of the thread is the agent's response. + public Task ProcessContext(ChatThread chatThread, IDictionary additionalData); + + /// + /// Processes the input content block and returns the agent's response. + /// + /// The content block to process. It represents the input. + /// Additional data to use for processing the input. + /// The content block representing the agent's response. + public Task ProcessInput(ContentBlock input, IDictionary additionalData); + + /// + /// The agent makes a decision based on the input. + /// + /// The content block to process. Should be a question or a request. + /// + /// True if a decision has been made based on the input, false otherwise. + /// + public Task MadeDecision(ContentBlock input); + + /// + /// Retrieves the context of the agent. + /// + /// The collection of content blocks representing the agent's context. This includes the user's and the other agent's messages. + public IReadOnlyCollection GetContext(); + + /// + /// Retrieves the answers from the agent's context. + /// + /// + /// The collection of content blocks representing the answers provided by this agent. + /// + public IReadOnlyCollection GetAnswers(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/Type.cs b/app/MindWork AI Studio/Agents/Type.cs new file mode 100644 index 00000000..559f1008 --- /dev/null +++ b/app/MindWork AI Studio/Agents/Type.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Agents; + +public enum Type +{ + /// + /// Represents an unspecified agent type. + /// + UNSPECIFIED = 0, + + /// + /// Represents a conversational agent who produces human-like responses and feedback (depending on the context and its job description). + /// For example, an expert agent for a specific domain. Answers might be detailed and informative. + /// + CONVERSATIONAL, + + /// + /// Represents a worker agent type who performs tasks and provides information or services (depending on the context and its job description). + /// For example, a quality assurance agent who assesses the quality of a product or service. Answers might be short and concise. + /// + WORKER, + + /// + /// Represents the system agent type who processes the input and provides a specific response (depending on the context and its job description). + /// For example, a HTML content agent who processes the arbitrary HTML content and provides a structured Markdown response. Answers might be structured and formatted. + /// + SYSTEM, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ChatRole.cs b/app/MindWork AI Studio/Chat/ChatRole.cs index 2b8712b2..339be971 100644 --- a/app/MindWork AI Studio/Chat/ChatRole.cs +++ b/app/MindWork AI Studio/Chat/ChatRole.cs @@ -11,6 +11,7 @@ public enum ChatRole SYSTEM, USER, AI, + AGENT, } /// diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 9c14e437..6bd7e0a7 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -1,4 +1,5 @@ using AIStudio; +using AIStudio.Agents; using AIStudio.Components; using AIStudio.Settings; using AIStudio.Tools; @@ -30,6 +31,7 @@ builder.Services.AddSingleton(); builder.Services.AddMudMarkdownClipboardService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddTransient(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddRazorComponents() diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 89c6fe05..ffeb2559 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -41,6 +41,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co { ChatRole.USER => "user", ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", _ => "user", }, diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 2f6d1ea0..cf29a6df 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -52,6 +52,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere { ChatRole.USER => "user", ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", ChatRole.SYSTEM => "system", _ => "user", diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 95256fb8..c3510811 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -51,6 +51,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/ { ChatRole.USER => "user", ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", ChatRole.SYSTEM => "system", _ => "user", diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index a18a403c..6e4af600 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -55,6 +55,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/" { ChatRole.USER => "user", ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", ChatRole.SYSTEM => "system", _ => "user", diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 7790d2e8..623c3b17 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -44,6 +44,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide { ChatRole.USER => "user", ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", ChatRole.SYSTEM => "system", _ => "user", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.8.6.md b/app/MindWork AI Studio/wwwroot/changelog/v0.8.6.md index 00280c0d..8354d039 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.8.6.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.8.6.md @@ -1,5 +1,8 @@ # v0.8.6, build 168 - Added possibility to configure a default provider for chats +- Added architecture for future agent usage +- Added a first agent to read, analyze and extract text from Markdown data - Improved the readability of the `settings.json` file by using indentation and enum names instead of numbers - Improved assistant overview; assistants will now wrap to the next line if there are too many to fit on the row -- Increased the default value for the live translation delay from 1,000 to 1,500 ms \ No newline at end of file +- Increased the default value for the live translation delay from 1,000 to 1,500 ms +- Fixed random number generator usage to be thread-safe \ No newline at end of file