mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 22:47:21 +00:00
Moved all extension methods into extension blocks (#1155)
This commit is contained in:
1 parent
acc7d09fa2
commit
c0a75f39ac
138 files changed
+5884
-4954
No files matched your search
@@ -6,26 +6,29 @@ public static class AssistantCategoryExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantCategoryExtensions).Namespace, nameof(AssistantCategoryExtensions));
|
||||
|
||||
public static string Name(this AssistantCategory category) => category switch
|
||||
extension(AssistantCategory category)
|
||||
{
|
||||
AssistantCategory.AS_IS => TB("Select the assistant category"),
|
||||
AssistantCategory.GENERAL => TB("General"),
|
||||
AssistantCategory.SCIENTIFIC => TB("Scientific"),
|
||||
AssistantCategory.BUSINESS => TB("Business"),
|
||||
AssistantCategory.PRODUCTIVITY => TB("Productivity"),
|
||||
AssistantCategory.DEVELOPMENT => TB("Software Development"),
|
||||
AssistantCategory.LEARNING => TB("Learning"),
|
||||
AssistantCategory.AI_STUDIO => TB("AI Studio Development"),
|
||||
AssistantCategory.OTHER => TB("Other"),
|
||||
public string Name() => category switch
|
||||
{
|
||||
AssistantCategory.AS_IS => TB("Select the assistant category"),
|
||||
AssistantCategory.GENERAL => TB("General"),
|
||||
AssistantCategory.SCIENTIFIC => TB("Scientific"),
|
||||
AssistantCategory.BUSINESS => TB("Business"),
|
||||
AssistantCategory.PRODUCTIVITY => TB("Productivity"),
|
||||
AssistantCategory.DEVELOPMENT => TB("Software Development"),
|
||||
AssistantCategory.LEARNING => TB("Learning"),
|
||||
AssistantCategory.AI_STUDIO => TB("AI Studio Development"),
|
||||
AssistantCategory.OTHER => TB("Other"),
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string NameSelecting(this AssistantCategory category)
|
||||
{
|
||||
if(category is AssistantCategory.AS_IS)
|
||||
return TB("Please select the assistant category");
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return category.Name();
|
||||
public string NameSelecting()
|
||||
{
|
||||
if(category is AssistantCategory.AS_IS)
|
||||
return TB("Please select the assistant category");
|
||||
|
||||
return category.Name();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,86 +10,89 @@ public static class AssistantVisibilityExtensions
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantVisibilityExtensions));
|
||||
|
||||
/// <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)
|
||||
extension(SettingsManager settingsManager)
|
||||
{
|
||||
withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName);
|
||||
/// <summary>
|
||||
/// Checks if an assistant should be visible based on configuration and optional preview feature requirements.
|
||||
/// </summary>
|
||||
/// <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 bool IsAssistantVisible(Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
|
||||
{
|
||||
withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName);
|
||||
|
||||
// Check if a preview feature is required and enabled:
|
||||
if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager))
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature);
|
||||
// Check if a preview feature is required and enabled:
|
||||
if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager))
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature);
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no component is specified, it's always visible:
|
||||
if (component is Components.NONE)
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName);
|
||||
// If no component is specified, it's always visible:
|
||||
if (component is Components.NONE)
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName);
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dynamic assistants are controlled through plugin activation and their security state.
|
||||
// The Assistant Builder is controlled through its preview feature. Neither belongs to the
|
||||
// built-in assistant visibility list, so both are expected to be visible at this point.
|
||||
if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT)
|
||||
return true;
|
||||
// Dynamic assistants are controlled through plugin activation and their security state.
|
||||
// The Assistant Builder is controlled through its preview feature. Neither belongs to the
|
||||
// built-in assistant visibility list, so both are expected to be visible at this point.
|
||||
if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT)
|
||||
return true;
|
||||
|
||||
// Map Components enum to ConfigurableAssistant enum:
|
||||
var configurableAssistant = component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT,
|
||||
Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT,
|
||||
Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT,
|
||||
Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT,
|
||||
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
|
||||
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT,
|
||||
Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT,
|
||||
Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT,
|
||||
Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT,
|
||||
Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT,
|
||||
Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT,
|
||||
Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT,
|
||||
Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT,
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
|
||||
Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT,
|
||||
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
|
||||
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
|
||||
// Map Components enum to ConfigurableAssistant enum:
|
||||
var configurableAssistant = component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT,
|
||||
Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT,
|
||||
Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT,
|
||||
Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT,
|
||||
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
|
||||
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT,
|
||||
Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT,
|
||||
Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT,
|
||||
Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT,
|
||||
Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT,
|
||||
Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT,
|
||||
Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT,
|
||||
Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT,
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
|
||||
Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT,
|
||||
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
|
||||
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
|
||||
|
||||
_ => ConfigurableAssistant.UNKNOWN,
|
||||
};
|
||||
_ => ConfigurableAssistant.UNKNOWN,
|
||||
};
|
||||
|
||||
// If the component doesn't map to a configurable assistant, it's always visible:
|
||||
if (configurableAssistant is ConfigurableAssistant.UNKNOWN)
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component);
|
||||
// If the component doesn't map to a configurable assistant, it's always visible:
|
||||
if (configurableAssistant is ConfigurableAssistant.UNKNOWN)
|
||||
{
|
||||
if(withLogging)
|
||||
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component);
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the assistant is hidden by any configuration plugin:
|
||||
var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant);
|
||||
if (isHidden && withLogging)
|
||||
LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName);
|
||||
|
||||
return !isHidden;
|
||||
}
|
||||
|
||||
// Check if the assistant is hidden by any configuration plugin:
|
||||
var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant);
|
||||
if (isHidden && withLogging)
|
||||
LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName);
|
||||
|
||||
return !isHidden;
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,16 @@ public static class AuthMethodsV1Extensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AuthMethodsV1Extensions).Namespace, nameof(AuthMethodsV1Extensions));
|
||||
|
||||
public static string DisplayName(this AuthMethod authMethod) => authMethod switch
|
||||
extension(AuthMethod authMethod)
|
||||
{
|
||||
AuthMethod.NONE => TB("None"),
|
||||
AuthMethod.USERNAME_PASSWORD => TB("Username & Password"),
|
||||
AuthMethod.KERBEROS => TB("SSO (Kerberos)"),
|
||||
AuthMethod.TOKEN => TB("Access Token"),
|
||||
public string DisplayName() => authMethod switch
|
||||
{
|
||||
AuthMethod.NONE => TB("None"),
|
||||
AuthMethod.USERNAME_PASSWORD => TB("Username & Password"),
|
||||
AuthMethod.KERBEROS => TB("SSO (Kerberos)"),
|
||||
AuthMethod.TOKEN => TB("Access Token"),
|
||||
|
||||
_ => TB("Unknown authentication method"),
|
||||
};
|
||||
_ => TB("Unknown authentication method"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class ChatTemplateExtensions
|
||||
{
|
||||
public static IEnumerable<ChatTemplate> GetAllChatTemplates(this IEnumerable<ChatTemplate> chatTemplates)
|
||||
extension(IEnumerable<ChatTemplate> chatTemplates)
|
||||
{
|
||||
yield return ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
foreach (var chatTemplate in chatTemplates)
|
||||
yield return chatTemplate;
|
||||
public IEnumerable<ChatTemplate> GetAllChatTemplates()
|
||||
{
|
||||
yield return ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
foreach (var chatTemplate in chatTemplates)
|
||||
yield return chatTemplate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,29 @@ namespace AIStudio.Tools.CodeHighlighting;
|
||||
|
||||
public static class CodeTokenKindExtensions
|
||||
{
|
||||
/// <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
|
||||
extension(CodeTokenKind kind)
|
||||
{
|
||||
CodeTokenKind.KEYWORD => "hljs-keyword",
|
||||
CodeTokenKind.TYPE => "hljs-type",
|
||||
CodeTokenKind.LITERAL => "hljs-literal",
|
||||
CodeTokenKind.STRING => "hljs-string",
|
||||
CodeTokenKind.COMMENT => "hljs-comment",
|
||||
CodeTokenKind.NUMBER => "hljs-number",
|
||||
CodeTokenKind.FUNCTION => "hljs-title function_",
|
||||
CodeTokenKind.META => "hljs-meta",
|
||||
CodeTokenKind.VARIABLE => "hljs-variable",
|
||||
CodeTokenKind.ATTRIBUTE => "hljs-attr",
|
||||
CodeTokenKind.SELECTOR => "hljs-selector-class",
|
||||
/// <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>
|
||||
/// <returns>The CSS class of the part.</returns>
|
||||
public string ToCssClass() => kind switch
|
||||
{
|
||||
CodeTokenKind.KEYWORD => "hljs-keyword",
|
||||
CodeTokenKind.TYPE => "hljs-type",
|
||||
CodeTokenKind.LITERAL => "hljs-literal",
|
||||
CodeTokenKind.STRING => "hljs-string",
|
||||
CodeTokenKind.COMMENT => "hljs-comment",
|
||||
CodeTokenKind.NUMBER => "hljs-number",
|
||||
CodeTokenKind.FUNCTION => "hljs-title function_",
|
||||
CodeTokenKind.META => "hljs-meta",
|
||||
CodeTokenKind.VARIABLE => "hljs-variable",
|
||||
CodeTokenKind.ATTRIBUTE => "hljs-attr",
|
||||
CodeTokenKind.SELECTOR => "hljs-selector-class",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,80 +6,83 @@ public static class CommonLanguageExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(CommonLanguageExtensions).Namespace, nameof(CommonLanguageExtensions));
|
||||
|
||||
public static string Name(this CommonLanguages language) => language switch
|
||||
extension(CommonLanguages language)
|
||||
{
|
||||
CommonLanguages.AS_IS => TB("Do not change the language"),
|
||||
public string Name() => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => TB("Do not change the language"),
|
||||
|
||||
CommonLanguages.EN_US => TB("English (US)"),
|
||||
CommonLanguages.EN_GB => TB("English (UK)"),
|
||||
CommonLanguages.ZH_CN => TB("Chinese (Simplified)"),
|
||||
CommonLanguages.HI_IN => TB("Hindi (India)"),
|
||||
CommonLanguages.ES_ES => TB("Spanish (Spain)"),
|
||||
CommonLanguages.FR_FR => TB("French (France)"),
|
||||
CommonLanguages.DE_DE => TB("German (Germany)"),
|
||||
CommonLanguages.DE_AT => TB("German (Austria)"),
|
||||
CommonLanguages.DE_CH => TB("German (Switzerland)"),
|
||||
CommonLanguages.JA_JP => TB("Japanese (Japan)"),
|
||||
CommonLanguages.RU_RU => TB("Russian (Russia)"),
|
||||
CommonLanguages.EN_US => TB("English (US)"),
|
||||
CommonLanguages.EN_GB => TB("English (UK)"),
|
||||
CommonLanguages.ZH_CN => TB("Chinese (Simplified)"),
|
||||
CommonLanguages.HI_IN => TB("Hindi (India)"),
|
||||
CommonLanguages.ES_ES => TB("Spanish (Spain)"),
|
||||
CommonLanguages.FR_FR => TB("French (France)"),
|
||||
CommonLanguages.DE_DE => TB("German (Germany)"),
|
||||
CommonLanguages.DE_AT => TB("German (Austria)"),
|
||||
CommonLanguages.DE_CH => TB("German (Switzerland)"),
|
||||
CommonLanguages.JA_JP => TB("Japanese (Japan)"),
|
||||
CommonLanguages.RU_RU => TB("Russian (Russia)"),
|
||||
|
||||
_ => TB("Other"),
|
||||
};
|
||||
|
||||
public static string ToIETFTag(this CommonLanguages language) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => string.Empty,
|
||||
_ => TB("Other"),
|
||||
};
|
||||
|
||||
public string ToIETFTag() => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => string.Empty,
|
||||
|
||||
CommonLanguages.EN_US => "en-US",
|
||||
CommonLanguages.EN_GB => "en-GB",
|
||||
CommonLanguages.ZH_CN => "zh-CN",
|
||||
CommonLanguages.HI_IN => "hi-IN",
|
||||
CommonLanguages.ES_ES => "es-ES",
|
||||
CommonLanguages.FR_FR => "fr-FR",
|
||||
CommonLanguages.DE_DE => "de-DE",
|
||||
CommonLanguages.DE_AT => "de-AT",
|
||||
CommonLanguages.DE_CH => "de-CH",
|
||||
CommonLanguages.JA_JP => "ja-JP",
|
||||
CommonLanguages.RU_RU => "ru-RU",
|
||||
CommonLanguages.EN_US => "en-US",
|
||||
CommonLanguages.EN_GB => "en-GB",
|
||||
CommonLanguages.ZH_CN => "zh-CN",
|
||||
CommonLanguages.HI_IN => "hi-IN",
|
||||
CommonLanguages.ES_ES => "es-ES",
|
||||
CommonLanguages.FR_FR => "fr-FR",
|
||||
CommonLanguages.DE_DE => "de-DE",
|
||||
CommonLanguages.DE_AT => "de-AT",
|
||||
CommonLanguages.DE_CH => "de-CH",
|
||||
CommonLanguages.JA_JP => "ja-JP",
|
||||
CommonLanguages.RU_RU => "ru-RU",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string PromptSummarizing(this CommonLanguages language, string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => "Use the language the text is written in for the summary.",
|
||||
CommonLanguages.OTHER => $"Output your summary in {customLanguage}.",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
_ => $"Output your summary in {language.Name()} ({language}).",
|
||||
};
|
||||
|
||||
public static string PromptTranslation(this CommonLanguages language, string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.",
|
||||
public string PromptSummarizing(string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => "Use the language the text is written in for the summary.",
|
||||
CommonLanguages.OTHER => $"Output your summary in {customLanguage}.",
|
||||
|
||||
_ => $"Translate the source text to {language.Name()} ({language}).",
|
||||
};
|
||||
|
||||
public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => "Use the language the user input is written in for the output.",
|
||||
CommonLanguages.OTHER => $"use the language {customLanguage} for your output.",
|
||||
_ => $"Output your summary in {language.Name()} ({language}).",
|
||||
};
|
||||
|
||||
_ => $"Use the language {language.Name()} ({language}) for your output.",
|
||||
};
|
||||
public string PromptTranslation(string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.",
|
||||
|
||||
public static string NameSelecting(this CommonLanguages language)
|
||||
{
|
||||
if(language is CommonLanguages.AS_IS)
|
||||
return TB("Please select the target language");
|
||||
_ => $"Translate the source text to {language.Name()} ({language}).",
|
||||
};
|
||||
|
||||
public string PromptGeneralPurpose(string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => "Use the language the user input is written in for the output.",
|
||||
CommonLanguages.OTHER => $"use the language {customLanguage} for your output.",
|
||||
|
||||
_ => $"Use the language {language.Name()} ({language}) for your output.",
|
||||
};
|
||||
|
||||
public string NameSelecting()
|
||||
{
|
||||
if(language is CommonLanguages.AS_IS)
|
||||
return TB("Please select the target language");
|
||||
|
||||
return language.Name();
|
||||
}
|
||||
|
||||
public static string NameSelectingOptional(this CommonLanguages language)
|
||||
{
|
||||
if(language is CommonLanguages.AS_IS)
|
||||
return TB("Do not specify the language");
|
||||
return language.Name();
|
||||
}
|
||||
|
||||
public string NameSelectingOptional()
|
||||
{
|
||||
if(language is CommonLanguages.AS_IS)
|
||||
return TB("Do not specify the language");
|
||||
|
||||
return language.Name();
|
||||
return language.Name();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,236 +10,237 @@ public static class ComponentsExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions));
|
||||
|
||||
/// <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
|
||||
extension(Components component)
|
||||
{
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
|
||||
|
||||
_ => PreviewFeatures.NONE,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a component owns exactly one assistant session slot, so that a running session
|
||||
/// blocks starting another one and inactive sessions can be cleared as a group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Components return <c>false</c> for three different reasons. The chat has no assistant sessions
|
||||
/// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys
|
||||
/// its sessions per briefing, so it owns one slot per stored briefing rather than one per
|
||||
/// component. All must be excluded from the single-slot checks, which is why this is a
|
||||
/// capability and not a component comparison.
|
||||
/// </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
|
||||
{
|
||||
Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <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
|
||||
{
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
|
||||
|
||||
_ => MediaImportOwnerKind.ASSISTANT,
|
||||
};
|
||||
|
||||
public static bool AllowSendTo(this Components component) => component switch
|
||||
{
|
||||
Components.NONE => false,
|
||||
Components.DYNAMIC_ASSISTANT => false,
|
||||
|
||||
Components.ERI_ASSISTANT => false,
|
||||
Components.BIAS_DAY_ASSISTANT => false,
|
||||
Components.I18N_ASSISTANT => false,
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
|
||||
Components.BATCH_PROCESSING_ASSISTANT => false,
|
||||
Components.LOG_VIEWER_ASSISTANT => false,
|
||||
|
||||
Components.APP_SETTINGS => false,
|
||||
Components.WRITER => false,
|
||||
|
||||
Components.AGENT_TEXT_CONTENT_CLEANER => false,
|
||||
Components.AGENT_DATA_SOURCE_SELECTION => false,
|
||||
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false,
|
||||
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false,
|
||||
Components.META_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
};
|
||||
|
||||
public static string Name(this Components component) => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"),
|
||||
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
|
||||
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
|
||||
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"),
|
||||
Components.AGENDA_ASSISTANT => TB("Agenda Assistant"),
|
||||
Components.CODING_ASSISTANT => TB("Coding Assistant"),
|
||||
Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"),
|
||||
Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"),
|
||||
Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"),
|
||||
Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"),
|
||||
Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"),
|
||||
Components.ERI_ASSISTANT => TB("ERI Server"),
|
||||
Components.I18N_ASSISTANT => TB("Localization Assistant"),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
|
||||
Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"),
|
||||
Components.META_ASSISTANT => TB("Assistant Builder"),
|
||||
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
|
||||
|
||||
Components.CHAT => TB("New Chat"),
|
||||
|
||||
_ => Enum.GetName(component)!,
|
||||
};
|
||||
|
||||
public static ComponentsData GetData(this Components destination) => destination switch
|
||||
{
|
||||
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
|
||||
Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
|
||||
Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER),
|
||||
Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
|
||||
Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
|
||||
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS),
|
||||
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
|
||||
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
|
||||
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
|
||||
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING),
|
||||
|
||||
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
|
||||
|
||||
_ => new(Event.NONE, Routes.ASSISTANTS),
|
||||
};
|
||||
|
||||
public static ConfidenceLevel MinimumConfidence(this Components component, SettingsManager settingsManager) => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
|
||||
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
|
||||
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
|
||||
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
|
||||
|
||||
// The minimum confidence for the Document Analysis Assistant is set per policy.
|
||||
// We do this inside the Document Analysis Assistant component:
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE,
|
||||
|
||||
// A policy-specific minimum is merged with this component default inside
|
||||
// the Batch Processing Assistant; the stricter level wins.
|
||||
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default,
|
||||
|
||||
_ => default,
|
||||
};
|
||||
|
||||
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected provider at the component level.
|
||||
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
|
||||
|
||||
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
|
||||
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
|
||||
|
||||
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
|
||||
|
||||
_ => Settings.Provider.NONE,
|
||||
};
|
||||
|
||||
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager)
|
||||
{
|
||||
var storedValue = component switch
|
||||
/// <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>
|
||||
/// <returns>The required preview feature.</returns>
|
||||
public PreviewFeatures RequiredPreviewFeature() => component switch
|
||||
{
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile,
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected profile at the component level.
|
||||
// The profile is selected per policy instead. We do this inside the Document Analysis Assistant component:
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id,
|
||||
|
||||
_ => string.Empty,
|
||||
_ => PreviewFeatures.NONE,
|
||||
};
|
||||
|
||||
return ProfilePreselection.FromStoredValue(storedValue);
|
||||
}
|
||||
|
||||
public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch
|
||||
{
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE,
|
||||
/// <summary>
|
||||
/// Gets whether a component owns exactly one assistant session slot, so that a running session
|
||||
/// blocks starting another one and inactive sessions can be cleared as a group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Components return <c>false</c> for three different reasons. The chat has no assistant sessions
|
||||
/// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys
|
||||
/// its sessions per briefing, so it owns one slot per stored briefing rather than one per
|
||||
/// component. All must be excluded from the single-slot checks, which is why this is a
|
||||
/// capability and not a component comparison.
|
||||
/// </remarks>
|
||||
/// <returns><c>true</c> when the component owns exactly one session slot.</returns>
|
||||
public bool HasSingleSessionSlot() => component switch
|
||||
{
|
||||
Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of media-import owner a component creates for its attachments.
|
||||
/// </summary>
|
||||
/// <returns>The media-import owner kind.</returns>
|
||||
public MediaImportOwnerKind MediaOwnerKind() => component switch
|
||||
{
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
|
||||
|
||||
_ => MediaImportOwnerKind.ASSISTANT,
|
||||
};
|
||||
|
||||
public bool AllowSendTo() => component switch
|
||||
{
|
||||
Components.NONE => false,
|
||||
Components.DYNAMIC_ASSISTANT => false,
|
||||
|
||||
Components.ERI_ASSISTANT => false,
|
||||
Components.BIAS_DAY_ASSISTANT => false,
|
||||
Components.I18N_ASSISTANT => false,
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
|
||||
Components.BATCH_PROCESSING_ASSISTANT => false,
|
||||
Components.LOG_VIEWER_ASSISTANT => false,
|
||||
|
||||
_ => ChatTemplate.NO_CHAT_TEMPLATE,
|
||||
};
|
||||
Components.APP_SETTINGS => false,
|
||||
Components.WRITER => false,
|
||||
|
||||
Components.AGENT_TEXT_CONTENT_CLEANER => false,
|
||||
Components.AGENT_DATA_SOURCE_SELECTION => false,
|
||||
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false,
|
||||
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false,
|
||||
Components.META_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
};
|
||||
|
||||
public string Name() => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"),
|
||||
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
|
||||
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
|
||||
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"),
|
||||
Components.AGENDA_ASSISTANT => TB("Agenda Assistant"),
|
||||
Components.CODING_ASSISTANT => TB("Coding Assistant"),
|
||||
Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"),
|
||||
Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"),
|
||||
Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"),
|
||||
Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"),
|
||||
Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"),
|
||||
Components.ERI_ASSISTANT => TB("ERI Server"),
|
||||
Components.I18N_ASSISTANT => TB("Localization Assistant"),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
|
||||
Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"),
|
||||
Components.META_ASSISTANT => TB("Assistant Builder"),
|
||||
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
|
||||
|
||||
Components.CHAT => TB("New Chat"),
|
||||
|
||||
_ => Enum.GetName(component)!,
|
||||
};
|
||||
|
||||
public ComponentsData GetData() => component switch
|
||||
{
|
||||
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
|
||||
Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
|
||||
Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER),
|
||||
Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
|
||||
Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
|
||||
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS),
|
||||
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
|
||||
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
|
||||
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
|
||||
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING),
|
||||
|
||||
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
|
||||
|
||||
_ => new(Event.NONE, Routes.ASSISTANTS),
|
||||
};
|
||||
|
||||
public ConfidenceLevel MinimumConfidence(SettingsManager settingsManager) => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
|
||||
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
|
||||
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
|
||||
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
|
||||
|
||||
// The minimum confidence for the Document Analysis Assistant is set per policy.
|
||||
// We do this inside the Document Analysis Assistant component:
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE,
|
||||
|
||||
// A policy-specific minimum is merged with this component default inside
|
||||
// the Batch Processing Assistant; the stricter level wins.
|
||||
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default,
|
||||
|
||||
_ => default,
|
||||
};
|
||||
|
||||
public AIStudio.Settings.Provider PreselectedProvider(SettingsManager settingsManager) => component switch
|
||||
{
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected provider at the component level.
|
||||
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
|
||||
|
||||
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
|
||||
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
|
||||
|
||||
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
|
||||
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
|
||||
|
||||
_ => Settings.Provider.NONE,
|
||||
};
|
||||
|
||||
public ProfilePreselection GetProfilePreselection(SettingsManager settingsManager)
|
||||
{
|
||||
var storedValue = component switch
|
||||
{
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty,
|
||||
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty,
|
||||
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty,
|
||||
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty,
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile,
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty,
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected profile at the component level.
|
||||
// The profile is selected per policy instead. We do this inside the Document Analysis Assistant component:
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id,
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return ProfilePreselection.FromStoredValue(storedValue);
|
||||
}
|
||||
|
||||
public ChatTemplate PreselectedChatTemplate(SettingsManager settingsManager) => component switch
|
||||
{
|
||||
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
|
||||
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE,
|
||||
|
||||
_ => ChatTemplate.NO_CHAT_TEMPLATE,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,20 @@ public static class ConfidenceSchemesExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceSchemesExtensions).Namespace, nameof(ConfidenceSchemesExtensions));
|
||||
|
||||
public static string GetListDescription(this ConfidenceSchemes scheme) => scheme switch
|
||||
extension(ConfidenceSchemes scheme)
|
||||
{
|
||||
ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"),
|
||||
ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"),
|
||||
ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"),
|
||||
ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"),
|
||||
ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"),
|
||||
ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"),
|
||||
public string GetListDescription() => scheme switch
|
||||
{
|
||||
ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"),
|
||||
ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"),
|
||||
ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"),
|
||||
ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"),
|
||||
ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"),
|
||||
ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"),
|
||||
|
||||
ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"),
|
||||
ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"),
|
||||
|
||||
_ => TB("Unknown confidence scheme"),
|
||||
};
|
||||
_ => TB("Unknown confidence scheme"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,66 +2,69 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class DirectoryInfoExtensions
|
||||
{
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
|
||||
/// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
|
||||
/// to cancel slow operations, e.g., when the directory is on a slow network drive.
|
||||
///
|
||||
/// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
|
||||
///
|
||||
/// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
|
||||
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
|
||||
/// method to update the UI from a background thread.
|
||||
/// </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>
|
||||
/// <param name="reportMaxFiles">The maximum number of files to report. A negative value reports all files.</param>
|
||||
/// <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)
|
||||
extension(DirectoryInfo directoryInfo)
|
||||
{
|
||||
var rootDirectoryLen = directoryInfo.FullName.Length;
|
||||
var filesToCount = selection ?? FolderFileSelection.EVERY_FILE;
|
||||
long totalSize = 0;
|
||||
long numFiles = 0;
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
|
||||
/// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
|
||||
/// to cancel slow operations, e.g., when the directory is on a slow network drive.
|
||||
///
|
||||
/// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
|
||||
///
|
||||
/// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
|
||||
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
|
||||
/// method to update the UI from a background thread.
|
||||
/// </remarks>
|
||||
/// <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>
|
||||
/// <param name="reportMaxFiles">The maximum number of files to report. A negative value reports all files.</param>
|
||||
/// <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 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;
|
||||
long totalSize = 0;
|
||||
long numFiles = 0;
|
||||
|
||||
await Task.Factory.StartNew(() => {
|
||||
foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (!filesToCount.Selects(filePath))
|
||||
continue;
|
||||
|
||||
var file = new FileInfo(filePath);
|
||||
if (filesToCount.IsTooLarge(file))
|
||||
continue;
|
||||
|
||||
totalSize += file.Length;
|
||||
numFiles++;
|
||||
|
||||
if (numFiles % 100 == 0)
|
||||
await Task.Factory.StartNew(() => {
|
||||
foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName))
|
||||
{
|
||||
reportCurrentTotalSize(totalSize);
|
||||
reportCurrentNumFiles(numFiles);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (!filesToCount.Selects(filePath))
|
||||
continue;
|
||||
|
||||
var file = new FileInfo(filePath);
|
||||
if (filesToCount.IsTooLarge(file))
|
||||
continue;
|
||||
|
||||
totalSize += file.Length;
|
||||
numFiles++;
|
||||
|
||||
if (numFiles % 100 == 0)
|
||||
{
|
||||
reportCurrentTotalSize(totalSize);
|
||||
reportCurrentNumFiles(numFiles);
|
||||
}
|
||||
|
||||
if (reportMaxFiles < 0 || numFiles <= reportMaxFiles)
|
||||
reportNextFile(file.FullName[rootDirectoryLen..]);
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
||||
if (reportMaxFiles < 0 || numFiles <= reportMaxFiles)
|
||||
reportNextFile(file.FullName[rootDirectoryLen..]);
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
reportCurrentTotalSize(totalSize);
|
||||
reportCurrentNumFiles(numFiles);
|
||||
|
||||
reportCurrentTotalSize(totalSize);
|
||||
reportCurrentNumFiles(numFiles);
|
||||
|
||||
done?.Invoke();
|
||||
done?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,15 @@ public static class ProviderTypeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderTypeExtensions).Namespace, nameof(ProviderTypeExtensions));
|
||||
|
||||
public static string Explain(this ProviderType providerType) => providerType switch
|
||||
extension(ProviderType providerType)
|
||||
{
|
||||
ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."),
|
||||
ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."),
|
||||
ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."),
|
||||
public string Explain() => providerType switch
|
||||
{
|
||||
ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."),
|
||||
ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."),
|
||||
ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."),
|
||||
|
||||
_ => TB("Unknown configuration. This data source cannot be used at the moment."),
|
||||
};
|
||||
_ => TB("Unknown configuration. This data source cannot be used at the moment."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class ElementReferenceExtensions
|
||||
{
|
||||
public static async ValueTask ScrollIntoViewAsync(this ElementReference elementReference, IJSRuntime jsRuntime)
|
||||
extension(ElementReference elementReference)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference);
|
||||
public async ValueTask ScrollIntoViewAsync(IJSRuntime jsRuntime)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,89 +8,95 @@ 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>
|
||||
/// <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)
|
||||
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression)
|
||||
{
|
||||
switch (expression.Body)
|
||||
/// <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 MemberExpression GetMemberExpression()
|
||||
{
|
||||
// Case for value types, which are wrapped in UnaryExpression:
|
||||
case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression:
|
||||
return (MemberExpression)unaryExpression.Operand;
|
||||
switch (expression.Body)
|
||||
{
|
||||
// Case for value types, which are wrapped in UnaryExpression:
|
||||
case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression:
|
||||
return (MemberExpression)unaryExpression.Operand;
|
||||
|
||||
// Case for reference types, which are directly MemberExpressions:
|
||||
case MemberExpression memberExpression:
|
||||
return memberExpression;
|
||||
// Case for reference types, which are directly MemberExpressions:
|
||||
case MemberExpression memberExpression:
|
||||
return memberExpression;
|
||||
|
||||
default:
|
||||
LOGGER.LogError($"Expression '{expression}' is not a valid property expression.");
|
||||
throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression));
|
||||
default:
|
||||
LOGGER.LogError($"Expression '{expression}' is not a valid property expression.");
|
||||
throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression) where TOut : IBinaryInteger<TOut>
|
||||
{
|
||||
// Ensure that the expression body is a member expression:
|
||||
if (expression.Body is not MemberExpression memberExpression)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Ensure that the member expression is a property:
|
||||
if (memberExpression.Member is not PropertyInfo propertyInfo)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Ensure that the member expression has a target object:
|
||||
if (memberExpression.Expression is null)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Get the target object for the expression, which is the object containing the property to increment:
|
||||
var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters);
|
||||
|
||||
// Compile the lambda expression to get the target object
|
||||
// (which is the object containing the property to increment):
|
||||
var targetObject = targetObjectExpression.Compile().DynamicInvoke(data);
|
||||
|
||||
// Was the compilation successful?
|
||||
if (targetObject is null)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Read the current value of the property:
|
||||
if (propertyInfo.GetValue(targetObject) is not TOut value)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Increment the value:
|
||||
switch (type)
|
||||
/// <summary>
|
||||
/// Attempts to increment the value of an uint property for a specified object using a
|
||||
/// provided expression.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>An IncrementResult object containing the result of the increment operation.</returns>
|
||||
public IncrementResult<TOut> TryIncrement(TIn data, IncrementType type)
|
||||
{
|
||||
case IncrementType.PRE:
|
||||
var nextValue = value + TOut.CreateChecked(1);
|
||||
propertyInfo.SetValue(targetObject, nextValue);
|
||||
return new(true, nextValue);
|
||||
|
||||
case IncrementType.POST:
|
||||
var currentValue = value;
|
||||
var incrementedValue = value + TOut.CreateChecked(1);
|
||||
propertyInfo.SetValue(targetObject, incrementedValue);
|
||||
return new(true, currentValue);
|
||||
|
||||
default:
|
||||
// Ensure that the expression body is a member expression:
|
||||
if (expression.Body is not MemberExpression memberExpression)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Ensure that the member expression is a property:
|
||||
if (memberExpression.Member is not PropertyInfo propertyInfo)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Ensure that the member expression has a target object:
|
||||
if (memberExpression.Expression is null)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Get the target object for the expression, which is the object containing the property to increment:
|
||||
var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters);
|
||||
|
||||
// Compile the lambda expression to get the target object
|
||||
// (which is the object containing the property to increment):
|
||||
var targetObject = targetObjectExpression.Compile().DynamicInvoke(data);
|
||||
|
||||
// Was the compilation successful?
|
||||
if (targetObject is null)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Read the current value of the property:
|
||||
if (propertyInfo.GetValue(targetObject) is not TOut value)
|
||||
return new(false, TOut.Zero);
|
||||
|
||||
// Increment the value:
|
||||
switch (type)
|
||||
{
|
||||
case IncrementType.PRE:
|
||||
var nextValue = value + TOut.CreateChecked(1);
|
||||
propertyInfo.SetValue(targetObject, nextValue);
|
||||
return new(true, nextValue);
|
||||
|
||||
case IncrementType.POST:
|
||||
var currentValue = value;
|
||||
var incrementedValue = value + TOut.CreateChecked(1);
|
||||
propertyInfo.SetValue(targetObject, incrementedValue);
|
||||
return new(true, currentValue);
|
||||
|
||||
default:
|
||||
return new(false, TOut.Zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,61 +52,215 @@ public static class FileExportFormatExtensions
|
||||
/// </remarks>
|
||||
public static readonly IReadOnlyList<FileExportFormat> ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS];
|
||||
|
||||
/// <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
|
||||
extension(FileExportFormat format)
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
|
||||
FileExportFormat.LATEX => TB("LaTeX (.tex)"),
|
||||
FileExportFormat.MARKDOWN => TB("Markdown (.md)"),
|
||||
FileExportFormat.HTML => TB("Webpage (.html)"),
|
||||
FileExportFormat.CSV => TB("Table (.csv)"),
|
||||
FileExportFormat.TSV => TB("Table (.tsv)"),
|
||||
FileExportFormat.SOURCE_CODE => TB("Source code"),
|
||||
/// <summary>
|
||||
/// Returns the name of the format as shown to the user.
|
||||
/// </summary>
|
||||
/// <returns>The name of the format.</returns>
|
||||
public string ToName() => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
|
||||
FileExportFormat.LATEX => TB("LaTeX (.tex)"),
|
||||
FileExportFormat.MARKDOWN => TB("Markdown (.md)"),
|
||||
FileExportFormat.HTML => TB("Webpage (.html)"),
|
||||
FileExportFormat.CSV => TB("Table (.csv)"),
|
||||
FileExportFormat.TSV => TB("Table (.tsv)"),
|
||||
FileExportFormat.SOURCE_CODE => TB("Source code"),
|
||||
|
||||
_ => TB("Unknown format"),
|
||||
};
|
||||
_ => TB("Unknown format"),
|
||||
};
|
||||
|
||||
/// <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
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord,
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument,
|
||||
FileExportFormat.LATEX => Icons.Material.Filled.Functions,
|
||||
FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields,
|
||||
FileExportFormat.HTML => Icons.Material.Filled.Html,
|
||||
FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart,
|
||||
FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code,
|
||||
/// <summary>
|
||||
/// Returns the icon of the format.
|
||||
/// </summary>
|
||||
/// <returns>The icon of the format.</returns>
|
||||
public string ToIcon() => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord,
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument,
|
||||
FileExportFormat.LATEX => Icons.Material.Filled.Functions,
|
||||
FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields,
|
||||
FileExportFormat.HTML => Icons.Material.Filled.Html,
|
||||
FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart,
|
||||
FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code,
|
||||
|
||||
_ => Icons.Material.Filled.Help,
|
||||
};
|
||||
_ => Icons.Material.Filled.Help,
|
||||
};
|
||||
|
||||
/// <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
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => ".docx",
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
|
||||
FileExportFormat.LATEX => ".tex",
|
||||
FileExportFormat.MARKDOWN => ".md",
|
||||
FileExportFormat.HTML => ".html",
|
||||
FileExportFormat.CSV => ".csv",
|
||||
FileExportFormat.TSV => ".tsv",
|
||||
/// <summary>
|
||||
/// Returns the file extension of the format, including the leading dot.
|
||||
/// </summary>
|
||||
/// <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 string ToFileExtension() => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => ".docx",
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
|
||||
FileExportFormat.LATEX => ".tex",
|
||||
FileExportFormat.MARKDOWN => ".md",
|
||||
FileExportFormat.HTML => ".html",
|
||||
FileExportFormat.CSV => ".csv",
|
||||
FileExportFormat.TSV => ".tsv",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <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
|
||||
@@ -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,98 +15,99 @@ internal static class FileExtractionErrorCodeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionErrorCodeExtensions).Namespace, nameof(FileExtractionErrorCodeExtensions));
|
||||
|
||||
/// <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
|
||||
extension(FileExtractionErrorCode code)
|
||||
{
|
||||
//
|
||||
// The reason lies in the file. Reading it again without changing it produces the same
|
||||
// outcome, so the indexer waits for a new fingerprint:
|
||||
//
|
||||
FileExtractionErrorCode.NO_TEXT_EXTRACTED => true,
|
||||
FileExtractionErrorCode.NO_CONTENT => true,
|
||||
FileExtractionErrorCode.NOT_TEXT_CONTENT => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_PDF => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true,
|
||||
FileExtractionErrorCode.PDF_ENCRYPTED => true,
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true,
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => true,
|
||||
FileExtractionErrorCode.UNSUPPORTED => true,
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true,
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether reading the file again will fail again, as long as the file
|
||||
/// itself does not change.
|
||||
/// </summary>
|
||||
/// <returns>True, when the reason lies in the file itself.</returns>
|
||||
internal bool IsPermanentIndexingFailure() => code switch
|
||||
{
|
||||
//
|
||||
// The reason lies in the file. Reading it again without changing it produces the same
|
||||
// outcome, so the indexer waits for a new fingerprint:
|
||||
//
|
||||
FileExtractionErrorCode.NO_TEXT_EXTRACTED => true,
|
||||
FileExtractionErrorCode.NO_CONTENT => true,
|
||||
FileExtractionErrorCode.NOT_TEXT_CONTENT => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_PDF => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true,
|
||||
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true,
|
||||
FileExtractionErrorCode.PDF_ENCRYPTED => true,
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true,
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => true,
|
||||
FileExtractionErrorCode.UNSUPPORTED => true,
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true,
|
||||
|
||||
// Pages holding nothing but images are one of the recurring cases here, and that is a
|
||||
// property of the document, not of the environment:
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true,
|
||||
// Pages holding nothing but images are one of the recurring cases here, and that is a
|
||||
// property of the document, not of the environment:
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true,
|
||||
|
||||
//
|
||||
// Everything else depends on the surroundings: an unavailable drive, a file someone else
|
||||
// has open, a missing engine, or a runtime which did not answer in time. All of them are
|
||||
// worth another attempt during the next run:
|
||||
//
|
||||
_ => false,
|
||||
};
|
||||
//
|
||||
// Everything else depends on the surroundings: an unavailable drive, a file someone else
|
||||
// has open, a missing engine, or a runtime which did not answer in time. All of them are
|
||||
// worth another attempt during the next run:
|
||||
//
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Names the cause in a few words.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
{
|
||||
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"),
|
||||
FileExtractionErrorCode.NO_CONTENT => TB("No content"),
|
||||
FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"),
|
||||
FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"),
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"),
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"),
|
||||
FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"),
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"),
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"),
|
||||
/// <summary>
|
||||
/// Names the cause in a few words.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
/// <returns>The localized name of the cause, or an empty text when the code has none.</returns>
|
||||
internal string GetIndexingCauseName() => code switch
|
||||
{
|
||||
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"),
|
||||
FileExtractionErrorCode.NO_CONTENT => TB("No content"),
|
||||
FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"),
|
||||
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"),
|
||||
FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"),
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"),
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"),
|
||||
FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"),
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"),
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"),
|
||||
|
||||
FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"),
|
||||
FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"),
|
||||
FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"),
|
||||
FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"),
|
||||
FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"),
|
||||
FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"),
|
||||
FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"),
|
||||
FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"),
|
||||
FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"),
|
||||
FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"),
|
||||
FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"),
|
||||
FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"),
|
||||
|
||||
// Nothing about these lies in the file: AI Studio asked its runtime for the content and
|
||||
// got back something it cannot work with. One name for all of them, because that is the
|
||||
// one thing the user can tell from them:
|
||||
FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"),
|
||||
FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"),
|
||||
FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"),
|
||||
FileExtractionErrorCode.INTERNAL => TB("Internal error"),
|
||||
// Nothing about these lies in the file: AI Studio asked its runtime for the content and
|
||||
// got back something it cannot work with. One name for all of them, because that is the
|
||||
// one thing the user can tell from them:
|
||||
FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"),
|
||||
FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"),
|
||||
FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"),
|
||||
FileExtractionErrorCode.INTERNAL => TB("Internal error"),
|
||||
|
||||
// Codes which say nothing beyond the message of the single file. The caller names those
|
||||
// files itself and shows their messages instead:
|
||||
_ => string.Empty,
|
||||
};
|
||||
// Codes which say nothing beyond the message of the single file. The caller names those
|
||||
// files itself and shows their messages instead:
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file was not indexed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These texts are the counterpart of the ones used for chat attachments: there, a file which
|
||||
/// cannot be read is simply not sent, while here it stays out of the index and the user needs
|
||||
/// to know whether AI Studio will come back to it on its own.
|
||||
/// </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);
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file was not indexed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These texts are the counterpart of the ones used for chat attachments: there, a file which
|
||||
/// cannot be read is simply not sent, while here it stays out of the index and the user needs
|
||||
/// to know whether AI Studio will come back to it on its own.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
internal string ToIndexingUserMessage(string fileName) => string.Format(ToIndexingMessageFormat(code), fileName);
|
||||
}
|
||||
|
||||
private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch
|
||||
{
|
||||
|
||||
@@ -14,60 +14,64 @@ internal static class FileExtractionResultExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions));
|
||||
|
||||
/// <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)
|
||||
extension(FileExtractionResult result)
|
||||
{
|
||||
// When we know what the file really is, naming it beats a generic "not supported":
|
||||
if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null)
|
||||
return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat);
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file could not be read.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
internal string ToUserMessage(string fileName)
|
||||
{
|
||||
// When we know what the file really is, naming it beats a generic "not supported":
|
||||
if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null)
|
||||
return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat);
|
||||
|
||||
return result.ErrorCode.ToUserMessage(fileName);
|
||||
return result.ErrorCode.ToUserMessage(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message for a file whose content does not match its file extension.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message for a file whose content does not match its file extension.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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(
|
||||
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 which explains why a file could not be read.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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)
|
||||
extension(FileExtractionErrorCode code)
|
||||
{
|
||||
if (result.FailedPages.Count == 0)
|
||||
return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName);
|
||||
|
||||
return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages));
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file could not be read.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
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
|
||||
{
|
||||
/// <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)
|
||||
extension(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
return "N/A";
|
||||
/// <summary>
|
||||
/// Returns the file size in human-readable format.
|
||||
/// </summary>
|
||||
/// <returns>The file size in human-readable format.</returns>
|
||||
public string FileSize()
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
return "N/A";
|
||||
|
||||
return fileInfo.Length.FileSize();
|
||||
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)
|
||||
{
|
||||
headers.Add("token", API_TOKEN);
|
||||
public void AddApiToken()
|
||||
{
|
||||
headers.Add("token", API_TOKEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,107 +4,110 @@ 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
|
||||
{
|
||||
switch (strategy)
|
||||
{
|
||||
case TargetWindowStrategy.A_FEW_GOOD_ONES:
|
||||
return new(1, 2, 3, 0f);
|
||||
|
||||
case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING:
|
||||
var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f);
|
||||
if(numItemsBetterThanGuessing < 3)
|
||||
return new(1, 2, 3, 0.5f);
|
||||
|
||||
// We want the top 10% of items better than guessing:
|
||||
var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f);
|
||||
|
||||
// When these 10% are just a few items, we take them all:
|
||||
if (numTop10Percent <= 10)
|
||||
{
|
||||
var diff = numItemsBetterThanGuessing - numTop10Percent;
|
||||
var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f);
|
||||
return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f);
|
||||
}
|
||||
|
||||
// Let's define the size of the window:
|
||||
const int MIN_NUM_ITEMS = 3;
|
||||
var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent);
|
||||
windowMin = Math.Min(windowMin, numMaximumItems - 1);
|
||||
var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3);
|
||||
var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent));
|
||||
var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize);
|
||||
return new(totalMin, windowMin, windowMax, 0.5f);
|
||||
|
||||
case TargetWindowStrategy.NONE:
|
||||
default:
|
||||
return new(-1, -1, -1, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
extension<T>(IReadOnlyList<T> items) where T : IConfidence
|
||||
{
|
||||
if(!targetWindow.IsValid())
|
||||
public TargetWindow DetermineTargetWindow(TargetWindowStrategy strategy, int numMaximumItems = 30)
|
||||
{
|
||||
LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
//
|
||||
// Without items there is no threshold to find, and the Min and Max calls below would throw
|
||||
// on an empty sequence. Every caller checks this today, which is precisely how such a guard
|
||||
// goes missing once a new caller arrives. It belongs here, next to the calls it protects:
|
||||
//
|
||||
if(items.Count == 0)
|
||||
{
|
||||
LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var confidenceValues = items.Select(x => x.Confidence).ToList();
|
||||
var minConfidence = confidenceValues.Min();
|
||||
var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold);
|
||||
var upperBound = confidenceValues.Max();
|
||||
|
||||
//
|
||||
// We search for a threshold so that we have between
|
||||
// targetWindowMin and targetWindowMax items. When not
|
||||
// possible, we take all items (e.g., threshold = 0f; depends on the used window strategy)
|
||||
//
|
||||
var threshold = 0.0f;
|
||||
|
||||
// Check the case where the confidence values are too close:
|
||||
if (upperBound - minConfidence >= 0.01)
|
||||
{
|
||||
var previousThreshold = threshold;
|
||||
for (var i = 0; i < maxSteps; i++)
|
||||
switch (strategy)
|
||||
{
|
||||
threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps;
|
||||
var numMatches = items.Count(x => x.Confidence >= threshold);
|
||||
if (numMatches <= targetWindow.NumMinItems)
|
||||
{
|
||||
threshold = previousThreshold;
|
||||
break;
|
||||
}
|
||||
case TargetWindowStrategy.A_FEW_GOOD_ONES:
|
||||
return new(1, 2, 3, 0f);
|
||||
|
||||
case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING:
|
||||
var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f);
|
||||
if(numItemsBetterThanGuessing < 3)
|
||||
return new(1, 2, 3, 0.5f);
|
||||
|
||||
if (targetWindow.InsideWindow(numMatches))
|
||||
break;
|
||||
// We want the top 10% of items better than guessing:
|
||||
var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f);
|
||||
|
||||
previousThreshold = threshold;
|
||||
// When these 10% are just a few items, we take them all:
|
||||
if (numTop10Percent <= 10)
|
||||
{
|
||||
var diff = numItemsBetterThanGuessing - numTop10Percent;
|
||||
var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f);
|
||||
return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f);
|
||||
}
|
||||
|
||||
// Let's define the size of the window:
|
||||
const int MIN_NUM_ITEMS = 3;
|
||||
var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent);
|
||||
windowMin = Math.Min(windowMin, numMaximumItems - 1);
|
||||
var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3);
|
||||
var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent));
|
||||
var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize);
|
||||
return new(totalMin, windowMin, windowMax, 0.5f);
|
||||
|
||||
case TargetWindowStrategy.NONE:
|
||||
default:
|
||||
return new(-1, -1, -1, 0f);
|
||||
}
|
||||
}
|
||||
else
|
||||
LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold.");
|
||||
|
||||
/// <summary>
|
||||
/// Determine the optimal confidence threshold for a list of items
|
||||
/// in order to match a target window of number of items.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>The confidence threshold.</returns>
|
||||
public float GetConfidenceThreshold(TargetWindow targetWindow, int maxSteps = 10)
|
||||
{
|
||||
if(!targetWindow.IsValid())
|
||||
{
|
||||
LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
//
|
||||
// Without items there is no threshold to find, and the Min and Max calls below would throw
|
||||
// on an empty sequence. Every caller checks this today, which is precisely how such a guard
|
||||
// goes missing once a new caller arrives. It belongs here, next to the calls it protects:
|
||||
//
|
||||
if(items.Count == 0)
|
||||
{
|
||||
LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var confidenceValues = items.Select(x => x.Confidence).ToList();
|
||||
var minConfidence = confidenceValues.Min();
|
||||
var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold);
|
||||
var upperBound = confidenceValues.Max();
|
||||
|
||||
//
|
||||
// We search for a threshold so that we have between
|
||||
// targetWindowMin and targetWindowMax items. When not
|
||||
// possible, we take all items (e.g., threshold = 0f; depends on the used window strategy)
|
||||
//
|
||||
var threshold = 0.0f;
|
||||
|
||||
// Check the case where the confidence values are too close:
|
||||
if (upperBound - minConfidence >= 0.01)
|
||||
{
|
||||
var previousThreshold = threshold;
|
||||
for (var i = 0; i < maxSteps; i++)
|
||||
{
|
||||
threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps;
|
||||
var numMatches = items.Count(x => x.Confidence >= threshold);
|
||||
if (numMatches <= targetWindow.NumMinItems)
|
||||
{
|
||||
threshold = previousThreshold;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetWindow.InsideWindow(numMatches))
|
||||
break;
|
||||
|
||||
previousThreshold = threshold;
|
||||
}
|
||||
}
|
||||
else
|
||||
LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold.");
|
||||
|
||||
return threshold;
|
||||
return threshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,27 @@ using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
//
|
||||
// The methods with a params parameter stay classic extension methods instead of moving into the
|
||||
// extension block: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns
|
||||
// about every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Move
|
||||
// them once the SDK in metadata.txt carries the fix; MWAIS0016 makes the same exception until then.
|
||||
//
|
||||
public static class JsRuntimeExtensions
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions));
|
||||
|
||||
public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2)
|
||||
extension(IJSRuntime jsRuntime)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID);
|
||||
}
|
||||
public async Task GenerateAndShowDiff(string text1, string text2)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID);
|
||||
}
|
||||
|
||||
public static async Task ClearDiv(this IJSRuntime jsRuntime, string divId)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("clearDiv", divId);
|
||||
public async Task ClearDiv(string divId)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("clearDiv", divId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,18 +4,21 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class JsonSerializerOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the options read-only and returns them, so that a static field gets its options in one expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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)
|
||||
extension(JsonSerializerOptions options)
|
||||
{
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
/// <summary>
|
||||
/// Makes the options read-only and returns them, so that a static field gets its options in one expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
/// <returns>The same options, now read-only.</returns>
|
||||
public JsonSerializerOptions AsReadOnly()
|
||||
{
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,67 +8,75 @@ public static class LongExtensions
|
||||
|
||||
private static readonly string[] SIZE_UNITS = ["B", "kB", "MB", "GB", "TB"];
|
||||
|
||||
/// <summary>
|
||||
/// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage
|
||||
/// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the
|
||||
/// difference between 4 and 999. The decimal separator follows the culture of the active language
|
||||
/// plugin rather than the one of the thread, which never moves along with the app's language.
|
||||
/// </remarks>
|
||||
/// <param name="count">The number to format.</param>
|
||||
/// <returns>The formatted number.</returns>
|
||||
public static string CompactCount(this long count)
|
||||
extension(long count)
|
||||
{
|
||||
var culture = I18N.I.Culture;
|
||||
if (count is > -1_000 and < 1_000)
|
||||
return count.ToString("N0", culture);
|
||||
|
||||
var order = -1;
|
||||
double value = count;
|
||||
while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1)
|
||||
/// <summary>
|
||||
/// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage
|
||||
/// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the
|
||||
/// difference between 4 and 999. The decimal separator follows the culture of the active language
|
||||
/// plugin rather than the one of the thread, which never moves along with the app's language.
|
||||
/// </remarks>
|
||||
/// <returns>The formatted number.</returns>
|
||||
public string CompactCount()
|
||||
{
|
||||
order++;
|
||||
value /= 1_000;
|
||||
}
|
||||
var culture = I18N.I.Culture;
|
||||
if (count is > -1_000 and < 1_000)
|
||||
return count.ToString("N0", culture);
|
||||
|
||||
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}";
|
||||
var order = -1;
|
||||
double value = count;
|
||||
while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1)
|
||||
{
|
||||
order++;
|
||||
value /= 1_000;
|
||||
}
|
||||
|
||||
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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)
|
||||
extension(int count)
|
||||
{
|
||||
var order = 0;
|
||||
double size = sizeBytes;
|
||||
while (size >= 1024 && order < SIZE_UNITS.Length - 1)
|
||||
{
|
||||
order++;
|
||||
size /= 1024;
|
||||
}
|
||||
/// <summary>
|
||||
/// Formats a count so that large numbers stay readable.
|
||||
/// </summary>
|
||||
/// <returns>The formatted number.</returns>
|
||||
public string CompactCount() => ((long)count).CompactCount();
|
||||
}
|
||||
|
||||
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}";
|
||||
/// <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>
|
||||
/// <returns>The formatted file size.</returns>
|
||||
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="culture">The culture whose decimal separator to use.</param>
|
||||
/// <returns>The formatted file size.</returns>
|
||||
internal string FileSize(IFormatProvider culture)
|
||||
{
|
||||
var order = 0;
|
||||
double size = sizeBytes;
|
||||
while (size >= 1024 && order < SIZE_UNITS.Length - 1)
|
||||
{
|
||||
order++;
|
||||
size /= 1024;
|
||||
}
|
||||
|
||||
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,17 @@ namespace AIStudio.Tools.MIME;
|
||||
|
||||
public static class MIMETypeExtensions
|
||||
{
|
||||
public static string[] ToStringArray(this MIMEType[] mimeTypes)
|
||||
extension(MIMEType[] mimeTypes)
|
||||
{
|
||||
var result = new string[mimeTypes.Length];
|
||||
for (var i = 0; i < mimeTypes.Length; i++)
|
||||
public string[] ToStringArray()
|
||||
{
|
||||
result[i] = mimeTypes[i];
|
||||
}
|
||||
var result = new string[mimeTypes.Length];
|
||||
for (var i = 0; i < mimeTypes.Length; i++)
|
||||
{
|
||||
result[i] = mimeTypes[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,21 @@ public static class MailboxConnectionFailureExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxConnectionFailureExtensions).Namespace, nameof(MailboxConnectionFailureExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// What went wrong, and what the user can do about it.
|
||||
/// </summary>
|
||||
public static string GetDescription(this MailboxConnectionFailure failure) => failure switch
|
||||
extension(MailboxConnectionFailure failure)
|
||||
{
|
||||
MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."),
|
||||
MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."),
|
||||
MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."),
|
||||
MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."),
|
||||
MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."),
|
||||
MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."),
|
||||
/// <summary>
|
||||
/// What went wrong, and what the user can do about it.
|
||||
/// </summary>
|
||||
public string GetDescription() => failure switch
|
||||
{
|
||||
MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."),
|
||||
MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."),
|
||||
MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."),
|
||||
MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."),
|
||||
MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."),
|
||||
MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."),
|
||||
|
||||
_ => TB("The connection to the server failed for an unknown reason."),
|
||||
};
|
||||
_ => TB("The connection to the server failed for an unknown reason."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,39 @@ public static class MailboxProviderTemplateExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxProviderTemplateExtensions).Namespace, nameof(MailboxProviderTemplateExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// What the user enters as the username at this provider.
|
||||
/// </summary>
|
||||
public static string GetDescription(this MailboxUsernameFormat format) => format switch
|
||||
extension(MailboxUsernameFormat format)
|
||||
{
|
||||
MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."),
|
||||
MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."),
|
||||
MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"),
|
||||
/// <summary>
|
||||
/// What the user enters as the username at this provider.
|
||||
/// </summary>
|
||||
public string GetDescription() => format switch
|
||||
{
|
||||
MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."),
|
||||
MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."),
|
||||
MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"),
|
||||
|
||||
_ => TB("The username your provider gave you."),
|
||||
};
|
||||
_ => TB("The username your provider gave you."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <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)
|
||||
extension(MailboxProviderRequirements requirements)
|
||||
{
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION))
|
||||
yield return TB("Enable the IMAP access in the settings of your webmail first. The provider may switch it off again after a longer time without use.");
|
||||
/// <summary>
|
||||
/// What the user has to do before AI Studio can sign in, one text per requirement.
|
||||
/// </summary>
|
||||
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.");
|
||||
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION))
|
||||
yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first.");
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION))
|
||||
yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first.");
|
||||
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD))
|
||||
yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here.");
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD))
|
||||
yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here.");
|
||||
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR))
|
||||
yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account.");
|
||||
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR))
|
||||
yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,20 @@ namespace AIStudio.Tools.Media;
|
||||
/// <summary>Capabilities of a media-import owner kind.</summary>
|
||||
public static class MediaImportOwnerKindExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the owner stores its own source list and transcripts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Owners that persist their own sources take the attached media over immediately and keep it
|
||||
/// next to the stored document, see <see cref="MediaImportOwnerKind.VISUAL_BRIEFING"/>. The
|
||||
/// attachment control must therefore neither wait for the transcription to finish before showing
|
||||
/// the file, nor deliver the completed transcripts back into its own list afterwards, because
|
||||
/// the owner already holds them. All other owners rely on that delivery instead.
|
||||
/// </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;
|
||||
extension(MediaImportOwnerKind kind)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the owner stores its own source list and transcripts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Owners that persist their own sources take the attached media over immediately and keep it
|
||||
/// next to the stored document, see <see cref="MediaImportOwnerKind.VISUAL_BRIEFING"/>. The
|
||||
/// attachment control must therefore neither wait for the transcription to finish before showing
|
||||
/// the file, nor deliver the completed transcripts back into its own list afterwards, because
|
||||
/// the owner already holds them. All other owners rely on that delivery instead.
|
||||
/// </remarks>
|
||||
/// <returns><c>true</c> when the owner persists its own sources.</returns>
|
||||
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)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data);
|
||||
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)
|
||||
{
|
||||
MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet());
|
||||
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)
|
||||
{
|
||||
true => theme.PaletteDark,
|
||||
false => theme.PaletteLight,
|
||||
};
|
||||
public Palette GetCurrentPalette(SettingsManager settingsManager) => settingsManager.IsDarkMode switch
|
||||
{
|
||||
true => theme.PaletteDark,
|
||||
false => theme.PaletteLight,
|
||||
};
|
||||
|
||||
public static string GetActivityIndicatorColor(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch
|
||||
{
|
||||
true => theme.GetActivityIndicatorDarkColor(),
|
||||
false => theme.GetActivityIndicatorLightColor(),
|
||||
};
|
||||
public string GetActivityIndicatorColor(SettingsManager settingsManager) => settingsManager.IsDarkMode switch
|
||||
{
|
||||
true => theme.GetActivityIndicatorDarkColor(),
|
||||
false => theme.GetActivityIndicatorLightColor(),
|
||||
};
|
||||
|
||||
public static string GetActivityIndicatorLightColor(this MudTheme theme) => theme.PaletteLight.Info.Value;
|
||||
public string GetActivityIndicatorLightColor() => theme.PaletteLight.Info.Value;
|
||||
|
||||
public static string GetActivityIndicatorDarkColor(this MudTheme theme) => theme.PaletteDark.InfoLighten;
|
||||
public string GetActivityIndicatorDarkColor() => theme.PaletteDark.InfoLighten;
|
||||
}
|
||||
}
|
||||
+61
-58
@@ -4,63 +4,66 @@ public static class AssistantComponentTypeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantComponentTypeExtensions).Namespace, nameof(AssistantComponentTypeExtensions));
|
||||
|
||||
public static string GetDisplayName(this AssistantComponentType type) => type switch
|
||||
extension(AssistantComponentType type)
|
||||
{
|
||||
AssistantComponentType.FORM => TB("Root"),
|
||||
AssistantComponentType.TEXT_AREA => TB("Text Area"),
|
||||
AssistantComponentType.BUTTON => TB("Button"),
|
||||
AssistantComponentType.BUTTON_GROUP => TB("Button group"),
|
||||
AssistantComponentType.DROPDOWN => TB("Dropdown"),
|
||||
AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"),
|
||||
AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"),
|
||||
AssistantComponentType.SWITCH => TB("Switch"),
|
||||
AssistantComponentType.HEADING => TB("Heading"),
|
||||
AssistantComponentType.TEXT => TB("Text"),
|
||||
AssistantComponentType.LIST => TB("List"),
|
||||
AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"),
|
||||
AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"),
|
||||
AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"),
|
||||
AssistantComponentType.IMAGE => TB("Image"),
|
||||
AssistantComponentType.COLOR_PICKER => TB("Color Selection"),
|
||||
AssistantComponentType.DATE_PICKER => TB("Date Selection"),
|
||||
AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"),
|
||||
AssistantComponentType.TIME_PICKER => TB("Time Selection"),
|
||||
AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"),
|
||||
AssistantComponentType.LAYOUT_GRID => TB("Grid"),
|
||||
AssistantComponentType.LAYOUT_PAPER => TB("Container"),
|
||||
AssistantComponentType.LAYOUT_STACK => TB("Stack"),
|
||||
AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"),
|
||||
AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"),
|
||||
_ => TB("Unknown Element")
|
||||
};
|
||||
|
||||
public static string GetIcon(this AssistantComponentType type) => type switch
|
||||
{
|
||||
AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick,
|
||||
AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale,
|
||||
AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule,
|
||||
AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory,
|
||||
AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge,
|
||||
AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn,
|
||||
AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title,
|
||||
AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields,
|
||||
AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg,
|
||||
AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List,
|
||||
AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public,
|
||||
AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile,
|
||||
AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile,
|
||||
AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image,
|
||||
AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette,
|
||||
AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth,
|
||||
AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange,
|
||||
AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule,
|
||||
AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize,
|
||||
AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView,
|
||||
AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox,
|
||||
AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers,
|
||||
AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay,
|
||||
AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit,
|
||||
AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree,
|
||||
_ => MudBlazor.Icons.Material.Filled.AccountTree,
|
||||
};
|
||||
public string GetDisplayName() => type switch
|
||||
{
|
||||
AssistantComponentType.FORM => TB("Root"),
|
||||
AssistantComponentType.TEXT_AREA => TB("Text Area"),
|
||||
AssistantComponentType.BUTTON => TB("Button"),
|
||||
AssistantComponentType.BUTTON_GROUP => TB("Button group"),
|
||||
AssistantComponentType.DROPDOWN => TB("Dropdown"),
|
||||
AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"),
|
||||
AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"),
|
||||
AssistantComponentType.SWITCH => TB("Switch"),
|
||||
AssistantComponentType.HEADING => TB("Heading"),
|
||||
AssistantComponentType.TEXT => TB("Text"),
|
||||
AssistantComponentType.LIST => TB("List"),
|
||||
AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"),
|
||||
AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"),
|
||||
AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"),
|
||||
AssistantComponentType.IMAGE => TB("Image"),
|
||||
AssistantComponentType.COLOR_PICKER => TB("Color Selection"),
|
||||
AssistantComponentType.DATE_PICKER => TB("Date Selection"),
|
||||
AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"),
|
||||
AssistantComponentType.TIME_PICKER => TB("Time Selection"),
|
||||
AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"),
|
||||
AssistantComponentType.LAYOUT_GRID => TB("Grid"),
|
||||
AssistantComponentType.LAYOUT_PAPER => TB("Container"),
|
||||
AssistantComponentType.LAYOUT_STACK => TB("Stack"),
|
||||
AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"),
|
||||
AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"),
|
||||
_ => TB("Unknown Element")
|
||||
};
|
||||
|
||||
public string GetIcon() => type switch
|
||||
{
|
||||
AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick,
|
||||
AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale,
|
||||
AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule,
|
||||
AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory,
|
||||
AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge,
|
||||
AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn,
|
||||
AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title,
|
||||
AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields,
|
||||
AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg,
|
||||
AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List,
|
||||
AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public,
|
||||
AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile,
|
||||
AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile,
|
||||
AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image,
|
||||
AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette,
|
||||
AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth,
|
||||
AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange,
|
||||
AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule,
|
||||
AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize,
|
||||
AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView,
|
||||
AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox,
|
||||
AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers,
|
||||
AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay,
|
||||
AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit,
|
||||
AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree,
|
||||
_ => MudBlazor.Icons.Material.Filled.AccountTree,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,34 @@ 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)
|
||||
{
|
||||
var type = lang.GetType();
|
||||
typeName ??= type.Name;
|
||||
typeNamespace ??= type.Namespace!;
|
||||
|
||||
// When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it:
|
||||
if(typeName.Contains('`'))
|
||||
typeName = typeName[..typeName.IndexOf('`')];
|
||||
|
||||
var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::");
|
||||
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
|
||||
|
||||
if(plugin is NoPluginLanguage)
|
||||
return fallbackEN;
|
||||
|
||||
if(plugin.TryGetText(key, out var text, logWarning: false))
|
||||
public string GetText(ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(text))
|
||||
var type = lang.GetType();
|
||||
typeName ??= type.Name;
|
||||
typeNamespace ??= type.Namespace!;
|
||||
|
||||
// When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it:
|
||||
if(typeName.Contains('`'))
|
||||
typeName = typeName[..typeName.IndexOf('`')];
|
||||
|
||||
var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::");
|
||||
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
|
||||
|
||||
if(plugin is NoPluginLanguage)
|
||||
return fallbackEN;
|
||||
|
||||
if(plugin.TryGetText(key, out var text, logWarning: false))
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(text))
|
||||
return fallbackEN;
|
||||
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'.");
|
||||
return fallbackEN;
|
||||
LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'.");
|
||||
return fallbackEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class InternalPluginExtensions
|
||||
{
|
||||
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
|
||||
extension(InternalPlugin plugin)
|
||||
{
|
||||
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
|
||||
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
|
||||
public InternalPluginData MetaData() => plugin switch
|
||||
{
|
||||
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
|
||||
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
|
||||
|
||||
_ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
|
||||
};
|
||||
_ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,37 +4,40 @@ public static class PluginCategoryExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginCategoryExtensions).Namespace, nameof(PluginCategoryExtensions));
|
||||
|
||||
public static string GetName(this PluginCategory type) => type switch
|
||||
extension(PluginCategory type)
|
||||
{
|
||||
PluginCategory.NONE => TB("None"),
|
||||
PluginCategory.CORE => TB("AI Studio Core"),
|
||||
public string GetName() => type switch
|
||||
{
|
||||
PluginCategory.NONE => TB("None"),
|
||||
PluginCategory.CORE => TB("AI Studio Core"),
|
||||
|
||||
PluginCategory.BUSINESS => TB("Business"),
|
||||
PluginCategory.INDUSTRY => TB("Industry"),
|
||||
PluginCategory.UTILITY => TB("Utility"),
|
||||
PluginCategory.SOFTWARE_DEVELOPMENT => TB("Software Development"),
|
||||
PluginCategory.GAMING => TB("Gaming"),
|
||||
PluginCategory.EDUCATION => TB("Education"),
|
||||
PluginCategory.ENTERTAINMENT => TB("Entertainment"),
|
||||
PluginCategory.SOCIAL => TB("Social"),
|
||||
PluginCategory.SHOPPING => TB("Shopping"),
|
||||
PluginCategory.TRAVEL => TB("Travel"),
|
||||
PluginCategory.HEALTH => TB("Health"),
|
||||
PluginCategory.FITNESS => TB("Fitness"),
|
||||
PluginCategory.FOOD => TB("Food"),
|
||||
PluginCategory.PARTY => TB("Party"),
|
||||
PluginCategory.SPORTS => TB("Sports"),
|
||||
PluginCategory.NEWS => TB("News"),
|
||||
PluginCategory.WEATHER => TB("Weather"),
|
||||
PluginCategory.MUSIC => TB("Music"),
|
||||
PluginCategory.POLITICAL => TB("Political"),
|
||||
PluginCategory.SCIENCE => TB("Science"),
|
||||
PluginCategory.TECHNOLOGY => TB("Technology"),
|
||||
PluginCategory.ART => TB("Art"),
|
||||
PluginCategory.FICTION => TB("Fiction"),
|
||||
PluginCategory.WRITING => TB("Writing"),
|
||||
PluginCategory.CONTENT_CREATION => TB("Content Creation"),
|
||||
PluginCategory.BUSINESS => TB("Business"),
|
||||
PluginCategory.INDUSTRY => TB("Industry"),
|
||||
PluginCategory.UTILITY => TB("Utility"),
|
||||
PluginCategory.SOFTWARE_DEVELOPMENT => TB("Software Development"),
|
||||
PluginCategory.GAMING => TB("Gaming"),
|
||||
PluginCategory.EDUCATION => TB("Education"),
|
||||
PluginCategory.ENTERTAINMENT => TB("Entertainment"),
|
||||
PluginCategory.SOCIAL => TB("Social"),
|
||||
PluginCategory.SHOPPING => TB("Shopping"),
|
||||
PluginCategory.TRAVEL => TB("Travel"),
|
||||
PluginCategory.HEALTH => TB("Health"),
|
||||
PluginCategory.FITNESS => TB("Fitness"),
|
||||
PluginCategory.FOOD => TB("Food"),
|
||||
PluginCategory.PARTY => TB("Party"),
|
||||
PluginCategory.SPORTS => TB("Sports"),
|
||||
PluginCategory.NEWS => TB("News"),
|
||||
PluginCategory.WEATHER => TB("Weather"),
|
||||
PluginCategory.MUSIC => TB("Music"),
|
||||
PluginCategory.POLITICAL => TB("Political"),
|
||||
PluginCategory.SCIENCE => TB("Science"),
|
||||
PluginCategory.TECHNOLOGY => TB("Technology"),
|
||||
PluginCategory.ART => TB("Art"),
|
||||
PluginCategory.FICTION => TB("Fiction"),
|
||||
PluginCategory.WRITING => TB("Writing"),
|
||||
PluginCategory.CONTENT_CREATION => TB("Content Creation"),
|
||||
|
||||
_ => TB("Unknown plugin category"),
|
||||
};
|
||||
_ => TB("Unknown plugin category"),
|
||||
};
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -4,19 +4,22 @@ public static class PluginConfigurationObjectTypeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfigurationObjectTypeExtensions).Namespace, nameof(PluginConfigurationObjectTypeExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// The name of a type of configuration object, as users read it, e.g., in a message.
|
||||
/// </summary>
|
||||
public static string GetName(this PluginConfigurationObjectType type) => type switch
|
||||
extension(PluginConfigurationObjectType type)
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => TB("LLM Provider"),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => TB("Embedding Provider"),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => TB("Transcription Provider"),
|
||||
PluginConfigurationObjectType.PROFILE => TB("Profile"),
|
||||
PluginConfigurationObjectType.CHAT_TEMPLATE => TB("Chat Template"),
|
||||
PluginConfigurationObjectType.DATA_SOURCE => TB("Data Source"),
|
||||
PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => TB("Document Analysis Policy"),
|
||||
/// <summary>
|
||||
/// The name of a type of configuration object, as users read it, e.g., in a message.
|
||||
/// </summary>
|
||||
public string GetName() => type switch
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => TB("LLM Provider"),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => TB("Embedding Provider"),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => TB("Transcription Provider"),
|
||||
PluginConfigurationObjectType.PROFILE => TB("Profile"),
|
||||
PluginConfigurationObjectType.CHAT_TEMPLATE => TB("Chat Template"),
|
||||
PluginConfigurationObjectType.DATA_SOURCE => TB("Data Source"),
|
||||
PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => TB("Document Analysis Policy"),
|
||||
|
||||
_ => TB("Unknown configuration"),
|
||||
};
|
||||
_ => TB("Unknown configuration"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,27 @@ public static class PluginTargetGroupExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginTargetGroupExtensions).Namespace, nameof(PluginTargetGroupExtensions));
|
||||
|
||||
public static string Name(this PluginTargetGroup group) => group switch
|
||||
extension(PluginTargetGroup group)
|
||||
{
|
||||
PluginTargetGroup.NONE => TB("No target group"),
|
||||
public string Name() => group switch
|
||||
{
|
||||
PluginTargetGroup.NONE => TB("No target group"),
|
||||
|
||||
PluginTargetGroup.EVERYONE => TB("Everyone"),
|
||||
PluginTargetGroup.CHILDREN => TB("Children"),
|
||||
PluginTargetGroup.TEENAGERS => TB("Teenagers"),
|
||||
PluginTargetGroup.STUDENTS => TB("Students"),
|
||||
PluginTargetGroup.ADULTS => TB("Adults"),
|
||||
PluginTargetGroup.EVERYONE => TB("Everyone"),
|
||||
PluginTargetGroup.CHILDREN => TB("Children"),
|
||||
PluginTargetGroup.TEENAGERS => TB("Teenagers"),
|
||||
PluginTargetGroup.STUDENTS => TB("Students"),
|
||||
PluginTargetGroup.ADULTS => TB("Adults"),
|
||||
|
||||
PluginTargetGroup.INDUSTRIAL_WORKERS => TB("Industrial workers"),
|
||||
PluginTargetGroup.OFFICE_WORKERS => TB("Office workers"),
|
||||
PluginTargetGroup.BUSINESS_PROFESSIONALS => TB("Business professionals"),
|
||||
PluginTargetGroup.SOFTWARE_DEVELOPERS => TB("Software developers"),
|
||||
PluginTargetGroup.SCIENTISTS => TB("Scientists"),
|
||||
PluginTargetGroup.TEACHERS => TB("Teachers"),
|
||||
PluginTargetGroup.ARTISTS => TB("Artists"),
|
||||
PluginTargetGroup.INDUSTRIAL_WORKERS => TB("Industrial workers"),
|
||||
PluginTargetGroup.OFFICE_WORKERS => TB("Office workers"),
|
||||
PluginTargetGroup.BUSINESS_PROFESSIONALS => TB("Business professionals"),
|
||||
PluginTargetGroup.SOFTWARE_DEVELOPERS => TB("Software developers"),
|
||||
PluginTargetGroup.SCIENTISTS => TB("Scientists"),
|
||||
PluginTargetGroup.TEACHERS => TB("Teachers"),
|
||||
PluginTargetGroup.ARTISTS => TB("Artists"),
|
||||
|
||||
_ => TB("Unknown target group"),
|
||||
};
|
||||
_ => TB("Unknown target group"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,28 @@ public static class PluginTypeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginTypeExtensions).Namespace, nameof(PluginTypeExtensions));
|
||||
|
||||
public static string GetName(this PluginType type) => type switch
|
||||
extension(PluginType type)
|
||||
{
|
||||
PluginType.LANGUAGE => TB("Language plugin"),
|
||||
PluginType.ASSISTANT => TB("Assistant plugin"),
|
||||
PluginType.CONFIGURATION => TB("Configuration plugin"),
|
||||
PluginType.THEME => TB("Theme plugin"),
|
||||
PluginType.MODEL => TB("Model plugin"),
|
||||
public string GetName() => type switch
|
||||
{
|
||||
PluginType.LANGUAGE => TB("Language plugin"),
|
||||
PluginType.ASSISTANT => TB("Assistant plugin"),
|
||||
PluginType.CONFIGURATION => TB("Configuration plugin"),
|
||||
PluginType.THEME => TB("Theme plugin"),
|
||||
PluginType.MODEL => TB("Model plugin"),
|
||||
|
||||
_ => TB("Unknown plugin type"),
|
||||
};
|
||||
|
||||
public static string GetDirectory(this PluginType type) => type switch
|
||||
{
|
||||
PluginType.LANGUAGE => "languages",
|
||||
PluginType.ASSISTANT => "assistants",
|
||||
PluginType.CONFIGURATION => "configurations",
|
||||
PluginType.THEME => "themes",
|
||||
PluginType.MODEL => "models",
|
||||
_ => TB("Unknown plugin type"),
|
||||
};
|
||||
|
||||
_ => "unknown",
|
||||
};
|
||||
public string GetDirectory() => type switch
|
||||
{
|
||||
PluginType.LANGUAGE => "languages",
|
||||
PluginType.ASSISTANT => "assistants",
|
||||
PluginType.CONFIGURATION => "configurations",
|
||||
PluginType.THEME => "themes",
|
||||
PluginType.MODEL => "models",
|
||||
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,23 @@ internal static class LayoutPurposeAreaExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => PluginSystem.I18N.I.T(fallbackEN, typeof(LayoutPurposeAreaExtensions).Namespace, nameof(LayoutPurposeAreaExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// The name of the area, as the forms show it above its kinds of slides.
|
||||
/// </summary>
|
||||
/// <param name="area">The area.</param>
|
||||
public static string Label(this LayoutPurposeArea area) => area switch
|
||||
extension(LayoutPurposeArea area)
|
||||
{
|
||||
LayoutPurposeArea.FRAMING => TB("Structure and framing"),
|
||||
LayoutPurposeArea.GENERAL => TB("General content"),
|
||||
LayoutPurposeArea.TEACHING => TB("Teaching and workshops"),
|
||||
LayoutPurposeArea.BUSINESS => TB("Business and projects"),
|
||||
LayoutPurposeArea.SCIENCE => TB("Science and research"),
|
||||
LayoutPurposeArea.MEDICINE => TB("Medicine"),
|
||||
LayoutPurposeArea.LAW => TB("Law"),
|
||||
/// <summary>
|
||||
/// The name of the area, as the forms show it above its kinds of slides.
|
||||
/// </summary>
|
||||
public string Label() => area switch
|
||||
{
|
||||
LayoutPurposeArea.FRAMING => TB("Structure and framing"),
|
||||
LayoutPurposeArea.GENERAL => TB("General content"),
|
||||
LayoutPurposeArea.TEACHING => TB("Teaching and workshops"),
|
||||
LayoutPurposeArea.BUSINESS => TB("Business and projects"),
|
||||
LayoutPurposeArea.SCIENCE => TB("Science and research"),
|
||||
LayoutPurposeArea.MEDICINE => TB("Medicine"),
|
||||
LayoutPurposeArea.LAW => TB("Law"),
|
||||
|
||||
_ => TB("Other kinds of slides"),
|
||||
};
|
||||
_ => TB("Other kinds of slides"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,66 +16,63 @@ internal static partial class LayoutPurposeExtensions
|
||||
/// </summary>
|
||||
public static string NoPreference => TB("No preference (the LLM decides)");
|
||||
|
||||
/// <summary>
|
||||
/// The area the forms list a kind of slide in.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
public static LayoutPurposeArea Area(this LayoutPurpose purpose) => Describe(purpose).Area;
|
||||
|
||||
/// <summary>
|
||||
/// The short name of the choice, e.g., "Layout for chapters".
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
public static string Label(this LayoutPurpose purpose) => Describe(purpose).Label;
|
||||
|
||||
/// <summary>
|
||||
/// The question the choice answers, which says which slides it applies to.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
public static string Question(this LayoutPurpose purpose) => Describe(purpose).Question;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the slides of this kind for the model, so that it knows which of its slides a choice applies to.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
/// <returns>The description, e.g., "timelines, e.g., the course of a project".</returns>
|
||||
public static string PromptDescription(this LayoutPurpose purpose) => Describe(purpose).PromptDescription;
|
||||
|
||||
/// <summary>
|
||||
/// Whether choosing a layout for this kind of slide also puts such a slide into the presentation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For most kinds, a choice says only which layout such a slide uses, and whether the
|
||||
/// presentation has one follows from its content. The slides which frame a presentation are
|
||||
/// different: whoever chooses a layout for the table of contents or the imprint expects them.
|
||||
/// </remarks>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
public static bool ChoiceAddsSlide(this LayoutPurpose purpose) => purpose.PromptPlacement() is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Where a slide goes which a choice puts into the presentation, and what it holds, for the model.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
/// <returns>The rule, or null when a choice adds no slide of this kind.</returns>
|
||||
public static string? PromptPlacement(this LayoutPurpose purpose) => purpose switch
|
||||
extension(LayoutPurpose purpose)
|
||||
{
|
||||
LayoutPurpose.TABLE_OF_CONTENTS => "right after the title slide",
|
||||
LayoutPurpose.Q_AND_A => "near the end, before the closing slide; where the occasion suits it, e.g., in a lecture or a workshop, also at the end of chapters, then naming the topic of the chapter",
|
||||
LayoutPurpose.ACKNOWLEDGEMENTS => "near the end; where thanks concern a single chapter, also at the end of that chapter",
|
||||
LayoutPurpose.REFERENCES => "near the end; it lists only the sources the content names, or else the attached documents, and never a source of your own",
|
||||
LayoutPurpose.CLOSING => "at the end, before an appendix and the imprint",
|
||||
LayoutPurpose.IMPRINT => "as the very last slide; it holds only what the request, the content, or the profile state, e.g., the title, the authors, the organization, and today's date, and it leaves a slot empty rather than inventing anything",
|
||||
/// <summary>
|
||||
/// The area the forms list a kind of slide in.
|
||||
/// </summary>
|
||||
public LayoutPurposeArea Area() => Describe(purpose).Area;
|
||||
|
||||
_ => null,
|
||||
};
|
||||
/// <summary>
|
||||
/// The short name of the choice, e.g., "Layout for chapters".
|
||||
/// </summary>
|
||||
public string Label() => Describe(purpose).Label;
|
||||
|
||||
/// <summary>
|
||||
/// The help below the choice: its question, and whether a choice adds such a slide.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The kind of slide.</param>
|
||||
public static string Help(this LayoutPurpose purpose) => purpose.ChoiceAddsSlide()
|
||||
? $"{purpose.Question()} {TB("Choosing a layout also adds such a slide to the presentation.")}"
|
||||
: purpose.Question();
|
||||
/// <summary>
|
||||
/// The question the choice answers, which says which slides it applies to.
|
||||
/// </summary>
|
||||
public string Question() => Describe(purpose).Question;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the slides of this kind for the model, so that it knows which of its slides a choice applies to.
|
||||
/// </summary>
|
||||
/// <returns>The description, e.g., "timelines, e.g., the course of a project".</returns>
|
||||
public string PromptDescription() => Describe(purpose).PromptDescription;
|
||||
|
||||
/// <summary>
|
||||
/// Whether choosing a layout for this kind of slide also puts such a slide into the presentation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For most kinds, a choice says only which layout such a slide uses, and whether the
|
||||
/// presentation has one follows from its content. The slides which frame a presentation are
|
||||
/// different: whoever chooses a layout for the table of contents or the imprint expects them.
|
||||
/// </remarks>
|
||||
public bool ChoiceAddsSlide() => purpose.PromptPlacement() is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Where a slide goes which a choice puts into the presentation, and what it holds, for the model.
|
||||
/// </summary>
|
||||
/// <returns>The rule, or null when a choice adds no slide of this kind.</returns>
|
||||
public string? PromptPlacement() => purpose switch
|
||||
{
|
||||
LayoutPurpose.TABLE_OF_CONTENTS => "right after the title slide",
|
||||
LayoutPurpose.Q_AND_A => "near the end, before the closing slide; where the occasion suits it, e.g., in a lecture or a workshop, also at the end of chapters, then naming the topic of the chapter",
|
||||
LayoutPurpose.ACKNOWLEDGEMENTS => "near the end; where thanks concern a single chapter, also at the end of that chapter",
|
||||
LayoutPurpose.REFERENCES => "near the end; it lists only the sources the content names, or else the attached documents, and never a source of your own",
|
||||
LayoutPurpose.CLOSING => "at the end, before an appendix and the imprint",
|
||||
LayoutPurpose.IMPRINT => "as the very last slide; it holds only what the request, the content, or the profile state, e.g., the title, the authors, the organization, and today's date, and it leaves a slot empty rather than inventing anything",
|
||||
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The help below the choice: its question, and whether a choice adds such a slide.
|
||||
/// </summary>
|
||||
public string Help() => purpose.ChoiceAddsSlide()
|
||||
? $"{purpose.Question()} {TB("Choosing a layout also adds such a slide to the presentation.")}"
|
||||
: purpose.Question();
|
||||
}
|
||||
|
||||
private static LayoutPurposeInfo Describe(LayoutPurpose purpose) =>
|
||||
DescribeFraming(purpose)
|
||||
|
||||
@@ -42,54 +42,57 @@ internal static class PlaceholderRoleExtensions
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether content of the presentation goes into a placeholder of this role.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Footer, slide number and date are filled as well, but by the presentation as a whole rather
|
||||
/// than by a single slide, so they are no slots of a layout.
|
||||
/// </remarks>
|
||||
public static bool IsSlot(this PlaceholderRole role) => role switch
|
||||
extension(PlaceholderRole role)
|
||||
{
|
||||
PlaceholderRole.TITLE => true,
|
||||
PlaceholderRole.SUBTITLE => true,
|
||||
PlaceholderRole.TEXT => true,
|
||||
PlaceholderRole.LABEL => true,
|
||||
PlaceholderRole.PICTURE => true,
|
||||
PlaceholderRole.TABLE => true,
|
||||
/// <summary>
|
||||
/// Whether content of the presentation goes into a placeholder of this role.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Footer, slide number and date are filled as well, but by the presentation as a whole rather
|
||||
/// than by a single slide, so they are no slots of a layout.
|
||||
/// </remarks>
|
||||
public bool IsSlot() => role switch
|
||||
{
|
||||
PlaceholderRole.TITLE => true,
|
||||
PlaceholderRole.SUBTITLE => true,
|
||||
PlaceholderRole.TEXT => true,
|
||||
PlaceholderRole.LABEL => true,
|
||||
PlaceholderRole.PICTURE => true,
|
||||
PlaceholderRole.TABLE => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether a placeholder of this role carries the content of a slide, rather than describing it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A layout offering nothing but a title and some labels has no room for content. Such layouts are
|
||||
/// samples or blank pages, and they are left out of the catalog.
|
||||
/// </remarks>
|
||||
public static bool CarriesContent(this PlaceholderRole role) => role switch
|
||||
{
|
||||
PlaceholderRole.SUBTITLE => true,
|
||||
PlaceholderRole.TEXT => true,
|
||||
PlaceholderRole.PICTURE => true,
|
||||
PlaceholderRole.TABLE => true,
|
||||
/// <summary>
|
||||
/// Whether a placeholder of this role carries the content of a slide, rather than describing it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A layout offering nothing but a title and some labels has no room for content. Such layouts are
|
||||
/// samples or blank pages, and they are left out of the catalog.
|
||||
/// </remarks>
|
||||
public bool CarriesContent() => role switch
|
||||
{
|
||||
PlaceholderRole.SUBTITLE => true,
|
||||
PlaceholderRole.TEXT => true,
|
||||
PlaceholderRole.PICTURE => true,
|
||||
PlaceholderRole.TABLE => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// How the role is called in the catalog the model reads.
|
||||
/// </summary>
|
||||
public static string CatalogName(this PlaceholderRole role) => role switch
|
||||
{
|
||||
PlaceholderRole.TITLE => "title",
|
||||
PlaceholderRole.SUBTITLE => "subtitle",
|
||||
PlaceholderRole.TEXT => "text",
|
||||
PlaceholderRole.LABEL => "label",
|
||||
PlaceholderRole.PICTURE => "picture",
|
||||
PlaceholderRole.TABLE => "table",
|
||||
/// <summary>
|
||||
/// How the role is called in the catalog the model reads.
|
||||
/// </summary>
|
||||
public string CatalogName() => role switch
|
||||
{
|
||||
PlaceholderRole.TITLE => "title",
|
||||
PlaceholderRole.SUBTITLE => "subtitle",
|
||||
PlaceholderRole.TEXT => "text",
|
||||
PlaceholderRole.LABEL => "label",
|
||||
PlaceholderRole.PICTURE => "picture",
|
||||
PlaceholderRole.TABLE => "table",
|
||||
|
||||
_ => "unsupported",
|
||||
};
|
||||
_ => "unsupported",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,23 @@ namespace AIStudio.Tools.PowerPoint;
|
||||
|
||||
internal static class TemplateSlideElementKindExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// How the kind is called in the list of template slides the model reads.
|
||||
/// </summary>
|
||||
public static string CatalogName(this TemplateSlideElementKind kind) => kind switch
|
||||
extension(TemplateSlideElementKind kind)
|
||||
{
|
||||
TemplateSlideElementKind.TEXT_BOX => "text box",
|
||||
TemplateSlideElementKind.SHAPE => "shape",
|
||||
TemplateSlideElementKind.LINE => "line",
|
||||
TemplateSlideElementKind.PICTURE => "picture",
|
||||
TemplateSlideElementKind.TABLE => "table",
|
||||
TemplateSlideElementKind.CHART => "chart",
|
||||
TemplateSlideElementKind.DIAGRAM => "diagram",
|
||||
TemplateSlideElementKind.GROUP => "group",
|
||||
/// <summary>
|
||||
/// How the kind is called in the list of template slides the model reads.
|
||||
/// </summary>
|
||||
public string CatalogName() => kind switch
|
||||
{
|
||||
TemplateSlideElementKind.TEXT_BOX => "text box",
|
||||
TemplateSlideElementKind.SHAPE => "shape",
|
||||
TemplateSlideElementKind.LINE => "line",
|
||||
TemplateSlideElementKind.PICTURE => "picture",
|
||||
TemplateSlideElementKind.TABLE => "table",
|
||||
TemplateSlideElementKind.CHART => "chart",
|
||||
TemplateSlideElementKind.DIAGRAM => "diagram",
|
||||
TemplateSlideElementKind.GROUP => "group",
|
||||
|
||||
_ => "object",
|
||||
};
|
||||
_ => "object",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,33 @@ namespace AIStudio.Tools.PowerPoint;
|
||||
|
||||
internal static class TemplateSlideUseListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// What the user chose for one slide of the template.
|
||||
/// </summary>
|
||||
/// <param name="uses">What the user chose for each slide of the template, by its number.</param>
|
||||
/// <param name="number">The number of the slide, starting at 1.</param>
|
||||
/// <returns>The choice; TemplateSlideUse.OMIT for a slide the list does not cover.</returns>
|
||||
public static TemplateSlideUse UseOf(this IReadOnlyList<TemplateSlideUse> uses, int number) => number >= 1 && number <= uses.Count ? uses[number - 1] : TemplateSlideUse.OMIT;
|
||||
extension(IReadOnlyList<TemplateSlideUse> uses)
|
||||
{
|
||||
/// <summary>
|
||||
/// What the user chose for one slide of the template.
|
||||
/// </summary>
|
||||
/// <param name="number">The number of the slide, starting at 1.</param>
|
||||
/// <returns>The choice; TemplateSlideUse.OMIT for a slide the list does not cover.</returns>
|
||||
public TemplateSlideUse UseOf(int number) => number >= 1 && number <= uses.Count ? uses[number - 1] : TemplateSlideUse.OMIT;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the presentation does with each slide of a template, by the choices for some of its slides.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A slide without a choice is left out, as by default. A choice to edit a slide which the model
|
||||
/// cannot edit keeps the slide unchanged instead: such a choice may come from an organization,
|
||||
/// which the user cannot overrule, and it would block the form. A choice for a slide the template
|
||||
/// does not have is passed over.
|
||||
/// </remarks>
|
||||
/// <param name="choices">The choices by the number of the slide, starting at 1.</param>
|
||||
/// <param name="template">The template whose slides the choices number.</param>
|
||||
/// <returns>A choice for each slide of the template, in its order.</returns>
|
||||
public static List<TemplateSlideUse> ToTemplateSlideUses(this IReadOnlyDictionary<int, TemplateSlideUse> choices, PresentationTemplate template) => template.Slides
|
||||
.Select(slide => choices.TryGetValue(slide.Number, out var use) ? use is TemplateSlideUse.EDIT && !slide.IsEditable ? TemplateSlideUse.KEEP : use : TemplateSlideUse.OMIT)
|
||||
.ToList();
|
||||
extension(IReadOnlyDictionary<int, TemplateSlideUse> choices)
|
||||
{
|
||||
/// <summary>
|
||||
/// What the presentation does with each slide of a template, by the choices for some of its slides.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A slide without a choice is left out, as by default. A choice to edit a slide which the model
|
||||
/// cannot edit keeps the slide unchanged instead: such a choice may come from an organization,
|
||||
/// which the user cannot overrule, and it would block the form. A choice for a slide the template
|
||||
/// does not have is passed over.
|
||||
/// </remarks>
|
||||
/// <param name="template">The template whose slides the choices number.</param>
|
||||
/// <returns>A choice for each slide of the template, in its order.</returns>
|
||||
public List<TemplateSlideUse> ToTemplateSlideUses(PresentationTemplate template) => template.Slides
|
||||
.Select(slide => choices.TryGetValue(slide.Number, out var use) ? use is TemplateSlideUse.EDIT && !slide.IsEditable ? TemplateSlideUse.KEEP : use : TemplateSlideUse.OMIT)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class ProfileExtensions
|
||||
{
|
||||
public static IEnumerable<Profile> GetAllProfiles(this IEnumerable<Profile> profiles)
|
||||
extension(IEnumerable<Profile> profiles)
|
||||
{
|
||||
yield return Profile.NO_PROFILE;
|
||||
foreach (var profile in profiles)
|
||||
yield return profile;
|
||||
public IEnumerable<Profile> GetAllProfiles()
|
||||
{
|
||||
yield return Profile.NO_PROFILE;
|
||||
foreach (var profile in profiles)
|
||||
yield return profile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,85 +90,88 @@ public static class IRetrievalContextExtensions
|
||||
contextBuilder.AppendLine("Matched image content: an image matched here, but it is not shown to you. Do not guess what it shows. When it matters for the answer, tell the user that an image matched, and where to find it, using the path and the links above.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares retrieval contexts for the AI: filters them for prompt injections, writes out what
|
||||
/// the AI is told about each, and keeps what the user is shown of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A data source is as untrusted as any other external content: it may serve text written to
|
||||
/// steer the model rather than to answer the query. So everything a data source wrote passes
|
||||
/// the filter: the description of each context, its matched text, and each part of its
|
||||
/// surrounding content. What the app writes around them, such as the labels and the fences,
|
||||
/// does not need to.<br/><br/>
|
||||
/// All contexts go to the runtime in one request, which also gives the user one report for all
|
||||
/// of them. The passages are made from the filtered texts, so the user reads what the AI read.
|
||||
/// </remarks>
|
||||
/// <param name="retrievalContexts">The contexts to prepare.</param>
|
||||
/// <param name="guardService">The prompt injection filter.</param>
|
||||
/// <param name="reportingScope">The action the contexts are retrieved for, or null when they are retrieved for none.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>One prepared context per context, in the order they came in.</returns>
|
||||
public static async Task<IReadOnlyList<PreparedRetrievalContext>> PrepareForModelAsync(this IReadOnlyList<IRetrievalContext> retrievalContexts, PromptInjectionGuardService guardService, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default)
|
||||
extension(IReadOnlyList<IRetrievalContext> retrievalContexts)
|
||||
{
|
||||
if (retrievalContexts.Count is 0)
|
||||
return [];
|
||||
|
||||
//
|
||||
// The texts of all contexts in one list, each context with its description first, then its
|
||||
// matched text, then its surrounding content. The answer comes back in the same order, and
|
||||
// is read back below in the same way:
|
||||
//
|
||||
var texts = new List<PromptInjectionText>();
|
||||
foreach (var retrievalContext in retrievalContexts)
|
||||
/// <summary>
|
||||
/// Prepares retrieval contexts for the AI: filters them for prompt injections, writes out what
|
||||
/// the AI is told about each, and keeps what the user is shown of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A data source is as untrusted as any other external content: it may serve text written to
|
||||
/// steer the model rather than to answer the query. So everything a data source wrote passes
|
||||
/// the filter: the description of each context, its matched text, and each part of its
|
||||
/// surrounding content. What the app writes around them, such as the labels and the fences,
|
||||
/// does not need to.<br/><br/>
|
||||
/// All contexts go to the runtime in one request, which also gives the user one report for all
|
||||
/// of them. The passages are made from the filtered texts, so the user reads what the AI read.
|
||||
/// </remarks>
|
||||
/// <param name="guardService">The prompt injection filter.</param>
|
||||
/// <param name="reportingScope">The action the contexts are retrieved for, or null when they are retrieved for none.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>One prepared context per context, in the order they came in.</returns>
|
||||
public async Task<IReadOnlyList<PreparedRetrievalContext>> PrepareForModelAsync(PromptInjectionGuardService guardService, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default)
|
||||
{
|
||||
var source = PromptInjectionSource.RetrievalContext(retrievalContext.DataSourceName, retrievalContext.Path);
|
||||
var description = new StringBuilder();
|
||||
AppendContextDescription(description, retrievalContext);
|
||||
texts.Add(new(description.ToString(), source));
|
||||
if (retrievalContexts.Count is 0)
|
||||
return [];
|
||||
|
||||
if (retrievalContext is not RetrievalTextContext textContext)
|
||||
continue;
|
||||
|
||||
texts.Add(new(textContext.MatchedText, source));
|
||||
foreach (var surrounding in textContext.SurroundingContent)
|
||||
texts.Add(new(surrounding, source));
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
var sanitizedTexts = await guardService.SanitizeAsync(texts, reportingScope);
|
||||
|
||||
var preparedContexts = new List<PreparedRetrievalContext>(retrievalContexts.Count);
|
||||
var nextText = 0;
|
||||
foreach (var retrievalContext in retrievalContexts)
|
||||
{
|
||||
var body = new StringBuilder();
|
||||
body.Append(sanitizedTexts[nextText++]);
|
||||
|
||||
var matchedText = string.Empty;
|
||||
IReadOnlyList<string> surroundingContent = [];
|
||||
switch (retrievalContext)
|
||||
//
|
||||
// The texts of all contexts in one list, each context with its description first, then its
|
||||
// matched text, then its surrounding content. The answer comes back in the same order, and
|
||||
// is read back below in the same way:
|
||||
//
|
||||
var texts = new List<PromptInjectionText>();
|
||||
foreach (var retrievalContext in retrievalContexts)
|
||||
{
|
||||
case RetrievalTextContext textContext:
|
||||
matchedText = sanitizedTexts[nextText++];
|
||||
surroundingContent = sanitizedTexts.Skip(nextText).Take(textContext.SurroundingContent.Count).ToList();
|
||||
nextText += surroundingContent.Count;
|
||||
AppendTextMatch(body, matchedText, surroundingContent);
|
||||
break;
|
||||
var source = PromptInjectionSource.RetrievalContext(retrievalContext.DataSourceName, retrievalContext.Path);
|
||||
var description = new StringBuilder();
|
||||
AppendContextDescription(description, retrievalContext);
|
||||
texts.Add(new(description.ToString(), source));
|
||||
|
||||
case RetrievalImageContext:
|
||||
AppendImageMatch(body);
|
||||
break;
|
||||
if (retrievalContext is not RetrievalTextContext textContext)
|
||||
continue;
|
||||
|
||||
default:
|
||||
LOGGER.LogWarning($"The retrieval content type '{retrievalContext.Type}' of data source '{retrievalContext.DataSourceName}' at location '{retrievalContext.Path}' is not supported yet.");
|
||||
break;
|
||||
texts.Add(new(textContext.MatchedText, source));
|
||||
foreach (var surrounding in textContext.SurroundingContent)
|
||||
texts.Add(new(surrounding, source));
|
||||
}
|
||||
|
||||
var sources = retrievalContext.ToSources();
|
||||
preparedContexts.Add(new(retrievalContext, body.ToString(), sources, CreatePassage(retrievalContext, sources, matchedText, surroundingContent)));
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
var sanitizedTexts = await guardService.SanitizeAsync(texts, reportingScope);
|
||||
|
||||
return preparedContexts;
|
||||
var preparedContexts = new List<PreparedRetrievalContext>(retrievalContexts.Count);
|
||||
var nextText = 0;
|
||||
foreach (var retrievalContext in retrievalContexts)
|
||||
{
|
||||
var body = new StringBuilder();
|
||||
body.Append(sanitizedTexts[nextText++]);
|
||||
|
||||
var matchedText = string.Empty;
|
||||
IReadOnlyList<string> surroundingContent = [];
|
||||
switch (retrievalContext)
|
||||
{
|
||||
case RetrievalTextContext textContext:
|
||||
matchedText = sanitizedTexts[nextText++];
|
||||
surroundingContent = sanitizedTexts.Skip(nextText).Take(textContext.SurroundingContent.Count).ToList();
|
||||
nextText += surroundingContent.Count;
|
||||
AppendTextMatch(body, matchedText, surroundingContent);
|
||||
break;
|
||||
|
||||
case RetrievalImageContext:
|
||||
AppendImageMatch(body);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGGER.LogWarning($"The retrieval content type '{retrievalContext.Type}' of data source '{retrievalContext.DataSourceName}' at location '{retrievalContext.Path}' is not supported yet.");
|
||||
break;
|
||||
}
|
||||
|
||||
var sources = retrievalContext.ToSources();
|
||||
preparedContexts.Add(new(retrievalContext, body.ToString(), sources, CreatePassage(retrievalContext, sources, matchedText, surroundingContent)));
|
||||
}
|
||||
|
||||
return preparedContexts;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -242,34 +245,37 @@ public static class IRetrievalContextExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sources a retrieval context lends to an answer, as they are listed below it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The reference comes first: the title and link of the passage itself where the data source
|
||||
/// names them, e.g., a local file with its page, and otherwise the data source and the path.
|
||||
/// The further links of the context follow. Only what can be opened becomes a link, i.e., a
|
||||
/// web address or a file with an absolute path. A relative path would point elsewhere depending
|
||||
/// on where it is opened from.<br/><br/>
|
||||
///
|
||||
/// A context which names nothing to open at all, e.g., by a path which only its ERI server knows,
|
||||
/// still lends one source: the AI received it, and its passage needs a place to be read at. That
|
||||
/// source is named as text, see SourceExtensions.CreateRetrievalSourceUrl.
|
||||
/// </remarks>
|
||||
/// <param name="retrievalContext">The retrieval context.</param>
|
||||
/// <returns>The sources; none only for a context which names neither a document nor its data source.</returns>
|
||||
public static IReadOnlyList<Source> ToSources(this IRetrievalContext retrievalContext)
|
||||
extension(IRetrievalContext retrievalContext)
|
||||
{
|
||||
var sources = new List<Source>();
|
||||
var referenceTitle = GetReferenceTitle(retrievalContext);
|
||||
AddSource(sources, referenceTitle, GetReferenceLink(retrievalContext));
|
||||
foreach (var link in retrievalContext.Links)
|
||||
AddSource(sources, retrievalContext.DataSourceName, link);
|
||||
/// <summary>
|
||||
/// The sources a retrieval context lends to an answer, as they are listed below it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The reference comes first: the title and link of the passage itself where the data source
|
||||
/// names them, e.g., a local file with its page, and otherwise the data source and the path.
|
||||
/// The further links of the context follow. Only what can be opened becomes a link, i.e., a
|
||||
/// web address or a file with an absolute path. A relative path would point elsewhere depending
|
||||
/// on where it is opened from.<br/><br/>
|
||||
///
|
||||
/// A context which names nothing to open at all, e.g., by a path which only its ERI server knows,
|
||||
/// still lends one source: the AI received it, and its passage needs a place to be read at. That
|
||||
/// source is named as text, see SourceExtensions.CreateRetrievalSourceUrl.
|
||||
/// </remarks>
|
||||
/// <returns>The sources; none only for a context which names neither a document nor its data source.</returns>
|
||||
public IReadOnlyList<Source> ToSources()
|
||||
{
|
||||
var sources = new List<Source>();
|
||||
var referenceTitle = GetReferenceTitle(retrievalContext);
|
||||
AddSource(sources, referenceTitle, GetReferenceLink(retrievalContext));
|
||||
foreach (var link in retrievalContext.Links)
|
||||
AddSource(sources, retrievalContext.DataSourceName, link);
|
||||
|
||||
if (sources.Count is 0 && !string.IsNullOrWhiteSpace(referenceTitle))
|
||||
sources.Add(new Source(referenceTitle, SourceExtensions.CreateRetrievalSourceUrl(referenceTitle, retrievalContext.Path), SourceOrigin.RAG));
|
||||
if (sources.Count is 0 && !string.IsNullOrWhiteSpace(referenceTitle))
|
||||
sources.Add(new Source(referenceTitle, SourceExtensions.CreateRetrievalSourceUrl(referenceTitle, retrievalContext.Path), SourceOrigin.RAG));
|
||||
|
||||
return sources;
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddSource(ICollection<Source> sources, string title, string link)
|
||||
|
||||
@@ -4,21 +4,24 @@ namespace AIStudio.Tools.RAG;
|
||||
|
||||
public static class RetrievalContentCategoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an ERI content type to a common retrieval content category.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type yielded by the ERI server.</param>
|
||||
/// <returns>The corresponding retrieval content category.</returns>
|
||||
public static RetrievalContentCategory ToRetrievalContentCategory(this ContentType contentType) => contentType switch
|
||||
extension(ContentType contentType)
|
||||
{
|
||||
ContentType.NONE => RetrievalContentCategory.NONE,
|
||||
ContentType.UNKNOWN => RetrievalContentCategory.UNKNOWN,
|
||||
ContentType.TEXT => RetrievalContentCategory.TEXT,
|
||||
ContentType.IMAGE => RetrievalContentCategory.IMAGE,
|
||||
ContentType.VIDEO => RetrievalContentCategory.VIDEO,
|
||||
ContentType.AUDIO => RetrievalContentCategory.AUDIO,
|
||||
ContentType.SPEECH => RetrievalContentCategory.AUDIO,
|
||||
/// <summary>
|
||||
/// Converts an ERI content type to a common retrieval content category.
|
||||
/// </summary>
|
||||
/// <returns>The corresponding retrieval content category.</returns>
|
||||
public RetrievalContentCategory ToRetrievalContentCategory() => contentType switch
|
||||
{
|
||||
ContentType.NONE => RetrievalContentCategory.NONE,
|
||||
ContentType.UNKNOWN => RetrievalContentCategory.UNKNOWN,
|
||||
ContentType.TEXT => RetrievalContentCategory.TEXT,
|
||||
ContentType.IMAGE => RetrievalContentCategory.IMAGE,
|
||||
ContentType.VIDEO => RetrievalContentCategory.VIDEO,
|
||||
ContentType.AUDIO => RetrievalContentCategory.AUDIO,
|
||||
ContentType.SPEECH => RetrievalContentCategory.AUDIO,
|
||||
|
||||
_ => RetrievalContentCategory.UNKNOWN,
|
||||
};
|
||||
_ => RetrievalContentCategory.UNKNOWN,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,138 +4,141 @@ namespace AIStudio.Tools.RAG;
|
||||
|
||||
public static class RetrievalContentTypeExtensions
|
||||
{
|
||||
public static RetrievalContentType ToRetrievalContentType(this Context eriContext)
|
||||
extension(Context eriContext)
|
||||
{
|
||||
//
|
||||
// Right now, we have to parse the category string along the type enum to
|
||||
// determine the common retrieval content type. In future ERI versions, we
|
||||
// might use the same enum.
|
||||
//
|
||||
|
||||
var lowerCategory = eriContext.Category.ToLowerInvariant();
|
||||
var type = eriContext.Type;
|
||||
return type switch
|
||||
public RetrievalContentType ToRetrievalContentType()
|
||||
{
|
||||
ContentType.TEXT when lowerCategory.Contains("book") => RetrievalContentType.TEXT_BOOK,
|
||||
ContentType.TEXT when lowerCategory.Contains("paper") => RetrievalContentType.TEXT_PAPER,
|
||||
ContentType.TEXT when lowerCategory.Contains("dictionary") => RetrievalContentType.TEXT_DICTIONARY,
|
||||
ContentType.TEXT when lowerCategory.Contains("encyclopedia") => RetrievalContentType.TEXT_ENCYCLOPEDIA,
|
||||
ContentType.TEXT when lowerCategory.Contains("glossary") => RetrievalContentType.TEXT_GLOSSARY,
|
||||
ContentType.TEXT when lowerCategory.Contains("journal") => RetrievalContentType.TEXT_JOURNAL,
|
||||
ContentType.TEXT when lowerCategory.Contains("magazine") => RetrievalContentType.TEXT_MAGAZINE,
|
||||
ContentType.TEXT when lowerCategory.Contains("newspaper") => RetrievalContentType.TEXT_NEWSPAPER,
|
||||
ContentType.TEXT when lowerCategory.Contains("report") => RetrievalContentType.TEXT_REPORT,
|
||||
ContentType.TEXT when lowerCategory.Contains("review") => RetrievalContentType.TEXT_REVIEW,
|
||||
ContentType.TEXT when lowerCategory.Contains("website") => RetrievalContentType.TEXT_WEBSITE,
|
||||
ContentType.TEXT when lowerCategory.Contains("idea") => RetrievalContentType.TEXT_IDEA,
|
||||
//
|
||||
// Right now, we have to parse the category string along the type enum to
|
||||
// determine the common retrieval content type. In future ERI versions, we
|
||||
// might use the same enum.
|
||||
//
|
||||
|
||||
var lowerCategory = eriContext.Category.ToLowerInvariant();
|
||||
var type = eriContext.Type;
|
||||
return type switch
|
||||
{
|
||||
ContentType.TEXT when lowerCategory.Contains("book") => RetrievalContentType.TEXT_BOOK,
|
||||
ContentType.TEXT when lowerCategory.Contains("paper") => RetrievalContentType.TEXT_PAPER,
|
||||
ContentType.TEXT when lowerCategory.Contains("dictionary") => RetrievalContentType.TEXT_DICTIONARY,
|
||||
ContentType.TEXT when lowerCategory.Contains("encyclopedia") => RetrievalContentType.TEXT_ENCYCLOPEDIA,
|
||||
ContentType.TEXT when lowerCategory.Contains("glossary") => RetrievalContentType.TEXT_GLOSSARY,
|
||||
ContentType.TEXT when lowerCategory.Contains("journal") => RetrievalContentType.TEXT_JOURNAL,
|
||||
ContentType.TEXT when lowerCategory.Contains("magazine") => RetrievalContentType.TEXT_MAGAZINE,
|
||||
ContentType.TEXT when lowerCategory.Contains("newspaper") => RetrievalContentType.TEXT_NEWSPAPER,
|
||||
ContentType.TEXT when lowerCategory.Contains("report") => RetrievalContentType.TEXT_REPORT,
|
||||
ContentType.TEXT when lowerCategory.Contains("review") => RetrievalContentType.TEXT_REVIEW,
|
||||
ContentType.TEXT when lowerCategory.Contains("website") => RetrievalContentType.TEXT_WEBSITE,
|
||||
ContentType.TEXT when lowerCategory.Contains("idea") => RetrievalContentType.TEXT_IDEA,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("business concept") => RetrievalContentType.TEXT_BUSINESS_CONCEPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("concept") => RetrievalContentType.TEXT_CONCEPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("business concept") => RetrievalContentType.TEXT_BUSINESS_CONCEPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("concept") => RetrievalContentType.TEXT_CONCEPT,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("definition") => RetrievalContentType.TEXT_DEFINITION,
|
||||
ContentType.TEXT when lowerCategory.Contains("example") => RetrievalContentType.TEXT_EXAMPLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("quote") => RetrievalContentType.TEXT_QUOTE,
|
||||
ContentType.TEXT when lowerCategory.Contains("article") => RetrievalContentType.TEXT_ARTICLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("chapter") => RetrievalContentType.TEXT_CHAPTER,
|
||||
ContentType.TEXT when lowerCategory.Contains("definition") => RetrievalContentType.TEXT_DEFINITION,
|
||||
ContentType.TEXT when lowerCategory.Contains("example") => RetrievalContentType.TEXT_EXAMPLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("quote") => RetrievalContentType.TEXT_QUOTE,
|
||||
ContentType.TEXT when lowerCategory.Contains("article") => RetrievalContentType.TEXT_ARTICLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("chapter") => RetrievalContentType.TEXT_CHAPTER,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("thesis") => RetrievalContentType.TEXT_THESIS,
|
||||
ContentType.TEXT when lowerCategory.Contains("dissertation") => RetrievalContentType.TEXT_THESIS,
|
||||
ContentType.TEXT when lowerCategory.Contains("thesis") => RetrievalContentType.TEXT_THESIS,
|
||||
ContentType.TEXT when lowerCategory.Contains("dissertation") => RetrievalContentType.TEXT_THESIS,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("draft") => RetrievalContentType.TEXT_DRAFT,
|
||||
ContentType.TEXT when lowerCategory.Contains("script") => RetrievalContentType.TEXT_SCRIPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("transcript") => RetrievalContentType.TEXT_TRANSCRIPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("subtitle") => RetrievalContentType.TEXT_SUBTITLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("caption") => RetrievalContentType.TEXT_CAPTION,
|
||||
ContentType.TEXT when lowerCategory.Contains("dialogue") => RetrievalContentType.TEXT_DIALOGUE,
|
||||
ContentType.TEXT when lowerCategory.Contains("project proposal") => RetrievalContentType.TEXT_PROJECT_PROPOSAL,
|
||||
ContentType.TEXT when lowerCategory.Contains("project plan") => RetrievalContentType.TEXT_PROJECT_PLAN,
|
||||
ContentType.TEXT when lowerCategory.Contains("spreadsheet") => RetrievalContentType.TEXT_SPREADSHEET,
|
||||
ContentType.TEXT when lowerCategory.Contains("draft") => RetrievalContentType.TEXT_DRAFT,
|
||||
ContentType.TEXT when lowerCategory.Contains("script") => RetrievalContentType.TEXT_SCRIPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("transcript") => RetrievalContentType.TEXT_TRANSCRIPT,
|
||||
ContentType.TEXT when lowerCategory.Contains("subtitle") => RetrievalContentType.TEXT_SUBTITLE,
|
||||
ContentType.TEXT when lowerCategory.Contains("caption") => RetrievalContentType.TEXT_CAPTION,
|
||||
ContentType.TEXT when lowerCategory.Contains("dialogue") => RetrievalContentType.TEXT_DIALOGUE,
|
||||
ContentType.TEXT when lowerCategory.Contains("project proposal") => RetrievalContentType.TEXT_PROJECT_PROPOSAL,
|
||||
ContentType.TEXT when lowerCategory.Contains("project plan") => RetrievalContentType.TEXT_PROJECT_PLAN,
|
||||
ContentType.TEXT when lowerCategory.Contains("spreadsheet") => RetrievalContentType.TEXT_SPREADSHEET,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("presentation") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
ContentType.TEXT when lowerCategory.Contains("powerpoint") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
ContentType.TEXT when lowerCategory.Contains("slide") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
ContentType.TEXT when lowerCategory.Contains("presentation") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
ContentType.TEXT when lowerCategory.Contains("powerpoint") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
ContentType.TEXT when lowerCategory.Contains("slide") => RetrievalContentType.TEXT_PRESENTATION,
|
||||
|
||||
ContentType.TEXT when lowerCategory.Contains("meeting minutes") => RetrievalContentType.TEXT_MEETING_MINUTES,
|
||||
ContentType.TEXT when lowerCategory.Contains("email") => RetrievalContentType.TEXT_EMAIL,
|
||||
ContentType.TEXT when lowerCategory.Contains("protocol") => RetrievalContentType.TEXT_PROTOCOL,
|
||||
ContentType.TEXT when lowerCategory.Contains("meeting minutes") => RetrievalContentType.TEXT_MEETING_MINUTES,
|
||||
ContentType.TEXT when lowerCategory.Contains("email") => RetrievalContentType.TEXT_EMAIL,
|
||||
ContentType.TEXT when lowerCategory.Contains("protocol") => RetrievalContentType.TEXT_PROTOCOL,
|
||||
|
||||
ContentType.TEXT => RetrievalContentType.TEXT_DOCUMENT,
|
||||
ContentType.TEXT => RetrievalContentType.TEXT_DOCUMENT,
|
||||
|
||||
|
||||
ContentType.IMAGE when lowerCategory.Contains("photo") => RetrievalContentType.IMAGE_PHOTO,
|
||||
ContentType.IMAGE when lowerCategory.Contains("illustration") => RetrievalContentType.IMAGE_ILLUSTRATION,
|
||||
ContentType.IMAGE when lowerCategory.Contains("diagram") => RetrievalContentType.IMAGE_DIAGRAM,
|
||||
ContentType.IMAGE when lowerCategory.Contains("chart") => RetrievalContentType.IMAGE_CHART,
|
||||
ContentType.IMAGE when lowerCategory.Contains("art") => RetrievalContentType.IMAGE_ART,
|
||||
ContentType.IMAGE when lowerCategory.Contains("drawing") => RetrievalContentType.IMAGE_DRAWING,
|
||||
ContentType.IMAGE when lowerCategory.Contains("painting") => RetrievalContentType.IMAGE_PAINTING,
|
||||
ContentType.IMAGE when lowerCategory.Contains("sketch") => RetrievalContentType.IMAGE_SKETCH,
|
||||
ContentType.IMAGE when lowerCategory.Contains("map") => RetrievalContentType.IMAGE_MAP,
|
||||
ContentType.IMAGE when lowerCategory.Contains("scene") => RetrievalContentType.IMAGE_SCENE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("character") => RetrievalContentType.IMAGE_CHARACTER,
|
||||
ContentType.IMAGE when lowerCategory.Contains("landscape") => RetrievalContentType.IMAGE_LANDSCAPE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("portrait") => RetrievalContentType.IMAGE_PORTRAIT,
|
||||
ContentType.IMAGE when lowerCategory.Contains("poster") => RetrievalContentType.IMAGE_POSTER,
|
||||
ContentType.IMAGE when lowerCategory.Contains("logo") => RetrievalContentType.IMAGE_LOGO,
|
||||
ContentType.IMAGE when lowerCategory.Contains("icon") => RetrievalContentType.IMAGE_ICON,
|
||||
ContentType.IMAGE when lowerCategory.Contains("photo") => RetrievalContentType.IMAGE_PHOTO,
|
||||
ContentType.IMAGE when lowerCategory.Contains("illustration") => RetrievalContentType.IMAGE_ILLUSTRATION,
|
||||
ContentType.IMAGE when lowerCategory.Contains("diagram") => RetrievalContentType.IMAGE_DIAGRAM,
|
||||
ContentType.IMAGE when lowerCategory.Contains("chart") => RetrievalContentType.IMAGE_CHART,
|
||||
ContentType.IMAGE when lowerCategory.Contains("art") => RetrievalContentType.IMAGE_ART,
|
||||
ContentType.IMAGE when lowerCategory.Contains("drawing") => RetrievalContentType.IMAGE_DRAWING,
|
||||
ContentType.IMAGE when lowerCategory.Contains("painting") => RetrievalContentType.IMAGE_PAINTING,
|
||||
ContentType.IMAGE when lowerCategory.Contains("sketch") => RetrievalContentType.IMAGE_SKETCH,
|
||||
ContentType.IMAGE when lowerCategory.Contains("map") => RetrievalContentType.IMAGE_MAP,
|
||||
ContentType.IMAGE when lowerCategory.Contains("scene") => RetrievalContentType.IMAGE_SCENE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("character") => RetrievalContentType.IMAGE_CHARACTER,
|
||||
ContentType.IMAGE when lowerCategory.Contains("landscape") => RetrievalContentType.IMAGE_LANDSCAPE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("portrait") => RetrievalContentType.IMAGE_PORTRAIT,
|
||||
ContentType.IMAGE when lowerCategory.Contains("poster") => RetrievalContentType.IMAGE_POSTER,
|
||||
ContentType.IMAGE when lowerCategory.Contains("logo") => RetrievalContentType.IMAGE_LOGO,
|
||||
ContentType.IMAGE when lowerCategory.Contains("icon") => RetrievalContentType.IMAGE_ICON,
|
||||
|
||||
ContentType.IMAGE when lowerCategory.Contains("satellite") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("EO") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("earth observation") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("satellite") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("EO") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
ContentType.IMAGE when lowerCategory.Contains("earth observation") => RetrievalContentType.IMAGE_SATELLITE_IMAGE,
|
||||
|
||||
ContentType.IMAGE => RetrievalContentType.NOT_SPECIFIED,
|
||||
ContentType.IMAGE => RetrievalContentType.NOT_SPECIFIED,
|
||||
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("speech") => RetrievalContentType.AUDIO_SPEECH,
|
||||
ContentType.AUDIO when lowerCategory.Contains("speech") => RetrievalContentType.AUDIO_SPEECH,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST,
|
||||
ContentType.SPEECH when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST,
|
||||
ContentType.AUDIO when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST,
|
||||
ContentType.SPEECH when lowerCategory.Contains("podcast") => RetrievalContentType.AUDIO_PODCAST,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK,
|
||||
ContentType.SPEECH when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK,
|
||||
ContentType.AUDIO when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK,
|
||||
ContentType.SPEECH when lowerCategory.Contains("book") => RetrievalContentType.AUDIO_BOOK,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW,
|
||||
ContentType.SPEECH when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW,
|
||||
ContentType.AUDIO when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW,
|
||||
ContentType.SPEECH when lowerCategory.Contains("interview") => RetrievalContentType.AUDIO_INTERVIEW,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE,
|
||||
ContentType.SPEECH when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE,
|
||||
ContentType.AUDIO when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE,
|
||||
ContentType.SPEECH when lowerCategory.Contains("lecture") => RetrievalContentType.AUDIO_LECTURE,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK,
|
||||
ContentType.SPEECH when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK,
|
||||
ContentType.AUDIO when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK,
|
||||
ContentType.SPEECH when lowerCategory.Contains("talk") => RetrievalContentType.AUDIO_TALK,
|
||||
|
||||
ContentType.AUDIO when lowerCategory.Contains("song") => RetrievalContentType.AUDIO_SONG,
|
||||
ContentType.AUDIO when lowerCategory.Contains("music") => RetrievalContentType.AUDIO_MUSIC,
|
||||
ContentType.AUDIO when lowerCategory.Contains("sound") => RetrievalContentType.AUDIO_SOUND,
|
||||
ContentType.AUDIO when lowerCategory.Contains("call") => RetrievalContentType.AUDIO_CALL,
|
||||
ContentType.AUDIO when lowerCategory.Contains("voice acting") => RetrievalContentType.AUDIO_VOICE_ACTING,
|
||||
ContentType.AUDIO when lowerCategory.Contains("description") => RetrievalContentType.AUDIO_DESCRIPTION,
|
||||
ContentType.AUDIO when lowerCategory.Contains("guide") => RetrievalContentType.AUDIO_GUIDE,
|
||||
ContentType.AUDIO when lowerCategory.Contains("dialogue") => RetrievalContentType.AUDIO_DIALOGUE,
|
||||
ContentType.AUDIO when lowerCategory.Contains("song") => RetrievalContentType.AUDIO_SONG,
|
||||
ContentType.AUDIO when lowerCategory.Contains("music") => RetrievalContentType.AUDIO_MUSIC,
|
||||
ContentType.AUDIO when lowerCategory.Contains("sound") => RetrievalContentType.AUDIO_SOUND,
|
||||
ContentType.AUDIO when lowerCategory.Contains("call") => RetrievalContentType.AUDIO_CALL,
|
||||
ContentType.AUDIO when lowerCategory.Contains("voice acting") => RetrievalContentType.AUDIO_VOICE_ACTING,
|
||||
ContentType.AUDIO when lowerCategory.Contains("description") => RetrievalContentType.AUDIO_DESCRIPTION,
|
||||
ContentType.AUDIO when lowerCategory.Contains("guide") => RetrievalContentType.AUDIO_GUIDE,
|
||||
ContentType.AUDIO when lowerCategory.Contains("dialogue") => RetrievalContentType.AUDIO_DIALOGUE,
|
||||
|
||||
ContentType.SPEECH => RetrievalContentType.AUDIO_SPEECH,
|
||||
ContentType.AUDIO => RetrievalContentType.NOT_SPECIFIED,
|
||||
ContentType.SPEECH => RetrievalContentType.AUDIO_SPEECH,
|
||||
ContentType.AUDIO => RetrievalContentType.NOT_SPECIFIED,
|
||||
|
||||
|
||||
ContentType.VIDEO when lowerCategory.Contains("movie") => RetrievalContentType.VIDEO_MOVIE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("film") => RetrievalContentType.VIDEO_FILM,
|
||||
ContentType.VIDEO when lowerCategory.Contains("tv show") => RetrievalContentType.VIDEO_TV_SHOW,
|
||||
ContentType.VIDEO when lowerCategory.Contains("series") => RetrievalContentType.VIDEO_SERIES,
|
||||
ContentType.VIDEO when lowerCategory.Contains("episode") => RetrievalContentType.VIDEO_EPISODE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("documentary") => RetrievalContentType.VIDEO_DOCUMENTARY,
|
||||
ContentType.VIDEO when lowerCategory.Contains("tutorial") => RetrievalContentType.VIDEO_TUTORIAL,
|
||||
ContentType.VIDEO when lowerCategory.Contains("lecture") => RetrievalContentType.VIDEO_LECTURE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("webinar") => RetrievalContentType.VIDEO_WEBINAR,
|
||||
ContentType.VIDEO when lowerCategory.Contains("game") => RetrievalContentType.VIDEO_GAME,
|
||||
ContentType.VIDEO when lowerCategory.Contains("animation") => RetrievalContentType.VIDEO_ANIMATION,
|
||||
ContentType.VIDEO when lowerCategory.Contains("cutscene") => RetrievalContentType.VIDEO_CUTSCENE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("trailer") => RetrievalContentType.VIDEO_TRAILER,
|
||||
ContentType.VIDEO when lowerCategory.Contains("advertisement") => RetrievalContentType.VIDEO_ADVERTISEMENT,
|
||||
ContentType.VIDEO when lowerCategory.Contains("movie") => RetrievalContentType.VIDEO_MOVIE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("film") => RetrievalContentType.VIDEO_FILM,
|
||||
ContentType.VIDEO when lowerCategory.Contains("tv show") => RetrievalContentType.VIDEO_TV_SHOW,
|
||||
ContentType.VIDEO when lowerCategory.Contains("series") => RetrievalContentType.VIDEO_SERIES,
|
||||
ContentType.VIDEO when lowerCategory.Contains("episode") => RetrievalContentType.VIDEO_EPISODE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("documentary") => RetrievalContentType.VIDEO_DOCUMENTARY,
|
||||
ContentType.VIDEO when lowerCategory.Contains("tutorial") => RetrievalContentType.VIDEO_TUTORIAL,
|
||||
ContentType.VIDEO when lowerCategory.Contains("lecture") => RetrievalContentType.VIDEO_LECTURE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("webinar") => RetrievalContentType.VIDEO_WEBINAR,
|
||||
ContentType.VIDEO when lowerCategory.Contains("game") => RetrievalContentType.VIDEO_GAME,
|
||||
ContentType.VIDEO when lowerCategory.Contains("animation") => RetrievalContentType.VIDEO_ANIMATION,
|
||||
ContentType.VIDEO when lowerCategory.Contains("cutscene") => RetrievalContentType.VIDEO_CUTSCENE,
|
||||
ContentType.VIDEO when lowerCategory.Contains("trailer") => RetrievalContentType.VIDEO_TRAILER,
|
||||
ContentType.VIDEO when lowerCategory.Contains("advertisement") => RetrievalContentType.VIDEO_ADVERTISEMENT,
|
||||
|
||||
ContentType.VIDEO => RetrievalContentType.NOT_SPECIFIED,
|
||||
ContentType.VIDEO => RetrievalContentType.NOT_SPECIFIED,
|
||||
|
||||
ContentType.NONE => RetrievalContentType.NOT_SPECIFIED,
|
||||
ContentType.NONE => RetrievalContentType.NOT_SPECIFIED,
|
||||
|
||||
_ => RetrievalContentType.UNKNOWN,
|
||||
};
|
||||
_ => RetrievalContentType.UNKNOWN,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,165 +7,186 @@ public static class SourcePassageExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourcePassageExtensions).Namespace, nameof(SourcePassageExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// Whether the passage comes from a transcript, e.g., of an audio or a video file.
|
||||
/// </summary>
|
||||
/// <param name="passage">The passage.</param>
|
||||
/// <returns>True for a transcript.</returns>
|
||||
public static bool IsTranscript(this SourcePassage passage) => passage.Type is RetrievalContentType.TEXT_TRANSCRIPT;
|
||||
|
||||
/// <summary>
|
||||
/// What sort of content the passage is, as the provenance store files it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Told by what the content is rather than by where it came from: a mail is a mail, whether a
|
||||
/// mailbox data source or a tool handed it over.
|
||||
/// </remarks>
|
||||
/// <param name="passage">The passage.</param>
|
||||
/// <returns>The kind of its entry.</returns>
|
||||
public static ProvenanceKind GetProvenanceKind(this SourcePassage passage) => passage.Type switch
|
||||
extension(SourcePassage passage)
|
||||
{
|
||||
RetrievalContentType.TEXT_EMAIL => ProvenanceKind.MAIL,
|
||||
RetrievalContentType.TEXT_WEBSITE => ProvenanceKind.WEB_PAGE,
|
||||
_ => ProvenanceKind.DATA_SOURCE_PASSAGE,
|
||||
};
|
||||
/// <summary>
|
||||
/// Whether the passage comes from a transcript, e.g., of an audio or a video file.
|
||||
/// </summary>
|
||||
/// <returns>True for a transcript.</returns>
|
||||
public bool IsTranscript() => passage.Type is RetrievalContentType.TEXT_TRANSCRIPT;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the passage of a web page which a tool read for the AI, so that the user can read under
|
||||
/// the source of the page what the AI received from it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Listed under the source by its address, see GetPassagesOf, and named by its title in the
|
||||
/// dialog of its passages. A page says nothing about pages or lines, so its source keeps the
|
||||
/// title the tool gave it, see NamePassagesInTitles.
|
||||
/// </remarks>
|
||||
/// <param name="source">The source of the page, as the tool lists it.</param>
|
||||
/// <param name="content">The content of the page as the model received it: filtered for prompt injections, and cut where the tool cut it.</param>
|
||||
/// <returns>The passage of the page.</returns>
|
||||
public static SourcePassage ToWebPagePassage(this Source source, string content) => new()
|
||||
/// <summary>
|
||||
/// What sort of content the passage is, as the provenance store files it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Told by what the content is rather than by where it came from: a mail is a mail, whether a
|
||||
/// mailbox data source or a tool handed it over.
|
||||
/// </remarks>
|
||||
/// <returns>The kind of its entry.</returns>
|
||||
public ProvenanceKind GetProvenanceKind() => passage.Type switch
|
||||
{
|
||||
RetrievalContentType.TEXT_EMAIL => ProvenanceKind.MAIL,
|
||||
RetrievalContentType.TEXT_WEBSITE => ProvenanceKind.WEB_PAGE,
|
||||
_ => ProvenanceKind.DATA_SOURCE_PASSAGE,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the passage holds texts which belong into the provenance store, see SourcePassage.ProvenanceId.
|
||||
/// </summary>
|
||||
/// <returns>False once the texts are stored, and for a passage without any, such as an image.</returns>
|
||||
public bool HoldsTextsToStore() => passage.ProvenanceId is null && (passage.Text.Length > 0 || passage.SurroundingContent.Count > 0);
|
||||
|
||||
/// <summary>
|
||||
/// Names where the passage is in its document, e.g., "Page 4", "Pages 4–5", "Budget: rows 3–9", "Lines 40–85", or "Part 3".
|
||||
/// </summary>
|
||||
/// <returns>The location, or an empty string when the data source does not say.</returns>
|
||||
public string GetLocationLabel() => GetLocationLabel([passage]);
|
||||
|
||||
/// <summary>
|
||||
/// Where the passage is in its document, as numbers only, e.g., its pages 4–5, its lines 40–85, or its part 3.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Orders the passages of one document, in the order of the document. The passages of the
|
||||
/// sheets of a spreadsheet give their parts, since rows start anew in every sheet.
|
||||
/// </remarks>
|
||||
/// <returns>The numbers, or null when the data source does not say.</returns>
|
||||
public LocationRange? GetLocationRange() => GetLocation([passage])?.Ranges[0];
|
||||
}
|
||||
|
||||
/// <param name="source">The source of the page or the mail, as the tool lists it.</param>
|
||||
extension(Source source)
|
||||
{
|
||||
SourceUrl = source.URL,
|
||||
DataSourceName = source.Title,
|
||||
Path = source.URL,
|
||||
Category = RetrievalContentCategory.TEXT,
|
||||
Type = RetrievalContentType.TEXT_WEBSITE,
|
||||
Text = content,
|
||||
};
|
||||
/// <summary>
|
||||
/// Makes the passage of a web page which a tool read for the AI, so that the user can read under
|
||||
/// the source of the page what the AI received from it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Listed under the source by its address, see GetPassagesOf, and named by its title in the
|
||||
/// dialog of its passages. A page says nothing about pages or lines, so its source keeps the
|
||||
/// title the tool gave it, see NamePassagesInTitles.
|
||||
/// </remarks>
|
||||
/// <param name="content">The content of the page as the model received it: filtered for prompt injections, and cut where the tool cut it.</param>
|
||||
/// <returns>The passage of the page.</returns>
|
||||
public SourcePassage ToWebPagePassage(string content) => new()
|
||||
{
|
||||
SourceUrl = source.URL,
|
||||
DataSourceName = source.Title,
|
||||
Path = source.URL,
|
||||
Category = RetrievalContentCategory.TEXT,
|
||||
Type = RetrievalContentType.TEXT_WEBSITE,
|
||||
Text = content,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Makes the passage of a mail which a tool handed to the AI, so that the user can read under
|
||||
/// the source of the mail what the AI received of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Listed and named like the passage of a web page, see ToWebPagePassage. A mail has no path:
|
||||
/// its address names it for AI Studio only, so there is nothing to copy or to open.
|
||||
/// </remarks>
|
||||
/// <param name="source">The source of the mail, as the tool lists it.</param>
|
||||
/// <param name="content">What the model received of the mail, filtered for prompt injections.</param>
|
||||
/// <returns>The passage of the mail.</returns>
|
||||
public static SourcePassage ToMailPassage(this Source source, string content) => new()
|
||||
{
|
||||
SourceUrl = source.URL,
|
||||
DataSourceName = source.Title,
|
||||
Category = RetrievalContentCategory.TEXT,
|
||||
Type = RetrievalContentType.TEXT_EMAIL,
|
||||
Text = content,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the passage holds texts which belong into the provenance store, see SourcePassage.ProvenanceId.
|
||||
/// </summary>
|
||||
/// <param name="passage">The passage.</param>
|
||||
/// <returns>False once the texts are stored, and for a passage without any, such as an image.</returns>
|
||||
public static bool HoldsTextsToStore(this SourcePassage passage) => passage.ProvenanceId is null && (passage.Text.Length > 0 || passage.SurroundingContent.Count > 0);
|
||||
/// <summary>
|
||||
/// Makes the passage of a mail which a tool handed to the AI, so that the user can read under
|
||||
/// the source of the mail what the AI received of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Listed and named like the passage of a web page, see ToWebPagePassage. A mail has no path:
|
||||
/// its address names it for AI Studio only, so there is nothing to copy or to open.
|
||||
/// </remarks>
|
||||
/// <param name="content">What the model received of the mail, filtered for prompt injections.</param>
|
||||
/// <returns>The passage of the mail.</returns>
|
||||
public SourcePassage ToMailPassage(string content) => new()
|
||||
{
|
||||
SourceUrl = source.URL,
|
||||
DataSourceName = source.Title,
|
||||
Category = RetrievalContentCategory.TEXT,
|
||||
Type = RetrievalContentType.TEXT_EMAIL,
|
||||
Text = content,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What stands between the sheets of a spreadsheet in a location, e.g., "Budget: rows 3–9 · Summary: row 2".
|
||||
/// </summary>
|
||||
private const string SHEET_SEPARATOR = " · ";
|
||||
|
||||
/// <summary>
|
||||
/// Names where the passage is in its document, e.g., "Page 4", "Pages 4–5", "Budget: rows 3–9", "Lines 40–85", or "Part 3".
|
||||
/// </summary>
|
||||
/// <param name="passage">The passage.</param>
|
||||
/// <returns>The location, or an empty string when the data source does not say.</returns>
|
||||
public static string GetLocationLabel(this SourcePassage passage) => GetLocationLabel([passage]);
|
||||
|
||||
/// <summary>
|
||||
/// Where the passage is in its document, as numbers only, e.g., its pages 4–5, its lines 40–85, or its part 3.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Orders the passages of one document, in the order of the document. The passages of the
|
||||
/// sheets of a spreadsheet give their parts, since rows start anew in every sheet.
|
||||
/// </remarks>
|
||||
/// <param name="passage">The passage.</param>
|
||||
/// <returns>The numbers, or null when the data source does not say.</returns>
|
||||
public static LocationRange? GetLocationRange(this SourcePassage passage) => GetLocation([passage])?.Ranges[0];
|
||||
|
||||
/// <summary>
|
||||
/// Names where several passages are in their document, e.g., "Pages 1, 11", "Budget: rows 3–9 · Summary: row 2", "Lines 40–85, 120" or "Parts 2, 5".
|
||||
/// </summary>
|
||||
/// <param name="passages">The passages.</param>
|
||||
/// <returns>The location, or an empty string when the data source does not say.</returns>
|
||||
public static string GetLocationLabel(this IEnumerable<SourcePassage> passages)
|
||||
extension(IEnumerable<SourcePassage> passages)
|
||||
{
|
||||
var passagesOfDocument = passages.ToList();
|
||||
if (passagesOfDocument.All(passage => passage.PageNumber is not > 0) && GetSheetsLabel(passagesOfDocument) is { } sheets)
|
||||
return sheets;
|
||||
/// <summary>
|
||||
/// Names where several passages are in their document, e.g., "Pages 1, 11", "Budget: rows 3–9 · Summary: row 2", "Lines 40–85, 120" or "Parts 2, 5".
|
||||
/// </summary>
|
||||
/// <returns>The location, or an empty string when the data source does not say.</returns>
|
||||
public string GetLocationLabel()
|
||||
{
|
||||
var passagesOfDocument = passages.ToList();
|
||||
if (passagesOfDocument.All(passage => passage.PageNumber is not > 0) && GetSheetsLabel(passagesOfDocument) is { } sheets)
|
||||
return sheets;
|
||||
|
||||
return passagesOfDocument.GetLocation()?.ToString() ?? string.Empty;
|
||||
}
|
||||
return passagesOfDocument.GetLocation()?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds where several passages are in their document, in parts, so that each page can be shown
|
||||
/// as a link.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A document with pages is named by its pages: they are what the user finds again in the
|
||||
/// document, and what a program can open it at. A table without sheets, such as a CSV file, is
|
||||
/// named by its rows, and a text by its lines. Rows and lines are nothing a program can open a
|
||||
/// document at, so they become no links. A document which says none of them is named by the
|
||||
/// parts it was split into for the search. Either way, the numbers follow the order of the
|
||||
/// document, each once: two passages on the same page name that page once. An end which is not
|
||||
/// after the start names no range.<br/><br/>
|
||||
/// The rows of a spreadsheet start anew in every sheet, so they need the name of their sheet
|
||||
/// and are named by GetLocationLabel, sheet by sheet. Here, its passages give their parts.
|
||||
/// </remarks>
|
||||
/// <param name="passages">The passages.</param>
|
||||
/// <returns>The location, or null when the data source says nothing about it.</returns>
|
||||
public static PassageLocation? GetLocation(this IEnumerable<SourcePassage> passages)
|
||||
{
|
||||
var passagesOfDocument = passages.ToList();
|
||||
var pages = passagesOfDocument
|
||||
.Where(passage => passage.PageNumber is > 0)
|
||||
.Select(passage => RangeOf(passage.PageNumber!.Value, passage.EndPageNumber))
|
||||
.ToList();
|
||||
/// <summary>
|
||||
/// Finds where several passages are in their document, in parts, so that each page can be shown
|
||||
/// as a link.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A document with pages is named by its pages: they are what the user finds again in the
|
||||
/// document, and what a program can open it at. A table without sheets, such as a CSV file, is
|
||||
/// named by its rows, and a text by its lines. Rows and lines are nothing a program can open a
|
||||
/// document at, so they become no links. A document which says none of them is named by the
|
||||
/// parts it was split into for the search. Either way, the numbers follow the order of the
|
||||
/// document, each once: two passages on the same page name that page once. An end which is not
|
||||
/// after the start names no range.<br/><br/>
|
||||
/// The rows of a spreadsheet start anew in every sheet, so they need the name of their sheet
|
||||
/// and are named by GetLocationLabel, sheet by sheet. Here, its passages give their parts.
|
||||
/// </remarks>
|
||||
/// <returns>The location, or null when the data source says nothing about it.</returns>
|
||||
public PassageLocation? GetLocation()
|
||||
{
|
||||
var passagesOfDocument = passages.ToList();
|
||||
var pages = passagesOfDocument
|
||||
.Where(passage => passage.PageNumber is > 0)
|
||||
.Select(passage => RangeOf(passage.PageNumber!.Value, passage.EndPageNumber))
|
||||
.ToList();
|
||||
|
||||
if (pages.Count > 0)
|
||||
return CreateLocation(pages, TB("Page {0}"), TB("Pages {0}"), arePages: true);
|
||||
if (pages.Count > 0)
|
||||
return CreateLocation(pages, TB("Page {0}"), TB("Pages {0}"), arePages: true);
|
||||
|
||||
var rows = passagesOfDocument
|
||||
.Where(passage => passage is { StartRow: > 0, SheetName: null or "" })
|
||||
.Select(passage => RangeOf(passage.StartRow!.Value, passage.EndRow))
|
||||
.ToList();
|
||||
var rows = passagesOfDocument
|
||||
.Where(passage => passage is { StartRow: > 0, SheetName: null or "" })
|
||||
.Select(passage => RangeOf(passage.StartRow!.Value, passage.EndRow))
|
||||
.ToList();
|
||||
|
||||
if (rows.Count > 0)
|
||||
return CreateLocation(rows, TB("Row {0}"), TB("Rows {0}"), arePages: false);
|
||||
if (rows.Count > 0)
|
||||
return CreateLocation(rows, TB("Row {0}"), TB("Rows {0}"), arePages: false);
|
||||
|
||||
var lines = passagesOfDocument
|
||||
.Where(passage => passage.StartLine is > 0)
|
||||
.Select(passage => RangeOf(passage.StartLine!.Value, passage.EndLine))
|
||||
.ToList();
|
||||
var lines = passagesOfDocument
|
||||
.Where(passage => passage.StartLine is > 0)
|
||||
.Select(passage => RangeOf(passage.StartLine!.Value, passage.EndLine))
|
||||
.ToList();
|
||||
|
||||
if (lines.Count > 0)
|
||||
return CreateLocation(lines, TB("Line {0}"), TB("Lines {0}"), arePages: false);
|
||||
if (lines.Count > 0)
|
||||
return CreateLocation(lines, TB("Line {0}"), TB("Lines {0}"), arePages: false);
|
||||
|
||||
var parts = passagesOfDocument
|
||||
.Where(passage => passage.ChunkNumber is > 0)
|
||||
.Select(passage => new LocationRange(passage.ChunkNumber!.Value, passage.ChunkNumber!.Value))
|
||||
.ToList();
|
||||
var parts = passagesOfDocument
|
||||
.Where(passage => passage.ChunkNumber is > 0)
|
||||
.Select(passage => new LocationRange(passage.ChunkNumber!.Value, passage.ChunkNumber!.Value))
|
||||
.ToList();
|
||||
|
||||
return parts.Count > 0 ? CreateLocation(parts, TB("Part {0}"), TB("Parts {0}"), arePages: false) : null;
|
||||
return parts.Count > 0 ? CreateLocation(parts, TB("Part {0}"), TB("Parts {0}"), arePages: false) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the passages listed under a source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list of sources keeps one entry per document, whichever page its link names, see
|
||||
/// SourceExtensions.MergeSources. The passages are matched the same way, so the entry of a
|
||||
/// document shows the passages of all its pages, and every passage shows up under its source.
|
||||
/// </remarks>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>The passages of the source, in their order; empty when it has none.</returns>
|
||||
public IReadOnlyList<SourcePassage> GetPassagesOf(ISource source) => passages.SortBySource().GetPassagesOf(source);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts passages by the source they are listed under, to find the passages of many sources.
|
||||
/// </summary>
|
||||
/// <returns>The passages by the identity of their source, see SourceExtensions.GetSourceIdentity.</returns>
|
||||
public ILookup<string, SourcePassage> SortBySource() => passages.ToLookup(passage => SourceExtensions.GetSourceIdentity(passage.SourceUrl), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -226,93 +247,82 @@ public static class SourcePassageExtensions
|
||||
: new PassageLocation(format[..placeholderAt], orderedRanges, format[(placeholderAt + PLACEHOLDER.Length)..], arePages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names in the title of each source where its passages are, e.g., "handbook.pdf (Pages 1, 11)".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list of sources keeps one entry per document, see SourceExtensions.MergeSources, and its
|
||||
/// title came from the passage found first, e.g., "handbook.pdf (Page 1)". A second passage on
|
||||
/// page 11 would go unmentioned, so the title is made anew from all passages of the source. A
|
||||
/// spreadsheet is named by its sheets and rows the same way, a text file by its lines, see
|
||||
/// GetLocationLabel. Passages whose data source
|
||||
/// says neither, e.g., an external one, are counted instead as soon as there are several of
|
||||
/// them. Otherwise, a source keeps the title its data source gave it, and so does every source
|
||||
/// without passages, such as a web page.
|
||||
/// </remarks>
|
||||
/// <param name="sources">The sources of an answer.</param>
|
||||
/// <param name="passages">The passages of the same answer.</param>
|
||||
public static void NamePassagesInTitles(this IList<Source> sources, IReadOnlyCollection<SourcePassage> passages)
|
||||
extension(IList<Source> sources)
|
||||
{
|
||||
if (passages.Count is 0)
|
||||
return;
|
||||
|
||||
var passagesBySource = passages.SortBySource();
|
||||
for (var index = 0; index < sources.Count; index++)
|
||||
/// <summary>
|
||||
/// Names in the title of each source where its passages are, e.g., "handbook.pdf (Pages 1, 11)".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list of sources keeps one entry per document, see SourceExtensions.MergeSources, and its
|
||||
/// title came from the passage found first, e.g., "handbook.pdf (Page 1)". A second passage on
|
||||
/// page 11 would go unmentioned, so the title is made anew from all passages of the source. A
|
||||
/// spreadsheet is named by its sheets and rows the same way, a text file by its lines, see
|
||||
/// GetLocationLabel. Passages whose data source
|
||||
/// says neither, e.g., an external one, are counted instead as soon as there are several of
|
||||
/// them. Otherwise, a source keeps the title its data source gave it, and so does every source
|
||||
/// without passages, such as a web page.
|
||||
/// </remarks>
|
||||
/// <param name="passages">The passages of the same answer.</param>
|
||||
public void NamePassagesInTitles(IReadOnlyCollection<SourcePassage> passages)
|
||||
{
|
||||
var passagesOfSource = passagesBySource.GetPassagesOf(sources[index]);
|
||||
if (passagesOfSource.Count is 0 || string.IsNullOrWhiteSpace(passagesOfSource[0].DataSourceName))
|
||||
continue;
|
||||
if (passages.Count is 0)
|
||||
return;
|
||||
|
||||
var location = passagesOfSource.GetLocationLabel();
|
||||
if (location.Length is 0 && passagesOfSource.Count > 1)
|
||||
location = string.Format(TB("{0} passages"), passagesOfSource.Count.CompactCount());
|
||||
var passagesBySource = passages.SortBySource();
|
||||
for (var index = 0; index < sources.Count; index++)
|
||||
{
|
||||
var passagesOfSource = passagesBySource.GetPassagesOf(sources[index]);
|
||||
if (passagesOfSource.Count is 0 || string.IsNullOrWhiteSpace(passagesOfSource[0].DataSourceName))
|
||||
continue;
|
||||
|
||||
if (location.Length is 0)
|
||||
continue;
|
||||
var location = passagesOfSource.GetLocationLabel();
|
||||
if (location.Length is 0 && passagesOfSource.Count > 1)
|
||||
location = string.Format(TB("{0} passages"), passagesOfSource.Count.CompactCount());
|
||||
|
||||
sources[index] = sources[index] with { Title = $"{passagesOfSource[0].DataSourceName} ({location})" };
|
||||
if (location.Length is 0)
|
||||
continue;
|
||||
|
||||
sources[index] = sources[index] with { Title = $"{passagesOfSource[0].DataSourceName} ({location})" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges added passages into an existing list of passages, leaving out those it holds already.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A passage is known when one with the same address and the same text is, e.g., the same chunk
|
||||
/// found by two searches for one answer. Unlike the sources, the address counts with its page:
|
||||
/// the same sentence on two pages of a document is two passages. A stored passage is told
|
||||
/// apart by its entry, since its text lies in the provenance store.
|
||||
/// </remarks>
|
||||
/// <param name="passages">The existing list of passages to merge into.</param>
|
||||
/// <param name="addedPassages">The passages to add.</param>
|
||||
public static void MergePassages(this IList<SourcePassage> passages, IEnumerable<SourcePassage> addedPassages)
|
||||
extension(IList<SourcePassage> passages)
|
||||
{
|
||||
var knownPassages = passages
|
||||
.Select(passage => (passage.SourceUrl, passage.Text, passage.ProvenanceId))
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var addedPassage in addedPassages)
|
||||
/// <summary>
|
||||
/// Merges added passages into an existing list of passages, leaving out those it holds already.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A passage is known when one with the same address and the same text is, e.g., the same chunk
|
||||
/// found by two searches for one answer. Unlike the sources, the address counts with its page:
|
||||
/// the same sentence on two pages of a document is two passages. A stored passage is told
|
||||
/// apart by its entry, since its text lies in the provenance store.
|
||||
/// </remarks>
|
||||
/// <param name="addedPassages">The passages to add.</param>
|
||||
public void MergePassages(IEnumerable<SourcePassage> addedPassages)
|
||||
{
|
||||
if (knownPassages.Add((addedPassage.SourceUrl, addedPassage.Text, addedPassage.ProvenanceId)))
|
||||
passages.Add(addedPassage);
|
||||
var knownPassages = passages
|
||||
.Select(passage => (passage.SourceUrl, passage.Text, passage.ProvenanceId))
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var addedPassage in addedPassages)
|
||||
{
|
||||
if (knownPassages.Add((addedPassage.SourceUrl, addedPassage.Text, addedPassage.ProvenanceId)))
|
||||
passages.Add(addedPassage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the passages listed under a source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list of sources keeps one entry per document, whichever page its link names, see
|
||||
/// SourceExtensions.MergeSources. The passages are matched the same way, so the entry of a
|
||||
/// document shows the passages of all its pages, and every passage shows up under its source.
|
||||
/// </remarks>
|
||||
/// <param name="passages">The passages of an answer.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>The passages of the source, in their order; empty when it has none.</returns>
|
||||
public static IReadOnlyList<SourcePassage> GetPassagesOf(this IEnumerable<SourcePassage> passages, ISource source) => passages.SortBySource().GetPassagesOf(source);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts passages by the source they are listed under, to find the passages of many sources.
|
||||
/// </summary>
|
||||
/// <param name="passages">The passages of an answer.</param>
|
||||
/// <returns>The passages by the identity of their source, see SourceExtensions.GetSourceIdentity.</returns>
|
||||
public static ILookup<string, SourcePassage> SortBySource(this IEnumerable<SourcePassage> passages) => passages.ToLookup(passage => SourceExtensions.GetSourceIdentity(passage.SourceUrl), StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the passages listed under a source, among passages sorted by their sources.
|
||||
/// </summary>
|
||||
/// <param name="passagesBySource">The passages of an answer, see SortBySource.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>The passages of the source, in their order; empty when it has none.</returns>
|
||||
public static IReadOnlyList<SourcePassage> GetPassagesOf(this ILookup<string, SourcePassage> passagesBySource, ISource source) => [..passagesBySource[SourceExtensions.GetSourceIdentity(source.URL)]];
|
||||
extension(ILookup<string, SourcePassage> passagesBySource)
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds the passages listed under a source, among passages sorted by their sources.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>The passages of the source, in their order; empty when it has none.</returns>
|
||||
public IReadOnlyList<SourcePassage> GetPassagesOf(ISource source) => [..passagesBySource[SourceExtensions.GetSourceIdentity(source.URL)]];
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,16 @@ public static class ModelImageErrorCodeExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelImageErrorCodeExtensions).Namespace, nameof(ModelImageErrorCodeExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why an image was not sent to the AI.
|
||||
/// </summary>
|
||||
/// <param name="code">Why the image could not be prepared.</param>
|
||||
/// <param name="fileName">The name of the image file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
public static string ToUserMessage(this ModelImageErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName);
|
||||
extension(ModelImageErrorCode code)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why an image was not sent to the AI.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the image file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
public string ToUserMessage(string fileName) => string.Format(ToUserMessageFormat(code), fileName);
|
||||
}
|
||||
|
||||
private static string ToUserMessageFormat(ModelImageErrorCode code) => code switch
|
||||
{
|
||||
|
||||
@@ -2,23 +2,26 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class SecretStoreTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the prefix string associated with the SecretStoreType.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
|
||||
/// </remarks>
|
||||
/// <param name="type">The SecretStoreType enum value.</param>
|
||||
/// <returns>The corresponding prefix string.</returns>
|
||||
public static string Prefix(this SecretStoreType type) => type switch
|
||||
extension(SecretStoreType type)
|
||||
{
|
||||
SecretStoreType.LLM_PROVIDER => "provider",
|
||||
SecretStoreType.EMBEDDING_PROVIDER => "embedding",
|
||||
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
||||
SecretStoreType.IMAGE_PROVIDER => "image",
|
||||
SecretStoreType.DATA_SOURCE => "data-source",
|
||||
SecretStoreType.TOOL_SETTINGS => "tool",
|
||||
/// <summary>
|
||||
/// Gets the prefix string associated with the SecretStoreType.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
|
||||
/// </remarks>
|
||||
/// <returns>The corresponding prefix string.</returns>
|
||||
public string Prefix() => type switch
|
||||
{
|
||||
SecretStoreType.LLM_PROVIDER => "provider",
|
||||
SecretStoreType.EMBEDDING_PROVIDER => "embedding",
|
||||
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
||||
SecretStoreType.IMAGE_PROVIDER => "image",
|
||||
SecretStoreType.DATA_SOURCE => "data-source",
|
||||
SecretStoreType.TOOL_SETTINGS => "tool",
|
||||
|
||||
_ => "provider",
|
||||
};
|
||||
_ => "provider",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,21 @@ public static class PromptInjectionFindingCategoryExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PromptInjectionFindingCategoryExtensions).Namespace, nameof(PromptInjectionFindingCategoryExtensions));
|
||||
|
||||
public static string GetDisplayName(this PromptInjectionFindingCategory category) => category switch
|
||||
extension(PromptInjectionFindingCategory category)
|
||||
{
|
||||
PromptInjectionFindingCategory.OVERRIDE => TB("Attempt to override instructions"),
|
||||
PromptInjectionFindingCategory.ROLE_OVERRIDE => TB("Attempt to change the AI's role"),
|
||||
PromptInjectionFindingCategory.EXFILTRATION => TB("Attempt to expose protected data"),
|
||||
PromptInjectionFindingCategory.JAILBREAK => TB("Attempt to bypass safeguards"),
|
||||
PromptInjectionFindingCategory.AGENT_MANIPULATION => TB("Attempt to manipulate an agent"),
|
||||
PromptInjectionFindingCategory.DELIMITER_EVASION => TB("Hidden instructions using delimiters"),
|
||||
PromptInjectionFindingCategory.MARKUP_EVASION => TB("Hidden instructions using markup"),
|
||||
PromptInjectionFindingCategory.ENCODING_EVASION => TB("Hidden instructions using encoding"),
|
||||
PromptInjectionFindingCategory.PERSISTENCE => TB("Persistent or delayed instruction"),
|
||||
PromptInjectionFindingCategory.EVASION => TB("Obfuscated instruction"),
|
||||
_ => TB("Unknown"),
|
||||
};
|
||||
public string GetDisplayName() => category switch
|
||||
{
|
||||
PromptInjectionFindingCategory.OVERRIDE => TB("Attempt to override instructions"),
|
||||
PromptInjectionFindingCategory.ROLE_OVERRIDE => TB("Attempt to change the AI's role"),
|
||||
PromptInjectionFindingCategory.EXFILTRATION => TB("Attempt to expose protected data"),
|
||||
PromptInjectionFindingCategory.JAILBREAK => TB("Attempt to bypass safeguards"),
|
||||
PromptInjectionFindingCategory.AGENT_MANIPULATION => TB("Attempt to manipulate an agent"),
|
||||
PromptInjectionFindingCategory.DELIMITER_EVASION => TB("Hidden instructions using delimiters"),
|
||||
PromptInjectionFindingCategory.MARKUP_EVASION => TB("Hidden instructions using markup"),
|
||||
PromptInjectionFindingCategory.ENCODING_EVASION => TB("Hidden instructions using encoding"),
|
||||
PromptInjectionFindingCategory.PERSISTENCE => TB("Persistent or delayed instruction"),
|
||||
PromptInjectionFindingCategory.EVASION => TB("Obfuscated instruction"),
|
||||
_ => TB("Unknown"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,18 @@ public static class PromptInjectionSourceKindExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PromptInjectionSourceKindExtensions).Namespace, nameof(PromptInjectionSourceKindExtensions));
|
||||
|
||||
public static string GetDisplayName(this PromptInjectionSourceKind kind) => kind switch
|
||||
extension(PromptInjectionSourceKind kind)
|
||||
{
|
||||
PromptInjectionSourceKind.WEB_CONTENT => TB("Web content"),
|
||||
PromptInjectionSourceKind.FILE_CONTENT => TB("File content"),
|
||||
PromptInjectionSourceKind.CHAT_ATTACHMENT => TB("Chat attachment"),
|
||||
PromptInjectionSourceKind.RETRIEVAL_CONTEXT => TB("Retrieved context"),
|
||||
PromptInjectionSourceKind.DATA_SOURCE_DESCRIPTION => TB("Data source description"),
|
||||
PromptInjectionSourceKind.MAIL_CONTENT => TB("Mail content"),
|
||||
PromptInjectionSourceKind.MODEL_ANSWER => TB("AI answer"),
|
||||
_ => TB("Unknown"),
|
||||
};
|
||||
public string GetDisplayName() => kind switch
|
||||
{
|
||||
PromptInjectionSourceKind.WEB_CONTENT => TB("Web content"),
|
||||
PromptInjectionSourceKind.FILE_CONTENT => TB("File content"),
|
||||
PromptInjectionSourceKind.CHAT_ATTACHMENT => TB("Chat attachment"),
|
||||
PromptInjectionSourceKind.RETRIEVAL_CONTEXT => TB("Retrieved context"),
|
||||
PromptInjectionSourceKind.DATA_SOURCE_DESCRIPTION => TB("Data source description"),
|
||||
PromptInjectionSourceKind.MAIL_CONTENT => TB("Mail content"),
|
||||
PromptInjectionSourceKind.MODEL_ANSWER => TB("AI answer"),
|
||||
_ => TB("Unknown"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,65 +17,67 @@ public static class SourceDocumentLocationExtensions
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourceDocumentLocationExtensions).Namespace, nameof(SourceDocumentLocationExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// Opens the document in the program the system uses for it, at its page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whether the program can be sent to a page is the runtime's business, and it says afterwards
|
||||
/// whether it managed to. Nothing is shown about that: the document is open, and the page is
|
||||
/// named where the user asked to open it.
|
||||
/// </remarks>
|
||||
/// <param name="document">The document to open, and the page to show.</param>
|
||||
/// <param name="rustService">The runtime, which opens the document.</param>
|
||||
public static async Task OpenAsync(this SourceDocumentLocation document, RustService rustService)
|
||||
/// <param name="document">The document to show, and the page to show it at.</param>
|
||||
extension(SourceDocumentLocation document)
|
||||
{
|
||||
OpenDocumentResponse response;
|
||||
try
|
||||
/// <summary>
|
||||
/// Opens the document in the program the system uses for it, at its page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whether the program can be sent to a page is the runtime's business, and it says afterwards
|
||||
/// whether it managed to. Nothing is shown about that: the document is open, and the page is
|
||||
/// named where the user asked to open it.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The runtime, which opens the document.</param>
|
||||
public async Task OpenAsync(RustService rustService)
|
||||
{
|
||||
response = await rustService.TryOpenDocumentInSystemViewer(document.Path, document.PageNumber);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogWarning(e, "Could not open a source document.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, TB("Could not open the document.")));
|
||||
return;
|
||||
OpenDocumentResponse response;
|
||||
try
|
||||
{
|
||||
response = await rustService.TryOpenDocumentInSystemViewer(document.Path, document.PageNumber);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogWarning(e, "Could not open a source document.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, TB("Could not open the document.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Success)
|
||||
return;
|
||||
|
||||
var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue;
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("Could not open the document: {0}"), issue)));
|
||||
}
|
||||
|
||||
if (response.Success)
|
||||
return;
|
||||
|
||||
var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue;
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("Could not open the document: {0}"), issue)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the file browser of the system and selects the document in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The second way out: a document which the system opens in the wrong program, or which the user
|
||||
/// wants to move or send on instead of read, is reached from here without being opened. This is
|
||||
/// the same way out the embeddings page offers for a file it could not read.
|
||||
/// </remarks>
|
||||
/// <param name="document">The document to show.</param>
|
||||
/// <param name="rustService">The runtime, which opens the file browser.</param>
|
||||
public static async Task ShowInFileManagerAsync(this SourceDocumentLocation document, RustService rustService)
|
||||
{
|
||||
OpenPathResponse response;
|
||||
try
|
||||
/// <summary>
|
||||
/// Opens the file browser of the system and selects the document in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The second way out: a document which the system opens in the wrong program, or which the user
|
||||
/// wants to move or send on instead of read, is reached from here without being opened. This is
|
||||
/// the same way out the embeddings page offers for a file it could not read.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The runtime, which opens the file browser.</param>
|
||||
public async Task ShowInFileManagerAsync(RustService rustService)
|
||||
{
|
||||
response = await rustService.TryOpenPathInRuntimeFileManager(document.Path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogWarning(e, "Could not show a source document in the file manager.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, TB("Could not open the file location.")));
|
||||
return;
|
||||
}
|
||||
OpenPathResponse response;
|
||||
try
|
||||
{
|
||||
response = await rustService.TryOpenPathInRuntimeFileManager(document.Path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogWarning(e, "Could not show a source document in the file manager.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, TB("Could not open the file location.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Success)
|
||||
return;
|
||||
if (response.Success)
|
||||
return;
|
||||
|
||||
var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue;
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(TB("Could not open the file location: {0}"), issue)));
|
||||
var issue = string.IsNullOrWhiteSpace(response.Issue) ? TB("Unknown error") : response.Issue;
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(TB("Could not open the file location: {0}"), issue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,58 @@ public static partial class SourceExtensions
|
||||
/// <returns>The address.</returns>
|
||||
public static string CreateMailSourceUrl(string mailboxId, string mailId) => $"{MAIL_SOURCE_SCHEME}://{mailboxId}/{mailId}";
|
||||
|
||||
/// <summary>
|
||||
/// Whether a source names a mail in one of the mailboxes of the user.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>True for a mail, which nothing can open by its address.</returns>
|
||||
public static bool IsMailSource(this ISource source) =>
|
||||
Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) && address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase);
|
||||
extension(ISource source)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a source names a mail in one of the mailboxes of the user.
|
||||
/// </summary>
|
||||
/// <returns>True for a mail, which nothing can open by its address.</returns>
|
||||
public bool IsMailSource() =>
|
||||
Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address) && address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Whether nothing can open a source by its address: a mail, or the passage of a data source
|
||||
/// whose path names nothing to open, see CreateRetrievalSourceUrl.
|
||||
/// </summary>
|
||||
/// <returns>True for a source which is named as text, without a link.</returns>
|
||||
public bool HasNoLink() =>
|
||||
Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address)
|
||||
&& (address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase) || address.Scheme.Equals(RETRIEVAL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Reads which document a source names, and which page of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only a source which names a file has such a location; a web source is opened by the browser
|
||||
/// and never asks. The page rides in the fragment of the link as `page=N`, which is what the PDF
|
||||
/// open parameters call for. A chat written before v26.9.1 carries `chunk=N` instead, which names
|
||||
/// nothing a program could be sent to: such a source keeps its document and loses only the page.
|
||||
/// </remarks>
|
||||
/// <param name="location">The document and its page, or the default when the source names no file.</param>
|
||||
/// <returns>Whether the source names a file.</returns>
|
||||
public bool TryGetDocumentLocation(out SourceDocumentLocation location)
|
||||
{
|
||||
location = default;
|
||||
if (string.IsNullOrWhiteSpace(source.URL))
|
||||
return false;
|
||||
|
||||
var cleanedUrl = source.URL.Trim().Replace("\r", string.Empty).Replace("\n", string.Empty);
|
||||
if (!Uri.TryCreate(cleanedUrl, UriKind.Absolute, out var absoluteUri) || !absoluteUri.IsFile)
|
||||
return false;
|
||||
|
||||
//
|
||||
// The link was made from a path of this system, so reading it back gives that path again --
|
||||
// percent-encoded spaces and umlauts included, and with the separators this system uses.
|
||||
//
|
||||
var path = absoluteUri.LocalPath;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return false;
|
||||
|
||||
location = new(path, ReadPageFromFragment(absoluteUri.Fragment));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The scheme of the address a passage of a data source leaves as a source, when its path is nothing a program can open.
|
||||
@@ -56,16 +101,6 @@ public static partial class SourceExtensions
|
||||
/// <returns>The address.</returns>
|
||||
public static string CreateRetrievalSourceUrl(string name, string path) => $"{RETRIEVAL_SOURCE_SCHEME}://context/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(path)}";
|
||||
|
||||
/// <summary>
|
||||
/// Whether nothing can open a source by its address: a mail, or the passage of a data source
|
||||
/// whose path names nothing to open, see CreateRetrievalSourceUrl.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>True for a source which is named as text, without a link.</returns>
|
||||
public static bool HasNoLink(this ISource source) =>
|
||||
Uri.TryCreate(source.URL.Trim(), UriKind.Absolute, out var address)
|
||||
&& (address.Scheme.Equals(MAIL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase) || address.Scheme.Equals(RETRIEVAL_SOURCE_SCHEME, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static void AppendMarkdownLink(StringBuilder sb, string title, string url)
|
||||
{
|
||||
sb.Append('[');
|
||||
@@ -136,46 +171,119 @@ public static partial class SourceExtensions
|
||||
return c is '-' or '.' or '_' or '~' or ':' or '/' or '?' or '#' or '[' or ']' or '@' or '!' or '$' or '&' or '\'' or '(' or ')' or '*' or '+' or ',' or ';' or '=';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts a list of sources into the groups it is shown in, and numbers them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The order of the groups and the running number are what a reader follows, and they have to
|
||||
/// be the same wherever the list appears: in the chat, in an exported document, and in the
|
||||
/// clipboard. This is why both the chat and the Markdown below ask here instead of sorting the
|
||||
/// list themselves.
|
||||
/// </remarks>
|
||||
/// <param name="sources">The list of sources to sort.</param>
|
||||
/// <returns>The groups which have sources, in the order they are shown; empty when there are none.</returns>
|
||||
public static IReadOnlyList<SourceGroup> GroupSources(this IList<Source> sources)
|
||||
/// <param name="sources">The list of sources.</param>
|
||||
extension(IList<Source> sources)
|
||||
{
|
||||
var llmSources = new List<Source>();
|
||||
var toolSources = new List<Source>();
|
||||
var ragSources = new List<Source>();
|
||||
foreach (var source in sources)
|
||||
/// <summary>
|
||||
/// Sorts a list of sources into the groups it is shown in, and numbers them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The order of the groups and the running number are what a reader follows, and they have to
|
||||
/// be the same wherever the list appears: in the chat, in an exported document, and in the
|
||||
/// clipboard. This is why both the chat and the Markdown below ask here instead of sorting the
|
||||
/// list themselves.
|
||||
/// </remarks>
|
||||
/// <returns>The groups which have sources, in the order they are shown; empty when there are none.</returns>
|
||||
public IReadOnlyList<SourceGroup> GroupSources()
|
||||
{
|
||||
switch (source.Origin)
|
||||
var llmSources = new List<Source>();
|
||||
var toolSources = new List<Source>();
|
||||
var ragSources = new List<Source>();
|
||||
foreach (var source in sources)
|
||||
{
|
||||
case SourceOrigin.LLM:
|
||||
llmSources.Add(source);
|
||||
break;
|
||||
switch (source.Origin)
|
||||
{
|
||||
case SourceOrigin.LLM:
|
||||
llmSources.Add(source);
|
||||
break;
|
||||
|
||||
case SourceOrigin.TOOL:
|
||||
toolSources.Add(source);
|
||||
break;
|
||||
case SourceOrigin.TOOL:
|
||||
toolSources.Add(source);
|
||||
break;
|
||||
|
||||
case SourceOrigin.RAG:
|
||||
ragSources.Add(source);
|
||||
break;
|
||||
case SourceOrigin.RAG:
|
||||
ragSources.Add(source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var groups = new List<SourceGroup>(3);
|
||||
var sourceNum = 0;
|
||||
AddGroup(groups, TB("Sources provided by the AI"), llmSources, ref sourceNum);
|
||||
AddGroup(groups, TB("Sources used by tools"), toolSources, ref sourceNum);
|
||||
AddGroup(groups, TB("Sources provided by the data providers"), ragSources, ref sourceNum);
|
||||
return groups;
|
||||
}
|
||||
|
||||
var groups = new List<SourceGroup>(3);
|
||||
var sourceNum = 0;
|
||||
AddGroup(groups, TB("Sources provided by the AI"), llmSources, ref sourceNum);
|
||||
AddGroup(groups, TB("Sources used by tools"), toolSources, ref sourceNum);
|
||||
AddGroup(groups, TB("Sources provided by the data providers"), ragSources, ref sourceNum);
|
||||
return groups;
|
||||
/// <summary>
|
||||
/// Converts a list of sources to a markdown-formatted string.
|
||||
/// </summary>
|
||||
/// <param name="keepPageAnchors">Whether a link into a local file may name its page; see the method below.</param>
|
||||
/// <returns>A markdown-formatted string representing the sources.</returns>
|
||||
public string ToMarkdown(bool keepPageAnchors = true)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var group in sources.GroupSources())
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
sb.AppendLine();
|
||||
|
||||
sb.Append("## ");
|
||||
sb.AppendLine(group.Heading);
|
||||
|
||||
foreach (var numberedSource in group.Sources)
|
||||
{
|
||||
sb.Append($"- [{numberedSource.Number}] ");
|
||||
|
||||
// A link to a mail, or to what only a data source knows, would lead nowhere in a document or in the clipboard, so it is named only:
|
||||
if (numberedSource.Source.HasNoLink())
|
||||
sb.Append(EscapeMarkdownLinkText(numberedSource.Source.Title));
|
||||
else
|
||||
AppendMarkdownLink(sb, numberedSource.Source.Title, keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL));
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of sources to a markdown-formatted string, headed by a title of its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat shows the sources in a box below the answer, so the reader sees where the one ends
|
||||
/// and the others begin. An exported document is one text: without a heading of its own, the
|
||||
/// source list would read like one more section the model wrote. This is why the export asks
|
||||
/// for this and the chat does not.
|
||||
/// </remarks>
|
||||
/// <param name="keepPageAnchors">Whether a link into a local file may name its page.</param>
|
||||
/// <returns>A markdown-formatted string representing the sources, or an empty string when there are none.</returns>
|
||||
public string ToExportMarkdown(bool keepPageAnchors = true)
|
||||
{
|
||||
var sourcesMarkdown = sources.ToMarkdown(keepPageAnchors);
|
||||
if (string.IsNullOrWhiteSpace(sourcesMarkdown))
|
||||
return string.Empty;
|
||||
|
||||
return $"# {TB("Sources")}{Environment.NewLine}{Environment.NewLine}{sourcesMarkdown}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges a list of added sources into an existing list of sources, avoiding duplicates based on normalized URLs.
|
||||
/// </summary>
|
||||
/// <param name="addedSources">The list of sources to add.</param>
|
||||
public void MergeSources(IEnumerable<ISource> addedSources)
|
||||
{
|
||||
var sourceIdentities = sources
|
||||
.Select(source => GetSourceIdentity(source.URL))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var addedSource in addedSources)
|
||||
{
|
||||
if (sourceIdentities.Add(GetSourceIdentity(addedSource.URL)))
|
||||
sources.Add((Source)addedSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddGroup(ICollection<SourceGroup> groups, string heading, IReadOnlyList<Source> sources, ref int sourceNum)
|
||||
@@ -190,40 +298,6 @@ public static partial class SourceExtensions
|
||||
groups.Add(new(heading, numberedSources));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of sources to a markdown-formatted string.
|
||||
/// </summary>
|
||||
/// <param name="sources">The list of sources to convert.</param>
|
||||
/// <param name="keepPageAnchors">Whether a link into a local file may name its page; see the method below.</param>
|
||||
/// <returns>A markdown-formatted string representing the sources.</returns>
|
||||
public static string ToMarkdown(this IList<Source> sources, bool keepPageAnchors = true)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var group in sources.GroupSources())
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
sb.AppendLine();
|
||||
|
||||
sb.Append("## ");
|
||||
sb.AppendLine(group.Heading);
|
||||
|
||||
foreach (var numberedSource in group.Sources)
|
||||
{
|
||||
sb.Append($"- [{numberedSource.Number}] ");
|
||||
|
||||
// A link to a mail, or to what only a data source knows, would lead nowhere in a document or in the clipboard, so it is named only:
|
||||
if (numberedSource.Source.HasNoLink())
|
||||
sb.Append(EscapeMarkdownLinkText(numberedSource.Source.Title));
|
||||
else
|
||||
AppendMarkdownLink(sb, numberedSource.Source.Title, keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL));
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the page off a link into a local file, for a reader which cannot follow it.
|
||||
/// </summary>
|
||||
@@ -247,61 +321,6 @@ public static partial class SourceExtensions
|
||||
return absoluteUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Fragment, UriFormat.UriEscaped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of sources to a markdown-formatted string, headed by a title of its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat shows the sources in a box below the answer, so the reader sees where the one ends
|
||||
/// and the others begin. An exported document is one text: without a heading of its own, the
|
||||
/// source list would read like one more section the model wrote. This is why the export asks
|
||||
/// for this and the chat does not.
|
||||
/// </remarks>
|
||||
/// <param name="sources">The list of sources to convert.</param>
|
||||
/// <param name="keepPageAnchors">Whether a link into a local file may name its page.</param>
|
||||
/// <returns>A markdown-formatted string representing the sources, or an empty string when there are none.</returns>
|
||||
public static string ToExportMarkdown(this IList<Source> sources, bool keepPageAnchors = true)
|
||||
{
|
||||
var sourcesMarkdown = sources.ToMarkdown(keepPageAnchors);
|
||||
if (string.IsNullOrWhiteSpace(sourcesMarkdown))
|
||||
return string.Empty;
|
||||
|
||||
return $"# {TB("Sources")}{Environment.NewLine}{Environment.NewLine}{sourcesMarkdown}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads which document a source names, and which page of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only a source which names a file has such a location; a web source is opened by the browser
|
||||
/// and never asks. The page rides in the fragment of the link as `page=N`, which is what the PDF
|
||||
/// open parameters call for. A chat written before v26.9.1 carries `chunk=N` instead, which names
|
||||
/// nothing a program could be sent to: such a source keeps its document and loses only the page.
|
||||
/// </remarks>
|
||||
/// <param name="source">The source to read.</param>
|
||||
/// <param name="location">The document and its page, or the default when the source names no file.</param>
|
||||
/// <returns>Whether the source names a file.</returns>
|
||||
public static bool TryGetDocumentLocation(this ISource source, out SourceDocumentLocation location)
|
||||
{
|
||||
location = default;
|
||||
if (string.IsNullOrWhiteSpace(source.URL))
|
||||
return false;
|
||||
|
||||
var cleanedUrl = source.URL.Trim().Replace("\r", string.Empty).Replace("\n", string.Empty);
|
||||
if (!Uri.TryCreate(cleanedUrl, UriKind.Absolute, out var absoluteUri) || !absoluteUri.IsFile)
|
||||
return false;
|
||||
|
||||
//
|
||||
// The link was made from a path of this system, so reading it back gives that path again --
|
||||
// percent-encoded spaces and umlauts included, and with the separators this system uses.
|
||||
//
|
||||
var path = absoluteUri.LocalPath;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return false;
|
||||
|
||||
location = new(path, ReadPageFromFragment(absoluteUri.Fragment));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int? ReadPageFromFragment(string fragment)
|
||||
{
|
||||
const string PAGE_PARAMETER = "page=";
|
||||
@@ -317,24 +336,6 @@ public static partial class SourceExtensions
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges a list of added sources into an existing list of sources, avoiding duplicates based on normalized URLs.
|
||||
/// </summary>
|
||||
/// <param name="sources">The existing list of sources to merge into.</param>
|
||||
/// <param name="addedSources">The list of sources to add.</param>
|
||||
public static void MergeSources(this IList<Source> sources, IEnumerable<ISource> addedSources)
|
||||
{
|
||||
var sourceIdentities = sources
|
||||
.Select(source => GetSourceIdentity(source.URL))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var addedSource in addedSources)
|
||||
{
|
||||
if (sourceIdentities.Add(GetSourceIdentity(addedSource.URL)))
|
||||
sources.Add((Source)addedSource);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What tells two sources apart: the address without its fragment.
|
||||
/// </summary>
|
||||
|
||||
@@ -4,25 +4,28 @@ public static class TaskExtensions
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(TaskExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// Lets a task run on its own, but keeps an eye on how it ends.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this wherever a task is started without awaiting it. Discarding one instead means that nobody
|
||||
/// ever looks at its outcome: the task carries its exception until the garbage collector finalizes it,
|
||||
/// and only then does it show up as an unobserved task exception — naming a task type, without any
|
||||
/// hint at what was running. Around a circuit which is gone, that is the common case: components of a
|
||||
/// reloaded or sleeping window still receive events and still schedule work.
|
||||
/// </remarks>
|
||||
/// <param name="task">The task to watch.</param>
|
||||
/// <param name="context">What this task was doing, for the log entry.</param>
|
||||
public static void Observe(this Task task, string context)
|
||||
extension(Task task)
|
||||
{
|
||||
task.ContinueWith(finishedTask =>
|
||||
LogFailure(finishedTask.Exception, context),
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
/// <summary>
|
||||
/// Lets a task run on its own, but keeps an eye on how it ends.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this wherever a task is started without awaiting it. Discarding one instead means that nobody
|
||||
/// ever looks at its outcome: the task carries its exception until the garbage collector finalizes it,
|
||||
/// and only then does it show up as an unobserved task exception — naming a task type, without any
|
||||
/// hint at what was running. Around a circuit which is gone, that is the common case: components of a
|
||||
/// reloaded or sleeping window still receive events and still schedule work.
|
||||
/// </remarks>
|
||||
/// <param name="context">What this task was doing, for the log entry.</param>
|
||||
public void Observe(string context)
|
||||
{
|
||||
task.ContinueWith(finishedTask =>
|
||||
LogFailure(finishedTask.Exception, context),
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogFailure(AggregateException? exception, string context)
|
||||
|
||||
@@ -4,15 +4,18 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class TextColorExtensions
|
||||
{
|
||||
public static string GetHTMLColor(this TextColor color, SettingsManager settingsManager) => color switch
|
||||
extension(TextColor color)
|
||||
{
|
||||
TextColor.DEFAULT => string.Empty,
|
||||
public string GetHTMLColor(SettingsManager settingsManager) => color switch
|
||||
{
|
||||
TextColor.DEFAULT => string.Empty,
|
||||
|
||||
TextColor.ERROR => settingsManager.IsDarkMode ? "#ff6c6c" : "#ff0000",
|
||||
TextColor.WARN => settingsManager.IsDarkMode ? "#c7a009" : "#c7c000",
|
||||
TextColor.SUCCESS => settingsManager.IsDarkMode ? "#08b342" : "#009933",
|
||||
TextColor.INFO => settingsManager.IsDarkMode ? "#5279b8" : "#2d67c4",
|
||||
TextColor.ERROR => settingsManager.IsDarkMode ? "#ff6c6c" : "#ff0000",
|
||||
TextColor.WARN => settingsManager.IsDarkMode ? "#c7a009" : "#c7c000",
|
||||
TextColor.SUCCESS => settingsManager.IsDarkMode ? "#08b342" : "#009933",
|
||||
TextColor.INFO => settingsManager.IsDarkMode ? "#5279b8" : "#2d67c4",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,32 @@ namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public static class SafeSearchPolicyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The value SearXNG expects for its safesearch parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SearXNG takes the level as a number. That number stays here, at the edge towards the
|
||||
/// search engine, instead of travelling through the settings where nobody can read it.
|
||||
/// </remarks>
|
||||
public static string ToSearXNGValue(this SafeSearchPolicy policy) => policy switch
|
||||
extension(SafeSearchPolicy policy)
|
||||
{
|
||||
SafeSearchPolicy.OFF => "0",
|
||||
SafeSearchPolicy.MODERATE => "1",
|
||||
SafeSearchPolicy.STRICT => "2",
|
||||
/// <summary>
|
||||
/// The value SearXNG expects for its safesearch parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SearXNG takes the level as a number. That number stays here, at the edge towards the
|
||||
/// search engine, instead of travelling through the settings where nobody can read it.
|
||||
/// </remarks>
|
||||
public string ToSearXNGValue() => policy switch
|
||||
{
|
||||
SafeSearchPolicy.OFF => "0",
|
||||
SafeSearchPolicy.MODERATE => "1",
|
||||
SafeSearchPolicy.STRICT => "2",
|
||||
|
||||
_ => "0",
|
||||
};
|
||||
_ => "0",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The value Tavily expects for its safe search parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tavily knows filtering only as on or off, so a moderate policy is filtered as strictly as
|
||||
/// a strict one. Of the two ways to round that, filtering more than was asked for is the one
|
||||
/// that cannot surprise anyone.
|
||||
/// </remarks>
|
||||
public static bool ToTavilyValue(this SafeSearchPolicy policy) => policy is not SafeSearchPolicy.OFF;
|
||||
/// <summary>
|
||||
/// The value Tavily expects for its safe search parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tavily knows filtering only as on or off, so a moderate policy is filtered as strictly as
|
||||
/// a strict one. Of the two ways to round that, filtering more than was asked for is the one
|
||||
/// that cannot surprise anyone.
|
||||
/// </remarks>
|
||||
public bool ToTavilyValue() => policy is not SafeSearchPolicy.OFF;
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -8,17 +8,20 @@ public static class ToolCallingAvailabilityExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ToolCallingAvailabilityExtensions).Namespace, nameof(ToolCallingAvailabilityExtensions));
|
||||
|
||||
public static ToolCallingAvailability GetToolCallingAvailability(this AIStudio.Settings.Provider provider)
|
||||
extension(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
return new(false, TB("Please select an LLM provider."));
|
||||
public ToolCallingAvailability GetToolCallingAvailability()
|
||||
{
|
||||
if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
return new(false, TB("Please select an LLM provider."));
|
||||
|
||||
var modelProfile = provider.GetModelProfile();
|
||||
var supportsRequiredApis = modelProfile.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API);
|
||||
var modelProfile = provider.GetModelProfile();
|
||||
var supportsRequiredApis = modelProfile.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API);
|
||||
|
||||
if (!supportsRequiredApis || !modelProfile.Has(Capability.FUNCTION_CALLING))
|
||||
return new(false, TB("Tool calling support is not enabled by default for this model, but you can enable this capability in the expert settings of the provider if you are sure the model supports it."));
|
||||
if (!supportsRequiredApis || !modelProfile.Has(Capability.FUNCTION_CALLING))
|
||||
return new(false, TB("Tool calling support is not enabled by default for this model, but you can enable this capability in the expert settings of the provider if you are sure the model supports it."));
|
||||
|
||||
return ToolCallingAvailability.Available();
|
||||
return ToolCallingAvailability.Available();
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -56,6 +56,12 @@ internal static class MailToolArguments
|
||||
/// <param name="toolAction">What the tool does with the mails, e.g., "search", completing "the mailboxes to ...".</param>
|
||||
/// <param name="mailboxIds">The ids of the mailboxes the tool offers, or none while no mailboxes are known.</param>
|
||||
/// <returns>The schema, for further arguments.</returns>
|
||||
/// <remarks>
|
||||
/// A classic extension method instead of a member of an extension block: for those, the compiler of
|
||||
/// the .NET 10.0.1xx SDK warns about every mailbox id passed one by one to the params parameter
|
||||
/// (CS8620, dotnet/roslyn#80024). Move it once the SDK in metadata.txt carries the fix; MWAIS0016 makes
|
||||
/// the same exception until then.
|
||||
/// </remarks>
|
||||
public static ToolParameterSchemaBuilder AddMailConditions(this ToolParameterSchemaBuilder builder, string toolAction, params string[] mailboxIds) => builder
|
||||
.OptionalStringArray(MAILBOX_IDS_ARGUMENT, $"Optional IDs of the mailboxes to {toolAction}, out of those listed in the description of this tool. Leave it out to {toolAction} all of them.", mailboxIds)
|
||||
.OptionalString(FROM_ARGUMENT, $"Optional part of the address or the name of the sender, such as 'alice@example.org', 'example.org', or 'Alice'. At most {MAX_ADDRESS_CHARACTERS} characters.")
|
||||
|
||||
+18
-15
@@ -2,21 +2,24 @@ namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch;
|
||||
|
||||
public static class WebSearchBackendExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of one search service, as the user reads it and as a search result reports it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Product names, so they are not translated: SearXNG is called SearXNG in every language.
|
||||
/// What gets stored is the enum member name instead, which is what leaves this free to be
|
||||
/// worded for people — in the settings dropdown, in a note explaining which service
|
||||
/// answered, and in the result the model reads.
|
||||
/// </remarks>
|
||||
public static string ToName(this WebSearchBackend backend) => backend switch
|
||||
extension(WebSearchBackend backend)
|
||||
{
|
||||
WebSearchBackend.SEARXNG => "SearXNG",
|
||||
WebSearchBackend.STAAN => "Staan",
|
||||
WebSearchBackend.TAVILY => "Tavily",
|
||||
/// <summary>
|
||||
/// The name of one search service, as the user reads it and as a search result reports it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Product names, so they are not translated: SearXNG is called SearXNG in every language.
|
||||
/// What gets stored is the enum member name instead, which is what leaves this free to be
|
||||
/// worded for people — in the settings dropdown, in a note explaining which service
|
||||
/// answered, and in the result the model reads.
|
||||
/// </remarks>
|
||||
public string ToName() => backend switch
|
||||
{
|
||||
WebSearchBackend.SEARXNG => "SearXNG",
|
||||
WebSearchBackend.STAAN => "Staan",
|
||||
WebSearchBackend.TAVILY => "Tavily",
|
||||
|
||||
_ => backend.ToString(),
|
||||
};
|
||||
_ => backend.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,19 @@ namespace AIStudio.Tools.Validation;
|
||||
|
||||
public static class CloudProcessingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether what the user acknowledged covers what the data source sends to the cloud now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An acknowledgment holds only for the providers it was given for. When the data source starts
|
||||
/// to send its recordings to a transcription provider in the cloud, say since a new pattern
|
||||
/// selects them, the user has to acknowledge that once more.
|
||||
/// </remarks>
|
||||
/// <param name="acknowledged">What the user acknowledged.</param>
|
||||
/// <param name="current">What the data source sends to the cloud with its current settings.</param>
|
||||
/// <returns>True when nothing is sent that the user did not acknowledge.</returns>
|
||||
public static bool Covers(this CloudProcessing acknowledged, CloudProcessing current) => (current & ~acknowledged) is CloudProcessing.NONE;
|
||||
extension(CloudProcessing acknowledged)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether what the user acknowledged covers what the data source sends to the cloud now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An acknowledgment holds only for the providers it was given for. When the data source starts
|
||||
/// to send its recordings to a transcription provider in the cloud, say since a new pattern
|
||||
/// selects them, the user has to acknowledge that once more.
|
||||
/// </remarks>
|
||||
/// <param name="current">What the data source sends to the cloud with its current settings.</param>
|
||||
/// <returns>True when nothing is sent that the user did not acknowledge.</returns>
|
||||
public bool Covers(CloudProcessing current) => (current & ~acknowledged) is CloudProcessing.NONE;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user