diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
new file mode 100644
index 00000000..d40d2237
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
@@ -0,0 +1,20 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// This Lua module loader does not load any modules.
+///
+public sealed class NoModuleLoader : ILuaModuleLoader
+{
+ #region Implementation of ILuaModuleLoader
+
+ public bool Exists(string moduleName) => false;
+
+ public ValueTask LoadAsync(string moduleName, CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult(new LuaModule());
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
new file mode 100644
index 00000000..ab08241f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
@@ -0,0 +1,10 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a plugin that could not be loaded.
+///
+/// The Lua state that the plugin was loaded into.
+/// The error message that occurred while parsing the plugin.
+public sealed class NoPlugin(LuaState state, string parsingError) : PluginBase(state, PluginType.NONE, parsingError);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
new file mode 100644
index 00000000..b48fab7f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
@@ -0,0 +1,327 @@
+using Lua;
+
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the base of any AI Studio plugin.
+///
+public abstract class PluginBase
+{
+ private readonly string parseError;
+
+ protected readonly LuaState state;
+ protected readonly Guid pluginId;
+ protected readonly string pluginName;
+ protected readonly PluginType pluginType;
+ protected readonly string pluginDescription;
+ protected readonly PluginVersion pluginVersion;
+ protected readonly string[] pluginAuthors;
+ protected readonly string supportContact;
+ protected readonly string sourceURL;
+ protected readonly PluginCategory[] pluginCategories;
+ protected readonly PluginTargetGroup[] pluginTargetGroups;
+
+ private readonly bool isInitialized;
+ private bool isValid;
+
+ protected PluginBase(LuaState state, PluginType type, string parseError = "")
+ {
+ this.state = state;
+ this.pluginType = type;
+ this.pluginId = this.Id();
+ this.pluginName = this.Name();
+ this.pluginDescription = this.Description();
+ this.pluginVersion = this.Version();
+ this.pluginAuthors = this.Authors();
+ this.supportContact = this.SupportContact();
+ this.sourceURL = this.SourceURL();
+ this.pluginCategories = this.Categories();
+ this.pluginTargetGroups = this.TargetGroups();
+ this.parseError = parseError;
+
+ // For security reasons, we don't want to allow the plugin to load modules:
+ this.state.ModuleLoader = new NoModuleLoader();
+
+ //
+ // Check if the plugin is valid:
+ //
+ if(!string.IsNullOrWhiteSpace(this.parseError))
+ this.isValid = false;
+
+ if(this is NoPlugin)
+ this.isValid = false;
+
+ this.isInitialized = true;
+ }
+
+ ///
+ /// Checks if the plugin is valid.
+ ///
+ /// The state of the plugin, which may contain an error message.
+ public PluginState IsValid()
+ {
+ if(!string.IsNullOrWhiteSpace(this.parseError))
+ {
+ this.isValid = false;
+ return new(false, this.parseError);
+ }
+
+ if(this is NoPlugin)
+ {
+ this.isValid = false;
+ return new(false, "Plugin is not valid.");
+ }
+
+ if(this.Id() == Guid.Empty)
+ {
+ this.isValid = false;
+ return new(false, "The field ID does not exist, is empty, or is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).");
+ }
+
+ if(string.IsNullOrWhiteSpace(this.Name()))
+ {
+ this.isValid = false;
+ return new(false, "The field NAME does not exist, is empty, or is not a valid string.");
+ }
+
+ if(string.IsNullOrWhiteSpace(this.Description()))
+ {
+ this.isValid = false;
+ return new(false, "The field DESCRIPTION does not exist, is empty, or is not a valid string.");
+ }
+
+ if(this.Version() == PluginVersion.NONE)
+ {
+ this.isValid = false;
+ return new(false, "The field VERSION does not exist, is empty, or is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).");
+ }
+
+ if(this.pluginType == PluginType.NONE)
+ {
+ this.isValid = false;
+ return new(false, $"The field TYPE does not exist, is empty, or is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues(PluginType.NONE)}.");
+ }
+
+ if(this.Authors().Length == 0)
+ {
+ this.isValid = false;
+ return new(false, "The table AUTHORS does not exist, is empty, or is not a valid table of strings.");
+ }
+
+ if(string.IsNullOrWhiteSpace(this.SupportContact()))
+ {
+ this.isValid = false;
+ return new(false, "The field SUPPORT_CONTACT does not exist, is empty, or is not a valid string.");
+ }
+
+ if(string.IsNullOrWhiteSpace(this.SourceURL()))
+ {
+ this.isValid = false;
+ return new(false, "The field SOURCE_URL does not exist, is empty, or is not a valid string. Additional, it must start with 'http://' or 'https://'.");
+ }
+
+ if(this.Categories().Length == 0)
+ {
+ this.isValid = false;
+ return new(false, $"The table CATEGORIES does not exist, is empty, or is not a valid table of strings. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}.");
+ }
+
+ if(this.TargetGroups().Length == 0)
+ {
+ this.isValid = false;
+ return new(false, $"The table TARGET_GROUPS does not exist, is empty, or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}.");
+ }
+
+ this.isValid = true;
+ return new(true, string.Empty);
+ }
+
+ ///
+ /// Returns the intended target groups for the plugin.
+ ///
+ /// The target groups.
+ public PluginTargetGroup[] TargetGroups()
+ {
+ if(this.isInitialized)
+ return this.pluginTargetGroups;
+
+ if(!this.isValid)
+ return [];
+
+ if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroups))
+ return [];
+
+ var targetGroupList = new List();
+ foreach(var luaTargetGroup in targetGroups.GetArraySpan())
+ if(luaTargetGroup.TryRead(out var targetGroupName))
+ if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE)
+ targetGroupList.Add(targetGroup);
+
+ return targetGroupList.ToArray();
+ }
+
+ ///
+ /// Returns the plugin categories.
+ ///
+ /// The plugin categories.
+ public PluginCategory[] Categories()
+ {
+ if(this.isInitialized)
+ return this.pluginCategories;
+
+ if(!this.isValid)
+ return [];
+
+ if (!this.state.Environment["CATEGORIES"].TryRead(out var categories))
+ return [];
+
+ var categoryList = new List();
+ foreach(var luaCategory in categories.GetArraySpan())
+ if(luaCategory.TryRead(out var categoryName))
+ if(Enum.TryParse(categoryName, out var category) && category != PluginCategory.NONE)
+ categoryList.Add(category);
+
+ return categoryList.ToArray();
+ }
+
+ ///
+ /// Returns the source URL of the plugin.
+ ///
+ /// The source URL.
+ public string SourceURL()
+ {
+ if(this.isInitialized)
+ return this.sourceURL;
+
+ if(!this.isValid)
+ return string.Empty;
+
+ if (!this.state.Environment["SOURCE_URL"].TryRead(out var url))
+ return string.Empty;
+
+ if(!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
+ return string.Empty;
+
+ return url;
+ }
+
+ ///
+ /// Returns the support contact of the plugin.
+ ///
+ /// The support contact.
+ public string SupportContact()
+ {
+ if(this.isInitialized)
+ return this.supportContact;
+
+ if(!this.isValid)
+ return string.Empty;
+
+ if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out var contact))
+ return string.Empty;
+
+ return contact;
+ }
+
+ ///
+ /// Returns the ID of the plugin.
+ ///
+ /// The plugin ID.
+ public Guid Id()
+ {
+ if(this.isInitialized)
+ return this.pluginId;
+
+ if(!this.isValid)
+ return Guid.Empty;
+
+ if (!this.state.Environment["ID"].TryRead(out var idText))
+ return Guid.Empty;
+
+ if (!Guid.TryParse(idText, out var id))
+ return Guid.Empty;
+
+ return id;
+ }
+
+ ///
+ /// Returns the name of the plugin.
+ ///
+ /// The plugin name.
+ public string Name()
+ {
+ if(this.isInitialized)
+ return this.pluginName;
+
+ if(!this.isValid)
+ return string.Empty;
+
+ if (!this.state.Environment["NAME"].TryRead(out var name))
+ return string.Empty;
+
+ return name;
+ }
+
+ ///
+ /// Returns the description of the plugin.
+ ///
+ /// The plugin description.
+ public string Description()
+ {
+ if(this.isInitialized)
+ return this.pluginDescription;
+
+ if(!this.isValid)
+ return string.Empty;
+
+ if (!this.state.Environment["DESCRIPTION"].TryRead(out var description))
+ return string.Empty;
+
+ return description;
+ }
+
+ ///
+ /// Returns the version of the plugin.
+ ///
+ /// The plugin version.
+ public PluginVersion Version()
+ {
+ if(this.isInitialized)
+ return this.pluginVersion;
+
+ if(!this.isValid)
+ return PluginVersion.NONE;
+
+ if (!this.state.Environment["VERSION"].TryRead(out var versionText))
+ return PluginVersion.NONE;
+
+ if (!PluginVersion.TryParse(versionText, out var version))
+ return PluginVersion.NONE;
+
+ return version;
+ }
+
+ ///
+ /// Returns the authors of the plugin.
+ ///
+ /// The plugin authors.
+ public string[] Authors()
+ {
+ if(this.isInitialized)
+ return this.pluginAuthors;
+
+ if (!this.isValid)
+ return [];
+
+ if (!this.state.Environment["AUTHORS"].TryRead(out var authors))
+ return [];
+
+ var authorList = new List();
+ foreach(var author in authors.GetArraySpan())
+ if(author.TryRead(out var authorName))
+ authorList.Add(authorName);
+
+ return authorList.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
new file mode 100644
index 00000000..00afcd0e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
@@ -0,0 +1,33 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginCategory
+{
+ NONE,
+ CORE,
+
+ BUSINESS,
+ INDUSTRY,
+ UTILITY,
+ SOFTWARE_DEVELOPMENT,
+ GAMING,
+ EDUCATION,
+ ENTERTAINMENT,
+ SOCIAL,
+ SHOPPING,
+ TRAVEL,
+ HEALTH,
+ FITNESS,
+ FOOD,
+ PARTY,
+ SPORTS,
+ NEWS,
+ WEATHER,
+ MUSIC,
+ POLITICAL,
+ SCIENCE,
+ TECHNOLOGY,
+ ART,
+ FICTION,
+ WRITING,
+ CONTENT_CREATION,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
new file mode 100644
index 00000000..35303c06
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
@@ -0,0 +1,38 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginCategoryExtensions
+{
+ public static string GetName(this PluginCategory type) => type switch
+ {
+ PluginCategory.NONE => "None",
+ PluginCategory.CORE => "AI Studio Core",
+
+ PluginCategory.BUSINESS => "Business",
+ PluginCategory.INDUSTRY => "Industry",
+ PluginCategory.UTILITY => "Utility",
+ PluginCategory.SOFTWARE_DEVELOPMENT => "Software Development",
+ PluginCategory.GAMING => "Gaming",
+ PluginCategory.EDUCATION => "Education",
+ PluginCategory.ENTERTAINMENT => "Entertainment",
+ PluginCategory.SOCIAL => "Social",
+ PluginCategory.SHOPPING => "Shopping",
+ PluginCategory.TRAVEL => "Travel",
+ PluginCategory.HEALTH => "Health",
+ PluginCategory.FITNESS => "Fitness",
+ PluginCategory.FOOD => "Food",
+ PluginCategory.PARTY => "Party",
+ PluginCategory.SPORTS => "Sports",
+ PluginCategory.NEWS => "News",
+ PluginCategory.WEATHER => "Weather",
+ PluginCategory.MUSIC => "Music",
+ PluginCategory.POLITICAL => "Political",
+ PluginCategory.SCIENCE => "Science",
+ PluginCategory.TECHNOLOGY => "Technology",
+ PluginCategory.ART => "Art",
+ PluginCategory.FICTION => "Fiction",
+ PluginCategory.WRITING => "Writing",
+ PluginCategory.CONTENT_CREATION => "Content Creation",
+
+ _ => "Unknown plugin category",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
new file mode 100644
index 00000000..0f1ce8d9
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -0,0 +1,40 @@
+using System.Text;
+
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginFactory
+{
+ public static async Task LoadAll()
+ {
+
+ }
+
+ public static async Task Load(string code, CancellationToken cancellationToken = default)
+ {
+ var state = LuaState.Create();
+
+ try
+ {
+ await state.DoStringAsync(code, cancellationToken: cancellationToken);
+ }
+ catch (LuaParseException e)
+ {
+ return new NoPlugin(state, $"Was not able to parse the plugin: {e.Message}");
+ }
+
+ if (!state.Environment["TYPE"].TryRead(out var typeText))
+ return new NoPlugin(state, "TYPE does not exist or is not a valid string.");
+
+ if (!Enum.TryParse(typeText, out var type))
+ return new NoPlugin(state, $"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+
+ return type switch
+ {
+ PluginType.LANGUAGE => new PluginLanguage(state, type),
+
+ _ => new NoPlugin(state, "This plugin type is not supported yet. Please try again with a future version of AI Studio.")
+ };
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginState.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginState.cs
new file mode 100644
index 00000000..60b11790
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginState.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the state of a plugin.
+///
+/// True, when the plugin is valid.
+/// When the plugin is invalid, this contains the error message.
+public readonly record struct PluginState(bool Valid, string Message);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
new file mode 100644
index 00000000..102aa857
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
@@ -0,0 +1,20 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginTargetGroup
+{
+ NONE,
+
+ EVERYONE,
+ CHILDREN,
+ TEENAGERS,
+ STUDENTS,
+ ADULTS,
+
+ INDUSTRIAL_WORKERS,
+ OFFICE_WORKERS,
+ BUSINESS_PROFESSIONALS,
+ SOFTWARE_DEVELOPERS,
+ SCIENTISTS,
+ TEACHERS,
+ ARTISTS,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
new file mode 100644
index 00000000..7a14123f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
@@ -0,0 +1,25 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTargetGroupExtensions
+{
+ public static string Name(this PluginTargetGroup group) => group switch
+ {
+ PluginTargetGroup.NONE => "No target group",
+
+ PluginTargetGroup.EVERYONE => "Everyone",
+ PluginTargetGroup.CHILDREN => "Children",
+ PluginTargetGroup.TEENAGERS => "Teenagers",
+ PluginTargetGroup.STUDENTS => "Students",
+ PluginTargetGroup.ADULTS => "Adults",
+
+ PluginTargetGroup.INDUSTRIAL_WORKERS => "Industrial workers",
+ PluginTargetGroup.OFFICE_WORKERS => "Office workers",
+ PluginTargetGroup.BUSINESS_PROFESSIONALS => "Business professionals",
+ PluginTargetGroup.SOFTWARE_DEVELOPERS => "Software developers",
+ PluginTargetGroup.SCIENTISTS => "Scientists",
+ PluginTargetGroup.TEACHERS => "Teachers",
+ PluginTargetGroup.ARTISTS => "Artists",
+
+ _ => "Unknown target group",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
new file mode 100644
index 00000000..f897eb6e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
@@ -0,0 +1,10 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginType
+{
+ NONE,
+
+ LANGUAGE,
+ ASSISTANT,
+ CONFIGURATION,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
new file mode 100644
index 00000000..9ef0535c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
@@ -0,0 +1,13 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTypeExtensions
+{
+ public static string GetName(this PluginType type) => type switch
+ {
+ PluginType.LANGUAGE => "Language plugin",
+ PluginType.ASSISTANT => "Assistant plugin",
+ PluginType.CONFIGURATION => "Configuration plugin",
+
+ _ => "Unknown plugin type",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
new file mode 100644
index 00000000..d5507a56
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
@@ -0,0 +1,90 @@
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a version number for a plugin.
+///
+/// The major version number.
+/// The minor version number.
+/// The patch version number.
+public readonly record struct PluginVersion(int Major, int Minor, int Patch) : IComparable
+{
+ ///
+ /// Represents no version number.
+ ///
+ public static readonly PluginVersion NONE = new(0, 0, 0);
+
+ ///
+ /// Tries to parse the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// True when the input string was successfully parsed; otherwise, false.
+ public static bool TryParse(string input, out PluginVersion version)
+ {
+ try
+ {
+ version = Parse(input);
+ return true;
+ }
+ catch
+ {
+ version = NONE;
+ return false;
+ }
+ }
+
+ ///
+ /// Parses the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// The input string is not in the correct format.
+ public static PluginVersion Parse(string input)
+ {
+ var segments = input.Split('.');
+ if (segments.Length != 3)
+ throw new FormatException("The input string must be in the format 'major.minor.patch'.");
+
+ var major = int.Parse(segments[0]);
+ var minor = int.Parse(segments[1]);
+ var patch = int.Parse(segments[2]);
+
+ if(major < 0 || minor < 0 || patch < 0)
+ throw new FormatException("The major, minor, and patch numbers must be greater than or equal to 0.");
+
+ return new PluginVersion(major, minor, patch);
+ }
+
+ ///
+ /// Converts the plugin version number to a string in the format 'major.minor.patch'.
+ ///
+ /// The plugin version number as a string.
+ public override string ToString() => $"{this.Major}.{this.Minor}.{this.Patch}";
+
+ ///
+ /// Compares the plugin version number to another plugin version number.
+ ///
+ /// The other plugin version number to compare to.
+ /// A value indicating the relative order of the plugin version numbers.
+ public int CompareTo(PluginVersion other)
+ {
+ var majorCompare = this.Major.CompareTo(other.Major);
+ if (majorCompare != 0)
+ return majorCompare;
+
+ var minorCompare = this.Minor.CompareTo(other.Minor);
+ if (minorCompare != 0)
+ return minorCompare;
+
+ return this.Patch.CompareTo(other.Patch);
+ }
+
+ public static bool operator >(PluginVersion left, PluginVersion right) => left.CompareTo(right) > 0;
+
+ public static bool operator <(PluginVersion left, PluginVersion right) => left.CompareTo(right) < 0;
+
+ public static bool operator >=(PluginVersion left, PluginVersion right) => left.CompareTo(right) >= 0;
+
+ public static bool operator <=(PluginVersion left, PluginVersion right) => left.CompareTo(right) <= 0;
+}
\ No newline at end of file