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