Moved all extension methods into extension blocks (#1155)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 10:46:18 +01:00
1 parent acc7d09fa2
commit c0a75f39ac
138 files changed
+1858 -928

No files matched your search

+1
View File
@@ -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 `<param>` of the receiver, and of a generic block its `<typeparam>`, 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`.
@@ -6,7 +6,9 @@ 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)
{
public string GetName() => level switch
{
AssistantAuditLevel.DANGEROUS => TB("Dangerous"),
AssistantAuditLevel.CAUTION => TB("Concerning"),
@@ -14,7 +16,7 @@ public static class AssistantAuditLevelExtensions
_ => TB("Unknown"),
};
public static Severity GetSeverity(this AssistantAuditLevel level) => level switch
public Severity GetSeverity() => level switch
{
AssistantAuditLevel.DANGEROUS => Severity.Error,
AssistantAuditLevel.CAUTION => Severity.Warning,
@@ -22,7 +24,7 @@ public static class AssistantAuditLevelExtensions
_ => Severity.Info,
};
public static Color GetColor(this AssistantAuditLevel level) => level switch
public Color GetColor() => level switch
{
AssistantAuditLevel.DANGEROUS => Color.Error,
AssistantAuditLevel.CAUTION => Color.Warning,
@@ -30,13 +32,14 @@ public static class AssistantAuditLevelExtensions
_ => Color.Default,
};
public static string GetIcon(this AssistantAuditLevel level) => level switch
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,
};
}
/// <summary>
/// Parses an audit level string and falls back to <see cref="AssistantAuditLevel.UNKNOWN"/> when parsing fails.
@@ -4,7 +4,9 @@ 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)
{
public string Name() => numberParticipants switch
{
NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"),
@@ -23,4 +25,5 @@ public static class NumberParticipantsExtensions
_ => "Unknown"
};
}
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => separator switch
{
BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"),
BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"),
@@ -17,7 +19,7 @@ public static class BatchProcessingCsvSeparatorExtensions
_ => TB("Unknown"),
};
public static char Character(this BatchProcessingCsvSeparator separator, string customSeparator) => separator switch
public char Character(string customSeparator) => separator switch
{
BatchProcessingCsvSeparator.COMMA => ',',
BatchProcessingCsvSeparator.SEMICOLON => ';',
@@ -27,6 +29,7 @@ public static class BatchProcessingCsvSeparatorExtensions
_ => DEFAULT_SEPARATOR,
};
}
internal static bool IsValidCustomSeparator(string separator)
{
@@ -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)
{
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"),
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => promptSource switch
{
BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"),
BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"),
@@ -12,4 +14,5 @@ public static class BatchProcessingPromptSourceExtensions
_ => TB("Unknown prompt source"),
};
}
}
@@ -2,9 +2,11 @@ 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)
{
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)
@@ -20,4 +22,5 @@ public static class LuaResponseParseErrorExtension
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."),
};
}
}
@@ -4,7 +4,9 @@ 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
extension(CommonCodingLanguages language)
{
public string Name() => language switch
{
CommonCodingLanguages.NONE => TB("None"),
@@ -42,4 +44,5 @@ public static class CommonCodingLanguageExtensions
CommonCodingLanguages.OTHER => TB("Other"),
_ => TB("Unknown")
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => style switch
{
WritingStyles.ACADEMIC => TB("Academic"),
WritingStyles.PERSONAL => TB("Personal"),
@@ -14,7 +16,7 @@ public static class WritingStylesExtensions
_ => TB("Not specified"),
};
public static string Prompt(this WritingStyles style) => style switch
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.",
@@ -23,4 +25,5 @@ public static class WritingStylesExtensions
_ => "Use a formal business style for this e-mail.",
};
}
}
@@ -4,7 +4,9 @@ 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)
{
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"),
@@ -12,4 +14,5 @@ public static class AllowedLLMProvidersExtensions
_ => TB("Unknown option was selected")
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => auth switch
{
Auth.NONE => TB("No login necessary: useful for public data sources"),
@@ -15,7 +17,7 @@ public static class AuthExtensions
_ => TB("Unknown login method")
};
public static string ToPrompt(this Auth auth) => auth switch
public string ToPrompt() => auth switch
{
Auth.NONE => "No login is necessary, the data source is public.",
@@ -25,4 +27,5 @@ public static class AuthExtensions
_ => string.Empty,
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => dataSource switch
{
DataSources.NONE => TB("No data source selected"),
DataSources.CUSTOM => TB("Custom description"),
@@ -18,4 +20,5 @@ public static class DataSourcesExtensions
_ => TB("Unknown data source")
};
}
}
@@ -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));
/// <param name="outcome">What became of the file.</param>
extension(ERIServerFileOutcome outcome)
{
/// <summary>
/// Gets the localized message which explains why a file was not written.
/// </summary>
/// <param name="outcome">What became of the file.</param>
/// <param name="file">The path of the file, as the model named it.</param>
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file);
internal string ToUserMessage(string file) => string.Format(ToUserMessageFormat(outcome), file);
}
private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch
{
@@ -2,14 +2,17 @@ namespace AIStudio.Assistants.ERI;
public static class ERIVersionExtensions
{
public static Task<string> ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
extension(ERIVersion version)
{
public Task<string> ReadSpecification(WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
public static string SpecificationURL(this ERIVersion version)
public string SpecificationURL()
{
var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
public bool WasSpecificationSelected() => version != ERIVersion.NONE;
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => os switch
{
OperatingSystem.NONE => TB("No operating system specified"),
@@ -14,7 +16,7 @@ public static class OperatingSystemExtensions
_ => TB("Unknown operating system")
};
public static string ToPrompt(this OperatingSystem os) => os switch
public string ToPrompt() => os switch
{
OperatingSystem.NONE => "No operating system specified",
@@ -23,4 +25,5 @@ public static class OperatingSystemExtensions
_ => "Unknown operating system"
};
}
}
@@ -4,7 +4,9 @@ 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
extension(ProgrammingLanguages language)
{
public string Name() => language switch
{
ProgrammingLanguages.NONE => TB("No programming language selected"),
@@ -24,7 +26,7 @@ public static class ProgrammingLanguagesExtensions
_ => TB("Unknown")
};
public static string ToPrompt(this ProgrammingLanguages language) => language switch
public string ToPrompt() => language switch
{
ProgrammingLanguages.NONE => "No programming language selected",
@@ -43,4 +45,5 @@ public static class ProgrammingLanguagesExtensions
ProgrammingLanguages.OTHER => "Other",
_ => "Unknown"
};
}
}
@@ -2,7 +2,9 @@ namespace AIStudio.Assistants.IconFinder;
public static class IconSourceExtensions
{
public static string Name(this IconSources iconSource) => iconSource switch
extension(IconSources iconSource)
{
public string Name() => iconSource switch
{
IconSources.FLAT_ICON => "Flaticon",
IconSources.FONT_AWESOME => "Font Awesome",
@@ -14,7 +16,7 @@ public static class IconSourceExtensions
_ => "Generic",
};
public static string Prompt(this IconSources iconSource) => iconSource switch
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`.",
@@ -26,7 +28,7 @@ public static class IconSourceExtensions
_ => string.Empty,
};
public static string URL(this IconSources iconSource) => iconSource switch
public string URL() => iconSource switch
{
IconSources.FLAT_ICON => "https://www.flaticon.com/",
IconSources.FONT_AWESOME => "https://fontawesome.com/",
@@ -37,4 +39,5 @@ public static class IconSourceExtensions
_ => string.Empty,
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => sentenceStructure switch
{
SentenceStructure.ACTIVE => TB("Active voice"),
SentenceStructure.PASSIVE => TB("Passive voice"),
@@ -12,11 +14,12 @@ public static class SentenceStructureExtensions
_ => TB("Not Specified"),
};
public static string Prompt(this SentenceStructure sentenceStructure) => sentenceStructure switch
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,
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => style switch
{
WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"),
WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"),
@@ -20,7 +22,7 @@ public static class WritingStylesExtensions
_ => TB("Not specified"),
};
public static string Prompt(this WritingStyles style) => style switch
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.",
@@ -35,4 +37,5 @@ public static class WritingStylesExtensions
_ => "Keep the style of the text as it is.",
};
}
}
@@ -2,10 +2,12 @@ namespace AIStudio.Assistants.SWOTAnalysis;
internal static class SwotAnalysisCategoryExtensions
{
extension(SwotAnalysisCategory category)
{
/// <summary>
/// The name of the category in the JSON the models read and write, e.g., "strengths".
/// </summary>
public static string JsonName(this SwotAnalysisCategory category) => category switch
public string JsonName() => category switch
{
SwotAnalysisCategory.STRENGTHS => "strengths",
SwotAnalysisCategory.WEAKNESSES => "weaknesses",
@@ -18,7 +20,7 @@ internal static class SwotAnalysisCategoryExtensions
/// <summary>
/// The letter the factor IDs of the category start with, e.g., S1 for the first strength.
/// </summary>
public static string FactorIdPrefix(this SwotAnalysisCategory category) => category switch
public string FactorIdPrefix() => category switch
{
SwotAnalysisCategory.STRENGTHS => "S",
SwotAnalysisCategory.WEAKNESSES => "W",
@@ -27,4 +29,5 @@ internal static class SwotAnalysisCategoryExtensions
_ => string.Empty,
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => ageGroup switch
{
AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"),
AudienceAgeGroup.CHILDREN => TB("Children"),
@@ -14,7 +16,7 @@ public static class AudienceAgeGroupExtensions
_ => TB("Unspecified age group"),
};
public static string Prompt(this AudienceAgeGroup ageGroup) => ageGroup switch
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.",
@@ -23,4 +25,5 @@ public static class AudienceAgeGroupExtensions
_ => "Do not tailor the text to a specific age group.",
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => expertise switch
{
AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"),
AudienceExpertise.NON_EXPERTS => TB("No expertise"),
@@ -15,7 +17,7 @@ public static class AudienceExpertiseExtensions
_ => TB("Unspecified expertise"),
};
public static string Prompt(this AudienceExpertise expertise) => expertise switch
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.",
@@ -25,4 +27,5 @@ public static class AudienceExpertiseExtensions
_ => "Do not tailor the text to a specific expertise level.",
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => level switch
{
AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"),
AudienceOrganizationalLevel.TRAINEES => TB("Trainees"),
@@ -17,7 +19,7 @@ public static class AudienceOrganizationalLevelExtensions
_ => TB("Unspecified organizational level"),
};
public static string Prompt(this AudienceOrganizationalLevel level) => level switch
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.",
@@ -29,4 +31,5 @@ public static class AudienceOrganizationalLevelExtensions
_ => "Do not tailor the text to a specific organizational level.",
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => profile switch
{
AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"),
AudienceProfile.STUDENTS => TB("Students"),
@@ -21,7 +23,7 @@ public static class AudienceProfileExtensions
_ => TB("Unspecified audience profile"),
};
public static string Prompt(this AudienceProfile profile) => profile switch
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.",
@@ -37,4 +39,5 @@ public static class AudienceProfileExtensions
_ => "Do not tailor the text to a specific audience profile.",
};
}
}
@@ -4,7 +4,10 @@ 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
/// <param name="occasion">The occasion.</param>
extension(PresentationOccasion occasion)
{
public string Name() => occasion switch
{
PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"),
PresentationOccasion.TALK => TB("Talk"),
@@ -30,9 +33,8 @@ public static class PresentationOccasionExtensions
/// <summary>
/// Tells the model what the occasion means for the structure of the presentation.
/// </summary>
/// <param name="occasion">The occasion.</param>
/// <param name="customOccasion">The occasion the user described, for OTHER.</param>
public static string Prompt(this PresentationOccasion occasion, string customOccasion) => occasion switch
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.",
@@ -53,4 +55,5 @@ public static class PresentationOccasionExtensions
_ => "No particular occasion is given. Structure the presentation by its content.",
};
}
}
@@ -11,6 +11,8 @@ internal static class TemplateSlideExtensions
/// </summary>
private const int MAX_SLIDE_TITLE_CHARACTERS = 60;
extension(TemplateSlide slide)
{
/// <summary>
/// How the forms name a slide of the template: by its number and its title, and what is special about it.
/// </summary>
@@ -18,7 +20,7 @@ internal static class TemplateSlideExtensions
/// 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.
/// </remarks>
public static string Label(this TemplateSlide slide)
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);
@@ -27,4 +29,5 @@ internal static class TemplateSlideExtensions
return label;
}
}
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => use switch
{
TemplateSlideUse.OMIT => TB("Leave out"),
TemplateSlideUse.KEEP => TB("Keep unchanged"),
@@ -14,4 +16,5 @@ internal static class TemplateSlideUseExtensions
_ => TB("Leave out"),
};
}
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => complexity switch
{
Complexity.NO_CHANGE => TB("No change in complexity"),
@@ -18,7 +20,7 @@ public static class ComplexityExtensions
_ => TB("No change in complexity"),
};
public static string Prompt(this Complexity complexity, string expertInField) => complexity switch
public string Prompt(string expertInField) => complexity switch
{
Complexity.NO_CHANGE => "Do not change the complexity of the text.",
@@ -31,4 +33,5 @@ public static class ComplexityExtensions
_ => "Do not change the complexity of the text.",
};
}
}
@@ -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));
/// <param name="failure">The recorded failure.</param>
extension(VisualBriefingFailure failure)
{
/// <summary>
/// Gets the localized message for one recorded failure.
/// </summary>
/// <param name="failure">The recorded failure.</param>
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this VisualBriefingFailure failure) => ToUserMessage(failure.Code, failure.ValidationRule);
internal string ToUserMessage() => ToUserMessage(failure.Code, failure.ValidationRule);
}
/// <summary>
/// Gets the localized message for one failure code and validation rule.
@@ -40,12 +43,14 @@ internal static class VisualBriefingFailureExtensions
_ => code.ToUserMessage(),
};
/// <param name="rule">The stable validation rule.</param>
extension(VisualBriefingValidationRule rule)
{
/// <summary>
/// Gets the localized message for one validation rule.
/// </summary>
/// <param name="rule">The stable validation rule.</param>
/// <returns>The localized message.</returns>
private static string ToUserMessage(this VisualBriefingValidationRule rule) => rule switch
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."),
@@ -74,13 +79,16 @@ internal static class VisualBriefingFailureExtensions
_ => string.Empty,
};
}
/// <param name="code">The stable failure code.</param>
extension(VisualBriefingFailureCode code)
{
/// <summary>
/// Gets the localized message for one failure code.
/// </summary>
/// <param name="code">The stable failure code.</param>
/// <returns>The localized message.</returns>
private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch
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."),
@@ -106,4 +114,5 @@ internal static class VisualBriefingFailureExtensions
_ => string.Empty,
};
}
}
@@ -15,11 +15,13 @@ internal static class VisualBriefingPreviewEndpoint
{
private const string ROUTE = "/visual-briefing/preview/{briefingId:guid}/{revisionId:guid}";
/// <param name="app">The web application.</param>
extension(WebApplication app)
{
/// <summary>
/// Maps the visual briefing preview endpoint.
/// </summary>
/// <param name="app">The web application.</param>
public static void MapVisualBriefingPreview(this WebApplication app) => app.MapGet(
public void MapVisualBriefingPreview() => app.MapGet(
ROUTE,
async (
Guid briefingId,
@@ -64,6 +66,7 @@ internal static class VisualBriefingPreviewEndpoint
return Results.File(preview.Value.Stream, "text/html; charset=utf-8", enableRangeProcessing: false);
});
}
/// <summary>
/// Creates the log event ID for one visual briefing log event.
@@ -16,6 +16,9 @@ public static class ChatProvenance
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(ChatProvenance));
/// <param name="thread">The chat.</param>
extension(ChatThread thread)
{
/// <summary>
/// Collects the entries of the provenance store which a chat refers to.
/// </summary>
@@ -24,9 +27,8 @@ public static class ChatProvenance
/// 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.
/// </remarks>
/// <param name="thread">The chat.</param>
/// <returns>The ids of the entries.</returns>
public static IReadOnlySet<Guid> CollectProvenanceIds(this ChatThread thread)
public IReadOnlySet<Guid> CollectProvenanceIds()
{
var provenanceIds = new HashSet<Guid>();
foreach (var block in thread.Blocks)
@@ -42,6 +44,7 @@ public static class ChatProvenance
provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId));
return provenanceIds;
}
}
/// <summary>
/// Deletes what the given chats received from the provenance store, for chats which were deleted.
@@ -6,6 +6,9 @@ namespace AIStudio.Chat;
public static class ChatRestrictionsExtensions
{
/// <param name="restrictions">What the content demands.</param>
extension(ChatRestrictions restrictions)
{
/// <summary>
/// Checks if the specified provider may see content with these restrictions.
/// </summary>
@@ -16,10 +19,9 @@ public static class ChatRestrictionsExtensions
/// 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.
/// </remarks>
/// <param name="restrictions">What the content demands.</param>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider may see the content. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatRestrictions restrictions, T provider)
public bool IsLLMProviderAllowed<T>(T provider)
{
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
@@ -73,6 +75,7 @@ public static class ChatRestrictionsExtensions
false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}
}
private static LLMProviders GetLLMProvider<T>(T provider) => provider switch
{
@@ -6,12 +6,14 @@ public static class ChatRoleExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatRoleExtensions).Namespace, nameof(ChatRoleExtensions));
/// <param name="role">The role.</param>
extension(ChatRole role)
{
/// <summary>
/// Returns the name of the role.
/// </summary>
/// <param name="role">The role.</param>
/// <returns>The name of the role.</returns>
public static string ToName(this ChatRole role) => role switch
public string ToName() => role switch
{
ChatRole.SYSTEM => TB("System"),
ChatRole.USER => TB("You"),
@@ -23,9 +25,8 @@ public static class ChatRoleExtensions
/// <summary>
/// Returns the color of the role.
/// </summary>
/// <param name="role">The role.</param>
/// <returns>The color of the role.</returns>
public static Color ToColor(this ChatRole role) => role switch
public Color ToColor() => role switch
{
ChatRole.SYSTEM => Color.Info,
ChatRole.USER => Color.Primary,
@@ -37,9 +38,8 @@ public static class ChatRoleExtensions
/// <summary>
/// Returns the icon of the role.
/// </summary>
/// <param name="role">The role.</param>
/// <returns>The icon of the role.</returns>
public static string ToIcon(this ChatRole role) => role switch
public string ToIcon() => role switch
{
ChatRole.SYSTEM => Icons.Material.Filled.Settings,
ChatRole.USER => Icons.Material.Filled.Person,
@@ -51,9 +51,8 @@ public static class ChatRoleExtensions
/// <summary>
/// Returns the specific name of the role for the chat template.
/// </summary>
/// <param name="role">The role.</param>
/// <returns>The name of the role.</returns>
public static string ToChatTemplateName(this ChatRole role) => role switch
public string ToChatTemplateName() => role switch
{
ChatRole.SYSTEM => TB("System"),
ChatRole.USER => TB("User"),
@@ -65,11 +64,11 @@ public static class ChatRoleExtensions
/// <summary>
/// Selects the next role for a chat template.
/// </summary>
/// <param name="currentRole">The current role.</param>
/// <returns>The next role for the chat template.</returns>
public static ChatRole SelectNextRoleForTemplate(this ChatRole currentRole) => currentRole switch
public ChatRole SelectNextRoleForTemplate() => role switch
{
ChatRole.USER => ChatRole.AI,
_ => ChatRole.USER,
};
}
}
@@ -2,6 +2,9 @@ namespace AIStudio.Chat;
public static class ChatThreadExtensions
{
/// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param>
extension(ChatThread? chatThread)
{
/// <summary>
/// Checks if the specified provider is allowed for the chat thread.
/// </summary>
@@ -18,8 +21,8 @@ public static class ChatThreadExtensions
/// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left
/// the thread is checked with as well.
/// </remarks>
/// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider);
public bool IsLLMProviderAllowed<T>(T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider);
}
}
@@ -2,6 +2,9 @@ namespace AIStudio.Chat;
public static class IContentExtensions
{
/// <param name="content">The content of a message in the chat.</param>
extension(IContent content)
{
/// <summary>
/// Detaches whoever listens to the stream of this content.
/// </summary>
@@ -11,8 +14,7 @@ public static class IContentExtensions
/// 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.
/// </remarks>
/// <param name="content">The content whose streaming handlers you want to detach.</param>
public static void ResetStreamingHandlers(this IContent content)
public void ResetStreamingHandlers()
{
content.StreamingEvent = IContent.NO_STREAMING_HANDLER;
content.StreamingDone = IContent.NO_STREAMING_HANDLER;
@@ -28,10 +30,9 @@ public static class IContentExtensions
/// 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.
/// </remarks>
/// <param name="content">The content to read.</param>
/// <param name="markdown">The Markdown text, or an empty string when there is none.</param>
/// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetMarkdownText(this IContent content, out string markdown)
public bool TryGetMarkdownText(out string markdown)
{
if (content is ContentText text)
{
@@ -53,13 +54,12 @@ public static class IContentExtensions
/// 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.
/// </remarks>
/// <param name="content">The content to read.</param>
/// <param name="markdown">The Markdown text including its sources, or an empty string when there is none.</param>
/// <param name="keepPageAnchors">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.</param>
/// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetExportMarkdown(this IContent content, out string markdown, bool keepPageAnchors = true)
public bool TryGetExportMarkdown(out string markdown, bool keepPageAnchors = true)
{
if (content is not ContentText text)
{
@@ -91,10 +91,9 @@ public static class IContentExtensions
/// 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.
/// </remarks>
/// <param name="content">The content the file was found in.</param>
/// <param name="file">The file, as PlainFileExport.ExtractFiles read it out of this content.</param>
/// <returns>The content of the file to write.</returns>
public static string ToExportContent(this IContent content, MessageFile file)
public string ToExportContent(MessageFile file)
{
if (file.Format.IsTabular())
return file.Content;
@@ -111,6 +110,7 @@ public static class IContentExtensions
return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}";
}
}
/// <summary>
/// Turns a code block into the file its tools expect.
@@ -30,6 +30,9 @@ public static class IImageSourceExtensions
return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES);
}
/// <param name="image">The image source.</param>
extension(IImageSource image)
{
/// <summary>
/// Read the image content as a base64 string.
/// </summary>
@@ -46,10 +49,9 @@ public static class IImageSourceExtensions
/// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed.
/// </remarks>
/// <param name="image">The image source.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The image content as a base64 string; might be empty.</returns>
public static async Task<(bool success, string base64Content)> TryAsBase64(this IImageSource image, CancellationToken token = default)
public async Task<(bool success, string base64Content)> TryAsBase64(CancellationToken token = default)
{
switch (image.SourceType)
{
@@ -102,4 +104,5 @@ public static class IImageSourceExtensions
return (success: false, string.Empty);
}
}
}
}
@@ -9,18 +9,19 @@ namespace AIStudio.Chat;
public static class ListContentBlockExtensions
{
/// <param name="blocks">The list of content blocks to process.</param>
extension(List<ContentBlock> blocks)
{
/// <summary>
/// Processes a list of content blocks by transforming them into a collection of message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
/// <param name="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
public static async Task<IList<IMessageBase>> BuildMessagesAsync(
this List<ContentBlock> blocks,
public async Task<IList<IMessageBase>> BuildMessagesAsync(
AIStudio.Settings.Provider provider,
Func<ChatRole, string> roleTransformer,
Func<string, ISubContent> textSubContentFactory,
@@ -125,7 +126,6 @@ public static class ListContentBlockExtensions
/// <summary>
/// Processes a list of content blocks using direct image URL format to create message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks>
@@ -135,8 +135,7 @@ public static class ListContentBlockExtensions
/// </code>
/// This format is used by OpenAI, Mistral, and Ollama.
/// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
this List<ContentBlock> blocks,
public async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider,
StandardRoleTransformer,
@@ -146,7 +145,6 @@ public static class ListContentBlockExtensions
/// <summary>
/// Processes a list of content blocks using nested image URL format to create message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks>
@@ -156,13 +154,13 @@ public static class ListContentBlockExtensions
/// </code>
/// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers.
/// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
this List<ContentBlock> blocks,
public async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider,
StandardRoleTransformer,
StandardTextSubContentFactory,
NestedImageSubContentFactory);
}
private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText
{
@@ -2,7 +2,10 @@ namespace AIStudio.Chat;
public static class ListFileAttachmentExtensions
{
public static bool ContainsImages(this List<FileAttachment> attachments) => attachments.Any(attachment => attachment.IsImage);
/// <param name="attachments">The attachments of the message.</param>
extension(List<FileAttachment> attachments)
{
public bool ContainsImages() => attachments.Any(attachment => attachment.IsImage);
/// <summary>
/// Whether preparing the message for the AI may read a document through the runtime.
@@ -12,7 +15,7 @@ public static class ListFileAttachmentExtensions
/// 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.
/// </remarks>
/// <param name="attachments">The attachments of the message.</param>
/// <returns>True when a document may be read.</returns>
public static bool MayContainDocuments(this List<FileAttachment> attachments) => attachments.Any(attachment => !attachment.IsImage);
public bool MayContainDocuments() => attachments.Any(attachment => !attachment.IsImage);
}
}
@@ -2,6 +2,9 @@ namespace AIStudio.Chat;
public static class SetFileAttachmentExtensions
{
/// <param name="attachments">The attachments of a message.</param>
extension(HashSet<FileAttachment> attachments)
{
/// <summary>
/// Attaches a file in place of the attachment of the same file the set held before.
/// </summary>
@@ -11,11 +14,11 @@ public static class SetFileAttachmentExtensions
/// 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.
/// </remarks>
/// <param name="attachments">The attachments of a message.</param>
/// <param name="attachment">The attachment of the file just attached.</param>
public static void AttachInPlaceOfSameFile(this HashSet<FileAttachment> attachments, FileAttachment attachment)
public void AttachInPlaceOfSameFile(FileAttachment attachment)
{
attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal));
attachments.Add(attachment);
}
}
}
+10 -7
View File
@@ -2,18 +2,21 @@
public static class StringExtensions
{
public static string RemoveThinkTags(this string input)
/// <param name="text">The text.</param>
extension(string text)
{
public string RemoveThinkTags()
{
const string OPEN_TAG = "<think>";
const string CLOSE_TAG = "</think>";
if (string.IsNullOrWhiteSpace(input) || !input.StartsWith(OPEN_TAG, StringComparison.Ordinal))
return input;
if (string.IsNullOrWhiteSpace(text) || !text.StartsWith(OPEN_TAG, StringComparison.Ordinal))
return text;
var endIndex = input.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
var endIndex = text.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
if (endIndex == -1)
return string.Empty;
return input[(endIndex + CLOSE_TAG.Length)..];
return text[(endIndex + CLOSE_TAG.Length)..];
}
/// <summary>
@@ -24,10 +27,9 @@ public static class StringExtensions
/// 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.
/// </remarks>
/// <param name="text">The text.</param>
/// <param name="maxCharacters">How many characters of the text to keep at most, the dots not counted.</param>
/// <returns>The text as it was when it is short enough, otherwise its beginning followed by three dots.</returns>
public static string Shorten(this string text, int maxCharacters)
public string Shorten(int maxCharacters)
{
if (text.Length <= maxCharacters)
return text;
@@ -35,4 +37,5 @@ public static class StringExtensions
var end = char.IsHighSurrogate(text[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters;
return string.Concat(text.AsSpan(0, end).TrimEnd(), "...");
}
}
}
@@ -6,12 +6,14 @@ public static class ReadWebContentStepsExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReadWebContentStepsExtensions).Namespace, nameof(ReadWebContentStepsExtensions));
/// <param name="step">The ReadWebContentSteps enum value.</param>
extension(ReadWebContentSteps step)
{
/// <summary>
/// Gets the text representation of a given ReadWebContentSteps enum value.
/// </summary>
/// <param name="step">The ReadWebContentSteps enum value.</param>
/// <returns>>The text representation of the process step.</returns>
public static string GetText(this ReadWebContentSteps step) => step switch
public string GetText() => step switch
{
ReadWebContentSteps.START => TB("Start"),
ReadWebContentSteps.LOADING => TB("Loading"),
@@ -21,4 +23,5 @@ public static class ReadWebContentStepsExtensions
_ => TB("n/a")
};
}
}
@@ -7,7 +7,9 @@ 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)
{
public string GetName() => level switch
{
ConfidenceLevel.NONE => TB("No provider selected"),
@@ -22,7 +24,7 @@ public static class ConfidenceLevelExtensions
_ => TB("Unknown confidence level"),
};
public static string GetColor(this ConfidenceLevel level, SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch
public string GetColor(SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch
{
(ConfidenceLevel.NONE, false) => "#cccccc",
(ConfidenceLevel.NONE, true) => "#666666",
@@ -52,5 +54,6 @@ public static class ConfidenceLevelExtensions
(_, true) => "#663300",
};
public static string SetColorStyle(this ConfidenceLevel level, SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};";
public string SetColorStyle(SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};";
}
}
@@ -6,12 +6,14 @@ public static class HFInferenceProviderExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions));
/// <param name="provider">The inference provider.</param>
extension(HFInferenceProvider provider)
{
/// <summary>
/// The slug Hugging Face uses for this inference provider.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
public static string EndpointsId(this HFInferenceProvider provider) => provider switch
public string EndpointsId() => provider switch
{
HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.CEREBRAS => "cerebras",
@@ -39,9 +41,8 @@ public static class HFInferenceProviderExtensions
/// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router
/// picks the fastest provider itself.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The suffix including its colon, or an empty string when the router should choose.</returns>
public static string ModelSuffix(this HFInferenceProvider provider) => provider switch
public string ModelSuffix() => provider switch
{
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
@@ -54,9 +55,8 @@ public static class HFInferenceProviderExtensions
/// <summary>
/// Whether this inference provider serves models to chat with.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when the provider serves chat models.</returns>
public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE;
public bool SupportsChat() => provider is not HFInferenceProvider.NONE;
/// <summary>
/// Whether this inference provider creates embeddings for us.
@@ -67,9 +67,8 @@ public static class HFInferenceProviderExtensions
/// OpenAI-compatible form we send. The routing strategies are out by their nature: without a
/// named provider there is no route to address.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can create embeddings through this provider.</returns>
public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
public bool SupportsEmbeddings() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// Whether this inference provider transcribes audio for us.
@@ -80,9 +79,8 @@ public static class HFInferenceProviderExtensions
/// 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".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can transcribe audio through this provider.</returns>
public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
public bool SupportsTranscription() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// The base URL of the provider's own OpenAI-compatible route.
@@ -93,9 +91,8 @@ public static class HFInferenceProviderExtensions
/// keeps it below an additional "openai" segment, and answers the path without it with
/// "Not allowed to POST /v1/embeddings for provider deepinfra".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The base URL, or an empty string when the provider has no such route.</returns>
public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch
public string ProviderBaseURL() => provider switch
{
HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/",
HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
@@ -103,36 +100,17 @@ public static class HFInferenceProviderExtensions
_ => string.Empty,
};
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="model">The model as it is configured.</param>
/// <returns>The model without its routing suffix.</returns>
public static Model WithoutRoutingSuffix(this Model model)
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public static string CatalogFilter(this HFInferenceProvider provider)
public string CatalogFilter()
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public static string ToName(this HFInferenceProvider provider) => provider switch
public string ToName() => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
@@ -155,4 +133,26 @@ public static class HFInferenceProviderExtensions
_ => string.Empty,
};
}
/// <param name="model">The model as it is configured.</param>
extension(Model model)
{
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>The model without its routing suffix.</returns>
public Model WithoutRoutingSuffix()
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
}
}
@@ -30,6 +30,9 @@ public static class LLMProvidersExtensions
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LLMProvidersExtensions).Namespace, nameof(LLMProvidersExtensions));
/// <param name="provider">The provider.</param>
extension(LLMProviders provider)
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
@@ -37,17 +40,15 @@ public static class LLMProvidersExtensions
/// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces,
/// or other stable identifiers.
/// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this LLMProviders llmProvider) => llmProvider.ToName(translate: true);
public string ToName() => provider.ToName(translate: true);
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="llmProvider">The provider.</param>
/// <param name="translate">Whether generic provider names should be translated.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this LLMProviders llmProvider, bool translate) => llmProvider switch
public string ToName(bool translate) => provider switch
{
LLMProviders.NONE => translate ? TB("No provider selected") : "No provider selected",
@@ -84,9 +85,8 @@ public static class LLMProvidersExtensions
/// These values are used for OS keyring namespaces. They must never be localized or changed without
/// an explicit migration for existing API keys.
/// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The stable secret namespace for the provider.</returns>
public static string ToSecretId(this LLMProviders llmProvider) => llmProvider switch
public string ToSecretId() => provider switch
{
LLMProviders.NONE => "No provider selected",
@@ -119,59 +119,58 @@ public static class LLMProvidersExtensions
/// <summary>
/// Get a provider's confidence.
/// </summary>
/// <param name="llmProvider">The provider.</param>
/// <param name="settingsManager">The settings manager.</param>
/// <returns>The confidence of the provider.</returns>
public static Confidence GetConfidence(this LLMProviders llmProvider, SettingsManager settingsManager) => llmProvider switch
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)),
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)),
).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)),
).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)),
).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)),
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
@@ -183,22 +182,21 @@ public static class LLMProvidersExtensions
"https://www.requesty.ai/privacy/subprocessors",
"https://www.requesty.ai/dpa",
"https://www.requesty.ai/terms"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
).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)),
};
/// <summary>
/// Determines if the specified provider supports embeddings.
/// </summary>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>True if the provider supports embeddings; otherwise, false.</returns>
public static bool ProvideEmbeddingAPI(this LLMProviders llmProvider) => llmProvider switch
public bool ProvideEmbeddingAPI() => provider switch
{
//
// Providers that support embeddings:
@@ -241,7 +239,7 @@ public static class LLMProvidersExtensions
_ => false,
};
public static bool ProvideTranscriptionAPI(this LLMProviders llmProvider) => llmProvider switch
public bool ProvideTranscriptionAPI() => provider switch
{
//
// Providers that support transcription:
@@ -296,9 +294,8 @@ public static class LLMProvidersExtensions
/// larger images through another provider. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit.
/// </remarks>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>The size limit of one image file, or null when the provider states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this LLMProviders llmProvider) => llmProvider switch
public long? GetMaxImageSizeBytes() => provider switch
{
//
// Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four
@@ -309,37 +306,7 @@ public static class LLMProvidersExtensions
_ => null,
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
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);
}
/// <summary>
/// Creates a new provider instance based on the embedding provider value.
/// </summary>
/// <param name="embeddingProviderSettings">The embedding provider settings.</param>
/// <returns>The provider instance.</returns>
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);
}
/// <summary>
/// Creates a new provider instance based on the speech provider value.
/// </summary>
/// <param name="transcriptionProviderSettings">The speech provider settings.</param>
/// <returns>The provider instance.</returns>
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 = "")
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.
@@ -393,22 +360,7 @@ public static class LLMProvidersExtensions
}
}
/// <summary>
/// Checks whether the organization prohibited the given provider.
/// </summary>
/// <remarks>
/// The settings come from Program.SERVICE_PROVIDER, because none of the callers of CreateProvider
/// hands them in. Tests which create providers without the services of the app have no settings
/// there, and nothing is prohibited for them.
/// </remarks>
private static bool IsProhibitedByOrganization(LLMProviders provider)
{
// Declared as never null, but it is null until the app has started its services:
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
return Program.SERVICE_PROVIDER?.GetService<SettingsManager>() is { } settingsManager && settingsManager.IsProviderProhibited(provider);
}
public static string GetCreationURL(this LLMProviders provider) => provider switch
public string GetCreationURL() => provider switch
{
LLMProviders.OPEN_AI => "https://platform.openai.com/signup",
LLMProviders.MISTRAL => "https://console.mistral.ai/",
@@ -433,7 +385,7 @@ public static class LLMProvidersExtensions
_ => string.Empty,
};
public static string GetDashboardURL(this LLMProviders provider) => provider switch
public string GetDashboardURL() => provider switch
{
LLMProviders.OPEN_AI => "https://platform.openai.com/usage",
LLMProviders.MISTRAL => "https://console.mistral.ai/usage/",
@@ -456,7 +408,7 @@ public static class LLMProvidersExtensions
_ => string.Empty,
};
public static bool HasDashboard(this LLMProviders provider) => provider switch
public bool HasDashboard() => provider switch
{
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
@@ -479,14 +431,14 @@ public static class LLMProvidersExtensions
_ => false,
};
public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch
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 static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch
public bool IsLLMModelProvidedManually() => provider switch
{
LLMProviders.FIREWORKS => true,
_ => false,
@@ -496,10 +448,9 @@ public static class LLMProvidersExtensions
/// Determines if the model selection should be completely hidden for LLM providers.
/// This is the case when the host does not support model selection.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="host">The host for self-hosted providers.</param>
/// <returns>True if model selection should be hidden; otherwise, false.</returns>
public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch
public bool IsLLMModelSelectionHidden(Host host) => provider switch
{
_ => false,
};
@@ -508,22 +459,21 @@ public static class LLMProvidersExtensions
/// 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).
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="host">The host for self-hosted providers.</param>
/// <returns>True if model selection should be hidden; otherwise, false.</returns>
public static bool IsTranscriptionModelSelectionHidden(this LLMProviders provider, Host host) => provider switch
public bool IsTranscriptionModelSelectionHidden(Host host) => provider switch
{
LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP,
_ => false,
};
public static bool IsHostNeeded(this LLMProviders provider) => provider switch
public bool IsHostNeeded() => provider switch
{
LLMProviders.SELF_HOSTED => true,
_ => false,
};
public static bool IsHostnameNeeded(this LLMProviders provider) => provider switch
public bool IsHostnameNeeded() => provider switch
{
LLMProviders.SELF_HOSTED => true,
LLMProviders.LITE_LLM => true,
@@ -540,16 +490,15 @@ public static class LLMProvidersExtensions
/// 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.
/// </remarks>
/// <param name="provider">The provider.</param>
/// <returns>True when the provider runs on a self-hosted server; otherwise, false.</returns>
public static bool RunsOnSelfHostedServer(this LLMProviders provider) => provider switch
public bool RunsOnSelfHostedServer() => provider switch
{
LLMProviders.SELF_HOSTED => true,
LLMProviders.LITE_LLM => true,
_ => false,
};
public static bool IsAPIKeyNeeded(this LLMProviders provider, Host host) => provider switch
public bool IsAPIKeyNeeded(Host host) => provider switch
{
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
@@ -580,7 +529,7 @@ public static class LLMProvidersExtensions
_ => false,
};
public static bool ShowRegisterButton(this LLMProviders provider) => provider switch
public bool ShowRegisterButton() => provider switch
{
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
@@ -604,7 +553,7 @@ public static class LLMProvidersExtensions
_ => false,
};
public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey)
public bool CanLoadModels(Host host, string? apiKey)
{
if (provider is LLMProviders.SELF_HOSTED)
return host.IsModelListSupported();
@@ -618,9 +567,64 @@ public static class LLMProvidersExtensions
return true;
}
public static bool IsHFInstanceProviderNeeded(this LLMProviders provider) => provider switch
public bool IsHFInstanceProviderNeeded() => provider switch
{
LLMProviders.HUGGINGFACE => true,
_ => false,
};
}
/// <param name="providerSettings">The provider settings.</param>
extension(AIStudio.Settings.Provider providerSettings)
{
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <returns>The provider instance.</returns>
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);
}
}
/// <param name="embeddingProviderSettings">The embedding provider settings.</param>
extension(EmbeddingProvider embeddingProviderSettings)
{
/// <summary>
/// Creates a new provider instance based on the embedding provider value.
/// </summary>
/// <returns>The provider instance.</returns>
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);
}
}
/// <param name="transcriptionProviderSettings">The speech provider settings.</param>
extension(TranscriptionProvider transcriptionProviderSettings)
{
/// <summary>
/// Creates a new provider instance based on the speech provider value.
/// </summary>
/// <returns>The provider instance.</returns>
public IProvider CreateProvider()
{
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION);
}
}
/// <summary>
/// Checks whether the organization prohibited the given provider.
/// </summary>
/// <remarks>
/// The settings come from Program.SERVICE_PROVIDER, because none of the callers of CreateProvider
/// hands them in. Tests which create providers without the services of the app have no settings
/// there, and nothing is prohibited for them.
/// </remarks>
private static bool IsProhibitedByOrganization(LLMProviders provider)
{
// Declared as never null, but it is null until the app has started its services:
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
return Program.SERVICE_PROVIDER?.GetService<SettingsManager>() is { } settingsManager && settingsManager.IsProviderProhibited(provider);
}
}
@@ -5,10 +5,15 @@ 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)
extension(AIStudio.Settings.Provider provider)
{
public string GetIconUrl(bool isDarkMode)
=> provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
}
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl)
extension(LLMProviders provider)
{
public string GetIconUrl(bool isDarkMode, string? customIconDataUrl)
{
if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true)
return customIconDataUrl;
@@ -16,7 +21,7 @@ public static class LLMProvidersIconExtensions
return provider.GetIconUrl(isDarkMode);
}
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch
public string GetIconUrl(bool isDarkMode) => provider switch
{
LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg",
@@ -40,6 +45,7 @@ public static class LLMProvidersIconExtensions
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
};
}
private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty;
}
@@ -6,7 +6,9 @@ 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)
{
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),
@@ -18,4 +20,5 @@ public static class ModelLoadFailureReasonExtensions
_ => string.Empty,
};
}
}
@@ -4,6 +4,9 @@ namespace AIStudio.Provider.OpenAI;
public static class SystemPromptRoleExtensions
{
/// <param name="role">The role the model profile states.</param>
extension(SystemPromptRole role)
{
/// <summary>
/// Spells a system prompt role the way OpenAI's APIs expect it.
/// </summary>
@@ -12,9 +15,8 @@ public static class SystemPromptRoleExtensions
/// 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.
/// </remarks>
/// <param name="role">The role the model profile states.</param>
/// <returns>The role, as OpenAI names it.</returns>
public static string ToOpenAIRole(this SystemPromptRole role) => role switch
public string ToOpenAIRole() => role switch
{
SystemPromptRole.SYSTEM => "system",
SystemPromptRole.DEVELOPER => "developer",
@@ -22,4 +24,5 @@ public static class SystemPromptRoleExtensions
_ => "developer",
};
}
}
@@ -6,6 +6,8 @@ public static class ProviderRequestFailureReasonExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderRequestFailureReasonExtensions).Namespace, nameof(ProviderRequestFailureReasonExtensions));
extension(ProviderRequestFailureReason failureReason)
{
/// <summary>
/// Names the kind of failure in a few words.
/// </summary>
@@ -13,7 +15,7 @@ public static class ProviderRequestFailureReasonExtensions
/// 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.
/// </remarks>
public static string GetName(this ProviderRequestFailureReason failureReason) => failureReason switch
public string GetName() => failureReason switch
{
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"),
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"),
@@ -37,11 +39,12 @@ public static class ProviderRequestFailureReasonExtensions
/// 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.
/// </remarks>
public static bool IsFixedInProviderSettings(this ProviderRequestFailureReason failureReason) => failureReason is
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;
}
}
@@ -2,7 +2,10 @@ namespace AIStudio.Provider.SelfHosted;
public static class HostExtensions
{
public static string Name(this Host host) => host switch
/// <param name="host">The engine.</param>
extension(Host host)
{
public string Name() => host switch
{
Host.NONE => "None",
@@ -16,22 +19,22 @@ public static class HostExtensions
_ => "Unknown",
};
public static string BaseURL(this Host host) => host switch
public string BaseURL() => host switch
{
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
public string ChatURL() => host switch
{
_ => "chat/completions",
};
public static string TranscriptionURL(this Host host) => host switch
public string TranscriptionURL() => host switch
{
_ => "audio/transcriptions",
};
public static string EmbeddingURL(this Host host) => host switch
public string EmbeddingURL() => host switch
{
_ => "embeddings",
};
@@ -44,9 +47,8 @@ public static class HostExtensions
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <param name="host">The engine.</param>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this Host host) => host switch
public long? GetMaxImageSizeBytes() => host switch
{
//
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
@@ -59,7 +61,7 @@ public static class HostExtensions
_ => null,
};
public static bool IsChatSupported(this Host host)
public bool IsChatSupported()
{
switch (host)
{
@@ -76,7 +78,7 @@ public static class HostExtensions
}
}
public static bool IsEmbeddingSupported(this Host host)
public bool IsEmbeddingSupported()
{
switch (host)
{
@@ -92,7 +94,7 @@ public static class HostExtensions
}
}
public static bool IsTranscriptionSupported(this Host host)
public bool IsTranscriptionSupported()
{
switch (host)
{
@@ -120,9 +122,8 @@ public static class HostExtensions
/// 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.
/// </remarks>
/// <param name="host">The engine.</param>
/// <returns>True when the models can be loaded from the engine; otherwise, false.</returns>
public static bool IsModelListSupported(this Host host)
public bool IsModelListSupported()
{
switch (host)
{
@@ -139,4 +140,5 @@ public static class HostExtensions
return false;
}
}
}
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class BiasCategoryExtensions
{
public static string ToName(this BiasCategory biasCategory) => biasCategory switch
extension(BiasCategory biasCategory)
{
public string ToName() => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
@@ -12,7 +14,7 @@ public static class BiasCategoryExtensions
_ => "Unknown category",
};
public static string GetThoughts(this BiasCategory biasCategory) => biasCategory switch
public string GetThoughts() => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER =>
"""
@@ -52,4 +54,5 @@ public static class BiasCategoryExtensions
_ => string.Empty,
};
}
}
@@ -6,7 +6,9 @@ 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)
{
public string ToSelectionText() => security switch
{
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"),
@@ -16,7 +18,7 @@ public static class DataSourceSecurityExtensions
_ => TB("Unknown security policy")
};
public static string ToInfoText(this DataSourceSecurity security) => security switch
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."),
@@ -26,6 +28,18 @@ public static class DataSourceSecurityExtensions
_ => TB("Unknown security policy")
};
public TextColor GetColor() => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
_ => TextColor.ERROR
};
}
/// <param name="held">The data security held so far.</param>
extension(DataSourceSecurity held)
{
/// <summary>
/// The data security which demands what the one held and the one arriving demand together.
/// </summary>
@@ -35,21 +49,13 @@ public static class DataSourceSecurityExtensions
/// 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.
/// </remarks>
/// <param name="held">The data security held so far.</param>
/// <param name="arriving">What the data arriving demands.</param>
/// <returns>The tightened data security; never looser than either of the two.</returns>
public static DataSourceSecurity TightenedBy(this DataSourceSecurity held, DataSourceSecurity arriving) => (held, arriving) switch
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,
};
public static TextColor GetColor(this DataSourceSecurity security) => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
_ => TextColor.ERROR
};
}
}
@@ -9,12 +9,14 @@ public static class DataSourceTypeExtension
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceTypeExtension).Namespace, nameof(DataSourceTypeExtension));
/// <param name="type">The data source type.</param>
extension(DataSourceType type)
{
/// <summary>
/// Get the display name of the data source type.
/// </summary>
/// <param name="type">The data source type.</param>
/// <returns>The display name of the data source type.</returns>
public static string GetDisplayName(this DataSourceType type) => type switch
public string GetDisplayName() => type switch
{
DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
@@ -23,4 +25,5 @@ public static class DataSourceTypeExtension
_ => TB("None"),
};
}
}
@@ -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)
{
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")
};
}
}
@@ -6,7 +6,10 @@ 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
/// <param name="maxAge">The period.</param>
extension(MailboxMaxAge maxAge)
{
public string GetName() => maxAge switch
{
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
@@ -20,10 +23,9 @@ public static class MailboxMaxAgeExtensions
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="maxAge">The period.</param>
/// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns>
public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch
public DateTimeOffset? GetReceivedSince(DateTimeOffset now) => maxAge switch
{
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
@@ -33,4 +35,5 @@ public static class MailboxMaxAgeExtensions
// The shortest period also stands in for a value this version does not know, as it does in the settings:
_ => now.AddMonths(-3),
};
}
}
@@ -16,7 +16,9 @@ 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)
{
public string GetName() => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
@@ -28,11 +30,12 @@ public static class MailboxTransportSecurityExtensions
/// The port the encryption usually comes with.
/// </summary>
/// <returns>The port, or null when there is no usual one.</returns>
public static int? GetUsualPort(this MailboxTransportSecurity transportSecurity) => transportSecurity switch
public int? GetUsualPort() => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT,
MailboxTransportSecurity.STARTTLS => STARTTLS_PORT,
_ => null,
};
}
}
@@ -6,6 +6,9 @@ public static class OutboundDataRequirementExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions));
/// <param name="requirement">What the chat demands.</param>
extension(OutboundDataRequirement requirement)
{
/// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary>
@@ -17,10 +20,9 @@ public static class OutboundDataRequirementExtensions
/// 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.
/// </remarks>
/// <param name="requirement">What the chat demands.</param>
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param>
/// <returns>The text for the user.</returns>
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch
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()),
@@ -31,6 +33,7 @@ public static class OutboundDataRequirementExtensions
// 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<DataSourceMailbox> mailboxes)
{
@@ -6,16 +6,18 @@ public static class OutboundDataRestrictionExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions));
/// <param name="restriction">The restriction, as it was read.</param>
extension(OutboundDataRestriction restriction)
{
/// <summary>
/// The stricter of two restrictions.
/// </summary>
/// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks>
/// <param name="restriction">The one restriction.</param>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other)
public OutboundDataRestriction StricterOf(OutboundDataRestriction other)
{
var knownRestriction = restriction.OrStrictestWhenUnknown();
var knownOther = other.OrStrictestWhenUnknown();
@@ -30,11 +32,10 @@ public static class OutboundDataRestrictionExtensions
/// 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.
/// </remarks>
/// <param name="restriction">The restriction as it was read.</param>
/// <returns>The restriction itself, or the strictest one when this version does not know it.</returns>
public static OutboundDataRestriction OrStrictestWhenUnknown(this OutboundDataRestriction restriction) => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public OutboundDataRestriction OrStrictestWhenUnknown() => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public static string GetName(this OutboundDataRestriction restriction) => restriction switch
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"),
@@ -46,7 +47,7 @@ public static class OutboundDataRestrictionExtensions
/// <summary>
/// What a chat may still do on this level, once internal content entered it.
/// </summary>
public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch
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."),
@@ -54,4 +55,5 @@ public static class OutboundDataRestrictionExtensions
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
}
}
@@ -6,7 +6,10 @@ 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
/// <param name="feature">The preview feature to check.</param>
extension(PreviewFeatures feature)
{
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"),
@@ -29,9 +32,8 @@ public static class PreviewFeaturesExtensions
/// 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.
/// </remarks>
/// <param name="feature">The preview feature to check.</param>
/// <returns>True when the preview feature is released, false otherwise.</returns>
public static bool IsReleased(this PreviewFeatures feature) => feature switch
public bool IsReleased() => feature switch
{
PreviewFeatures.PRE_READ_PDF_2025 => true,
PreviewFeatures.PRE_PLUGINS_2025 => true,
@@ -41,11 +43,12 @@ public static class PreviewFeaturesExtensions
_ => false
};
public static bool IsEnabled(this PreviewFeatures feature, SettingsManager settingsManager)
public bool IsEnabled(SettingsManager settingsManager)
{
if(feature.IsReleased())
return true;
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature);
}
}
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class PreviewVisibilityExtensions
{
public static IList<PreviewFeatures> GetPreviewFeatures(this PreviewVisibility visibility)
extension(PreviewVisibility visibility)
{
public IList<PreviewFeatures> GetPreviewFeatures()
{
var features = new List<PreviewFeatures>();
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
@@ -34,7 +36,7 @@ public static class PreviewVisibilityExtensions
return features;
}
public static HashSet<PreviewFeatures> FilterPreviewFeatures(this PreviewVisibility visibility, HashSet<PreviewFeatures> enabledFeatures)
public HashSet<PreviewFeatures> FilterPreviewFeatures(HashSet<PreviewFeatures> enabledFeatures)
{
var filteredFeatures = new HashSet<PreviewFeatures>();
var previewFeatures = visibility.GetPreviewFeatures();
@@ -46,4 +48,5 @@ public static class PreviewVisibilityExtensions
return filteredFeatures;
}
}
}
@@ -2,12 +2,17 @@ namespace AIStudio.Settings.DataModel.PreviousModels;
public static class ProviderV4Extensions
{
public static List<AIStudio.Settings.Provider> MigrateFromV4ToV5(this IEnumerable<Provider> providers)
extension(IEnumerable<Provider> providers)
{
public List<AIStudio.Settings.Provider> MigrateFromV4ToV5()
{
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList();
}
}
public static AIStudio.Settings.Provider MigrateFromV4ToV5(this Provider provider) => new()
extension(Provider provider)
{
public AIStudio.Settings.Provider MigrateFromV4ToV5() => new()
{
Num = provider.Num,
Id = provider.Id,
@@ -18,4 +23,5 @@ public static class ProviderV4Extensions
Hostname = provider.Hostname,
Host = provider.Host,
};
}
}
@@ -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)
{
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"),
};
}
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class StartPageExtensions
{
public static string ToRoute(this StartPage startPage) => startPage switch
extension(StartPage startPage)
{
public string ToRoute() => startPage switch
{
StartPage.HOME => string.Empty,
StartPage.CHAT => Routes.CHAT,
@@ -14,4 +16,5 @@ public static class StartPageExtensions
_ => string.Empty,
};
}
}
@@ -6,7 +6,9 @@ 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)
{
public string GetName() => theme switch
{
Themes.SYSTEM => TB("Synchronized with the operating system settings"),
Themes.LIGHT => TB("Always use light theme"),
@@ -14,4 +16,5 @@ public static class ThemesExtensions
_ => TB("Unknown setting"),
};
}
}
@@ -6,7 +6,9 @@ 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)
{
public string GetName() => bitrate switch
{
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
@@ -15,7 +17,7 @@ public static class TranscriptionOpusBitrateExtensions
_ => TB("Unknown"),
};
public static uint GetBitsPerSecond(this TranscriptionOpusBitrate bitrate) => bitrate switch
public uint GetBitsPerSecond() => bitrate switch
{
TranscriptionOpusBitrate.KBPS_32 => 32_000,
TranscriptionOpusBitrate.KBPS_64 => 64_000,
@@ -26,4 +28,5 @@ public static class TranscriptionOpusBitrateExtensions
// went missing from transcripts in the first place. Fall back to the recommended bitrate.
_ => 128_000,
};
}
}
@@ -2,6 +2,9 @@ namespace AIStudio.Settings;
public static class DataSourceOrderExtensions
{
/// <param name="dataSources">The data sources to sort.</param>
extension(IEnumerable<IDataSource> dataSources)
{
/// <summary>
/// Sorts data sources in a fixed order: by their number, then by their ID.
/// </summary>
@@ -12,10 +15,10 @@ public static class DataSourceOrderExtensions
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks>
/// <param name="dataSources">The data sources to sort.</param>
/// <returns>The data sources in their fixed order.</returns>
public static IReadOnlyList<IDataSource> InFixedOrder(this IEnumerable<IDataSource> dataSources) => dataSources
public IReadOnlyList<IDataSource> InFixedOrder() => dataSources
.OrderBy(dataSource => dataSource.Num)
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal)
.ToList();
}
}
@@ -30,13 +30,15 @@ public static class DataSourceOutboundDataExtensions
? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)
: organizationMinimum;
/// <param name="dataSource">The data source.</param>
extension(IDataSource dataSource)
{
/// <summary>
/// The least strict level this data source may have.
/// </summary>
/// <param name="dataSource">The data source.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The least strict level allowed.</returns>
public static OutboundDataRestriction GetMinimumOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) =>
public OutboundDataRestriction GetMinimumOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum);
/// <summary>
@@ -47,9 +49,9 @@ public static class DataSourceOutboundDataExtensions
/// 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.
/// </remarks>
/// <param name="dataSource">The data source.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The level which applies.</returns>
public static OutboundDataRestriction GetEffectiveOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) =>
public OutboundDataRestriction GetEffectiveOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum));
}
}
@@ -5,7 +5,9 @@ namespace AIStudio.Settings;
public static class DataSourceSecurityTrustExtensions
{
public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager)
extension(Provider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return false;
@@ -13,7 +15,29 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager)
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);
}
extension(EmbeddingProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
@@ -21,7 +45,20 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager 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;
@@ -29,7 +66,20 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager 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;
@@ -37,31 +87,7 @@ public static class DataSourceSecurityTrustExtensions
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)
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider is NoProvider)
return ConfidenceLevel.NONE;
@@ -69,32 +95,29 @@ public static class DataSourceSecurityTrustExtensions
return provider.Provider.GetConfidence(settingsManager).Level;
}
public static bool AllowsDataSourceAccess(this Provider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
public bool AllowsDataSourceAccess(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 bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
}
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
extension(bool usingTrustedProvider)
{
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
{
DataSourceSecurity.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false,
};
}
public static bool AllowsDataSourceConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel requiredConfidenceLevel)
extension(ConfidenceLevel providerConfidenceLevel)
{
public bool AllowsDataSourceConfidenceLevel(ConfidenceLevel requiredConfidenceLevel)
{
if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true;
@@ -102,16 +125,6 @@ public static class DataSourceSecurityTrustExtensions
return providerConfidenceLevel >= requiredConfidenceLevel;
}
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static bool IsAllowedMailboxConfidence(this ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
/// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary>
@@ -121,15 +134,31 @@ public static class DataSourceSecurityTrustExtensions
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails.
/// </remarks>
public static bool AllowsMailboxConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel mailboxConfidenceLevel)
public bool AllowsMailboxConfidenceLevel(ConfidenceLevel mailboxConfidenceLevel)
{
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false;
return providerConfidenceLevel >= mailboxConfidenceLevel;
}
}
public static ConfidenceLevel GetRequiredConfidenceLevel(this IEnumerable<IDataSource> dataSources)
extension(ConfidenceLevel confidenceLevel)
{
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool IsAllowedMailboxConfidence() => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
}
extension(IEnumerable<IDataSource> dataSources)
{
public ConfidenceLevel GetRequiredConfidenceLevel()
{
var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
@@ -139,7 +168,7 @@ public static class DataSourceSecurityTrustExtensions
return requiredConfidenceLevel;
}
public static DataSourceSecurity GetRequiredSecurityPolicy(this IEnumerable<IDataSource> dataSources)
public DataSourceSecurity GetRequiredSecurityPolicy()
{
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
@@ -153,12 +182,7 @@ public static class DataSourceSecurityTrustExtensions
return requiredSecurityPolicy;
}
public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager)
{
@@ -6,6 +6,9 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <param name="provider">The configured provider.</param>
extension(Provider provider)
{
/// <summary>
/// Get the effective reasoning indicator state for the configured provider instance.
/// </summary>
@@ -15,9 +18,8 @@ public static partial class ProviderExtensions
/// 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.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The effective reasoning indicator state.</returns>
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider)
public ReasoningIndicatorState GetReasoningIndicatorState()
{
var reasoning = provider.GetModelProfile().Reasoning;
if (reasoning is ReasoningSupport.ALWAYS)
@@ -40,4 +42,5 @@ public static partial class ProviderExtensions
return ReasoningIndicatorState.NONE;
}
}
}
@@ -7,6 +7,9 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <param name="provider">The configured provider.</param>
extension(Provider provider)
{
/// <summary>
/// Everything the app knows about the model this provider instance is configured with.
/// </summary>
@@ -16,9 +19,8 @@ public static partial class ProviderExtensions
/// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model.</returns>
public static ModelProfile GetModelProfile(this Provider provider)
public ModelProfile GetModelProfile()
{
var automatic = provider.GetAutomaticModelProfile();
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
@@ -33,46 +35,13 @@ public static partial class ProviderExtensions
/// 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.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
public static ModelProfile GetAutomaticModelProfile(this Provider provider)
public ModelProfile GetAutomaticModelProfile()
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
}
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own installation.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="provider">The LLM provider the model is reached through.</param>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public static ModelProfile GetModelProfile(this LLMProviders provider, 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;
}
/// <summary>
/// Get whether the model used by the configured provider accepts images as input.
/// </summary>
@@ -85,14 +54,52 @@ public static partial class ProviderExtensions
/// 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.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider)
public bool SupportsImageInput()
{
var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
}
}
/// <param name="provider">The LLM provider the model is reached through.</param>
extension(LLMProviders provider)
{
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own installation.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
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;
}
}
/// <param name="model">The model to check.</param>
extension(Model model)
{
/// <summary>
/// Checks whether this model can be used for chatting.
/// </summary>
@@ -107,32 +114,29 @@ public static partial class ProviderExtensions
/// 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.
/// </remarks>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
public bool IsChatModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary>
/// Checks whether this model creates embeddings.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns>
public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
public bool IsEmbeddingModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary>
/// Checks whether this model transcribes audio.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns>
public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
public bool IsTranscriptionModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary>
/// Checks whether this model generates images.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns>
public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
public bool IsImageModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
}
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => category switch
{
AssistantCategory.AS_IS => TB("Select the assistant category"),
AssistantCategory.GENERAL => TB("General"),
@@ -21,11 +23,12 @@ public static class AssistantCategoryExtensions
_ => string.Empty,
};
public static string NameSelecting(this AssistantCategory category)
public string NameSelecting()
{
if(category is AssistantCategory.AS_IS)
return TB("Please select the assistant category");
return category.Name();
}
}
}
@@ -10,16 +10,18 @@ public static class AssistantVisibilityExtensions
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantVisibilityExtensions));
/// <param name="settingsManager">The settings manager to check configuration against.</param>
extension(SettingsManager settingsManager)
{
/// <summary>
/// Checks if an assistant should be visible based on configuration and optional preview feature requirements.
/// </summary>
/// <param name="settingsManager">The settings manager to check configuration against.</param>
/// <param name="withLogging">Whether to log visibility decisions.</param>
/// <param name="assistantName">The name of the assistant to check (for logging purposes).</param>
/// <param name="component">The assistant component to check.</param>
/// <param name="requiredPreviewFeature">Optional preview feature that must be enabled for the assistant to be visible.</param>
/// <returns>True if the assistant should be visible, false otherwise.</returns>
public static bool IsAssistantVisible(this SettingsManager settingsManager, Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
public bool IsAssistantVisible(Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
{
withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName);
@@ -92,4 +94,5 @@ public static class AssistantVisibilityExtensions
return !isHidden;
}
}
}
@@ -7,7 +7,9 @@ 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)
{
public string DisplayName() => authMethod switch
{
AuthMethod.NONE => TB("None"),
AuthMethod.USERNAME_PASSWORD => TB("Username & Password"),
@@ -16,4 +18,5 @@ public static class AuthMethodsV1Extensions
_ => TB("Unknown authentication method"),
};
}
}
@@ -4,10 +4,13 @@ namespace AIStudio.Tools;
public static class ChatTemplateExtensions
{
public static IEnumerable<ChatTemplate> GetAllChatTemplates(this IEnumerable<ChatTemplate> chatTemplates)
extension(IEnumerable<ChatTemplate> chatTemplates)
{
public IEnumerable<ChatTemplate> GetAllChatTemplates()
{
yield return ChatTemplate.NO_CHAT_TEMPLATE;
foreach (var chatTemplate in chatTemplates)
yield return chatTemplate;
}
}
}
@@ -2,13 +2,15 @@ namespace AIStudio.Tools.CodeHighlighting;
public static class CodeTokenKindExtensions
{
/// <param name="kind">What the part is.</param>
extension(CodeTokenKind kind)
{
/// <summary>
/// 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.
/// </summary>
/// <param name="kind">What the part is.</param>
/// <returns>The CSS class of the part.</returns>
public static string ToCssClass(this CodeTokenKind kind) => kind switch
public string ToCssClass() => kind switch
{
CodeTokenKind.KEYWORD => "hljs-keyword",
CodeTokenKind.TYPE => "hljs-type",
@@ -24,4 +26,5 @@ public static class CodeTokenKindExtensions
_ => string.Empty,
};
}
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => language switch
{
CommonLanguages.AS_IS => TB("Do not change the language"),
@@ -25,7 +27,7 @@ public static class CommonLanguageExtensions
_ => TB("Other"),
};
public static string ToIETFTag(this CommonLanguages language) => language switch
public string ToIETFTag() => language switch
{
CommonLanguages.AS_IS => string.Empty,
@@ -44,7 +46,7 @@ public static class CommonLanguageExtensions
_ => string.Empty,
};
public static string PromptSummarizing(this CommonLanguages language, string customLanguage) => language switch
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}.",
@@ -52,14 +54,14 @@ public static class CommonLanguageExtensions
_ => $"Output your summary in {language.Name()} ({language}).",
};
public static string PromptTranslation(this CommonLanguages language, string customLanguage) => language switch
public string PromptTranslation(string customLanguage) => language switch
{
CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.",
_ => $"Translate the source text to {language.Name()} ({language}).",
};
public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch
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.",
@@ -67,7 +69,7 @@ public static class CommonLanguageExtensions
_ => $"Use the language {language.Name()} ({language}) for your output.",
};
public static string NameSelecting(this CommonLanguages language)
public string NameSelecting()
{
if(language is CommonLanguages.AS_IS)
return TB("Please select the target language");
@@ -75,11 +77,12 @@ public static class CommonLanguageExtensions
return language.Name();
}
public static string NameSelectingOptional(this CommonLanguages language)
public string NameSelectingOptional()
{
if(language is CommonLanguages.AS_IS)
return TB("Do not specify the language");
return language.Name();
}
}
}
@@ -10,14 +10,16 @@ public static class ComponentsExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions));
/// <param name="component">The component to look up.</param>
extension(Components component)
{
/// <summary>
/// Gets the preview feature a component belongs to. Components that are generally available
/// return <see cref="PreviewFeatures.NONE"/>. This is the single place that maps a component to
/// its preview feature, so visibility checks never need to special-case one assistant.
/// </summary>
/// <param name="component">The component to look up.</param>
/// <returns>The required preview feature.</returns>
public static PreviewFeatures RequiredPreviewFeature(this Components component) => component switch
public PreviewFeatures RequiredPreviewFeature() => component switch
{
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
@@ -35,9 +37,8 @@ public static class ComponentsExtensions
/// component. All must be excluded from the single-slot checks, which is why this is a
/// capability and not a component comparison.
/// </remarks>
/// <param name="component">The component to look up.</param>
/// <returns><c>true</c> when the component owns exactly one session slot.</returns>
public static bool HasSingleSessionSlot(this Components component) => component switch
public bool HasSingleSessionSlot() => component switch
{
Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
Components.VISUAL_BRIEFING_ASSISTANT => false,
@@ -48,16 +49,15 @@ public static class ComponentsExtensions
/// <summary>
/// Gets the kind of media-import owner a component creates for its attachments.
/// </summary>
/// <param name="component">The component to look up.</param>
/// <returns>The media-import owner kind.</returns>
public static MediaImportOwnerKind MediaOwnerKind(this Components component) => component switch
public MediaImportOwnerKind MediaOwnerKind() => component switch
{
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
_ => MediaImportOwnerKind.ASSISTANT,
};
public static bool AllowSendTo(this Components component) => component switch
public bool AllowSendTo() => component switch
{
Components.NONE => false,
Components.DYNAMIC_ASSISTANT => false,
@@ -81,7 +81,7 @@ public static class ComponentsExtensions
_ => true,
};
public static string Name(this Components component) => component switch
public string Name() => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
@@ -111,7 +111,7 @@ public static class ComponentsExtensions
_ => Enum.GetName(component)!,
};
public static ComponentsData GetData(this Components destination) => destination switch
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),
@@ -136,7 +136,7 @@ public static class ComponentsExtensions
_ => new(Event.NONE, Routes.ASSISTANTS),
};
public static ConfidenceLevel MinimumConfidence(this Components component, SettingsManager settingsManager) => component switch
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,
@@ -169,7 +169,7 @@ public static class ComponentsExtensions
_ => default,
};
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
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,
@@ -209,7 +209,7 @@ public static class ComponentsExtensions
_ => Settings.Provider.NONE,
};
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager)
public ProfilePreselection GetProfilePreselection(SettingsManager settingsManager)
{
var storedValue = component switch
{
@@ -235,11 +235,12 @@ public static class ComponentsExtensions
return ProfilePreselection.FromStoredValue(storedValue);
}
public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch
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,
};
}
}
@@ -6,7 +6,9 @@ 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)
{
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"),
@@ -19,4 +21,5 @@ public static class ConfidenceSchemesExtensions
_ => TB("Unknown confidence scheme"),
};
}
}
@@ -2,6 +2,9 @@ namespace AIStudio.Tools;
public static class DirectoryInfoExtensions
{
/// <param name="directoryInfo">The root directory to determine the size of.</param>
extension(DirectoryInfo directoryInfo)
{
/// <summary>
/// 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.
@@ -17,7 +20,6 @@ public static class DirectoryInfoExtensions
/// 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.
/// </remarks>
/// <param name="directoryInfo">The root directory to determine the size of.</param>
/// <param name="reportCurrentTotalSize">The callback to report the current total size of the directory.</param>
/// <param name="reportCurrentNumFiles">The callback to report the current number of files found.</param>
/// <param name="reportNextFile">The callback to report the next file found. The file name is relative to the root directory.</param>
@@ -25,7 +27,7 @@ public static class DirectoryInfoExtensions
/// <param name="done">The callback to report that the operation is done.</param>
/// <param name="cancellationToken">The cancellation token to stop the operation.</param>
/// <param name="selection">Which files count, e.g., those a data source indexes. Every file counts when it is missing.</param>
internal static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action<long> reportCurrentTotalSize, Action<long> reportCurrentNumFiles, Action<string> reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null)
internal async Task DetermineContentSize(Action<long> reportCurrentTotalSize, Action<long> reportCurrentNumFiles, Action<string> reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null)
{
var rootDirectoryLen = directoryInfo.FullName.Length;
var filesToCount = selection ?? FolderFileSelection.EVERY_FILE;
@@ -64,4 +66,5 @@ public static class DirectoryInfoExtensions
done?.Invoke();
}
}
}
@@ -6,7 +6,9 @@ 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)
{
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)."),
@@ -14,4 +16,5 @@ public static class ProviderTypeExtensions
_ => TB("Unknown configuration. This data source cannot be used at the moment."),
};
}
}
@@ -4,8 +4,11 @@ namespace AIStudio.Tools;
public static class ElementReferenceExtensions
{
public static async ValueTask ScrollIntoViewAsync(this ElementReference elementReference, IJSRuntime jsRuntime)
extension(ElementReference elementReference)
{
public async ValueTask ScrollIntoViewAsync(IJSRuntime jsRuntime)
{
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference);
}
}
}
@@ -2,7 +2,13 @@ namespace AIStudio.Tools;
public static class EncryptedTextExtensions
{
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
extension(string data)
{
public async Task<EncryptedText> Encrypt(Encryption encryption) => await encryption.Encrypt(data);
}
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
extension(EncryptedText encryptedText)
{
public async Task<string> Decrypt(Encryption encryption) => await encryption.Decrypt(encryptedText);
}
}
@@ -8,16 +8,18 @@ public static class ExpressionExtensions
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions));
/// <summary>
/// Extracts the member expression from a given lambda expression representing a property.
/// </summary>
/// <param name="expression">A lambda expression specifying the property for which the member expression is to be extracted.
/// The lambda expression body must represent member access.</param>
/// <typeparam name="TIn">The type of the object containing the property referenced in the lambda expression.</typeparam>
/// <typeparam name="TOut">The type of the property being accessed in the lambda expression.</typeparam>
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression)
{
/// <summary>
/// Extracts the member expression from a given lambda expression representing a property.
/// </summary>
/// <returns>The member expression that represents the property access.</returns>
/// <exception cref="ArgumentException">Thrown if the provided lambda expression does not represent a valid property expression.</exception>
public static MemberExpression GetMemberExpression<TIn, TOut>(this Expression<Func<TIn, TOut>> expression)
public MemberExpression GetMemberExpression()
{
switch (expression.Body)
{
@@ -34,19 +36,22 @@ public static class ExpressionExtensions
throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression));
}
}
}
/// <param name="expression">An expression representing the property to be incremented. The property
/// must be of type uint and belong to the provided object.</param>
/// <typeparam name="TIn">The type of the object that contains the property to be incremented.</typeparam>
/// <typeparam name="TOut">The type of the property to be incremented.</typeparam>
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression) where TOut : IBinaryInteger<TOut>
{
/// <summary>
/// Attempts to increment the value of an uint property for a specified object using a
/// provided expression.
/// </summary>
/// <param name="expression">An expression representing the property to be incremented. The property
/// must be of type uint and belong to the provided object.</param>
/// <param name="data">The object that contains the property referenced by the expression.</param>
/// <param name="type">The type of increment operation to perform (e.g., prefix or postfix).</param>
/// <typeparam name="TIn">The type of the object that contains the property to be incremented.</typeparam>
/// <typeparam name="TOut">The type of the property to be incremented.</typeparam>
/// <returns>An IncrementResult object containing the result of the increment operation.</returns>
public static IncrementResult<TOut> TryIncrement<TIn, TOut>(this Expression<Func<TIn, TOut>> expression, TIn data, IncrementType type) where TOut : IBinaryInteger<TOut>
public IncrementResult<TOut> TryIncrement(TIn data, IncrementType type)
{
// Ensure that the expression body is a member expression:
if (expression.Body is not MemberExpression memberExpression)
@@ -93,4 +98,5 @@ public static class ExpressionExtensions
return new(false, TOut.Zero);
}
}
}
}
@@ -52,12 +52,14 @@ public static class FileExportFormatExtensions
/// </remarks>
public static readonly IReadOnlyList<FileExportFormat> ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS];
/// <param name="format">The format.</param>
extension(FileExportFormat format)
{
/// <summary>
/// Returns the name of the format as shown to the user.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The name of the format.</returns>
public static string ToName(this FileExportFormat format) => format switch
public string ToName() => format switch
{
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
@@ -74,9 +76,8 @@ public static class FileExportFormatExtensions
/// <summary>
/// Returns the icon of the format.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The icon of the format.</returns>
public static string ToIcon(this FileExportFormat format) => format switch
public string ToIcon() => format switch
{
FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord,
FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument,
@@ -92,10 +93,9 @@ public static class FileExportFormatExtensions
/// <summary>
/// Returns the file extension of the format, including the leading dot.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The file extension, or an empty string when the format writes no file or, as code
/// does, takes the extension from its language.</returns>
public static string ToFileExtension(this FileExportFormat format) => format switch
public string ToFileExtension() => format switch
{
FileExportFormat.MICROSOFT_WORD => ".docx",
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
@@ -108,6 +108,160 @@ public static class FileExportFormatExtensions
_ => string.Empty,
};
/// <summary>
/// Determines whether the format holds a table rather than a text.
/// </summary>
/// <returns>True for the formats a spreadsheet opens.</returns>
public bool IsTabular() => format is FileExportFormat.CSV or FileExportFormat.TSV;
/// <summary>
/// Determines whether a file of the format is plain text, which AI Studio writes as it is.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>True, when a text written as it is makes a valid file of the format.</returns>
public bool IsPlainText() => format is FileExportFormat.LATEX or FileExportFormat.MARKDOWN or FileExportFormat.HTML or FileExportFormat.CSV or FileExportFormat.TSV or FileExportFormat.SOURCE_CODE;
/// <summary>
/// Returns the file name the save dialog starts with.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="name">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.</param>
/// <returns>The suggested file name, including its extension.</returns>
public string ToSuggestedFileName(string? name = null) => SuggestFileName(name, format.ToFileExtension());
/// <summary>
/// Returns the filter which the save dialog offers for the format.
/// </summary>
/// <returns>The filter, or null when the format cannot be written or, as code does, takes the
/// filter from its language.</returns>
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,
};
/// <summary>
/// Returns the encoding the file gets written with.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>The encoding to write the file with.</returns>
public Encoding ToFileEncoding() => format switch
{
FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK,
_ => WITHOUT_BYTE_ORDER_MARK,
};
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="text">The text to put into the comment.</param>
/// <param name="comment">The comment, or an empty string when the format has none.</param>
/// <returns>True, when the format knows comments.</returns>
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 = $"<!--{Environment.NewLine}{commentText}{Environment.NewLine}-->";
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;
}
}
/// <summary>
/// Determines whether a link into a local file may name the page it points at.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>True, when a reader of this format follows such a link.</returns>
public bool FollowsPageAnchors() => format switch
{
FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false,
_ => true,
};
/// <summary>
/// Determines whether Pandoc has to be told the title of a document in the format.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>True, when a document of this format needs a title besides its content.</returns>
public bool NeedsPageTitle() => format is FileExportFormat.HTML;
/// <summary>
/// Returns the name Pandoc knows the format by.
/// </summary>
/// <returns>The Pandoc output format, or an empty string when AI Studio writes the file itself.</returns>
public string ToPandocOutputFormat() => format switch
{
FileExportFormat.MICROSOFT_WORD => "docx",
FileExportFormat.OPEN_DOCUMENT_TEXT => "odt",
FileExportFormat.LATEX => "latex",
FileExportFormat.HTML => "html",
_ => string.Empty,
};
/// <summary>
/// Determines whether writing the format needs Pandoc.
/// </summary>
/// <returns>True, when Pandoc converts the message; false, when AI Studio writes the file itself.</returns>
public bool UsesPandoc() => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat());
}
/// <summary>
/// Reads which format a model means when it names a language behind the opening fence of a
/// code block.
@@ -139,41 +293,6 @@ public static class FileExportFormatExtensions
return format is not FileExportFormat.NONE;
}
/// <summary>
/// Determines whether the format holds a table rather than a text.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>True for the formats a spreadsheet opens.</returns>
public static bool IsTabular(this FileExportFormat format) => format is FileExportFormat.CSV or FileExportFormat.TSV;
/// <summary>
/// Determines whether a file of the format is plain text, which AI Studio writes as it is.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a text written as it is makes a valid file of the format.</returns>
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;
/// <summary>
/// Returns the file name the save dialog starts with.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <param name="name">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.</param>
/// <returns>The suggested file name, including its extension.</returns>
public static string ToSuggestedFileName(this FileExportFormat format, string? name = null) => SuggestFileName(name, format.ToFileExtension());
/// <summary>
/// Returns the file name the save dialog starts with, for a file with the given extension.
/// </summary>
@@ -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('.');
}
/// <summary>
/// Returns the filter which the save dialog offers for the format.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The filter, or null when the format cannot be written or, as code does, takes the
/// filter from its language.</returns>
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,
};
/// <summary>
/// Returns the encoding the file gets written with.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>The encoding to write the file with.</returns>
public static Encoding ToFileEncoding(this FileExportFormat format) => format switch
{
FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK,
_ => WITHOUT_BYTE_ORDER_MARK,
};
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <param name="text">The text to put into the comment.</param>
/// <param name="comment">The comment, or an empty string when the format has none.</param>
/// <returns>True, when the format knows comments.</returns>
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 = $"<!--{Environment.NewLine}{commentText}{Environment.NewLine}-->";
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;
}
}
/// <summary>
/// Determines whether a link into a local file may name the page it points at.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a reader of this format follows such a link.</returns>
public static bool FollowsPageAnchors(this FileExportFormat format) => format switch
{
FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false,
_ => true,
};
/// <summary>
/// Determines whether Pandoc has to be told the title of a document in the format.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a document of this format needs a title besides its content.</returns>
public static bool NeedsPageTitle(this FileExportFormat format) => format is FileExportFormat.HTML;
/// <summary>
/// Returns the name Pandoc knows the format by.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The Pandoc output format, or an empty string when AI Studio writes the file itself.</returns>
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,
};
/// <summary>
/// Determines whether writing the format needs Pandoc.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>True, when Pandoc converts the message; false, when AI Studio writes the file itself.</returns>
public static bool UsesPandoc(this FileExportFormat format) => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat());
}
@@ -15,13 +15,15 @@ internal static class FileExtractionErrorCodeExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionErrorCodeExtensions).Namespace, nameof(FileExtractionErrorCodeExtensions));
/// <param name="code">The stable failure code.</param>
extension(FileExtractionErrorCode code)
{
/// <summary>
/// Gets a value indicating whether reading the file again will fail again, as long as the file
/// itself does not change.
/// </summary>
/// <param name="code">The stable failure code.</param>
/// <returns>True, when the reason lies in the file itself.</returns>
internal static bool IsPermanentIndexingFailure(this FileExtractionErrorCode code) => code switch
internal bool IsPermanentIndexingFailure() => code switch
{
//
// The reason lies in the file. Reading it again without changing it produces the same
@@ -58,9 +60,8 @@ internal static class FileExtractionErrorCodeExtensions
/// 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.
/// </remarks>
/// <param name="code">The stable failure code.</param>
/// <returns>The localized name of the cause, or an empty text when the code has none.</returns>
internal static string GetIndexingCauseName(this FileExtractionErrorCode code) => code switch
internal string GetIndexingCauseName() => code switch
{
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"),
FileExtractionErrorCode.NO_CONTENT => TB("No content"),
@@ -103,10 +104,10 @@ internal static class FileExtractionErrorCodeExtensions
/// 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.
/// </remarks>
/// <param name="code">The stable failure code.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToIndexingUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToIndexingMessageFormat(code), fileName);
internal string ToIndexingUserMessage(string fileName) => string.Format(ToIndexingMessageFormat(code), fileName);
}
private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch
{
@@ -14,13 +14,15 @@ internal static class FileExtractionResultExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions));
/// <param name="result">The extraction result.</param>
extension(FileExtractionResult result)
{
/// <summary>
/// Gets the localized message which explains why a file could not be read.
/// </summary>
/// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this FileExtractionResult result, string fileName)
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)
@@ -36,14 +38,30 @@ internal static class FileExtractionResultExtensions
/// 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.
/// </remarks>
/// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToExtensionMismatchUserMessage(this FileExtractionResult result, string fileName) => string.Format(
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);
/// <summary>
/// Gets the localized message for a file which was read, but lost some of its pages.
/// </summary>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
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));
}
}
/// <param name="code">The stable failure code.</param>
extension(FileExtractionErrorCode code)
{
/// <summary>
/// Gets the localized message which explains why a file could not be read.
/// </summary>
@@ -51,23 +69,9 @@ internal static class FileExtractionResultExtensions
/// 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.
/// </remarks>
/// <param name="code">The stable failure code.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName);
/// <summary>
/// Gets the localized message for a file which was read, but lost some of its pages.
/// </summary>
/// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToPartialUserMessage(this FileExtractionResult result, 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));
internal string ToUserMessage(string fileName) => string.Format(ToUserMessageFormat(code), fileName);
}
private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch
@@ -2,16 +2,19 @@ namespace AIStudio.Tools;
public static class FileInfoExtensions
{
/// <param name="fileInfo">The file info object.</param>
extension(FileInfo fileInfo)
{
/// <summary>
/// Returns the file size in human-readable format.
/// </summary>
/// <param name="fileInfo">The file info object.</param>
/// <returns>The file size in human-readable format.</returns>
public static string FileSize(this FileInfo fileInfo)
public string FileSize()
{
if (!fileInfo.Exists)
return "N/A";
return fileInfo.Length.FileSize();
}
}
}
@@ -11,8 +11,11 @@ public static class HttpRequestHeadersExtensions
API_TOKEN = Program.API_TOKEN;
}
public static void AddApiToken(this HttpRequestHeaders headers)
extension(HttpRequestHeaders headers)
{
public void AddApiToken()
{
headers.Add("token", API_TOKEN);
}
}
}
@@ -4,7 +4,11 @@ public static class IConfidenceExtensions
{
private static readonly ILogger<IConfidence> LOGGER = Program.LOGGER_FACTORY.CreateLogger<IConfidence>();
public static TargetWindow DetermineTargetWindow<T>(this IReadOnlyList<T> items, TargetWindowStrategy strategy, int numMaximumItems = 30) where T : IConfidence
/// <param name="items">The list of confidence items to analyze.</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
extension<T>(IReadOnlyList<T> items) where T : IConfidence
{
public TargetWindow DetermineTargetWindow(TargetWindowStrategy strategy, int numMaximumItems = 30)
{
switch (strategy)
{
@@ -46,12 +50,10 @@ public static class IConfidenceExtensions
/// Determine the optimal confidence threshold for a list of items
/// in order to match a target window of number of items.
/// </summary>
/// <param name="items">The list of confidence items to analyze.</param>
/// <param name="targetWindow">The target window for the number of items.</param>
/// <param name="maxSteps">The maximum number of steps to search for the threshold.</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
/// <returns>The confidence threshold.</returns>
public static float GetConfidenceThreshold<T>(this IReadOnlyList<T> items, TargetWindow targetWindow, int maxSteps = 10) where T : IConfidence
public float GetConfidenceThreshold(TargetWindow targetWindow, int maxSteps = 10)
{
if(!targetWindow.IsValid())
{
@@ -107,4 +109,5 @@ public static class IConfidenceExtensions
return threshold;
}
}
}
@@ -3,19 +3,28 @@ 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)
{
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)
public async Task ClearDiv(string divId)
{
await jsRuntime.InvokeVoidAsync("clearDiv", divId);
}
}
/// <summary>
/// Calls a JavaScript function which returns nothing, and tolerates a circuit which is already gone.
@@ -4,6 +4,9 @@ namespace AIStudio.Tools;
public static class JsonSerializerOptionsExtensions
{
/// <param name="options">The options, with their TypeInfoResolver set.</param>
extension(JsonSerializerOptions options)
{
/// <summary>
/// Makes the options read-only and returns them, so that a static field gets its options in one expression.
/// </summary>
@@ -11,11 +14,11 @@ public static class JsonSerializerOptionsExtensions
/// 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.
/// </remarks>
/// <param name="options">The options, with their TypeInfoResolver set.</param>
/// <returns>The same options, now read-only.</returns>
public static JsonSerializerOptions AsReadOnly(this JsonSerializerOptions options)
public JsonSerializerOptions AsReadOnly()
{
options.MakeReadOnly();
return options;
}
}
}
+16 -8
View File
@@ -8,6 +8,9 @@ public static class LongExtensions
private static readonly string[] SIZE_UNITS = ["B", "kB", "MB", "GB", "TB"];
/// <param name="count">The number to format.</param>
extension(long count)
{
/// <summary>
/// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
/// </summary>
@@ -17,9 +20,8 @@ public static class LongExtensions
/// 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.
/// </remarks>
/// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns>
public static string CompactCount(this long count)
public string CompactCount()
{
var culture = I18N.I.Culture;
if (count is > -1_000 and < 1_000)
@@ -35,31 +37,36 @@ public static class LongExtensions
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}";
}
}
/// <param name="count">The number to format.</param>
extension(int count)
{
/// <summary>
/// Formats a count so that large numbers stay readable.
/// </summary>
/// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns>
public static string CompactCount(this int count) => ((long)count).CompactCount();
public string CompactCount() => ((long)count).CompactCount();
}
/// <param name="sizeBytes">The size in bytes.</param>
extension(long sizeBytes)
{
/// <summary>
/// Formats the file size in a human-readable format, e.g., 1.9 MB.
/// </summary>
/// <remarks>
/// Like CompactCount, the decimal separator follows the culture of the active language plugin.
/// </remarks>
/// <param name="sizeBytes">The size in bytes.</param>
/// <returns>The formatted file size.</returns>
public static string FileSize(this long sizeBytes) => sizeBytes.FileSize(I18N.I.Culture);
public string FileSize() => sizeBytes.FileSize(I18N.I.Culture);
/// <summary>
/// Formats the file size in a human-readable format, with the decimal separator of the given culture.
/// </summary>
/// <param name="sizeBytes">The size in bytes.</param>
/// <param name="culture">The culture whose decimal separator to use.</param>
/// <returns>The formatted file size.</returns>
internal static string FileSize(this long sizeBytes, IFormatProvider culture)
internal string FileSize(IFormatProvider culture)
{
var order = 0;
double size = sizeBytes;
@@ -71,4 +78,5 @@ public static class LongExtensions
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}";
}
}
}
@@ -2,7 +2,9 @@ namespace AIStudio.Tools.MIME;
public static class MIMETypeExtensions
{
public static string[] ToStringArray(this MIMEType[] mimeTypes)
extension(MIMEType[] mimeTypes)
{
public string[] ToStringArray()
{
var result = new string[mimeTypes.Length];
for (var i = 0; i < mimeTypes.Length; i++)
@@ -12,4 +14,5 @@ public static class MIMETypeExtensions
return result;
}
}
}
@@ -6,10 +6,12 @@ public static class MailboxConnectionFailureExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxConnectionFailureExtensions).Namespace, nameof(MailboxConnectionFailureExtensions));
extension(MailboxConnectionFailure failure)
{
/// <summary>
/// What went wrong, and what the user can do about it.
/// </summary>
public static string GetDescription(this MailboxConnectionFailure failure) => failure switch
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."),
@@ -20,4 +22,5 @@ public static class MailboxConnectionFailureExtensions
_ => TB("The connection to the server failed for an unknown reason."),
};
}
}
@@ -6,10 +6,12 @@ public static class MailboxProviderTemplateExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxProviderTemplateExtensions).Namespace, nameof(MailboxProviderTemplateExtensions));
extension(MailboxUsernameFormat format)
{
/// <summary>
/// What the user enters as the username at this provider.
/// </summary>
public static string GetDescription(this MailboxUsernameFormat format) => format switch
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."),
@@ -17,11 +19,14 @@ public static class MailboxProviderTemplateExtensions
_ => TB("The username your provider gave you."),
};
}
extension(MailboxProviderRequirements requirements)
{
/// <summary>
/// What the user has to do before AI Studio can sign in, one text per requirement.
/// </summary>
public static IEnumerable<string> GetDescriptions(this MailboxProviderRequirements requirements)
public IEnumerable<string> 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.");
@@ -35,4 +40,5 @@ public static class MailboxProviderTemplateExtensions
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.");
}
}
}
@@ -3,6 +3,9 @@ namespace AIStudio.Tools.Media;
/// <summary>Capabilities of a media-import owner kind.</summary>
public static class MediaImportOwnerKindExtensions
{
/// <param name="kind">The owner kind to look up.</param>
extension(MediaImportOwnerKind kind)
{
/// <summary>
/// Gets whether the owner stores its own source list and transcripts.
/// </summary>
@@ -13,7 +16,7 @@ public static class MediaImportOwnerKindExtensions
/// 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.
/// </remarks>
/// <param name="kind">The owner kind to look up.</param>
/// <returns><c>true</c> when the owner persists its own sources.</returns>
public static bool PersistsOwnSources(this MediaImportOwnerKind kind) => kind is MediaImportOwnerKind.VISUAL_BRIEFING;
public bool PersistsOwnSources() => kind is MediaImportOwnerKind.VISUAL_BRIEFING;
}
}
@@ -4,13 +4,19 @@ namespace AIStudio.Tools;
public static class MessageBusExtensions
{
public static async Task SendMessage<T>(this ComponentBase component, Event triggeredEvent, T? data = default)
extension(ComponentBase component)
{
public async Task SendMessage<T>(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)
{
public void ApplyFilters(ComponentBase[] components, Event[] events)
{
MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet());
}
}
}
@@ -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)
{
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
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;
}
}
@@ -4,7 +4,9 @@ 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)
{
public string GetDisplayName() => type switch
{
AssistantComponentType.FORM => TB("Root"),
AssistantComponentType.TEXT_AREA => TB("Text Area"),
@@ -34,7 +36,7 @@ public static class AssistantComponentTypeExtensions
_ => TB("Unknown Element")
};
public static string GetIcon(this AssistantComponentType type) => type switch
public string GetIcon() => type switch
{
AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick,
AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale,
@@ -63,4 +65,5 @@ public static class AssistantComponentTypeExtensions
AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree,
_ => MudBlazor.Icons.Material.Filled.AccountTree,
};
}
}
@@ -6,7 +6,9 @@ public static class ILangExtensions
{
private static readonly ILogger<ILang> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ILang>();
public static string GetText(this ILang lang, ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null)
extension(ILang lang)
{
public string GetText(ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null)
{
var type = lang.GetType();
typeName ??= type.Name;
@@ -33,4 +35,5 @@ public static class ILangExtensions
LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'.");
return fallbackEN;
}
}
}
@@ -2,11 +2,14 @@ namespace AIStudio.Tools.PluginSystem;
public static class InternalPluginExtensions
{
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
extension(InternalPlugin plugin)
{
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")
};
}
}
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more