Skip to content

Add plugin system #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Mar 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5d38364
Removed unused class
SommerEngineering Mar 10, 2025
0d25e96
Added a feature flag for the plugin system
SommerEngineering Mar 10, 2025
37562d3
Added the Lua library we use for the plugin system to the about page
SommerEngineering Mar 10, 2025
a45649d
Added handling for default values for methods
SommerEngineering Mar 11, 2025
f3c9ff5
Added GetAllEnumValues to common tools
SommerEngineering Mar 14, 2025
28009e1
Implemented the plugin base system
SommerEngineering Mar 14, 2025
cba651d
Refactored the plugin base class
SommerEngineering Mar 15, 2025
4d79b0e
Added another check
SommerEngineering Mar 15, 2025
ee5237a
Added the theme plugin type
SommerEngineering Mar 15, 2025
6f772e7
Removed plugin state
SommerEngineering Mar 15, 2025
a87cd41
Added an example plugin for language support
SommerEngineering Mar 17, 2025
abeee24
Load some default Lua libraries
SommerEngineering Mar 18, 2025
a845cff
Added the logger factory for static classes
SommerEngineering Mar 20, 2025
cc26926
Embed all internal plugins
SommerEngineering Mar 20, 2025
8a4031e
Updated
SommerEngineering Mar 20, 2025
c679e17
Ensure deployment of internal plugins
SommerEngineering Mar 20, 2025
51f3439
Added a loader which allows Lua scripts to load other Lua scripts
SommerEngineering Mar 21, 2025
8fc6cb3
Simplify NoPlugin
SommerEngineering Mar 21, 2025
24f5127
Implemented a mechanism for excluding forbidden plugins
SommerEngineering Mar 21, 2025
ff788b3
Add maintenance and deprecation message flags to plugins
SommerEngineering Mar 22, 2025
14ce3be
Add maintenance and deprecation message flags to plugins
SommerEngineering Mar 22, 2025
dfc8ddb
Updated changelog
SommerEngineering Mar 22, 2025
5eafb20
Improved visibility & mutability
SommerEngineering Mar 22, 2025
568a427
Add an init method for UI text content & method to read text tables
SommerEngineering Mar 22, 2025
bcd0138
Implemented the language plugin logic
SommerEngineering Mar 22, 2025
2d7419c
Updated language plugins for DE-DE and EN-US
SommerEngineering Mar 22, 2025
7ebdeaa
Delete app/MindWork AI Studio/wwwroot/changelog/v0.9.33.md
SommerEngineering Mar 22, 2025
4c4ee62
Merged project file & changelog from main
SommerEngineering Mar 22, 2025
48f2d71
Added changelog
SommerEngineering Mar 22, 2025
262cebd
Embed all internal plugins
SommerEngineering Mar 22, 2025
d0e94c9
Added the Lua library
SommerEngineering Mar 22, 2025
cf21c4f
Merge branch 'main' into add-plugin-system
SommerEngineering Mar 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions app/MindWork AI Studio/Chat/Workspace.cs

This file was deleted.

4 changes: 4 additions & 0 deletions app/MindWork AI Studio/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;

Expand Down Expand Up @@ -81,6 +82,9 @@ protected override async Task OnInitializedAsync()
// Ensure that all settings are loaded:
await this.SettingsManager.LoadSettings();

// Ensure that all internal plugins are present:
await PluginFactory.EnsureInternalPlugins();

// Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED ]);
Expand Down
3 changes: 2 additions & 1 deletion app/MindWork AI Studio/MindWork AI Studio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
</ItemGroup>

