diff --git a/app/AGENTS.md b/app/AGENTS.md index e88ddd5c6..70f1159f4 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -28,6 +28,7 @@ Key structure: ## Important Development Notes - **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. +- **Extension members** - Declare extension methods in an `extension` block of their static class, one block per receiver, never with a `this` parameter: MWAIS0016 refuses the classic form. The `` of the receiver, and of a generic block its ``, belongs on the block, not on its members. The one exception is a method with a `params` parameter, which stays classic until the SDK in `metadata.txt` carries the fix for dotnet/roslyn#80024: for a member of an extension block, the compiler warns about every argument passed one by one to a `params` parameter (CS8620), see `JsRuntimeExtensions`. - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app - **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends. - **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`. diff --git a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs index 4e7b05dd8..b0d4fb25d 100644 --- a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs +++ b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs @@ -6,37 +6,40 @@ public static class AssistantAuditLevelExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantAuditLevelExtensions).Namespace, nameof(AssistantAuditLevelExtensions)); - public static string GetName(this AssistantAuditLevel level) => level switch + extension(AssistantAuditLevel level) { - AssistantAuditLevel.DANGEROUS => TB("Dangerous"), - AssistantAuditLevel.CAUTION => TB("Concerning"), - AssistantAuditLevel.SAFE => TB("Safe"), - _ => TB("Unknown"), - }; - - public static Severity GetSeverity(this AssistantAuditLevel level) => level switch - { - AssistantAuditLevel.DANGEROUS => Severity.Error, - AssistantAuditLevel.CAUTION => Severity.Warning, - AssistantAuditLevel.SAFE => Severity.Success, - _ => Severity.Info, - }; + public string GetName() => level switch + { + AssistantAuditLevel.DANGEROUS => TB("Dangerous"), + AssistantAuditLevel.CAUTION => TB("Concerning"), + AssistantAuditLevel.SAFE => TB("Safe"), + _ => TB("Unknown"), + }; - public static Color GetColor(this AssistantAuditLevel level) => level switch - { - AssistantAuditLevel.DANGEROUS => Color.Error, - AssistantAuditLevel.CAUTION => Color.Warning, - AssistantAuditLevel.SAFE => Color.Success, - _ => Color.Default, - }; + public Severity GetSeverity() => level switch + { + AssistantAuditLevel.DANGEROUS => Severity.Error, + AssistantAuditLevel.CAUTION => Severity.Warning, + AssistantAuditLevel.SAFE => Severity.Success, + _ => Severity.Info, + }; - public static string GetIcon(this AssistantAuditLevel level) => level switch - { - AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous, - AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning, - AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified, - _ => Icons.Material.Filled.HelpOutline, - }; + public Color GetColor() => level switch + { + AssistantAuditLevel.DANGEROUS => Color.Error, + AssistantAuditLevel.CAUTION => Color.Warning, + AssistantAuditLevel.SAFE => Color.Success, + _ => Color.Default, + }; + + public string GetIcon() => level switch + { + AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous, + AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning, + AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified, + _ => Icons.Material.Filled.HelpOutline, + }; + } /// /// Parses an audit level string and falls back to when parsing fails. diff --git a/app/MindWork AI Studio/Assistants/Agenda/NumberParticipantsExtensions.cs b/app/MindWork AI Studio/Assistants/Agenda/NumberParticipantsExtensions.cs index 028ad223a..e582ec8fe 100644 --- a/app/MindWork AI Studio/Assistants/Agenda/NumberParticipantsExtensions.cs +++ b/app/MindWork AI Studio/Assistants/Agenda/NumberParticipantsExtensions.cs @@ -4,23 +4,26 @@ public static class NumberParticipantsExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(NumberParticipantsExtensions).Namespace, nameof(NumberParticipantsExtensions)); - public static string Name(this NumberParticipants numberParticipants) => numberParticipants switch + extension(NumberParticipants numberParticipants) { - NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"), + public string Name() => numberParticipants switch + { + NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"), - NumberParticipants.PEER_TO_PEER => TB("2 (peer to peer)"), + NumberParticipants.PEER_TO_PEER => TB("2 (peer to peer)"), - NumberParticipants.SMALL_GROUP => TB("3 - 5 (small group)"), - NumberParticipants.LARGE_GROUP => TB("6 - 12 (large group)"), - NumberParticipants.MULTIPLE_SMALL_GROUPS => TB("13 - 20 (multiple small groups)"), - NumberParticipants.MULTIPLE_LARGE_GROUPS => TB("21 - 30 (multiple large groups)"), + NumberParticipants.SMALL_GROUP => TB("3 - 5 (small group)"), + NumberParticipants.LARGE_GROUP => TB("6 - 12 (large group)"), + NumberParticipants.MULTIPLE_SMALL_GROUPS => TB("13 - 20 (multiple small groups)"), + NumberParticipants.MULTIPLE_LARGE_GROUPS => TB("21 - 30 (multiple large groups)"), - NumberParticipants.SYMPOSIUM => TB("31 - 100 (symposium)"), - NumberParticipants.CONFERENCE => TB("101 - 200 (conference)"), - NumberParticipants.CONGRESS => TB("201 - 1,000 (congress)"), + NumberParticipants.SYMPOSIUM => TB("31 - 100 (symposium)"), + NumberParticipants.CONFERENCE => TB("101 - 200 (conference)"), + NumberParticipants.CONGRESS => TB("201 - 1,000 (congress)"), - NumberParticipants.LARGE_EVENT => TB("1,000+ (large event)"), + NumberParticipants.LARGE_EVENT => TB("1,000+ (large event)"), - _ => "Unknown" - }; + _ => "Unknown" + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsvSeparatorExtensions.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsvSeparatorExtensions.cs index d6f08f06d..2d3225dbc 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsvSeparatorExtensions.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsvSeparatorExtensions.cs @@ -6,27 +6,30 @@ public static class BatchProcessingCsvSeparatorExtensions private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingCsvSeparatorExtensions).Namespace, nameof(BatchProcessingCsvSeparatorExtensions)); - public static string Name(this BatchProcessingCsvSeparator separator) => separator switch + extension(BatchProcessingCsvSeparator separator) { - BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"), - BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"), - BatchProcessingCsvSeparator.PIPE => TB("Vertical bar (|)"), - BatchProcessingCsvSeparator.TAB => TB("Tab"), - BatchProcessingCsvSeparator.CUSTOM => TB("Custom character"), + public string Name() => separator switch + { + BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"), + BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"), + BatchProcessingCsvSeparator.PIPE => TB("Vertical bar (|)"), + BatchProcessingCsvSeparator.TAB => TB("Tab"), + BatchProcessingCsvSeparator.CUSTOM => TB("Custom character"), - _ => TB("Unknown"), - }; + _ => TB("Unknown"), + }; - public static char Character(this BatchProcessingCsvSeparator separator, string customSeparator) => separator switch - { - BatchProcessingCsvSeparator.COMMA => ',', - BatchProcessingCsvSeparator.SEMICOLON => ';', - BatchProcessingCsvSeparator.PIPE => '|', - BatchProcessingCsvSeparator.TAB => '\t', - BatchProcessingCsvSeparator.CUSTOM when IsValidCustomSeparator(customSeparator) => customSeparator[0], + public char Character(string customSeparator) => separator switch + { + BatchProcessingCsvSeparator.COMMA => ',', + BatchProcessingCsvSeparator.SEMICOLON => ';', + BatchProcessingCsvSeparator.PIPE => '|', + BatchProcessingCsvSeparator.TAB => '\t', + BatchProcessingCsvSeparator.CUSTOM when IsValidCustomSeparator(customSeparator) => customSeparator[0], - _ => DEFAULT_SEPARATOR, - }; + _ => DEFAULT_SEPARATOR, + }; + } internal static bool IsValidCustomSeparator(string separator) { diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs index 3bdfe402c..0bf81ea2d 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs @@ -4,11 +4,14 @@ public static class BatchProcessingOutputModeExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingOutputModeExtensions).Namespace, nameof(BatchProcessingOutputModeExtensions)); - public static string Name(this BatchProcessingOutputMode outputMode) => outputMode switch + extension(BatchProcessingOutputMode outputMode) { - BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"), - BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"), + public string Name() => outputMode switch + { + BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"), + BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"), - _ => TB("Unknown output mode"), - }; + _ => TB("Unknown output mode"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingPromptSourceExtensions.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingPromptSourceExtensions.cs index 90ec8e9bd..e8c11cc99 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingPromptSourceExtensions.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingPromptSourceExtensions.cs @@ -4,12 +4,15 @@ public static class BatchProcessingPromptSourceExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingPromptSourceExtensions).Namespace, nameof(BatchProcessingPromptSourceExtensions)); - public static string Name(this BatchProcessingPromptSource promptSource) => promptSource switch + extension(BatchProcessingPromptSource promptSource) { - BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"), - BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"), - BatchProcessingPromptSource.FILE_IMPORT => TB("Import from a file (.md)"), + public string Name() => promptSource switch + { + BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"), + BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"), + BatchProcessingPromptSource.FILE_IMPORT => TB("Import from a file (.md)"), - _ => TB("Unknown prompt source"), - }; + _ => TB("Unknown prompt source"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseErrorExtension.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseErrorExtension.cs index 66795d248..2ea73e18d 100644 --- a/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseErrorExtension.cs +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseErrorExtension.cs @@ -2,22 +2,25 @@ namespace AIStudio.Assistants.Builder; public static class LuaResponseParseErrorExtension { - private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseError).Namespace, nameof(LuaResponseParseError)); + 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 + extension(LuaResponseParseError parseError) { - 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."), - }; + public string GetMessage(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."), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/Coding/CommonCodingLanguageExtensions.cs b/app/MindWork AI Studio/Assistants/Coding/CommonCodingLanguageExtensions.cs index fab5cc064..7c3721376 100644 --- a/app/MindWork AI Studio/Assistants/Coding/CommonCodingLanguageExtensions.cs +++ b/app/MindWork AI Studio/Assistants/Coding/CommonCodingLanguageExtensions.cs @@ -4,42 +4,45 @@ public static class CommonCodingLanguageExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(CommonCodingLanguageExtensions).Namespace, nameof(CommonCodingLanguageExtensions)); - public static string Name(this CommonCodingLanguages language) => language switch - { - CommonCodingLanguages.NONE => TB("None"), + extension(CommonCodingLanguages language) + { + public string Name() => language switch + { + CommonCodingLanguages.NONE => TB("None"), - CommonCodingLanguages.BASH => "Bash", - CommonCodingLanguages.BLAZOR => ".NET Blazor", - CommonCodingLanguages.C => "C", - CommonCodingLanguages.CPP => "C++", - CommonCodingLanguages.CSHARP => "C#", - CommonCodingLanguages.CSS => "CSS", - CommonCodingLanguages.FORTRAN => "Fortran", - CommonCodingLanguages.GDSCRIPT => "GDScript", - CommonCodingLanguages.GO => "Go", - CommonCodingLanguages.HTML => "HTML", - CommonCodingLanguages.JAVA => "Java", - CommonCodingLanguages.JAVASCRIPT => "JavaScript", - CommonCodingLanguages.JSON => "JSON", - CommonCodingLanguages.JULIA => "Julia", - CommonCodingLanguages.KOTLIN => "Kotlin", - CommonCodingLanguages.LUA => "Lua", - CommonCodingLanguages.MARKDOWN => "Markdown", - CommonCodingLanguages.MATHEMATICA => "Mathematica", - CommonCodingLanguages.MATLAB => "MATLAB", - CommonCodingLanguages.PHP => "PHP", - CommonCodingLanguages.POWERSHELL => "PowerShell", - CommonCodingLanguages.PROLOG => "Prolog", - CommonCodingLanguages.PYTHON => "Python", - CommonCodingLanguages.R => "R", - CommonCodingLanguages.RUBY => "Ruby", - CommonCodingLanguages.RUST => "Rust", - CommonCodingLanguages.SQL => "SQL", - CommonCodingLanguages.SWIFT => "Swift", - CommonCodingLanguages.TYPESCRIPT => "TypeScript", - CommonCodingLanguages.XML => "XML", + CommonCodingLanguages.BASH => "Bash", + CommonCodingLanguages.BLAZOR => ".NET Blazor", + CommonCodingLanguages.C => "C", + CommonCodingLanguages.CPP => "C++", + CommonCodingLanguages.CSHARP => "C#", + CommonCodingLanguages.CSS => "CSS", + CommonCodingLanguages.FORTRAN => "Fortran", + CommonCodingLanguages.GDSCRIPT => "GDScript", + CommonCodingLanguages.GO => "Go", + CommonCodingLanguages.HTML => "HTML", + CommonCodingLanguages.JAVA => "Java", + CommonCodingLanguages.JAVASCRIPT => "JavaScript", + CommonCodingLanguages.JSON => "JSON", + CommonCodingLanguages.JULIA => "Julia", + CommonCodingLanguages.KOTLIN => "Kotlin", + CommonCodingLanguages.LUA => "Lua", + CommonCodingLanguages.MARKDOWN => "Markdown", + CommonCodingLanguages.MATHEMATICA => "Mathematica", + CommonCodingLanguages.MATLAB => "MATLAB", + CommonCodingLanguages.PHP => "PHP", + CommonCodingLanguages.POWERSHELL => "PowerShell", + CommonCodingLanguages.PROLOG => "Prolog", + CommonCodingLanguages.PYTHON => "Python", + CommonCodingLanguages.R => "R", + CommonCodingLanguages.RUBY => "Ruby", + CommonCodingLanguages.RUST => "Rust", + CommonCodingLanguages.SQL => "SQL", + CommonCodingLanguages.SWIFT => "Swift", + CommonCodingLanguages.TYPESCRIPT => "TypeScript", + CommonCodingLanguages.XML => "XML", - CommonCodingLanguages.OTHER => TB("Other"), - _ => TB("Unknown") - }; + CommonCodingLanguages.OTHER => TB("Other"), + _ => TB("Unknown") + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/EMail/WritingStylesExtensions.cs b/app/MindWork AI Studio/Assistants/EMail/WritingStylesExtensions.cs index 7c9ddffb1..6075894a1 100644 --- a/app/MindWork AI Studio/Assistants/EMail/WritingStylesExtensions.cs +++ b/app/MindWork AI Studio/Assistants/EMail/WritingStylesExtensions.cs @@ -4,23 +4,26 @@ public static class WritingStylesExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions)); - public static string Name(this WritingStyles style) => style switch + extension(WritingStyles style) { - WritingStyles.ACADEMIC => TB("Academic"), - WritingStyles.PERSONAL => TB("Personal"), - WritingStyles.BUSINESS_FORMAL => TB("Business formal"), - WritingStyles.BUSINESS_INFORMAL => TB("Business informal"), + public string Name() => style switch + { + WritingStyles.ACADEMIC => TB("Academic"), + WritingStyles.PERSONAL => TB("Personal"), + WritingStyles.BUSINESS_FORMAL => TB("Business formal"), + WritingStyles.BUSINESS_INFORMAL => TB("Business informal"), - _ => TB("Not specified"), - }; - - public static string Prompt(this WritingStyles style) => style switch - { - WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.", - WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.", - WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.", - WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.", + _ => TB("Not specified"), + }; + + public string Prompt() => style switch + { + WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.", + WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.", + WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.", + WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.", - _ => "Use a formal business style for this e-mail.", - }; + _ => "Use a formal business style for this e-mail.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs index c9cfb3f2d..0156c7b64 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs @@ -4,12 +4,15 @@ public static class AllowedLLMProvidersExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AllowedLLMProvidersExtensions).Namespace, nameof(AllowedLLMProvidersExtensions)); - public static string Description(this AllowedLLMProviders provider) => provider switch + extension(AllowedLLMProviders provider) { - AllowedLLMProviders.NONE => TB("Please select what kind of LLM provider are allowed for this data source"), - AllowedLLMProviders.ANY => TB("Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider"), - AllowedLLMProviders.SELF_HOSTED => TB("Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider"), + public string Description() => provider switch + { + AllowedLLMProviders.NONE => TB("Please select what kind of LLM provider are allowed for this data source"), + AllowedLLMProviders.ANY => TB("Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider"), + AllowedLLMProviders.SELF_HOSTED => TB("Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider"), - _ => TB("Unknown option was selected") - }; + _ => TB("Unknown option was selected") + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs index 32be6d70c..789132bf2 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs @@ -4,25 +4,28 @@ public static class AuthExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AuthExtensions).Namespace, nameof(AuthExtensions)); - public static string Name(this Auth auth) => auth switch + extension(Auth auth) { - Auth.NONE => TB("No login necessary: useful for public data sources"), + public string Name() => auth switch + { + Auth.NONE => TB("No login necessary: useful for public data sources"), - Auth.KERBEROS => TB("Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users"), - Auth.USERNAME_PASSWORD => TB("Login by username and password: simple to implement and to operate, useful for few users; easy to use for users"), - Auth.TOKEN => TB("Login by token: simple to implement and to operate, useful for few users; unusual for many users"), + Auth.KERBEROS => TB("Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users"), + Auth.USERNAME_PASSWORD => TB("Login by username and password: simple to implement and to operate, useful for few users; easy to use for users"), + Auth.TOKEN => TB("Login by token: simple to implement and to operate, useful for few users; unusual for many users"), - _ => TB("Unknown login method") - }; - - public static string ToPrompt(this Auth auth) => auth switch - { - Auth.NONE => "No login is necessary, the data source is public.", + _ => TB("Unknown login method") + }; + + public string ToPrompt() => auth switch + { + Auth.NONE => "No login is necessary, the data source is public.", - Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.", - Auth.USERNAME_PASSWORD => "Login by username and password.", - Auth.TOKEN => "Login by static token per user.", + Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.", + Auth.USERNAME_PASSWORD => "Login by username and password.", + Auth.TOKEN => "Login by static token per user.", - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs index deeecc1ea..133033dce 100644 --- a/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs @@ -4,18 +4,21 @@ public static class DataSourcesExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(DataSourcesExtensions).Namespace, nameof(DataSourcesExtensions)); - public static string Name(this DataSources dataSource) => dataSource switch + extension(DataSources dataSource) { - DataSources.NONE => TB("No data source selected"), - DataSources.CUSTOM => TB("Custom description"), + public string Name() => dataSource switch + { + DataSources.NONE => TB("No data source selected"), + DataSources.CUSTOM => TB("Custom description"), - DataSources.FILE_SYSTEM => TB("File system (local or network share)"), - DataSources.OBJECT_STORAGE => TB("Object storage, like Amazon S3, MinIO, etc."), - DataSources.KEY_VALUE_STORE => TB("Key-Value store, like Redis, etc."), - DataSources.DOCUMENT_STORE => TB("Document store, like MongoDB, etc."), - DataSources.RELATIONAL_DATABASE => TB("Relational database, like MySQL, PostgreSQL, etc."), - DataSources.GRAPH_DATABASE => TB("Graph database, like Neo4j, ArangoDB, etc."), + DataSources.FILE_SYSTEM => TB("File system (local or network share)"), + DataSources.OBJECT_STORAGE => TB("Object storage, like Amazon S3, MinIO, etc."), + DataSources.KEY_VALUE_STORE => TB("Key-Value store, like Redis, etc."), + DataSources.DOCUMENT_STORE => TB("Document store, like MongoDB, etc."), + DataSources.RELATIONAL_DATABASE => TB("Relational database, like MySQL, PostgreSQL, etc."), + DataSources.GRAPH_DATABASE => TB("Graph database, like Neo4j, ArangoDB, etc."), - _ => TB("Unknown data source") - }; + _ => TB("Unknown data source") + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIServerFileOutcomeExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ERIServerFileOutcomeExtensions.cs index b7ae72714..0ee193312 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ERIServerFileOutcomeExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ERIServerFileOutcomeExtensions.cs @@ -11,13 +11,16 @@ internal static class ERIServerFileOutcomeExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ERIServerFileOutcomeExtensions).Namespace, nameof(ERIServerFileOutcomeExtensions)); - /// - /// Gets the localized message which explains why a file was not written. - /// /// What became of the file. - /// The path of the file, as the model named it. - /// The localized message. - internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file); + extension(ERIServerFileOutcome outcome) + { + /// + /// Gets the localized message which explains why a file was not written. + /// + /// The path of the file, as the model named it. + /// The localized message. + internal string ToUserMessage(string file) => string.Format(ToUserMessageFormat(outcome), file); + } private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch { diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs index 78d095c83..2cb7578da 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs @@ -2,14 +2,17 @@ namespace AIStudio.Assistants.ERI; public static class ERIVersionExtensions { - public static Task ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL()); - - public static string SpecificationURL(this ERIVersion version) + extension(ERIVersion version) { - var nameLower = version.ToString().ToLowerInvariant(); - var filename = $"{nameLower}.json"; - return $"specs/eri/{filename}"; - } + public Task ReadSpecification(WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL()); - public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE; + public string SpecificationURL() + { + var nameLower = version.ToString().ToLowerInvariant(); + var filename = $"{nameLower}.json"; + return $"specs/eri/{filename}"; + } + + public bool WasSpecificationSelected() => version != ERIVersion.NONE; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs index cfb02a5c8..6259d7559 100644 --- a/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs @@ -4,23 +4,26 @@ public static class OperatingSystemExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(OperatingSystemExtensions).Namespace, nameof(OperatingSystemExtensions)); - public static string Name(this OperatingSystem os) => os switch + extension(OperatingSystem os) { - OperatingSystem.NONE => TB("No operating system specified"), + public string Name() => os switch + { + OperatingSystem.NONE => TB("No operating system specified"), - OperatingSystem.WINDOWS => TB("Windows"), - OperatingSystem.LINUX => TB("Linux"), + OperatingSystem.WINDOWS => TB("Windows"), + OperatingSystem.LINUX => TB("Linux"), - _ => TB("Unknown operating system") - }; - - public static string ToPrompt(this OperatingSystem os) => os switch - { - OperatingSystem.NONE => "No operating system specified", + _ => TB("Unknown operating system") + }; + + public string ToPrompt() => os switch + { + OperatingSystem.NONE => "No operating system specified", - OperatingSystem.WINDOWS => "Windows", - OperatingSystem.LINUX => "Linux", + OperatingSystem.WINDOWS => "Windows", + OperatingSystem.LINUX => "Linux", - _ => "Unknown operating system" - }; + _ => "Unknown operating system" + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs index 6bd59d71a..ffd1626de 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs @@ -4,43 +4,46 @@ public static class ProgrammingLanguagesExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ProgrammingLanguagesExtensions).Namespace, nameof(ProgrammingLanguagesExtensions)); - public static string Name(this ProgrammingLanguages language) => language switch - { - ProgrammingLanguages.NONE => TB("No programming language selected"), + extension(ProgrammingLanguages language) + { + public string Name() => language switch + { + ProgrammingLanguages.NONE => TB("No programming language selected"), - ProgrammingLanguages.C => "C", - ProgrammingLanguages.CPP => "C++", - ProgrammingLanguages.CSHARP => "C#", - ProgrammingLanguages.GO => "Go", - ProgrammingLanguages.JAVA => "Java", - ProgrammingLanguages.JAVASCRIPT => "JavaScript", - ProgrammingLanguages.JULIA => "Julia", - ProgrammingLanguages.MATLAB => "MATLAB", - ProgrammingLanguages.PHP => "PHP", - ProgrammingLanguages.PYTHON => "Python", - ProgrammingLanguages.RUST => "Rust", + ProgrammingLanguages.C => "C", + ProgrammingLanguages.CPP => "C++", + ProgrammingLanguages.CSHARP => "C#", + ProgrammingLanguages.GO => "Go", + ProgrammingLanguages.JAVA => "Java", + ProgrammingLanguages.JAVASCRIPT => "JavaScript", + ProgrammingLanguages.JULIA => "Julia", + ProgrammingLanguages.MATLAB => "MATLAB", + ProgrammingLanguages.PHP => "PHP", + ProgrammingLanguages.PYTHON => "Python", + ProgrammingLanguages.RUST => "Rust", - ProgrammingLanguages.OTHER => TB("Other"), - _ => TB("Unknown") - }; - - public static string ToPrompt(this ProgrammingLanguages language) => language switch - { - ProgrammingLanguages.NONE => "No programming language selected", + ProgrammingLanguages.OTHER => TB("Other"), + _ => TB("Unknown") + }; + + public string ToPrompt() => language switch + { + ProgrammingLanguages.NONE => "No programming language selected", - ProgrammingLanguages.C => "C", - ProgrammingLanguages.CPP => "C++", - ProgrammingLanguages.CSHARP => "C#", - ProgrammingLanguages.GO => "Go", - ProgrammingLanguages.JAVA => "Java", - ProgrammingLanguages.JAVASCRIPT => "JavaScript", - ProgrammingLanguages.JULIA => "Julia", - ProgrammingLanguages.MATLAB => "MATLAB", - ProgrammingLanguages.PHP => "PHP", - ProgrammingLanguages.PYTHON => "Python", - ProgrammingLanguages.RUST => "Rust", + ProgrammingLanguages.C => "C", + ProgrammingLanguages.CPP => "C++", + ProgrammingLanguages.CSHARP => "C#", + ProgrammingLanguages.GO => "Go", + ProgrammingLanguages.JAVA => "Java", + ProgrammingLanguages.JAVASCRIPT => "JavaScript", + ProgrammingLanguages.JULIA => "Julia", + ProgrammingLanguages.MATLAB => "MATLAB", + ProgrammingLanguages.PHP => "PHP", + ProgrammingLanguages.PYTHON => "Python", + ProgrammingLanguages.RUST => "Rust", - ProgrammingLanguages.OTHER => "Other", - _ => "Unknown" - }; + ProgrammingLanguages.OTHER => "Other", + _ => "Unknown" + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/IconFinder/IconSourceExtensions.cs b/app/MindWork AI Studio/Assistants/IconFinder/IconSourceExtensions.cs index f7112cfc4..e8c3231fd 100644 --- a/app/MindWork AI Studio/Assistants/IconFinder/IconSourceExtensions.cs +++ b/app/MindWork AI Studio/Assistants/IconFinder/IconSourceExtensions.cs @@ -2,39 +2,42 @@ namespace AIStudio.Assistants.IconFinder; public static class IconSourceExtensions { - public static string Name(this IconSources iconSource) => iconSource switch + extension(IconSources iconSource) { - IconSources.FLAT_ICON => "Flaticon", - IconSources.FONT_AWESOME => "Font Awesome", - IconSources.MATERIAL_ICONS => "Material Icons", - IconSources.FEATHER_ICONS => "Feather Icons", - IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons", - IconSources.ICONS8 => "Icons8", + public string Name() => iconSource switch + { + IconSources.FLAT_ICON => "Flaticon", + IconSources.FONT_AWESOME => "Font Awesome", + IconSources.MATERIAL_ICONS => "Material Icons", + IconSources.FEATHER_ICONS => "Feather Icons", + IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons", + IconSources.ICONS8 => "Icons8", - _ => "Generic", - }; - - public static string Prompt(this IconSources iconSource) => iconSource switch - { - IconSources.FLAT_ICON => "My icon source is Flaticon.", - IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.", - IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.", - IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.", - IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.", - IconSources.ICONS8 => "I look for an icon on Icons8.", - - _ => string.Empty, - }; + _ => "Generic", + }; - public static string URL(this IconSources iconSource) => iconSource switch - { - IconSources.FLAT_ICON => "https://www.flaticon.com/", - IconSources.FONT_AWESOME => "https://fontawesome.com/", - IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/", - IconSources.FEATHER_ICONS => "https://feathericons.com/", - IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/", - IconSources.ICONS8 => "https://icons8.com/", + public string Prompt() => iconSource switch + { + IconSources.FLAT_ICON => "My icon source is Flaticon.", + IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.", + IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.", + IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.", + IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.", + IconSources.ICONS8 => "I look for an icon on Icons8.", - _ => string.Empty, - }; + _ => string.Empty, + }; + + public string URL() => iconSource switch + { + IconSources.FLAT_ICON => "https://www.flaticon.com/", + IconSources.FONT_AWESOME => "https://fontawesome.com/", + IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/", + IconSources.FEATHER_ICONS => "https://feathericons.com/", + IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/", + IconSources.ICONS8 => "https://icons8.com/", + + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/SentenceStructureExtensions.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/SentenceStructureExtensions.cs index 66104e01c..4f3019b41 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/SentenceStructureExtensions.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/SentenceStructureExtensions.cs @@ -4,19 +4,22 @@ public static class SentenceStructureExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(SentenceStructureExtensions).Namespace, nameof(SentenceStructureExtensions)); - public static string Name(this SentenceStructure sentenceStructure) => sentenceStructure switch + extension(SentenceStructure sentenceStructure) { - SentenceStructure.ACTIVE => TB("Active voice"), - SentenceStructure.PASSIVE => TB("Passive voice"), + public string Name() => sentenceStructure switch + { + SentenceStructure.ACTIVE => TB("Active voice"), + SentenceStructure.PASSIVE => TB("Passive voice"), - _ => TB("Not Specified"), - }; + _ => TB("Not Specified"), + }; - public static string Prompt(this SentenceStructure sentenceStructure) => sentenceStructure switch - { - SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.", - SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.", + public string Prompt() => sentenceStructure switch + { + SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.", + SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.", - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/WritingStylesExtensions.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/WritingStylesExtensions.cs index f7648e86b..1bd047264 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/WritingStylesExtensions.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/WritingStylesExtensions.cs @@ -4,35 +4,38 @@ public static class WritingStylesExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions)); - public static string Name(this WritingStyles style) => style switch + extension(WritingStyles style) { - WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"), - WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"), - WritingStyles.SCIENTIFIC => TB("Scientific (scientific papers, research reports)"), - WritingStyles.JOURNALISTIC => TB("Journalistic (magazines, newspapers, news)"), - WritingStyles.LITERARY => TB("Literary (fiction, poetry)"), - WritingStyles.TECHNICAL => TB("Technical (manuals, documentation)"), - WritingStyles.MARKETING => TB("Marketing (advertisements, sales texts)"), - WritingStyles.ACADEMIC => TB("Academic (essays, seminar papers)"), - WritingStyles.LEGAL => TB("Legal (legal texts, contracts)"), - WritingStyles.CHANGELOG => TB("Changelog (release notes, version history)"), + public string Name() => style switch + { + WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"), + WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"), + WritingStyles.SCIENTIFIC => TB("Scientific (scientific papers, research reports)"), + WritingStyles.JOURNALISTIC => TB("Journalistic (magazines, newspapers, news)"), + WritingStyles.LITERARY => TB("Literary (fiction, poetry)"), + WritingStyles.TECHNICAL => TB("Technical (manuals, documentation)"), + WritingStyles.MARKETING => TB("Marketing (advertisements, sales texts)"), + WritingStyles.ACADEMIC => TB("Academic (essays, seminar papers)"), + WritingStyles.LEGAL => TB("Legal (legal texts, contracts)"), + WritingStyles.CHANGELOG => TB("Changelog (release notes, version history)"), - _ => TB("Not specified"), - }; + _ => TB("Not specified"), + }; - public static string Prompt(this WritingStyles style) => style switch - { - WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.", - WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.", - WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.", - WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.", - WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.", - WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.", - WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.", - WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.", - WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.", - WritingStyles.CHANGELOG => "Use a changelog style like for release notes, version history, and software updates. Most important is clarity and conciseness. The changelog is structured as a Markdown list. Most list items start with one of the following verbs: Added, Changed, Deprecated, Removed, Fixed, Refactored, Improved, or Upgraded -- these verbs should also translated to the target language. Also, changelogs use past tense.", + public string Prompt() => style switch + { + WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.", + WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.", + WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.", + WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.", + WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.", + WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.", + WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.", + WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.", + WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.", + WritingStyles.CHANGELOG => "Use a changelog style like for release notes, version history, and software updates. Most important is clarity and conciseness. The changelog is structured as a Markdown list. Most list items start with one of the following verbs: Added, Changed, Deprecated, Removed, Fixed, Refactored, Improved, or Upgraded -- these verbs should also translated to the target language. Also, changelogs use past tense.", - _ => "Keep the style of the text as it is.", - }; + _ => "Keep the style of the text as it is.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotAnalysisCategoryExtensions.cs b/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotAnalysisCategoryExtensions.cs index e1a97dfd3..47d10dd1c 100644 --- a/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotAnalysisCategoryExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotAnalysisCategoryExtensions.cs @@ -2,29 +2,32 @@ namespace AIStudio.Assistants.SWOTAnalysis; internal static class SwotAnalysisCategoryExtensions { - /// - /// The name of the category in the JSON the models read and write, e.g., "strengths". - /// - public static string JsonName(this SwotAnalysisCategory category) => category switch + extension(SwotAnalysisCategory category) { - SwotAnalysisCategory.STRENGTHS => "strengths", - SwotAnalysisCategory.WEAKNESSES => "weaknesses", - SwotAnalysisCategory.OPPORTUNITIES => "opportunities", - SwotAnalysisCategory.THREATS => "threats", + /// + /// The name of the category in the JSON the models read and write, e.g., "strengths". + /// + public string JsonName() => category switch + { + SwotAnalysisCategory.STRENGTHS => "strengths", + SwotAnalysisCategory.WEAKNESSES => "weaknesses", + SwotAnalysisCategory.OPPORTUNITIES => "opportunities", + SwotAnalysisCategory.THREATS => "threats", - _ => string.Empty, - }; + _ => string.Empty, + }; - /// - /// The letter the factor IDs of the category start with, e.g., S1 for the first strength. - /// - public static string FactorIdPrefix(this SwotAnalysisCategory category) => category switch - { - SwotAnalysisCategory.STRENGTHS => "S", - SwotAnalysisCategory.WEAKNESSES => "W", - SwotAnalysisCategory.OPPORTUNITIES => "O", - SwotAnalysisCategory.THREATS => "T", + /// + /// The letter the factor IDs of the category start with, e.g., S1 for the first strength. + /// + public string FactorIdPrefix() => category switch + { + SwotAnalysisCategory.STRENGTHS => "S", + SwotAnalysisCategory.WEAKNESSES => "W", + SwotAnalysisCategory.OPPORTUNITIES => "O", + SwotAnalysisCategory.THREATS => "T", - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs index d1f199c75..0bcf474f1 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs @@ -4,23 +4,26 @@ public static class AudienceAgeGroupExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceAgeGroupExtensions).Namespace, nameof(AudienceAgeGroupExtensions)); - public static string Name(this AudienceAgeGroup ageGroup) => ageGroup switch + extension(AudienceAgeGroup ageGroup) { - AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"), - AudienceAgeGroup.CHILDREN => TB("Children"), - AudienceAgeGroup.TEENAGERS => TB("Teenagers"), - AudienceAgeGroup.ADULTS => TB("Adults"), + public string Name() => ageGroup switch + { + AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"), + AudienceAgeGroup.CHILDREN => TB("Children"), + AudienceAgeGroup.TEENAGERS => TB("Teenagers"), + AudienceAgeGroup.ADULTS => TB("Adults"), - _ => TB("Unspecified age group"), - }; + _ => TB("Unspecified age group"), + }; - public static string Prompt(this AudienceAgeGroup ageGroup) => ageGroup switch - { - AudienceAgeGroup.UNSPECIFIED => "Do not tailor the text to a specific age group.", - AudienceAgeGroup.CHILDREN => "Use simple, concrete language with short sentences and minimal jargon.", - AudienceAgeGroup.TEENAGERS => "Use clear, approachable language with relatable examples and limited jargon.", - AudienceAgeGroup.ADULTS => "Use adult-appropriate language with clear structure and direct explanations.", + public string Prompt() => ageGroup switch + { + AudienceAgeGroup.UNSPECIFIED => "Do not tailor the text to a specific age group.", + AudienceAgeGroup.CHILDREN => "Use simple, concrete language with short sentences and minimal jargon.", + AudienceAgeGroup.TEENAGERS => "Use clear, approachable language with relatable examples and limited jargon.", + AudienceAgeGroup.ADULTS => "Use adult-appropriate language with clear structure and direct explanations.", - _ => "Do not tailor the text to a specific age group.", - }; + _ => "Do not tailor the text to a specific age group.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs index 11649d420..aab3b907e 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs @@ -4,25 +4,28 @@ public static class AudienceExpertiseExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceExpertiseExtensions).Namespace, nameof(AudienceExpertiseExtensions)); - public static string Name(this AudienceExpertise expertise) => expertise switch + extension(AudienceExpertise expertise) { - AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"), - AudienceExpertise.NON_EXPERTS => TB("No expertise"), - AudienceExpertise.BASIC => TB("Basic expertise"), - AudienceExpertise.INTERMEDIATE => TB("Intermediate expertise"), - AudienceExpertise.EXPERTS => TB("Experts"), + public string Name() => expertise switch + { + AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"), + AudienceExpertise.NON_EXPERTS => TB("No expertise"), + AudienceExpertise.BASIC => TB("Basic expertise"), + AudienceExpertise.INTERMEDIATE => TB("Intermediate expertise"), + AudienceExpertise.EXPERTS => TB("Experts"), - _ => TB("Unspecified expertise"), - }; + _ => TB("Unspecified expertise"), + }; - public static string Prompt(this AudienceExpertise expertise) => expertise switch - { - AudienceExpertise.UNSPECIFIED => "Do not tailor the text to a specific expertise level.", - AudienceExpertise.NON_EXPERTS => "Avoid jargon and explain specialized concepts plainly.", - AudienceExpertise.BASIC => "Use simple terminology and briefly explain important technical terms.", - AudienceExpertise.INTERMEDIATE => "Assume some familiarity with the topic, but still explain important details clearly.", - AudienceExpertise.EXPERTS => "Assume deep familiarity with the topic and use precise domain-specific terminology.", + public string Prompt() => expertise switch + { + AudienceExpertise.UNSPECIFIED => "Do not tailor the text to a specific expertise level.", + AudienceExpertise.NON_EXPERTS => "Avoid jargon and explain specialized concepts plainly.", + AudienceExpertise.BASIC => "Use simple terminology and briefly explain important technical terms.", + AudienceExpertise.INTERMEDIATE => "Assume some familiarity with the topic, but still explain important details clearly.", + AudienceExpertise.EXPERTS => "Assume deep familiarity with the topic and use precise domain-specific terminology.", - _ => "Do not tailor the text to a specific expertise level.", - }; + _ => "Do not tailor the text to a specific expertise level.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs index 68bfed30d..685c463e0 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs @@ -4,29 +4,32 @@ public static class AudienceOrganizationalLevelExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceOrganizationalLevelExtensions).Namespace, nameof(AudienceOrganizationalLevelExtensions)); - public static string Name(this AudienceOrganizationalLevel level) => level switch + extension(AudienceOrganizationalLevel level) { - AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"), - AudienceOrganizationalLevel.TRAINEES => TB("Trainees"), - AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => TB("Individual contributors"), - AudienceOrganizationalLevel.TEAM_LEADS => TB("Team leads"), - AudienceOrganizationalLevel.MANAGERS => TB("Managers"), - AudienceOrganizationalLevel.EXECUTIVES => TB("Executives"), - AudienceOrganizationalLevel.BOARD_MEMBERS => TB("Board members"), + public string Name() => level switch + { + AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"), + AudienceOrganizationalLevel.TRAINEES => TB("Trainees"), + AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => TB("Individual contributors"), + AudienceOrganizationalLevel.TEAM_LEADS => TB("Team leads"), + AudienceOrganizationalLevel.MANAGERS => TB("Managers"), + AudienceOrganizationalLevel.EXECUTIVES => TB("Executives"), + AudienceOrganizationalLevel.BOARD_MEMBERS => TB("Board members"), - _ => TB("Unspecified organizational level"), - }; + _ => TB("Unspecified organizational level"), + }; - public static string Prompt(this AudienceOrganizationalLevel level) => level switch - { - AudienceOrganizationalLevel.UNSPECIFIED => "Do not tailor the text to a specific organizational level.", - AudienceOrganizationalLevel.TRAINEES => "Keep the content supportive and introductory. Explain context and avoid assuming prior organizational knowledge.", - AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => "Focus on execution, clarity, responsibilities, and practical next steps.", - AudienceOrganizationalLevel.TEAM_LEADS => "Focus on coordination, tradeoffs, risks, and concrete actions for a small team.", - AudienceOrganizationalLevel.MANAGERS => "Focus on planning, priorities, outcomes, risks, and resource implications.", - AudienceOrganizationalLevel.EXECUTIVES => "Focus on strategy, business impact, risks, and the decisions required.", - AudienceOrganizationalLevel.BOARD_MEMBERS => "Provide a concise executive-level summary with governance, strategy, risk, and decision relevance.", + public string Prompt() => level switch + { + AudienceOrganizationalLevel.UNSPECIFIED => "Do not tailor the text to a specific organizational level.", + AudienceOrganizationalLevel.TRAINEES => "Keep the content supportive and introductory. Explain context and avoid assuming prior organizational knowledge.", + AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => "Focus on execution, clarity, responsibilities, and practical next steps.", + AudienceOrganizationalLevel.TEAM_LEADS => "Focus on coordination, tradeoffs, risks, and concrete actions for a small team.", + AudienceOrganizationalLevel.MANAGERS => "Focus on planning, priorities, outcomes, risks, and resource implications.", + AudienceOrganizationalLevel.EXECUTIVES => "Focus on strategy, business impact, risks, and the decisions required.", + AudienceOrganizationalLevel.BOARD_MEMBERS => "Provide a concise executive-level summary with governance, strategy, risk, and decision relevance.", - _ => "Do not tailor the text to a specific organizational level.", - }; + _ => "Do not tailor the text to a specific organizational level.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs index 67fb38270..fe60a70f1 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs @@ -4,37 +4,40 @@ public static class AudienceProfileExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceProfileExtensions).Namespace, nameof(AudienceProfileExtensions)); - public static string Name(this AudienceProfile profile) => profile switch + extension(AudienceProfile profile) { - AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"), - AudienceProfile.STUDENTS => TB("Students"), - AudienceProfile.SCIENTISTS => TB("Scientists"), - AudienceProfile.LAWYERS => TB("Lawyers"), - AudienceProfile.INVESTORS => TB("Investors"), - AudienceProfile.ENGINEERS => TB("Engineers"), - AudienceProfile.SOFTWARE_DEVELOPERS => TB("Software developers"), - AudienceProfile.JOURNALISTS => TB("Journalists"), - AudienceProfile.HEALTHCARE_PROFESSIONALS => TB("Healthcare professionals"), - AudienceProfile.PUBLIC_OFFICIALS => TB("Public officials"), - AudienceProfile.BUSINESS_PROFESSIONALS => TB("Business professionals"), + public string Name() => profile switch + { + AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"), + AudienceProfile.STUDENTS => TB("Students"), + AudienceProfile.SCIENTISTS => TB("Scientists"), + AudienceProfile.LAWYERS => TB("Lawyers"), + AudienceProfile.INVESTORS => TB("Investors"), + AudienceProfile.ENGINEERS => TB("Engineers"), + AudienceProfile.SOFTWARE_DEVELOPERS => TB("Software developers"), + AudienceProfile.JOURNALISTS => TB("Journalists"), + AudienceProfile.HEALTHCARE_PROFESSIONALS => TB("Healthcare professionals"), + AudienceProfile.PUBLIC_OFFICIALS => TB("Public officials"), + AudienceProfile.BUSINESS_PROFESSIONALS => TB("Business professionals"), - _ => TB("Unspecified audience profile"), - }; + _ => TB("Unspecified audience profile"), + }; - public static string Prompt(this AudienceProfile profile) => profile switch - { - AudienceProfile.UNSPECIFIED => "Do not tailor the text to a specific audience profile.", - AudienceProfile.STUDENTS => "Write for students. Keep it structured, easy to study, and focused on key takeaways.", - AudienceProfile.SCIENTISTS => "Use precise, technical language. Structure the content logically and focus on methods, evidence, and results.", - AudienceProfile.LAWYERS => "Write with precise wording. Emphasize definitions, implications, compliance, and risks.", - AudienceProfile.INVESTORS => "Focus on market potential, business model, differentiation, traction, risks, and financial upside.", - AudienceProfile.ENGINEERS => "Be technically precise and practical. Focus on systems, constraints, implementation, and tradeoffs.", - AudienceProfile.SOFTWARE_DEVELOPERS => "Use concise technical language. Focus on architecture, implementation details, tradeoffs, and maintainability.", - AudienceProfile.JOURNALISTS => "Be clear, factual, and concise. Highlight the most newsworthy points and explain relevance plainly.", - AudienceProfile.HEALTHCARE_PROFESSIONALS => "Use accurate professional language. Emphasize outcomes, safety, evidence, and practical implications.", - AudienceProfile.PUBLIC_OFFICIALS => "Focus on public impact, feasibility, budget, compliance, risks, and implementation in a neutral institutional tone.", - AudienceProfile.BUSINESS_PROFESSIONALS => "Be clear, practical, and concise. Focus on business relevance, decisions, and next steps.", + public string Prompt() => profile switch + { + AudienceProfile.UNSPECIFIED => "Do not tailor the text to a specific audience profile.", + AudienceProfile.STUDENTS => "Write for students. Keep it structured, easy to study, and focused on key takeaways.", + AudienceProfile.SCIENTISTS => "Use precise, technical language. Structure the content logically and focus on methods, evidence, and results.", + AudienceProfile.LAWYERS => "Write with precise wording. Emphasize definitions, implications, compliance, and risks.", + AudienceProfile.INVESTORS => "Focus on market potential, business model, differentiation, traction, risks, and financial upside.", + AudienceProfile.ENGINEERS => "Be technically precise and practical. Focus on systems, constraints, implementation, and tradeoffs.", + AudienceProfile.SOFTWARE_DEVELOPERS => "Use concise technical language. Focus on architecture, implementation details, tradeoffs, and maintainability.", + AudienceProfile.JOURNALISTS => "Be clear, factual, and concise. Highlight the most newsworthy points and explain relevance plainly.", + AudienceProfile.HEALTHCARE_PROFESSIONALS => "Use accurate professional language. Emphasize outcomes, safety, evidence, and practical implications.", + AudienceProfile.PUBLIC_OFFICIALS => "Focus on public impact, feasibility, budget, compliance, risks, and implementation in a neutral institutional tone.", + AudienceProfile.BUSINESS_PROFESSIONALS => "Be clear, practical, and concise. Focus on business relevance, decisions, and next steps.", - _ => "Do not tailor the text to a specific audience profile.", - }; + _ => "Do not tailor the text to a specific audience profile.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/PresentationOccasionExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/PresentationOccasionExtensions.cs index 44936b346..e7ee144a0 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/PresentationOccasionExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/PresentationOccasionExtensions.cs @@ -4,53 +4,56 @@ public static class PresentationOccasionExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(PresentationOccasionExtensions).Namespace, nameof(PresentationOccasionExtensions)); - public static string Name(this PresentationOccasion occasion) => occasion switch - { - PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"), - PresentationOccasion.TALK => TB("Talk"), - PresentationOccasion.KEYNOTE => TB("Keynote"), - PresentationOccasion.CONFERENCE_TALK => TB("Conference talk"), - PresentationOccasion.LECTURE => TB("Lecture"), - PresentationOccasion.TRAINING => TB("Training"), - PresentationOccasion.WORKSHOP => TB("Workshop of one day"), - PresentationOccasion.MULTI_DAY_WORKSHOP => TB("Workshop of several days"), - PresentationOccasion.THESIS_DEFENSE => TB("Defense of a thesis or dissertation"), - PresentationOccasion.RESEARCH_RESULTS => TB("Presentation of research results"), - PresentationOccasion.PROJECT_REPORT => TB("Project or status report"), - PresentationOccasion.MANAGEMENT_REPORT => TB("Report to management"), - PresentationOccasion.PITCH => TB("Pitch"), - PresentationOccasion.PRODUCT_PRESENTATION => TB("Product presentation"), - PresentationOccasion.KICK_OFF => TB("Kick-off"), - PresentationOccasion.TEAM_MEETING => TB("Team meeting"), - PresentationOccasion.OTHER => TB("Other occasion"), - - _ => TB("Unspecified occasion"), - }; - - /// - /// Tells the model what the occasion means for the structure of the presentation. - /// /// The occasion. - /// The occasion the user described, for OTHER. - public static string Prompt(this PresentationOccasion occasion, string customOccasion) => occasion switch + extension(PresentationOccasion occasion) { - PresentationOccasion.TALK => "The presentation accompanies a talk. Structure it for a speaker who talks the audience through it.", - PresentationOccasion.KEYNOTE => "The presentation accompanies a keynote. Favor few words per slide, strong key messages, and a story which carries the audience.", - PresentationOccasion.CONFERENCE_TALK => "The presentation accompanies a talk at a conference. Keep to the essentials of a short time slot, and end with the key messages and room for questions.", - PresentationOccasion.LECTURE => "The presentation accompanies a lecture. Teach step by step, with examples, and where it suits, end chapters with an interim summary or with room for questions.", - PresentationOccasion.TRAINING => "The presentation accompanies a training. Teach practical skills step by step, with examples and exercises where the content offers them.", - PresentationOccasion.WORKSHOP => "The presentation accompanies a workshop of one day. Plan for interaction, e.g., exercises, group work, or discussions, and for the breaks the schedule of the content has.", - PresentationOccasion.MULTI_DAY_WORKSHOP => "The presentation accompanies a workshop of several days. Plan for interaction, mark where a day ends and the next one starts, and look back at the start of each day, following the schedule of the content.", - PresentationOccasion.THESIS_DEFENSE => "The presentation defends a thesis or a dissertation before a committee. Present the research questions, the method, the results, and the contribution precisely, and anticipate critical questions.", - PresentationOccasion.RESEARCH_RESULTS => "The presentation presents research results. Present the questions, the method, the results, and their meaning precisely and comprehensibly.", - PresentationOccasion.PROJECT_REPORT => "The presentation reports on a project. Present the status, the progress, the problems, and the next steps clearly.", - PresentationOccasion.MANAGEMENT_REPORT => "The presentation reports to management and may prepare a decision. Lead with the key messages, keep details short, and state clearly what is to be decided.", - PresentationOccasion.PITCH => "The presentation pitches an idea, a product, or an offer. Lead with the benefit for the audience, and end with a clear call to action.", - PresentationOccasion.PRODUCT_PRESENTATION => "The presentation presents a product. Show what it does and what it brings the audience, with examples.", - PresentationOccasion.KICK_OFF => "The presentation opens a project at its kick-off. Present the goals, the team, the plan, and how the work is organized.", - PresentationOccasion.TEAM_MEETING => "The presentation serves a team meeting. Keep it short and practical, with clear points and next steps.", - PresentationOccasion.OTHER => $"The presentation is for this occasion: {customOccasion}. Structure it accordingly.", + public string Name() => occasion switch + { + PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"), + PresentationOccasion.TALK => TB("Talk"), + PresentationOccasion.KEYNOTE => TB("Keynote"), + PresentationOccasion.CONFERENCE_TALK => TB("Conference talk"), + PresentationOccasion.LECTURE => TB("Lecture"), + PresentationOccasion.TRAINING => TB("Training"), + PresentationOccasion.WORKSHOP => TB("Workshop of one day"), + PresentationOccasion.MULTI_DAY_WORKSHOP => TB("Workshop of several days"), + PresentationOccasion.THESIS_DEFENSE => TB("Defense of a thesis or dissertation"), + PresentationOccasion.RESEARCH_RESULTS => TB("Presentation of research results"), + PresentationOccasion.PROJECT_REPORT => TB("Project or status report"), + PresentationOccasion.MANAGEMENT_REPORT => TB("Report to management"), + PresentationOccasion.PITCH => TB("Pitch"), + PresentationOccasion.PRODUCT_PRESENTATION => TB("Product presentation"), + PresentationOccasion.KICK_OFF => TB("Kick-off"), + PresentationOccasion.TEAM_MEETING => TB("Team meeting"), + PresentationOccasion.OTHER => TB("Other occasion"), - _ => "No particular occasion is given. Structure the presentation by its content.", - }; + _ => TB("Unspecified occasion"), + }; + + /// + /// Tells the model what the occasion means for the structure of the presentation. + /// + /// The occasion the user described, for OTHER. + public string Prompt(string customOccasion) => occasion switch + { + PresentationOccasion.TALK => "The presentation accompanies a talk. Structure it for a speaker who talks the audience through it.", + PresentationOccasion.KEYNOTE => "The presentation accompanies a keynote. Favor few words per slide, strong key messages, and a story which carries the audience.", + PresentationOccasion.CONFERENCE_TALK => "The presentation accompanies a talk at a conference. Keep to the essentials of a short time slot, and end with the key messages and room for questions.", + PresentationOccasion.LECTURE => "The presentation accompanies a lecture. Teach step by step, with examples, and where it suits, end chapters with an interim summary or with room for questions.", + PresentationOccasion.TRAINING => "The presentation accompanies a training. Teach practical skills step by step, with examples and exercises where the content offers them.", + PresentationOccasion.WORKSHOP => "The presentation accompanies a workshop of one day. Plan for interaction, e.g., exercises, group work, or discussions, and for the breaks the schedule of the content has.", + PresentationOccasion.MULTI_DAY_WORKSHOP => "The presentation accompanies a workshop of several days. Plan for interaction, mark where a day ends and the next one starts, and look back at the start of each day, following the schedule of the content.", + PresentationOccasion.THESIS_DEFENSE => "The presentation defends a thesis or a dissertation before a committee. Present the research questions, the method, the results, and the contribution precisely, and anticipate critical questions.", + PresentationOccasion.RESEARCH_RESULTS => "The presentation presents research results. Present the questions, the method, the results, and their meaning precisely and comprehensibly.", + PresentationOccasion.PROJECT_REPORT => "The presentation reports on a project. Present the status, the progress, the problems, and the next steps clearly.", + PresentationOccasion.MANAGEMENT_REPORT => "The presentation reports to management and may prepare a decision. Lead with the key messages, keep details short, and state clearly what is to be decided.", + PresentationOccasion.PITCH => "The presentation pitches an idea, a product, or an offer. Lead with the benefit for the audience, and end with a clear call to action.", + PresentationOccasion.PRODUCT_PRESENTATION => "The presentation presents a product. Show what it does and what it brings the audience, with examples.", + PresentationOccasion.KICK_OFF => "The presentation opens a project at its kick-off. Present the goals, the team, the plan, and how the work is organized.", + PresentationOccasion.TEAM_MEETING => "The presentation serves a team meeting. Keep it short and practical, with clear points and next steps.", + PresentationOccasion.OTHER => $"The presentation is for this occasion: {customOccasion}. Structure it accordingly.", + + _ => "No particular occasion is given. Structure the presentation by its content.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideExtensions.cs index 7ea3bd98b..d5974b9e8 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideExtensions.cs @@ -11,20 +11,23 @@ internal static class TemplateSlideExtensions /// private const int MAX_SLIDE_TITLE_CHARACTERS = 60; - /// - /// How the forms name a slide of the template: by its number and its title, and what is special about it. - /// - /// - /// The title tells the slides apart at a glance, since a template rarely gives its slides more - /// than a title. A long one is cut, so that the label stays on one line. - /// - public static string Label(this TemplateSlide slide) + extension(TemplateSlide slide) { - var title = slide.Title.Length > MAX_SLIDE_TITLE_CHARACTERS ? $"{slide.Title[..MAX_SLIDE_TITLE_CHARACTERS].TrimEnd()}…" : slide.Title; - var label = title.Length > 0 ? string.Format(TB("Slide {0}: {1}"), slide.Number, title) : string.Format(TB("Slide {0}"), slide.Number); - if (slide.IsHidden) - label = $"{label} ({TB("hidden")})"; + /// + /// How the forms name a slide of the template: by its number and its title, and what is special about it. + /// + /// + /// The title tells the slides apart at a glance, since a template rarely gives its slides more + /// than a title. A long one is cut, so that the label stays on one line. + /// + public string Label() + { + var title = slide.Title.Length > MAX_SLIDE_TITLE_CHARACTERS ? $"{slide.Title[..MAX_SLIDE_TITLE_CHARACTERS].TrimEnd()}…" : slide.Title; + var label = title.Length > 0 ? string.Format(TB("Slide {0}: {1}"), slide.Number, title) : string.Format(TB("Slide {0}"), slide.Number); + if (slide.IsHidden) + label = $"{label} ({TB("hidden")})"; - return label; + return label; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideUseExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideUseExtensions.cs index 1994351c3..aa8d56f1a 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideUseExtensions.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/TemplateSlideUseExtensions.cs @@ -6,12 +6,15 @@ internal static class TemplateSlideUseExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(TemplateSlideUseExtensions).Namespace, nameof(TemplateSlideUseExtensions)); - public static string Name(this TemplateSlideUse use) => use switch + extension(TemplateSlideUse use) { - TemplateSlideUse.OMIT => TB("Leave out"), - TemplateSlideUse.KEEP => TB("Keep unchanged"), - TemplateSlideUse.EDIT => TB("Let the LLM edit it"), + public string Name() => use switch + { + TemplateSlideUse.OMIT => TB("Leave out"), + TemplateSlideUse.KEEP => TB("Keep unchanged"), + TemplateSlideUse.EDIT => TB("Let the LLM edit it"), - _ => TB("Leave out"), - }; + _ => TB("Leave out"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/ComplexityExtensions.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/ComplexityExtensions.cs index 15cb399b0..bc3ab983d 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/ComplexityExtensions.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/ComplexityExtensions.cs @@ -4,31 +4,34 @@ public static class ComplexityExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ComplexityExtensions).Namespace, nameof(ComplexityExtensions)); - public static string Name(this Complexity complexity) => complexity switch + extension(Complexity complexity) { - Complexity.NO_CHANGE => TB("No change in complexity"), + public string Name() => complexity switch + { + Complexity.NO_CHANGE => TB("No change in complexity"), - Complexity.SIMPLE_LANGUAGE => TB("Simple language, e.g., for children"), - Complexity.TEEN_LANGUAGE => TB("Teen language, e.g., for teenagers"), - Complexity.EVERYDAY_LANGUAGE => TB("Everyday language, e.g., for adults"), - Complexity.POPULAR_SCIENCE_LANGUAGE => TB("Popular science language, e.g., for people interested in science"), - Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => TB("Scientific language for experts in this field"), - Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => TB("Scientific language for experts from other fields (interdisciplinary)"), + Complexity.SIMPLE_LANGUAGE => TB("Simple language, e.g., for children"), + Complexity.TEEN_LANGUAGE => TB("Teen language, e.g., for teenagers"), + Complexity.EVERYDAY_LANGUAGE => TB("Everyday language, e.g., for adults"), + Complexity.POPULAR_SCIENCE_LANGUAGE => TB("Popular science language, e.g., for people interested in science"), + Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => TB("Scientific language for experts in this field"), + Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => TB("Scientific language for experts from other fields (interdisciplinary)"), - _ => TB("No change in complexity"), - }; - - public static string Prompt(this Complexity complexity, string expertInField) => complexity switch - { - Complexity.NO_CHANGE => "Do not change the complexity of the text.", + _ => TB("No change in complexity"), + }; + + public string Prompt(string expertInField) => complexity switch + { + Complexity.NO_CHANGE => "Do not change the complexity of the text.", - Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.", - Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.", - Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.", - Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.", - Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.", - Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.", + Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.", + Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.", + Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.", + Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.", + Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.", + Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.", - _ => "Do not change the complexity of the text.", - }; + _ => "Do not change the complexity of the text.", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingFailureExtensions.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingFailureExtensions.cs index fd6e29e22..ed5cae860 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingFailureExtensions.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingFailureExtensions.cs @@ -13,12 +13,15 @@ internal static class VisualBriefingFailureExtensions { private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(VisualBriefingFailureExtensions).Namespace, nameof(VisualBriefingFailureExtensions)); - /// - /// Gets the localized message for one recorded failure. - /// /// The recorded failure. - /// The localized message. - internal static string ToUserMessage(this VisualBriefingFailure failure) => ToUserMessage(failure.Code, failure.ValidationRule); + extension(VisualBriefingFailure failure) + { + /// + /// Gets the localized message for one recorded failure. + /// + /// The localized message. + internal string ToUserMessage() => ToUserMessage(failure.Code, failure.ValidationRule); + } /// /// Gets the localized message for one failure code and validation rule. @@ -40,70 +43,76 @@ internal static class VisualBriefingFailureExtensions _ => code.ToUserMessage(), }; - /// - /// Gets the localized message for one validation rule. - /// /// The stable validation rule. - /// The localized message. - private static string ToUserMessage(this VisualBriefingValidationRule rule) => rule switch + extension(VisualBriefingValidationRule rule) { - VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), - VisualBriefingValidationRule.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. Please try again or select another model."), - VisualBriefingValidationRule.UNKNOWN_FIELD => TB("The model response contained unexpected fields. Please try again or select another model."), - VisualBriefingValidationRule.CONTRACT_VERSION_UNSUPPORTED => TB("The model response used an unsupported contract version. Please try again or select another model."), - VisualBriefingValidationRule.ID_INVALID => TB("The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."), - VisualBriefingValidationRule.REFERENCE_INVALID => TB("The model response referenced content that does not exist. Please try again or select another model."), - VisualBriefingValidationRule.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), - VisualBriefingValidationRule.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), - VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID => TB("The model did not fill every planned content slot exactly once. Please try again or select another model."), - VisualBriefingValidationRule.SLOT_VALUE_TYPE_INVALID => TB("The model filled a content slot with the wrong kind of value. Please try again or select another model."), - VisualBriefingValidationRule.CHART_SET_INVALID => TB("The charts of the model response did not match the planned briefing elements. Please try again or select another model."), - VisualBriefingValidationRule.CHART_DATA_INVALID => TB("A chart of the model response contained invalid categories or data series. Please try again or select another model."), - VisualBriefingValidationRule.CONTROL_ID_INVALID => TB("An interactive control of the model response used an invalid identifier. Please try again or select another model."), - VisualBriefingValidationRule.CONTROL_TARGET_INVALID => TB("An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."), - VisualBriefingValidationRule.CONTROL_STATE_INVALID => TB("An interactive control of the model response used an invalid initial state. Please try again or select another model."), - VisualBriefingValidationRule.CONTROL_REQUIREMENT_INVALID => TB("A briefing element of the model response was missing its required interactive controls. Please try again or select another model."), - VisualBriefingValidationRule.FORMULA_TARGET_INVALID => TB("A calculation of the model response targeted an invalid briefing element. Please try again or select another model."), - VisualBriefingValidationRule.FORMULA_AST_INVALID => TB("A calculation of the model response used an invalid operation. Please try again or select another model."), - VisualBriefingValidationRule.ACCESSIBILITY_SET_INVALID => TB("The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."), - VisualBriefingValidationRule.ACCESSIBILITY_TEXT_INVALID => TB("An accessibility text of the model response was empty or invalid. Please try again or select another model."), - VisualBriefingValidationRule.LAYOUT_INVALID => TB("The model response used an invalid briefing layout. Please try again or select another model."), - VisualBriefingValidationRule.TEMPLATE_ATTRIBUTE_PROHIBITED => TB("The model response used a prohibited attribute. Please try again or select another model."), - VisualBriefingValidationRule.MODEL_MARKUP_PROHIBITED => TB("The model response contained markup or code, which this briefing does not allow. Please try again or select another model."), - VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), + /// + /// Gets the localized message for one validation rule. + /// + /// The localized message. + private string ToUserMessage() => rule switch + { + VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), + VisualBriefingValidationRule.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. Please try again or select another model."), + VisualBriefingValidationRule.UNKNOWN_FIELD => TB("The model response contained unexpected fields. Please try again or select another model."), + VisualBriefingValidationRule.CONTRACT_VERSION_UNSUPPORTED => TB("The model response used an unsupported contract version. Please try again or select another model."), + VisualBriefingValidationRule.ID_INVALID => TB("The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."), + VisualBriefingValidationRule.REFERENCE_INVALID => TB("The model response referenced content that does not exist. Please try again or select another model."), + VisualBriefingValidationRule.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), + VisualBriefingValidationRule.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), + VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID => TB("The model did not fill every planned content slot exactly once. Please try again or select another model."), + VisualBriefingValidationRule.SLOT_VALUE_TYPE_INVALID => TB("The model filled a content slot with the wrong kind of value. Please try again or select another model."), + VisualBriefingValidationRule.CHART_SET_INVALID => TB("The charts of the model response did not match the planned briefing elements. Please try again or select another model."), + VisualBriefingValidationRule.CHART_DATA_INVALID => TB("A chart of the model response contained invalid categories or data series. Please try again or select another model."), + VisualBriefingValidationRule.CONTROL_ID_INVALID => TB("An interactive control of the model response used an invalid identifier. Please try again or select another model."), + VisualBriefingValidationRule.CONTROL_TARGET_INVALID => TB("An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."), + VisualBriefingValidationRule.CONTROL_STATE_INVALID => TB("An interactive control of the model response used an invalid initial state. Please try again or select another model."), + VisualBriefingValidationRule.CONTROL_REQUIREMENT_INVALID => TB("A briefing element of the model response was missing its required interactive controls. Please try again or select another model."), + VisualBriefingValidationRule.FORMULA_TARGET_INVALID => TB("A calculation of the model response targeted an invalid briefing element. Please try again or select another model."), + VisualBriefingValidationRule.FORMULA_AST_INVALID => TB("A calculation of the model response used an invalid operation. Please try again or select another model."), + VisualBriefingValidationRule.ACCESSIBILITY_SET_INVALID => TB("The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."), + VisualBriefingValidationRule.ACCESSIBILITY_TEXT_INVALID => TB("An accessibility text of the model response was empty or invalid. Please try again or select another model."), + VisualBriefingValidationRule.LAYOUT_INVALID => TB("The model response used an invalid briefing layout. Please try again or select another model."), + VisualBriefingValidationRule.TEMPLATE_ATTRIBUTE_PROHIBITED => TB("The model response used a prohibited attribute. Please try again or select another model."), + VisualBriefingValidationRule.MODEL_MARKUP_PROHIBITED => TB("The model response contained markup or code, which this briefing does not allow. Please try again or select another model."), + VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), - _ => string.Empty, - }; + _ => string.Empty, + }; + } - /// - /// Gets the localized message for one failure code. - /// /// The stable failure code. - /// The localized message. - private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch + extension(VisualBriefingFailureCode code) { - VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."), - VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."), - VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."), - VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."), - VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."), - VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED => TB("The sources of this briefing could not be prepared."), - VisualBriefingFailureCode.PROVIDER_CALL_FAILED => TB("The selected provider could not complete this briefing stage."), - VisualBriefingFailureCode.RESPONSE_JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), - VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID => TB("The model response did not match the required contract. Please try again or select another model."), - VisualBriefingFailureCode.COMPILER_INVARIANT_VIOLATED => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), - VisualBriefingFailureCode.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), - VisualBriefingFailureCode.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), - VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE => TB("The updated content no longer fits the current presentation. You can continue as a rebuild."), - VisualBriefingFailureCode.PRESENTATION_INVALID => TB("The presentation of the model response did not match the briefing contract. Please try again or select another model."), - VisualBriefingFailureCode.ASSEMBLY_FAILED => TB("This briefing could not be assembled."), - VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED => TB("The assembled briefing did not pass the security validation."), - VisualBriefingFailureCode.STORE_FAILED => TB("The new version of this briefing could not be saved."), - VisualBriefingFailureCode.NO_CHANGES => TB("This operation did not change the briefing, so no new version was created."), - VisualBriefingFailureCode.CANCELED => TB("This visual briefing operation was canceled."), - VisualBriefingFailureCode.BUILD_INTERRUPTED => TB("AI Studio was closed while this briefing was being built. You can resume the build."), - VisualBriefingFailureCode.UNEXPECTED => TB("This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."), + /// + /// Gets the localized message for one failure code. + /// + /// The localized message. + private string ToUserMessage() => code switch + { + VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."), + VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."), + VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."), + VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."), + VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."), + VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED => TB("The sources of this briefing could not be prepared."), + VisualBriefingFailureCode.PROVIDER_CALL_FAILED => TB("The selected provider could not complete this briefing stage."), + VisualBriefingFailureCode.RESPONSE_JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), + VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID => TB("The model response did not match the required contract. Please try again or select another model."), + VisualBriefingFailureCode.COMPILER_INVARIANT_VIOLATED => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), + VisualBriefingFailureCode.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), + VisualBriefingFailureCode.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), + VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE => TB("The updated content no longer fits the current presentation. You can continue as a rebuild."), + VisualBriefingFailureCode.PRESENTATION_INVALID => TB("The presentation of the model response did not match the briefing contract. Please try again or select another model."), + VisualBriefingFailureCode.ASSEMBLY_FAILED => TB("This briefing could not be assembled."), + VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED => TB("The assembled briefing did not pass the security validation."), + VisualBriefingFailureCode.STORE_FAILED => TB("The new version of this briefing could not be saved."), + VisualBriefingFailureCode.NO_CHANGES => TB("This operation did not change the briefing, so no new version was created."), + VisualBriefingFailureCode.CANCELED => TB("This visual briefing operation was canceled."), + VisualBriefingFailureCode.BUILD_INTERRUPTED => TB("AI Studio was closed while this briefing was being built. You can resume the build."), + VisualBriefingFailureCode.UNEXPECTED => TB("This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."), - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPreviewEndpoint.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPreviewEndpoint.cs index 532e6f0d7..ae98f1fff 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPreviewEndpoint.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPreviewEndpoint.cs @@ -15,55 +15,58 @@ internal static class VisualBriefingPreviewEndpoint { private const string ROUTE = "/visual-briefing/preview/{briefingId:guid}/{revisionId:guid}"; - /// - /// Maps the visual briefing preview endpoint. - /// /// The web application. - public static void MapVisualBriefingPreview(this WebApplication app) => app.MapGet( - ROUTE, - async ( - Guid briefingId, - Guid revisionId, - string? token, - HttpContext context, - VisualBriefingPreviewTokenService tokenService, - VisualBriefingStore store, - ILoggerFactory loggerFactory, - CancellationToken cancellationToken) => - { - var logger = loggerFactory.CreateLogger(nameof(VisualBriefingPreviewEndpoint)); - if (!tokenService.Validate(token, briefingId, revisionId)) + extension(WebApplication app) + { + /// + /// Maps the visual briefing preview endpoint. + /// + public void MapVisualBriefingPreview() => app.MapGet( + ROUTE, + async ( + Guid briefingId, + Guid revisionId, + string? token, + HttpContext context, + VisualBriefingPreviewTokenService tokenService, + VisualBriefingStore store, + ILoggerFactory loggerFactory, + CancellationToken cancellationToken) => { - logger.LogWarning( - Event(VisualBriefingLogEventId.PREVIEW_REJECTED), - "Visual briefing preview token rejected. BriefingId={BriefingId} RevisionId={RevisionId}", - briefingId, - revisionId); + var logger = loggerFactory.CreateLogger(nameof(VisualBriefingPreviewEndpoint)); + if (!tokenService.Validate(token, briefingId, revisionId)) + { + logger.LogWarning( + Event(VisualBriefingLogEventId.PREVIEW_REJECTED), + "Visual briefing preview token rejected. BriefingId={BriefingId} RevisionId={RevisionId}", + briefingId, + revisionId); - return Results.NotFound(); - } + return Results.NotFound(); + } - // The store re-validates the stored artifact before handing out a stream, so a manually - // modified file on disk never reaches the preview: - var preview = await store.OpenIntegrityCheckedVersionAsync(briefingId, revisionId, cancellationToken); - if (preview is null) - { - logger.LogWarning( - Event(VisualBriefingLogEventId.SECURITY_REJECTED), - "Visual briefing preview artifact rejected. BriefingId={BriefingId} RevisionId={RevisionId}", - briefingId, - revisionId); + // The store re-validates the stored artifact before handing out a stream, so a manually + // modified file on disk never reaches the preview: + var preview = await store.OpenIntegrityCheckedVersionAsync(briefingId, revisionId, cancellationToken); + if (preview is null) + { + logger.LogWarning( + Event(VisualBriefingLogEventId.SECURITY_REJECTED), + "Visual briefing preview artifact rejected. BriefingId={BriefingId} RevisionId={RevisionId}", + briefingId, + revisionId); - return Results.NotFound(); - } + return Results.NotFound(); + } - context.Response.Headers.CacheControl = "no-store"; - context.Response.Headers.XContentTypeOptions = "nosniff"; - context.Response.Headers["Referrer-Policy"] = "no-referrer"; - context.Response.Headers.ContentSecurityPolicy = VisualBriefingArtifactService.GetContentSecurityPolicy(preview.Value.Parts); + context.Response.Headers.CacheControl = "no-store"; + context.Response.Headers.XContentTypeOptions = "nosniff"; + context.Response.Headers["Referrer-Policy"] = "no-referrer"; + context.Response.Headers.ContentSecurityPolicy = VisualBriefingArtifactService.GetContentSecurityPolicy(preview.Value.Parts); - return Results.File(preview.Value.Stream, "text/html; charset=utf-8", enableRangeProcessing: false); - }); + return Results.File(preview.Value.Stream, "text/html; charset=utf-8", enableRangeProcessing: false); + }); + } /// /// Creates the log event ID for one visual briefing log event. diff --git a/app/MindWork AI Studio/Chat/ChatProvenance.cs b/app/MindWork AI Studio/Chat/ChatProvenance.cs index f45d27a9d..534cb4a3e 100644 --- a/app/MindWork AI Studio/Chat/ChatProvenance.cs +++ b/app/MindWork AI Studio/Chat/ChatProvenance.cs @@ -16,31 +16,34 @@ public static class ChatProvenance { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(ChatProvenance)); - /// - /// Collects the entries of the provenance store which a chat refers to. - /// - /// - /// The passages of its answers, the documents attached to its messages, and those attached to - /// the message being written, which may come from a message sent before, e.g., one being edited, - /// together with the texts a preview read of them. - /// /// The chat. - /// The ids of the entries. - public static IReadOnlySet CollectProvenanceIds(this ChatThread thread) + extension(ChatThread thread) { - var provenanceIds = new HashSet(); - foreach (var block in thread.Blocks) + /// + /// Collects the entries of the provenance store which a chat refers to. + /// + /// + /// The passages of its answers, the documents attached to its messages, and those attached to + /// the message being written, which may come from a message sent before, e.g., one being edited, + /// together with the texts a preview read of them. + /// + /// The ids of the entries. + public IReadOnlySet CollectProvenanceIds() { - if (block.Content is ContentText text) - provenanceIds.UnionWith(text.SourcePassages.Select(passage => passage.ProvenanceId).OfType()); + var provenanceIds = new HashSet(); + foreach (var block in thread.Blocks) + { + if (block.Content is ContentText text) + provenanceIds.UnionWith(text.SourcePassages.Select(passage => passage.ProvenanceId).OfType()); - if (block.Content is not null) - provenanceIds.UnionWith(block.Content.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType()); + if (block.Content is not null) + provenanceIds.UnionWith(block.Content.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType()); + } + + provenanceIds.UnionWith(thread.Draft.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType()); + provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId)); + return provenanceIds; } - - provenanceIds.UnionWith(thread.Draft.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType()); - provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId)); - return provenanceIds; } /// diff --git a/app/MindWork AI Studio/Chat/ChatRestrictionsExtensions.cs b/app/MindWork AI Studio/Chat/ChatRestrictionsExtensions.cs index bc37e19cd..962e71d3d 100644 --- a/app/MindWork AI Studio/Chat/ChatRestrictionsExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatRestrictionsExtensions.cs @@ -6,72 +6,75 @@ namespace AIStudio.Chat; public static class ChatRestrictionsExtensions { - /// - /// Checks if the specified provider may see content with these restrictions. - /// - /// - /// A provider the organization prohibited is never allowed, whatever the restrictions.

- /// - /// We don't check if the provider is allowed to use the data sources the content came from. - /// That kind of check is done when the available data sources are resolved. What counts here is - /// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed. - ///
/// What the content demands. - /// The provider to check. - /// True, when the provider may see the content. False, otherwise. - public static bool IsLLMProviderAllowed(this ChatRestrictions restrictions, T provider) + extension(ChatRestrictions restrictions) { - var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService(); - - // A provider the organization prohibited is allowed nowhere, not even for content which demands nothing: - if (settingsManager.IsProviderProhibited(GetLLMProvider(provider))) - return false; - - var providerConfidence = provider switch + /// + /// Checks if the specified provider may see content with these restrictions. + /// + /// + /// A provider the organization prohibited is never allowed, whatever the restrictions.

+ /// + /// We don't check if the provider is allowed to use the data sources the content came from. + /// That kind of check is done when the available data sources are resolved. What counts here is + /// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed. + ///
+ /// The provider to check. + /// True, when the provider may see the content. False, otherwise. + public bool IsLLMProviderAllowed(T provider) { - IProvider p => p.GetConfidenceLevel(settingsManager), - AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager), + var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService(); - _ => ConfidenceLevel.UNKNOWN, - }; + // A provider the organization prohibited is allowed nowhere, not even for content which demands nothing: + if (settingsManager.IsProviderProhibited(GetLLMProvider(provider))) + return false; - // - // The confidence axis is checked on its own: a provider trusted by configuration counts as - // self-hosted for data-source security, which is the check further down, but that trust - // says nothing about how confidential the provider is. An organization which wants its - // contractually covered cloud provider to pass here raises its level through the custom - // confidence scheme instead. - // - if (providerConfidence < restrictions.ProviderConfidence) - return false; + var providerConfidence = provider switch + { + IProvider p => p.GetConfidenceLevel(settingsManager), + AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager), - // The data security is not specified. Means, the content never came from RAG, or RAG was - // enabled, but no data sources were selected. That's fine: - if (restrictions.DataSecurity is DataSourceSecurity.NOT_SPECIFIED) - return true; + _ => ConfidenceLevel.UNKNOWN, + }; - // - // Is the provider trusted for data-source security checks? - // - var isTrustedProvider = provider switch - { - IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), - AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), + // + // The confidence axis is checked on its own: a provider trusted by configuration counts as + // self-hosted for data-source security, which is the check further down, but that trust + // says nothing about how confidential the provider is. An organization which wants its + // contractually covered cloud provider to pass here raises its level through the custom + // confidence scheme instead. + // + if (providerConfidence < restrictions.ProviderConfidence) + return false; - _ => false, - }; + // The data security is not specified. Means, the content never came from RAG, or RAG was + // enabled, but no data sources were selected. That's fine: + if (restrictions.DataSecurity is DataSourceSecurity.NOT_SPECIFIED) + return true; - // - // Check the data security of the content against the selected provider: - // - return isTrustedProvider switch - { - // The provider is trusted -- it may see data of any data source: - true => true, + // + // Is the provider trusted for data-source security checks? + // + var isTrustedProvider = provider switch + { + IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), + AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), - // The provider is not trusted -- it depends on the data security of the content: - false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED, - }; + _ => false, + }; + + // + // Check the data security of the content against the selected provider: + // + return isTrustedProvider switch + { + // The provider is trusted -- it may see data of any data source: + true => true, + + // The provider is not trusted -- it depends on the data security of the content: + false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED, + }; + } } private static LLMProviders GetLLMProvider(T provider) => provider switch diff --git a/app/MindWork AI Studio/Chat/ChatRoleExtensions.cs b/app/MindWork AI Studio/Chat/ChatRoleExtensions.cs index 40987d09f..37c968c9e 100644 --- a/app/MindWork AI Studio/Chat/ChatRoleExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatRoleExtensions.cs @@ -6,70 +6,69 @@ public static class ChatRoleExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatRoleExtensions).Namespace, nameof(ChatRoleExtensions)); - /// - /// Returns the name of the role. - /// /// The role. - /// The name of the role. - public static string ToName(this ChatRole role) => role switch + extension(ChatRole role) { - ChatRole.SYSTEM => TB("System"), - ChatRole.USER => TB("You"), - ChatRole.AI => TB("AI"), + /// + /// Returns the name of the role. + /// + /// The name of the role. + public string ToName() => role switch + { + ChatRole.SYSTEM => TB("System"), + ChatRole.USER => TB("You"), + ChatRole.AI => TB("AI"), - _ => TB("Unknown"), - }; - - /// - /// Returns the color of the role. - /// - /// The role. - /// The color of the role. - public static Color ToColor(this ChatRole role) => role switch - { - ChatRole.SYSTEM => Color.Info, - ChatRole.USER => Color.Primary, - ChatRole.AI => Color.Tertiary, - - _ => Color.Error, - }; - - /// - /// Returns the icon of the role. - /// - /// The role. - /// The icon of the role. - public static string ToIcon(this ChatRole role) => role switch - { - ChatRole.SYSTEM => Icons.Material.Filled.Settings, - ChatRole.USER => Icons.Material.Filled.Person, - ChatRole.AI => Icons.Material.Filled.AutoAwesome, - - _ => Icons.Material.Filled.Help, - }; + _ => TB("Unknown"), + }; - /// - /// Returns the specific name of the role for the chat template. - /// - /// The role. - /// The name of the role. - public static string ToChatTemplateName(this ChatRole role) => role switch - { - ChatRole.SYSTEM => TB("System"), - ChatRole.USER => TB("User"), - ChatRole.AI => TB("Assistant"), + /// + /// Returns the color of the role. + /// + /// The color of the role. + public Color ToColor() => role switch + { + ChatRole.SYSTEM => Color.Info, + ChatRole.USER => Color.Primary, + ChatRole.AI => Color.Tertiary, + + _ => Color.Error, + }; - _ => TB("Unknown"), - }; - - /// - /// Selects the next role for a chat template. - /// - /// The current role. - /// The next role for the chat template. - public static ChatRole SelectNextRoleForTemplate(this ChatRole currentRole) => currentRole switch - { - ChatRole.USER => ChatRole.AI, - _ => ChatRole.USER, - }; + /// + /// Returns the icon of the role. + /// + /// The icon of the role. + public string ToIcon() => role switch + { + ChatRole.SYSTEM => Icons.Material.Filled.Settings, + ChatRole.USER => Icons.Material.Filled.Person, + ChatRole.AI => Icons.Material.Filled.AutoAwesome, + + _ => Icons.Material.Filled.Help, + }; + + /// + /// Returns the specific name of the role for the chat template. + /// + /// The name of the role. + public string ToChatTemplateName() => role switch + { + ChatRole.SYSTEM => TB("System"), + ChatRole.USER => TB("User"), + ChatRole.AI => TB("Assistant"), + + _ => TB("Unknown"), + }; + + /// + /// Selects the next role for a chat template. + /// + /// The next role for the chat template. + public ChatRole SelectNextRoleForTemplate() => role switch + { + ChatRole.USER => ChatRole.AI, + _ => ChatRole.USER, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs index 9c7828ad3..f007b8ace 100644 --- a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs @@ -2,24 +2,27 @@ namespace AIStudio.Chat; public static class ChatThreadExtensions { - /// - /// Checks if the specified provider is allowed for the chat thread. - /// - /// - /// A provider the organization prohibited is never allowed, whatever the chat thread.

- /// - /// We don't check if the provider is allowed to use the data sources of the chat thread. - /// That kind of check is done when the available data sources are resolved.

- /// - /// One thing which is not so obvious: after RAG was used on this thread, the entire chat - /// thread is kind of a data source by itself. Why? Because the augmentation data collected - /// from the data sources is stored in the chat thread. This means we must check if the - /// selected provider is allowed to use this thread's data security and confidence level. - /// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left - /// the thread is checked with as well. - ///
/// The chat thread to check; null for a new chat, which demands nothing yet. - /// The provider to check. - /// True, when the provider is allowed for the chat thread. False, otherwise. - public static bool IsLLMProviderAllowed(this ChatThread? chatThread, T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider); + extension(ChatThread? chatThread) + { + /// + /// Checks if the specified provider is allowed for the chat thread. + /// + /// + /// A provider the organization prohibited is never allowed, whatever the chat thread.

+ /// + /// We don't check if the provider is allowed to use the data sources of the chat thread. + /// That kind of check is done when the available data sources are resolved.

+ /// + /// One thing which is not so obvious: after RAG was used on this thread, the entire chat + /// thread is kind of a data source by itself. Why? Because the augmentation data collected + /// from the data sources is stored in the chat thread. This means we must check if the + /// selected provider is allowed to use this thread's data security and confidence level. + /// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left + /// the thread is checked with as well. + ///
+ /// The provider to check. + /// True, when the provider is allowed for the chat thread. False, otherwise. + public bool IsLLMProviderAllowed(T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider); + } } diff --git a/app/MindWork AI Studio/Chat/IContentExtensions.cs b/app/MindWork AI Studio/Chat/IContentExtensions.cs index d0082d3c7..6c09a6302 100644 --- a/app/MindWork AI Studio/Chat/IContentExtensions.cs +++ b/app/MindWork AI Studio/Chat/IContentExtensions.cs @@ -2,114 +2,114 @@ namespace AIStudio.Chat; public static class IContentExtensions { - /// - /// Detaches whoever listens to the stream of this content. - /// - /// - /// The streaming handlers are closures over the component which registered them. A content - /// object belongs to the chat thread and therefore outlives every component which renders it, - /// so handlers left behind would keep those components alive for as long as the thread exists. - /// Whoever registers a handler calls this when it is no longer needed. - /// - /// The content whose streaming handlers you want to detach. - public static void ResetStreamingHandlers(this IContent content) + /// The content of a message in the chat. + extension(IContent content) { - content.StreamingEvent = IContent.NO_STREAMING_HANDLER; - content.StreamingDone = IContent.NO_STREAMING_HANDLER; - } - - /// - /// Reads this content as the Markdown text the AI produced. - /// - /// - /// Only text content carries Markdown. Everything else, an image for example, has no text - /// representation at all, which is why this reports failure instead of returning a placeholder: - /// a caller which writes files must not put an excuse into the file it writes. This is the text - /// the model wrote and nothing else: whoever reads a table out of a message wants exactly that, - /// while whoever writes a file wants the sources along with it and asks for the export reading. - /// - /// The content to read. - /// The Markdown text, or an empty string when there is none. - /// True, when this content carries Markdown text. - public static bool TryGetMarkdownText(this IContent content, out string markdown) - { - if (content is ContentText text) + /// + /// Detaches whoever listens to the stream of this content. + /// + /// + /// The streaming handlers are closures over the component which registered them. A content + /// object belongs to the chat thread and therefore outlives every component which renders it, + /// so handlers left behind would keep those components alive for as long as the thread exists. + /// Whoever registers a handler calls this when it is no longer needed. + /// + public void ResetStreamingHandlers() { - markdown = text.Text; - return true; + content.StreamingEvent = IContent.NO_STREAMING_HANDLER; + content.StreamingDone = IContent.NO_STREAMING_HANDLER; } - markdown = string.Empty; - return false; - } - - /// - /// Reads this content the way it leaves AI Studio, as a file or through the clipboard. - /// - /// - /// What the user sees is the answer together with the sources AI Studio collected for it, and - /// that is what a document has to hold as well: an answer built on a web page a tool read, or on - /// a document of the user, is worth little when the reader cannot tell which one it was. Those - /// sources are not part of the text the model wrote, they hang on the content, which is why - /// every path out of the app asks for this and not for the text alone. - /// - /// The content to read. - /// The Markdown text including its sources, or an empty string when there is none. - /// Whether a link into a local file may name its page. Only a - /// format whose reader stumbles over such a link says no here; the clipboard and every text - /// format keep the page. - /// True, when this content carries Markdown text. - public static bool TryGetExportMarkdown(this IContent content, out string markdown, bool keepPageAnchors = true) - { - if (content is not ContentText text) + /// + /// Reads this content as the Markdown text the AI produced. + /// + /// + /// Only text content carries Markdown. Everything else, an image for example, has no text + /// representation at all, which is why this reports failure instead of returning a placeholder: + /// a caller which writes files must not put an excuse into the file it writes. This is the text + /// the model wrote and nothing else: whoever reads a table out of a message wants exactly that, + /// while whoever writes a file wants the sources along with it and asks for the export reading. + /// + /// The Markdown text, or an empty string when there is none. + /// True, when this content carries Markdown text. + public bool TryGetMarkdownText(out string markdown) { + if (content is ContentText text) + { + markdown = text.Text; + return true; + } + markdown = string.Empty; return false; } - markdown = AppendSources(text.Text.Trim(), text.Sources.ToExportMarkdown(keepPageAnchors)); - return true; - } + /// + /// Reads this content the way it leaves AI Studio, as a file or through the clipboard. + /// + /// + /// What the user sees is the answer together with the sources AI Studio collected for it, and + /// that is what a document has to hold as well: an answer built on a web page a tool read, or on + /// a document of the user, is worth little when the reader cannot tell which one it was. Those + /// sources are not part of the text the model wrote, they hang on the content, which is why + /// every path out of the app asks for this and not for the text alone. + /// + /// The Markdown text including its sources, or an empty string when there is none. + /// Whether a link into a local file may name its page. Only a + /// format whose reader stumbles over such a link says no here; the clipboard and every text + /// format keep the page. + /// True, when this content carries Markdown text. + public bool TryGetExportMarkdown(out string markdown, bool keepPageAnchors = true) + { + if (content is not ContentText text) + { + markdown = string.Empty; + return false; + } - /// - /// Reads one file out of this content the way it leaves AI Studio, together with the sources - /// the answer rests on. - /// - /// - /// A document saved on its own came out of the same answer, so it rests on the same sources - /// and takes them along. How depends on the format. Markdown is what the source list is written - /// in, so a Markdown text gets it just as the entire answer does. A web page or a LaTeX document - /// gets it as a comment at its end: anything visible would have to be woven into markup the - /// model wrote. A fragment has no body to put it in, a page may hide whatever lies outside its - /// layout, and one underscore in a title is enough to stop a LaTeX run. A comment breaks - /// neither, and whoever opens the file finds it. A table gets no sources at all, since it has - /// no column a link list would fit into. Neither does code: a program, a script, or a - /// configuration is meant for a compiler or a tool, and a comment of ours would be the one part - /// of it nobody asked for. In some languages, such as JSON, it would even break the file. - /// - /// Apart from that, the file is what the model wrote, scripts of a web page included. Saving it - /// is what the user chose to do; the chat still never renders it. Code only gets what its tools - /// expect of a file, see ToSourceCodeFile. - /// - /// The content the file was found in. - /// The file, as PlainFileExport.ExtractFiles read it out of this content. - /// The content of the file to write. - public static string ToExportContent(this IContent content, MessageFile file) - { - if (file.Format.IsTabular()) - return file.Content; + markdown = AppendSources(text.Text.Trim(), text.Sources.ToExportMarkdown(keepPageAnchors)); + return true; + } - if (file.Format is FileExportFormat.SOURCE_CODE) - return ToSourceCodeFile(file); + /// + /// Reads one file out of this content the way it leaves AI Studio, together with the sources + /// the answer rests on. + /// + /// + /// A document saved on its own came out of the same answer, so it rests on the same sources + /// and takes them along. How depends on the format. Markdown is what the source list is written + /// in, so a Markdown text gets it just as the entire answer does. A web page or a LaTeX document + /// gets it as a comment at its end: anything visible would have to be woven into markup the + /// model wrote. A fragment has no body to put it in, a page may hide whatever lies outside its + /// layout, and one underscore in a title is enough to stop a LaTeX run. A comment breaks + /// neither, and whoever opens the file finds it. A table gets no sources at all, since it has + /// no column a link list would fit into. Neither does code: a program, a script, or a + /// configuration is meant for a compiler or a tool, and a comment of ours would be the one part + /// of it nobody asked for. In some languages, such as JSON, it would even break the file. + /// + /// Apart from that, the file is what the model wrote, scripts of a web page included. Saving it + /// is what the user chose to do; the chat still never renders it. Code only gets what its tools + /// expect of a file, see ToSourceCodeFile. + /// + /// The file, as PlainFileExport.ExtractFiles read it out of this content. + /// The content of the file to write. + public string ToExportContent(MessageFile file) + { + if (file.Format.IsTabular()) + return file.Content; - var sources = content.Sources.ToExportMarkdown(file.Format.FollowsPageAnchors()); - if (file.Format is FileExportFormat.MARKDOWN) - return AppendSources(file.Content, sources); + if (file.Format is FileExportFormat.SOURCE_CODE) + return ToSourceCodeFile(file); - if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment)) - return file.Content; + var sources = content.Sources.ToExportMarkdown(file.Format.FollowsPageAnchors()); + if (file.Format is FileExportFormat.MARKDOWN) + return AppendSources(file.Content, sources); - return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; + if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment)) + return file.Content; + + return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; + } } /// diff --git a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs index 2f8210b67..d065ea00c 100644 --- a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs +++ b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs @@ -30,76 +30,79 @@ public static class IImageSourceExtensions return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES); } - /// - /// Read the image content as a base64 string. - /// - /// - /// This is for the images inside the content of a chat, which go to external data sources and - /// agents as they are. Attached images, which go to a model, take ModelImageService instead: it - /// scales them down and encodes them anew within the limit of the provider.
- ///
- /// The images are directly converted to base64 strings. The maximum - /// size of the image is around 10 MB. If the image is larger, the method - /// returns an empty string.
- ///
- /// Note as well that this method returns just the base64 string without - /// any data URI prefix (like "data:image/png;base64,"). The caller has - /// to take care of that if needed. - ///
/// The image source. - /// The cancellation token. - /// The image content as a base64 string; might be empty. - public static async Task<(bool success, string base64Content)> TryAsBase64(this IImageSource image, CancellationToken token = default) + extension(IImageSource image) { - switch (image.SourceType) + /// + /// Read the image content as a base64 string. + /// + /// + /// This is for the images inside the content of a chat, which go to external data sources and + /// agents as they are. Attached images, which go to a model, take ModelImageService instead: it + /// scales them down and encodes them anew within the limit of the provider.
+ ///
+ /// The images are directly converted to base64 strings. The maximum + /// size of the image is around 10 MB. If the image is larger, the method + /// returns an empty string.
+ ///
+ /// Note as well that this method returns just the base64 string without + /// any data URI prefix (like "data:image/png;base64,"). The caller has + /// to take care of that if needed. + ///
+ /// The cancellation token. + /// The image content as a base64 string; might be empty. + public async Task<(bool success, string base64Content)> TryAsBase64(CancellationToken token = default) { - case ContentImageSource.BASE64: - return (success: true, image.Source); - - case ContentImageSource.URL: + switch (image.SourceType) { - using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED); - using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token); - var timeoutToken = timeoutTokenSource.Token; - using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken); - if(response.IsSuccessStatusCode) - { - // Read the length of the content: - var lengthBytes = response.Content.Headers.ContentLength; - if(lengthBytes > MAX_IMAGE_SIZE_BYTES) - { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image."))); - return (success: false, string.Empty); - } - - var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken); - return (success: true, Convert.ToBase64String(bytes)); - } - - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("Failed to download the image from the URL. Skipping the image."))); - return (success: false, string.Empty); - } - - case ContentImageSource.LOCAL_PATH: - if(File.Exists(image.Source)) - { - // Read the content length: - var length = new FileInfo(image.Source).Length; - if(length > MAX_IMAGE_SIZE_BYTES) - { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image."))); - return (success: false, string.Empty); - } - - var bytes = await File.ReadAllBytesAsync(image.Source, token); - return (success: true, Convert.ToBase64String(bytes)); - } - - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file does not exist. Skipping the image."))); - return (success: false, string.Empty); + case ContentImageSource.BASE64: + return (success: true, image.Source); - default: - return (success: false, string.Empty); + case ContentImageSource.URL: + { + using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED); + using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token); + var timeoutToken = timeoutTokenSource.Token; + using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken); + if(response.IsSuccessStatusCode) + { + // Read the length of the content: + var lengthBytes = response.Content.Headers.ContentLength; + if(lengthBytes > MAX_IMAGE_SIZE_BYTES) + { + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image."))); + return (success: false, string.Empty); + } + + var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken); + return (success: true, Convert.ToBase64String(bytes)); + } + + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("Failed to download the image from the URL. Skipping the image."))); + return (success: false, string.Empty); + } + + case ContentImageSource.LOCAL_PATH: + if(File.Exists(image.Source)) + { + // Read the content length: + var length = new FileInfo(image.Source).Length; + if(length > MAX_IMAGE_SIZE_BYTES) + { + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image."))); + return (success: false, string.Empty); + } + + var bytes = await File.ReadAllBytesAsync(image.Source, token); + return (success: true, Convert.ToBase64String(bytes)); + } + + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file does not exist. Skipping the image."))); + return (success: false, string.Empty); + + default: + return (success: false, string.Empty); + } } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs b/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs index e1fce1b3a..55e184208 100644 --- a/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs +++ b/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs @@ -9,161 +9,159 @@ namespace AIStudio.Chat; public static class ListContentBlockExtensions { - /// - /// Processes a list of content blocks by transforming them into a collection of message results asynchronously. - /// /// The list of content blocks to process. - /// A function that transforms each content block into a message result asynchronously. - /// The configured provider, whose model is being written to. - /// A factory function to create text sub-content. - /// A factory function to create image sub-content from an image prepared for the model. - /// The cancellation token. - /// An asynchronous task that resolves to a list of transformed results. - public static async Task> BuildMessagesAsync( - this List blocks, - AIStudio.Settings.Provider provider, - Func roleTransformer, - Func textSubContentFactory, - Func imageSubContentFactory, - CancellationToken token = default) + extension(List blocks) { - // - // Asked through the configured provider, so that what a person set in their expert settings - // counts here too. It did not: this path read the automatic answer alone, so somebody who - // switched image input on saw it work while attaching the picture and saw it ignored while - // the message was built -- every chat round and every tool round. - // - var canProcessImages = provider.SupportsImageInput(); - - // - // One report for the whole request: every message of the conversation reads its attached - // documents anew, all at the same time. Twenty documents must produce one dialog listing - // all of them, not twenty dialogs in a row. A request without documents has nothing to - // report, and needs no scope. - // - await using var reportingScope = blocks.Any(block => block.Content is ContentText text && text.FileAttachments.MayContainDocuments()) - ? Program.SERVICE_PROVIDER.GetRequiredService().BeginAction() - : null; - - var messageTaskList = new List>(blocks.Count); - foreach (var block in blocks) + /// + /// Processes a list of content blocks by transforming them into a collection of message results asynchronously. + /// + /// A function that transforms each content block into a message result asynchronously. + /// The configured provider, whose model is being written to. + /// A factory function to create text sub-content. + /// A factory function to create image sub-content from an image prepared for the model. + /// The cancellation token. + /// An asynchronous task that resolves to a list of transformed results. + public async Task> BuildMessagesAsync( + AIStudio.Settings.Provider provider, + Func roleTransformer, + Func textSubContentFactory, + Func imageSubContentFactory, + CancellationToken token = default) { - switch (block.Content) + // + // Asked through the configured provider, so that what a person set in their expert settings + // counts here too. It did not: this path read the automatic answer alone, so somebody who + // switched image input on saw it work while attaching the picture and saw it ignored while + // the message was built -- every chat round and every tool round. + // + var canProcessImages = provider.SupportsImageInput(); + + // + // One report for the whole request: every message of the conversation reads its attached + // documents anew, all at the same time. Twenty documents must produce one dialog listing + // all of them, not twenty dialogs in a row. A request without documents has nothing to + // report, and needs no scope. + // + await using var reportingScope = blocks.Any(block => block.Content is ContentText text && text.FileAttachments.MayContainDocuments()) + ? Program.SERVICE_PROVIDER.GetRequiredService().BeginAction() + : null; + + var messageTaskList = new List>(blocks.Count); + foreach (var block in blocks) { - // The prompt may or may not contain image(s), but the provider/model cannot process images. - // Thus, we treat it as a regular text message. - case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !canProcessImages: - messageTaskList.Add(CreateTextMessageAsync(block, text)); - break; + switch (block.Content) + { + // The prompt may or may not contain image(s), but the provider/model cannot process images. + // Thus, we treat it as a regular text message. + case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !canProcessImages: + messageTaskList.Add(CreateTextMessageAsync(block, text)); + break; - // The regular case for text content without images: - case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !text.FileAttachments.ContainsImages(): - messageTaskList.Add(CreateTextMessageAsync(block, text)); - break; + // The regular case for text content without images: + case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !text.FileAttachments.ContainsImages(): + messageTaskList.Add(CreateTextMessageAsync(block, text)); + break; - // Text prompt with images as attachments, and the provider/model can process images: - case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages(): - messageTaskList.Add(CreateMultimodalMessageAsync(block, text)); - break; + // Text prompt with images as attachments, and the provider/model can process images: + case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages(): + messageTaskList.Add(CreateMultimodalMessageAsync(block, text)); + break; + } + } + + // Await all messages: + await Task.WhenAll(messageTaskList); + + // Select all results: + return messageTaskList.Select(n => n.Result).ToList(); + + // Local function to create a text message asynchronously. + Task CreateTextMessageAsync(ContentBlock block, ContentText text) + { + return Task.Run(async () => new TextMessage + { + Role = roleTransformer(block.Role), + Content = await text.PrepareTextContentForAI(reportingScope), + } as IMessageBase); + } + + // Local function to create a multimodal message asynchronously. + Task CreateMultimodalMessageAsync(ContentBlock block, ContentText text) + { + return Task.Run(async () => + { + var images = await Task.WhenAll(text.FileAttachments + .Where(x => x is { IsImage: true, Exists: true }) + .Cast() + .Select(CreateImageSubContentAsync)); + + return new MultimodalMessage + { + Role = roleTransformer(block.Role), + Content = + [ + textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)), + ..images.OfType(), + ] + } as IMessageBase; + }); + } + + // + // Local function to prepare one image for the model. An image which cannot be prepared + // stays out of the message, and the user learns why. It used to go along as an image + // without data, which the providers refuse together with the whole request. + // + async Task CreateImageSubContentAsync(FileAttachmentImage image) + { + var prepared = await Program.SERVICE_PROVIDER.GetRequiredService().PrepareAsync(image, provider, token); + if (prepared.IsPrepared) + return imageSubContentFactory(prepared); + + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName))); + return null; } } - // Await all messages: - await Task.WhenAll(messageTaskList); - - // Select all results: - return messageTaskList.Select(n => n.Result).ToList(); - - // Local function to create a text message asynchronously. - Task CreateTextMessageAsync(ContentBlock block, ContentText text) - { - return Task.Run(async () => new TextMessage - { - Role = roleTransformer(block.Role), - Content = await text.PrepareTextContentForAI(reportingScope), - } as IMessageBase); - } - - // Local function to create a multimodal message asynchronously. - Task CreateMultimodalMessageAsync(ContentBlock block, ContentText text) - { - return Task.Run(async () => - { - var images = await Task.WhenAll(text.FileAttachments - .Where(x => x is { IsImage: true, Exists: true }) - .Cast() - .Select(CreateImageSubContentAsync)); - - return new MultimodalMessage - { - Role = roleTransformer(block.Role), - Content = - [ - textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)), - ..images.OfType(), - ] - } as IMessageBase; - }); - } + /// + /// Processes a list of content blocks using direct image URL format to create message results asynchronously. + /// + /// The configured provider, whose model is being written to. + /// An asynchronous task that resolves to a list of transformed message results. + /// + /// Uses direct image URL format where the image data is placed directly in the image_url field: + /// + /// { "type": "image_url", "image_url": "data:image/jpeg;base64,..." } + /// + /// This format is used by OpenAI, Mistral, and Ollama. + /// + public async Task> BuildMessagesUsingDirectImageUrlAsync( + AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( + provider, + StandardRoleTransformer, + StandardTextSubContentFactory, + DirectImageSubContentFactory); - // - // Local function to prepare one image for the model. An image which cannot be prepared - // stays out of the message, and the user learns why. It used to go along as an image - // without data, which the providers refuse together with the whole request. - // - async Task CreateImageSubContentAsync(FileAttachmentImage image) - { - var prepared = await Program.SERVICE_PROVIDER.GetRequiredService().PrepareAsync(image, provider, token); - if (prepared.IsPrepared) - return imageSubContentFactory(prepared); - - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName))); - return null; - } + /// + /// Processes a list of content blocks using nested image URL format to create message results asynchronously. + /// + /// The configured provider, whose model is being written to. + /// An asynchronous task that resolves to a list of transformed message results. + /// + /// Uses nested image URL format where the image data is wrapped in an object: + /// + /// { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } } + /// + /// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers. + /// + public async Task> BuildMessagesUsingNestedImageUrlAsync( + AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( + provider, + StandardRoleTransformer, + StandardTextSubContentFactory, + NestedImageSubContentFactory); } - /// - /// Processes a list of content blocks using direct image URL format to create message results asynchronously. - /// - /// The list of content blocks to process. - /// The configured provider, whose model is being written to. - /// An asynchronous task that resolves to a list of transformed message results. - /// - /// Uses direct image URL format where the image data is placed directly in the image_url field: - /// - /// { "type": "image_url", "image_url": "data:image/jpeg;base64,..." } - /// - /// This format is used by OpenAI, Mistral, and Ollama. - /// - public static async Task> BuildMessagesUsingDirectImageUrlAsync( - this List blocks, - AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( - provider, - StandardRoleTransformer, - StandardTextSubContentFactory, - DirectImageSubContentFactory); - - /// - /// Processes a list of content blocks using nested image URL format to create message results asynchronously. - /// - /// The list of content blocks to process. - /// The configured provider, whose model is being written to. - /// An asynchronous task that resolves to a list of transformed message results. - /// - /// Uses nested image URL format where the image data is wrapped in an object: - /// - /// { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } } - /// - /// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers. - /// - public static async Task> BuildMessagesUsingNestedImageUrlAsync( - this List blocks, - AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( - provider, - StandardRoleTransformer, - StandardTextSubContentFactory, - NestedImageSubContentFactory); - private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText { Text = text, diff --git a/app/MindWork AI Studio/Chat/ListFileAttachmentExtensions.cs b/app/MindWork AI Studio/Chat/ListFileAttachmentExtensions.cs index aaa2f65c9..8653cadef 100644 --- a/app/MindWork AI Studio/Chat/ListFileAttachmentExtensions.cs +++ b/app/MindWork AI Studio/Chat/ListFileAttachmentExtensions.cs @@ -2,17 +2,20 @@ namespace AIStudio.Chat; public static class ListFileAttachmentExtensions { - public static bool ContainsImages(this List attachments) => attachments.Any(attachment => attachment.IsImage); - - /// - /// Whether preparing the message for the AI may read a document through the runtime. - /// - /// - /// Every attachment but a picture counts: its type is checked again with the latest rules - /// before it is read, cf. FileAttachment.Normalize, and may turn out to be a document then. - /// Checking that here would touch every file twice, which takes long on a network drive. - /// /// The attachments of the message. - /// True when a document may be read. - public static bool MayContainDocuments(this List attachments) => attachments.Any(attachment => !attachment.IsImage); + extension(List attachments) + { + public bool ContainsImages() => attachments.Any(attachment => attachment.IsImage); + + /// + /// Whether preparing the message for the AI may read a document through the runtime. + /// + /// + /// Every attachment but a picture counts: its type is checked again with the latest rules + /// before it is read, cf. FileAttachment.Normalize, and may turn out to be a document then. + /// Checking that here would touch every file twice, which takes long on a network drive. + /// + /// True when a document may be read. + public bool MayContainDocuments() => attachments.Any(attachment => !attachment.IsImage); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/SetFileAttachmentExtensions.cs b/app/MindWork AI Studio/Chat/SetFileAttachmentExtensions.cs index 5445d7b9b..c9738e547 100644 --- a/app/MindWork AI Studio/Chat/SetFileAttachmentExtensions.cs +++ b/app/MindWork AI Studio/Chat/SetFileAttachmentExtensions.cs @@ -2,20 +2,23 @@ namespace AIStudio.Chat; public static class SetFileAttachmentExtensions { - /// - /// Attaches a file in place of the attachment of the same file the set held before. - /// - /// - /// The attachments differ by more than their files: one of a message being edited holds the - /// entry of the text it was sent with, and the size of the file may have changed since. Both - /// would stay in the set, and the message would go with the old and the new text of the file. - /// Attaching a file again means the file as it is now. - /// /// The attachments of a message. - /// The attachment of the file just attached. - public static void AttachInPlaceOfSameFile(this HashSet attachments, FileAttachment attachment) + extension(HashSet attachments) { - attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal)); - attachments.Add(attachment); + /// + /// Attaches a file in place of the attachment of the same file the set held before. + /// + /// + /// The attachments differ by more than their files: one of a message being edited holds the + /// entry of the text it was sent with, and the size of the file may have changed since. Both + /// would stay in the set, and the message would go with the old and the new text of the file. + /// Attaching a file again means the file as it is now. + /// + /// The attachment of the file just attached. + public void AttachInPlaceOfSameFile(FileAttachment attachment) + { + attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal)); + attachments.Add(attachment); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/StringExtension.cs b/app/MindWork AI Studio/Chat/StringExtension.cs index fac9f1431..309c33165 100644 --- a/app/MindWork AI Studio/Chat/StringExtension.cs +++ b/app/MindWork AI Studio/Chat/StringExtension.cs @@ -2,37 +2,40 @@ public static class StringExtensions { - public static string RemoveThinkTags(this string input) - { - const string OPEN_TAG = ""; - const string CLOSE_TAG = ""; - if (string.IsNullOrWhiteSpace(input) || !input.StartsWith(OPEN_TAG, StringComparison.Ordinal)) - return input; - - var endIndex = input.IndexOf(CLOSE_TAG, StringComparison.Ordinal); - if (endIndex == -1) - return string.Empty; - - return input[(endIndex + CLOSE_TAG.Length)..]; - } - - /// - /// Shortens a text to the given number of characters, and marks the cut with three dots. - /// - /// - /// Never cuts between the two halves of a surrogate pair, which no JSON writer takes.

- /// Takes and returns a string rather than a span on purpose: most texts are short enough, and - /// those come back as the very same instance. Only a text which is cut costs one new string. - ///
/// The text. - /// How many characters of the text to keep at most, the dots not counted. - /// The text as it was when it is short enough, otherwise its beginning followed by three dots. - public static string Shorten(this string text, int maxCharacters) + extension(string text) { - if (text.Length <= maxCharacters) - return text; + public string RemoveThinkTags() + { + const string OPEN_TAG = ""; + const string CLOSE_TAG = ""; + if (string.IsNullOrWhiteSpace(text) || !text.StartsWith(OPEN_TAG, StringComparison.Ordinal)) + return text; + + var endIndex = text.IndexOf(CLOSE_TAG, StringComparison.Ordinal); + if (endIndex == -1) + return string.Empty; + + return text[(endIndex + CLOSE_TAG.Length)..]; + } - var end = char.IsHighSurrogate(text[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters; - return string.Concat(text.AsSpan(0, end).TrimEnd(), "..."); + /// + /// Shortens a text to the given number of characters, and marks the cut with three dots. + /// + /// + /// Never cuts between the two halves of a surrogate pair, which no JSON writer takes.

+ /// Takes and returns a string rather than a span on purpose: most texts are short enough, and + /// those come back as the very same instance. Only a text which is cut costs one new string. + ///
+ /// How many characters of the text to keep at most, the dots not counted. + /// The text as it was when it is short enough, otherwise its beginning followed by three dots. + public string Shorten(int maxCharacters) + { + if (text.Length <= maxCharacters) + return text; + + var end = char.IsHighSurrogate(text[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters; + return string.Concat(text.AsSpan(0, end).TrimEnd(), "..."); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ReadWebContentStepsExtensions.cs b/app/MindWork AI Studio/Components/ReadWebContentStepsExtensions.cs index d7c01c516..419cfb37e 100644 --- a/app/MindWork AI Studio/Components/ReadWebContentStepsExtensions.cs +++ b/app/MindWork AI Studio/Components/ReadWebContentStepsExtensions.cs @@ -6,19 +6,22 @@ public static class ReadWebContentStepsExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReadWebContentStepsExtensions).Namespace, nameof(ReadWebContentStepsExtensions)); - /// - /// Gets the text representation of a given ReadWebContentSteps enum value. - /// /// The ReadWebContentSteps enum value. - /// >The text representation of the process step. - public static string GetText(this ReadWebContentSteps step) => step switch + extension(ReadWebContentSteps step) { - ReadWebContentSteps.START => TB("Start"), - ReadWebContentSteps.LOADING => TB("Loading"), - ReadWebContentSteps.PARSING => TB("Parsing"), - ReadWebContentSteps.CLEANING => TB("Cleaning"), - ReadWebContentSteps.DONE => TB("Done"), + /// + /// Gets the text representation of a given ReadWebContentSteps enum value. + /// + /// >The text representation of the process step. + public string GetText() => step switch + { + ReadWebContentSteps.START => TB("Start"), + ReadWebContentSteps.LOADING => TB("Loading"), + ReadWebContentSteps.PARSING => TB("Parsing"), + ReadWebContentSteps.CLEANING => TB("Cleaning"), + ReadWebContentSteps.DONE => TB("Done"), - _ => TB("n/a") - }; + _ => TB("n/a") + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ConfidenceLevelExtensions.cs b/app/MindWork AI Studio/Provider/ConfidenceLevelExtensions.cs index c0053b61b..22d12227a 100644 --- a/app/MindWork AI Studio/Provider/ConfidenceLevelExtensions.cs +++ b/app/MindWork AI Studio/Provider/ConfidenceLevelExtensions.cs @@ -7,50 +7,53 @@ public static class ConfidenceLevelExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceLevelExtensions).Namespace, nameof(ConfidenceLevelExtensions)); - public static string GetName(this ConfidenceLevel level) => level switch + extension(ConfidenceLevel level) { - ConfidenceLevel.NONE => TB("No provider selected"), + public string GetName() => level switch + { + ConfidenceLevel.NONE => TB("No provider selected"), - ConfidenceLevel.UNTRUSTED => TB("Untrusted"), - ConfidenceLevel.UNKNOWN => TB("Unknown"), - ConfidenceLevel.VERY_LOW => TB("Very Low"), - ConfidenceLevel.LOW => TB("Low"), - ConfidenceLevel.MODERATE => TB("Moderate"), - ConfidenceLevel.MEDIUM => TB("Medium"), - ConfidenceLevel.HIGH => TB("High"), + ConfidenceLevel.UNTRUSTED => TB("Untrusted"), + ConfidenceLevel.UNKNOWN => TB("Unknown"), + ConfidenceLevel.VERY_LOW => TB("Very Low"), + ConfidenceLevel.LOW => TB("Low"), + ConfidenceLevel.MODERATE => TB("Moderate"), + ConfidenceLevel.MEDIUM => TB("Medium"), + ConfidenceLevel.HIGH => TB("High"), - _ => TB("Unknown confidence level"), - }; - - public static string GetColor(this ConfidenceLevel level, SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch - { - (ConfidenceLevel.NONE, false) => "#cccccc", - (ConfidenceLevel.NONE, true) => "#666666", + _ => TB("Unknown confidence level"), + }; - (ConfidenceLevel.UNKNOWN, false) => "#777777", - (ConfidenceLevel.UNKNOWN, true) => "#aaaaaa", + public string GetColor(SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch + { + (ConfidenceLevel.NONE, false) => "#cccccc", + (ConfidenceLevel.NONE, true) => "#666666", + + (ConfidenceLevel.UNKNOWN, false) => "#777777", + (ConfidenceLevel.UNKNOWN, true) => "#aaaaaa", - (ConfidenceLevel.UNTRUSTED, false) => "#ff0000", - (ConfidenceLevel.UNTRUSTED, true) => "#800000", + (ConfidenceLevel.UNTRUSTED, false) => "#ff0000", + (ConfidenceLevel.UNTRUSTED, true) => "#800000", - (ConfidenceLevel.VERY_LOW, false) => "#ff6600", - (ConfidenceLevel.VERY_LOW, true) => "#803300", + (ConfidenceLevel.VERY_LOW, false) => "#ff6600", + (ConfidenceLevel.VERY_LOW, true) => "#803300", - (ConfidenceLevel.LOW, false) => "#ffcc00", - (ConfidenceLevel.LOW, true) => "#806600", + (ConfidenceLevel.LOW, false) => "#ffcc00", + (ConfidenceLevel.LOW, true) => "#806600", - (ConfidenceLevel.MODERATE, false) => "#99cc00", - (ConfidenceLevel.MODERATE, true) => "#4d6600", + (ConfidenceLevel.MODERATE, false) => "#99cc00", + (ConfidenceLevel.MODERATE, true) => "#4d6600", - (ConfidenceLevel.MEDIUM, false) => "#86b300", - (ConfidenceLevel.MEDIUM, true) => "#394d00", + (ConfidenceLevel.MEDIUM, false) => "#86b300", + (ConfidenceLevel.MEDIUM, true) => "#394d00", - (ConfidenceLevel.HIGH, false) => "#009933", - (ConfidenceLevel.HIGH, true) => "#004d1a", + (ConfidenceLevel.HIGH, false) => "#009933", + (ConfidenceLevel.HIGH, true) => "#004d1a", - (_, false) => "#cc6600", - (_, true) => "#663300", - }; - - public static string SetColorStyle(this ConfidenceLevel level, SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};"; + (_, false) => "#cc6600", + (_, true) => "#663300", + }; + + public string SetColorStyle(SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};"; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index de57ea88c..0ffc33c25 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -6,153 +6,153 @@ public static class HFInferenceProviderExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions)); - /// - /// The slug Hugging Face uses for this inference provider. - /// /// The inference provider. - /// The slug, or an empty string for the routing strategies, which name no provider. - public static string EndpointsId(this HFInferenceProvider provider) => provider switch + extension(HFInferenceProvider provider) { - HFInferenceProvider.BASETEN => "baseten", - HFInferenceProvider.CEREBRAS => "cerebras", - HFInferenceProvider.COHERE => "cohere", - HFInferenceProvider.DEEPINFRA => "deepinfra", - HFInferenceProvider.FEATHERLESS_AI => "featherless-ai", - HFInferenceProvider.FIREWORKS => "fireworks-ai", - HFInferenceProvider.GROQ => "groq", - HFInferenceProvider.NOVITA => "novita", - HFInferenceProvider.NSCALE => "nscale", - HFInferenceProvider.OVHCLOUD => "ovhcloud", - HFInferenceProvider.PUBLIC_AI => "publicai", - HFInferenceProvider.SCALEWAY => "scaleway", - HFInferenceProvider.TOGETHER_AI => "together", - HFInferenceProvider.ZAI => "zai-org", + /// + /// The slug Hugging Face uses for this inference provider. + /// + /// The slug, or an empty string for the routing strategies, which name no provider. + public string EndpointsId() => provider switch + { + HFInferenceProvider.BASETEN => "baseten", + HFInferenceProvider.CEREBRAS => "cerebras", + HFInferenceProvider.COHERE => "cohere", + HFInferenceProvider.DEEPINFRA => "deepinfra", + HFInferenceProvider.FEATHERLESS_AI => "featherless-ai", + HFInferenceProvider.FIREWORKS => "fireworks-ai", + HFInferenceProvider.GROQ => "groq", + HFInferenceProvider.NOVITA => "novita", + HFInferenceProvider.NSCALE => "nscale", + HFInferenceProvider.OVHCLOUD => "ovhcloud", + HFInferenceProvider.PUBLIC_AI => "publicai", + HFInferenceProvider.SCALEWAY => "scaleway", + HFInferenceProvider.TOGETHER_AI => "together", + HFInferenceProvider.ZAI => "zai-org", - _ => string.Empty, - }; + _ => string.Empty, + }; - /// - /// The suffix which tells the router where to send the request. - /// - /// - /// The router serves every provider through one endpoint. Which provider answers is decided by - /// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router - /// picks the fastest provider itself. - /// - /// The inference provider. - /// The suffix including its colon, or an empty string when the router should choose. - public static string ModelSuffix(this HFInferenceProvider provider) => provider switch - { - HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty, + /// + /// The suffix which tells the router where to send the request. + /// + /// + /// The router serves every provider through one endpoint. Which provider answers is decided by + /// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router + /// picks the fastest provider itself. + /// + /// The suffix including its colon, or an empty string when the router should choose. + public string ModelSuffix() => provider switch + { + HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty, - HFInferenceProvider.CHEAPEST => ":cheapest", - HFInferenceProvider.PREFERRED => ":preferred", + HFInferenceProvider.CHEAPEST => ":cheapest", + HFInferenceProvider.PREFERRED => ":preferred", - _ => $":{provider.EndpointsId()}", - }; + _ => $":{provider.EndpointsId()}", + }; - /// - /// Whether this inference provider serves models to chat with. - /// - /// The inference provider. - /// True, when the provider serves chat models. - public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE; + /// + /// Whether this inference provider serves models to chat with. + /// + /// True, when the provider serves chat models. + public bool SupportsChat() => provider is not HFInferenceProvider.NONE; - /// - /// Whether this inference provider creates embeddings for us. - /// - /// - /// Embeddings are a much shorter story than chatting. The router serves them nowhere near its - /// own endpoint, only through the route of a provider, and only two of those answer the - /// OpenAI-compatible form we send. The routing strategies are out by their nature: without a - /// named provider there is no route to address. - /// - /// The inference provider. - /// True, when we can create embeddings through this provider. - public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; + /// + /// Whether this inference provider creates embeddings for us. + /// + /// + /// Embeddings are a much shorter story than chatting. The router serves them nowhere near its + /// own endpoint, only through the route of a provider, and only two of those answer the + /// OpenAI-compatible form we send. The routing strategies are out by their nature: without a + /// named provider there is no route to address. + /// + /// True, when we can create embeddings through this provider. + public bool SupportsEmbeddings() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; - /// - /// Whether this inference provider transcribes audio for us. - /// - /// - /// The same two providers as for embeddings, and for the same reason: transcription lives on a - /// provider's own route, and only these two answer the OpenAI-compatible form there. Others do - /// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and - /// Replicate both turn the request down with "Model not supported by provider". - /// - /// The inference provider. - /// True, when we can transcribe audio through this provider. - public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; + /// + /// Whether this inference provider transcribes audio for us. + /// + /// + /// The same two providers as for embeddings, and for the same reason: transcription lives on a + /// provider's own route, and only these two answer the OpenAI-compatible form there. Others do + /// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and + /// Replicate both turn the request down with "Model not supported by provider". + /// + /// True, when we can transcribe audio through this provider. + public bool SupportsTranscription() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; - /// - /// The base URL of the provider's own OpenAI-compatible route. - /// - /// - /// Only chatting goes through the router's own endpoint. Everything else has to address the - /// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra - /// keeps it below an additional "openai" segment, and answers the path without it with - /// "Not allowed to POST /v1/embeddings for provider deepinfra". - /// - /// The inference provider. - /// The base URL, or an empty string when the provider has no such route. - public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch - { - HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/", - HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/", + /// + /// The base URL of the provider's own OpenAI-compatible route. + /// + /// + /// Only chatting goes through the router's own endpoint. Everything else has to address the + /// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra + /// keeps it below an additional "openai" segment, and answers the path without it with + /// "Not allowed to POST /v1/embeddings for provider deepinfra". + /// + /// The base URL, or an empty string when the provider has no such route. + public string ProviderBaseURL() => provider switch + { + HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/", + HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/", - _ => string.Empty, - }; + _ => string.Empty, + }; + + /// + /// The value to filter the Hugging Face model catalog by. + /// + /// The provider slug, or "all" when no particular provider was chosen. + public string CatalogFilter() + { + var slug = provider.EndpointsId(); + return string.IsNullOrEmpty(slug) ? "all" : slug; + } + + public string ToName() => provider switch + { + HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"), + HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"), + HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"), + + HFInferenceProvider.BASETEN => "Baseten", + HFInferenceProvider.CEREBRAS => "Cerebras", + HFInferenceProvider.COHERE => "Cohere", + HFInferenceProvider.DEEPINFRA => "DeepInfra", + HFInferenceProvider.FEATHERLESS_AI => "Featherless AI", + HFInferenceProvider.FIREWORKS => "Fireworks AI", + HFInferenceProvider.GROQ => "Groq", + HFInferenceProvider.NOVITA => "Novita", + HFInferenceProvider.NSCALE => "Nscale", + HFInferenceProvider.OVHCLOUD => "OVHcloud", + HFInferenceProvider.PUBLIC_AI => "Public AI", + HFInferenceProvider.SCALEWAY => "Scaleway", + HFInferenceProvider.TOGETHER_AI => "Together AI", + HFInferenceProvider.ZAI => "Z.ai", + + _ => string.Empty, + }; + } - /// - /// Removes the routing suffix from a model, if it carries one. - /// - /// - /// The suffix says where a request goes, not what the model is. Everything asking what a model - /// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as - /// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it. - /// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last - /// colon always starts the suffix. - /// /// The model as it is configured. - /// The model without its routing suffix. - public static Model WithoutRoutingSuffix(this Model model) + extension(Model model) { - var separatorIndex = model.Id.LastIndexOf(':'); - return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] }; + /// + /// Removes the routing suffix from a model, if it carries one. + /// + /// + /// The suffix says where a request goes, not what the model is. Everything asking what a model + /// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as + /// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it. + /// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last + /// colon always starts the suffix. + /// + /// The model without its routing suffix. + public Model WithoutRoutingSuffix() + { + var separatorIndex = model.Id.LastIndexOf(':'); + return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] }; + } } - - /// - /// The value to filter the Hugging Face model catalog by. - /// - /// The inference provider. - /// The provider slug, or "all" when no particular provider was chosen. - public static string CatalogFilter(this HFInferenceProvider provider) - { - var slug = provider.EndpointsId(); - return string.IsNullOrEmpty(slug) ? "all" : slug; - } - - public static string ToName(this HFInferenceProvider provider) => provider switch - { - HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"), - HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"), - HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"), - - HFInferenceProvider.BASETEN => "Baseten", - HFInferenceProvider.CEREBRAS => "Cerebras", - HFInferenceProvider.COHERE => "Cohere", - HFInferenceProvider.DEEPINFRA => "DeepInfra", - HFInferenceProvider.FEATHERLESS_AI => "Featherless AI", - HFInferenceProvider.FIREWORKS => "Fireworks AI", - HFInferenceProvider.GROQ => "Groq", - HFInferenceProvider.NOVITA => "Novita", - HFInferenceProvider.NSCALE => "Nscale", - HFInferenceProvider.OVHCLOUD => "OVHcloud", - HFInferenceProvider.PUBLIC_AI => "Public AI", - HFInferenceProvider.SCALEWAY => "Scaleway", - HFInferenceProvider.TOGETHER_AI => "Together AI", - HFInferenceProvider.ZAI => "Z.ai", - - _ => string.Empty, - }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 6394228a7..b415e0aaa 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -30,366 +30,586 @@ public static class LLMProvidersExtensions private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LLMProvidersExtensions).Namespace, nameof(LLMProvidersExtensions)); - /// - /// Returns the human-readable name of the provider. - /// - /// - /// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces, - /// or other stable identifiers. - /// - /// The provider. - /// The human-readable name of the provider. - public static string ToName(this LLMProviders llmProvider) => llmProvider.ToName(translate: true); - - /// - /// Returns the human-readable name of the provider. - /// - /// The provider. - /// Whether generic provider names should be translated. - /// The human-readable name of the provider. - public static string ToName(this LLMProviders llmProvider, bool translate) => llmProvider switch + /// The provider. + extension(LLMProviders provider) { - LLMProviders.NONE => translate ? TB("No provider selected") : "No provider selected", - - LLMProviders.OPEN_AI => "OpenAI", - LLMProviders.ANTHROPIC => "Anthropic", - LLMProviders.MISTRAL => "Mistral", - LLMProviders.GOOGLE => "Google", - LLMProviders.X => "xAI", - LLMProviders.DEEP_SEEK => "DeepSeek", - LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", - LLMProviders.PERPLEXITY => "Perplexity", - LLMProviders.OPEN_ROUTER => "OpenRouter", - LLMProviders.HETZNER => "Hetzner (Experimental)", - LLMProviders.IONOS => "IONOS", - LLMProviders.LITE_LLM => "LiteLLM", - LLMProviders.REQUESTY => "Requesty", + /// + /// Returns the human-readable name of the provider. + /// + /// + /// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces, + /// or other stable identifiers. + /// + /// The human-readable name of the provider. + public string ToName() => provider.ToName(translate: true); - LLMProviders.GROQ => "Groq", - LLMProviders.FIREWORKS => "Fireworks.ai", - LLMProviders.HUGGINGFACE => "Hugging Face", + /// + /// Returns the human-readable name of the provider. + /// + /// Whether generic provider names should be translated. + /// The human-readable name of the provider. + public string ToName(bool translate) => provider switch + { + LLMProviders.NONE => translate ? TB("No provider selected") : "No provider selected", + + LLMProviders.OPEN_AI => "OpenAI", + LLMProviders.ANTHROPIC => "Anthropic", + LLMProviders.MISTRAL => "Mistral", + LLMProviders.GOOGLE => "Google", + LLMProviders.X => "xAI", + LLMProviders.DEEP_SEEK => "DeepSeek", + LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", + LLMProviders.PERPLEXITY => "Perplexity", + LLMProviders.OPEN_ROUTER => "OpenRouter", + LLMProviders.HETZNER => "Hetzner (Experimental)", + LLMProviders.IONOS => "IONOS", + LLMProviders.LITE_LLM => "LiteLLM", + LLMProviders.REQUESTY => "Requesty", - LLMProviders.SELF_HOSTED => translate ? TB("Self-hosted") : "Self-hosted", - - LLMProviders.HELMHOLTZ => "Helmholtz Blablador", - LLMProviders.GWDG => "GWDG SAIA", - - _ => translate ? TB("Unknown") : "Unknown", - }; + LLMProviders.GROQ => "Groq", + LLMProviders.FIREWORKS => "Fireworks.ai", + LLMProviders.HUGGINGFACE => "Hugging Face", - /// - /// Returns the stable secret namespace for the provider. - /// - /// - /// These values are used for OS keyring namespaces. They must never be localized or changed without - /// an explicit migration for existing API keys. - /// - /// The provider. - /// The stable secret namespace for the provider. - public static string ToSecretId(this LLMProviders llmProvider) => llmProvider switch - { - LLMProviders.NONE => "No provider selected", + LLMProviders.SELF_HOSTED => translate ? TB("Self-hosted") : "Self-hosted", + + LLMProviders.HELMHOLTZ => "Helmholtz Blablador", + LLMProviders.GWDG => "GWDG SAIA", + + _ => translate ? TB("Unknown") : "Unknown", + }; - LLMProviders.OPEN_AI => "OpenAI", - LLMProviders.ANTHROPIC => "Anthropic", - LLMProviders.MISTRAL => "Mistral", - LLMProviders.GOOGLE => "Google", - LLMProviders.X => "xAI", - LLMProviders.DEEP_SEEK => "DeepSeek", - LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", - LLMProviders.PERPLEXITY => "Perplexity", - LLMProviders.OPEN_ROUTER => "OpenRouter", - LLMProviders.HETZNER => "Hetzner", - LLMProviders.IONOS => "IONOS", - LLMProviders.LITE_LLM => "LiteLLM", - LLMProviders.REQUESTY => "Requesty", + /// + /// Returns the stable secret namespace for the provider. + /// + /// + /// These values are used for OS keyring namespaces. They must never be localized or changed without + /// an explicit migration for existing API keys. + /// + /// The stable secret namespace for the provider. + public string ToSecretId() => provider switch + { + LLMProviders.NONE => "No provider selected", - LLMProviders.GROQ => "Groq", - LLMProviders.FIREWORKS => "Fireworks.ai", - LLMProviders.HUGGINGFACE => "Hugging Face", + LLMProviders.OPEN_AI => "OpenAI", + LLMProviders.ANTHROPIC => "Anthropic", + LLMProviders.MISTRAL => "Mistral", + LLMProviders.GOOGLE => "Google", + LLMProviders.X => "xAI", + LLMProviders.DEEP_SEEK => "DeepSeek", + LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", + LLMProviders.PERPLEXITY => "Perplexity", + LLMProviders.OPEN_ROUTER => "OpenRouter", + LLMProviders.HETZNER => "Hetzner", + LLMProviders.IONOS => "IONOS", + LLMProviders.LITE_LLM => "LiteLLM", + LLMProviders.REQUESTY => "Requesty", - LLMProviders.SELF_HOSTED => "Self-hosted", + LLMProviders.GROQ => "Groq", + LLMProviders.FIREWORKS => "Fireworks.ai", + LLMProviders.HUGGINGFACE => "Hugging Face", - LLMProviders.HELMHOLTZ => "Helmholtz Blablador", - LLMProviders.GWDG => "GWDG SAIA", + LLMProviders.SELF_HOSTED => "Self-hosted", - _ => "Unknown", - }; - - /// - /// Get a provider's confidence. - /// - /// The provider. - /// The settings manager. - /// The confidence of the provider. - public static Confidence GetConfidence(this LLMProviders llmProvider, SettingsManager settingsManager) => llmProvider switch - { - LLMProviders.NONE => Confidence.NONE, + LLMProviders.HELMHOLTZ => "Helmholtz Blablador", + LLMProviders.GWDG => "GWDG SAIA", + + _ => "Unknown", + }; + + /// + /// Get a provider's confidence. + /// + /// The settings manager. + /// The confidence of the provider. + public Confidence GetConfidence(SettingsManager settingsManager) => provider switch + { + LLMProviders.NONE => Confidence.NONE, - LLMProviders.FIREWORKS => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.FIREWORKS => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - // Not trusted, because huggingface only routes you to a third-party-provider and we can't make sure they do not use your data - LLMProviders.HUGGINGFACE => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://huggingface.co/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + // Not trusted, because huggingface only routes you to a third-party-provider and we can't make sure they do not use your data + LLMProviders.HUGGINGFACE => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://huggingface.co/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources( - "https://platform.openai.com/docs/models/default-usage-policies-by-endpoint", - "https://openai.com/policies/terms-of-use/", - "https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance", - "https://openai.com/enterprise-privacy/" - ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources( + "https://platform.openai.com/docs/models/default-usage-policies-by-endpoint", + "https://openai.com/policies/terms-of-use/", + "https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance", + "https://openai.com/enterprise-privacy/" + ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.GOOGLE => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://ai.google.dev/gemini-api/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.GOOGLE => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://ai.google.dev/gemini-api/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.GROQ => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://wow.groq.com/terms-of-use/").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.GROQ => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://wow.groq.com/terms-of-use/").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources( - "https://experiments.hetzner.com/docs/inference", - "https://www.hetzner.com/legal/privacy-policy/", - "https://www.hetzner.com/legal/terms-and-conditions/" - ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources( + "https://experiments.hetzner.com/docs/inference", + "https://www.hetzner.com/legal/privacy-policy/", + "https://www.hetzner.com/legal/terms-and-conditions/" + ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.IONOS => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources( - "https://docs.ionos.com/cloud/ai/ai-model-hub/governance-and-compliance/data-handling", - "https://www.ionos.com/terms-gtc/privacy-policy/" - ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.IONOS => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources( + "https://docs.ionos.com/cloud/ai/ai-model-hub/governance-and-compliance/data-handling", + "https://www.ionos.com/terms-gtc/privacy-policy/" + ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - // LiteLLM is a gateway the user runs, but it is not a self-hosted LLM: the proxy owner decides - // which downstream providers it routes to, and those are usually cloud services. Self-hosting - // the proxy therefore says nothing about where the data ends up, so we do not claim the trust - // of a self-hosted model here and let the user assign the level themselves. - LLMProviders.LITE_LLM => Confidence.USER_OPERATED_GATEWAY.WithSources("https://docs.litellm.ai/docs/data_security").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + // LiteLLM is a gateway the user runs, but it is not a self-hosted LLM: the proxy owner decides + // which downstream providers it routes to, and those are usually cloud services. Self-hosting + // the proxy therefore says nothing about where the data ends up, so we do not claim the trust + // of a self-hosted model here and let the user assign the level themselves. + LLMProviders.LITE_LLM => Confidence.USER_OPERATED_GATEWAY.WithSources("https://docs.litellm.ai/docs/data_security").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - // Requesty Ltd is based in London and works under the UK GDPR and the EU GDPR. Unless logging - // is switched off, it keeps requests for up to 30 days, and it may use the content of free - // accounts for training. Where a model runs depends on the model rather than on Requesty: - // names ending in "@eu" or in another European region stay in Europe, the others may not. - // That is why we state the region but leave the trust level unknown. - LLMProviders.REQUESTY => Confidence.UNKNOWN.WithRegion("Europe, United Kingdom").WithSources( - "https://www.requesty.ai/privacy", - "https://www.requesty.ai/privacy/subprocessors", - "https://www.requesty.ai/dpa", - "https://www.requesty.ai/terms" - ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + // Requesty Ltd is based in London and works under the UK GDPR and the EU GDPR. Unless logging + // is switched off, it keeps requests for up to 30 days, and it may use the content of free + // accounts for training. Where a model runs depends on the model rather than on Requesty: + // names ending in "@eu" or in another European region stay in Europe, the others may not. + // That is why we state the region but leave the trust level unknown. + LLMProviders.REQUESTY => Confidence.UNKNOWN.WithRegion("Europe, United Kingdom").WithSources( + "https://www.requesty.ai/privacy", + "https://www.requesty.ai/privacy/subprocessors", + "https://www.requesty.ai/dpa", + "https://www.requesty.ai/terms" + ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), - LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/ai-services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), + LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/ai-services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), - _ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), - }; + _ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)), + }; - /// - /// Determines if the specified provider supports embeddings. - /// - /// The provider to check. - /// True if the provider supports embeddings; otherwise, false. - public static bool ProvideEmbeddingAPI(this LLMProviders llmProvider) => llmProvider switch - { - // - // Providers that support embeddings: - // - LLMProviders.OPEN_AI => true, - LLMProviders.MISTRAL => true, - LLMProviders.GOOGLE => true, - LLMProviders.HELMHOLTZ => true, - LLMProviders.ALIBABA_CLOUD => true, - LLMProviders.IONOS => true, - LLMProviders.GWDG => true, - LLMProviders.OPEN_ROUTER => true, - LLMProviders.LITE_LLM => true, - LLMProviders.REQUESTY => true, - - // - // Providers that do not support embeddings: - // - LLMProviders.GROQ => false, - LLMProviders.ANTHROPIC => false, - LLMProviders.FIREWORKS => false, - LLMProviders.X => false, - LLMProviders.DEEP_SEEK => false, - LLMProviders.PERPLEXITY => false, - LLMProviders.HETZNER => false, - - // - // Hugging Face serves embeddings, but not through the router endpoint we chat with: that - // one answers "/v1/embeddings" with a plain "Not Found". They have to be asked of one - // inference provider directly, and only some of them answer the OpenAI-compatible form. - // Which ones is decided by HFInferenceProviderExtensions.SupportsEmbeddings. - // - LLMProviders.HUGGINGFACE => true, - - // - // Self-hosted providers are treated as a special case anyway. - // - LLMProviders.SELF_HOSTED => true, - - _ => false, - }; - - public static bool ProvideTranscriptionAPI(this LLMProviders llmProvider) => llmProvider switch - { - // - // Providers that support transcription: - // - LLMProviders.OPEN_AI => true, - LLMProviders.MISTRAL => true, - LLMProviders.FIREWORKS => true, - LLMProviders.GWDG => true, - LLMProviders.HELMHOLTZ => true, - LLMProviders.GROQ => true, - LLMProviders.LITE_LLM => true, - LLMProviders.REQUESTY => true, - - // - // Providers that support transcription but provide no OpenAI-compatible API yet: - // - LLMProviders.ALIBABA_CLOUD => false, - LLMProviders.GOOGLE => false, - - // - // Providers that do not support transcription: - // - LLMProviders.OPEN_ROUTER => false, - LLMProviders.HETZNER => false, - LLMProviders.IONOS => false, - LLMProviders.ANTHROPIC => false, - LLMProviders.X => false, - LLMProviders.DEEP_SEEK => false, - LLMProviders.PERPLEXITY => false, - - // - // Hugging Face transcribes audio, but like embeddings, not through the router endpoint we - // chat with: that one answers "/v1/audio/transcriptions" with a plain "Not Found". Only - // some of the inference providers answer the OpenAI-compatible form, which - // HFInferenceProviderExtensions.SupportsTranscription decides. - // - LLMProviders.HUGGINGFACE => true, - - // - // Self-hosted providers are treated as a special case anyway. - // - LLMProviders.SELF_HOSTED => true, - - _ => false, - }; - - /// - /// The largest image file, in bytes, which the provider accepts in a request, where it states one. - /// - /// - /// The limit belongs to the API of the provider, not to the model: the same model may accept - /// larger images through another provider. What we actually send is asked through - /// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit. - /// - /// The provider to check. - /// The size limit of one image file, or null when the provider states none we know of. - public static long? GetMaxImageSizeBytes(this LLMProviders llmProvider) => llmProvider switch - { - // - // Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four - // characters for every three bytes, which leaves 3,932,160 bytes for the image file itself: - // - LLMProviders.ANTHROPIC => 3_932_160, - - _ => null, - }; - - /// - /// Creates a new provider instance based on the provider value. - /// - /// The provider settings. - /// The provider instance. - public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) - { - return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration, capabilityOverrides: providerSettings.CapabilityOverrides, tokenizerPath: providerSettings.TokenizerPath); - } - - /// - /// Creates a new provider instance based on the embedding provider value. - /// - /// The embedding provider settings. - /// The provider instance. - public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) - { - return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING, tokenizerPath: embeddingProviderSettings.TokenizerPath); - } - - /// - /// Creates a new provider instance based on the speech provider value. - /// - /// The speech provider settings. - /// The provider instance. - public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) - { - return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION); - } - - private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT, ProviderCapabilityOverrides? capabilityOverrides = null, string tokenizerPath = "") - { - // - // Every provider instance comes from here, for LLMs, embeddings, and transcriptions alike. - // Whatever a caller forgot to check, no request reaches a provider the organization prohibited: - // - if (IsProhibitedByOrganization(provider)) + /// + /// Determines if the specified provider supports embeddings. + /// + /// True if the provider supports embeddings; otherwise, false. + public bool ProvideEmbeddingAPI() => provider switch { - LOGGER.LogWarning($"Refused to create the provider '{instanceName}' of '{provider}': the organization prohibited this provider."); - return new NoProvider(); - } + // + // Providers that support embeddings: + // + LLMProviders.OPEN_AI => true, + LLMProviders.MISTRAL => true, + LLMProviders.GOOGLE => true, + LLMProviders.HELMHOLTZ => true, + LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.IONOS => true, + LLMProviders.GWDG => true, + LLMProviders.OPEN_ROUTER => true, + LLMProviders.LITE_LLM => true, + LLMProviders.REQUESTY => true, - try + // + // Providers that do not support embeddings: + // + LLMProviders.GROQ => false, + LLMProviders.ANTHROPIC => false, + LLMProviders.FIREWORKS => false, + LLMProviders.X => false, + LLMProviders.DEEP_SEEK => false, + LLMProviders.PERPLEXITY => false, + LLMProviders.HETZNER => false, + + // + // Hugging Face serves embeddings, but not through the router endpoint we chat with: that + // one answers "/v1/embeddings" with a plain "Not Found". They have to be asked of one + // inference provider directly, and only some of them answer the OpenAI-compatible form. + // Which ones is decided by HFInferenceProviderExtensions.SupportsEmbeddings. + // + LLMProviders.HUGGINGFACE => true, + + // + // Self-hosted providers are treated as a special case anyway. + // + LLMProviders.SELF_HOSTED => true, + + _ => false, + }; + + public bool ProvideTranscriptionAPI() => provider switch { - IProvider providerInstance = provider switch + // + // Providers that support transcription: + // + LLMProviders.OPEN_AI => true, + LLMProviders.MISTRAL => true, + LLMProviders.FIREWORKS => true, + LLMProviders.GWDG => true, + LLMProviders.HELMHOLTZ => true, + LLMProviders.GROQ => true, + LLMProviders.LITE_LLM => true, + LLMProviders.REQUESTY => true, + + // + // Providers that support transcription but provide no OpenAI-compatible API yet: + // + LLMProviders.ALIBABA_CLOUD => false, + LLMProviders.GOOGLE => false, + + // + // Providers that do not support transcription: + // + LLMProviders.OPEN_ROUTER => false, + LLMProviders.HETZNER => false, + LLMProviders.IONOS => false, + LLMProviders.ANTHROPIC => false, + LLMProviders.X => false, + LLMProviders.DEEP_SEEK => false, + LLMProviders.PERPLEXITY => false, + + // + // Hugging Face transcribes audio, but like embeddings, not through the router endpoint we + // chat with: that one answers "/v1/audio/transcriptions" with a plain "Not Found". Only + // some of the inference providers answer the OpenAI-compatible form, which + // HFInferenceProviderExtensions.SupportsTranscription decides. + // + LLMProviders.HUGGINGFACE => true, + + // + // Self-hosted providers are treated as a special case anyway. + // + LLMProviders.SELF_HOSTED => true, + + _ => false, + }; + + /// + /// The largest image file, in bytes, which the provider accepts in a request, where it states one. + /// + /// + /// The limit belongs to the API of the provider, not to the model: the same model may accept + /// larger images through another provider. What we actually send is asked through + /// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit. + /// + /// The size limit of one image file, or null when the provider states none we know of. + public long? GetMaxImageSizeBytes() => provider switch + { + // + // Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four + // characters for every three bytes, which leaves 3,932,160 bytes for the image file itself: + // + LLMProviders.ANTHROPIC => 3_932_160, + + _ => null, + }; + + private IProvider CreateProvider(string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT, ProviderCapabilityOverrides? capabilityOverrides = null, string tokenizerPath = "") + { + // + // Every provider instance comes from here, for LLMs, embeddings, and transcriptions alike. + // Whatever a caller forgot to check, no request reaches a provider the organization prohibited: + // + if (IsProhibitedByOrganization(provider)) { - LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.X => new ProviderX { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HETZNER => new ProviderHetzner { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.IONOS => new ProviderIONOS { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.LITE_LLM => new ProviderLiteLLM(hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.REQUESTY => new ProviderRequesty { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LOGGER.LogWarning($"Refused to create the provider '{instanceName}' of '{provider}': the organization prohibited this provider."); + return new NoProvider(); + } - LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, hfEndpointKind) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + try + { + IProvider providerInstance = provider switch + { + LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.X => new ProviderX { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HETZNER => new ProviderHetzner { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.IONOS => new ProviderIONOS { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.LITE_LLM => new ProviderLiteLLM(hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.REQUESTY => new ProviderRequesty { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, hfEndpointKind) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - _ => new NoProvider(), - }; + LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - if (providerInstance is BaseProvider baseProvider) - baseProvider.CapabilityOverrides = capabilityOverrides; + _ => new NoProvider(), + }; - return providerInstance; + if (providerInstance is BaseProvider baseProvider) + baseProvider.CapabilityOverrides = capabilityOverrides; + + return providerInstance; + } + catch (Exception e) + { + LOGGER.LogError($"Failed to create provider: {e.Message}"); + return new NoProvider(); + } } - catch (Exception e) + + public string GetCreationURL() => provider switch { - LOGGER.LogError($"Failed to create provider: {e.Message}"); - return new NoProvider(); + LLMProviders.OPEN_AI => "https://platform.openai.com/signup", + LLMProviders.MISTRAL => "https://console.mistral.ai/", + LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard", + LLMProviders.GOOGLE => "https://console.cloud.google.com/", + LLMProviders.X => "https://accounts.x.ai/sign-up", + LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up", + LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm", + LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api", + LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys", + LLMProviders.HETZNER => "https://experiments.hetzner.com", + LLMProviders.IONOS => "https://cloud.ionos.com/compute/sign-up", + LLMProviders.REQUESTY => "https://app.requesty.ai/api-keys", + + LLMProviders.GROQ => "https://console.groq.com/", + LLMProviders.FIREWORKS => "https://fireworks.ai/login", + LLMProviders.HUGGINGFACE => "https://huggingface.co/login", + + LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab", + LLMProviders.GWDG => "https://saia.gwdg.de", + + _ => string.Empty, + }; + + public string GetDashboardURL() => provider switch + { + LLMProviders.OPEN_AI => "https://platform.openai.com/usage", + LLMProviders.MISTRAL => "https://console.mistral.ai/usage/", + LLMProviders.ANTHROPIC => "https://console.anthropic.com/settings/plans", + LLMProviders.X => "https://console.x.ai/", + LLMProviders.GROQ => "https://console.groq.com/settings/usage", + LLMProviders.GOOGLE => "https://console.cloud.google.com/billing", + LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing", + LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage", + LLMProviders.ALIBABA_CLOUD => "https://usercenter2-intl.aliyun.com/billing", + LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/", + LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity", + LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing", + LLMProviders.HETZNER => "https://experiments.hetzner.com", + LLMProviders.IONOS => "https://dcd.ionos.com/latest/?page=dcd-ai-model-hub", + LLMProviders.REQUESTY => "https://app.requesty.ai/", + + LLMProviders.GWDG => "https://saia.gwdg.de/keys", + + _ => string.Empty, + }; + + public bool HasDashboard() => provider switch + { + LLMProviders.OPEN_AI => true, + LLMProviders.MISTRAL => true, + LLMProviders.ANTHROPIC => true, + LLMProviders.X => true, + LLMProviders.GROQ => true, + LLMProviders.FIREWORKS => true, + LLMProviders.GOOGLE => true, + LLMProviders.DEEP_SEEK => true, + LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, + LLMProviders.OPEN_ROUTER => true, + LLMProviders.HUGGINGFACE => true, + LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, + LLMProviders.REQUESTY => true, + + LLMProviders.GWDG => true, + + _ => false, + }; + + public string GetModelsOverviewURL(HFInferenceProvider inferenceProvider) => provider switch + { + LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless", + LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}", + _ => string.Empty, + }; + + public bool IsLLMModelProvidedManually() => provider switch + { + LLMProviders.FIREWORKS => true, + _ => false, + }; + + /// + /// Determines if the model selection should be completely hidden for LLM providers. + /// This is the case when the host does not support model selection. + /// + /// The host for self-hosted providers. + /// True if model selection should be hidden; otherwise, false. + public bool IsLLMModelSelectionHidden(Host host) => provider switch + { + _ => false, + }; + + /// + /// Determines if the model selection should be completely hidden for transcription providers. + /// This is the case when the host does not support model selection (e.g., whisper.cpp). + /// + /// The host for self-hosted providers. + /// True if model selection should be hidden; otherwise, false. + public bool IsTranscriptionModelSelectionHidden(Host host) => provider switch + { + LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP, + _ => false, + }; + + public bool IsHostNeeded() => provider switch + { + LLMProviders.SELF_HOSTED => true, + _ => false, + }; + + public bool IsHostnameNeeded() => provider switch + { + LLMProviders.SELF_HOSTED => true, + LLMProviders.LITE_LLM => true, + _ => false, + }; + + /// + /// Whether the provider runs on a server that the user or their organization operates. + /// + /// + /// The address of such a server comes from the user: a self-hosted model, or a LiteLLM gateway. + /// Only their operators may learn which feature of AI Studio sent a request, see AppUserAgent. + /// This is deliberately not derived from IsHostnameNeeded, even though both name the same providers + /// today: whether a dialog asks for a hostname is a question of the user interface, and it must + /// never silently decide what a server learns about the user. + /// + /// True when the provider runs on a self-hosted server; otherwise, false. + public bool RunsOnSelfHostedServer() => provider switch + { + LLMProviders.SELF_HOSTED => true, + LLMProviders.LITE_LLM => true, + _ => false, + }; + + public bool IsAPIKeyNeeded(Host host) => provider switch + { + LLMProviders.OPEN_AI => true, + LLMProviders.MISTRAL => true, + LLMProviders.ANTHROPIC => true, + LLMProviders.GOOGLE => true, + LLMProviders.X => true, + LLMProviders.DEEP_SEEK => true, + LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, + LLMProviders.OPEN_ROUTER => true, + LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, + LLMProviders.LITE_LLM => true, + LLMProviders.REQUESTY => true, + + LLMProviders.GROQ => true, + LLMProviders.FIREWORKS => true, + LLMProviders.HELMHOLTZ => true, + LLMProviders.GWDG => true, + LLMProviders.HUGGINGFACE => true, + + // Every self-hosted engine can ask for a key: LM Studio brings its own tokens, and any of + // them can sit behind a proxy which authenticates. The field is labeled as optional for + // them, so offering it costs nothing where no key is needed, while leaving it out means + // the user cannot enter the one their server expects: + LLMProviders.SELF_HOSTED => host is not Host.NONE, + + _ => false, + }; + + public bool ShowRegisterButton() => provider switch + { + LLMProviders.OPEN_AI => true, + LLMProviders.MISTRAL => true, + LLMProviders.ANTHROPIC => true, + LLMProviders.GOOGLE => true, + LLMProviders.X => true, + LLMProviders.DEEP_SEEK => true, + LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, + LLMProviders.OPEN_ROUTER => true, + LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, + LLMProviders.REQUESTY => true, + + LLMProviders.GROQ => true, + LLMProviders.FIREWORKS => true, + LLMProviders.HELMHOLTZ => true, + LLMProviders.GWDG => true, + LLMProviders.HUGGINGFACE => true, + + _ => false, + }; + + public bool CanLoadModels(Host host, string? apiKey) + { + if (provider is LLMProviders.SELF_HOSTED) + return host.IsModelListSupported(); + + if(provider is LLMProviders.NONE) + return false; + + if(string.IsNullOrWhiteSpace(apiKey)) + return false; + + return true; + } + + public bool IsHFInstanceProviderNeeded() => provider switch + { + LLMProviders.HUGGINGFACE => true, + _ => false, + }; + } + + /// The provider settings. + extension(AIStudio.Settings.Provider providerSettings) + { + /// + /// Creates a new provider instance based on the provider value. + /// + /// The provider instance. + public IProvider CreateProvider() + { + return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration, capabilityOverrides: providerSettings.CapabilityOverrides, tokenizerPath: providerSettings.TokenizerPath); + } + } + + /// The embedding provider settings. + extension(EmbeddingProvider embeddingProviderSettings) + { + /// + /// Creates a new provider instance based on the embedding provider value. + /// + /// The provider instance. + public IProvider CreateProvider() + { + return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING, tokenizerPath: embeddingProviderSettings.TokenizerPath); + } + } + + /// The speech provider settings. + extension(TranscriptionProvider transcriptionProviderSettings) + { + /// + /// Creates a new provider instance based on the speech provider value. + /// + /// The provider instance. + public IProvider CreateProvider() + { + return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION); } } @@ -407,220 +627,4 @@ public static class LLMProvidersExtensions // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract return Program.SERVICE_PROVIDER?.GetService() is { } settingsManager && settingsManager.IsProviderProhibited(provider); } - - public static string GetCreationURL(this LLMProviders provider) => provider switch - { - LLMProviders.OPEN_AI => "https://platform.openai.com/signup", - LLMProviders.MISTRAL => "https://console.mistral.ai/", - LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard", - LLMProviders.GOOGLE => "https://console.cloud.google.com/", - LLMProviders.X => "https://accounts.x.ai/sign-up", - LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up", - LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm", - LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api", - LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys", - LLMProviders.HETZNER => "https://experiments.hetzner.com", - LLMProviders.IONOS => "https://cloud.ionos.com/compute/sign-up", - LLMProviders.REQUESTY => "https://app.requesty.ai/api-keys", - - LLMProviders.GROQ => "https://console.groq.com/", - LLMProviders.FIREWORKS => "https://fireworks.ai/login", - LLMProviders.HUGGINGFACE => "https://huggingface.co/login", - - LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab", - LLMProviders.GWDG => "https://saia.gwdg.de", - - _ => string.Empty, - }; - - public static string GetDashboardURL(this LLMProviders provider) => provider switch - { - LLMProviders.OPEN_AI => "https://platform.openai.com/usage", - LLMProviders.MISTRAL => "https://console.mistral.ai/usage/", - LLMProviders.ANTHROPIC => "https://console.anthropic.com/settings/plans", - LLMProviders.X => "https://console.x.ai/", - LLMProviders.GROQ => "https://console.groq.com/settings/usage", - LLMProviders.GOOGLE => "https://console.cloud.google.com/billing", - LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing", - LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage", - LLMProviders.ALIBABA_CLOUD => "https://usercenter2-intl.aliyun.com/billing", - LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/", - LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity", - LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing", - LLMProviders.HETZNER => "https://experiments.hetzner.com", - LLMProviders.IONOS => "https://dcd.ionos.com/latest/?page=dcd-ai-model-hub", - LLMProviders.REQUESTY => "https://app.requesty.ai/", - - LLMProviders.GWDG => "https://saia.gwdg.de/keys", - - _ => string.Empty, - }; - - public static bool HasDashboard(this LLMProviders provider) => provider switch - { - LLMProviders.OPEN_AI => true, - LLMProviders.MISTRAL => true, - LLMProviders.ANTHROPIC => true, - LLMProviders.X => true, - LLMProviders.GROQ => true, - LLMProviders.FIREWORKS => true, - LLMProviders.GOOGLE => true, - LLMProviders.DEEP_SEEK => true, - LLMProviders.ALIBABA_CLOUD => true, - LLMProviders.PERPLEXITY => true, - LLMProviders.OPEN_ROUTER => true, - LLMProviders.HUGGINGFACE => true, - LLMProviders.HETZNER => true, - LLMProviders.IONOS => true, - LLMProviders.REQUESTY => true, - - LLMProviders.GWDG => true, - - _ => false, - }; - - public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch - { - LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless", - LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}", - _ => string.Empty, - }; - - public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch - { - LLMProviders.FIREWORKS => true, - _ => false, - }; - - /// - /// Determines if the model selection should be completely hidden for LLM providers. - /// This is the case when the host does not support model selection. - /// - /// The provider. - /// The host for self-hosted providers. - /// True if model selection should be hidden; otherwise, false. - public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch - { - _ => false, - }; - - /// - /// Determines if the model selection should be completely hidden for transcription providers. - /// This is the case when the host does not support model selection (e.g., whisper.cpp). - /// - /// The provider. - /// The host for self-hosted providers. - /// True if model selection should be hidden; otherwise, false. - public static bool IsTranscriptionModelSelectionHidden(this LLMProviders provider, Host host) => provider switch - { - LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP, - _ => false, - }; - - public static bool IsHostNeeded(this LLMProviders provider) => provider switch - { - LLMProviders.SELF_HOSTED => true, - _ => false, - }; - - public static bool IsHostnameNeeded(this LLMProviders provider) => provider switch - { - LLMProviders.SELF_HOSTED => true, - LLMProviders.LITE_LLM => true, - _ => false, - }; - - /// - /// Whether the provider runs on a server that the user or their organization operates. - /// - /// - /// The address of such a server comes from the user: a self-hosted model, or a LiteLLM gateway. - /// Only their operators may learn which feature of AI Studio sent a request, see AppUserAgent. - /// This is deliberately not derived from IsHostnameNeeded, even though both name the same providers - /// today: whether a dialog asks for a hostname is a question of the user interface, and it must - /// never silently decide what a server learns about the user. - /// - /// The provider. - /// True when the provider runs on a self-hosted server; otherwise, false. - public static bool RunsOnSelfHostedServer(this LLMProviders provider) => provider switch - { - LLMProviders.SELF_HOSTED => true, - LLMProviders.LITE_LLM => true, - _ => false, - }; - - public static bool IsAPIKeyNeeded(this LLMProviders provider, Host host) => provider switch - { - LLMProviders.OPEN_AI => true, - LLMProviders.MISTRAL => true, - LLMProviders.ANTHROPIC => true, - LLMProviders.GOOGLE => true, - LLMProviders.X => true, - LLMProviders.DEEP_SEEK => true, - LLMProviders.ALIBABA_CLOUD => true, - LLMProviders.PERPLEXITY => true, - LLMProviders.OPEN_ROUTER => true, - LLMProviders.HETZNER => true, - LLMProviders.IONOS => true, - LLMProviders.LITE_LLM => true, - LLMProviders.REQUESTY => true, - - LLMProviders.GROQ => true, - LLMProviders.FIREWORKS => true, - LLMProviders.HELMHOLTZ => true, - LLMProviders.GWDG => true, - LLMProviders.HUGGINGFACE => true, - - // Every self-hosted engine can ask for a key: LM Studio brings its own tokens, and any of - // them can sit behind a proxy which authenticates. The field is labeled as optional for - // them, so offering it costs nothing where no key is needed, while leaving it out means - // the user cannot enter the one their server expects: - LLMProviders.SELF_HOSTED => host is not Host.NONE, - - _ => false, - }; - - public static bool ShowRegisterButton(this LLMProviders provider) => provider switch - { - LLMProviders.OPEN_AI => true, - LLMProviders.MISTRAL => true, - LLMProviders.ANTHROPIC => true, - LLMProviders.GOOGLE => true, - LLMProviders.X => true, - LLMProviders.DEEP_SEEK => true, - LLMProviders.ALIBABA_CLOUD => true, - LLMProviders.PERPLEXITY => true, - LLMProviders.OPEN_ROUTER => true, - LLMProviders.HETZNER => true, - LLMProviders.IONOS => true, - LLMProviders.REQUESTY => true, - - LLMProviders.GROQ => true, - LLMProviders.FIREWORKS => true, - LLMProviders.HELMHOLTZ => true, - LLMProviders.GWDG => true, - LLMProviders.HUGGINGFACE => true, - - _ => false, - }; - - public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey) - { - if (provider is LLMProviders.SELF_HOSTED) - return host.IsModelListSupported(); - - if(provider is LLMProviders.NONE) - return false; - - if(string.IsNullOrWhiteSpace(apiKey)) - return false; - - return true; - } - - public static bool IsHFInstanceProviderNeeded(this LLMProviders provider) => provider switch - { - LLMProviders.HUGGINGFACE => true, - _ => false, - }; } diff --git a/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs index 0caa13869..ec1e052d1 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs @@ -5,41 +5,47 @@ public static class LLMProvidersIconExtensions private const string ICON_ROOT = "/images/provider-icons"; private const string SVG_DATA_URL_PREFIX = "data:image/svg+xml;base64,"; - public static string GetIconUrl(this AIStudio.Settings.Provider provider, bool isDarkMode) - => provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl); - - public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl) + extension(AIStudio.Settings.Provider provider) { - if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true) - return customIconDataUrl; - - return provider.GetIconUrl(isDarkMode); + public string GetIconUrl(bool isDarkMode) + => provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl); } - public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch + extension(LLMProviders provider) { - LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", - LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg", - LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg", - LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg", - LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg", - LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg", - LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg", - LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg", - LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg", - LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg", - LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg", - LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg", - LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg", - LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg", - LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg", - LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg", - LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg", - LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg", - LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg", - LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg", - _ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", - }; + public string GetIconUrl(bool isDarkMode, string? customIconDataUrl) + { + if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true) + return customIconDataUrl; + + return provider.GetIconUrl(isDarkMode); + } + + public string GetIconUrl(bool isDarkMode) => provider switch + { + LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", + LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg", + LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg", + LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg", + LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg", + LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg", + LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg", + LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg", + LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg", + LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg", + LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg", + LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg", + LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg", + LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg", + LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg", + LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg", + LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg", + LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg", + LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg", + LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg", + _ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", + }; + } private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty; } diff --git a/app/MindWork AI Studio/Provider/ModelLoadFailureReasonExtensions.cs b/app/MindWork AI Studio/Provider/ModelLoadFailureReasonExtensions.cs index 542fbccb3..6b2f9b2ff 100644 --- a/app/MindWork AI Studio/Provider/ModelLoadFailureReasonExtensions.cs +++ b/app/MindWork AI Studio/Provider/ModelLoadFailureReasonExtensions.cs @@ -6,16 +6,19 @@ public static class ModelLoadFailureReasonExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelLoadFailureReasonExtensions).Namespace, nameof(ModelLoadFailureReasonExtensions)); - public static string ToUserMessage(this ModelLoadFailureReason failureReason, string providerName) => failureReason switch + extension(ModelLoadFailureReason failureReason) { - ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName), - ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName), - ModelLoadFailureReason.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName), - ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), providerName), - ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName), - ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName), - ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName), + public string ToUserMessage(string providerName) => failureReason switch + { + ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName), + ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName), + ModelLoadFailureReason.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName), + ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), providerName), + ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName), + ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName), + ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName), - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/OpenAI/SystemPromptRoleExtensions.cs b/app/MindWork AI Studio/Provider/OpenAI/SystemPromptRoleExtensions.cs index 6cea1c9f9..2ee0c4a95 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/SystemPromptRoleExtensions.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/SystemPromptRoleExtensions.cs @@ -4,22 +4,25 @@ namespace AIStudio.Provider.OpenAI; public static class SystemPromptRoleExtensions { - /// - /// Spells a system prompt role the way OpenAI's APIs expect it. - /// - /// - /// A role no rule states becomes "developer". OpenAI's current documentation names no other, a - /// model the rules do not know is almost always one released after they were written, and the - /// older generations which still want "system" say so in their families. - /// /// The role the model profile states. - /// The role, as OpenAI names it. - public static string ToOpenAIRole(this SystemPromptRole role) => role switch + extension(SystemPromptRole role) { - SystemPromptRole.SYSTEM => "system", - SystemPromptRole.DEVELOPER => "developer", - SystemPromptRole.USER => "user", + /// + /// Spells a system prompt role the way OpenAI's APIs expect it. + /// + /// + /// A role no rule states becomes "developer". OpenAI's current documentation names no other, a + /// model the rules do not know is almost always one released after they were written, and the + /// older generations which still want "system" say so in their families. + /// + /// The role, as OpenAI names it. + public string ToOpenAIRole() => role switch + { + SystemPromptRole.SYSTEM => "system", + SystemPromptRole.DEVELOPER => "developer", + SystemPromptRole.USER => "user", - _ => "developer", - }; + _ => "developer", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ProviderRequestFailureReasonExtensions.cs b/app/MindWork AI Studio/Provider/ProviderRequestFailureReasonExtensions.cs index 7c0cc4a89..1346406c9 100644 --- a/app/MindWork AI Studio/Provider/ProviderRequestFailureReasonExtensions.cs +++ b/app/MindWork AI Studio/Provider/ProviderRequestFailureReasonExtensions.cs @@ -6,42 +6,45 @@ public static class ProviderRequestFailureReasonExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderRequestFailureReasonExtensions).Namespace, nameof(ProviderRequestFailureReasonExtensions)); - /// - /// Names the kind of failure in a few words. - /// - /// - /// Meant as a label beside the full message, so a list of failures can be scanned instead of - /// read: twenty entries which all say API key are one problem, not twenty. - /// - public static string GetName(this ProviderRequestFailureReason failureReason) => failureReason switch + extension(ProviderRequestFailureReason failureReason) { - ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"), - ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"), - ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"), - ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"), - ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"), - ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"), - ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"), - ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"), - ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"), - ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"), - ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"), + /// + /// Names the kind of failure in a few words. + /// + /// + /// Meant as a label beside the full message, so a list of failures can be scanned instead of + /// read: twenty entries which all say API key are one problem, not twenty. + /// + public string GetName() => failureReason switch + { + ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"), + ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"), + ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"), + ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"), + ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"), + ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"), + ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"), + ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"), + ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"), + ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"), + ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"), - _ => string.Empty, - }; + _ => string.Empty, + }; - /// - /// Gets a value indicating whether the way out of this failure is in the provider settings. - /// - /// - /// Only for the failures a setting actually fixes. Pointing at the settings for a provider - /// which is merely overloaded would send the user looking for a mistake they never made. - /// - public static bool IsFixedInProviderSettings(this ProviderRequestFailureReason failureReason) => failureReason is - ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or - ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or - ProviderRequestFailureReason.MODEL_NOT_FOUND or - ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or - ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or - ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED; + /// + /// Gets a value indicating whether the way out of this failure is in the provider settings. + /// + /// + /// Only for the failures a setting actually fixes. Pointing at the settings for a provider + /// which is merely overloaded would send the user looking for a mistake they never made. + /// + public bool IsFixedInProviderSettings() => failureReason is + ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or + ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or + ProviderRequestFailureReason.MODEL_NOT_FOUND or + ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or + ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or + ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs index 07594af81..037246edb 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs @@ -2,141 +2,143 @@ namespace AIStudio.Provider.SelfHosted; public static class HostExtensions { - public static string Name(this Host host) => host switch - { - Host.NONE => "None", - - Host.LM_STUDIO => "LM Studio", - Host.LLAMA_CPP => "llama.cpp", - Host.WHISPER_CPP => "whisper.cpp", - Host.OLLAMA => "ollama", - Host.VLLM => "vLLM", - Host.LLMMAN => "llmman", - - _ => "Unknown", - }; - - public static string BaseURL(this Host host) => host switch - { - _ => "/v1/", - }; - - public static string ChatURL(this Host host) => host switch - { - _ => "chat/completions", - }; - - public static string TranscriptionURL(this Host host) => host switch - { - _ => "audio/transcriptions", - }; - - public static string EmbeddingURL(this Host host) => host switch - { - _ => "embeddings", - }; - - /// - /// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one. - /// - /// - /// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this - /// belongs to the API of the engine, not to the model. What we actually send is asked through - /// IImageSourceExtensions.MaxImageSizeBytesFor. - /// /// The engine. - /// The size limit of one image file, or null when the engine states none we know of. - public static long? GetMaxImageSizeBytes(this Host host) => host switch + extension(Host host) { - // - // llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions - // keeps the default limit of its web framework, read on 2026-10-22 at - // https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes - // about 1.33 MiB as base64, which leaves room for the text of the conversation: - // - Host.LLMMAN => 1_048_576, - - _ => null, - }; - - public static bool IsChatSupported(this Host host) - { - switch (host) + public string Name() => host switch { - case Host.WHISPER_CPP: - return false; + Host.NONE => "None", + + Host.LM_STUDIO => "LM Studio", + Host.LLAMA_CPP => "llama.cpp", + Host.WHISPER_CPP => "whisper.cpp", + Host.OLLAMA => "ollama", + Host.VLLM => "vLLM", + Host.LLMMAN => "llmman", + + _ => "Unknown", + }; + + public string BaseURL() => host switch + { + _ => "/v1/", + }; + + public string ChatURL() => host switch + { + _ => "chat/completions", + }; + + public string TranscriptionURL() => host switch + { + _ => "audio/transcriptions", + }; + + public string EmbeddingURL() => host switch + { + _ => "embeddings", + }; + + /// + /// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one. + /// + /// + /// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this + /// belongs to the API of the engine, not to the model. What we actually send is asked through + /// IImageSourceExtensions.MaxImageSizeBytesFor. + /// + /// The size limit of one image file, or null when the engine states none we know of. + public long? GetMaxImageSizeBytes() => host switch + { + // + // llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions + // keeps the default limit of its web framework, read on 2026-10-22 at + // https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes + // about 1.33 MiB as base64, which leaves room for the text of the conversation: + // + Host.LLMMAN => 1_048_576, + + _ => null, + }; + + public bool IsChatSupported() + { + switch (host) + { + case Host.WHISPER_CPP: + return false; - default: - case Host.OLLAMA: - case Host.VLLM: - case Host.LM_STUDIO: - case Host.LLAMA_CPP: - case Host.LLMMAN: - return true; + default: + case Host.OLLAMA: + case Host.VLLM: + case Host.LM_STUDIO: + case Host.LLAMA_CPP: + case Host.LLMMAN: + return true; + } } - } - - public static bool IsEmbeddingSupported(this Host host) - { - switch (host) + + public bool IsEmbeddingSupported() { - case Host.LM_STUDIO: - case Host.OLLAMA: - case Host.VLLM: - case Host.LLMMAN: - return true; + switch (host) + { + case Host.LM_STUDIO: + case Host.OLLAMA: + case Host.VLLM: + case Host.LLMMAN: + return true; - default: - case Host.LLAMA_CPP: - return false; + default: + case Host.LLAMA_CPP: + return false; + } } - } - - public static bool IsTranscriptionSupported(this Host host) - { - switch (host) - { - case Host.OLLAMA: - case Host.VLLM: - case Host.WHISPER_CPP: - case Host.LLMMAN: - return true; - default: - case Host.LM_STUDIO: - case Host.LLAMA_CPP: - return false; + public bool IsTranscriptionSupported() + { + switch (host) + { + case Host.OLLAMA: + case Host.VLLM: + case Host.WHISPER_CPP: + case Host.LLMMAN: + return true; + + default: + case Host.LM_STUDIO: + case Host.LLAMA_CPP: + return false; + } } - } - /// - /// Whether the engine answers the models route with the models it serves. - /// - /// - /// This is the only place which says so. The provider asks it before it loads the models of a - /// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload - /// them; a new engine therefore needs no more than its line here. whisper.cpp is not among - /// them: it serves the one model it was started with and has no route to list it, so the - /// provider offers that model under a fixed name instead. llama.cpp is, even though builds - /// older than the route answer it with 404; the provider handles those builds itself. - /// - /// The engine. - /// True when the models can be loaded from the engine; otherwise, false. - public static bool IsModelListSupported(this Host host) - { - switch (host) + /// + /// Whether the engine answers the models route with the models it serves. + /// + /// + /// This is the only place which says so. The provider asks it before it loads the models of a + /// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload + /// them; a new engine therefore needs no more than its line here. whisper.cpp is not among + /// them: it serves the one model it was started with and has no route to list it, so the + /// provider offers that model under a fixed name instead. llama.cpp is, even though builds + /// older than the route answer it with 404; the provider handles those builds itself. + /// + /// True when the models can be loaded from the engine; otherwise, false. + public bool IsModelListSupported() { - case Host.LLAMA_CPP: - case Host.OLLAMA: - case Host.LM_STUDIO: - case Host.VLLM: - case Host.LLMMAN: - return true; + switch (host) + { + case Host.LLAMA_CPP: + case Host.OLLAMA: + case Host.LM_STUDIO: + case Host.VLLM: + case Host.LLMMAN: + return true; - default: - case Host.NONE: - case Host.WHISPER_CPP: - return false; + default: + case Host.NONE: + case Host.WHISPER_CPP: + return false; + } } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/BiasCategoryExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/BiasCategoryExtensions.cs index 4398c9c25..e16e1e554 100644 --- a/app/MindWork AI Studio/Settings/DataModel/BiasCategoryExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/BiasCategoryExtensions.cs @@ -2,54 +2,57 @@ namespace AIStudio.Settings.DataModel; public static class BiasCategoryExtensions { - public static string ToName(this BiasCategory biasCategory) => biasCategory switch + extension(BiasCategory biasCategory) { - BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?", - BiasCategory.TOO_MUCH_INFORMATION => "Too much information", - BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning", - BiasCategory.NEED_TO_ACT_FAST => "Need to act fast", + public string ToName() => biasCategory switch + { + BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?", + BiasCategory.TOO_MUCH_INFORMATION => "Too much information", + BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning", + BiasCategory.NEED_TO_ACT_FAST => "Need to act fast", - _ => "Unknown category", - }; - - public static string GetThoughts(this BiasCategory biasCategory) => biasCategory switch - { - BiasCategory.WHAT_SHOULD_WE_REMEMBER => - """ - - We store memories differently based on how they were experienced - - We reduce events and lists to their key elements - - We discard specifics to form generalities - - We edit and reinforce some memories after the fact - """, + _ => "Unknown category", + }; + + public string GetThoughts() => biasCategory switch + { + BiasCategory.WHAT_SHOULD_WE_REMEMBER => + """ + - We store memories differently based on how they were experienced + - We reduce events and lists to their key elements + - We discard specifics to form generalities + - We edit and reinforce some memories after the fact + """, - BiasCategory.TOO_MUCH_INFORMATION => - """ - - We notice things already primed in memory or repeated often - - Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things - - We notice when something has changed - - We are drawn to details that confirm our own existing beliefs - - We notice flaws in others more easily than we notice flaws in ourselves - """, + BiasCategory.TOO_MUCH_INFORMATION => + """ + - We notice things already primed in memory or repeated often + - Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things + - We notice when something has changed + - We are drawn to details that confirm our own existing beliefs + - We notice flaws in others more easily than we notice flaws in ourselves + """, - BiasCategory.NOT_ENOUGH_MEANING => - """ - - We tend to find stories and patterns even when looking at sparse data - - We fill in characteristics from stereotypes, generalities, and prior histories - - We imagine things and people we’re familiar with or fond of as better - - We simplify probabilities and numbers to make them easier to think about - - We project our current mindset and assumptions onto the past and future - - We think we know what other people are thinking - """, + BiasCategory.NOT_ENOUGH_MEANING => + """ + - We tend to find stories and patterns even when looking at sparse data + - We fill in characteristics from stereotypes, generalities, and prior histories + - We imagine things and people we’re familiar with or fond of as better + - We simplify probabilities and numbers to make them easier to think about + - We project our current mindset and assumptions onto the past and future + - We think we know what other people are thinking + """, - BiasCategory.NEED_TO_ACT_FAST => - """ - - We favor simple-looking options and complete information over complex, ambiguous options - - To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions - - To get things done, we tend to complete things we’ve invested time & energy in - - To stay focused, we favor the immediate, relatable thing in front of us - - To act, we must be confident we can make an impact and feel what we do is important - """, + BiasCategory.NEED_TO_ACT_FAST => + """ + - We favor simple-looking options and complete information over complex, ambiguous options + - To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions + - To get things done, we tend to complete things we’ve invested time & energy in + - To stay focused, we favor the immediate, relatable thing in front of us + - To act, we must be confident we can make an impact and feel what we do is important + """, - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs index 738f4fa28..42290910a 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs @@ -6,50 +6,56 @@ public static class DataSourceSecurityExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceSecurityExtensions).Namespace, nameof(DataSourceSecurityExtensions)); - public static string ToSelectionText(this DataSourceSecurity security) => security switch + extension(DataSourceSecurity security) { - DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"), + public string ToSelectionText() => security switch + { + DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"), - DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), - DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), + DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), + DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), - _ => TB("Unknown security policy") - }; - - public static string ToInfoText(this DataSourceSecurity security) => security switch - { - DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."), - - DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), - DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), - - _ => TB("Unknown security policy") - }; - - /// - /// The data security which demands what the one held and the one arriving demand together. - /// - /// - /// Data which may only be used with self-hosted providers keeps them as the only choice, no matter - /// what arrives later: the data was seen. Data which may be used with any provider marks the holder - /// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED - /// demands nothing and changes nothing. The order does not matter. - /// - /// The data security held so far. - /// What the data arriving demands. - /// The tightened data security; never looser than either of the two. - public static DataSourceSecurity TightenedBy(this DataSourceSecurity held, DataSourceSecurity arriving) => (held, arriving) switch - { - (DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED, - (_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY, - _ => held, - }; + _ => TB("Unknown security policy") + }; - public static TextColor GetColor(this DataSourceSecurity security) => security switch - { - DataSourceSecurity.ALLOW_ANY => TextColor.WARN, - DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS, + public string ToInfoText() => security switch + { + DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."), - _ => TextColor.ERROR - }; + DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), + DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), + + _ => TB("Unknown security policy") + }; + + public TextColor GetColor() => security switch + { + DataSourceSecurity.ALLOW_ANY => TextColor.WARN, + DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS, + + _ => TextColor.ERROR + }; + } + + /// The data security held so far. + extension(DataSourceSecurity held) + { + /// + /// The data security which demands what the one held and the one arriving demand together. + /// + /// + /// Data which may only be used with self-hosted providers keeps them as the only choice, no matter + /// what arrives later: the data was seen. Data which may be used with any provider marks the holder + /// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED + /// demands nothing and changes nothing. The order does not matter. + /// + /// What the data arriving demands. + /// The tightened data security; never looser than either of the two. + public DataSourceSecurity TightenedBy(DataSourceSecurity arriving) => (held, arriving) switch + { + (DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED, + (_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY, + _ => held, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs index ca5f77bd4..fd6c2f928 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs @@ -9,18 +9,21 @@ public static class DataSourceTypeExtension { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceTypeExtension).Namespace, nameof(DataSourceTypeExtension)); - /// - /// Get the display name of the data source type. - /// /// The data source type. - /// The display name of the data source type. - public static string GetDisplayName(this DataSourceType type) => type switch + extension(DataSourceType type) { - DataSourceType.LOCAL_FILE => TB("Local File"), - DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"), - DataSourceType.ERI_V1 => TB("External ERI Server (v1)"), - DataSourceType.MAILBOX => TB("Mailbox"), + /// + /// Get the display name of the data source type. + /// + /// The display name of the data source type. + public string GetDisplayName() => type switch + { + DataSourceType.LOCAL_FILE => TB("Local File"), + DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"), + DataSourceType.ERI_V1 => TB("External ERI Server (v1)"), + DataSourceType.MAILBOX => TB("Mailbox"), - _ => TB("None"), - }; + _ => TB("None"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/LangBehaviorExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/LangBehaviorExtensions.cs index a480ef360..920aa514e 100644 --- a/app/MindWork AI Studio/Settings/DataModel/LangBehaviorExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/LangBehaviorExtensions.cs @@ -6,11 +6,14 @@ public static class LangBehaviorExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LangBehaviorExtensions).Namespace, nameof(LangBehaviorExtensions)); - public static string Name(this LangBehavior langBehavior) => langBehavior switch + extension(LangBehavior langBehavior) { - LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."), - LangBehavior.MANUAL => TB("Choose the language manually."), + public string Name() => langBehavior switch + { + LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."), + LangBehavior.MANUAL => TB("Choose the language manually."), - _ => TB("Unknown option") - }; + _ => TB("Unknown option") + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/MailboxMaxAgeExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/MailboxMaxAgeExtensions.cs index 74f0942c4..3312997d9 100644 --- a/app/MindWork AI Studio/Settings/DataModel/MailboxMaxAgeExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/MailboxMaxAgeExtensions.cs @@ -6,31 +6,34 @@ public static class MailboxMaxAgeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxMaxAgeExtensions).Namespace, nameof(MailboxMaxAgeExtensions)); - public static string GetName(this MailboxMaxAge maxAge) => maxAge switch - { - MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"), - MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"), - MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"), - MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"), - MailboxMaxAge.ALL => TB("All mails"), - - _ => TB("Unknown period"), - }; - - /// - /// The first day of the period, as the sync asks the server for it. - /// /// The period. - /// The current time. - /// The first day, or null when the period has no end. - public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch + extension(MailboxMaxAge maxAge) { - MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6), - MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12), - MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24), - MailboxMaxAge.ALL => null, + public string GetName() => maxAge switch + { + MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"), + MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"), + MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"), + MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"), + MailboxMaxAge.ALL => TB("All mails"), - // The shortest period also stands in for a value this version does not know, as it does in the settings: - _ => now.AddMonths(-3), - }; + _ => TB("Unknown period"), + }; + + /// + /// The first day of the period, as the sync asks the server for it. + /// + /// The current time. + /// The first day, or null when the period has no end. + public DateTimeOffset? GetReceivedSince(DateTimeOffset now) => maxAge switch + { + MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6), + MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12), + MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24), + MailboxMaxAge.ALL => null, + + // The shortest period also stands in for a value this version does not know, as it does in the settings: + _ => now.AddMonths(-3), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/MailboxTransportSecurityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/MailboxTransportSecurityExtensions.cs index 2898a266f..9300ffbee 100644 --- a/app/MindWork AI Studio/Settings/DataModel/MailboxTransportSecurityExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/MailboxTransportSecurityExtensions.cs @@ -16,23 +16,26 @@ public static class MailboxTransportSecurityExtensions private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxTransportSecurityExtensions).Namespace, nameof(MailboxTransportSecurityExtensions)); - public static string GetName(this MailboxTransportSecurity transportSecurity) => transportSecurity switch + extension(MailboxTransportSecurity transportSecurity) { - MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"), - MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"), + public string GetName() => transportSecurity switch + { + MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"), + MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"), - _ => TB("Unknown encryption"), - }; + _ => TB("Unknown encryption"), + }; - /// - /// The port the encryption usually comes with. - /// - /// The port, or null when there is no usual one. - public static int? GetUsualPort(this MailboxTransportSecurity transportSecurity) => transportSecurity switch - { - MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT, - MailboxTransportSecurity.STARTTLS => STARTTLS_PORT, + /// + /// The port the encryption usually comes with. + /// + /// The port, or null when there is no usual one. + public int? GetUsualPort() => transportSecurity switch + { + MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT, + MailboxTransportSecurity.STARTTLS => STARTTLS_PORT, - _ => null, - }; + _ => null, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/OutboundDataRequirementExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/OutboundDataRequirementExtensions.cs index b60112036..f6365b9f1 100644 --- a/app/MindWork AI Studio/Settings/DataModel/OutboundDataRequirementExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/OutboundDataRequirementExtensions.cs @@ -6,31 +6,34 @@ public static class OutboundDataRequirementExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions)); - /// - /// Why a tool cannot run in a chat with this requirement, and how to get it back. - /// - /// - /// One text for the tool selection and for a call which was turned down, so the user reads the - /// same in both places, and the model can pass it on. It names the source which demands the - /// requirement, and the setting as the user finds it labeled. The level is the one the setting - /// had when the content was read: a change made later does not loosen the chat.

- /// The text speaks of content the chat holds, not of content it read: the content may come from - /// another chat or an assistant, which sent it here along with its restrictions. - ///
/// What the chat demands. - /// The configuration, to find the name of the mailbox or data source which demands it. - /// The text for the user. - public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch + extension(OutboundDataRequirement requirement) { - OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes), - OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), - OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()), - OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources), + /// + /// Why a tool cannot run in a chat with this requirement, and how to get it back. + /// + /// + /// One text for the tool selection and for a call which was turned down, so the user reads the + /// same in both places, and the model can pass it on. It names the source which demands the + /// requirement, and the setting as the user finds it labeled. The level is the one the setting + /// had when the content was read: a change made later does not loosen the chat.

+ /// The text speaks of content the chat holds, not of content it read: the content may come from + /// another chat or an assistant, which sent it here along with its restrictions. + ///
+ /// The configuration, to find the name of the mailbox or data source which demands it. + /// The text for the user. + public string GetToolBlockedMessage(Data settings) => requirement.Origin switch + { + OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes), + OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), + OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()), + OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources), - // NONE keeps no tool from running, since nothing restricts such a chat. What is left is an - // origin this version does not know: - _ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), - }; + // NONE keeps no tool from running, since nothing restricts such a chat. What is left is an + // origin this version does not know: + _ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), + }; + } private static string GetMailboxBlockedMessage(OutboundDataRequirement requirement, IEnumerable mailboxes) { diff --git a/app/MindWork AI Studio/Settings/DataModel/OutboundDataRestrictionExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/OutboundDataRestrictionExtensions.cs index 42b6110c8..12c777422 100644 --- a/app/MindWork AI Studio/Settings/DataModel/OutboundDataRestrictionExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/OutboundDataRestrictionExtensions.cs @@ -6,52 +6,54 @@ public static class OutboundDataRestrictionExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions)); - /// - /// The stricter of two restrictions. - /// - /// - /// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown. - /// - /// The one restriction. - /// The other restriction. - /// The restriction which allows less. - public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other) + /// The restriction, as it was read. + extension(OutboundDataRestriction restriction) { - var knownRestriction = restriction.OrStrictestWhenUnknown(); - var knownOther = other.OrStrictestWhenUnknown(); - return knownOther < knownRestriction ? knownOther : knownRestriction; + /// + /// The stricter of two restrictions. + /// + /// + /// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown. + /// + /// The other restriction. + /// The restriction which allows less. + public OutboundDataRestriction StricterOf(OutboundDataRestriction other) + { + var knownRestriction = restriction.OrStrictestWhenUnknown(); + var knownOther = other.OrStrictestWhenUnknown(); + return knownOther < knownRestriction ? knownOther : knownRestriction; + } + + /// + /// The restriction as this version applies it. + /// + /// + /// The settings file and a saved chat may hold a number which names no member, e.g., one written + /// by hand, and the settings read it all the same. Compared as it is, such a number would rank + /// above UNRESTRICTED and lift every restriction it meets. + /// + /// The restriction itself, or the strictest one when this version does not know it. + public OutboundDataRestriction OrStrictestWhenUnknown() => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES; + + public string GetName() => restriction switch + { + OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"), + OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"), + OutboundDataRestriction.UNRESTRICTED => TB("No restriction"), + + _ => TB("Unknown restriction"), + }; + + /// + /// What a chat may still do on this level, once internal content entered it. + /// + public string GetDescription() => restriction switch + { + OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."), + OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."), + OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."), + + _ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."), + }; } - - /// - /// The restriction as this version applies it. - /// - /// - /// The settings file and a saved chat may hold a number which names no member, e.g., one written - /// by hand, and the settings read it all the same. Compared as it is, such a number would rank - /// above UNRESTRICTED and lift every restriction it meets. - /// - /// The restriction as it was read. - /// The restriction itself, or the strictest one when this version does not know it. - public static OutboundDataRestriction OrStrictestWhenUnknown(this OutboundDataRestriction restriction) => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES; - - public static string GetName(this OutboundDataRestriction restriction) => restriction switch - { - OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"), - OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"), - OutboundDataRestriction.UNRESTRICTED => TB("No restriction"), - - _ => TB("Unknown restriction"), - }; - - /// - /// What a chat may still do on this level, once internal content entered it. - /// - public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch - { - OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."), - OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."), - OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."), - - _ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."), - }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs index d8a5e7891..3e7fd1536 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs @@ -6,46 +6,49 @@ public static class PreviewFeaturesExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PreviewFeaturesExtensions).Namespace, nameof(PreviewFeaturesExtensions)); - public static string GetPreviewDescription(this PreviewFeatures feature) => feature switch - { - PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"), - PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"), - - PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"), - PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"), - PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"), - PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"), - PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"), - PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"), - PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"), - - _ => TB("Unknown preview feature") - }; - - /// - /// Check if the preview feature is released or not. - /// - /// - /// This metadata is necessary because we cannot remove the enum values from the list. - /// Otherwise, we could not deserialize old settings files that may contain these values. - /// /// The preview feature to check. - /// True when the preview feature is released, false otherwise. - public static bool IsReleased(this PreviewFeatures feature) => feature switch + extension(PreviewFeatures feature) { - PreviewFeatures.PRE_READ_PDF_2025 => true, - PreviewFeatures.PRE_PLUGINS_2025 => true, - PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, - PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true, + public string GetPreviewDescription() => feature switch + { + PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"), + PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"), - _ => false - }; - - public static bool IsEnabled(this PreviewFeatures feature, SettingsManager settingsManager) - { - if(feature.IsReleased()) - return true; + PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"), + PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"), + PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"), + PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"), + PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"), + PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"), + PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"), + + _ => TB("Unknown preview feature") + }; + + /// + /// Check if the preview feature is released or not. + /// + /// + /// This metadata is necessary because we cannot remove the enum values from the list. + /// Otherwise, we could not deserialize old settings files that may contain these values. + /// + /// True when the preview feature is released, false otherwise. + public bool IsReleased() => feature switch + { + PreviewFeatures.PRE_READ_PDF_2025 => true, + PreviewFeatures.PRE_PLUGINS_2025 => true, + PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, + PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true, - return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature); + _ => false + }; + + public bool IsEnabled(SettingsManager settingsManager) + { + if(feature.IsReleased()) + return true; + + return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs index e43d572c1..cfa3b3b83 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs @@ -2,48 +2,51 @@ namespace AIStudio.Settings.DataModel; public static class PreviewVisibilityExtensions { - public static IList GetPreviewFeatures(this PreviewVisibility visibility) + extension(PreviewVisibility visibility) { - var features = new List(); - if (visibility >= PreviewVisibility.RELEASE_CANDIDATE) + public IList GetPreviewFeatures() { - } + var features = new List(); + if (visibility >= PreviewVisibility.RELEASE_CANDIDATE) + { + } - if (visibility >= PreviewVisibility.BETA) - { - features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025); - features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1); - features.Add(PreviewFeatures.PRE_RAG_2024); - features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026); - } + if (visibility >= PreviewVisibility.BETA) + { + features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025); + features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1); + features.Add(PreviewFeatures.PRE_RAG_2024); + features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026); + } - if (visibility >= PreviewVisibility.ALPHA) - { - } + if (visibility >= PreviewVisibility.ALPHA) + { + } - if (visibility >= PreviewVisibility.PROTOTYPE) - { - features.Add(PreviewFeatures.PRE_MAILBOXES_2026); + if (visibility >= PreviewVisibility.PROTOTYPE) + { + features.Add(PreviewFeatures.PRE_MAILBOXES_2026); + } + + if (visibility >= PreviewVisibility.EXPERIMENTAL) + { + features.Add(PreviewFeatures.PRE_WRITER_MODE_2024); + } + + return features; } - if (visibility >= PreviewVisibility.EXPERIMENTAL) + public HashSet FilterPreviewFeatures(HashSet enabledFeatures) { - features.Add(PreviewFeatures.PRE_WRITER_MODE_2024); - } + var filteredFeatures = new HashSet(); + var previewFeatures = visibility.GetPreviewFeatures(); + foreach (var feature in enabledFeatures) + { + if (previewFeatures.Contains(feature)) + filteredFeatures.Add(feature); + } - return features; - } - - public static HashSet FilterPreviewFeatures(this PreviewVisibility visibility, HashSet enabledFeatures) - { - var filteredFeatures = new HashSet(); - var previewFeatures = visibility.GetPreviewFeatures(); - foreach (var feature in enabledFeatures) - { - if (previewFeatures.Contains(feature)) - filteredFeatures.Add(feature); + return filteredFeatures; } - - return filteredFeatures; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/ProviderV4Extensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/ProviderV4Extensions.cs index 4699f7bb0..a51736802 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/ProviderV4Extensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/ProviderV4Extensions.cs @@ -2,20 +2,26 @@ namespace AIStudio.Settings.DataModel.PreviousModels; public static class ProviderV4Extensions { - public static List MigrateFromV4ToV5(this IEnumerable providers) + extension(IEnumerable providers) { - return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList(); + public List MigrateFromV4ToV5() + { + return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList(); + } } - public static AIStudio.Settings.Provider MigrateFromV4ToV5(this Provider provider) => new() + extension(Provider provider) { - Num = provider.Num, - Id = provider.Id, - InstanceName = provider.InstanceName, - UsedLLMProvider = provider.UsedProvider, - Model = provider.Model, - IsSelfHosted = provider.IsSelfHosted, - Hostname = provider.Hostname, - Host = provider.Host, - }; + public AIStudio.Settings.Provider MigrateFromV4ToV5() => new() + { + Num = provider.Num, + Id = provider.Id, + InstanceName = provider.InstanceName, + UsedLLMProvider = provider.UsedProvider, + Model = provider.Model, + IsSelfHosted = provider.IsSelfHosted, + Hostname = provider.Hostname, + Host = provider.Host, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs index 9c8e4ebcd..a04c95d1d 100644 --- a/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs @@ -6,11 +6,14 @@ public static class SendToChatDataSourceBehaviorExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SendToChatDataSourceBehaviorExtensions).Namespace, nameof(SendToChatDataSourceBehaviorExtensions)); - public static string Description(this SendToChatDataSourceBehavior behavior) => behavior switch + extension(SendToChatDataSourceBehavior behavior) { - SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"), - SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"), + public string Description() => behavior switch + { + SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"), + SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"), - _ => TB("Unknown behavior"), - }; + _ => TB("Unknown behavior"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs index d37ca52b3..1edce9840 100644 --- a/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs @@ -2,16 +2,19 @@ namespace AIStudio.Settings.DataModel; public static class StartPageExtensions { - public static string ToRoute(this StartPage startPage) => startPage switch + extension(StartPage startPage) { - StartPage.HOME => string.Empty, - StartPage.CHAT => Routes.CHAT, - StartPage.ASSISTANTS => Routes.ASSISTANTS, - StartPage.INFORMATION => Routes.ABOUT, - StartPage.PLUGINS => Routes.PLUGINS, - StartPage.SUPPORTERS => Routes.SUPPORTERS, - StartPage.SETTINGS => Routes.SETTINGS, + public string ToRoute() => startPage switch + { + StartPage.HOME => string.Empty, + StartPage.CHAT => Routes.CHAT, + StartPage.ASSISTANTS => Routes.ASSISTANTS, + StartPage.INFORMATION => Routes.ABOUT, + StartPage.PLUGINS => Routes.PLUGINS, + StartPage.SUPPORTERS => Routes.SUPPORTERS, + StartPage.SETTINGS => Routes.SETTINGS, - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs index 128e8d4d9..b3c038bec 100644 --- a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs @@ -6,12 +6,15 @@ public static class ThemesExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ThemesExtensions).Namespace, nameof(ThemesExtensions)); - public static string GetName(this Themes theme) => theme switch + extension(Themes theme) { - Themes.SYSTEM => TB("Synchronized with the operating system settings"), - Themes.LIGHT => TB("Always use light theme"), - Themes.DARK => TB("Always use dark theme"), + public string GetName() => theme switch + { + Themes.SYSTEM => TB("Synchronized with the operating system settings"), + Themes.LIGHT => TB("Always use light theme"), + Themes.DARK => TB("Always use dark theme"), - _ => TB("Unknown setting"), - }; + _ => TB("Unknown setting"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/TranscriptionOpusBitrateExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/TranscriptionOpusBitrateExtensions.cs index a24aedc6a..6ca58727d 100644 --- a/app/MindWork AI Studio/Settings/DataModel/TranscriptionOpusBitrateExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/TranscriptionOpusBitrateExtensions.cs @@ -6,24 +6,27 @@ public static class TranscriptionOpusBitrateExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(TranscriptionOpusBitrateExtensions).Namespace, nameof(TranscriptionOpusBitrateExtensions)); - public static string GetName(this TranscriptionOpusBitrate bitrate) => bitrate switch + extension(TranscriptionOpusBitrate bitrate) { - TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"), - TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"), - TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"), - TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"), - _ => TB("Unknown"), - }; + public string GetName() => bitrate switch + { + TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"), + TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"), + TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"), + TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"), + _ => TB("Unknown"), + }; - public static uint GetBitsPerSecond(this TranscriptionOpusBitrate bitrate) => bitrate switch - { - TranscriptionOpusBitrate.KBPS_32 => 32_000, - TranscriptionOpusBitrate.KBPS_64 => 64_000, - TranscriptionOpusBitrate.KBPS_128 => 128_000, - TranscriptionOpusBitrate.KBPS_256 => 256_000, + public uint GetBitsPerSecond() => bitrate switch + { + TranscriptionOpusBitrate.KBPS_32 => 32_000, + TranscriptionOpusBitrate.KBPS_64 => 64_000, + TranscriptionOpusBitrate.KBPS_128 => 128_000, + TranscriptionOpusBitrate.KBPS_256 => 256_000, - // A value we do not know must never mean the lowest quality: that is how quiet passages - // went missing from transcripts in the first place. Fall back to the recommended bitrate. - _ => 128_000, - }; + // A value we do not know must never mean the lowest quality: that is how quiet passages + // went missing from transcripts in the first place. Fall back to the recommended bitrate. + _ => 128_000, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceOrderExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceOrderExtensions.cs index 4c28fa2b9..cde0a33ba 100644 --- a/app/MindWork AI Studio/Settings/DataSourceOrderExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataSourceOrderExtensions.cs @@ -2,20 +2,23 @@ namespace AIStudio.Settings; public static class DataSourceOrderExtensions { - /// - /// Sorts data sources in a fixed order: by their number, then by their ID. - /// - /// - /// The same data sources always come in the same order, however the settings list them, the - /// checks return them, or an agent selects them. Semantic Search offers them this way, because - /// the providers cache a request from its beginning, and the tools are part of that beginning, - /// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of - /// several equally strict data sources this way, so that both ways of searching name the same. - /// /// The data sources to sort. - /// The data sources in their fixed order. - public static IReadOnlyList InFixedOrder(this IEnumerable dataSources) => dataSources - .OrderBy(dataSource => dataSource.Num) - .ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal) - .ToList(); + extension(IEnumerable dataSources) + { + /// + /// Sorts data sources in a fixed order: by their number, then by their ID. + /// + /// + /// The same data sources always come in the same order, however the settings list them, the + /// checks return them, or an agent selects them. Semantic Search offers them this way, because + /// the providers cache a request from its beginning, and the tools are part of that beginning, + /// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of + /// several equally strict data sources this way, so that both ways of searching name the same. + /// + /// The data sources in their fixed order. + public IReadOnlyList InFixedOrder() => dataSources + .OrderBy(dataSource => dataSource.Num) + .ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal) + .ToList(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceOutboundDataExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceOutboundDataExtensions.cs index 1d83b4900..50af1da25 100644 --- a/app/MindWork AI Studio/Settings/DataSourceOutboundDataExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataSourceOutboundDataExtensions.cs @@ -30,26 +30,28 @@ public static class DataSourceOutboundDataExtensions ? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES) : organizationMinimum; - /// - /// The least strict level this data source may have. - /// /// The data source. - /// The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction. - /// The least strict level allowed. - public static OutboundDataRestriction GetMinimumOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => - GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum); + extension(IDataSource dataSource) + { + /// + /// The least strict level this data source may have. + /// + /// The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction. + /// The least strict level allowed. + public OutboundDataRestriction GetMinimumOutboundDataRestriction(OutboundDataRestriction organizationMinimum) => + GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum); - /// - /// Where a chat may still send data, once passages of this data source reached it. - /// - /// - /// The level of the data source, tightened to the least strict one it may have. This holds at - /// runtime, not only in the dialog: a data source stored before, or one a configuration plugin - /// provides, is never edited, so its stored level may be less strict. - /// - /// The data source. - /// The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction. - /// The level which applies. - public static OutboundDataRestriction GetEffectiveOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => - dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum)); + /// + /// Where a chat may still send data, once passages of this data source reached it. + /// + /// + /// The level of the data source, tightened to the least strict one it may have. This holds at + /// runtime, not only in the dialog: a data source stored before, or one a configuration plugin + /// provides, is never edited, so its stored level may be less strict. + /// + /// The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction. + /// The level which applies. + public OutboundDataRestriction GetEffectiveOutboundDataRestriction(OutboundDataRestriction organizationMinimum) => + dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum)); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs index 3f7a19e63..b4032dfbe 100644 --- a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs @@ -5,160 +5,184 @@ namespace AIStudio.Settings; public static class DataSourceSecurityTrustExtensions { - public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager) + extension(Provider provider) { - if (provider == Provider.NONE) - return false; - - return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); - } - - public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager) - { - if (provider == EmbeddingProvider.NONE) - return false; - - return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); - } - - public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager settingsManager) - { - if (provider == TranscriptionProvider.NONE) - return false; - - return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); - } - - public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager settingsManager) - { - if (provider is NoProvider) - return false; - - return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager); - } - - public static ConfidenceLevel GetConfidenceLevel(this Provider provider, SettingsManager settingsManager) - { - if (provider == Provider.NONE) - return ConfidenceLevel.NONE; - - return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; - } - - public static ConfidenceLevel GetConfidenceLevel(this EmbeddingProvider provider, SettingsManager settingsManager) - { - if (provider == EmbeddingProvider.NONE) - return ConfidenceLevel.NONE; - - return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; - } - - public static ConfidenceLevel GetConfidenceLevel(this TranscriptionProvider provider, SettingsManager settingsManager) - { - if (provider == TranscriptionProvider.NONE) - return ConfidenceLevel.NONE; - - return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; - } - - public static ConfidenceLevel GetConfidenceLevel(this IProvider provider, SettingsManager settingsManager) - { - if (provider is NoProvider) - return ConfidenceLevel.NONE; - - return provider.Provider.GetConfidence(settingsManager).Level; - } - - public static bool AllowsDataSourceAccess(this Provider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) - { - return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager) - && provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel); - } - - public static bool AllowsDataSourceAccess(this IProvider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) - { - return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager) - && provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel); - } - - public static bool AllowsDataSourceSecurity(this Provider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager) - => provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity); - - public static bool AllowsDataSourceSecurity(this IProvider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager) - => provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity); - - public static bool AllowsDataSourceSecurity(this bool usingTrustedProvider, DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch - { - DataSourceSecurity.ALLOW_ANY => true, - DataSourceSecurity.SELF_HOSTED => usingTrustedProvider, - _ => false, - }; - - public static bool AllowsDataSourceConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel requiredConfidenceLevel) - { - if (requiredConfidenceLevel is ConfidenceLevel.NONE) - return true; - - return providerConfidenceLevel >= requiredConfidenceLevel; - } - - /// - /// Whether a mailbox may require this provider confidence level. - /// - /// - /// Only the levels from very low to high qualify. NONE lets every provider through, cf. - /// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or - /// below the level of a provider nobody rated, so even such a provider would meet them. - /// - public static bool IsAllowedMailboxConfidence(this ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH; - - /// - /// Whether a provider of this confidence level may see the content of a mailbox. - /// - /// - /// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every - /// provider, whether it was never chosen or edited by hand in the settings file. Every provider - /// which sees mail content has to pass it: the chat provider, the embedding provider, and the - /// provider of every agent reading mails. - /// - public static bool AllowsMailboxConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel mailboxConfidenceLevel) - { - if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence()) - return false; - - return providerConfidenceLevel >= mailboxConfidenceLevel; - } - - public static ConfidenceLevel GetRequiredConfidenceLevel(this IEnumerable dataSources) - { - var requiredConfidenceLevel = ConfidenceLevel.NONE; - foreach (var dataSource in dataSources.OfType()) - if (dataSource.ConfidenceLevel > requiredConfidenceLevel) - requiredConfidenceLevel = dataSource.ConfidenceLevel; - - return requiredConfidenceLevel; - } - - public static DataSourceSecurity GetRequiredSecurityPolicy(this IEnumerable dataSources) - { - var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY; - foreach (var dataSource in dataSources.OfType()) + public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager) { - if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED) - return DataSourceSecurity.NOT_SPECIFIED; + if (provider == Provider.NONE) + return false; - if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED) - requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED; + return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); } - return requiredSecurityPolicy; + public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager) + { + if (provider == Provider.NONE) + return ConfidenceLevel.NONE; + + return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; + } + + public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) + { + return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager) + && provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel); + } + + public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager) + => provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity); + + public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); } - public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + extension(EmbeddingProvider provider) + { + public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager) + { + if (provider == EmbeddingProvider.NONE) + return false; - public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); + } - public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager) + { + if (provider == EmbeddingProvider.NONE) + return ConfidenceLevel.NONE; + + return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; + } + + public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + } + + extension(TranscriptionProvider provider) + { + public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager) + { + if (provider == TranscriptionProvider.NONE) + return false; + + return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); + } + + public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager) + { + if (provider == TranscriptionProvider.NONE) + return ConfidenceLevel.NONE; + + return provider.UsedLLMProvider.GetConfidence(settingsManager).Level; + } + + public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + } + + extension(IProvider provider) + { + public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager) + { + if (provider is NoProvider) + return false; + + return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager); + } + + public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager) + { + if (provider is NoProvider) + return ConfidenceLevel.NONE; + + return provider.Provider.GetConfidence(settingsManager).Level; + } + + public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) + { + return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager) + && provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel); + } + + public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager) + => provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity); + } + + extension(bool usingTrustedProvider) + { + public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch + { + DataSourceSecurity.ALLOW_ANY => true, + DataSourceSecurity.SELF_HOSTED => usingTrustedProvider, + _ => false, + }; + } + + extension(ConfidenceLevel providerConfidenceLevel) + { + public bool AllowsDataSourceConfidenceLevel(ConfidenceLevel requiredConfidenceLevel) + { + if (requiredConfidenceLevel is ConfidenceLevel.NONE) + return true; + + return providerConfidenceLevel >= requiredConfidenceLevel; + } + + /// + /// Whether a provider of this confidence level may see the content of a mailbox. + /// + /// + /// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every + /// provider, whether it was never chosen or edited by hand in the settings file. Every provider + /// which sees mail content has to pass it: the chat provider, the embedding provider, and the + /// provider of every agent reading mails. + /// + public bool AllowsMailboxConfidenceLevel(ConfidenceLevel mailboxConfidenceLevel) + { + if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence()) + return false; + + return providerConfidenceLevel >= mailboxConfidenceLevel; + } + } + + extension(ConfidenceLevel confidenceLevel) + { + /// + /// Whether a mailbox may require this provider confidence level. + /// + /// + /// Only the levels from very low to high qualify. NONE lets every provider through, cf. + /// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or + /// below the level of a provider nobody rated, so even such a provider would meet them. + /// + public bool IsAllowedMailboxConfidence() => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH; + } + + extension(IEnumerable dataSources) + { + public ConfidenceLevel GetRequiredConfidenceLevel() + { + var requiredConfidenceLevel = ConfidenceLevel.NONE; + foreach (var dataSource in dataSources.OfType()) + if (dataSource.ConfidenceLevel > requiredConfidenceLevel) + requiredConfidenceLevel = dataSource.ConfidenceLevel; + + return requiredConfidenceLevel; + } + + public DataSourceSecurity GetRequiredSecurityPolicy() + { + var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY; + foreach (var dataSource in dataSources.OfType()) + { + if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED) + return DataSourceSecurity.NOT_SPECIFIED; + + if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED) + requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED; + } + + return requiredSecurityPolicy; + } + } private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager) { diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs index 475e6bd53..698b3e9d5 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs @@ -6,38 +6,41 @@ namespace AIStudio.Settings; public static partial class ProviderExtensions { - /// - /// Get the effective reasoning indicator state for the configured provider instance. - /// - /// - /// Two answers meet here, and they answer different questions. What a model is able to do comes - /// from the rules; what this person asked for comes from the parameters they wrote into their - /// own provider. A model which thinks unless told otherwise stops showing the indicator when a - /// parameter turns it off, and a model which can be asked to think shows it only once one does. - /// /// The configured provider. - /// The effective reasoning indicator state. - public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider) + extension(Provider provider) { - var reasoning = provider.GetModelProfile().Reasoning; - if (reasoning is ReasoningSupport.ALWAYS) - return ReasoningIndicatorState.ALWAYS_ON; - - var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters); - if (reasoning is ReasoningSupport.ON_BY_DEFAULT) + /// + /// Get the effective reasoning indicator state for the configured provider instance. + /// + /// + /// Two answers meet here, and they answer different questions. What a model is able to do comes + /// from the rules; what this person asked for comes from the parameters they wrote into their + /// own provider. A model which thinks unless told otherwise stops showing the indicator when a + /// parameter turns it off, and a model which can be asked to think shows it only once one does. + /// + /// The effective reasoning indicator state. + public ReasoningIndicatorState GetReasoningIndicatorState() { - return configured switch + var reasoning = provider.GetModelProfile().Reasoning; + if (reasoning is ReasoningSupport.ALWAYS) + return ReasoningIndicatorState.ALWAYS_ON; + + var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters); + if (reasoning is ReasoningSupport.ON_BY_DEFAULT) { - ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE, - ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED, + return configured switch + { + ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE, + ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED, - _ => ReasoningIndicatorState.DEFAULT_ON, - }; + _ => ReasoningIndicatorState.DEFAULT_ON, + }; + } + + if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED) + return ReasoningIndicatorState.CONFIGURED; + + return ReasoningIndicatorState.NONE; } - - if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED) - return ReasoningIndicatorState.CONFIGURED; - - return ReasoningIndicatorState.NONE; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index 23d850618..335eff3bc 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -7,132 +7,136 @@ namespace AIStudio.Settings; public static partial class ProviderExtensions { - /// - /// Everything the app knows about the model this provider instance is configured with. - /// - /// - /// The one door to that question. Behind it stand the links of the chain, in the order they - /// win: what the person said about their own installation, then what the installation itself - /// reported, then what the rules worked out from the name, and last what the app assumes when - /// nothing else said anything. - /// /// The configured provider. - /// The profile of the configured model. - public static ModelProfile GetModelProfile(this Provider provider) + extension(Provider provider) { - var automatic = provider.GetAutomaticModelProfile(); - return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic; + /// + /// Everything the app knows about the model this provider instance is configured with. + /// + /// + /// The one door to that question. Behind it stand the links of the chain, in the order they + /// win: what the person said about their own installation, then what the installation itself + /// reported, then what the rules worked out from the name, and last what the app assumes when + /// nothing else said anything. + /// + /// The profile of the configured model. + public ModelProfile GetModelProfile() + { + var automatic = provider.GetAutomaticModelProfile(); + return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic; + } + + /// + /// Everything known about the configured model except what the person themselves switched. + /// + /// + /// This is what happens when somebody fills in nothing, which is why the expert dialog shows it + /// as the automatic answer. It has to include what the provider reported: a person who leaves + /// the window empty gets the number their own engine stated, and a placeholder showing them a + /// different one would be a promise the app does not keep. + /// + /// The profile of the configured model, without that provider's overrides. + public ModelProfile GetAutomaticModelProfile() + { + var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model); + return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated); + } + + /// + /// Get whether the model used by the configured provider accepts images as input. + /// + /// + /// Two capabilities express image input, one for a single image and one for several. Anything that + /// wants to know whether an image may be sent has to accept both, which is why the question is asked + /// here instead of at each call site: attaching a file and validating an already attached file must + /// never disagree about it. + /// + /// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator + /// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing. + /// + /// true when the model accepts image input. + public bool SupportsImageInput() + { + var profile = provider.GetModelProfile(); + return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0; + } } - /// - /// Everything known about the configured model except what the person themselves switched. - /// - /// - /// This is what happens when somebody fills in nothing, which is why the expert dialog shows it - /// as the automatic answer. It has to include what the provider reported: a person who leaves - /// the window empty gets the number their own engine stated, and a placeholder showing them a - /// different one would be a promise the app does not keep. - /// - /// The configured provider. - /// The profile of the configured model, without that provider's overrides. - public static ModelProfile GetAutomaticModelProfile(this Provider provider) - { - var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model); - return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated); - } - - /// - /// Everything the rules know about a model at a provider, without anybody's own installation. - /// - /// - /// The answer to the model as such, which is the same for everybody who uses that name at that - /// provider -- and therefore the answer the registry caches. What one particular installation - /// says about it is asked one link further up, where the instance is known. - /// - /// The assumed profile fills in where no rule stated a single capability. It fills in the - /// capabilities only: a modifier may well have said what the model is made for without any rule - /// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding - /// model rather than turning into a chat model with an assumption attached. - /// /// The LLM provider the model is reached through. - /// The model, named the way that provider names it. - /// The profile, which knows nothing when there is nothing to reach. - public static ModelProfile GetModelProfile(this LLMProviders provider, Model model) + extension(LLMProviders provider) { - // - // Without a provider there is nothing to reach the model through, and an empty name is what - // a provider reports before anybody picked one. Neither is a model we could assume anything - // about, so neither gets the assumption. - // - if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id)) - return ModelProfile.UNKNOWN; + /// + /// Everything the rules know about a model at a provider, without anybody's own installation. + /// + /// + /// The answer to the model as such, which is the same for everybody who uses that name at that + /// provider -- and therefore the answer the registry caches. What one particular installation + /// says about it is asked one link further up, where the instance is known. + /// + /// The assumed profile fills in where no rule stated a single capability. It fills in the + /// capabilities only: a modifier may well have said what the model is made for without any rule + /// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding + /// model rather than turning into a chat model with an assumption attached. + /// + /// The model, named the way that provider names it. + /// The profile, which knows nothing when there is nothing to reach. + public ModelProfile GetModelProfile(Model model) + { + // + // Without a provider there is nothing to reach the model through, and an empty name is what + // a provider reports before anybody picked one. Neither is a model we could assume anything + // about, so neither gets the assumption. + // + if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id)) + return ModelProfile.UNKNOWN; - var stated = ModelRegistry.Shared.Profile(provider, model.Id); - return stated.Capabilities is Capability.NONE - ? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities } - : stated; + var stated = ModelRegistry.Shared.Profile(provider, model.Id); + return stated.Capabilities is Capability.NONE + ? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities } + : stated; + } } - /// - /// Get whether the model used by the configured provider accepts images as input. - /// - /// - /// Two capabilities express image input, one for a single image and one for several. Anything that - /// wants to know whether an image may be sent has to accept both, which is why the question is asked - /// here instead of at each call site: attaching a file and validating an already attached file must - /// never disagree about it. - /// - /// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator - /// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing. - /// - /// The configured provider. - /// true when the model accepts image input. - public static bool SupportsImageInput(this Provider provider) + /// The model to check. + extension(Model model) { - var profile = provider.GetModelProfile(); - return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0; + /// + /// Checks whether this model can be used for chatting. + /// + /// + /// What a model can do and what it is made for used to be two questions answered by two pieces + /// of code, each walking the same name with rules of its own. They disagreed: a model like + /// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both + /// come out of the same rules now, which is why this takes the provider -- the same name means + /// different things depending on who serves it, and only the provider knows how to unwrap it. + /// + /// The direction of the answer is deliberate. Everything not recognized as something else is a + /// chat model, so a provider adding a family we have never seen keeps it visible to the person + /// paying for it. Getting it wrong the other way would hide a model. + /// + /// The provider serving it. + /// True, when the model is a chat model or when we recognize no other kind. + public bool IsChatModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT; + + /// + /// Checks whether this model creates embeddings. + /// + /// The provider serving it. + /// True, when the model is an embedding model. + public bool IsEmbeddingModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING; + + /// + /// Checks whether this model transcribes audio. + /// + /// The provider serving it. + /// True, when the model is a transcription model. + public bool IsTranscriptionModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION; + + /// + /// Checks whether this model generates images. + /// + /// The provider serving it. + /// True, when the model is an image generation model. + public bool IsImageModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION; } - - /// - /// Checks whether this model can be used for chatting. - /// - /// - /// What a model can do and what it is made for used to be two questions answered by two pieces - /// of code, each walking the same name with rules of its own. They disagreed: a model like - /// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both - /// come out of the same rules now, which is why this takes the provider -- the same name means - /// different things depending on who serves it, and only the provider knows how to unwrap it. - /// - /// The direction of the answer is deliberate. Everything not recognized as something else is a - /// chat model, so a provider adding a family we have never seen keeps it visible to the person - /// paying for it. Getting it wrong the other way would hide a model. - /// - /// The model to check. - /// The provider serving it. - /// True, when the model is a chat model or when we recognize no other kind. - public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT; - - /// - /// Checks whether this model creates embeddings. - /// - /// The model to check. - /// The provider serving it. - /// True, when the model is an embedding model. - public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING; - - /// - /// Checks whether this model transcribes audio. - /// - /// The model to check. - /// The provider serving it. - /// True, when the model is a transcription model. - public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION; - - /// - /// Checks whether this model generates images. - /// - /// The model to check. - /// The provider serving it. - /// True, when the model is an image generation model. - public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs b/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs index 6ba303932..92e6b4a27 100644 --- a/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs @@ -6,26 +6,29 @@ public static class AssistantCategoryExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantCategoryExtensions).Namespace, nameof(AssistantCategoryExtensions)); - public static string Name(this AssistantCategory category) => category switch + extension(AssistantCategory category) { - AssistantCategory.AS_IS => TB("Select the assistant category"), - AssistantCategory.GENERAL => TB("General"), - AssistantCategory.SCIENTIFIC => TB("Scientific"), - AssistantCategory.BUSINESS => TB("Business"), - AssistantCategory.PRODUCTIVITY => TB("Productivity"), - AssistantCategory.DEVELOPMENT => TB("Software Development"), - AssistantCategory.LEARNING => TB("Learning"), - AssistantCategory.AI_STUDIO => TB("AI Studio Development"), - AssistantCategory.OTHER => TB("Other"), + public string Name() => category switch + { + AssistantCategory.AS_IS => TB("Select the assistant category"), + AssistantCategory.GENERAL => TB("General"), + AssistantCategory.SCIENTIFIC => TB("Scientific"), + AssistantCategory.BUSINESS => TB("Business"), + AssistantCategory.PRODUCTIVITY => TB("Productivity"), + AssistantCategory.DEVELOPMENT => TB("Software Development"), + AssistantCategory.LEARNING => TB("Learning"), + AssistantCategory.AI_STUDIO => TB("AI Studio Development"), + AssistantCategory.OTHER => TB("Other"), - _ => string.Empty, - }; - - public static string NameSelecting(this AssistantCategory category) - { - if(category is AssistantCategory.AS_IS) - return TB("Please select the assistant category"); + _ => string.Empty, + }; - return category.Name(); + public string NameSelecting() + { + if(category is AssistantCategory.AS_IS) + return TB("Please select the assistant category"); + + return category.Name(); + } } } diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs index 25f5d0a84..bcc5b77f6 100644 --- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs @@ -10,86 +10,89 @@ public static class AssistantVisibilityExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantVisibilityExtensions)); - /// - /// Checks if an assistant should be visible based on configuration and optional preview feature requirements. - /// /// The settings manager to check configuration against. - /// Whether to log visibility decisions. - /// The name of the assistant to check (for logging purposes). - /// The assistant component to check. - /// Optional preview feature that must be enabled for the assistant to be visible. - /// True if the assistant should be visible, false otherwise. - public static bool IsAssistantVisible(this SettingsManager settingsManager, Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE) + extension(SettingsManager settingsManager) { - withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName); + /// + /// Checks if an assistant should be visible based on configuration and optional preview feature requirements. + /// + /// Whether to log visibility decisions. + /// The name of the assistant to check (for logging purposes). + /// The assistant component to check. + /// Optional preview feature that must be enabled for the assistant to be visible. + /// True if the assistant should be visible, false otherwise. + public bool IsAssistantVisible(Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE) + { + withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName); - // Check if a preview feature is required and enabled: - if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager)) - { - if(withLogging) - LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature); + // Check if a preview feature is required and enabled: + if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager)) + { + if(withLogging) + LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature); - return false; - } + return false; + } - // If no component is specified, it's always visible: - if (component is Components.NONE) - { - if(withLogging) - LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName); + // If no component is specified, it's always visible: + if (component is Components.NONE) + { + if(withLogging) + LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName); - return true; - } + return true; + } - // Dynamic assistants are controlled through plugin activation and their security state. - // The Assistant Builder is controlled through its preview feature. Neither belongs to the - // built-in assistant visibility list, so both are expected to be visible at this point. - if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT) - return true; + // Dynamic assistants are controlled through plugin activation and their security state. + // The Assistant Builder is controlled through its preview feature. Neither belongs to the + // built-in assistant visibility list, so both are expected to be visible at this point. + if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT) + return true; - // Map Components enum to ConfigurableAssistant enum: - var configurableAssistant = component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT, - Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT, - Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT, - Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT, - Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT, - Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT, - Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT, - Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT, - Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT, - Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT, - Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT, - Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT, - Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT, - Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT, - Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT, - Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT, - Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT, - Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT, - Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT, - Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT, - Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT, - Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT, + // Map Components enum to ConfigurableAssistant enum: + var configurableAssistant = component switch + { + Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT, + Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT, + Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT, + Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT, + Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT, + Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT, + Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT, + Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT, + Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT, + Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT, + Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT, + Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT, + Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT, + Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT, + Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT, + Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT, + Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT, + Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT, + Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT, + Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT, + Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT, + Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT, - _ => ConfigurableAssistant.UNKNOWN, - }; + _ => ConfigurableAssistant.UNKNOWN, + }; - // If the component doesn't map to a configurable assistant, it's always visible: - if (configurableAssistant is ConfigurableAssistant.UNKNOWN) - { - if(withLogging) - LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component); + // If the component doesn't map to a configurable assistant, it's always visible: + if (configurableAssistant is ConfigurableAssistant.UNKNOWN) + { + if(withLogging) + LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component); - return true; + return true; + } + + // Check if the assistant is hidden by any configuration plugin: + var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant); + if (isHidden && withLogging) + LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName); + + return !isHidden; } - - // Check if the assistant is hidden by any configuration plugin: - var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant); - if (isHidden && withLogging) - LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName); - - return !isHidden; } } diff --git a/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs index af93103f0..1496be1d2 100644 --- a/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs +++ b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs @@ -7,13 +7,16 @@ public static class AuthMethodsV1Extensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AuthMethodsV1Extensions).Namespace, nameof(AuthMethodsV1Extensions)); - public static string DisplayName(this AuthMethod authMethod) => authMethod switch + extension(AuthMethod authMethod) { - AuthMethod.NONE => TB("None"), - AuthMethod.USERNAME_PASSWORD => TB("Username & Password"), - AuthMethod.KERBEROS => TB("SSO (Kerberos)"), - AuthMethod.TOKEN => TB("Access Token"), + public string DisplayName() => authMethod switch + { + AuthMethod.NONE => TB("None"), + AuthMethod.USERNAME_PASSWORD => TB("Username & Password"), + AuthMethod.KERBEROS => TB("SSO (Kerberos)"), + AuthMethod.TOKEN => TB("Access Token"), - _ => TB("Unknown authentication method"), - }; + _ => TB("Unknown authentication method"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs b/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs index 28b5f4e73..56b7f0cd8 100644 --- a/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs +++ b/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs @@ -4,10 +4,13 @@ namespace AIStudio.Tools; public static class ChatTemplateExtensions { - public static IEnumerable GetAllChatTemplates(this IEnumerable chatTemplates) + extension(IEnumerable chatTemplates) { - yield return ChatTemplate.NO_CHAT_TEMPLATE; - foreach (var chatTemplate in chatTemplates) - yield return chatTemplate; + public IEnumerable GetAllChatTemplates() + { + yield return ChatTemplate.NO_CHAT_TEMPLATE; + foreach (var chatTemplate in chatTemplates) + yield return chatTemplate; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/CodeHighlighting/CodeTokenKindExtensions.cs b/app/MindWork AI Studio/Tools/CodeHighlighting/CodeTokenKindExtensions.cs index 2c4f88c39..4a2a35196 100644 --- a/app/MindWork AI Studio/Tools/CodeHighlighting/CodeTokenKindExtensions.cs +++ b/app/MindWork AI Studio/Tools/CodeHighlighting/CodeTokenKindExtensions.cs @@ -2,26 +2,29 @@ namespace AIStudio.Tools.CodeHighlighting; public static class CodeTokenKindExtensions { - /// - /// The CSS class which MudBlazor.Markdown gives this kind of part, so that it takes its color from - /// the palette of the theme, just as the languages the library highlights itself. - /// /// What the part is. - /// The CSS class of the part. - public static string ToCssClass(this CodeTokenKind kind) => kind switch + extension(CodeTokenKind kind) { - CodeTokenKind.KEYWORD => "hljs-keyword", - CodeTokenKind.TYPE => "hljs-type", - CodeTokenKind.LITERAL => "hljs-literal", - CodeTokenKind.STRING => "hljs-string", - CodeTokenKind.COMMENT => "hljs-comment", - CodeTokenKind.NUMBER => "hljs-number", - CodeTokenKind.FUNCTION => "hljs-title function_", - CodeTokenKind.META => "hljs-meta", - CodeTokenKind.VARIABLE => "hljs-variable", - CodeTokenKind.ATTRIBUTE => "hljs-attr", - CodeTokenKind.SELECTOR => "hljs-selector-class", + /// + /// The CSS class which MudBlazor.Markdown gives this kind of part, so that it takes its color from + /// the palette of the theme, just as the languages the library highlights itself. + /// + /// The CSS class of the part. + public string ToCssClass() => kind switch + { + CodeTokenKind.KEYWORD => "hljs-keyword", + CodeTokenKind.TYPE => "hljs-type", + CodeTokenKind.LITERAL => "hljs-literal", + CodeTokenKind.STRING => "hljs-string", + CodeTokenKind.COMMENT => "hljs-comment", + CodeTokenKind.NUMBER => "hljs-number", + CodeTokenKind.FUNCTION => "hljs-title function_", + CodeTokenKind.META => "hljs-meta", + CodeTokenKind.VARIABLE => "hljs-variable", + CodeTokenKind.ATTRIBUTE => "hljs-attr", + CodeTokenKind.SELECTOR => "hljs-selector-class", - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs index 9246a27c9..4151eafc4 100644 --- a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs +++ b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs @@ -6,80 +6,83 @@ public static class CommonLanguageExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(CommonLanguageExtensions).Namespace, nameof(CommonLanguageExtensions)); - public static string Name(this CommonLanguages language) => language switch + extension(CommonLanguages language) { - CommonLanguages.AS_IS => TB("Do not change the language"), + public string Name() => language switch + { + CommonLanguages.AS_IS => TB("Do not change the language"), - CommonLanguages.EN_US => TB("English (US)"), - CommonLanguages.EN_GB => TB("English (UK)"), - CommonLanguages.ZH_CN => TB("Chinese (Simplified)"), - CommonLanguages.HI_IN => TB("Hindi (India)"), - CommonLanguages.ES_ES => TB("Spanish (Spain)"), - CommonLanguages.FR_FR => TB("French (France)"), - CommonLanguages.DE_DE => TB("German (Germany)"), - CommonLanguages.DE_AT => TB("German (Austria)"), - CommonLanguages.DE_CH => TB("German (Switzerland)"), - CommonLanguages.JA_JP => TB("Japanese (Japan)"), - CommonLanguages.RU_RU => TB("Russian (Russia)"), + CommonLanguages.EN_US => TB("English (US)"), + CommonLanguages.EN_GB => TB("English (UK)"), + CommonLanguages.ZH_CN => TB("Chinese (Simplified)"), + CommonLanguages.HI_IN => TB("Hindi (India)"), + CommonLanguages.ES_ES => TB("Spanish (Spain)"), + CommonLanguages.FR_FR => TB("French (France)"), + CommonLanguages.DE_DE => TB("German (Germany)"), + CommonLanguages.DE_AT => TB("German (Austria)"), + CommonLanguages.DE_CH => TB("German (Switzerland)"), + CommonLanguages.JA_JP => TB("Japanese (Japan)"), + CommonLanguages.RU_RU => TB("Russian (Russia)"), - _ => TB("Other"), - }; - - public static string ToIETFTag(this CommonLanguages language) => language switch - { - CommonLanguages.AS_IS => string.Empty, + _ => TB("Other"), + }; + + public string ToIETFTag() => language switch + { + CommonLanguages.AS_IS => string.Empty, - CommonLanguages.EN_US => "en-US", - CommonLanguages.EN_GB => "en-GB", - CommonLanguages.ZH_CN => "zh-CN", - CommonLanguages.HI_IN => "hi-IN", - CommonLanguages.ES_ES => "es-ES", - CommonLanguages.FR_FR => "fr-FR", - CommonLanguages.DE_DE => "de-DE", - CommonLanguages.DE_AT => "de-AT", - CommonLanguages.DE_CH => "de-CH", - CommonLanguages.JA_JP => "ja-JP", - CommonLanguages.RU_RU => "ru-RU", + CommonLanguages.EN_US => "en-US", + CommonLanguages.EN_GB => "en-GB", + CommonLanguages.ZH_CN => "zh-CN", + CommonLanguages.HI_IN => "hi-IN", + CommonLanguages.ES_ES => "es-ES", + CommonLanguages.FR_FR => "fr-FR", + CommonLanguages.DE_DE => "de-DE", + CommonLanguages.DE_AT => "de-AT", + CommonLanguages.DE_CH => "de-CH", + CommonLanguages.JA_JP => "ja-JP", + CommonLanguages.RU_RU => "ru-RU", - _ => string.Empty, - }; - - public static string PromptSummarizing(this CommonLanguages language, string customLanguage) => language switch - { - CommonLanguages.AS_IS => "Use the language the text is written in for the summary.", - CommonLanguages.OTHER => $"Output your summary in {customLanguage}.", + _ => string.Empty, + }; - _ => $"Output your summary in {language.Name()} ({language}).", - }; - - public static string PromptTranslation(this CommonLanguages language, string customLanguage) => language switch - { - CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.", + public string PromptSummarizing(string customLanguage) => language switch + { + CommonLanguages.AS_IS => "Use the language the text is written in for the summary.", + CommonLanguages.OTHER => $"Output your summary in {customLanguage}.", - _ => $"Translate the source text to {language.Name()} ({language}).", - }; - - public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch - { - CommonLanguages.AS_IS => "Use the language the user input is written in for the output.", - CommonLanguages.OTHER => $"use the language {customLanguage} for your output.", + _ => $"Output your summary in {language.Name()} ({language}).", + }; - _ => $"Use the language {language.Name()} ({language}) for your output.", - }; + public string PromptTranslation(string customLanguage) => language switch + { + CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.", - public static string NameSelecting(this CommonLanguages language) - { - if(language is CommonLanguages.AS_IS) - return TB("Please select the target language"); + _ => $"Translate the source text to {language.Name()} ({language}).", + }; + + public string PromptGeneralPurpose(string customLanguage) => language switch + { + CommonLanguages.AS_IS => "Use the language the user input is written in for the output.", + CommonLanguages.OTHER => $"use the language {customLanguage} for your output.", + + _ => $"Use the language {language.Name()} ({language}) for your output.", + }; + + public string NameSelecting() + { + if(language is CommonLanguages.AS_IS) + return TB("Please select the target language"); - return language.Name(); - } - - public static string NameSelectingOptional(this CommonLanguages language) - { - if(language is CommonLanguages.AS_IS) - return TB("Do not specify the language"); + return language.Name(); + } + + public string NameSelectingOptional() + { + if(language is CommonLanguages.AS_IS) + return TB("Do not specify the language"); - return language.Name(); + return language.Name(); + } } } diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index b2f43de64..0ab13fe95 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -10,236 +10,237 @@ public static class ComponentsExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions)); - /// - /// Gets the preview feature a component belongs to. Components that are generally available - /// return . This is the single place that maps a component to - /// its preview feature, so visibility checks never need to special-case one assistant. - /// /// The component to look up. - /// The required preview feature. - public static PreviewFeatures RequiredPreviewFeature(this Components component) => component switch + extension(Components component) { - Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026, - - _ => PreviewFeatures.NONE, - }; - - /// - /// Gets whether a component owns exactly one assistant session slot, so that a running session - /// blocks starting another one and inactive sessions can be cleared as a group. - /// - /// - /// Components return false for three different reasons. The chat has no assistant sessions - /// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys - /// its sessions per briefing, so it owns one slot per stored briefing rather than one per - /// component. All must be excluded from the single-slot checks, which is why this is a - /// capability and not a component comparison. - /// - /// The component to look up. - /// true when the component owns exactly one session slot. - public static bool HasSingleSessionSlot(this Components component) => component switch - { - Components.CHAT or Components.DYNAMIC_ASSISTANT => false, - Components.VISUAL_BRIEFING_ASSISTANT => false, - - _ => true, - }; - - /// - /// Gets the kind of media-import owner a component creates for its attachments. - /// - /// The component to look up. - /// The media-import owner kind. - public static MediaImportOwnerKind MediaOwnerKind(this Components component) => component switch - { - Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING, - - _ => MediaImportOwnerKind.ASSISTANT, - }; - - public static bool AllowSendTo(this Components component) => component switch - { - Components.NONE => false, - Components.DYNAMIC_ASSISTANT => false, - - Components.ERI_ASSISTANT => false, - Components.BIAS_DAY_ASSISTANT => false, - Components.I18N_ASSISTANT => false, - Components.DOCUMENT_ANALYSIS_ASSISTANT => false, - Components.BATCH_PROCESSING_ASSISTANT => false, - Components.LOG_VIEWER_ASSISTANT => false, - - Components.APP_SETTINGS => false, - Components.WRITER => false, - - Components.AGENT_TEXT_CONTENT_CLEANER => false, - Components.AGENT_DATA_SOURCE_SELECTION => false, - Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false, - Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false, - Components.META_ASSISTANT => false, - - _ => true, - }; - - public static string Name(this Components component) => component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"), - Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"), - Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"), - Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"), - Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"), - Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"), - Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"), - Components.AGENDA_ASSISTANT => TB("Agenda Assistant"), - Components.CODING_ASSISTANT => TB("Coding Assistant"), - Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"), - Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"), - Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"), - Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"), - Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"), - Components.ERI_ASSISTANT => TB("ERI Server"), - Components.I18N_ASSISTANT => TB("Localization Assistant"), - Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"), - Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"), - Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"), - Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"), - Components.META_ASSISTANT => TB("Assistant Builder"), - Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"), - - Components.CHAT => TB("New Chat"), - - _ => Enum.GetName(component)!, - }; - - public static ComponentsData GetData(this Components destination) => destination switch - { - Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA), - Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING), - Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE), - Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER), - Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL), - Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION), - Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER), - Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING), - Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER), - Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS), - Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK), - Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS), - Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS), - Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING), - Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS), - Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER), - Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING), - - Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT), - - _ => new(Event.NONE, Routes.ASSISTANTS), - }; - - public static ConfidenceLevel MinimumConfidence(this Components component, SettingsManager settingsManager) => component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default, - Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default, - Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default, - Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default, - Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default, - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default, - Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default, - Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default, - Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default, - Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, - Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default, - Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default, - Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence, - - // The minimum confidence for the Document Analysis Assistant is set per policy. - // We do this inside the Document Analysis Assistant component: - Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE, - - // A policy-specific minimum is merged with this component default inside - // the Batch Processing Assistant; the stricter level wins. - Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default, - - _ => default, - }; - - public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE, - Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE, - Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE, - Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE, - Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE, - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE, - Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE, - Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE, - Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE, - Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE, - Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE, - Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE, - Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE, - Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), - - // The Document Analysis Assistant does not have a preselected provider at the component level. - // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. - Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, - - Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE, - - // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: - Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE, - - Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE, - Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE, - Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE, - Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), - - _ => Settings.Provider.NONE, - }; - - public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) - { - var storedValue = component switch + /// + /// Gets the preview feature a component belongs to. Components that are generally available + /// return . This is the single place that maps a component to + /// its preview feature, so visibility checks never need to special-case one assistant. + /// + /// The required preview feature. + public PreviewFeatures RequiredPreviewFeature() => component switch { - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty, - Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty, - Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile, - // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: - Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty, + Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026, - // The Document Analysis Assistant does not have a preselected profile at the component level. - // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: - Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id, - - _ => string.Empty, + _ => PreviewFeatures.NONE, }; - return ProfilePreselection.FromStoredValue(storedValue); - } - - public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch - { - // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: - Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE, + /// + /// Gets whether a component owns exactly one assistant session slot, so that a running session + /// blocks starting another one and inactive sessions can be cleared as a group. + /// + /// + /// Components return false for three different reasons. The chat has no assistant sessions + /// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys + /// its sessions per briefing, so it owns one slot per stored briefing rather than one per + /// component. All must be excluded from the single-slot checks, which is why this is a + /// capability and not a component comparison. + /// + /// true when the component owns exactly one session slot. + public bool HasSingleSessionSlot() => component switch + { + Components.CHAT or Components.DYNAMIC_ASSISTANT => false, + Components.VISUAL_BRIEFING_ASSISTANT => false, + + _ => true, + }; + + /// + /// Gets the kind of media-import owner a component creates for its attachments. + /// + /// The media-import owner kind. + public MediaImportOwnerKind MediaOwnerKind() => component switch + { + Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING, + + _ => MediaImportOwnerKind.ASSISTANT, + }; + + public bool AllowSendTo() => component switch + { + Components.NONE => false, + Components.DYNAMIC_ASSISTANT => false, + + Components.ERI_ASSISTANT => false, + Components.BIAS_DAY_ASSISTANT => false, + Components.I18N_ASSISTANT => false, + Components.DOCUMENT_ANALYSIS_ASSISTANT => false, + Components.BATCH_PROCESSING_ASSISTANT => false, + Components.LOG_VIEWER_ASSISTANT => false, - _ => ChatTemplate.NO_CHAT_TEMPLATE, - }; + Components.APP_SETTINGS => false, + Components.WRITER => false, + + Components.AGENT_TEXT_CONTENT_CLEANER => false, + Components.AGENT_DATA_SOURCE_SELECTION => false, + Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false, + Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false, + Components.META_ASSISTANT => false, + + _ => true, + }; + + public string Name() => component switch + { + Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"), + Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"), + Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"), + Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"), + Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"), + Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"), + Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"), + Components.AGENDA_ASSISTANT => TB("Agenda Assistant"), + Components.CODING_ASSISTANT => TB("Coding Assistant"), + Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"), + Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"), + Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"), + Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"), + Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"), + Components.ERI_ASSISTANT => TB("ERI Server"), + Components.I18N_ASSISTANT => TB("Localization Assistant"), + Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"), + Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"), + Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"), + Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"), + Components.META_ASSISTANT => TB("Assistant Builder"), + Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"), + + Components.CHAT => TB("New Chat"), + + _ => Enum.GetName(component)!, + }; + + public ComponentsData GetData() => component switch + { + Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA), + Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING), + Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE), + Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER), + Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL), + Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION), + Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER), + Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING), + Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER), + Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS), + Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK), + Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS), + Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS), + Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING), + Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS), + Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER), + Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING), + + Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT), + + _ => new(Event.NONE, Routes.ASSISTANTS), + }; + + public ConfidenceLevel MinimumConfidence(SettingsManager settingsManager) => component switch + { + Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default, + Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default, + Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default, + Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default, + Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default, + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default, + Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default, + Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default, + Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default, + Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default, + Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default, + Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence, + + // The minimum confidence for the Document Analysis Assistant is set per policy. + // We do this inside the Document Analysis Assistant component: + Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE, + + // A policy-specific minimum is merged with this component default inside + // the Batch Processing Assistant; the stricter level wins. + Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default, + + _ => default, + }; + + public AIStudio.Settings.Provider PreselectedProvider(SettingsManager settingsManager) => component switch + { + Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE, + Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE, + Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE, + Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE, + Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE, + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE, + Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE, + Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE, + Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE, + Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE, + Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE, + Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE, + Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), + + // The Document Analysis Assistant does not have a preselected provider at the component level. + // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. + Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, + + Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE, + + // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: + Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE, + + Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), + + _ => Settings.Provider.NONE, + }; + + public ProfilePreselection GetProfilePreselection(SettingsManager settingsManager) + { + var storedValue = component switch + { + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty, + Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile, + // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: + Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty, + + // The Document Analysis Assistant does not have a preselected profile at the component level. + // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: + Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id, + + _ => string.Empty, + }; + + return ProfilePreselection.FromStoredValue(storedValue); + } + + public ChatTemplate PreselectedChatTemplate(SettingsManager settingsManager) => component switch + { + // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: + Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE, + + _ => ChatTemplate.NO_CHAT_TEMPLATE, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs b/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs index 52ea512b2..e656e4881 100644 --- a/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs +++ b/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs @@ -6,17 +6,20 @@ public static class ConfidenceSchemesExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceSchemesExtensions).Namespace, nameof(ConfidenceSchemesExtensions)); - public static string GetListDescription(this ConfidenceSchemes scheme) => scheme switch + extension(ConfidenceSchemes scheme) { - ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"), - ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"), - ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"), - ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"), - ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"), - ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"), + public string GetListDescription() => scheme switch + { + ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"), + ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"), + ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"), + ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"), + ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"), + ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"), - ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"), + ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"), - _ => TB("Unknown confidence scheme"), - }; + _ => TB("Unknown confidence scheme"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs index 1a1c41529..477287570 100644 --- a/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs +++ b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs @@ -2,66 +2,69 @@ namespace AIStudio.Tools; public static class DirectoryInfoExtensions { - /// - /// Determines the size and file count of the files a selection takes from a directory. When - /// desired, it reports the found files up to a limit. - /// - /// - /// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number - /// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able - /// to cancel slow operations, e.g., when the directory is on a slow network drive. - /// - /// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation. - /// - /// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the - /// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync - /// method to update the UI from a background thread. - /// /// The root directory to determine the size of. - /// The callback to report the current total size of the directory. - /// The callback to report the current number of files found. - /// The callback to report the next file found. The file name is relative to the root directory. - /// The maximum number of files to report. A negative value reports all files. - /// The callback to report that the operation is done. - /// The cancellation token to stop the operation. - /// Which files count, e.g., those a data source indexes. Every file counts when it is missing. - internal static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action reportCurrentTotalSize, Action reportCurrentNumFiles, Action reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null) + extension(DirectoryInfo directoryInfo) { - var rootDirectoryLen = directoryInfo.FullName.Length; - var filesToCount = selection ?? FolderFileSelection.EVERY_FILE; - long totalSize = 0; - long numFiles = 0; + /// + /// Determines the size and file count of the files a selection takes from a directory. When + /// desired, it reports the found files up to a limit. + /// + /// + /// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number + /// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able + /// to cancel slow operations, e.g., when the directory is on a slow network drive. + /// + /// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation. + /// + /// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the + /// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync + /// method to update the UI from a background thread. + /// + /// The callback to report the current total size of the directory. + /// The callback to report the current number of files found. + /// The callback to report the next file found. The file name is relative to the root directory. + /// The maximum number of files to report. A negative value reports all files. + /// The callback to report that the operation is done. + /// The cancellation token to stop the operation. + /// Which files count, e.g., those a data source indexes. Every file counts when it is missing. + internal async Task DetermineContentSize(Action reportCurrentTotalSize, Action reportCurrentNumFiles, Action reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null) + { + var rootDirectoryLen = directoryInfo.FullName.Length; + var filesToCount = selection ?? FolderFileSelection.EVERY_FILE; + long totalSize = 0; + long numFiles = 0; - await Task.Factory.StartNew(() => { - foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName)) - { - if (cancellationToken.IsCancellationRequested) - return; - - if (!filesToCount.Selects(filePath)) - continue; - - var file = new FileInfo(filePath); - if (filesToCount.IsTooLarge(file)) - continue; - - totalSize += file.Length; - numFiles++; - - if (numFiles % 100 == 0) + await Task.Factory.StartNew(() => { + foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName)) { - reportCurrentTotalSize(totalSize); - reportCurrentNumFiles(numFiles); + if (cancellationToken.IsCancellationRequested) + return; + + if (!filesToCount.Selects(filePath)) + continue; + + var file = new FileInfo(filePath); + if (filesToCount.IsTooLarge(file)) + continue; + + totalSize += file.Length; + numFiles++; + + if (numFiles % 100 == 0) + { + reportCurrentTotalSize(totalSize); + reportCurrentNumFiles(numFiles); + } + + if (reportMaxFiles < 0 || numFiles <= reportMaxFiles) + reportNextFile(file.FullName[rootDirectoryLen..]); } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); - if (reportMaxFiles < 0 || numFiles <= reportMaxFiles) - reportNextFile(file.FullName[rootDirectoryLen..]); - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + reportCurrentTotalSize(totalSize); + reportCurrentNumFiles(numFiles); - reportCurrentTotalSize(totalSize); - reportCurrentNumFiles(numFiles); - - done?.Invoke(); + done?.Invoke(); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs index 643d48b58..b4147de75 100644 --- a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs @@ -6,12 +6,15 @@ public static class ProviderTypeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderTypeExtensions).Namespace, nameof(ProviderTypeExtensions)); - public static string Explain(this ProviderType providerType) => providerType switch + extension(ProviderType providerType) { - ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."), - ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."), - ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."), + public string Explain() => providerType switch + { + ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."), + ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."), + ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."), - _ => TB("Unknown configuration. This data source cannot be used at the moment."), - }; + _ => TB("Unknown configuration. This data source cannot be used at the moment."), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ElementReferenceExtensions.cs b/app/MindWork AI Studio/Tools/ElementReferenceExtensions.cs index fe46acf15..d9b3ed390 100644 --- a/app/MindWork AI Studio/Tools/ElementReferenceExtensions.cs +++ b/app/MindWork AI Studio/Tools/ElementReferenceExtensions.cs @@ -4,8 +4,11 @@ namespace AIStudio.Tools; public static class ElementReferenceExtensions { - public static async ValueTask ScrollIntoViewAsync(this ElementReference elementReference, IJSRuntime jsRuntime) + extension(ElementReference elementReference) { - await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference); + public async ValueTask ScrollIntoViewAsync(IJSRuntime jsRuntime) + { + await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs b/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs index 8e77f8f80..e33823fbd 100644 --- a/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs +++ b/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs @@ -2,7 +2,13 @@ namespace AIStudio.Tools; public static class EncryptedTextExtensions { - public static async Task Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data); + extension(string data) + { + public async Task Encrypt(Encryption encryption) => await encryption.Encrypt(data); + } - public static async Task Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText); + extension(EncryptedText encryptedText) + { + public async Task Decrypt(Encryption encryption) => await encryption.Decrypt(encryptedText); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ExpressionExtensions.cs b/app/MindWork AI Studio/Tools/ExpressionExtensions.cs index bb57fad32..d251de723 100644 --- a/app/MindWork AI Studio/Tools/ExpressionExtensions.cs +++ b/app/MindWork AI Studio/Tools/ExpressionExtensions.cs @@ -8,89 +8,95 @@ public static class ExpressionExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); - /// - /// Extracts the member expression from a given lambda expression representing a property. - /// /// A lambda expression specifying the property for which the member expression is to be extracted. /// The lambda expression body must represent member access. /// The type of the object containing the property referenced in the lambda expression. /// The type of the property being accessed in the lambda expression. - /// The member expression that represents the property access. - /// Thrown if the provided lambda expression does not represent a valid property expression. - public static MemberExpression GetMemberExpression(this Expression> expression) + extension(Expression> expression) { - switch (expression.Body) + /// + /// Extracts the member expression from a given lambda expression representing a property. + /// + /// The member expression that represents the property access. + /// Thrown if the provided lambda expression does not represent a valid property expression. + public MemberExpression GetMemberExpression() { - // Case for value types, which are wrapped in UnaryExpression: - case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: - return (MemberExpression)unaryExpression.Operand; + switch (expression.Body) + { + // Case for value types, which are wrapped in UnaryExpression: + case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: + return (MemberExpression)unaryExpression.Operand; - // Case for reference types, which are directly MemberExpressions: - case MemberExpression memberExpression: - return memberExpression; + // Case for reference types, which are directly MemberExpressions: + case MemberExpression memberExpression: + return memberExpression; - default: - LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); - throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); + default: + LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); + throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); + } } } - /// - /// Attempts to increment the value of an uint property for a specified object using a - /// provided expression. - /// /// An expression representing the property to be incremented. The property /// must be of type uint and belong to the provided object. - /// The object that contains the property referenced by the expression. - /// The type of increment operation to perform (e.g., prefix or postfix). /// The type of the object that contains the property to be incremented. /// The type of the property to be incremented. - /// An IncrementResult object containing the result of the increment operation. - public static IncrementResult TryIncrement(this Expression> expression, TIn data, IncrementType type) where TOut : IBinaryInteger + extension(Expression> expression) where TOut : IBinaryInteger { - // Ensure that the expression body is a member expression: - if (expression.Body is not MemberExpression memberExpression) - return new(false, TOut.Zero); - - // Ensure that the member expression is a property: - if (memberExpression.Member is not PropertyInfo propertyInfo) - return new(false, TOut.Zero); - - // Ensure that the member expression has a target object: - if (memberExpression.Expression is null) - return new(false, TOut.Zero); - - // Get the target object for the expression, which is the object containing the property to increment: - var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters); - - // Compile the lambda expression to get the target object - // (which is the object containing the property to increment): - var targetObject = targetObjectExpression.Compile().DynamicInvoke(data); - - // Was the compilation successful? - if (targetObject is null) - return new(false, TOut.Zero); - - // Read the current value of the property: - if (propertyInfo.GetValue(targetObject) is not TOut value) - return new(false, TOut.Zero); - - // Increment the value: - switch (type) + /// + /// Attempts to increment the value of an uint property for a specified object using a + /// provided expression. + /// + /// The object that contains the property referenced by the expression. + /// The type of increment operation to perform (e.g., prefix or postfix). + /// An IncrementResult object containing the result of the increment operation. + public IncrementResult TryIncrement(TIn data, IncrementType type) { - case IncrementType.PRE: - var nextValue = value + TOut.CreateChecked(1); - propertyInfo.SetValue(targetObject, nextValue); - return new(true, nextValue); - - case IncrementType.POST: - var currentValue = value; - var incrementedValue = value + TOut.CreateChecked(1); - propertyInfo.SetValue(targetObject, incrementedValue); - return new(true, currentValue); - - default: + // Ensure that the expression body is a member expression: + if (expression.Body is not MemberExpression memberExpression) return new(false, TOut.Zero); + + // Ensure that the member expression is a property: + if (memberExpression.Member is not PropertyInfo propertyInfo) + return new(false, TOut.Zero); + + // Ensure that the member expression has a target object: + if (memberExpression.Expression is null) + return new(false, TOut.Zero); + + // Get the target object for the expression, which is the object containing the property to increment: + var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters); + + // Compile the lambda expression to get the target object + // (which is the object containing the property to increment): + var targetObject = targetObjectExpression.Compile().DynamicInvoke(data); + + // Was the compilation successful? + if (targetObject is null) + return new(false, TOut.Zero); + + // Read the current value of the property: + if (propertyInfo.GetValue(targetObject) is not TOut value) + return new(false, TOut.Zero); + + // Increment the value: + switch (type) + { + case IncrementType.PRE: + var nextValue = value + TOut.CreateChecked(1); + propertyInfo.SetValue(targetObject, nextValue); + return new(true, nextValue); + + case IncrementType.POST: + var currentValue = value; + var incrementedValue = value + TOut.CreateChecked(1); + propertyInfo.SetValue(targetObject, incrementedValue); + return new(true, currentValue); + + default: + return new(false, TOut.Zero); + } } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 04b885616..9a15ff21d 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -52,61 +52,215 @@ public static class FileExportFormatExtensions /// public static readonly IReadOnlyList ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS]; - /// - /// Returns the name of the format as shown to the user. - /// /// The format. - /// The name of the format. - public static string ToName(this FileExportFormat format) => format switch + extension(FileExportFormat format) { - FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"), - FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"), - FileExportFormat.LATEX => TB("LaTeX (.tex)"), - FileExportFormat.MARKDOWN => TB("Markdown (.md)"), - FileExportFormat.HTML => TB("Webpage (.html)"), - FileExportFormat.CSV => TB("Table (.csv)"), - FileExportFormat.TSV => TB("Table (.tsv)"), - FileExportFormat.SOURCE_CODE => TB("Source code"), + /// + /// Returns the name of the format as shown to the user. + /// + /// The name of the format. + public string ToName() => format switch + { + FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"), + FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"), + FileExportFormat.LATEX => TB("LaTeX (.tex)"), + FileExportFormat.MARKDOWN => TB("Markdown (.md)"), + FileExportFormat.HTML => TB("Webpage (.html)"), + FileExportFormat.CSV => TB("Table (.csv)"), + FileExportFormat.TSV => TB("Table (.tsv)"), + FileExportFormat.SOURCE_CODE => TB("Source code"), - _ => TB("Unknown format"), - }; + _ => TB("Unknown format"), + }; - /// - /// Returns the icon of the format. - /// - /// The format. - /// The icon of the format. - public static string ToIcon(this FileExportFormat format) => format switch - { - FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord, - FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument, - FileExportFormat.LATEX => Icons.Material.Filled.Functions, - FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields, - FileExportFormat.HTML => Icons.Material.Filled.Html, - FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart, - FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code, + /// + /// Returns the icon of the format. + /// + /// The icon of the format. + public string ToIcon() => format switch + { + FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord, + FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument, + FileExportFormat.LATEX => Icons.Material.Filled.Functions, + FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields, + FileExportFormat.HTML => Icons.Material.Filled.Html, + FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart, + FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code, - _ => Icons.Material.Filled.Help, - }; + _ => Icons.Material.Filled.Help, + }; - /// - /// Returns the file extension of the format, including the leading dot. - /// - /// The format. - /// The file extension, or an empty string when the format writes no file or, as code - /// does, takes the extension from its language. - public static string ToFileExtension(this FileExportFormat format) => format switch - { - FileExportFormat.MICROSOFT_WORD => ".docx", - FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt", - FileExportFormat.LATEX => ".tex", - FileExportFormat.MARKDOWN => ".md", - FileExportFormat.HTML => ".html", - FileExportFormat.CSV => ".csv", - FileExportFormat.TSV => ".tsv", + /// + /// Returns the file extension of the format, including the leading dot. + /// + /// The file extension, or an empty string when the format writes no file or, as code + /// does, takes the extension from its language. + public string ToFileExtension() => format switch + { + FileExportFormat.MICROSOFT_WORD => ".docx", + FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt", + FileExportFormat.LATEX => ".tex", + FileExportFormat.MARKDOWN => ".md", + FileExportFormat.HTML => ".html", + FileExportFormat.CSV => ".csv", + FileExportFormat.TSV => ".tsv", - _ => string.Empty, - }; + _ => string.Empty, + }; + + /// + /// Determines whether the format holds a table rather than a text. + /// + /// True for the formats a spreadsheet opens. + public bool IsTabular() => format is FileExportFormat.CSV or FileExportFormat.TSV; + + /// + /// Determines whether a file of the format is plain text, which AI Studio writes as it is. + /// + /// + /// That holds for a web page and a LaTeX document as well, even though an entire answer needs + /// Pandoc to become one: the answer is Markdown, whereas a page the model wrote is a finished + /// file already. A Word or an OpenDocument file is an archive, and only Pandoc produces one. The + /// list is spelled out on purpose, so a format added later counts as plain text only once + /// somebody says so. + /// + /// True, when a text written as it is makes a valid file of the format. + public bool IsPlainText() => format is FileExportFormat.LATEX or FileExportFormat.MARKDOWN or FileExportFormat.HTML or FileExportFormat.CSV or FileExportFormat.TSV or FileExportFormat.SOURCE_CODE; + + /// + /// Returns the file name the save dialog starts with. + /// + /// + /// Without a name, the dialog opens with an empty field and the user easily ends up with a + /// file which carries no extension at all. The fallback name is deliberately not translated: + /// a file name should survive being copied between systems and locales. + /// + /// What the file is about, for example the heading above a table. Anything + /// a file name cannot hold is removed. Null or blank falls back to a generic name. + /// The suggested file name, including its extension. + public string ToSuggestedFileName(string? name = null) => SuggestFileName(name, format.ToFileExtension()); + + /// + /// Returns the filter which the save dialog offers for the format. + /// + /// The filter, or null when the format cannot be written or, as code does, takes the + /// filter from its language. + public FileTypeFilter? ToFileTypeFilter() => format switch + { + FileExportFormat.MICROSOFT_WORD => FileTypes.MS_WORD, + FileExportFormat.OPEN_DOCUMENT_TEXT => FileTypes.ODT, + FileExportFormat.LATEX => FileTypes.TEX, + FileExportFormat.MARKDOWN => FileTypes.MARKDOWN, + FileExportFormat.HTML => FileTypes.HTML_DOCUMENT, + FileExportFormat.CSV => FileTypes.CSV, + FileExportFormat.TSV => FileTypes.TSV, + + _ => null, + }; + + /// + /// Returns the encoding the file gets written with. + /// + /// + /// Everything is UTF-8, the question is only whether the file starts with a byte order mark. + /// Tabular files get one, because Excel otherwise reads them in the local ANSI code page and + /// turns every umlaut into garbage. Text files get none: editors, compilers, and LaTeX have + /// no use for it and some of them stumble over it. + /// + /// The encoding to write the file with. + public Encoding ToFileEncoding() => format switch + { + FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK, + + _ => WITHOUT_BYTE_ORDER_MARK, + }; + + /// + /// Wraps a text into a comment of the format: whoever opens the file in an editor reads it, + /// while a browser or a LaTeX run skips it. + /// + /// + /// A comment in HTML, and so in Markdown, ends at the first --> it holds, and a browser takes + /// --!> for the same; the rest of the text would spill onto the page from there. The title of + /// a web page may hold either, so a space goes in before the bracket, which keeps the text + /// readable and ends nothing. Every other pair of dashes stays, because a web address may carry + /// one, as in the xn-- of a domain with an umlaut. A LaTeX comment has no end to watch for: it + /// runs to the end of its line, so every line starts one. + /// + /// The text to put into the comment. + /// The comment, or an empty string when the format has none. + /// True, when the format knows comments. + public bool TryToComment(string text, out string comment) + { + var lines = text.TrimEnd().ReplaceLineEndings("\n").Split('\n'); + switch (format) + { + case FileExportFormat.HTML or FileExportFormat.MARKDOWN: + var commentText = string.Join(Environment.NewLine, lines).Replace("-->", "-- >").Replace("--!>", "--! >"); + comment = $""; + return true; + + case FileExportFormat.LATEX: + comment = string.Join(Environment.NewLine, lines.Select(line => line.Length is 0 ? "%" : $"% {line}")); + return true; + + default: + comment = string.Empty; + return false; + } + } + + /// + /// Determines whether a link into a local file may name the page it points at. + /// + /// + /// A page is named by the fragment of the link, the way the PDF open parameters call for. A + /// browser and a PDF reader follow that and open the document on the page; Word and LibreOffice + /// take the fragment for part of the file name, look for a file which does not exist, and refuse + /// the link altogether. There the page is dropped, so the link at least opens the document -- + /// which page it was stays in the title of the source. Verified on 2026-09-15 with LibreOffice + /// on an exported .odt. A format added later keeps the page unless it is known to stumble too. + /// + /// True, when a reader of this format follows such a link. + public bool FollowsPageAnchors() => format switch + { + FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false, + + _ => true, + }; + + /// + /// Determines whether Pandoc has to be told the title of a document in the format. + /// + /// + /// A web page shows its title in the browser tab. Without one, Pandoc names the page after its + /// input file, which is a temporary file of ours with a random name. Word and OpenDocument show + /// no such title, and handed one anyway, they keep it as a document property nobody asked for; + /// verified with Pandoc 3.8.3 on 2026-09-23. LaTeX ignores it. + /// + /// True, when a document of this format needs a title besides its content. + public bool NeedsPageTitle() => format is FileExportFormat.HTML; + + /// + /// Returns the name Pandoc knows the format by. + /// + /// The Pandoc output format, or an empty string when AI Studio writes the file itself. + public string ToPandocOutputFormat() => format switch + { + FileExportFormat.MICROSOFT_WORD => "docx", + FileExportFormat.OPEN_DOCUMENT_TEXT => "odt", + FileExportFormat.LATEX => "latex", + FileExportFormat.HTML => "html", + + _ => string.Empty, + }; + + /// + /// Determines whether writing the format needs Pandoc. + /// + /// True, when Pandoc converts the message; false, when AI Studio writes the file itself. + public bool UsesPandoc() => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat()); + } /// /// Reads which format a model means when it names a language behind the opening fence of a @@ -139,41 +293,6 @@ public static class FileExportFormatExtensions return format is not FileExportFormat.NONE; } - /// - /// Determines whether the format holds a table rather than a text. - /// - /// The format. - /// True for the formats a spreadsheet opens. - public static bool IsTabular(this FileExportFormat format) => format is FileExportFormat.CSV or FileExportFormat.TSV; - - /// - /// Determines whether a file of the format is plain text, which AI Studio writes as it is. - /// - /// - /// That holds for a web page and a LaTeX document as well, even though an entire answer needs - /// Pandoc to become one: the answer is Markdown, whereas a page the model wrote is a finished - /// file already. A Word or an OpenDocument file is an archive, and only Pandoc produces one. The - /// list is spelled out on purpose, so a format added later counts as plain text only once - /// somebody says so. - /// - /// The format. - /// True, when a text written as it is makes a valid file of the format. - public static bool IsPlainText(this FileExportFormat format) => format is FileExportFormat.LATEX or FileExportFormat.MARKDOWN or FileExportFormat.HTML or FileExportFormat.CSV or FileExportFormat.TSV or FileExportFormat.SOURCE_CODE; - - /// - /// Returns the file name the save dialog starts with. - /// - /// - /// Without a name, the dialog opens with an empty field and the user easily ends up with a - /// file which carries no extension at all. The fallback name is deliberately not translated: - /// a file name should survive being copied between systems and locales. - /// - /// The format. - /// What the file is about, for example the heading above a table. Anything - /// a file name cannot hold is removed. Null or blank falls back to a generic name. - /// The suggested file name, including its extension. - public static string ToSuggestedFileName(this FileExportFormat format, string? name = null) => SuggestFileName(name, format.ToFileExtension()); - /// /// Returns the file name the save dialog starts with, for a file with the given extension. /// @@ -236,132 +355,4 @@ public static class FileExportFormatExtensions // A trailing dot makes a file invisible on Unix and is dropped by Windows: return fragment.ToString().TrimEnd('.'); } - - /// - /// Returns the filter which the save dialog offers for the format. - /// - /// The format. - /// The filter, or null when the format cannot be written or, as code does, takes the - /// filter from its language. - public static FileTypeFilter? ToFileTypeFilter(this FileExportFormat format) => format switch - { - FileExportFormat.MICROSOFT_WORD => FileTypes.MS_WORD, - FileExportFormat.OPEN_DOCUMENT_TEXT => FileTypes.ODT, - FileExportFormat.LATEX => FileTypes.TEX, - FileExportFormat.MARKDOWN => FileTypes.MARKDOWN, - FileExportFormat.HTML => FileTypes.HTML_DOCUMENT, - FileExportFormat.CSV => FileTypes.CSV, - FileExportFormat.TSV => FileTypes.TSV, - - _ => null, - }; - - /// - /// Returns the encoding the file gets written with. - /// - /// - /// Everything is UTF-8, the question is only whether the file starts with a byte order mark. - /// Tabular files get one, because Excel otherwise reads them in the local ANSI code page and - /// turns every umlaut into garbage. Text files get none: editors, compilers, and LaTeX have - /// no use for it and some of them stumble over it. - /// - /// The format. - /// The encoding to write the file with. - public static Encoding ToFileEncoding(this FileExportFormat format) => format switch - { - FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK, - - _ => WITHOUT_BYTE_ORDER_MARK, - }; - - /// - /// Wraps a text into a comment of the format: whoever opens the file in an editor reads it, - /// while a browser or a LaTeX run skips it. - /// - /// - /// A comment in HTML, and so in Markdown, ends at the first --> it holds, and a browser takes - /// --!> for the same; the rest of the text would spill onto the page from there. The title of - /// a web page may hold either, so a space goes in before the bracket, which keeps the text - /// readable and ends nothing. Every other pair of dashes stays, because a web address may carry - /// one, as in the xn-- of a domain with an umlaut. A LaTeX comment has no end to watch for: it - /// runs to the end of its line, so every line starts one. - /// - /// The format. - /// The text to put into the comment. - /// The comment, or an empty string when the format has none. - /// True, when the format knows comments. - public static bool TryToComment(this FileExportFormat format, string text, out string comment) - { - var lines = text.TrimEnd().ReplaceLineEndings("\n").Split('\n'); - switch (format) - { - case FileExportFormat.HTML or FileExportFormat.MARKDOWN: - var commentText = string.Join(Environment.NewLine, lines).Replace("-->", "-- >").Replace("--!>", "--! >"); - comment = $""; - return true; - - case FileExportFormat.LATEX: - comment = string.Join(Environment.NewLine, lines.Select(line => line.Length is 0 ? "%" : $"% {line}")); - return true; - - default: - comment = string.Empty; - return false; - } - } - - /// - /// Determines whether a link into a local file may name the page it points at. - /// - /// - /// A page is named by the fragment of the link, the way the PDF open parameters call for. A - /// browser and a PDF reader follow that and open the document on the page; Word and LibreOffice - /// take the fragment for part of the file name, look for a file which does not exist, and refuse - /// the link altogether. There the page is dropped, so the link at least opens the document -- - /// which page it was stays in the title of the source. Verified on 2026-09-15 with LibreOffice - /// on an exported .odt. A format added later keeps the page unless it is known to stumble too. - /// - /// The format. - /// True, when a reader of this format follows such a link. - public static bool FollowsPageAnchors(this FileExportFormat format) => format switch - { - FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false, - - _ => true, - }; - - /// - /// Determines whether Pandoc has to be told the title of a document in the format. - /// - /// - /// A web page shows its title in the browser tab. Without one, Pandoc names the page after its - /// input file, which is a temporary file of ours with a random name. Word and OpenDocument show - /// no such title, and handed one anyway, they keep it as a document property nobody asked for; - /// verified with Pandoc 3.8.3 on 2026-09-23. LaTeX ignores it. - /// - /// The format. - /// True, when a document of this format needs a title besides its content. - public static bool NeedsPageTitle(this FileExportFormat format) => format is FileExportFormat.HTML; - - /// - /// Returns the name Pandoc knows the format by. - /// - /// The format. - /// The Pandoc output format, or an empty string when AI Studio writes the file itself. - public static string ToPandocOutputFormat(this FileExportFormat format) => format switch - { - FileExportFormat.MICROSOFT_WORD => "docx", - FileExportFormat.OPEN_DOCUMENT_TEXT => "odt", - FileExportFormat.LATEX => "latex", - FileExportFormat.HTML => "html", - - _ => string.Empty, - }; - - /// - /// Determines whether writing the format needs Pandoc. - /// - /// The format. - /// True, when Pandoc converts the message; false, when AI Studio writes the file itself. - public static bool UsesPandoc(this FileExportFormat format) => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat()); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/FileExtractionErrorCodeExtensions.cs b/app/MindWork AI Studio/Tools/FileExtractionErrorCodeExtensions.cs index 56f92e1aa..e215fb01d 100644 --- a/app/MindWork AI Studio/Tools/FileExtractionErrorCodeExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExtractionErrorCodeExtensions.cs @@ -15,98 +15,99 @@ internal static class FileExtractionErrorCodeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionErrorCodeExtensions).Namespace, nameof(FileExtractionErrorCodeExtensions)); - /// - /// Gets a value indicating whether reading the file again will fail again, as long as the file - /// itself does not change. - /// /// The stable failure code. - /// True, when the reason lies in the file itself. - internal static bool IsPermanentIndexingFailure(this FileExtractionErrorCode code) => code switch + extension(FileExtractionErrorCode code) { - // - // The reason lies in the file. Reading it again without changing it produces the same - // outcome, so the indexer waits for a new fingerprint: - // - FileExtractionErrorCode.NO_TEXT_EXTRACTED => true, - FileExtractionErrorCode.NO_CONTENT => true, - FileExtractionErrorCode.NOT_TEXT_CONTENT => true, - FileExtractionErrorCode.NOT_A_VALID_PDF => true, - FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true, - FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true, - FileExtractionErrorCode.PDF_ENCRYPTED => true, - FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true, - FileExtractionErrorCode.EXECUTABLE_REJECTED => true, - FileExtractionErrorCode.UNSUPPORTED => true, - FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true, + /// + /// Gets a value indicating whether reading the file again will fail again, as long as the file + /// itself does not change. + /// + /// True, when the reason lies in the file itself. + internal bool IsPermanentIndexingFailure() => code switch + { + // + // The reason lies in the file. Reading it again without changing it produces the same + // outcome, so the indexer waits for a new fingerprint: + // + FileExtractionErrorCode.NO_TEXT_EXTRACTED => true, + FileExtractionErrorCode.NO_CONTENT => true, + FileExtractionErrorCode.NOT_TEXT_CONTENT => true, + FileExtractionErrorCode.NOT_A_VALID_PDF => true, + FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true, + FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true, + FileExtractionErrorCode.PDF_ENCRYPTED => true, + FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true, + FileExtractionErrorCode.EXECUTABLE_REJECTED => true, + FileExtractionErrorCode.UNSUPPORTED => true, + FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true, - // Pages holding nothing but images are one of the recurring cases here, and that is a - // property of the document, not of the environment: - FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true, + // Pages holding nothing but images are one of the recurring cases here, and that is a + // property of the document, not of the environment: + FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true, - // - // Everything else depends on the surroundings: an unavailable drive, a file someone else - // has open, a missing engine, or a runtime which did not answer in time. All of them are - // worth another attempt during the next run: - // - _ => false, - }; + // + // Everything else depends on the surroundings: an unavailable drive, a file someone else + // has open, a missing engine, or a runtime which did not answer in time. All of them are + // worth another attempt during the next run: + // + _ => false, + }; - /// - /// Names the cause in a few words. - /// - /// - /// Used to group the files of an indexing run by what happened to them: nine hundred entries - /// which all say the same sentence are one cause, not nine hundred. - /// - /// The stable failure code. - /// The localized name of the cause, or an empty text when the code has none. - internal static string GetIndexingCauseName(this FileExtractionErrorCode code) => code switch - { - FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"), - FileExtractionErrorCode.NO_CONTENT => TB("No content"), - FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"), - FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"), - FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"), - FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"), - FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"), - FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"), - FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"), - FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"), - FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"), - FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"), + /// + /// Names the cause in a few words. + /// + /// + /// Used to group the files of an indexing run by what happened to them: nine hundred entries + /// which all say the same sentence are one cause, not nine hundred. + /// + /// The localized name of the cause, or an empty text when the code has none. + internal string GetIndexingCauseName() => code switch + { + FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"), + FileExtractionErrorCode.NO_CONTENT => TB("No content"), + FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"), + FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"), + FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"), + FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"), + FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"), + FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"), + FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"), + FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"), + FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"), + FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"), - FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"), - FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"), - FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"), - FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"), - FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"), - FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"), + FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"), + FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"), + FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"), + FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"), + FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"), + FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"), - // Nothing about these lies in the file: AI Studio asked its runtime for the content and - // got back something it cannot work with. One name for all of them, because that is the - // one thing the user can tell from them: - FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"), - FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"), - FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"), - FileExtractionErrorCode.INTERNAL => TB("Internal error"), + // Nothing about these lies in the file: AI Studio asked its runtime for the content and + // got back something it cannot work with. One name for all of them, because that is the + // one thing the user can tell from them: + FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"), + FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"), + FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"), + FileExtractionErrorCode.INTERNAL => TB("Internal error"), - // Codes which say nothing beyond the message of the single file. The caller names those - // files itself and shows their messages instead: - _ => string.Empty, - }; + // Codes which say nothing beyond the message of the single file. The caller names those + // files itself and shows their messages instead: + _ => string.Empty, + }; - /// - /// Gets the localized message which explains why a file was not indexed. - /// - /// - /// These texts are the counterpart of the ones used for chat attachments: there, a file which - /// cannot be read is simply not sent, while here it stays out of the index and the user needs - /// to know whether AI Studio will come back to it on its own. - /// - /// The stable failure code. - /// The name of the file, as shown to the user. - /// The localized message. - internal static string ToIndexingUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToIndexingMessageFormat(code), fileName); + /// + /// Gets the localized message which explains why a file was not indexed. + /// + /// + /// These texts are the counterpart of the ones used for chat attachments: there, a file which + /// cannot be read is simply not sent, while here it stays out of the index and the user needs + /// to know whether AI Studio will come back to it on its own. + /// + /// The name of the file, as shown to the user. + /// The localized message. + internal string ToIndexingUserMessage(string fileName) => string.Format(ToIndexingMessageFormat(code), fileName); + } private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch { diff --git a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs index 82d82771c..688367e1a 100644 --- a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs @@ -14,60 +14,64 @@ internal static class FileExtractionResultExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions)); - /// - /// Gets the localized message which explains why a file could not be read. - /// /// The extraction result. - /// The name of the file, as shown to the user. - /// The localized message. - internal static string ToUserMessage(this FileExtractionResult result, string fileName) + extension(FileExtractionResult result) { - // When we know what the file really is, naming it beats a generic "not supported": - if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null) - return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat); + /// + /// Gets the localized message which explains why a file could not be read. + /// + /// The name of the file, as shown to the user. + /// The localized message. + internal string ToUserMessage(string fileName) + { + // When we know what the file really is, naming it beats a generic "not supported": + if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null) + return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat); - return result.ErrorCode.ToUserMessage(fileName); + return result.ErrorCode.ToUserMessage(fileName); + } + + /// + /// Gets the localized message for a file whose content does not match its file extension. + /// + /// + /// This is a notice, not a failure: the file was read according to its content. We still tell + /// the user, because a wrong extension is a real problem for every other program as well. + /// + /// The name of the file, as shown to the user. + /// The localized message. + internal string ToExtensionMismatchUserMessage(string fileName) => string.Format( + TB("The file '{0}' is actually a {1} and was read as such. Please correct its file extension."), + fileName, + result.DetectedFormat); + + /// + /// Gets the localized message for a file which was read, but lost some of its pages. + /// + /// The name of the file, as shown to the user. + /// The localized message. + internal string ToPartialUserMessage(string fileName) + { + if (result.FailedPages.Count == 0) + return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName); + + return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages)); + } } - /// - /// Gets the localized message for a file whose content does not match its file extension. - /// - /// - /// This is a notice, not a failure: the file was read according to its content. We still tell - /// the user, because a wrong extension is a real problem for every other program as well. - /// - /// The extraction result. - /// The name of the file, as shown to the user. - /// The localized message. - internal static string ToExtensionMismatchUserMessage(this FileExtractionResult result, string fileName) => string.Format( - TB("The file '{0}' is actually a {1} and was read as such. Please correct its file extension."), - fileName, - result.DetectedFormat); - - /// - /// Gets the localized message which explains why a file could not be read. - /// - /// - /// This overload exists for the places which know the reason before an extraction was even - /// attempted, so both ways of skipping a file tell the user the same thing. - /// /// The stable failure code. - /// The name of the file, as shown to the user. - /// The localized message. - internal static string ToUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName); - - /// - /// Gets the localized message for a file which was read, but lost some of its pages. - /// - /// The extraction result. - /// The name of the file, as shown to the user. - /// The localized message. - internal static string ToPartialUserMessage(this FileExtractionResult result, string fileName) + extension(FileExtractionErrorCode code) { - if (result.FailedPages.Count == 0) - return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName); - - return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages)); + /// + /// Gets the localized message which explains why a file could not be read. + /// + /// + /// This overload exists for the places which know the reason before an extraction was even + /// attempted, so both ways of skipping a file tell the user the same thing. + /// + /// The name of the file, as shown to the user. + /// The localized message. + internal string ToUserMessage(string fileName) => string.Format(ToUserMessageFormat(code), fileName); } private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch diff --git a/app/MindWork AI Studio/Tools/FileInfoExtensions.cs b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs index c30026244..8368324cb 100644 --- a/app/MindWork AI Studio/Tools/FileInfoExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs @@ -2,16 +2,19 @@ namespace AIStudio.Tools; public static class FileInfoExtensions { - /// - /// Returns the file size in human-readable format. - /// /// The file info object. - /// The file size in human-readable format. - public static string FileSize(this FileInfo fileInfo) + extension(FileInfo fileInfo) { - if (!fileInfo.Exists) - return "N/A"; + /// + /// Returns the file size in human-readable format. + /// + /// The file size in human-readable format. + public string FileSize() + { + if (!fileInfo.Exists) + return "N/A"; - return fileInfo.Length.FileSize(); + return fileInfo.Length.FileSize(); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/HttpRequestHeadersExtensions.cs b/app/MindWork AI Studio/Tools/HttpRequestHeadersExtensions.cs index 7a3fc122d..3a248968e 100644 --- a/app/MindWork AI Studio/Tools/HttpRequestHeadersExtensions.cs +++ b/app/MindWork AI Studio/Tools/HttpRequestHeadersExtensions.cs @@ -11,8 +11,11 @@ public static class HttpRequestHeadersExtensions API_TOKEN = Program.API_TOKEN; } - public static void AddApiToken(this HttpRequestHeaders headers) + extension(HttpRequestHeaders headers) { - headers.Add("token", API_TOKEN); + public void AddApiToken() + { + headers.Add("token", API_TOKEN); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs b/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs index df35f7f32..968596d5f 100644 --- a/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs +++ b/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs @@ -4,107 +4,110 @@ public static class IConfidenceExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - public static TargetWindow DetermineTargetWindow(this IReadOnlyList items, TargetWindowStrategy strategy, int numMaximumItems = 30) where T : IConfidence - { - switch (strategy) - { - case TargetWindowStrategy.A_FEW_GOOD_ONES: - return new(1, 2, 3, 0f); - - case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING: - var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f); - if(numItemsBetterThanGuessing < 3) - return new(1, 2, 3, 0.5f); - - // We want the top 10% of items better than guessing: - var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f); - - // When these 10% are just a few items, we take them all: - if (numTop10Percent <= 10) - { - var diff = numItemsBetterThanGuessing - numTop10Percent; - var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f); - return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f); - } - - // Let's define the size of the window: - const int MIN_NUM_ITEMS = 3; - var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent); - windowMin = Math.Min(windowMin, numMaximumItems - 1); - var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3); - var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent)); - var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize); - return new(totalMin, windowMin, windowMax, 0.5f); - - case TargetWindowStrategy.NONE: - default: - return new(-1, -1, -1, 0f); - } - } - - /// - /// Determine the optimal confidence threshold for a list of items - /// in order to match a target window of number of items. - /// /// The list of confidence items to analyze. - /// The target window for the number of items. - /// The maximum number of steps to search for the threshold. /// The type of items in the list. - /// The confidence threshold. - public static float GetConfidenceThreshold(this IReadOnlyList items, TargetWindow targetWindow, int maxSteps = 10) where T : IConfidence + extension(IReadOnlyList items) where T : IConfidence { - if(!targetWindow.IsValid()) + public TargetWindow DetermineTargetWindow(TargetWindowStrategy strategy, int numMaximumItems = 30) { - LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold."); - return 0f; - } - - // - // Without items there is no threshold to find, and the Min and Max calls below would throw - // on an empty sequence. Every caller checks this today, which is precisely how such a guard - // goes missing once a new caller arrives. It belongs here, next to the calls it protects: - // - if(items.Count == 0) - { - LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold."); - return 0f; - } - - var confidenceValues = items.Select(x => x.Confidence).ToList(); - var minConfidence = confidenceValues.Min(); - var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold); - var upperBound = confidenceValues.Max(); - - // - // We search for a threshold so that we have between - // targetWindowMin and targetWindowMax items. When not - // possible, we take all items (e.g., threshold = 0f; depends on the used window strategy) - // - var threshold = 0.0f; - - // Check the case where the confidence values are too close: - if (upperBound - minConfidence >= 0.01) - { - var previousThreshold = threshold; - for (var i = 0; i < maxSteps; i++) + switch (strategy) { - threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps; - var numMatches = items.Count(x => x.Confidence >= threshold); - if (numMatches <= targetWindow.NumMinItems) - { - threshold = previousThreshold; - break; - } + case TargetWindowStrategy.A_FEW_GOOD_ONES: + return new(1, 2, 3, 0f); + + case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING: + var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f); + if(numItemsBetterThanGuessing < 3) + return new(1, 2, 3, 0.5f); - if (targetWindow.InsideWindow(numMatches)) - break; + // We want the top 10% of items better than guessing: + var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f); - previousThreshold = threshold; + // When these 10% are just a few items, we take them all: + if (numTop10Percent <= 10) + { + var diff = numItemsBetterThanGuessing - numTop10Percent; + var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f); + return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f); + } + + // Let's define the size of the window: + const int MIN_NUM_ITEMS = 3; + var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent); + windowMin = Math.Min(windowMin, numMaximumItems - 1); + var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3); + var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent)); + var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize); + return new(totalMin, windowMin, windowMax, 0.5f); + + case TargetWindowStrategy.NONE: + default: + return new(-1, -1, -1, 0f); } } - else - LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold."); + + /// + /// Determine the optimal confidence threshold for a list of items + /// in order to match a target window of number of items. + /// + /// The target window for the number of items. + /// The maximum number of steps to search for the threshold. + /// The confidence threshold. + public float GetConfidenceThreshold(TargetWindow targetWindow, int maxSteps = 10) + { + if(!targetWindow.IsValid()) + { + LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold."); + return 0f; + } + + // + // Without items there is no threshold to find, and the Min and Max calls below would throw + // on an empty sequence. Every caller checks this today, which is precisely how such a guard + // goes missing once a new caller arrives. It belongs here, next to the calls it protects: + // + if(items.Count == 0) + { + LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold."); + return 0f; + } + + var confidenceValues = items.Select(x => x.Confidence).ToList(); + var minConfidence = confidenceValues.Min(); + var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold); + var upperBound = confidenceValues.Max(); + + // + // We search for a threshold so that we have between + // targetWindowMin and targetWindowMax items. When not + // possible, we take all items (e.g., threshold = 0f; depends on the used window strategy) + // + var threshold = 0.0f; + + // Check the case where the confidence values are too close: + if (upperBound - minConfidence >= 0.01) + { + var previousThreshold = threshold; + for (var i = 0; i < maxSteps; i++) + { + threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps; + var numMatches = items.Count(x => x.Confidence >= threshold); + if (numMatches <= targetWindow.NumMinItems) + { + threshold = previousThreshold; + break; + } + + if (targetWindow.InsideWindow(numMatches)) + break; + + previousThreshold = threshold; + } + } + else + LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold."); - return threshold; + return threshold; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs index 63e8f62a0..118278d19 100644 --- a/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs +++ b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs @@ -3,18 +3,27 @@ using AIStudio.Tools.Services; namespace AIStudio.Tools; +// +// The methods with a params parameter stay classic extension methods instead of moving into the +// extension block: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns +// about every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Move +// them once the SDK in metadata.txt carries the fix; MWAIS0016 makes the same exception until then. +// public static class JsRuntimeExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions)); - public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2) + extension(IJSRuntime jsRuntime) { - await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID); - } + public async Task GenerateAndShowDiff(string text1, string text2) + { + await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID); + } - public static async Task ClearDiv(this IJSRuntime jsRuntime, string divId) - { - await jsRuntime.InvokeVoidAsync("clearDiv", divId); + public async Task ClearDiv(string divId) + { + await jsRuntime.InvokeVoidAsync("clearDiv", divId); + } } /// diff --git a/app/MindWork AI Studio/Tools/JsonSerializerOptionsExtensions.cs b/app/MindWork AI Studio/Tools/JsonSerializerOptionsExtensions.cs index d6b198651..bdf22d6bf 100644 --- a/app/MindWork AI Studio/Tools/JsonSerializerOptionsExtensions.cs +++ b/app/MindWork AI Studio/Tools/JsonSerializerOptionsExtensions.cs @@ -4,18 +4,21 @@ namespace AIStudio.Tools; public static class JsonSerializerOptionsExtensions { - /// - /// Makes the options read-only and returns them, so that a static field gets its options in one expression. - /// - /// - /// Options with contracts of their own, i.e., with a TypeInfoResolver, should never change after they were - /// declared: whoever used them before such a change would have read or written with other contracts. - /// /// The options, with their TypeInfoResolver set. - /// The same options, now read-only. - public static JsonSerializerOptions AsReadOnly(this JsonSerializerOptions options) + extension(JsonSerializerOptions options) { - options.MakeReadOnly(); - return options; + /// + /// Makes the options read-only and returns them, so that a static field gets its options in one expression. + /// + /// + /// Options with contracts of their own, i.e., with a TypeInfoResolver, should never change after they were + /// declared: whoever used them before such a change would have read or written with other contracts. + /// + /// The same options, now read-only. + public JsonSerializerOptions AsReadOnly() + { + options.MakeReadOnly(); + return options; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/LongExtensions.cs b/app/MindWork AI Studio/Tools/LongExtensions.cs index a06b91bb2..3f9d10331 100644 --- a/app/MindWork AI Studio/Tools/LongExtensions.cs +++ b/app/MindWork AI Studio/Tools/LongExtensions.cs @@ -8,67 +8,75 @@ public static class LongExtensions private static readonly string[] SIZE_UNITS = ["B", "kB", "MB", "GB", "TB"]; - /// - /// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M. - /// - /// - /// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage - /// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the - /// difference between 4 and 999. The decimal separator follows the culture of the active language - /// plugin rather than the one of the thread, which never moves along with the app's language. - /// /// The number to format. - /// The formatted number. - public static string CompactCount(this long count) + extension(long count) { - var culture = I18N.I.Culture; - if (count is > -1_000 and < 1_000) - return count.ToString("N0", culture); - - var order = -1; - double value = count; - while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1) + /// + /// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M. + /// + /// + /// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage + /// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the + /// difference between 4 and 999. The decimal separator follows the culture of the active language + /// plugin rather than the one of the thread, which never moves along with the app's language. + /// + /// The formatted number. + public string CompactCount() { - order++; - value /= 1_000; - } + var culture = I18N.I.Culture; + if (count is > -1_000 and < 1_000) + return count.ToString("N0", culture); - return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}"; + var order = -1; + double value = count; + while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1) + { + order++; + value /= 1_000; + } + + return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}"; + } } - /// - /// Formats a count so that large numbers stay readable. - /// /// The number to format. - /// The formatted number. - public static string CompactCount(this int count) => ((long)count).CompactCount(); - - /// - /// Formats the file size in a human-readable format, e.g., 1.9 MB. - /// - /// - /// Like CompactCount, the decimal separator follows the culture of the active language plugin. - /// - /// The size in bytes. - /// The formatted file size. - public static string FileSize(this long sizeBytes) => sizeBytes.FileSize(I18N.I.Culture); - - /// - /// Formats the file size in a human-readable format, with the decimal separator of the given culture. - /// - /// The size in bytes. - /// The culture whose decimal separator to use. - /// The formatted file size. - internal static string FileSize(this long sizeBytes, IFormatProvider culture) + extension(int count) { - var order = 0; - double size = sizeBytes; - while (size >= 1024 && order < SIZE_UNITS.Length - 1) - { - order++; - size /= 1024; - } + /// + /// Formats a count so that large numbers stay readable. + /// + /// The formatted number. + public string CompactCount() => ((long)count).CompactCount(); + } - return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}"; + /// The size in bytes. + extension(long sizeBytes) + { + /// + /// Formats the file size in a human-readable format, e.g., 1.9 MB. + /// + /// + /// Like CompactCount, the decimal separator follows the culture of the active language plugin. + /// + /// The formatted file size. + public string FileSize() => sizeBytes.FileSize(I18N.I.Culture); + + /// + /// Formats the file size in a human-readable format, with the decimal separator of the given culture. + /// + /// The culture whose decimal separator to use. + /// The formatted file size. + internal string FileSize(IFormatProvider culture) + { + var order = 0; + double size = sizeBytes; + while (size >= 1024 && order < SIZE_UNITS.Length - 1) + { + order++; + size /= 1024; + } + + return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}"; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/MIMETypeExtensions.cs b/app/MindWork AI Studio/Tools/MIME/MIMETypeExtensions.cs index cfcd9053d..f6aa1e084 100644 --- a/app/MindWork AI Studio/Tools/MIME/MIMETypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/MIME/MIMETypeExtensions.cs @@ -2,14 +2,17 @@ namespace AIStudio.Tools.MIME; public static class MIMETypeExtensions { - public static string[] ToStringArray(this MIMEType[] mimeTypes) + extension(MIMEType[] mimeTypes) { - var result = new string[mimeTypes.Length]; - for (var i = 0; i < mimeTypes.Length; i++) + public string[] ToStringArray() { - result[i] = mimeTypes[i]; - } + var result = new string[mimeTypes.Length]; + for (var i = 0; i < mimeTypes.Length; i++) + { + result[i] = mimeTypes[i]; + } - return result; + return result; + } } } diff --git a/app/MindWork AI Studio/Tools/Mail/MailboxConnectionFailureExtensions.cs b/app/MindWork AI Studio/Tools/Mail/MailboxConnectionFailureExtensions.cs index 892297f35..acb19f578 100644 --- a/app/MindWork AI Studio/Tools/Mail/MailboxConnectionFailureExtensions.cs +++ b/app/MindWork AI Studio/Tools/Mail/MailboxConnectionFailureExtensions.cs @@ -6,18 +6,21 @@ public static class MailboxConnectionFailureExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxConnectionFailureExtensions).Namespace, nameof(MailboxConnectionFailureExtensions)); - /// - /// What went wrong, and what the user can do about it. - /// - public static string GetDescription(this MailboxConnectionFailure failure) => failure switch + extension(MailboxConnectionFailure failure) { - MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."), - MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."), - MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."), - MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."), - MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."), - MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."), + /// + /// What went wrong, and what the user can do about it. + /// + public string GetDescription() => failure switch + { + MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."), + MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."), + MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."), + MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."), + MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."), + MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."), - _ => TB("The connection to the server failed for an unknown reason."), - }; + _ => TB("The connection to the server failed for an unknown reason."), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Mail/MailboxProviderTemplateExtensions.cs b/app/MindWork AI Studio/Tools/Mail/MailboxProviderTemplateExtensions.cs index 591440c93..423c62cbf 100644 --- a/app/MindWork AI Studio/Tools/Mail/MailboxProviderTemplateExtensions.cs +++ b/app/MindWork AI Studio/Tools/Mail/MailboxProviderTemplateExtensions.cs @@ -6,33 +6,39 @@ public static class MailboxProviderTemplateExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxProviderTemplateExtensions).Namespace, nameof(MailboxProviderTemplateExtensions)); - /// - /// What the user enters as the username at this provider. - /// - public static string GetDescription(this MailboxUsernameFormat format) => format switch + extension(MailboxUsernameFormat format) { - MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."), - MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."), - MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"), + /// + /// What the user enters as the username at this provider. + /// + public string GetDescription() => format switch + { + MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."), + MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."), + MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"), - _ => TB("The username your provider gave you."), - }; + _ => TB("The username your provider gave you."), + }; + } - /// - /// What the user has to do before AI Studio can sign in, one text per requirement. - /// - public static IEnumerable GetDescriptions(this MailboxProviderRequirements requirements) + extension(MailboxProviderRequirements requirements) { - if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION)) - yield return TB("Enable the IMAP access in the settings of your webmail first. The provider may switch it off again after a longer time without use."); + /// + /// What the user has to do before AI Studio can sign in, one text per requirement. + /// + public IEnumerable GetDescriptions() + { + if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION)) + yield return TB("Enable the IMAP access in the settings of your webmail first. The provider may switch it off again after a longer time without use."); - if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION)) - yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first."); + if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION)) + yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first."); - if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD)) - yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here."); + if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD)) + yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here."); - if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR)) - yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account."); + if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR)) + yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account."); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Media/MediaImportOwnerKindExtensions.cs b/app/MindWork AI Studio/Tools/Media/MediaImportOwnerKindExtensions.cs index 44ce04542..84af128ee 100644 --- a/app/MindWork AI Studio/Tools/Media/MediaImportOwnerKindExtensions.cs +++ b/app/MindWork AI Studio/Tools/Media/MediaImportOwnerKindExtensions.cs @@ -3,17 +3,20 @@ namespace AIStudio.Tools.Media; /// Capabilities of a media-import owner kind. public static class MediaImportOwnerKindExtensions { - /// - /// Gets whether the owner stores its own source list and transcripts. - /// - /// - /// Owners that persist their own sources take the attached media over immediately and keep it - /// next to the stored document, see . The - /// attachment control must therefore neither wait for the transcription to finish before showing - /// the file, nor deliver the completed transcripts back into its own list afterwards, because - /// the owner already holds them. All other owners rely on that delivery instead. - /// /// The owner kind to look up. - /// true when the owner persists its own sources. - public static bool PersistsOwnSources(this MediaImportOwnerKind kind) => kind is MediaImportOwnerKind.VISUAL_BRIEFING; + extension(MediaImportOwnerKind kind) + { + /// + /// Gets whether the owner stores its own source list and transcripts. + /// + /// + /// Owners that persist their own sources take the attached media over immediately and keep it + /// next to the stored document, see . The + /// attachment control must therefore neither wait for the transcription to finish before showing + /// the file, nor deliver the completed transcripts back into its own list afterwards, because + /// the owner already holds them. All other owners rely on that delivery instead. + /// + /// true when the owner persists its own sources. + public bool PersistsOwnSources() => kind is MediaImportOwnerKind.VISUAL_BRIEFING; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MessageBusExtensions.cs b/app/MindWork AI Studio/Tools/MessageBusExtensions.cs index 36d8b71e1..897ea9419 100644 --- a/app/MindWork AI Studio/Tools/MessageBusExtensions.cs +++ b/app/MindWork AI Studio/Tools/MessageBusExtensions.cs @@ -4,13 +4,19 @@ namespace AIStudio.Tools; public static class MessageBusExtensions { - public static async Task SendMessage(this ComponentBase component, Event triggeredEvent, T? data = default) + extension(ComponentBase component) { - await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data); + public async Task SendMessage(Event triggeredEvent, T? data = default) + { + await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data); + } } - public static void ApplyFilters(this IMessageBusReceiver component, ComponentBase[] components, Event[] events) + extension(IMessageBusReceiver component) { - MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet()); + public void ApplyFilters(ComponentBase[] components, Event[] events) + { + MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet()); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MudThemeExtensions.cs b/app/MindWork AI Studio/Tools/MudThemeExtensions.cs index 6f70b1050..43d964470 100644 --- a/app/MindWork AI Studio/Tools/MudThemeExtensions.cs +++ b/app/MindWork AI Studio/Tools/MudThemeExtensions.cs @@ -4,19 +4,22 @@ namespace AIStudio.Tools; public static class MudThemeExtensions { - public static Palette GetCurrentPalette(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch + extension(MudTheme theme) { - true => theme.PaletteDark, - false => theme.PaletteLight, - }; + public Palette GetCurrentPalette(SettingsManager settingsManager) => settingsManager.IsDarkMode switch + { + true => theme.PaletteDark, + false => theme.PaletteLight, + }; - public static string GetActivityIndicatorColor(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch - { - true => theme.GetActivityIndicatorDarkColor(), - false => theme.GetActivityIndicatorLightColor(), - }; + public string GetActivityIndicatorColor(SettingsManager settingsManager) => settingsManager.IsDarkMode switch + { + true => theme.GetActivityIndicatorDarkColor(), + false => theme.GetActivityIndicatorLightColor(), + }; - public static string GetActivityIndicatorLightColor(this MudTheme theme) => theme.PaletteLight.Info.Value; + public string GetActivityIndicatorLightColor() => theme.PaletteLight.Info.Value; - public static string GetActivityIndicatorDarkColor(this MudTheme theme) => theme.PaletteDark.InfoLighten; + public string GetActivityIndicatorDarkColor() => theme.PaletteDark.InfoLighten; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentTypeExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentTypeExtensions.cs index 187eb7578..58fe09d35 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentTypeExtensions.cs @@ -4,63 +4,66 @@ public static class AssistantComponentTypeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantComponentTypeExtensions).Namespace, nameof(AssistantComponentTypeExtensions)); - public static string GetDisplayName(this AssistantComponentType type) => type switch + extension(AssistantComponentType type) { - AssistantComponentType.FORM => TB("Root"), - AssistantComponentType.TEXT_AREA => TB("Text Area"), - AssistantComponentType.BUTTON => TB("Button"), - AssistantComponentType.BUTTON_GROUP => TB("Button group"), - AssistantComponentType.DROPDOWN => TB("Dropdown"), - AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"), - AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"), - AssistantComponentType.SWITCH => TB("Switch"), - AssistantComponentType.HEADING => TB("Heading"), - AssistantComponentType.TEXT => TB("Text"), - AssistantComponentType.LIST => TB("List"), - AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"), - AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"), - AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"), - AssistantComponentType.IMAGE => TB("Image"), - AssistantComponentType.COLOR_PICKER => TB("Color Selection"), - AssistantComponentType.DATE_PICKER => TB("Date Selection"), - AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"), - AssistantComponentType.TIME_PICKER => TB("Time Selection"), - AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"), - AssistantComponentType.LAYOUT_GRID => TB("Grid"), - AssistantComponentType.LAYOUT_PAPER => TB("Container"), - AssistantComponentType.LAYOUT_STACK => TB("Stack"), - AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"), - AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"), - _ => TB("Unknown Element") - }; - - public static string GetIcon(this AssistantComponentType type) => type switch - { - AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick, - AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale, - AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule, - AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory, - AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge, - AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn, - AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title, - AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields, - AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg, - AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List, - AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public, - AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile, - AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile, - AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image, - AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette, - AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth, - AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange, - AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule, - AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize, - AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView, - AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox, - AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers, - AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay, - AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit, - AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree, - _ => MudBlazor.Icons.Material.Filled.AccountTree, - }; + public string GetDisplayName() => type switch + { + AssistantComponentType.FORM => TB("Root"), + AssistantComponentType.TEXT_AREA => TB("Text Area"), + AssistantComponentType.BUTTON => TB("Button"), + AssistantComponentType.BUTTON_GROUP => TB("Button group"), + AssistantComponentType.DROPDOWN => TB("Dropdown"), + AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"), + AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"), + AssistantComponentType.SWITCH => TB("Switch"), + AssistantComponentType.HEADING => TB("Heading"), + AssistantComponentType.TEXT => TB("Text"), + AssistantComponentType.LIST => TB("List"), + AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"), + AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"), + AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"), + AssistantComponentType.IMAGE => TB("Image"), + AssistantComponentType.COLOR_PICKER => TB("Color Selection"), + AssistantComponentType.DATE_PICKER => TB("Date Selection"), + AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"), + AssistantComponentType.TIME_PICKER => TB("Time Selection"), + AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"), + AssistantComponentType.LAYOUT_GRID => TB("Grid"), + AssistantComponentType.LAYOUT_PAPER => TB("Container"), + AssistantComponentType.LAYOUT_STACK => TB("Stack"), + AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"), + AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"), + _ => TB("Unknown Element") + }; + + public string GetIcon() => type switch + { + AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick, + AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale, + AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule, + AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory, + AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge, + AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn, + AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title, + AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields, + AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg, + AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List, + AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public, + AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile, + AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile, + AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image, + AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette, + AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth, + AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange, + AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule, + AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize, + AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView, + AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox, + AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers, + AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay, + AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit, + AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree, + _ => MudBlazor.Icons.Material.Filled.AccountTree, + }; + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILangExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILangExtensions.cs index 61abbf427..515a1e9fb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/ILangExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/ILangExtensions.cs @@ -6,31 +6,34 @@ public static class ILangExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - public static string GetText(this ILang lang, ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null) + extension(ILang lang) { - var type = lang.GetType(); - typeName ??= type.Name; - typeNamespace ??= type.Namespace!; - - // When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it: - if(typeName.Contains('`')) - typeName = typeName[..typeName.IndexOf('`')]; - - var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::"); - var key = $"root::{ns}::T{fallbackEN.ToFNV32()}"; - - if(plugin is NoPluginLanguage) - return fallbackEN; - - if(plugin.TryGetText(key, out var text, logWarning: false)) + public string GetText(ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null) { - if(string.IsNullOrWhiteSpace(text)) + var type = lang.GetType(); + typeName ??= type.Name; + typeNamespace ??= type.Namespace!; + + // When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it: + if(typeName.Contains('`')) + typeName = typeName[..typeName.IndexOf('`')]; + + var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::"); + var key = $"root::{ns}::T{fallbackEN.ToFNV32()}"; + + if(plugin is NoPluginLanguage) return fallbackEN; + + if(plugin.TryGetText(key, out var text, logWarning: false)) + { + if(string.IsNullOrWhiteSpace(text)) + return fallbackEN; - return text; - } + return text; + } - LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'."); - return fallbackEN; + LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'."); + return fallbackEN; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs index 3a25aa189..faba8edd6 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs @@ -2,11 +2,14 @@ namespace AIStudio.Tools.PluginSystem; public static class InternalPluginExtensions { - public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch + extension(InternalPlugin plugin) { - 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"), + public InternalPluginData MetaData() => 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") - }; + _ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown") + }; + } } \ 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 index ef8dd37dc..755c73b34 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs @@ -4,37 +4,40 @@ public static class PluginCategoryExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginCategoryExtensions).Namespace, nameof(PluginCategoryExtensions)); - public static string GetName(this PluginCategory type) => type switch + extension(PluginCategory type) { - PluginCategory.NONE => TB("None"), - PluginCategory.CORE => TB("AI Studio Core"), + public string GetName() => type switch + { + PluginCategory.NONE => TB("None"), + PluginCategory.CORE => TB("AI Studio Core"), - PluginCategory.BUSINESS => TB("Business"), - PluginCategory.INDUSTRY => TB("Industry"), - PluginCategory.UTILITY => TB("Utility"), - PluginCategory.SOFTWARE_DEVELOPMENT => TB("Software Development"), - PluginCategory.GAMING => TB("Gaming"), - PluginCategory.EDUCATION => TB("Education"), - PluginCategory.ENTERTAINMENT => TB("Entertainment"), - PluginCategory.SOCIAL => TB("Social"), - PluginCategory.SHOPPING => TB("Shopping"), - PluginCategory.TRAVEL => TB("Travel"), - PluginCategory.HEALTH => TB("Health"), - PluginCategory.FITNESS => TB("Fitness"), - PluginCategory.FOOD => TB("Food"), - PluginCategory.PARTY => TB("Party"), - PluginCategory.SPORTS => TB("Sports"), - PluginCategory.NEWS => TB("News"), - PluginCategory.WEATHER => TB("Weather"), - PluginCategory.MUSIC => TB("Music"), - PluginCategory.POLITICAL => TB("Political"), - PluginCategory.SCIENCE => TB("Science"), - PluginCategory.TECHNOLOGY => TB("Technology"), - PluginCategory.ART => TB("Art"), - PluginCategory.FICTION => TB("Fiction"), - PluginCategory.WRITING => TB("Writing"), - PluginCategory.CONTENT_CREATION => TB("Content Creation"), + PluginCategory.BUSINESS => TB("Business"), + PluginCategory.INDUSTRY => TB("Industry"), + PluginCategory.UTILITY => TB("Utility"), + PluginCategory.SOFTWARE_DEVELOPMENT => TB("Software Development"), + PluginCategory.GAMING => TB("Gaming"), + PluginCategory.EDUCATION => TB("Education"), + PluginCategory.ENTERTAINMENT => TB("Entertainment"), + PluginCategory.SOCIAL => TB("Social"), + PluginCategory.SHOPPING => TB("Shopping"), + PluginCategory.TRAVEL => TB("Travel"), + PluginCategory.HEALTH => TB("Health"), + PluginCategory.FITNESS => TB("Fitness"), + PluginCategory.FOOD => TB("Food"), + PluginCategory.PARTY => TB("Party"), + PluginCategory.SPORTS => TB("Sports"), + PluginCategory.NEWS => TB("News"), + PluginCategory.WEATHER => TB("Weather"), + PluginCategory.MUSIC => TB("Music"), + PluginCategory.POLITICAL => TB("Political"), + PluginCategory.SCIENCE => TB("Science"), + PluginCategory.TECHNOLOGY => TB("Technology"), + PluginCategory.ART => TB("Art"), + PluginCategory.FICTION => TB("Fiction"), + PluginCategory.WRITING => TB("Writing"), + PluginCategory.CONTENT_CREATION => TB("Content Creation"), - _ => TB("Unknown plugin category"), - }; + _ => TB("Unknown plugin category"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectTypeExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectTypeExtensions.cs index 7a94e1eb3..8804e72fb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectTypeExtensions.cs @@ -4,19 +4,22 @@ public static class PluginConfigurationObjectTypeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfigurationObjectTypeExtensions).Namespace, nameof(PluginConfigurationObjectTypeExtensions)); - /// - /// The name of a type of configuration object, as users read it, e.g., in a message. - /// - public static string GetName(this PluginConfigurationObjectType type) => type switch + extension(PluginConfigurationObjectType type) { - PluginConfigurationObjectType.LLM_PROVIDER => TB("LLM Provider"), - PluginConfigurationObjectType.EMBEDDING_PROVIDER => TB("Embedding Provider"), - PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => TB("Transcription Provider"), - PluginConfigurationObjectType.PROFILE => TB("Profile"), - PluginConfigurationObjectType.CHAT_TEMPLATE => TB("Chat Template"), - PluginConfigurationObjectType.DATA_SOURCE => TB("Data Source"), - PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => TB("Document Analysis Policy"), + /// + /// The name of a type of configuration object, as users read it, e.g., in a message. + /// + public string GetName() => type switch + { + PluginConfigurationObjectType.LLM_PROVIDER => TB("LLM Provider"), + PluginConfigurationObjectType.EMBEDDING_PROVIDER => TB("Embedding Provider"), + PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => TB("Transcription Provider"), + PluginConfigurationObjectType.PROFILE => TB("Profile"), + PluginConfigurationObjectType.CHAT_TEMPLATE => TB("Chat Template"), + PluginConfigurationObjectType.DATA_SOURCE => TB("Data Source"), + PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => TB("Document Analysis Policy"), - _ => TB("Unknown configuration"), - }; + _ => TB("Unknown configuration"), + }; + } } \ 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 index 7e47a1301..5dfad0da0 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs @@ -4,24 +4,27 @@ public static class PluginTargetGroupExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginTargetGroupExtensions).Namespace, nameof(PluginTargetGroupExtensions)); - public static string Name(this PluginTargetGroup group) => group switch + extension(PluginTargetGroup group) { - PluginTargetGroup.NONE => TB("No target group"), + public string Name() => group switch + { + PluginTargetGroup.NONE => TB("No target group"), - PluginTargetGroup.EVERYONE => TB("Everyone"), - PluginTargetGroup.CHILDREN => TB("Children"), - PluginTargetGroup.TEENAGERS => TB("Teenagers"), - PluginTargetGroup.STUDENTS => TB("Students"), - PluginTargetGroup.ADULTS => TB("Adults"), + PluginTargetGroup.EVERYONE => TB("Everyone"), + PluginTargetGroup.CHILDREN => TB("Children"), + PluginTargetGroup.TEENAGERS => TB("Teenagers"), + PluginTargetGroup.STUDENTS => TB("Students"), + PluginTargetGroup.ADULTS => TB("Adults"), - PluginTargetGroup.INDUSTRIAL_WORKERS => TB("Industrial workers"), - PluginTargetGroup.OFFICE_WORKERS => TB("Office workers"), - PluginTargetGroup.BUSINESS_PROFESSIONALS => TB("Business professionals"), - PluginTargetGroup.SOFTWARE_DEVELOPERS => TB("Software developers"), - PluginTargetGroup.SCIENTISTS => TB("Scientists"), - PluginTargetGroup.TEACHERS => TB("Teachers"), - PluginTargetGroup.ARTISTS => TB("Artists"), + PluginTargetGroup.INDUSTRIAL_WORKERS => TB("Industrial workers"), + PluginTargetGroup.OFFICE_WORKERS => TB("Office workers"), + PluginTargetGroup.BUSINESS_PROFESSIONALS => TB("Business professionals"), + PluginTargetGroup.SOFTWARE_DEVELOPERS => TB("Software developers"), + PluginTargetGroup.SCIENTISTS => TB("Scientists"), + PluginTargetGroup.TEACHERS => TB("Teachers"), + PluginTargetGroup.ARTISTS => TB("Artists"), - _ => TB("Unknown target group"), - }; + _ => TB("Unknown target group"), + }; + } } \ 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 index 6b7d21043..15c28a219 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs @@ -4,25 +4,28 @@ public static class PluginTypeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginTypeExtensions).Namespace, nameof(PluginTypeExtensions)); - public static string GetName(this PluginType type) => type switch + extension(PluginType type) { - PluginType.LANGUAGE => TB("Language plugin"), - PluginType.ASSISTANT => TB("Assistant plugin"), - PluginType.CONFIGURATION => TB("Configuration plugin"), - PluginType.THEME => TB("Theme plugin"), - PluginType.MODEL => TB("Model plugin"), + public string GetName() => type switch + { + PluginType.LANGUAGE => TB("Language plugin"), + PluginType.ASSISTANT => TB("Assistant plugin"), + PluginType.CONFIGURATION => TB("Configuration plugin"), + PluginType.THEME => TB("Theme plugin"), + PluginType.MODEL => TB("Model plugin"), - _ => TB("Unknown plugin type"), - }; - - public static string GetDirectory(this PluginType type) => type switch - { - PluginType.LANGUAGE => "languages", - PluginType.ASSISTANT => "assistants", - PluginType.CONFIGURATION => "configurations", - PluginType.THEME => "themes", - PluginType.MODEL => "models", + _ => TB("Unknown plugin type"), + }; - _ => "unknown", - }; + public string GetDirectory() => type switch + { + PluginType.LANGUAGE => "languages", + PluginType.ASSISTANT => "assistants", + PluginType.CONFIGURATION => "configurations", + PluginType.THEME => "themes", + PluginType.MODEL => "models", + + _ => "unknown", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeAreaExtensions.cs b/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeAreaExtensions.cs index d424beaea..704dc10db 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeAreaExtensions.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeAreaExtensions.cs @@ -7,20 +7,23 @@ internal static class LayoutPurposeAreaExtensions { private static string TB(string fallbackEN) => PluginSystem.I18N.I.T(fallbackEN, typeof(LayoutPurposeAreaExtensions).Namespace, nameof(LayoutPurposeAreaExtensions)); - /// - /// The name of the area, as the forms show it above its kinds of slides. - /// /// The area. - public static string Label(this LayoutPurposeArea area) => area switch + extension(LayoutPurposeArea area) { - LayoutPurposeArea.FRAMING => TB("Structure and framing"), - LayoutPurposeArea.GENERAL => TB("General content"), - LayoutPurposeArea.TEACHING => TB("Teaching and workshops"), - LayoutPurposeArea.BUSINESS => TB("Business and projects"), - LayoutPurposeArea.SCIENCE => TB("Science and research"), - LayoutPurposeArea.MEDICINE => TB("Medicine"), - LayoutPurposeArea.LAW => TB("Law"), + /// + /// The name of the area, as the forms show it above its kinds of slides. + /// + public string Label() => area switch + { + LayoutPurposeArea.FRAMING => TB("Structure and framing"), + LayoutPurposeArea.GENERAL => TB("General content"), + LayoutPurposeArea.TEACHING => TB("Teaching and workshops"), + LayoutPurposeArea.BUSINESS => TB("Business and projects"), + LayoutPurposeArea.SCIENCE => TB("Science and research"), + LayoutPurposeArea.MEDICINE => TB("Medicine"), + LayoutPurposeArea.LAW => TB("Law"), - _ => TB("Other kinds of slides"), - }; + _ => TB("Other kinds of slides"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeExtensions.cs b/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeExtensions.cs index ce8d0877b..bc58fa642 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeExtensions.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/LayoutPurposeExtensions.cs @@ -16,66 +16,63 @@ internal static partial class LayoutPurposeExtensions /// public static string NoPreference => TB("No preference (the LLM decides)"); - /// - /// The area the forms list a kind of slide in. - /// /// The kind of slide. - public static LayoutPurposeArea Area(this LayoutPurpose purpose) => Describe(purpose).Area; - - /// - /// The short name of the choice, e.g., "Layout for chapters". - /// - /// The kind of slide. - public static string Label(this LayoutPurpose purpose) => Describe(purpose).Label; - - /// - /// The question the choice answers, which says which slides it applies to. - /// - /// The kind of slide. - public static string Question(this LayoutPurpose purpose) => Describe(purpose).Question; - - /// - /// Describes the slides of this kind for the model, so that it knows which of its slides a choice applies to. - /// - /// The kind of slide. - /// The description, e.g., "timelines, e.g., the course of a project". - public static string PromptDescription(this LayoutPurpose purpose) => Describe(purpose).PromptDescription; - - /// - /// Whether choosing a layout for this kind of slide also puts such a slide into the presentation. - /// - /// - /// For most kinds, a choice says only which layout such a slide uses, and whether the - /// presentation has one follows from its content. The slides which frame a presentation are - /// different: whoever chooses a layout for the table of contents or the imprint expects them. - /// - /// The kind of slide. - public static bool ChoiceAddsSlide(this LayoutPurpose purpose) => purpose.PromptPlacement() is not null; - - /// - /// Where a slide goes which a choice puts into the presentation, and what it holds, for the model. - /// - /// The kind of slide. - /// The rule, or null when a choice adds no slide of this kind. - public static string? PromptPlacement(this LayoutPurpose purpose) => purpose switch + extension(LayoutPurpose purpose) { - LayoutPurpose.TABLE_OF_CONTENTS => "right after the title slide", - LayoutPurpose.Q_AND_A => "near the end, before the closing slide; where the occasion suits it, e.g., in a lecture or a workshop, also at the end of chapters, then naming the topic of the chapter", - LayoutPurpose.ACKNOWLEDGEMENTS => "near the end; where thanks concern a single chapter, also at the end of that chapter", - LayoutPurpose.REFERENCES => "near the end; it lists only the sources the content names, or else the attached documents, and never a source of your own", - LayoutPurpose.CLOSING => "at the end, before an appendix and the imprint", - LayoutPurpose.IMPRINT => "as the very last slide; it holds only what the request, the content, or the profile state, e.g., the title, the authors, the organization, and today's date, and it leaves a slot empty rather than inventing anything", + /// + /// The area the forms list a kind of slide in. + /// + public LayoutPurposeArea Area() => Describe(purpose).Area; - _ => null, - }; + /// + /// The short name of the choice, e.g., "Layout for chapters". + /// + public string Label() => Describe(purpose).Label; - /// - /// The help below the choice: its question, and whether a choice adds such a slide. - /// - /// The kind of slide. - public static string Help(this LayoutPurpose purpose) => purpose.ChoiceAddsSlide() - ? $"{purpose.Question()} {TB("Choosing a layout also adds such a slide to the presentation.")}" - : purpose.Question(); + /// + /// The question the choice answers, which says which slides it applies to. + /// + public string Question() => Describe(purpose).Question; + + /// + /// Describes the slides of this kind for the model, so that it knows which of its slides a choice applies to. + /// + /// The description, e.g., "timelines, e.g., the course of a project". + public string PromptDescription() => Describe(purpose).PromptDescription; + + /// + /// Whether choosing a layout for this kind of slide also puts such a slide into the presentation. + /// + /// + /// For most kinds, a choice says only which layout such a slide uses, and whether the + /// presentation has one follows from its content. The slides which frame a presentation are + /// different: whoever chooses a layout for the table of contents or the imprint expects them. + /// + public bool ChoiceAddsSlide() => purpose.PromptPlacement() is not null; + + /// + /// Where a slide goes which a choice puts into the presentation, and what it holds, for the model. + /// + /// The rule, or null when a choice adds no slide of this kind. + public string? PromptPlacement() => purpose switch + { + LayoutPurpose.TABLE_OF_CONTENTS => "right after the title slide", + LayoutPurpose.Q_AND_A => "near the end, before the closing slide; where the occasion suits it, e.g., in a lecture or a workshop, also at the end of chapters, then naming the topic of the chapter", + LayoutPurpose.ACKNOWLEDGEMENTS => "near the end; where thanks concern a single chapter, also at the end of that chapter", + LayoutPurpose.REFERENCES => "near the end; it lists only the sources the content names, or else the attached documents, and never a source of your own", + LayoutPurpose.CLOSING => "at the end, before an appendix and the imprint", + LayoutPurpose.IMPRINT => "as the very last slide; it holds only what the request, the content, or the profile state, e.g., the title, the authors, the organization, and today's date, and it leaves a slot empty rather than inventing anything", + + _ => null, + }; + + /// + /// The help below the choice: its question, and whether a choice adds such a slide. + /// + public string Help() => purpose.ChoiceAddsSlide() + ? $"{purpose.Question()} {TB("Choosing a layout also adds such a slide to the presentation.")}" + : purpose.Question(); + } private static LayoutPurposeInfo Describe(LayoutPurpose purpose) => DescribeFraming(purpose) diff --git a/app/MindWork AI Studio/Tools/PowerPoint/PlaceholderRoleExtensions.cs b/app/MindWork AI Studio/Tools/PowerPoint/PlaceholderRoleExtensions.cs index 727c6f198..1e6b75edb 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/PlaceholderRoleExtensions.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/PlaceholderRoleExtensions.cs @@ -42,54 +42,57 @@ internal static class PlaceholderRoleExtensions return false; } - /// - /// Whether content of the presentation goes into a placeholder of this role. - /// - /// - /// Footer, slide number and date are filled as well, but by the presentation as a whole rather - /// than by a single slide, so they are no slots of a layout. - /// - public static bool IsSlot(this PlaceholderRole role) => role switch + extension(PlaceholderRole role) { - PlaceholderRole.TITLE => true, - PlaceholderRole.SUBTITLE => true, - PlaceholderRole.TEXT => true, - PlaceholderRole.LABEL => true, - PlaceholderRole.PICTURE => true, - PlaceholderRole.TABLE => true, + /// + /// Whether content of the presentation goes into a placeholder of this role. + /// + /// + /// Footer, slide number and date are filled as well, but by the presentation as a whole rather + /// than by a single slide, so they are no slots of a layout. + /// + public bool IsSlot() => role switch + { + PlaceholderRole.TITLE => true, + PlaceholderRole.SUBTITLE => true, + PlaceholderRole.TEXT => true, + PlaceholderRole.LABEL => true, + PlaceholderRole.PICTURE => true, + PlaceholderRole.TABLE => true, - _ => false, - }; + _ => false, + }; - /// - /// Whether a placeholder of this role carries the content of a slide, rather than describing it. - /// - /// - /// A layout offering nothing but a title and some labels has no room for content. Such layouts are - /// samples or blank pages, and they are left out of the catalog. - /// - public static bool CarriesContent(this PlaceholderRole role) => role switch - { - PlaceholderRole.SUBTITLE => true, - PlaceholderRole.TEXT => true, - PlaceholderRole.PICTURE => true, - PlaceholderRole.TABLE => true, + /// + /// Whether a placeholder of this role carries the content of a slide, rather than describing it. + /// + /// + /// A layout offering nothing but a title and some labels has no room for content. Such layouts are + /// samples or blank pages, and they are left out of the catalog. + /// + public bool CarriesContent() => role switch + { + PlaceholderRole.SUBTITLE => true, + PlaceholderRole.TEXT => true, + PlaceholderRole.PICTURE => true, + PlaceholderRole.TABLE => true, - _ => false, - }; + _ => false, + }; - /// - /// How the role is called in the catalog the model reads. - /// - public static string CatalogName(this PlaceholderRole role) => role switch - { - PlaceholderRole.TITLE => "title", - PlaceholderRole.SUBTITLE => "subtitle", - PlaceholderRole.TEXT => "text", - PlaceholderRole.LABEL => "label", - PlaceholderRole.PICTURE => "picture", - PlaceholderRole.TABLE => "table", + /// + /// How the role is called in the catalog the model reads. + /// + public string CatalogName() => role switch + { + PlaceholderRole.TITLE => "title", + PlaceholderRole.SUBTITLE => "subtitle", + PlaceholderRole.TEXT => "text", + PlaceholderRole.LABEL => "label", + PlaceholderRole.PICTURE => "picture", + PlaceholderRole.TABLE => "table", - _ => "unsupported", - }; + _ => "unsupported", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideElementKindExtensions.cs b/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideElementKindExtensions.cs index f814e85cc..51c0b7ddb 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideElementKindExtensions.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideElementKindExtensions.cs @@ -2,20 +2,23 @@ namespace AIStudio.Tools.PowerPoint; internal static class TemplateSlideElementKindExtensions { - /// - /// How the kind is called in the list of template slides the model reads. - /// - public static string CatalogName(this TemplateSlideElementKind kind) => kind switch + extension(TemplateSlideElementKind kind) { - TemplateSlideElementKind.TEXT_BOX => "text box", - TemplateSlideElementKind.SHAPE => "shape", - TemplateSlideElementKind.LINE => "line", - TemplateSlideElementKind.PICTURE => "picture", - TemplateSlideElementKind.TABLE => "table", - TemplateSlideElementKind.CHART => "chart", - TemplateSlideElementKind.DIAGRAM => "diagram", - TemplateSlideElementKind.GROUP => "group", + /// + /// How the kind is called in the list of template slides the model reads. + /// + public string CatalogName() => kind switch + { + TemplateSlideElementKind.TEXT_BOX => "text box", + TemplateSlideElementKind.SHAPE => "shape", + TemplateSlideElementKind.LINE => "line", + TemplateSlideElementKind.PICTURE => "picture", + TemplateSlideElementKind.TABLE => "table", + TemplateSlideElementKind.CHART => "chart", + TemplateSlideElementKind.DIAGRAM => "diagram", + TemplateSlideElementKind.GROUP => "group", - _ => "object", - }; + _ => "object", + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideUseListExtensions.cs b/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideUseListExtensions.cs index d7596b30c..e0e3caae2 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideUseListExtensions.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/TemplateSlideUseListExtensions.cs @@ -2,27 +2,33 @@ namespace AIStudio.Tools.PowerPoint; internal static class TemplateSlideUseListExtensions { - /// - /// What the user chose for one slide of the template. - /// /// What the user chose for each slide of the template, by its number. - /// The number of the slide, starting at 1. - /// The choice; TemplateSlideUse.OMIT for a slide the list does not cover. - public static TemplateSlideUse UseOf(this IReadOnlyList uses, int number) => number >= 1 && number <= uses.Count ? uses[number - 1] : TemplateSlideUse.OMIT; + extension(IReadOnlyList uses) + { + /// + /// What the user chose for one slide of the template. + /// + /// The number of the slide, starting at 1. + /// The choice; TemplateSlideUse.OMIT for a slide the list does not cover. + public TemplateSlideUse UseOf(int number) => number >= 1 && number <= uses.Count ? uses[number - 1] : TemplateSlideUse.OMIT; + } - /// - /// What the presentation does with each slide of a template, by the choices for some of its slides. - /// - /// - /// A slide without a choice is left out, as by default. A choice to edit a slide which the model - /// cannot edit keeps the slide unchanged instead: such a choice may come from an organization, - /// which the user cannot overrule, and it would block the form. A choice for a slide the template - /// does not have is passed over. - /// /// The choices by the number of the slide, starting at 1. - /// The template whose slides the choices number. - /// A choice for each slide of the template, in its order. - public static List ToTemplateSlideUses(this IReadOnlyDictionary choices, PresentationTemplate template) => template.Slides - .Select(slide => choices.TryGetValue(slide.Number, out var use) ? use is TemplateSlideUse.EDIT && !slide.IsEditable ? TemplateSlideUse.KEEP : use : TemplateSlideUse.OMIT) - .ToList(); + extension(IReadOnlyDictionary choices) + { + /// + /// What the presentation does with each slide of a template, by the choices for some of its slides. + /// + /// + /// A slide without a choice is left out, as by default. A choice to edit a slide which the model + /// cannot edit keeps the slide unchanged instead: such a choice may come from an organization, + /// which the user cannot overrule, and it would block the form. A choice for a slide the template + /// does not have is passed over. + /// + /// The template whose slides the choices number. + /// A choice for each slide of the template, in its order. + public List ToTemplateSlideUses(PresentationTemplate template) => template.Slides + .Select(slide => choices.TryGetValue(slide.Number, out var use) ? use is TemplateSlideUse.EDIT && !slide.IsEditable ? TemplateSlideUse.KEEP : use : TemplateSlideUse.OMIT) + .ToList(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ProfileExtensions.cs b/app/MindWork AI Studio/Tools/ProfileExtensions.cs index 53cb25626..57a11354a 100644 --- a/app/MindWork AI Studio/Tools/ProfileExtensions.cs +++ b/app/MindWork AI Studio/Tools/ProfileExtensions.cs @@ -4,10 +4,13 @@ namespace AIStudio.Tools; public static class ProfileExtensions { - public static IEnumerable GetAllProfiles(this IEnumerable profiles) + extension(IEnumerable profiles) { - yield return Profile.NO_PROFILE; - foreach (var profile in profiles) - yield return profile; + public IEnumerable GetAllProfiles() + { + yield return Profile.NO_PROFILE; + foreach (var profile in profiles) + yield return profile; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs b/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs index 3862e2f93..72a1934d1 100644 --- a/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs @@ -90,85 +90,88 @@ public static class IRetrievalContextExtensions contextBuilder.AppendLine("Matched image content: an image matched here, but it is not shown to you. Do not guess what it shows. When it matters for the answer, tell the user that an image matched, and where to find it, using the path and the links above."); } - /// - /// Prepares retrieval contexts for the AI: filters them for prompt injections, writes out what - /// the AI is told about each, and keeps what the user is shown of it. - /// - /// - /// A data source is as untrusted as any other external content: it may serve text written to - /// steer the model rather than to answer the query. So everything a data source wrote passes - /// the filter: the description of each context, its matched text, and each part of its - /// surrounding content. What the app writes around them, such as the labels and the fences, - /// does not need to.

- /// All contexts go to the runtime in one request, which also gives the user one report for all - /// of them. The passages are made from the filtered texts, so the user reads what the AI read. - ///
/// The contexts to prepare. - /// The prompt injection filter. - /// The action the contexts are retrieved for, or null when they are retrieved for none. - /// The cancellation token. - /// One prepared context per context, in the order they came in. - public static async Task> PrepareForModelAsync(this IReadOnlyList retrievalContexts, PromptInjectionGuardService guardService, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default) + extension(IReadOnlyList retrievalContexts) { - if (retrievalContexts.Count is 0) - return []; - - // - // The texts of all contexts in one list, each context with its description first, then its - // matched text, then its surrounding content. The answer comes back in the same order, and - // is read back below in the same way: - // - var texts = new List(); - foreach (var retrievalContext in retrievalContexts) + /// + /// Prepares retrieval contexts for the AI: filters them for prompt injections, writes out what + /// the AI is told about each, and keeps what the user is shown of it. + /// + /// + /// A data source is as untrusted as any other external content: it may serve text written to + /// steer the model rather than to answer the query. So everything a data source wrote passes + /// the filter: the description of each context, its matched text, and each part of its + /// surrounding content. What the app writes around them, such as the labels and the fences, + /// does not need to.

+ /// All contexts go to the runtime in one request, which also gives the user one report for all + /// of them. The passages are made from the filtered texts, so the user reads what the AI read. + ///
+ /// The prompt injection filter. + /// The action the contexts are retrieved for, or null when they are retrieved for none. + /// The cancellation token. + /// One prepared context per context, in the order they came in. + public async Task> PrepareForModelAsync(PromptInjectionGuardService guardService, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default) { - var source = PromptInjectionSource.RetrievalContext(retrievalContext.DataSourceName, retrievalContext.Path); - var description = new StringBuilder(); - AppendContextDescription(description, retrievalContext); - texts.Add(new(description.ToString(), source)); + if (retrievalContexts.Count is 0) + return []; - if (retrievalContext is not RetrievalTextContext textContext) - continue; - - texts.Add(new(textContext.MatchedText, source)); - foreach (var surrounding in textContext.SurroundingContent) - texts.Add(new(surrounding, source)); - } - - token.ThrowIfCancellationRequested(); - var sanitizedTexts = await guardService.SanitizeAsync(texts, reportingScope); - - var preparedContexts = new List(retrievalContexts.Count); - var nextText = 0; - foreach (var retrievalContext in retrievalContexts) - { - var body = new StringBuilder(); - body.Append(sanitizedTexts[nextText++]); - - var matchedText = string.Empty; - IReadOnlyList surroundingContent = []; - switch (retrievalContext) + // + // The texts of all contexts in one list, each context with its description first, then its + // matched text, then its surrounding content. The answer comes back in the same order, and + // is read back below in the same way: + // + var texts = new List(); + foreach (var retrievalContext in retrievalContexts) { - case RetrievalTextContext textContext: - matchedText = sanitizedTexts[nextText++]; - surroundingContent = sanitizedTexts.Skip(nextText).Take(textContext.SurroundingContent.Count).ToList(); - nextText += surroundingContent.Count; - AppendTextMatch(body, matchedText, surroundingContent); - break; + var source = PromptInjectionSource.RetrievalContext(retrievalContext.DataSourceName, retrievalContext.Path); + var description = new StringBuilder(); + AppendContextDescription(description, retrievalContext); + texts.Add(new(description.ToString(), source)); - case RetrievalImageContext: - AppendImageMatch(body); - break; + if (retrievalContext is not RetrievalTextContext textContext) + continue; - default: - LOGGER.LogWarning($"The retrieval content type '{retrievalContext.Type}' of data source '{retrievalContext.DataSourceName}' at location '{retrievalContext.Path}' is not supported yet."); - break; + texts.Add(new(textContext.MatchedText, source)); + foreach (var surrounding in textContext.SurroundingContent) + texts.Add(new(surrounding, source)); } - var sources = retrievalContext.ToSources(); - preparedContexts.Add(new(retrievalContext, body.ToString(), sources, CreatePassage(retrievalContext, sources, matchedText, surroundingContent))); - } + token.ThrowIfCancellationRequested(); + var sanitizedTexts = await guardService.SanitizeAsync(texts, reportingScope); - return preparedContexts; + var preparedContexts = new List(retrievalContexts.Count); + var nextText = 0; + foreach (var retrievalContext in retrievalContexts) + { + var body = new StringBuilder(); + body.Append(sanitizedTexts[nextText++]); + + var matchedText = string.Empty; + IReadOnlyList surroundingContent = []; + switch (retrievalContext) + { + case RetrievalTextContext textContext: + matchedText = sanitizedTexts[nextText++]; + surroundingContent = sanitizedTexts.Skip(nextText).Take(textContext.SurroundingContent.Count).ToList(); + nextText += surroundingContent.Count; + AppendTextMatch(body, matchedText, surroundingContent); + break; + + case RetrievalImageContext: + AppendImageMatch(body); + break; + + default: + LOGGER.LogWarning($"The retrieval content type '{retrievalContext.Type}' of data source '{retrievalContext.DataSourceName}' at location '{retrievalContext.Path}' is not supported yet."); + break; + } + + var sources = retrievalContext.ToSources(); + preparedContexts.Add(new(retrievalContext, body.ToString(), sources, CreatePassage(retrievalContext, sources, matchedText, surroundingContent))); + } + + return preparedContexts; + } } /// @@ -242,34 +245,37 @@ public static class IRetrievalContextExtensions }; } - /// - /// The sources a retrieval context lends to an answer, as they are listed below it. - /// - /// - /// The reference comes first: the title and link of the passage itself where the data source - /// names them, e.g., a local file with its page, and otherwise the data source and the path. - /// The further links of the context follow. Only what can be opened becomes a link, i.e., a - /// web address or a file with an absolute path. A relative path would point elsewhere depending - /// on where it is opened from.

- /// - /// A context which names nothing to open at all, e.g., by a path which only its ERI server knows, - /// still lends one source: the AI received it, and its passage needs a place to be read at. That - /// source is named as text, see SourceExtensions.CreateRetrievalSourceUrl. - ///
/// The retrieval context. - /// The sources; none only for a context which names neither a document nor its data source. - public static IReadOnlyList ToSources(this IRetrievalContext retrievalContext) + extension(IRetrievalContext retrievalContext) { - var sources = new List(); - var referenceTitle = GetReferenceTitle(retrievalContext); - AddSource(sources, referenceTitle, GetReferenceLink(retrievalContext)); - foreach (var link in retrievalContext.Links) - AddSource(sources, retrievalContext.DataSourceName, link); + /// + /// The sources a retrieval context lends to an answer, as they are listed below it. + /// + /// + /// The reference comes first: the title and link of the passage itself where the data source + /// names them, e.g., a local file with its page, and otherwise the data source and the path. + /// The further links of the context follow. Only what can be opened becomes a link, i.e., a + /// web address or a file with an absolute path. A relative path would point elsewhere depending + /// on where it is opened from.

+ /// + /// A context which names nothing to open at all, e.g., by a path which only its ERI server knows, + /// still lends one source: the AI received it, and its passage needs a place to be read at. That + /// source is named as text, see SourceExtensions.CreateRetrievalSourceUrl. + ///
+ /// The sources; none only for a context which names neither a document nor its data source. + public IReadOnlyList ToSources() + { + var sources = new List(); + var referenceTitle = GetReferenceTitle(retrievalContext); + AddSource(sources, referenceTitle, GetReferenceLink(retrievalContext)); + foreach (var link in retrievalContext.Links) + AddSource(sources, retrievalContext.DataSourceName, link); - if (sources.Count is 0 && !string.IsNullOrWhiteSpace(referenceTitle)) - sources.Add(new Source(referenceTitle, SourceExtensions.CreateRetrievalSourceUrl(referenceTitle, retrievalContext.Path), SourceOrigin.RAG)); + if (sources.Count is 0 && !string.IsNullOrWhiteSpace(referenceTitle)) + sources.Add(new Source(referenceTitle, SourceExtensions.CreateRetrievalSourceUrl(referenceTitle, retrievalContext.Path), SourceOrigin.RAG)); - return sources; + return sources; + } } private static void AddSource(ICollection sources, string title, string link) diff --git a/app/MindWork AI Studio/Tools/RAG/RetrievalContentCategoryExtensions.cs b/app/MindWork AI Studio/Tools/RAG/RetrievalContentCategoryExtensions.cs index a54816957..595bd6d41 100644 --- a/app/MindWork AI Studio/Tools/RAG/RetrievalContentCategoryExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/RetrievalContentCategoryExtensions.cs @@ -4,21 +4,24 @@ namespace AIStudio.Tools.RAG; public static class RetrievalContentCategoryExtensions { - /// - /// Converts an ERI content type to a common retrieval content category. - /// /// The content type yielded by the ERI server. - /// The corresponding retrieval content category. - public static RetrievalContentCategory ToRetrievalContentCategory(this ContentType contentType) => contentType switch + extension(ContentType contentType) { - ContentType.NONE => RetrievalContentCategory.NONE, - ContentType.UNKNOWN => RetrievalContentCategory.UNKNOWN, - ContentType.TEXT => RetrievalContentCategory.TEXT, - ContentType.IMAGE => RetrievalContentCategory.IMAGE, - ContentType.VIDEO => RetrievalContentCategory.VIDEO, - ContentType.AUDIO => RetrievalContentCategory.AUDIO, - ContentType.SPEECH => RetrievalContentCategory.AUDIO, + /// + /// Converts an ERI content type to a common retrieval content category. + /// + /// The corresponding retrieval content category. + public RetrievalContentCategory ToRetrievalContentCategory() => contentType switch + { + ContentType.NONE => RetrievalContentCategory.NONE, + ContentType.UNKNOWN => RetrievalContentCategory.UNKNOWN, + ContentType.TEXT => RetrievalContentCategory.TEXT, + ContentType.IMAGE => RetrievalContentCategory.IMAGE, + ContentType.VIDEO => RetrievalContentCategory.VIDEO, + ContentType.AUDIO => RetrievalContentCategory.AUDIO, + ContentType.SPEECH => RetrievalContentCategory.AUDIO, - _ => RetrievalContentCategory.UNKNOWN, - }; + _ => RetrievalContentCategory.UNKNOWN, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RAG/RetrievalContentTypeExtensions.cs b/app/MindWork AI Studio/Tools/RAG/RetrievalContentTypeExtensions.cs index 6abc4750b..9c73c1ca0 100644 --- a/app/MindWork AI Studio/Tools/RAG/RetrievalContentTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/RetrievalContentTypeExtensions.cs @@ -4,138 +4,141 @@ namespace AIStudio.Tools.RAG; public static class RetrievalContentTypeExtensions { - public static RetrievalContentType ToRetrievalContentType(this Context eriContext) + extension(Context eriContext) { - // - // Right now, we have to parse the category string along the type enum to - // determine the common retrieval content type. In future ERI versions, we - // might use the same enum. - // - - var lowerCategory = eriContext.Category.ToLowerInvariant(); - var type = eriContext.Type; - return type switch + public RetrievalContentType ToRetrievalContentType() { - ContentType.TEXT when lowerCategory.Contains("book") => RetrievalContentType.TEXT_BOOK, - ContentType.TEXT when lowerCategory.Contains("paper") => RetrievalContentType.TEXT_PAPER, - ContentType.TEXT when lowerCategory.Contains("dictionary") => RetrievalContentType.TEXT_DICTIONARY, - ContentType.TEXT when lowerCategory.Contains("encyclopedia") => RetrievalContentType.TEXT_ENCYCLOPEDIA, - ContentType.TEXT when lowerCategory.Contains("glossary") => RetrievalContentType.TEXT_GLOSSARY, - ContentType.TEXT when lowerCategory.Contains("journal") => RetrievalContentType.TEXT_JOURNAL, - ContentType.TEXT when lowerCategory.Contains("magazine") => RetrievalContentType.TEXT_MAGAZINE, - ContentType.TEXT when lowerCategory.Contains("newspaper") => RetrievalContentType.TEXT_NEWSPAPER, - ContentType.TEXT when lowerCategory.Contains("report") => RetrievalContentType.TEXT_REPORT, - ContentType.TEXT when lowerCategory.Contains("review") => RetrievalContentType.TEXT_REVIEW, - ContentType.TEXT when lowerCategory.Contains("website") => RetrievalContentType.TEXT_WEBSITE, - ContentType.TEXT when lowerCategory.Contains("idea") => RetrievalContentType.TEXT_IDEA, + // + // Right now, we have to parse the category string along the type enum to + // determine the common retrieval content type. In future ERI versions, we + // might use the same enum. + // + + var lowerCategory = eriContext.Category.ToLowerInvariant(); + var type = eriContext.Type; + return type switch + { + ContentType.TEXT when lowerCategory.Contains("book") => RetrievalContentType.TEXT_BOOK, + ContentType.TEXT when lowerCategory.Contains("paper") => RetrievalContentType.TEXT_PAPER, + ContentType.TEXT when lowerCategory.Contains("dictionary") => RetrievalContentType.TEXT_DICTIONARY, + ContentType.TEXT when lowerCategory.Contains("encyclopedia") => RetrievalContentType.TEXT_ENCYCLOPEDIA, + ContentType.TEXT when lowerCategory.Contains("glossary") => RetrievalContentType.TEXT_GLOSSARY, + ContentType.TEXT when lowerCategory.Contains("journal") => RetrievalContentType.TEXT_JOURNAL, + ContentType.TEXT when lowerCategory.Contains("magazine") => RetrievalContentType.TEXT_MAGAZINE, + ContentType.TEXT when lowerCategory.Contains("newspaper") => RetrievalContentType.TEXT_NEWSPAPER, + ContentType.TEXT when lowerCategory.Contains("report") => RetrievalContentType.TEXT_REPORT, + ContentType.TEXT when lowerCategory.Contains("review") => RetrievalContentType.TEXT_REVIEW, + ContentType.TEXT when lowerCategory.Contains("website") => RetrievalContentType.TEXT_WEBSITE, + ContentType.TEXT when lowerCategory.Contains("idea") => RetrievalContentType.TEXT_IDEA, - ContentType.TEXT when lowerCategory.Contains("business concept") => RetrievalContentType.TEXT_BUSINESS_CONCEPT, - ContentType.TEXT when lowerCategory.Contains("concept") => RetrievalContentType.TEXT_CONCEPT, + ContentType.TEXT when lowerCategory.Contains("business concept") => RetrievalContentType.TEXT_BUSINESS_CONCEPT, + ContentType.TEXT when lowerCategory.Contains("concept") => RetrievalContentType.TEXT_CONCEPT, - ContentType.TEXT when lowerCategory.Contains("definition") => RetrievalContentType.TEXT_DEFINITION, - ContentType.TEXT when lowerCategory.Contains("example") => RetrievalContentType.TEXT_EXAMPLE, - ContentType.TEXT when lowerCategory.Contains("quote") => RetrievalContentType.TEXT_QUOTE, - ContentType.TEXT when lowerCategory.Contains("article") => RetrievalContentType.TEXT_ARTICLE, - ContentType.TEXT when lowerCategory.Contains("chapter") => RetrievalContentType.TEXT_CHAPTER, + ContentType.TEXT when lowerCategory.Contains("definition") => RetrievalContentType.TEXT_DEFINITION, + ContentType.TEXT when lowerCategory.Contains("example") => RetrievalContentType.TEXT_EXAMPLE, + ContentType.TEXT when lowerCategory.Contains("quote") => RetrievalContentType.TEXT_QUOTE, + ContentType.TEXT when lowerCategory.Contains("article") => RetrievalContentType.TEXT_ARTICLE, + ContentType.TEXT when lowerCategory.Contains("chapter") => RetrievalContentType.TEXT_CHAPTER, - ContentType.TEXT when lowerCategory.Contains("thesis") => RetrievalContentType.TEXT_THESIS, - ContentType.TEXT when lowerCategory.Contains("dissertation") => RetrievalContentType.TEXT_THESIS, + ContentType.TEXT when lowerCategory.Contains("thesis") => RetrievalContentType.TEXT_THESIS, + ContentType.TEXT when lowerCategory.Contains("dissertation") => RetrievalContentType.TEXT_THESIS, - ContentType.TEXT when lowerCategory.Contains("draft") => RetrievalContentType.TEXT_DRAFT, - ContentType.TEXT when lowerCategory.Contains("script") => RetrievalContentType.TEXT_SCRIPT, - ContentType.TEXT when lowerCategory.Contains("transcript") => RetrievalContentType.TEXT_TRANSCRIPT, - ContentType.TEXT when lowerCategory.Contains("subtitle") => RetrievalContentType.TEXT_SUBTITLE, - ContentType.TEXT when lowerCategory.Contains("caption") => RetrievalContentType.TEXT_CAPTION, - ContentType.TEXT when lowerCategory.Contains("dialogue") => RetrievalContentType.TEXT_DIALOGUE, - ContentType.TEXT when lowerCategory.Contains("project proposal") => RetrievalContentType.TEXT_PROJECT_PROPOSAL, - ContentType.TEXT when lowerCategory.Contains("project plan") => RetrievalContentType.TEXT_PROJECT_PLAN, - ContentType.TEXT when lowerCategory.Contains("spreadsheet") => RetrievalContentType.TEXT_SPREADSHEET, + ContentType.TEXT when lowerCategory.Contains("draft") => RetrievalContentType.TEXT_DRAFT, + ContentType.TEXT when lowerCategory.Contains("script") => RetrievalContentType.TEXT_SCRIPT, + ContentType.TEXT when lowerCategory.Contains("transcript") => RetrievalContentType.TEXT_TRANSCRIPT, + ContentType.TEXT when lowerCategory.Contains("subtitle") => RetrievalContentType.TEXT_SUBTITLE, + ContentType.TEXT when lowerCategory.Contains("caption") => RetrievalContentType.TEXT_CAPTION, + ContentType.TEXT when lowerCategory.Contains("dialogue") => RetrievalContentType.TEXT_DIALOGUE, + ContentType.TEXT when lowerCategory.Contains("project proposal") => RetrievalContentType.TEXT_PROJECT_PROPOSAL, + ContentType.TEXT when lowerCategory.Contains("project plan") => RetrievalContentType.TEXT_PROJECT_PLAN, + ContentType.TEXT when lowerCategory.Contains("spreadsheet") => RetrievalContentType.TEXT_SPREADSHEET, - ContentType.TEXT when lowerCategory.Contains("presentation") => RetrievalContentType.TEXT_PRESENTATION, - ContentType.TEXT when lowerCategory.Contains("powerpoint") => RetrievalContentType.TEXT_PRESENTATION, - ContentType.TEXT when lowerCategory.Contains("slide") => RetrievalContentType.TEXT_PRESENTATION, + ContentType.TEXT when lowerCategory.Contains("presentation") => RetrievalContentType.TEXT_PRESENTATION, + ContentType.TEXT when lowerCategory.Contains("powerpoint") => RetrievalContentType.TEXT_PRESENTATION, + ContentType.TEXT when lowerCategory.Contains("slide") => RetrievalContentType.TEXT_PRESENTATION, - ContentType.TEXT when lowerCategory.Contains("meeting minutes") => RetrievalContentType.TEXT_MEETING_MINUTES, - ContentType.TEXT when lowerCategory.Contains("email") => RetrievalContentType.TEXT_EMAIL, - ContentType.TEXT when lowerCategory.Contains("protocol") => RetrievalContentType.TEXT_PROTOCOL, + ContentType.TEXT when lowerCategory.Contains("meeting minutes") => RetrievalContentType.TEXT_MEETING_MINUTES, + ContentType.TEXT when lowerCategory.Contains("email") => RetrievalContentType.TEXT_EMAIL, + ContentType.TEXT when lowerCategory.Contains("protocol") => RetrievalContentType.TEXT_PROTOCOL, - ContentType.TEXT => RetrievalContentType.TEXT_DOCUMENT, + ContentType.TEXT => RetrievalContentType.TEXT_DOCUMENT, - ContentType.IMAGE when lowerCategory.Contains("photo") => RetrievalContentType.IMAGE_PHOTO, - ContentType.IMAGE when lowerCategory.Contains("illustration") => RetrievalContentType.IMAGE_ILLUSTRATION, - ContentType.IMAGE when lowerCategory.Contains("diagram") => RetrievalContentType.IMAGE_DIAGRAM, - ContentType.IMAGE when lowerCategory.Contains("chart") => RetrievalContentType.IMAGE_CHART, - ContentType.IMAGE when lowerCategory.Contains("art") => RetrievalContentType.IMAGE_ART, - ContentType.IMAGE when lowerCategory.Contains("drawing") => RetrievalContentType.IMAGE_DRAWING, - ContentType.IMAGE when lowerCategory.Contains("painting") => RetrievalContentType.IMAGE_PAINTING, - ContentType.IMAGE when lowerCategory.Contains("sketch") => RetrievalContentType.IMAGE_SKETCH, - ContentType.IMAGE when lowerCategory.Contains("map") => RetrievalContentType.IMAGE_MAP, - ContentType.IMAGE when lowerCategory.Contains("scene") => RetrievalContentType.IMAGE_SCENE, - ContentType.IMAGE when lowerCategory.Contains("character") => RetrievalContentType.IMAGE_CHARACTER, - ContentType.IMAGE when lowerCategory.Contains("landscape") => RetrievalContentType.IMAGE_LANDSCAPE, - ContentType.IMAGE when lowerCategory.Contains("portrait") => RetrievalContentType.IMAGE_PORTRAIT, - ContentType.IMAGE when lowerCategory.Contains("poster") => RetrievalContentType.IMAGE_POSTER, - ContentType.IMAGE when lowerCategory.Contains("logo") => RetrievalContentType.IMAGE_LOGO, - ContentType.IMAGE when lowerCategory.Contains("icon") => RetrievalContentType.IMAGE_ICON, + ContentType.IMAGE when lowerCategory.Contains("photo") => RetrievalContentType.IMAGE_PHOTO, + ContentType.IMAGE when lowerCategory.Contains("illustration") => RetrievalContentType.IMAGE_ILLUSTRATION, + ContentType.IMAGE when lowerCategory.Contains("diagram") => RetrievalContentType.IMAGE_DIAGRAM, + ContentType.IMAGE when lowerCategory.Contains("chart") => RetrievalContentType.IMAGE_CHART, + ContentType.IMAGE when lowerCategory.Contains("art") => RetrievalContentType.IMAGE_ART, + ContentType.IMAGE when lowerCategory.Contains("drawing") => RetrievalContentType.IMAGE_DRAWING, + ContentType.IMAGE when lowerCategory.Contains("painting") => RetrievalContentType.IMAGE_PAINTING, + ContentType.IMAGE when lowerCategory.Contains("sketch") => RetrievalContentType.IMAGE_SKETCH, + ContentType.IMAGE when lowerCategory.Contains("map") => RetrievalContentType.IMAGE_MAP, + ContentType.IMAGE when lowerCategory.Contains("scene") => RetrievalContentType.IMAGE_SCENE, + ContentType.IMAGE when lowerCategory.Contains("character") => RetrievalContentType.IMAGE_CHARACTER, + ContentType.IMAGE when lowerCategory.Contains("landscape") => RetrievalContentType.IMAGE_LANDSCAPE, + ContentType.IMAGE when lowerCategory.Contains("portrait") => RetrievalContentType.IMAGE_PORTRAIT, + ContentType.IMAGE when lowerCategory.Contains("poster") => RetrievalContentType.IMAGE_POSTER, + ContentType.IMAGE when lowerCategory.Contains("logo") => RetrievalContentType.IMAGE_LOGO, + ContentType.IMAGE when lowerCategory.Contains("icon") => RetrievalContentType.IMAGE_ICON, - ContentType.IMAGE when lowerCategory.Contains("satellite") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, - ContentType.IMAGE when lowerCategory.Contains("EO") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, - ContentType.IMAGE when lowerCategory.Contains("earth observation") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, + ContentType.IMAGE when lowerCategory.Contains("satellite") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, + ContentType.IMAGE when lowerCategory.Contains("EO") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, + ContentType.IMAGE when lowerCategory.Contains("earth observation") => RetrievalContentType.IMAGE_SATELLITE_IMAGE, - ContentType.IMAGE => RetrievalContentType.NOT_SPECIFIED, + ContentType.IMAGE => RetrievalContentType.NOT_SPECIFIED, - ContentType.AUDIO when lowerCategory.Contains("speech") => RetrievalContentType.AUDIO_SPEECH, + ContentType.AUDIO when lowerCategory.Contains("speech") => RetrievalContentType.AUDIO_SPEECH, - ContentType.AUDIO when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST, - ContentType.SPEECH when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST, + ContentType.AUDIO when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST, + ContentType.SPEECH when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST, - ContentType.AUDIO when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK, - ContentType.SPEECH when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK, + ContentType.AUDIO when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK, + ContentType.SPEECH when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK, - ContentType.AUDIO when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW, - ContentType.SPEECH when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW, + ContentType.AUDIO when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW, + ContentType.SPEECH when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW, - ContentType.AUDIO when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE, - ContentType.SPEECH when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE, + ContentType.AUDIO when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE, + ContentType.SPEECH when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE, - ContentType.AUDIO when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK, - ContentType.SPEECH when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK, + ContentType.AUDIO when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK, + ContentType.SPEECH when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK, - ContentType.AUDIO when lowerCategory.Contains("song") => RetrievalContentType.AUDIO_SONG, - ContentType.AUDIO when lowerCategory.Contains("music") => RetrievalContentType.AUDIO_MUSIC, - ContentType.AUDIO when lowerCategory.Contains("sound") => RetrievalContentType.AUDIO_SOUND, - ContentType.AUDIO when lowerCategory.Contains("call") => RetrievalContentType.AUDIO_CALL, - ContentType.AUDIO when lowerCategory.Contains("voice acting") => RetrievalContentType.AUDIO_VOICE_ACTING, - ContentType.AUDIO when lowerCategory.Contains("description") => RetrievalContentType.AUDIO_DESCRIPTION, - ContentType.AUDIO when lowerCategory.Contains("guide") => RetrievalContentType.AUDIO_GUIDE, - ContentType.AUDIO when lowerCategory.Contains("dialogue") => RetrievalContentType.AUDIO_DIALOGUE, + ContentType.AUDIO when lowerCategory.Contains("song") => RetrievalContentType.AUDIO_SONG, + ContentType.AUDIO when lowerCategory.Contains("music") => RetrievalContentType.AUDIO_MUSIC, + ContentType.AUDIO when lowerCategory.Contains("sound") => RetrievalContentType.AUDIO_SOUND, + ContentType.AUDIO when lowerCategory.Contains("call") => RetrievalContentType.AUDIO_CALL, + ContentType.AUDIO when lowerCategory.Contains("voice acting") => RetrievalContentType.AUDIO_VOICE_ACTING, + ContentType.AUDIO when lowerCategory.Contains("description") => RetrievalContentType.AUDIO_DESCRIPTION, + ContentType.AUDIO when lowerCategory.Contains("guide") => RetrievalContentType.AUDIO_GUIDE, + ContentType.AUDIO when lowerCategory.Contains("dialogue") => RetrievalContentType.AUDIO_DIALOGUE, - ContentType.SPEECH => RetrievalContentType.AUDIO_SPEECH, - ContentType.AUDIO => RetrievalContentType.NOT_SPECIFIED, + ContentType.SPEECH => RetrievalContentType.AUDIO_SPEECH, + ContentType.AUDIO => RetrievalContentType.NOT_SPECIFIED, - ContentType.VIDEO when lowerCategory.Contains("movie") => RetrievalContentType.VIDEO_MOVIE, - ContentType.VIDEO when lowerCategory.Contains("film") => RetrievalContentType.VIDEO_FILM, - ContentType.VIDEO when lowerCategory.Contains("tv show") => RetrievalContentType.VIDEO_TV_SHOW, - ContentType.VIDEO when lowerCategory.Contains("series") => RetrievalContentType.VIDEO_SERIES, - ContentType.VIDEO when lowerCategory.Contains("episode") => RetrievalContentType.VIDEO_EPISODE, - ContentType.VIDEO when lowerCategory.Contains("documentary") => RetrievalContentType.VIDEO_DOCUMENTARY, - ContentType.VIDEO when lowerCategory.Contains("tutorial") => RetrievalContentType.VIDEO_TUTORIAL, - ContentType.VIDEO when lowerCategory.Contains("lecture") => RetrievalContentType.VIDEO_LECTURE, - ContentType.VIDEO when lowerCategory.Contains("webinar") => RetrievalContentType.VIDEO_WEBINAR, - ContentType.VIDEO when lowerCategory.Contains("game") => RetrievalContentType.VIDEO_GAME, - ContentType.VIDEO when lowerCategory.Contains("animation") => RetrievalContentType.VIDEO_ANIMATION, - ContentType.VIDEO when lowerCategory.Contains("cutscene") => RetrievalContentType.VIDEO_CUTSCENE, - ContentType.VIDEO when lowerCategory.Contains("trailer") => RetrievalContentType.VIDEO_TRAILER, - ContentType.VIDEO when lowerCategory.Contains("advertisement") => RetrievalContentType.VIDEO_ADVERTISEMENT, + ContentType.VIDEO when lowerCategory.Contains("movie") => RetrievalContentType.VIDEO_MOVIE, + ContentType.VIDEO when lowerCategory.Contains("film") => RetrievalContentType.VIDEO_FILM, + ContentType.VIDEO when lowerCategory.Contains("tv show") => RetrievalContentType.VIDEO_TV_SHOW, + ContentType.VIDEO when lowerCategory.Contains("series") => RetrievalContentType.VIDEO_SERIES, + ContentType.VIDEO when lowerCategory.Contains("episode") => RetrievalContentType.VIDEO_EPISODE, + ContentType.VIDEO when lowerCategory.Contains("documentary") => RetrievalContentType.VIDEO_DOCUMENTARY, + ContentType.VIDEO when lowerCategory.Contains("tutorial") => RetrievalContentType.VIDEO_TUTORIAL, + ContentType.VIDEO when lowerCategory.Contains("lecture") => RetrievalContentType.VIDEO_LECTURE, + ContentType.VIDEO when lowerCategory.Contains("webinar") => RetrievalContentType.VIDEO_WEBINAR, + ContentType.VIDEO when lowerCategory.Contains("game") => RetrievalContentType.VIDEO_GAME, + ContentType.VIDEO when lowerCategory.Contains("animation") => RetrievalContentType.VIDEO_ANIMATION, + ContentType.VIDEO when lowerCategory.Contains("cutscene") => RetrievalContentType.VIDEO_CUTSCENE, + ContentType.VIDEO when lowerCategory.Contains("trailer") => RetrievalContentType.VIDEO_TRAILER, + ContentType.VIDEO when lowerCategory.Contains("advertisement") => RetrievalContentType.VIDEO_ADVERTISEMENT, - ContentType.VIDEO => RetrievalContentType.NOT_SPECIFIED, + ContentType.VIDEO => RetrievalContentType.NOT_SPECIFIED, - ContentType.NONE => RetrievalContentType.NOT_SPECIFIED, + ContentType.NONE => RetrievalContentType.NOT_SPECIFIED, - _ => RetrievalContentType.UNKNOWN, - }; + _ => RetrievalContentType.UNKNOWN, + }; + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RAG/SourcePassageExtensions.cs b/app/MindWork AI Studio/Tools/RAG/SourcePassageExtensions.cs index ac1625d1b..bbebec553 100644 --- a/app/MindWork AI Studio/Tools/RAG/SourcePassageExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/SourcePassageExtensions.cs @@ -7,165 +7,186 @@ public static class SourcePassageExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourcePassageExtensions).Namespace, nameof(SourcePassageExtensions)); - /// - /// Whether the passage comes from a transcript, e.g., of an audio or a video file. - /// /// The passage. - /// True for a transcript. - public static bool IsTranscript(this SourcePassage passage) => passage.Type is RetrievalContentType.TEXT_TRANSCRIPT; - - /// - /// What sort of content the passage is, as the provenance store files it. - /// - /// - /// Told by what the content is rather than by where it came from: a mail is a mail, whether a - /// mailbox data source or a tool handed it over. - /// - /// The passage. - /// The kind of its entry. - public static ProvenanceKind GetProvenanceKind(this SourcePassage passage) => passage.Type switch + extension(SourcePassage passage) { - RetrievalContentType.TEXT_EMAIL => ProvenanceKind.MAIL, - RetrievalContentType.TEXT_WEBSITE => ProvenanceKind.WEB_PAGE, - _ => ProvenanceKind.DATA_SOURCE_PASSAGE, - }; + /// + /// Whether the passage comes from a transcript, e.g., of an audio or a video file. + /// + /// True for a transcript. + public bool IsTranscript() => passage.Type is RetrievalContentType.TEXT_TRANSCRIPT; - /// - /// Makes the passage of a web page which a tool read for the AI, so that the user can read under - /// the source of the page what the AI received from it. - /// - /// - /// Listed under the source by its address, see GetPassagesOf, and named by its title in the - /// dialog of its passages. A page says nothing about pages or lines, so its source keeps the - /// title the tool gave it, see NamePassagesInTitles. - /// - /// The source of the page, as the tool lists it. - /// The content of the page as the model received it: filtered for prompt injections, and cut where the tool cut it. - /// The passage of the page. - public static SourcePassage ToWebPagePassage(this Source source, string content) => new() + /// + /// What sort of content the passage is, as the provenance store files it. + /// + /// + /// Told by what the content is rather than by where it came from: a mail is a mail, whether a + /// mailbox data source or a tool handed it over. + /// + /// The kind of its entry. + public ProvenanceKind GetProvenanceKind() => passage.Type switch + { + RetrievalContentType.TEXT_EMAIL => ProvenanceKind.MAIL, + RetrievalContentType.TEXT_WEBSITE => ProvenanceKind.WEB_PAGE, + _ => ProvenanceKind.DATA_SOURCE_PASSAGE, + }; + + /// + /// Whether the passage holds texts which belong into the provenance store, see SourcePassage.ProvenanceId. + /// + /// False once the texts are stored, and for a passage without any, such as an image. + public bool HoldsTextsToStore() => passage.ProvenanceId is null && (passage.Text.Length > 0 || passage.SurroundingContent.Count > 0); + + /// + /// Names where the passage is in its document, e.g., "Page 4", "Pages 4–5", "Budget: rows 3–9", "Lines 40–85", or "Part 3". + /// + /// The location, or an empty string when the data source does not say. + public string GetLocationLabel() => GetLocationLabel([passage]); + + /// + /// Where the passage is in its document, as numbers only, e.g., its pages 4–5, its lines 40–85, or its part 3. + /// + /// + /// Orders the passages of one document, in the order of the document. The passages of the + /// sheets of a spreadsheet give their parts, since rows start anew in every sheet. + /// + /// The numbers, or null when the data source does not say. + public LocationRange? GetLocationRange() => GetLocation([passage])?.Ranges[0]; + } + + /// The source of the page or the mail, as the tool lists it. + extension(Source source) { - SourceUrl = source.URL, - DataSourceName = source.Title, - Path = source.URL, - Category = RetrievalContentCategory.TEXT, - Type = RetrievalContentType.TEXT_WEBSITE, - Text = content, - }; + /// + /// Makes the passage of a web page which a tool read for the AI, so that the user can read under + /// the source of the page what the AI received from it. + /// + /// + /// Listed under the source by its address, see GetPassagesOf, and named by its title in the + /// dialog of its passages. A page says nothing about pages or lines, so its source keeps the + /// title the tool gave it, see NamePassagesInTitles. + /// + /// The content of the page as the model received it: filtered for prompt injections, and cut where the tool cut it. + /// The passage of the page. + public SourcePassage ToWebPagePassage(string content) => new() + { + SourceUrl = source.URL, + DataSourceName = source.Title, + Path = source.URL, + Category = RetrievalContentCategory.TEXT, + Type = RetrievalContentType.TEXT_WEBSITE, + Text = content, + }; - /// - /// Makes the passage of a mail which a tool handed to the AI, so that the user can read under - /// the source of the mail what the AI received of it. - /// - /// - /// Listed and named like the passage of a web page, see ToWebPagePassage. A mail has no path: - /// its address names it for AI Studio only, so there is nothing to copy or to open. - /// - /// The source of the mail, as the tool lists it. - /// What the model received of the mail, filtered for prompt injections. - /// The passage of the mail. - public static SourcePassage ToMailPassage(this Source source, string content) => new() - { - SourceUrl = source.URL, - DataSourceName = source.Title, - Category = RetrievalContentCategory.TEXT, - Type = RetrievalContentType.TEXT_EMAIL, - Text = content, - }; - - /// - /// Whether the passage holds texts which belong into the provenance store, see SourcePassage.ProvenanceId. - /// - /// The passage. - /// False once the texts are stored, and for a passage without any, such as an image. - public static bool HoldsTextsToStore(this SourcePassage passage) => passage.ProvenanceId is null && (passage.Text.Length > 0 || passage.SurroundingContent.Count > 0); + /// + /// Makes the passage of a mail which a tool handed to the AI, so that the user can read under + /// the source of the mail what the AI received of it. + /// + /// + /// Listed and named like the passage of a web page, see ToWebPagePassage. A mail has no path: + /// its address names it for AI Studio only, so there is nothing to copy or to open. + /// + /// What the model received of the mail, filtered for prompt injections. + /// The passage of the mail. + public SourcePassage ToMailPassage(string content) => new() + { + SourceUrl = source.URL, + DataSourceName = source.Title, + Category = RetrievalContentCategory.TEXT, + Type = RetrievalContentType.TEXT_EMAIL, + Text = content, + }; + } /// /// What stands between the sheets of a spreadsheet in a location, e.g., "Budget: rows 3–9 · Summary: row 2". /// private const string SHEET_SEPARATOR = " · "; - /// - /// Names where the passage is in its document, e.g., "Page 4", "Pages 4–5", "Budget: rows 3–9", "Lines 40–85", or "Part 3". - /// - /// The passage. - /// The location, or an empty string when the data source does not say. - public static string GetLocationLabel(this SourcePassage passage) => GetLocationLabel([passage]); - - /// - /// Where the passage is in its document, as numbers only, e.g., its pages 4–5, its lines 40–85, or its part 3. - /// - /// - /// Orders the passages of one document, in the order of the document. The passages of the - /// sheets of a spreadsheet give their parts, since rows start anew in every sheet. - /// - /// The passage. - /// The numbers, or null when the data source does not say. - public static LocationRange? GetLocationRange(this SourcePassage passage) => GetLocation([passage])?.Ranges[0]; - - /// - /// Names where several passages are in their document, e.g., "Pages 1, 11", "Budget: rows 3–9 · Summary: row 2", "Lines 40–85, 120" or "Parts 2, 5". - /// /// The passages. - /// The location, or an empty string when the data source does not say. - public static string GetLocationLabel(this IEnumerable passages) + extension(IEnumerable passages) { - var passagesOfDocument = passages.ToList(); - if (passagesOfDocument.All(passage => passage.PageNumber is not > 0) && GetSheetsLabel(passagesOfDocument) is { } sheets) - return sheets; + /// + /// Names where several passages are in their document, e.g., "Pages 1, 11", "Budget: rows 3–9 · Summary: row 2", "Lines 40–85, 120" or "Parts 2, 5". + /// + /// The location, or an empty string when the data source does not say. + public string GetLocationLabel() + { + var passagesOfDocument = passages.ToList(); + if (passagesOfDocument.All(passage => passage.PageNumber is not > 0) && GetSheetsLabel(passagesOfDocument) is { } sheets) + return sheets; - return passagesOfDocument.GetLocation()?.ToString() ?? string.Empty; - } + return passagesOfDocument.GetLocation()?.ToString() ?? string.Empty; + } - /// - /// Finds where several passages are in their document, in parts, so that each page can be shown - /// as a link. - /// - /// - /// A document with pages is named by its pages: they are what the user finds again in the - /// document, and what a program can open it at. A table without sheets, such as a CSV file, is - /// named by its rows, and a text by its lines. Rows and lines are nothing a program can open a - /// document at, so they become no links. A document which says none of them is named by the - /// parts it was split into for the search. Either way, the numbers follow the order of the - /// document, each once: two passages on the same page name that page once. An end which is not - /// after the start names no range.

- /// The rows of a spreadsheet start anew in every sheet, so they need the name of their sheet - /// and are named by GetLocationLabel, sheet by sheet. Here, its passages give their parts. - ///
- /// The passages. - /// The location, or null when the data source says nothing about it. - public static PassageLocation? GetLocation(this IEnumerable passages) - { - var passagesOfDocument = passages.ToList(); - var pages = passagesOfDocument - .Where(passage => passage.PageNumber is > 0) - .Select(passage => RangeOf(passage.PageNumber!.Value, passage.EndPageNumber)) - .ToList(); + /// + /// Finds where several passages are in their document, in parts, so that each page can be shown + /// as a link. + /// + /// + /// A document with pages is named by its pages: they are what the user finds again in the + /// document, and what a program can open it at. A table without sheets, such as a CSV file, is + /// named by its rows, and a text by its lines. Rows and lines are nothing a program can open a + /// document at, so they become no links. A document which says none of them is named by the + /// parts it was split into for the search. Either way, the numbers follow the order of the + /// document, each once: two passages on the same page name that page once. An end which is not + /// after the start names no range.

+ /// The rows of a spreadsheet start anew in every sheet, so they need the name of their sheet + /// and are named by GetLocationLabel, sheet by sheet. Here, its passages give their parts. + ///
+ /// The location, or null when the data source says nothing about it. + public PassageLocation? GetLocation() + { + var passagesOfDocument = passages.ToList(); + var pages = passagesOfDocument + .Where(passage => passage.PageNumber is > 0) + .Select(passage => RangeOf(passage.PageNumber!.Value, passage.EndPageNumber)) + .ToList(); - if (pages.Count > 0) - return CreateLocation(pages, TB("Page {0}"), TB("Pages {0}"), arePages: true); + if (pages.Count > 0) + return CreateLocation(pages, TB("Page {0}"), TB("Pages {0}"), arePages: true); - var rows = passagesOfDocument - .Where(passage => passage is { StartRow: > 0, SheetName: null or "" }) - .Select(passage => RangeOf(passage.StartRow!.Value, passage.EndRow)) - .ToList(); + var rows = passagesOfDocument + .Where(passage => passage is { StartRow: > 0, SheetName: null or "" }) + .Select(passage => RangeOf(passage.StartRow!.Value, passage.EndRow)) + .ToList(); - if (rows.Count > 0) - return CreateLocation(rows, TB("Row {0}"), TB("Rows {0}"), arePages: false); + if (rows.Count > 0) + return CreateLocation(rows, TB("Row {0}"), TB("Rows {0}"), arePages: false); - var lines = passagesOfDocument - .Where(passage => passage.StartLine is > 0) - .Select(passage => RangeOf(passage.StartLine!.Value, passage.EndLine)) - .ToList(); + var lines = passagesOfDocument + .Where(passage => passage.StartLine is > 0) + .Select(passage => RangeOf(passage.StartLine!.Value, passage.EndLine)) + .ToList(); - if (lines.Count > 0) - return CreateLocation(lines, TB("Line {0}"), TB("Lines {0}"), arePages: false); + if (lines.Count > 0) + return CreateLocation(lines, TB("Line {0}"), TB("Lines {0}"), arePages: false); - var parts = passagesOfDocument - .Where(passage => passage.ChunkNumber is > 0) - .Select(passage => new LocationRange(passage.ChunkNumber!.Value, passage.ChunkNumber!.Value)) - .ToList(); + var parts = passagesOfDocument + .Where(passage => passage.ChunkNumber is > 0) + .Select(passage => new LocationRange(passage.ChunkNumber!.Value, passage.ChunkNumber!.Value)) + .ToList(); - return parts.Count > 0 ? CreateLocation(parts, TB("Part {0}"), TB("Parts {0}"), arePages: false) : null; + return parts.Count > 0 ? CreateLocation(parts, TB("Part {0}"), TB("Parts {0}"), arePages: false) : null; + } + + /// + /// Finds the passages listed under a source. + /// + /// + /// The list of sources keeps one entry per document, whichever page its link names, see + /// SourceExtensions.MergeSources. The passages are matched the same way, so the entry of a + /// document shows the passages of all its pages, and every passage shows up under its source. + /// + /// The source. + /// The passages of the source, in their order; empty when it has none. + public IReadOnlyList GetPassagesOf(ISource source) => passages.SortBySource().GetPassagesOf(source); + + /// + /// Sorts passages by the source they are listed under, to find the passages of many sources. + /// + /// The passages by the identity of their source, see SourceExtensions.GetSourceIdentity. + public ILookup SortBySource() => passages.ToLookup(passage => SourceExtensions.GetSourceIdentity(passage.SourceUrl), StringComparer.Ordinal); } /// @@ -226,93 +247,82 @@ public static class SourcePassageExtensions : new PassageLocation(format[..placeholderAt], orderedRanges, format[(placeholderAt + PLACEHOLDER.Length)..], arePages); } - /// - /// Names in the title of each source where its passages are, e.g., "handbook.pdf (Pages 1, 11)". - /// - /// - /// The list of sources keeps one entry per document, see SourceExtensions.MergeSources, and its - /// title came from the passage found first, e.g., "handbook.pdf (Page 1)". A second passage on - /// page 11 would go unmentioned, so the title is made anew from all passages of the source. A - /// spreadsheet is named by its sheets and rows the same way, a text file by its lines, see - /// GetLocationLabel. Passages whose data source - /// says neither, e.g., an external one, are counted instead as soon as there are several of - /// them. Otherwise, a source keeps the title its data source gave it, and so does every source - /// without passages, such as a web page. - /// /// The sources of an answer. - /// The passages of the same answer. - public static void NamePassagesInTitles(this IList sources, IReadOnlyCollection passages) + extension(IList sources) { - if (passages.Count is 0) - return; - - var passagesBySource = passages.SortBySource(); - for (var index = 0; index < sources.Count; index++) + /// + /// Names in the title of each source where its passages are, e.g., "handbook.pdf (Pages 1, 11)". + /// + /// + /// The list of sources keeps one entry per document, see SourceExtensions.MergeSources, and its + /// title came from the passage found first, e.g., "handbook.pdf (Page 1)". A second passage on + /// page 11 would go unmentioned, so the title is made anew from all passages of the source. A + /// spreadsheet is named by its sheets and rows the same way, a text file by its lines, see + /// GetLocationLabel. Passages whose data source + /// says neither, e.g., an external one, are counted instead as soon as there are several of + /// them. Otherwise, a source keeps the title its data source gave it, and so does every source + /// without passages, such as a web page. + /// + /// The passages of the same answer. + public void NamePassagesInTitles(IReadOnlyCollection passages) { - var passagesOfSource = passagesBySource.GetPassagesOf(sources[index]); - if (passagesOfSource.Count is 0 || string.IsNullOrWhiteSpace(passagesOfSource[0].DataSourceName)) - continue; + if (passages.Count is 0) + return; - var location = passagesOfSource.GetLocationLabel(); - if (location.Length is 0 && passagesOfSource.Count > 1) - location = string.Format(TB("{0} passages"), passagesOfSource.Count.CompactCount()); + var passagesBySource = passages.SortBySource(); + for (var index = 0; index < sources.Count; index++) + { + var passagesOfSource = passagesBySource.GetPassagesOf(sources[index]); + if (passagesOfSource.Count is 0 || string.IsNullOrWhiteSpace(passagesOfSource[0].DataSourceName)) + continue; - if (location.Length is 0) - continue; + var location = passagesOfSource.GetLocationLabel(); + if (location.Length is 0 && passagesOfSource.Count > 1) + location = string.Format(TB("{0} passages"), passagesOfSource.Count.CompactCount()); - sources[index] = sources[index] with { Title = $"{passagesOfSource[0].DataSourceName} ({location})" }; + if (location.Length is 0) + continue; + + sources[index] = sources[index] with { Title = $"{passagesOfSource[0].DataSourceName} ({location})" }; + } } } - /// - /// Merges added passages into an existing list of passages, leaving out those it holds already. - /// - /// - /// A passage is known when one with the same address and the same text is, e.g., the same chunk - /// found by two searches for one answer. Unlike the sources, the address counts with its page: - /// the same sentence on two pages of a document is two passages. A stored passage is told - /// apart by its entry, since its text lies in the provenance store. - /// /// The existing list of passages to merge into. - /// The passages to add. - public static void MergePassages(this IList passages, IEnumerable addedPassages) + extension(IList passages) { - var knownPassages = passages - .Select(passage => (passage.SourceUrl, passage.Text, passage.ProvenanceId)) - .ToHashSet(); - - foreach (var addedPassage in addedPassages) + /// + /// Merges added passages into an existing list of passages, leaving out those it holds already. + /// + /// + /// A passage is known when one with the same address and the same text is, e.g., the same chunk + /// found by two searches for one answer. Unlike the sources, the address counts with its page: + /// the same sentence on two pages of a document is two passages. A stored passage is told + /// apart by its entry, since its text lies in the provenance store. + /// + /// The passages to add. + public void MergePassages(IEnumerable addedPassages) { - if (knownPassages.Add((addedPassage.SourceUrl, addedPassage.Text, addedPassage.ProvenanceId))) - passages.Add(addedPassage); + var knownPassages = passages + .Select(passage => (passage.SourceUrl, passage.Text, passage.ProvenanceId)) + .ToHashSet(); + + foreach (var addedPassage in addedPassages) + { + if (knownPassages.Add((addedPassage.SourceUrl, addedPassage.Text, addedPassage.ProvenanceId))) + passages.Add(addedPassage); + } } } - /// - /// Finds the passages listed under a source. - /// - /// - /// The list of sources keeps one entry per document, whichever page its link names, see - /// SourceExtensions.MergeSources. The passages are matched the same way, so the entry of a - /// document shows the passages of all its pages, and every passage shows up under its source. - /// - /// The passages of an answer. - /// The source. - /// The passages of the source, in their order; empty when it has none. - public static IReadOnlyList GetPassagesOf(this IEnumerable passages, ISource source) => passages.SortBySource().GetPassagesOf(source); - - /// - /// Sorts passages by the source they are listed under, to find the passages of many sources. - /// - /// The passages of an answer. - /// The passages by the identity of their source, see SourceExtensions.GetSourceIdentity. - public static ILookup SortBySource(this IEnumerable passages) => passages.ToLookup(passage => SourceExtensions.GetSourceIdentity(passage.SourceUrl), StringComparer.Ordinal); - - /// - /// Finds the passages listed under a source, among passages sorted by their sources. - /// /// The passages of an answer, see SortBySource. - /// The source. - /// The passages of the source, in their order; empty when it has none. - public static IReadOnlyList GetPassagesOf(this ILookup passagesBySource, ISource source) => [..passagesBySource[SourceExtensions.GetSourceIdentity(source.URL)]]; + extension(ILookup passagesBySource) + { + /// + /// Finds the passages listed under a source, among passages sorted by their sources. + /// + /// The source. + /// The passages of the source, in their order; empty when it has none. + public IReadOnlyList GetPassagesOf(ISource source) => [..passagesBySource[SourceExtensions.GetSourceIdentity(source.URL)]]; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/ModelImageErrorCodeExtensions.cs b/app/MindWork AI Studio/Tools/Rust/ModelImageErrorCodeExtensions.cs index 426b65b22..fdb8f1caa 100644 --- a/app/MindWork AI Studio/Tools/Rust/ModelImageErrorCodeExtensions.cs +++ b/app/MindWork AI Studio/Tools/Rust/ModelImageErrorCodeExtensions.cs @@ -13,13 +13,16 @@ public static class ModelImageErrorCodeExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelImageErrorCodeExtensions).Namespace, nameof(ModelImageErrorCodeExtensions)); - /// - /// Gets the localized message which explains why an image was not sent to the AI. - /// /// Why the image could not be prepared. - /// The name of the image file, as shown to the user. - /// The localized message. - public static string ToUserMessage(this ModelImageErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName); + extension(ModelImageErrorCode code) + { + /// + /// Gets the localized message which explains why an image was not sent to the AI. + /// + /// The name of the image file, as shown to the user. + /// The localized message. + public string ToUserMessage(string fileName) => string.Format(ToUserMessageFormat(code), fileName); + } private static string ToUserMessageFormat(ModelImageErrorCode code) => code switch { diff --git a/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs b/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs index f1e90d81f..963c25209 100644 --- a/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs @@ -2,23 +2,26 @@ namespace AIStudio.Tools; public static class SecretStoreTypeExtensions { - /// - /// Gets the prefix string associated with the SecretStoreType. - /// - /// - /// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility. - /// /// The SecretStoreType enum value. - /// The corresponding prefix string. - public static string Prefix(this SecretStoreType type) => type switch + extension(SecretStoreType type) { - SecretStoreType.LLM_PROVIDER => "provider", - SecretStoreType.EMBEDDING_PROVIDER => "embedding", - SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription", - SecretStoreType.IMAGE_PROVIDER => "image", - SecretStoreType.DATA_SOURCE => "data-source", - SecretStoreType.TOOL_SETTINGS => "tool", + /// + /// Gets the prefix string associated with the SecretStoreType. + /// + /// + /// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility. + /// + /// The corresponding prefix string. + public string Prefix() => type switch + { + SecretStoreType.LLM_PROVIDER => "provider", + SecretStoreType.EMBEDDING_PROVIDER => "embedding", + SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription", + SecretStoreType.IMAGE_PROVIDER => "image", + SecretStoreType.DATA_SOURCE => "data-source", + SecretStoreType.TOOL_SETTINGS => "tool", - _ => "provider", - }; + _ => "provider", + }; + } } diff --git a/app/MindWork AI Studio/Tools/Security/PromptInjectionFindingCategoryExtensions.cs b/app/MindWork AI Studio/Tools/Security/PromptInjectionFindingCategoryExtensions.cs index 86955a3e1..73db014ed 100644 --- a/app/MindWork AI Studio/Tools/Security/PromptInjectionFindingCategoryExtensions.cs +++ b/app/MindWork AI Studio/Tools/Security/PromptInjectionFindingCategoryExtensions.cs @@ -6,18 +6,21 @@ public static class PromptInjectionFindingCategoryExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PromptInjectionFindingCategoryExtensions).Namespace, nameof(PromptInjectionFindingCategoryExtensions)); - public static string GetDisplayName(this PromptInjectionFindingCategory category) => category switch + extension(PromptInjectionFindingCategory category) { - PromptInjectionFindingCategory.OVERRIDE => TB("Attempt to override instructions"), - PromptInjectionFindingCategory.ROLE_OVERRIDE => TB("Attempt to change the AI's role"), - PromptInjectionFindingCategory.EXFILTRATION => TB("Attempt to expose protected data"), - PromptInjectionFindingCategory.JAILBREAK => TB("Attempt to bypass safeguards"), - PromptInjectionFindingCategory.AGENT_MANIPULATION => TB("Attempt to manipulate an agent"), - PromptInjectionFindingCategory.DELIMITER_EVASION => TB("Hidden instructions using delimiters"), - PromptInjectionFindingCategory.MARKUP_EVASION => TB("Hidden instructions using markup"), - PromptInjectionFindingCategory.ENCODING_EVASION => TB("Hidden instructions using encoding"), - PromptInjectionFindingCategory.PERSISTENCE => TB("Persistent or delayed instruction"), - PromptInjectionFindingCategory.EVASION => TB("Obfuscated instruction"), - _ => TB("Unknown"), - }; + public string GetDisplayName() => category switch + { + PromptInjectionFindingCategory.OVERRIDE => TB("Attempt to override instructions"), + PromptInjectionFindingCategory.ROLE_OVERRIDE => TB("Attempt to change the AI's role"), + PromptInjectionFindingCategory.EXFILTRATION => TB("Attempt to expose protected data"), + PromptInjectionFindingCategory.JAILBREAK => TB("Attempt to bypass safeguards"), + PromptInjectionFindingCategory.AGENT_MANIPULATION => TB("Attempt to manipulate an agent"), + PromptInjectionFindingCategory.DELIMITER_EVASION => TB("Hidden instructions using delimiters"), + PromptInjectionFindingCategory.MARKUP_EVASION => TB("Hidden instructions using markup"), + PromptInjectionFindingCategory.ENCODING_EVASION => TB("Hidden instructions using encoding"), + PromptInjectionFindingCategory.PERSISTENCE => TB("Persistent or delayed instruction"), + PromptInjectionFindingCategory.EVASION => TB("Obfuscated instruction"), + _ => TB("Unknown"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Security/PromptInjectionSourceKindExtensions.cs b/app/MindWork AI Studio/Tools/Security/PromptInjectionSourceKindExtensions.cs index 171fd3270..c46569781 100644 --- a/app/MindWork AI Studio/Tools/Security/PromptInjectionSourceKindExtensions.cs +++ b/app/MindWork AI Studio/Tools/Security/PromptInjectionSourceKindExtensions.cs @@ -6,15 +6,18 @@ public static class PromptInjectionSourceKindExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PromptInjectionSourceKindExtensions).Namespace, nameof(PromptInjectionSourceKindExtensions)); - public static string GetDisplayName(this PromptInjectionSourceKind kind) => kind switch + extension(PromptInjectionSourceKind kind) { - PromptInjectionSourceKind.WEB_CONTENT => TB("Web content"), - PromptInjectionSourceKind.FILE_CONTENT => TB("File content"), - PromptInjectionSourceKind.CHAT_ATTACHMENT => TB("Chat attachment"), - PromptInjectionSourceKind.RETRIEVAL_CONTEXT => TB("Retrieved context"), - PromptInjectionSourceKind.DATA_SOURCE_DESCRIPTION => TB("Data source description"), - PromptInjectionSourceKind.MAIL_CONTENT => TB("Mail content"), - PromptInjectionSourceKind.MODEL_ANSWER => TB("AI answer"), - _ => TB("Unknown"), - }; + public string GetDisplayName() => kind switch + { + PromptInjectionSourceKind.WEB_CONTENT => TB("Web content"), + PromptInjectionSourceKind.FILE_CONTENT => TB("File content"), + PromptInjectionSourceKind.CHAT_ATTACHMENT => TB("Chat attachment"), + PromptInjectionSourceKind.RETRIEVAL_CONTEXT => TB("Retrieved context"), + PromptInjectionSourceKind.DATA_SOURCE_DESCRIPTION => TB("Data source description"), + PromptInjectionSourceKind.MAIL_CONTENT => TB("Mail content"), + PromptInjectionSourceKind.MODEL_ANSWER => TB("AI answer"), + _ => TB("Unknown"), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/SourceDocumentLocationExtensions.cs b/app/MindWork AI Studio/Tools/SourceDocumentLocationExtensions.cs index 59504d6f9..e53f6ff72 100644 --- a/app/MindWork AI Studio/Tools/SourceDocumentLocationExtensions.cs +++ b/app/MindWork AI Studio/Tools/SourceDocumentLocationExtensions.cs @@ -17,65 +17,67 @@ public static class SourceDocumentLocationExtensions private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourceDocumentLocationExtensions).Namespace, nameof(SourceDocumentLocationExtensions)); - /// - /// Opens the document in the program the system uses for it, at its page. - /// - /// - /// Whether the program can be sent to a page is the runtime's business, and it says afterwards - /// whether it managed to. Nothing is shown about that: the document is open, and the page is - /// named where the user asked to open it. - /// - /// The document to open, and the page to show. - /// The runtime, which opens the document. - public static async Task OpenAsync(this SourceDocumentLocation document, RustService rustService) + /// The document to show, and the page to show it at. + extension(SourceDocumentLocation document) { - OpenDocumentResponse response; - try + /// + /// Opens the document in the program the system uses for it, at its page. + /// + /// + /// Whether the program can be sent to a page is the runtime's business, and it says afterwards + /// whether it managed to. Nothing is shown about that: the document is open, and the page is + /// named where the user asked to open it. + /// + /// The runtime, which opens the document. + public async Task OpenAsync(RustService rustService) { - response = await rustService.TryOpenDocumentInSystemViewer(document.Path, document.PageNumber); - } - catch (Exception e) - { - LOGGER.LogWarning(e, "Could not open a source document."); - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, TB("Could not open the document."))); - return; + OpenDocumentResponse response; + try + { + response = await rustService.TryOpenDocumentInSystemViewer(document.Path, document.PageNumber); + } + catch (Exception e) + { + LOGGER.LogWarning(e, "Could not open a source document."); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, TB("Could not open the document."))); + return; + } + + if (response.Success) + return; + + var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue; + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("Could not open the document: {0}"), issue))); } - if (response.Success) - return; - - var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue; - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("Could not open the document: {0}"), issue))); - } - - /// - /// Opens the file browser of the system and selects the document in it. - /// - /// - /// The second way out: a document which the system opens in the wrong program, or which the user - /// wants to move or send on instead of read, is reached from here without being opened. This is - /// the same way out the embeddings page offers for a file it could not read. - /// - /// The document to show. - /// The runtime, which opens the file browser. - public static async Task ShowInFileManagerAsync(this SourceDocumentLocation document, RustService rustService) - { - OpenPathResponse response; - try + /// + /// Opens the file browser of the system and selects the document in it. + /// + /// + /// The second way out: a document which the system opens in the wrong program, or which the user + /// wants to move or send on instead of read, is reached from here without being opened. This is + /// the same way out the embeddings page offers for a file it could not read. + /// + /// The runtime, which opens the file browser. + public async Task ShowInFileManagerAsync(RustService rustService) { - response = await rustService.TryOpenPathInRuntimeFileManager(document.Path); - } - catch (Exception e) - { - LOGGER.LogWarning(e, "Could not show a source document in the file manager."); - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, TB("Could not open the file location."))); - return; - } + OpenPathResponse response; + try + { + response = await rustService.TryOpenPathInRuntimeFileManager(document.Path); + } + catch (Exception e) + { + LOGGER.LogWarning(e, "Could not show a source document in the file manager."); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, TB("Could not open the file location."))); + return; + } - if (response.Success) - return; + if (response.Success) + return; - var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue; - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(TB("Could not open the file location: {0}"), issue))); + var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue; + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(TB("Could not open the file location: {0}"), issue))); + } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/SourceExtensions.cs b/app/MindWork AI Studio/Tools/SourceExtensions.cs index 7fb17c4db..f7d9e5ffe 100644 --- a/app/MindWork AI Studio/Tools/SourceExtensions.cs +++ b/app/MindWork AI Studio/Tools/SourceExtensions.cs @@ -28,13 +28,58 @@ public static partial class SourceExtensions /// The address. public static string CreateMailSourceUrl(string mailboxId, string mailId) => $"{MAIL_SOURCE_SCHEME}://{mailboxId}/{mailId}"; - /// - /// Whether a source names a mail in one of the mailboxes of the user. - /// /// The source. - /// True for a mail, which nothing can open by its address. - public static bool IsMailSource(this ISource source) => - Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) && address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase); + extension(ISource source) + { + /// + /// Whether a source names a mail in one of the mailboxes of the user. + /// + /// True for a mail, which nothing can open by its address. + public bool IsMailSource() => + Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) && address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase); + + /// + /// Whether nothing can open a source by its address: a mail, or the passage of a data source + /// whose path names nothing to open, see CreateRetrievalSourceUrl. + /// + /// True for a source which is named as text, without a link. + public bool HasNoLink() => + Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) + && (address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase) || address.Scheme.Equals(RETRIEVAL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase)); + + /// + /// Reads which document a source names, and which page of it. + /// + /// + /// Only a source which names a file has such a location; a web source is opened by the browser + /// and never asks. The page rides in the fragment of the link as `page=N`, which is what the PDF + /// open parameters call for. A chat written before v26.9.1 carries `chunk=N` instead, which names + /// nothing a program could be sent to: such a source keeps its document and loses only the page. + /// + /// The document and its page, or the default when the source names no file. + /// Whether the source names a file. + public bool TryGetDocumentLocation(out SourceDocumentLocation location) + { + location = default; + if (string.IsNullOrWhiteSpace(source.URL)) + return false; + + var cleanedUrl = source.URL.Trim().Replace("\r", string.Empty).Replace("\n", string.Empty); + if (!Uri.TryCreate(cleanedUrl, UriKind.Absolute, out var absoluteUri) || !absoluteUri.IsFile) + return false; + + // + // The link was made from a path of this system, so reading it back gives that path again -- + // percent-encoded spaces and umlauts included, and with the separators this system uses. + // + var path = absoluteUri.LocalPath; + if (string.IsNullOrWhiteSpace(path)) + return false; + + location = new(path, ReadPageFromFragment(absoluteUri.Fragment)); + return true; + } + } /// /// The scheme of the address a passage of a data source leaves as a source, when its path is nothing a program can open. @@ -56,16 +101,6 @@ public static partial class SourceExtensions /// The address. public static string CreateRetrievalSourceUrl(string name, string path) => $"{RETRIEVAL_SOURCE_SCHEME}://context/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(path)}"; - /// - /// Whether nothing can open a source by its address: a mail, or the passage of a data source - /// whose path names nothing to open, see CreateRetrievalSourceUrl. - /// - /// The source. - /// True for a source which is named as text, without a link. - public static bool HasNoLink(this ISource source) => - Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) - && (address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase) || address.Scheme.Equals(RETRIEVAL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase)); - private static void AppendMarkdownLink(StringBuilder sb, string title, string url) { sb.Append('['); @@ -136,46 +171,119 @@ public static partial class SourceExtensions return c is '-' or '.' or '_' or '~' or ':' or '/' or '?' or '#' or '[' or ']' or '@' or '!' or '$' or '&' or '\'' or '(' or ')' or '*' or '+' or ',' or ';' or '='; } - /// - /// Sorts a list of sources into the groups it is shown in, and numbers them. - /// - /// - /// The order of the groups and the running number are what a reader follows, and they have to - /// be the same wherever the list appears: in the chat, in an exported document, and in the - /// clipboard. This is why both the chat and the Markdown below ask here instead of sorting the - /// list themselves. - /// - /// The list of sources to sort. - /// The groups which have sources, in the order they are shown; empty when there are none. - public static IReadOnlyList GroupSources(this IList sources) + /// The list of sources. + extension(IList sources) { - var llmSources = new List(); - var toolSources = new List(); - var ragSources = new List(); - foreach (var source in sources) + /// + /// Sorts a list of sources into the groups it is shown in, and numbers them. + /// + /// + /// The order of the groups and the running number are what a reader follows, and they have to + /// be the same wherever the list appears: in the chat, in an exported document, and in the + /// clipboard. This is why both the chat and the Markdown below ask here instead of sorting the + /// list themselves. + /// + /// The groups which have sources, in the order they are shown; empty when there are none. + public IReadOnlyList GroupSources() { - switch (source.Origin) + var llmSources = new List(); + var toolSources = new List(); + var ragSources = new List(); + foreach (var source in sources) { - case SourceOrigin.LLM: - llmSources.Add(source); - break; + switch (source.Origin) + { + case SourceOrigin.LLM: + llmSources.Add(source); + break; - case SourceOrigin.TOOL: - toolSources.Add(source); - break; + case SourceOrigin.TOOL: + toolSources.Add(source); + break; - case SourceOrigin.RAG: - ragSources.Add(source); - break; + case SourceOrigin.RAG: + ragSources.Add(source); + break; + } } + + var groups = new List(3); + var sourceNum = 0; + AddGroup(groups, TB("Sources provided by the AI"), llmSources, ref sourceNum); + AddGroup(groups, TB("Sources used by tools"), toolSources, ref sourceNum); + AddGroup(groups, TB("Sources provided by the data providers"), ragSources, ref sourceNum); + return groups; } - var groups = new List(3); - var sourceNum = 0; - AddGroup(groups, TB("Sources provided by the AI"), llmSources, ref sourceNum); - AddGroup(groups, TB("Sources used by tools"), toolSources, ref sourceNum); - AddGroup(groups, TB("Sources provided by the data providers"), ragSources, ref sourceNum); - return groups; + /// + /// Converts a list of sources to a markdown-formatted string. + /// + /// Whether a link into a local file may name its page; see the method below. + /// A markdown-formatted string representing the sources. + public string ToMarkdown(bool keepPageAnchors = true) + { + var sb = new StringBuilder(); + foreach (var group in sources.GroupSources()) + { + if (sb.Length > 0) + sb.AppendLine(); + + sb.Append("## "); + sb.AppendLine(group.Heading); + + foreach (var numberedSource in group.Sources) + { + sb.Append($"- [{numberedSource.Number}] "); + + // A link to a mail, or to what only a data source knows, would lead nowhere in a document or in the clipboard, so it is named only: + if (numberedSource.Source.HasNoLink()) + sb.Append(EscapeMarkdownLinkText(numberedSource.Source.Title)); + else + AppendMarkdownLink(sb, numberedSource.Source.Title, keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL)); + + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + /// + /// Converts a list of sources to a markdown-formatted string, headed by a title of its own. + /// + /// + /// The chat shows the sources in a box below the answer, so the reader sees where the one ends + /// and the others begin. An exported document is one text: without a heading of its own, the + /// source list would read like one more section the model wrote. This is why the export asks + /// for this and the chat does not. + /// + /// Whether a link into a local file may name its page. + /// A markdown-formatted string representing the sources, or an empty string when there are none. + public string ToExportMarkdown(bool keepPageAnchors = true) + { + var sourcesMarkdown = sources.ToMarkdown(keepPageAnchors); + if (string.IsNullOrWhiteSpace(sourcesMarkdown)) + return string.Empty; + + return $"# {TB("Sources")}{Environment.NewLine}{Environment.NewLine}{sourcesMarkdown}"; + } + + /// + /// Merges a list of added sources into an existing list of sources, avoiding duplicates based on normalized URLs. + /// + /// The list of sources to add. + public void MergeSources(IEnumerable addedSources) + { + var sourceIdentities = sources + .Select(source => GetSourceIdentity(source.URL)) + .ToHashSet(StringComparer.Ordinal); + + foreach (var addedSource in addedSources) + { + if (sourceIdentities.Add(GetSourceIdentity(addedSource.URL))) + sources.Add((Source)addedSource); + } + } } private static void AddGroup(ICollection groups, string heading, IReadOnlyList sources, ref int sourceNum) @@ -190,40 +298,6 @@ public static partial class SourceExtensions groups.Add(new(heading, numberedSources)); } - /// - /// Converts a list of sources to a markdown-formatted string. - /// - /// The list of sources to convert. - /// Whether a link into a local file may name its page; see the method below. - /// A markdown-formatted string representing the sources. - public static string ToMarkdown(this IList sources, bool keepPageAnchors = true) - { - var sb = new StringBuilder(); - foreach (var group in sources.GroupSources()) - { - if (sb.Length > 0) - sb.AppendLine(); - - sb.Append("## "); - sb.AppendLine(group.Heading); - - foreach (var numberedSource in group.Sources) - { - sb.Append($"- [{numberedSource.Number}] "); - - // A link to a mail, or to what only a data source knows, would lead nowhere in a document or in the clipboard, so it is named only: - if (numberedSource.Source.HasNoLink()) - sb.Append(EscapeMarkdownLinkText(numberedSource.Source.Title)); - else - AppendMarkdownLink(sb, numberedSource.Source.Title, keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL)); - - sb.AppendLine(); - } - } - - return sb.ToString(); - } - /// /// Takes the page off a link into a local file, for a reader which cannot follow it. /// @@ -247,61 +321,6 @@ public static partial class SourceExtensions return absoluteUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Fragment, UriFormat.UriEscaped); } - /// - /// Converts a list of sources to a markdown-formatted string, headed by a title of its own. - /// - /// - /// The chat shows the sources in a box below the answer, so the reader sees where the one ends - /// and the others begin. An exported document is one text: without a heading of its own, the - /// source list would read like one more section the model wrote. This is why the export asks - /// for this and the chat does not. - /// - /// The list of sources to convert. - /// Whether a link into a local file may name its page. - /// A markdown-formatted string representing the sources, or an empty string when there are none. - public static string ToExportMarkdown(this IList sources, bool keepPageAnchors = true) - { - var sourcesMarkdown = sources.ToMarkdown(keepPageAnchors); - if (string.IsNullOrWhiteSpace(sourcesMarkdown)) - return string.Empty; - - return $"# {TB("Sources")}{Environment.NewLine}{Environment.NewLine}{sourcesMarkdown}"; - } - - /// - /// Reads which document a source names, and which page of it. - /// - /// - /// Only a source which names a file has such a location; a web source is opened by the browser - /// and never asks. The page rides in the fragment of the link as `page=N`, which is what the PDF - /// open parameters call for. A chat written before v26.9.1 carries `chunk=N` instead, which names - /// nothing a program could be sent to: such a source keeps its document and loses only the page. - /// - /// The source to read. - /// The document and its page, or the default when the source names no file. - /// Whether the source names a file. - public static bool TryGetDocumentLocation(this ISource source, out SourceDocumentLocation location) - { - location = default; - if (string.IsNullOrWhiteSpace(source.URL)) - return false; - - var cleanedUrl = source.URL.Trim().Replace("\r", string.Empty).Replace("\n", string.Empty); - if (!Uri.TryCreate(cleanedUrl, UriKind.Absolute, out var absoluteUri) || !absoluteUri.IsFile) - return false; - - // - // The link was made from a path of this system, so reading it back gives that path again -- - // percent-encoded spaces and umlauts included, and with the separators this system uses. - // - var path = absoluteUri.LocalPath; - if (string.IsNullOrWhiteSpace(path)) - return false; - - location = new(path, ReadPageFromFragment(absoluteUri.Fragment)); - return true; - } - private static int? ReadPageFromFragment(string fragment) { const string PAGE_PARAMETER = "page="; @@ -317,24 +336,6 @@ public static partial class SourceExtensions return null; } - /// - /// Merges a list of added sources into an existing list of sources, avoiding duplicates based on normalized URLs. - /// - /// The existing list of sources to merge into. - /// The list of sources to add. - public static void MergeSources(this IList sources, IEnumerable addedSources) - { - var sourceIdentities = sources - .Select(source => GetSourceIdentity(source.URL)) - .ToHashSet(StringComparer.Ordinal); - - foreach (var addedSource in addedSources) - { - if (sourceIdentities.Add(GetSourceIdentity(addedSource.URL))) - sources.Add((Source)addedSource); - } - } - /// /// What tells two sources apart: the address without its fragment. /// diff --git a/app/MindWork AI Studio/Tools/TaskExtensions.cs b/app/MindWork AI Studio/Tools/TaskExtensions.cs index e8e98d0a6..366d4f5ec 100644 --- a/app/MindWork AI Studio/Tools/TaskExtensions.cs +++ b/app/MindWork AI Studio/Tools/TaskExtensions.cs @@ -4,25 +4,28 @@ public static class TaskExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(TaskExtensions)); - /// - /// Lets a task run on its own, but keeps an eye on how it ends. - /// - /// - /// Use this wherever a task is started without awaiting it. Discarding one instead means that nobody - /// ever looks at its outcome: the task carries its exception until the garbage collector finalizes it, - /// and only then does it show up as an unobserved task exception — naming a task type, without any - /// hint at what was running. Around a circuit which is gone, that is the common case: components of a - /// reloaded or sleeping window still receive events and still schedule work. - /// /// The task to watch. - /// What this task was doing, for the log entry. - public static void Observe(this Task task, string context) + extension(Task task) { - task.ContinueWith(finishedTask => - LogFailure(finishedTask.Exception, context), - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); + /// + /// Lets a task run on its own, but keeps an eye on how it ends. + /// + /// + /// Use this wherever a task is started without awaiting it. Discarding one instead means that nobody + /// ever looks at its outcome: the task carries its exception until the garbage collector finalizes it, + /// and only then does it show up as an unobserved task exception — naming a task type, without any + /// hint at what was running. Around a circuit which is gone, that is the common case: components of a + /// reloaded or sleeping window still receive events and still schedule work. + /// + /// What this task was doing, for the log entry. + public void Observe(string context) + { + task.ContinueWith(finishedTask => + LogFailure(finishedTask.Exception, context), + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } } private static void LogFailure(AggregateException? exception, string context) diff --git a/app/MindWork AI Studio/Tools/TextColorExtensions.cs b/app/MindWork AI Studio/Tools/TextColorExtensions.cs index eb70213b4..67518d42b 100644 --- a/app/MindWork AI Studio/Tools/TextColorExtensions.cs +++ b/app/MindWork AI Studio/Tools/TextColorExtensions.cs @@ -4,15 +4,18 @@ namespace AIStudio.Tools; public static class TextColorExtensions { - public static string GetHTMLColor(this TextColor color, SettingsManager settingsManager) => color switch + extension(TextColor color) { - TextColor.DEFAULT => string.Empty, + public string GetHTMLColor(SettingsManager settingsManager) => color switch + { + TextColor.DEFAULT => string.Empty, - TextColor.ERROR => settingsManager.IsDarkMode ? "#ff6c6c" : "#ff0000", - TextColor.WARN => settingsManager.IsDarkMode ? "#c7a009" : "#c7c000", - TextColor.SUCCESS => settingsManager.IsDarkMode ? "#08b342" : "#009933", - TextColor.INFO => settingsManager.IsDarkMode ? "#5279b8" : "#2d67c4", + TextColor.ERROR => settingsManager.IsDarkMode ? "#ff6c6c" : "#ff0000", + TextColor.WARN => settingsManager.IsDarkMode ? "#c7a009" : "#c7c000", + TextColor.SUCCESS => settingsManager.IsDarkMode ? "#08b342" : "#009933", + TextColor.INFO => settingsManager.IsDarkMode ? "#5279b8" : "#2d67c4", - _ => string.Empty, - }; + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/SafeSearchPolicyExtensions.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/SafeSearchPolicyExtensions.cs index 232aba56c..dd232b97c 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/SafeSearchPolicyExtensions.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/SafeSearchPolicyExtensions.cs @@ -2,29 +2,32 @@ namespace AIStudio.Tools.ToolCallingSystem; public static class SafeSearchPolicyExtensions { - /// - /// The value SearXNG expects for its safesearch parameter. - /// - /// - /// SearXNG takes the level as a number. That number stays here, at the edge towards the - /// search engine, instead of travelling through the settings where nobody can read it. - /// - public static string ToSearXNGValue(this SafeSearchPolicy policy) => policy switch + extension(SafeSearchPolicy policy) { - SafeSearchPolicy.OFF => "0", - SafeSearchPolicy.MODERATE => "1", - SafeSearchPolicy.STRICT => "2", + /// + /// The value SearXNG expects for its safesearch parameter. + /// + /// + /// SearXNG takes the level as a number. That number stays here, at the edge towards the + /// search engine, instead of travelling through the settings where nobody can read it. + /// + public string ToSearXNGValue() => policy switch + { + SafeSearchPolicy.OFF => "0", + SafeSearchPolicy.MODERATE => "1", + SafeSearchPolicy.STRICT => "2", - _ => "0", - }; + _ => "0", + }; - /// - /// The value Tavily expects for its safe search parameter. - /// - /// - /// Tavily knows filtering only as on or off, so a moderate policy is filtered as strictly as - /// a strict one. Of the two ways to round that, filtering more than was asked for is the one - /// that cannot surprise anyone. - /// - public static bool ToTavilyValue(this SafeSearchPolicy policy) => policy is not SafeSearchPolicy.OFF; + /// + /// The value Tavily expects for its safe search parameter. + /// + /// + /// Tavily knows filtering only as on or off, so a moderate policy is filtered as strictly as + /// a strict one. Of the two ways to round that, filtering more than was asked for is the one + /// that cannot surprise anyone. + /// + public bool ToTavilyValue() => policy is not SafeSearchPolicy.OFF; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingAvailabilityExtensions.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingAvailabilityExtensions.cs index b4d7c83b2..a3c4203cc 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingAvailabilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingAvailabilityExtensions.cs @@ -8,17 +8,20 @@ public static class ToolCallingAvailabilityExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ToolCallingAvailabilityExtensions).Namespace, nameof(ToolCallingAvailabilityExtensions)); - public static ToolCallingAvailability GetToolCallingAvailability(this AIStudio.Settings.Provider provider) + extension(AIStudio.Settings.Provider provider) { - if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE) - return new(false, TB("Please select an LLM provider.")); + public ToolCallingAvailability GetToolCallingAvailability() + { + if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE) + return new(false, TB("Please select an LLM provider.")); - var modelProfile = provider.GetModelProfile(); - var supportsRequiredApis = modelProfile.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API); + var modelProfile = provider.GetModelProfile(); + var supportsRequiredApis = modelProfile.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API); - if (!supportsRequiredApis || !modelProfile.Has(Capability.FUNCTION_CALLING)) - return new(false, TB("Tool calling support is not enabled by default for this model, but you can enable this capability in the expert settings of the provider if you are sure the model supports it.")); + if (!supportsRequiredApis || !modelProfile.Has(Capability.FUNCTION_CALLING)) + return new(false, TB("Tool calling support is not enabled by default for this model, but you can enable this capability in the expert settings of the provider if you are sure the model supports it.")); - return ToolCallingAvailability.Available(); + return ToolCallingAvailability.Available(); + } } } diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolArguments.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolArguments.cs index bac8cdd54..c5b30acd1 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolArguments.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolArguments.cs @@ -56,6 +56,12 @@ internal static class MailToolArguments /// What the tool does with the mails, e.g., "search", completing "the mailboxes to ...". /// The ids of the mailboxes the tool offers, or none while no mailboxes are known. /// The schema, for further arguments. + /// + /// A classic extension method instead of a member of an extension block: for those, the compiler of + /// the .NET 10.0.1xx SDK warns about every mailbox id passed one by one to the params parameter + /// (CS8620, dotnet/roslyn#80024). Move it once the SDK in metadata.txt carries the fix; MWAIS0016 makes + /// the same exception until then. + /// public static ToolParameterSchemaBuilder AddMailConditions(this ToolParameterSchemaBuilder builder, string toolAction, params string[] mailboxIds) => builder .OptionalStringArray(MAILBOX_IDS_ARGUMENT, $"Optional IDs of the mailboxes to {toolAction}, out of those listed in the description of this tool. Leave it out to {toolAction} all of them.", mailboxIds) .OptionalString(FROM_ARGUMENT, $"Optional part of the address or the name of the sender, such as 'alice@example.org', 'example.org', or 'Alice'. At most {MAX_ADDRESS_CHARACTERS} characters.") diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/WebSearch/WebSearchBackendExtensions.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/WebSearch/WebSearchBackendExtensions.cs index 8c4105a2f..13a74d4ae 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/WebSearch/WebSearchBackendExtensions.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/WebSearch/WebSearchBackendExtensions.cs @@ -2,21 +2,24 @@ namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch; public static class WebSearchBackendExtensions { - /// - /// The name of one search service, as the user reads it and as a search result reports it. - /// - /// - /// Product names, so they are not translated: SearXNG is called SearXNG in every language. - /// What gets stored is the enum member name instead, which is what leaves this free to be - /// worded for people — in the settings dropdown, in a note explaining which service - /// answered, and in the result the model reads. - /// - public static string ToName(this WebSearchBackend backend) => backend switch + extension(WebSearchBackend backend) { - WebSearchBackend.SEARXNG => "SearXNG", - WebSearchBackend.STAAN => "Staan", - WebSearchBackend.TAVILY => "Tavily", + /// + /// The name of one search service, as the user reads it and as a search result reports it. + /// + /// + /// Product names, so they are not translated: SearXNG is called SearXNG in every language. + /// What gets stored is the enum member name instead, which is what leaves this free to be + /// worded for people — in the settings dropdown, in a note explaining which service + /// answered, and in the result the model reads. + /// + public string ToName() => backend switch + { + WebSearchBackend.SEARXNG => "SearXNG", + WebSearchBackend.STAAN => "Staan", + WebSearchBackend.TAVILY => "Tavily", - _ => backend.ToString(), - }; + _ => backend.ToString(), + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Validation/CloudProcessingExtensions.cs b/app/MindWork AI Studio/Tools/Validation/CloudProcessingExtensions.cs index 0b91a6e6e..3c8a25dcf 100644 --- a/app/MindWork AI Studio/Tools/Validation/CloudProcessingExtensions.cs +++ b/app/MindWork AI Studio/Tools/Validation/CloudProcessingExtensions.cs @@ -2,16 +2,19 @@ namespace AIStudio.Tools.Validation; public static class CloudProcessingExtensions { - /// - /// Whether what the user acknowledged covers what the data source sends to the cloud now. - /// - /// - /// An acknowledgment holds only for the providers it was given for. When the data source starts - /// to send its recordings to a transcription provider in the cloud, say since a new pattern - /// selects them, the user has to acknowledge that once more. - /// /// What the user acknowledged. - /// What the data source sends to the cloud with its current settings. - /// True when nothing is sent that the user did not acknowledge. - public static bool Covers(this CloudProcessing acknowledged, CloudProcessing current) => (current & ~acknowledged) is CloudProcessing.NONE; + extension(CloudProcessing acknowledged) + { + /// + /// Whether what the user acknowledged covers what the data source sends to the cloud now. + /// + /// + /// An acknowledgment holds only for the providers it was given for. When the data source starts + /// to send its recordings to a transcription provider in the cloud, say since a new pattern + /// selects them, the user has to acknowledge that once more. + /// + /// What the data source sends to the cloud with its current settings. + /// True when nothing is sent that the user did not acknowledge. + public bool Covers(CloudProcessing current) => (current & ~acknowledged) is CloudProcessing.NONE; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.11.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.11.1.md index 16e84318a..8d8a6ffa6 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.11.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.11.1.md @@ -21,6 +21,7 @@ - Fixed chats being stored although you chose "Store chats manually" or "Disable workspaces". AI Studio now stores a chat only when you click "Save chat", or not at all while workspaces are disabled. In both modes, leaving a chat whose answer is still being written asks you first and stops that answer. Chats outside of workspaces that earlier versions stored this way appear among your disappearing chats once you use workspaces again. - Fixed saving a code block as a file, where the last line could end differently from all other lines, for example, on Windows. All lines of the file now end the same way. - Fixed the result of a tool in the chat showing umlauts, quotation marks, and other special characters as codes instead of the characters themselves. +- Fixed the Assistant Builder telling you in English why it could not use the answer of the AI model, even when you chose another language for AI Studio. - Upgraded several libraries to improve security. - Upgraded Tauri to v2.12.5. - Upgraded MudBlazor to v9.12.0. diff --git a/app/SharedTools/FNVHash.cs b/app/SharedTools/FNVHash.cs index a60beeffc..edbc6882a 100644 --- a/app/SharedTools/FNVHash.cs +++ b/app/SharedTools/FNVHash.cs @@ -12,51 +12,55 @@ public static class FNVHash private const uint FNV_PRIME_32_BIT = 16_777_619; private const ulong FNV_PRIME_64_BIT = 1_099_511_628_211; - /// - /// Computes the 32bit FNV-1a hash of a string. - /// /// The string to hash. - /// The 32bit FNV-1a hash of the string. - public static uint ToFNV32(this string text) => ToFNV32(text.AsSpan()); - - /// - /// Computes the 32bit FNV-1a hash of a string. - /// - /// The string to hash. - /// The 32bit FNV-1a hash of the string. - public static uint ToFNV32(this ReadOnlySpan text) + extension(string text) { - var hash = FNV_OFFSET_BASIS_32_BIT; - foreach (var c in text) - { - hash ^= c; - hash *= FNV_PRIME_32_BIT; - } + /// + /// Computes the 32bit FNV-1a hash of a string. + /// + /// The 32bit FNV-1a hash of the string. + public uint ToFNV32() => ToFNV32(text.AsSpan()); - return hash; + /// + /// Computes the 64bit FNV-1a hash of a string. + /// + /// The 64bit FNV-1a hash of the string. + public ulong ToFNV64() => ToFNV64(text.AsSpan()); } - - /// - /// Computes the 64bit FNV-1a hash of a string. - /// + /// The string to hash. - /// The 64bit FNV-1a hash of the string. - public static ulong ToFNV64(this string text) => ToFNV64(text.AsSpan()); - - /// - /// Computes the 64bit FNV-1a hash of a string. - /// - /// The string to hash. - /// The 64bit FNV-1a hash of the string. - public static ulong ToFNV64(this ReadOnlySpan text) + extension(ReadOnlySpan text) { - var hash = FNV_OFFSET_BASIS_64_BIT; - foreach (var c in text) + /// + /// Computes the 32bit FNV-1a hash of a string. + /// + /// The 32bit FNV-1a hash of the string. + public uint ToFNV32() { - hash ^= c; - hash *= FNV_PRIME_64_BIT; + var hash = FNV_OFFSET_BASIS_32_BIT; + foreach (var c in text) + { + hash ^= c; + hash *= FNV_PRIME_32_BIT; + } + + return hash; } - return hash; + /// + /// Computes the 64bit FNV-1a hash of a string. + /// + /// The 64bit FNV-1a hash of the string. + public ulong ToFNV64() + { + var hash = FNV_OFFSET_BASIS_64_BIT; + foreach (var c in text) + { + hash ^= c; + hash *= FNV_PRIME_64_BIT; + } + + return hash; + } } } \ No newline at end of file diff --git a/app/SharedTools/RIDExtensions.cs b/app/SharedTools/RIDExtensions.cs index e8552c3da..8e17465b9 100644 --- a/app/SharedTools/RIDExtensions.cs +++ b/app/SharedTools/RIDExtensions.cs @@ -34,45 +34,51 @@ public static class RIDExtensions }; } - public static string AsMicrosoftRid(this RID rid) => rid switch + extension(RID rid) { - RID.WIN_X64 => "win-x64", - RID.WIN_ARM64 => "win-arm64", + public string AsMicrosoftRid() => rid switch + { + RID.WIN_X64 => "win-x64", + RID.WIN_ARM64 => "win-arm64", - RID.LINUX_X64 => "linux-x64", - RID.LINUX_ARM64 => "linux-arm64", + RID.LINUX_X64 => "linux-x64", + RID.LINUX_ARM64 => "linux-arm64", - RID.OSX_X64 => "osx-x64", - RID.OSX_ARM64 => "osx-arm64", + RID.OSX_X64 => "osx-x64", + RID.OSX_ARM64 => "osx-arm64", - _ => string.Empty, - }; + _ => string.Empty, + }; + + public string ToUserFriendlyName() => rid switch + { + RID.WIN_X64 => "Windows x64", + RID.WIN_ARM64 => "Windows ARM64", + + RID.LINUX_X64 => "Linux x64", + RID.LINUX_ARM64 => "Linux ARM64", + + RID.OSX_X64 => "macOS x64", + RID.OSX_ARM64 => "macOS ARM64", + + _ => "unknown", + }; + } - public static string ToUserFriendlyName(this RID rid) => rid switch + extension(string rid) { - RID.WIN_X64 => "Windows x64", - RID.WIN_ARM64 => "Windows ARM64", + public RID ToRID() => rid switch + { + "win-x64" => RID.WIN_X64, + "win-arm64" => RID.WIN_ARM64, - RID.LINUX_X64 => "Linux x64", - RID.LINUX_ARM64 => "Linux ARM64", + "linux-x64" => RID.LINUX_X64, + "linux-arm64" => RID.LINUX_ARM64, - RID.OSX_X64 => "macOS x64", - RID.OSX_ARM64 => "macOS ARM64", + "osx-x64" => RID.OSX_X64, + "osx-arm64" => RID.OSX_ARM64, - _ => "unknown", - }; - - public static RID ToRID(this string rid) => rid switch - { - "win-x64" => RID.WIN_X64, - "win-arm64" => RID.WIN_ARM64, - - "linux-x64" => RID.LINUX_X64, - "linux-arm64" => RID.LINUX_ARM64, - - "osx-x64" => RID.OSX_X64, - "osx-arm64" => RID.OSX_ARM64, - - _ => RID.NONE, - }; + _ => RID.NONE, + }; + } } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 716a5fb13..f71fb7372 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -19,3 +19,4 @@ MWAIS0013 | Usage | Error | ModelPatternLiteralAnalyzer MWAIS0014 | Usage | Error | MissingInterpolationAnalyzer MWAIS0015 | Usage | Error | JsonSourceGenerationAnalyzer + MWAIS0016 | Style | Error | ClassicExtensionMethodAnalyzer diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index 38cf392b3..fc62ed47e 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -17,4 +17,5 @@ public static class Identifier public const string MODEL_PATTERN_LITERAL_ANALYZER = $"{Tools.ID_PREFIX}0013"; public const string MISSING_INTERPOLATION_ANALYZER = $"{Tools.ID_PREFIX}0014"; public const string JSON_SOURCE_GENERATION_ANALYZER = $"{Tools.ID_PREFIX}0015"; + public const string CLASSIC_EXTENSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0016"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/ClassicExtensionMethodAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/ClassicExtensionMethodAnalyzer.cs new file mode 100644 index 000000000..fec369c29 --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/ClassicExtensionMethodAnalyzer.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; +using System.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SourceCodeRules.StyleAnalyzers; + +/// +/// Refuses extension methods which are declared with a this parameter instead of in an extension block. +/// +/// +/// AI Studio declares its extension members in extension blocks, one per receiver, so that all members of a +/// receiver read alike and extension properties can join them. A method with a params parameter stays a +/// classic one for now: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns about +/// every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Drop that exception +/// once the SDK in metadata.txt carries the fix. +/// +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public sealed class ClassicExtensionMethodAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = Identifier.CLASSIC_EXTENSION_METHOD_ANALYZER; + + private const string TITLE = "Extension methods belong in extension blocks"; + + private const string MESSAGE_FORMAT = "Declare '{0}' as a member of an extension block for its receiver instead of with a this parameter"; + + private const string DESCRIPTION = "AI Studio declares its extension members in extension blocks, one per receiver. Only a method with a params parameter stays a classic extension method, until the SDK fixes dotnet/roslyn#80024."; + + private const string CATEGORY = "Style"; + + private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); + + public override ImmutableArray SupportedDiagnostics => [RULE]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeMethod(SyntaxNodeAnalysisContext context) + { + var method = (MethodDeclarationSyntax)context.Node; + var parameters = method.ParameterList.Parameters; + if (parameters.Count == 0) + return; + + var thisKeyword = parameters[0].Modifiers.FirstOrDefault(modifier => modifier.IsKind(SyntaxKind.ThisKeyword)); + if (thisKeyword.IsKind(SyntaxKind.None)) + return; + + if (parameters.Any(parameter => parameter.Modifiers.Any(SyntaxKind.ParamsKeyword))) + return; + + context.ReportDiagnostic(Diagnostic.Create(RULE, thisKeyword.GetLocation(), method.Identifier.Text)); + } +} diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs index be800ffab..b722803ba 100644 --- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs @@ -42,7 +42,7 @@ public sealed class DirectI18NGetTextAnalyzer : DiagnosticAnalyzer return; var targetMethod = method.ReducedFrom ?? method; - if (targetMethod.Name != "GetText" || targetMethod.ContainingType.Name != "ILangExtensions" || targetMethod.ContainingNamespace.ToDisplayString() != "AIStudio.Tools.PluginSystem") + if (targetMethod.Name != "GetText" || !IsLangExtensions(targetMethod.ContainingType)) return; if (context.SemanticModel.GetOperation(invocation) is IInvocationOperation operation @@ -52,6 +52,19 @@ public sealed class DirectI18NGetTextAnalyzer : DiagnosticAnalyzer context.ReportDiagnostic(Diagnostic.Create(RULE, invocation.GetLocation())); } + /// + /// Whether a method of this type is the GetText of the language plugins. + /// + /// + /// A classic extension method belongs to its static class directly. A member of an extension block + /// belongs to the block, and only the block belongs to the static class. + /// + private static bool IsLangExtensions(INamedTypeSymbol? type) + { + var staticClass = type is { IsExtension: true } ? type.ContainingType : type; + return staticClass?.Name == "ILangExtensions" && staticClass.ContainingNamespace.ToDisplayString() == "AIStudio.Tools.PluginSystem"; + } + private static bool IsCollectorCompatibleWrapper(IMethodSymbol? containingMethod, IInvocationOperation invocation) { if (containingMethod?.Name is not ("T" or "TB") diff --git a/app/Tests/Analyzers/ClassicExtensionMethodAnalyzerTests.cs b/app/Tests/Analyzers/ClassicExtensionMethodAnalyzerTests.cs new file mode 100644 index 000000000..b8205e214 --- /dev/null +++ b/app/Tests/Analyzers/ClassicExtensionMethodAnalyzerTests.cs @@ -0,0 +1,69 @@ +using AIStudio.Tests.Models.Generation; + +using Microsoft.CodeAnalysis; + +using SourceCodeRules.StyleAnalyzers; + +namespace AIStudio.Tests.Analyzers; + +/// +/// Checks that extension methods are declared in extension blocks, not with a this parameter. +/// +/// +/// AI Studio declares all its extension members in extension blocks, one per receiver. MWAIS0016 refuses the +/// classic form while compiling, except for a method with a params parameter: for a member of an extension +/// block, the compiler of the .NET 10.0.1xx SDK warns about every argument passed one by one to a params +/// parameter (CS8620, dotnet/roslyn#80024). +/// +[TestFixture] +public sealed class ClassicExtensionMethodAnalyzerTests +{ + [TestCase("public static string Label(this Meeting meeting) => meeting.Name;", "Label", TestName = "A public extension method")] + [TestCase("private static string Label(this Meeting meeting) => meeting.Name;", "Label", TestName = "A private extension method")] + [TestCase("public static string Label(this Meeting meeting, string prefix) => prefix + meeting.Name;", "Label", TestName = "An extension method with further parameters")] + [TestCase("public static T Echo(this T value) where T : class => value;", "Echo", TestName = "A generic extension method")] + [TestCase("public static void Bump(this ref int value) => value++;", "Bump", TestName = "An extension method with a ref receiver")] + [TestCase("public static System.Threading.Tasks.Task LabelAsync(this Meeting meeting) => System.Threading.Tasks.Task.FromResult(meeting.Name);", "LabelAsync", TestName = "An asynchronous extension method")] + public async Task AClassicExtensionMethodIsRefused(string member, string name) + { + var reported = await AnalyzeAsync(member); + + using (Assert.EnterMultipleScope()) + { + Assert.That(reported, Has.Count.EqualTo(1)); + Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0016")); + Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{name}'")); + } + } + + [TestCase("extension(Meeting meeting) { public string Label() => meeting.Name; }", TestName = "A member of an extension block")] + [TestCase("extension(T value) where T : class { public T Echo() => value; }", TestName = "A member of a generic extension block")] + [TestCase("public static string Label(Meeting meeting) => meeting.Name;", TestName = "A static method without a this parameter")] + [TestCase("public static string Label(this Meeting meeting, params string[] prefixes) => string.Concat(prefixes) + meeting.Name;", TestName = "An extension method with a params parameter")] + public async Task AnythingElseIsLeftAlone(string member) + { + var reported = await AnalyzeAsync(member); + + Assert.That(reported, Is.Empty); + } + + private static async Task> AnalyzeAsync(string member) + { + var source = + $$""" + namespace Sample; + + public sealed class Meeting + { + public string Name { get; set; } = "Kickoff"; + } + + public static class MeetingExtensions + { + {{member}} + } + """; + + return await CompilationHarness.AnalyzeAsync(source, new ClassicExtensionMethodAnalyzer()); + } +} diff --git a/app/Tests/Analyzers/DirectI18NGetTextAnalyzerTests.cs b/app/Tests/Analyzers/DirectI18NGetTextAnalyzerTests.cs new file mode 100644 index 000000000..a2735c47c --- /dev/null +++ b/app/Tests/Analyzers/DirectI18NGetTextAnalyzerTests.cs @@ -0,0 +1,121 @@ +using AIStudio.Tests.Models.Generation; + +using Microsoft.CodeAnalysis; + +using SourceCodeRules.UsageAnalyzers; + +namespace AIStudio.Tests.Analyzers; + +/// +/// Checks that the texts of the app are looked up only through a T or TB wrapper, whose fallback text the I18N +/// collector finds. +/// +/// +/// The collector reads the texts from calls of T and TB. A text handed to GetText anywhere else never reaches +/// the language plugins, so it is never translated, and nothing at runtime shows it. MWAIS0012 refuses such a +/// call while compiling. GetText is an extension method, so these tests ask in both of the forms C# knows for +/// one: the classic method with a this parameter and the member of an extension block. +/// +[TestFixture] +public sealed class DirectI18NGetTextAnalyzerTests +{ + private const string CLASSIC_FORM = + """ + public static class ILangExtensions + { + public static string GetText(this ILang lang, string fallbackEN) => fallbackEN; + } + """; + + private const string EXTENSION_BLOCK_FORM = + """ + public static class ILangExtensions + { + extension(ILang lang) + { + public string GetText(string fallbackEN) => fallbackEN; + } + } + """; + + [TestCase(CLASSIC_FORM, "T", TestName = "A T wrapper around a classic extension method")] + [TestCase(CLASSIC_FORM, "TB", TestName = "A TB wrapper around a classic extension method")] + [TestCase(EXTENSION_BLOCK_FORM, "T", TestName = "A T wrapper around the member of an extension block")] + [TestCase(EXTENSION_BLOCK_FORM, "TB", TestName = "A TB wrapper around the member of an extension block")] + public async Task AWrapperWhichForwardsItsTextIsAllowed(string form, string wrapper) + { + var reported = await AnalyzeAsync(form, $"public string {wrapper}(string fallbackEN) => this.GetText(fallbackEN);"); + + Assert.That(reported, Is.Empty); + } + + [TestCase(CLASSIC_FORM, """public string Describe() => this.GetText("Hello");""", TestName = "A classic extension method called from another method")] + [TestCase(CLASSIC_FORM, """public string T(string fallbackEN) => this.GetText("Hello");""", TestName = "A classic extension method called by a wrapper with a text of its own")] + [TestCase(CLASSIC_FORM, """public string Describe() => ILangExtensions.GetText(this, "Hello");""", TestName = "A classic extension method called as a static method")] + [TestCase(EXTENSION_BLOCK_FORM, """public string Describe() => this.GetText("Hello");""", TestName = "The member of an extension block called from another method")] + [TestCase(EXTENSION_BLOCK_FORM, """public string T(string fallbackEN) => this.GetText("Hello");""", TestName = "The member of an extension block called by a wrapper with a text of its own")] + [TestCase(EXTENSION_BLOCK_FORM, """public string Describe() => ILangExtensions.GetText(this, "Hello");""", TestName = "The member of an extension block called as a static method")] + public async Task ADirectLookupIsRefused(string form, string member) + { + var reported = await AnalyzeAsync(form, member); + + using (Assert.EnterMultipleScope()) + { + Assert.That(reported, Has.Count.EqualTo(1)); + Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0012")); + } + } + + [TestCase("this.GetText(PLUGIN, fallbackEN)", 0, TestName = "The GetText of the app in a wrapper")] + [TestCase("""this.GetText(PLUGIN, "Hello")""", 1, TestName = "The GetText of the app in a wrapper with a text of its own")] + public async Task TheGetTextOfTheAppIsRecognized(string lookup, int expectedReports) + { + // + // Whatever form the app gives its GetText, the analyzer has to know it: + // + var source = + $$""" + using AIStudio.Tools.PluginSystem; + + namespace Sample; + + public sealed class Component : ILang + { + private static readonly ILanguagePlugin PLUGIN = NoPluginLanguage.INSTANCE; + + public string T(string fallbackEN) => {{lookup}}; + + public string T(string fallbackEN, string? typeNamespace, string? typeName) => fallbackEN; + } + """; + + var reported = await CompilationHarness.AnalyzeAsync(source, new DirectI18NGetTextAnalyzer()); + + Assert.That(reported, Has.Count.EqualTo(expectedReports)); + } + + private static async Task> AnalyzeAsync(string form, string member) + { + // + // The snippet declares its own ILang and ILangExtensions where the analyzer looks for them. They take + // the place of those of the app, which the compilation references as well. + // + var source = + $$""" + namespace AIStudio.Tools.PluginSystem; + + public interface ILang + { + } + + {{form}} + + public sealed class Component : ILang + { + {{member}} + } + """; + + return await CompilationHarness.AnalyzeAsync(source, new DirectI18NGetTextAnalyzer()); + } +} diff --git a/app/Tests/Analyzers/ThisUsageAnalyzerTests.cs b/app/Tests/Analyzers/ThisUsageAnalyzerTests.cs new file mode 100644 index 000000000..b8dd8e699 --- /dev/null +++ b/app/Tests/Analyzers/ThisUsageAnalyzerTests.cs @@ -0,0 +1,143 @@ +using AIStudio.Tests.Models.Generation; + +using Microsoft.CodeAnalysis; + +using SourceCodeRules.UsageAnalyzers; + +namespace AIStudio.Tests.Analyzers; + +/// +/// Checks which members MWAIS0005 wants to be reached through this., and that extension methods are never +/// asked for it. +/// +/// +/// An extension member has no this: a classic extension method is static, and in an extension block C# +/// refuses the keyword. There, every instance member is reached through the name of the receiver, and a sibling +/// member without it does not even compile. Should a later version of C# reach the receiver without its name, +/// the analyzer would demand a this. which nobody can write, and the tests for extension blocks fail. +/// +[TestFixture] +public sealed class ThisUsageAnalyzerTests +{ + [TestCase("this.location", TestName = "A field through this")] + [TestCase("this.Name", TestName = "A property through this")] + [TestCase("this.Describe()", TestName = "A method through this")] + [TestCase("CITY", TestName = "A static field")] + [TestCase("ROOM", TestName = "A constant")] + [TestCase("Shorten(this.location)", TestName = "A static method")] + public async Task AMemberWhichNeedsNoThisIsLeftAlone(string expression) + { + var reported = await AnalyzeClassAsync(expression); + + Assert.That(reported, Is.Empty); + } + + [TestCase("location", TestName = "A field without this")] + [TestCase("Name", TestName = "A property without this")] + [TestCase("Describe()", TestName = "A method without this")] + public async Task AnInstanceMemberWithoutThisIsRefused(string expression) + { + var reported = await AnalyzeClassAsync(expression); + + using (Assert.EnterMultipleScope()) + { + Assert.That(reported, Has.Count.EqualTo(1)); + Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0005")); + } + } + + [Test] + public async Task AClassicExtensionMethodIsLeftAlone() + { + var source = + """ + namespace Sample; + + public sealed class Meeting + { + public string Name { get; set; } = "Kickoff"; + } + + public static class MeetingExtensions + { + private static readonly string PREFIX = "Meeting: "; + + public static string Label(this Meeting meeting) => PREFIX + meeting.Name; + + public static string Title(this Meeting meeting) => Label(meeting).ToUpperInvariant(); + } + """; + + var reported = await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer()); + + Assert.That(reported, Is.Empty); + } + + [TestCase("public string Label() => meeting.Name;", TestName = "A member of the receiver")] + [TestCase("public string Label() => meeting.Title();", TestName = "A sibling through the receiver")] + [TestCase("public string Label() => Title(meeting);", TestName = "A sibling as a static method")] + [TestCase("""public string Label() => PREFIX + SUFFIX + TB("Meeting");""", TestName = "The static members of the class")] + [TestCase("public string Label() { System.Func title = () => meeting.Title(); return title(); }", TestName = "A lambda")] + [TestCase("public string Label() { string Title() => meeting.Name; return Title(); }", TestName = "A local function")] + [TestCase("public string Label => meeting.Title();", TestName = "An extension property")] + [TestCase("public static Meeting Create() => new(); public static Meeting Again() => Create();", TestName = "A static member of the block")] + public async Task AMemberOfAnExtensionBlockIsLeftAlone(string member) + { + var source = + $$""" + namespace Sample; + + public sealed class Meeting + { + public string Name { get; set; } = "Kickoff"; + } + + public static class MeetingExtensions + { + private const string SUFFIX = "."; + + private static readonly string PREFIX = "Meeting: "; + + private static string TB(string fallbackEN) => fallbackEN; + + extension(Meeting meeting) + { + public string Title() => meeting.Name; + + {{member}} + } + } + """; + + var reported = await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer()); + + Assert.That(reported, Is.Empty); + } + + private static async Task> AnalyzeClassAsync(string expression) + { + var source = + $$""" + namespace Sample; + + public sealed class Meeting + { + private const string ROOM = "A"; + + private static readonly string CITY = "Cologne"; + + private readonly string location = "Town hall"; + + public string Name { get; set; } = "Kickoff"; + + public string Run() => {{expression}}; + + private string Describe() => this.Name; + + private static string Shorten(string text) => text[..1]; + } + """; + + return await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer()); + } +} diff --git a/app/Tests/Tools/PluginSystem/TranslationWrapperTests.cs b/app/Tests/Tools/PluginSystem/TranslationWrapperTests.cs new file mode 100644 index 000000000..eca8b7ee6 --- /dev/null +++ b/app/Tests/Tools/PluginSystem/TranslationWrapperTests.cs @@ -0,0 +1,94 @@ +using System.Runtime.CompilerServices; + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AIStudio.Tests.Tools.PluginSystem; + +/// +/// Checks that every TB wrapper of the app looks its texts up under the class it belongs to. +/// +/// +/// The I18N collector files the texts of a TB wrapper under the namespace and the class of its source file, and +/// the language plugins translate them under that key. A wrapper which names another class in its typeof and +/// nameof looks every text up under a key no plugin knows, so its texts stay English in every language, and +/// nothing reports it. The wrapper of the Assistant Builder's parse errors named the enum instead of its own +/// class, which left twelve messages untranslated. +/// +[TestFixture] +public sealed class TranslationWrapperTests +{ + [Test] + public void EveryTBWrapperNamesItsOwnClass() + { + var appDirectory = AppSourceDirectory(); + var wrappers = 0; + var mismatches = new List(); + var files = Directory.EnumerateFiles(appDirectory, "*.cs", SearchOption.AllDirectories) + .Where(path => !IsBuildOutput(Path.GetRelativePath(appDirectory, path))); + + foreach (var file in files) + { + var source = File.ReadAllText(file); + if (!source.Contains("TB(", StringComparison.Ordinal)) + continue; + + var root = CSharpSyntaxTree.ParseText(source).GetRoot(); + foreach (var wrapper in root.DescendantNodes().OfType().Where(method => method.Identifier.Text == "TB")) + { + wrappers++; + var owner = wrapper.Ancestors().OfType().Last().Identifier.Text; + var named = NamedTypes(wrapper).ToList(); + if (named.Count == 0 || named.Any(name => name != owner)) + mismatches.Add($"{Path.GetRelativePath(appDirectory, file)}: the TB of {owner} names {(named.Count == 0 ? "no class" : string.Join(", ", named))}"); + } + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(wrappers, Is.GreaterThan(100), "The test found too few TB wrappers to be looking at the source of the app."); + Assert.That(mismatches, Is.Empty); + } + } + + /// + /// The types a wrapper names to build its key, through typeof and nameof. + /// + private static IEnumerable NamedTypes(MethodDeclarationSyntax wrapper) + { + foreach (var typeOf in wrapper.DescendantNodes().OfType()) + yield return SimpleName(typeOf.Type); + + foreach (var invocation in wrapper.DescendantNodes().OfType()) + { + if (invocation.Expression is not IdentifierNameSyntax { Identifier.Text: "nameof" }) + continue; + + switch (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression) + { + case MemberAccessExpressionSyntax memberAccess: + yield return memberAccess.Name.Identifier.Text; + break; + + case TypeSyntax type: + yield return SimpleName(type); + break; + } + } + } + + private static string SimpleName(TypeSyntax type) => type switch + { + QualifiedNameSyntax qualified => SimpleName(qualified.Right), + SimpleNameSyntax simple => simple.Identifier.Text, + _ => type.ToString(), + }; + + private static bool IsBuildOutput(string relativePath) + { + var firstDirectory = relativePath.Split(Path.DirectorySeparatorChar)[0]; + return firstDirectory is "bin" or "obj"; + } + + private static string AppSourceDirectory([CallerFilePath] string sourceFilePath = "") => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "..", "..", "..", "MindWork AI Studio")); +}