diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs new file mode 100644 index 00000000..0a287a2b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs @@ -0,0 +1,150 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.Builder; + +internal sealed partial class LuaResponse +{ + private static readonly JsonSerializerOptions JSON_OPTIONS = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + AllowTrailingCommas = false, + ReadCommentHandling = JsonCommentHandling.Disallow, + MaxDepth = 32, + }; + + public static bool TryParse(string modelResponse, out LuaResponse response, out LuaResponseParseError error, out string technicalDetails) + { + response = new(); + error = LuaResponseParseError.NONE; + technicalDetails = string.Empty; + + var json = ExtractJson(modelResponse); + if (string.IsNullOrWhiteSpace(json)) + { + error = LuaResponseParseError.MISSING_JSON_OBJECT; + return false; + } + + LuaResponse? parsed; + try + { + parsed = JsonSerializer.Deserialize(json, JSON_OPTIONS); + } + catch (JsonException e) + { + error = LuaResponseParseError.INVALID_JSON; + technicalDetails = e.Message; + return false; + } + + if (parsed is null) + { + error = LuaResponseParseError.EMPTY_JSON_OBJECT; + return false; + } + + if (!parsed.IsValid(out error)) + return false; + + response = parsed; + return true; + } + + private bool IsValid(out LuaResponseParseError error) + { + error = LuaResponseParseError.NONE; + + if (!string.Equals(this.SchemaVersion, SCHEMA_VERSION_VALUE, StringComparison.Ordinal)) + { + error = LuaResponseParseError.UNSUPPORTED_SCHEMA_VERSION; + return false; + } + + if (this.Plugin is null) + { + error = LuaResponseParseError.MISSING_PLUGIN_METADATA; + return false; + } + + if (this.Assistant is null) + { + error = LuaResponseParseError.MISSING_ASSISTANT_METADATA; + return false; + } + + if (string.IsNullOrWhiteSpace(this.Plugin.Name) || + string.IsNullOrWhiteSpace(this.Plugin.Description) || + this.Plugin.Categories is null || + this.Plugin.Categories.Length == 0 || + this.Plugin.Categories.Any(string.IsNullOrWhiteSpace)) + { + error = LuaResponseParseError.INCOMPLETE_PLUGIN_METADATA; + return false; + } + + if (string.IsNullOrWhiteSpace(this.Assistant.Title) || + string.IsNullOrWhiteSpace(this.Assistant.Description) || + string.IsNullOrWhiteSpace(this.Assistant.SystemPrompt) || + string.IsNullOrWhiteSpace(this.Assistant.SubmitText)) + { + error = LuaResponseParseError.INCOMPLETE_ASSISTANT_METADATA; + return false; + } + + if (string.IsNullOrWhiteSpace(this.FullLua)) + { + error = LuaResponseParseError.MISSING_LUA; + return false; + } + + if (!this.FullLua.Contains("ID = \"", StringComparison.Ordinal)) + { + error = LuaResponseParseError.LUA_MISSING_ID; + return false; + } + + return true; + } + + private static string ExtractJson(string input) + { + var start = input.IndexOf('{'); + if (start < 0) + return string.Empty; + + var depth = 0; + var insideString = false; + for (var index = start; index < input.Length; index++) + { + if (input[index] == '"' && !IsEscaped(input, index)) + insideString = !insideString; + + if (insideString) + continue; + + switch (input[index]) + { + case '{': + depth++; + break; + case '}': + depth--; + break; + } + + if (depth == 0) + return input[start..(index + 1)]; + } + + return string.Empty; + } + + private static bool IsEscaped(string input, int index) + { + var backslashCount = 0; + for (var i = index - 1; i >= 0 && input[i] == '\\'; i--) + backslashCount++; + + return backslashCount % 2 == 1; + } +} diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponse.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.cs new file mode 100644 index 00000000..7a11bf02 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Assistants.Builder; + +internal sealed partial class LuaResponse +{ + public const string SCHEMA_VERSION_VALUE = "assistant_builder_lua_response_v1"; + public string SchemaVersion { get; init; } = string.Empty; + public AssistantBuilderPluginMetadata? Plugin { get; init; } + public AssistantBuilderAssistantMetadata? Assistant { get; init; } + public string FullLua { get; init; } = string.Empty; +} + +internal sealed class AssistantBuilderPluginMetadata +{ + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string[] Categories { get; init; } = []; +} + +internal sealed class AssistantBuilderAssistantMetadata +{ + public string Title { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string SystemPrompt { get; init; } = string.Empty; + public string SubmitText { get; init; } = string.Empty; + public bool AllowAiStudioProfiles { get; init; } +} diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs new file mode 100644 index 00000000..4b7ed309 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs @@ -0,0 +1,38 @@ +namespace AIStudio.Assistants.Builder; + +public enum LuaResponseParseError +{ + NONE, + MISSING_JSON_OBJECT, + INVALID_JSON, + EMPTY_JSON_OBJECT, + UNSUPPORTED_SCHEMA_VERSION, + MISSING_PLUGIN_METADATA, + MISSING_ASSISTANT_METADATA, + INCOMPLETE_PLUGIN_METADATA, + INCOMPLETE_ASSISTANT_METADATA, + MISSING_LUA, + LUA_MISSING_ID, +} + +public static class LuaResponseParseErrorExtension +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseErrorExtension).Namespace, nameof(LuaResponseParseErrorExtension)); + + public static string GetMessage(this LuaResponseParseError parseError, string technicalDetails) => parseError switch + { + LuaResponseParseError.MISSING_JSON_OBJECT => TB("The model response is missing or unreadable."), + LuaResponseParseError.INVALID_JSON => string.IsNullOrWhiteSpace(technicalDetails) + ? TB("The model returned an invalid response.") + : string.Format(TB("The model returned an invalid response: {0}"), technicalDetails), + LuaResponseParseError.EMPTY_JSON_OBJECT => TB("The model returned an empty JSON object."), + LuaResponseParseError.UNSUPPORTED_SCHEMA_VERSION => TB("The model responded with an unsupported or deprecated JSON schema."), + LuaResponseParseError.MISSING_PLUGIN_METADATA => TB("The model's answer is missing the plugin metadata."), + LuaResponseParseError.MISSING_ASSISTANT_METADATA => TB("The model's answer is missing the assistant metadata."), + LuaResponseParseError.INCOMPLETE_PLUGIN_METADATA => TB("The model's answer contains incomplete plugin metadata."), + LuaResponseParseError.INCOMPLETE_ASSISTANT_METADATA => TB("The model's answer contains incomplete assistant metadata."), + LuaResponseParseError.MISSING_LUA => TB("The model response does not contain the generated Lua plugin code."), + LuaResponseParseError.LUA_MISSING_ID => TB("The generated Lua plugin code does not contain a readable plugin ID."), + _ => TB("The model returned an unusable JSON response."), + }; +}