<ItemGroup>
<!-- Embed all files in wwwroot folder -->
<EmbeddedResource Include="wwwroot\**" CopyToOutputDirectory="PreserveNewest" />
<EmbeddedResource Include="Plugins\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
Expand All @@ -52,6 +52,7 @@
<PackageReference Include="MudBlazor" Version="8.4.0" />
<PackageReference Include="MudBlazor.Markdown" Version="8.0.0" />
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
<PackageReference Include="LuaCSharp" Version="0.4.2" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions app/MindWork AI Studio/Pages/About.razor
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."/>
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project."/>
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose."/>
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." />
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CONTENT_HOME = {
LetsGetStarted = "Lass uns anfangen",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- The ID for this plugin:
ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc"

-- The name of the plugin:
NAME = "MindWork AI Studio - German / Deutsch"

-- The description of the plugin:
DESCRIPTION = "Dieses Plugin bietet deutsche Sprachunterstützung für MindWork AI Studio."

-- The version of the plugin:
VERSION = "1.0.0"

-- The type of the plugin:
TYPE = "LANGUAGE"

-- The authors of the plugin:
AUTHORS = {"MindWork AI Community"}

-- The support contact for the plugin:
SUPPORT_CONTACT = "MindWork AI Community"

-- The source URL for the plugin:
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"

-- The categories for the plugin:
CATEGORIES = { "CORE" }

-- The target groups for the plugin:
TARGET_GROUPS = { "EVERYONE" }

-- The flag for whether the plugin is maintained:
IS_MAINTAINED = true

-- When the plugin is deprecated, this message will be shown to users:
DEPRECATION_MESSAGE = nil

UI_TEXT_CONTENT = {
HOME = CONTENT_HOME,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CONTENT_HOME = {
LetsGetStarted = "Let's get started",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require("contentHome")

-- The ID for this plugin:
ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8"

-- The name of the plugin:
NAME = "MindWork AI Studio - US English"

-- The description of the plugin:
DESCRIPTION = "This plugin provides US English language support for MindWork AI Studio."

-- The version of the plugin:
VERSION = "1.0.0"

-- The type of the plugin:
TYPE = "LANGUAGE"

-- The authors of the plugin:
AUTHORS = {"MindWork AI Community"}

-- The support contact for the plugin:
SUPPORT_CONTACT = "MindWork AI Community"

-- The source URL for the plugin:
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"

-- The categories for the plugin:
CATEGORIES = { "CORE" }

-- The target groups for the plugin:
TARGET_GROUPS = { "EVERYONE" }

-- The flag for whether the plugin is maintained:
IS_MAINTAINED = true

-- When the plugin is deprecated, this message will be shown to users:
DEPRECATION_MESSAGE = nil

UI_TEXT_CONTENT = {
HOME = CONTENT_HOME,
}
4 changes: 4 additions & 0 deletions app/MindWork AI Studio/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class Program
public static Encryption ENCRYPTION = null!;
public static string API_TOKEN = null!;
public static IServiceProvider SERVICE_PROVIDER = null!;
public static ILoggerFactory LOGGER_FACTORY = null!;

public static async Task Main(string[] args)
{
Expand Down Expand Up @@ -147,6 +148,9 @@ public static async Task Main(string[] args)
// Execute the builder to get the app:
var app = builder.Build();

// Get the logging factory for e.g., static classes:
LOGGER_FACTORY = app.Services.GetRequiredService<ILoggerFactory>();

// Get a program logger:
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
programLogger.LogInformation("Starting the AI Studio server.");
Expand Down
2 changes: 2 additions & 0 deletions app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public enum PreviewFeatures
//
PRE_WRITER_MODE_2024,
PRE_RAG_2024,

PRE_PLUGINS_2025,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public static class PreviewFeaturesExtensions
{
PreviewFeatures.PRE_WRITER_MODE_2024 => "Writer Mode: Experiments about how to write long texts using AI",
PreviewFeatures.PRE_RAG_2024 => "RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company",
PreviewFeatures.PRE_PLUGINS_2025 => "Plugins: Preview of our plugin system where you can extend the functionality of the app",

_ => "Unknown preview feature"
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static IList<PreviewFeatures> GetPreviewFeatures(this PreviewVisibility v
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
features.Add(PreviewFeatures.PRE_PLUGINS_2025);
}

return features;
Expand Down
22 changes: 22 additions & 0 deletions app/MindWork AI Studio/Tools/CommonTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text;

namespace AIStudio.Tools;

public static class CommonTools
{
/// <summary>
/// Get all the values (the names) of an enum as a string, separated by commas.
/// </summary>
/// <typeparam name="TEnum">The enum type to get the values of.</typeparam>
/// <param name="exceptions">The values to exclude from the result.</param>
/// <returns>The values of the enum as a string, separated by commas.</returns>
public static string GetAllEnumValues<TEnum>(params TEnum[] exceptions) where TEnum : struct, Enum
{
var sb = new StringBuilder();
foreach (var value in Enum.GetValues<TEnum>())
if(!exceptions.Contains(value))
sb.Append(value).Append(", ");

return sb.ToString();
}
}
99 changes: 99 additions & 0 deletions app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
namespace AIStudio.Tools.PluginSystem;

/// <summary>
/// Checks if a plugin is forbidden.
/// </summary>
public static class ForbiddenPlugins
{
private const string ID_PATTERN = "ID = \"";
private static readonly int ID_PATTERN_LEN = ID_PATTERN.Length;

/// <summary>
/// Checks if the given code represents a forbidden plugin.
/// </summary>
/// <param name="code">The code to check.</param>
/// <returns>The result of the check.</returns>
public static PluginCheckResult Check(ReadOnlySpan<char> code)
{
var endIndex = 0;
var foundAnyId = false;
var id = ReadOnlySpan<char>.Empty;
while (true)
{
// Create a slice of the code starting at the end index.
// This way we can search for all IDs in the code:
code = code[endIndex..];

// Read the next ID as a string:
if (!TryGetId(code, out id, out endIndex))
{
// When no ID was found at all, we block this plugin.
// When another ID was found previously, we allow this plugin.
if(foundAnyId)
return new PluginCheckResult(false, null);

return new PluginCheckResult(true, "No ID was found.");
}

// Try to parse the ID as a GUID:
if (!Guid.TryParse(id, out var parsedGuid))
{
// Again, when no ID was found at all, we block this plugin.
if(foundAnyId)
return new PluginCheckResult(false, null);

return new PluginCheckResult(true, "The ID is not a valid GUID.");
}

// Check if the GUID is forbidden:
if (FORBIDDEN_PLUGINS.TryGetValue(parsedGuid, out var reason))
return new PluginCheckResult(true, reason);

foundAnyId = true;
}
}

private static bool TryGetId(ReadOnlySpan<char> code, out ReadOnlySpan<char> id, out int endIndex)
{
//
// Please note: the code variable is a slice of the original code.
// That means the indices are relative to the slice, not the original code.
//

id = ReadOnlySpan<char>.Empty;
endIndex = 0;

// Find the next ID:
var idStartIndex = code.IndexOf(ID_PATTERN);
if (idStartIndex < 0)
return false;

// Find the start index of the value (Guid):
var valueStartIndex = idStartIndex + ID_PATTERN_LEN;

// Find the end index of the value. In order to do that,
// we create a slice of the code starting at the value
// start index. That means that the end index is relative
// to the inner slice, not the original code nor the outer slice.
var valueEndIndex = code[valueStartIndex..].IndexOf('"');
if (valueEndIndex < 0)
return false;

// From the perspective of the start index is the end index
// the length of the value:
endIndex = valueStartIndex + valueEndIndex;
id = code.Slice(valueStartIndex, valueEndIndex);
return true;
}

/// <summary>
/// The forbidden plugins.
/// </summary>
/// <remarks>
/// A dictionary that maps the GUID of a plugin to the reason why it is forbidden.
/// </remarks>
// ReSharper disable once CollectionNeverUpdated.Local
private static readonly Dictionary<Guid, string> FORBIDDEN_PLUGINS =
[
];
}
21 changes: 21 additions & 0 deletions app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace AIStudio.Tools.PluginSystem;

/// <summary>
/// Represents a contract for a language plugin.
/// </summary>
public interface ILanguagePlugin
{
/// <summary>
/// Tries to get a text from the language plugin.
/// </summary>
/// <remarks>
/// When the key does not exist, the value will be an empty string.
/// Please note that the key is case-sensitive. Furthermore, the keys
/// are in the format "root::key". That means that the keys are
/// hierarchical and separated by "::".
/// </remarks>
/// <param name="key">The key to use to get the text.</param>
/// <param name="value">The desired text.</param>
/// <returns>True if the key exists, false otherwise.</returns>
public bool TryGetText(string key, out string value);
}
7 changes: 7 additions & 0 deletions app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace AIStudio.Tools.PluginSystem;

public enum InternalPlugin
{
LANGUAGE_EN_US,
LANGUAGE_DE_DE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AIStudio.Tools.PluginSystem;

public readonly record struct InternalPluginData(PluginType Type, Guid Id, string ShortName)
{
public string ResourcePath => $"{this.Type.GetDirectory()}/{this.ShortName.ToLowerInvariant()}-{this.Id}";

public string ResourceName => $"{this.ShortName.ToLowerInvariant()}-{this.Id}";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AIStudio.Tools.PluginSystem;

public static class InternalPluginExtensions
{
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
{
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),

_ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
};
}
20 changes: 20 additions & 0 deletions app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Lua;

namespace AIStudio.Tools.PluginSystem;

/// <summary>
/// This Lua module loader does not load any modules.
/// </summary>
public sealed class NoModuleLoader : ILuaModuleLoader
{
#region Implementation of ILuaModuleLoader

public bool Exists(string moduleName) => false;

public ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(new LuaModule());
}

#endregion
}
Loading