Moved all extension methods into extension blocks (#1155)

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

No files matched your search

@@ -2,54 +2,57 @@ namespace AIStudio.Settings.DataModel;
public static class BiasCategoryExtensions
{
public static string ToName(this BiasCategory biasCategory) => biasCategory switch
extension(BiasCategory biasCategory)
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning",
BiasCategory.NEED_TO_ACT_FAST => "Need to act fast",
public string ToName() => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning",
BiasCategory.NEED_TO_ACT_FAST => "Need to act fast",
_ => "Unknown category",
};
public static string GetThoughts(this BiasCategory biasCategory) => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER =>
"""
- We store memories differently based on how they were experienced
- We reduce events and lists to their key elements
- We discard specifics to form generalities
- We edit and reinforce some memories after the fact
""",
_ => "Unknown category",
};
public string GetThoughts() => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER =>
"""
- We store memories differently based on how they were experienced
- We reduce events and lists to their key elements
- We discard specifics to form generalities
- We edit and reinforce some memories after the fact
""",
BiasCategory.TOO_MUCH_INFORMATION =>
"""
- We notice things already primed in memory or repeated often
- Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things
- We notice when something has changed
- We are drawn to details that confirm our own existing beliefs
- We notice flaws in others more easily than we notice flaws in ourselves
""",
BiasCategory.TOO_MUCH_INFORMATION =>
"""
- We notice things already primed in memory or repeated often
- Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things
- We notice when something has changed
- We are drawn to details that confirm our own existing beliefs
- We notice flaws in others more easily than we notice flaws in ourselves
""",
BiasCategory.NOT_ENOUGH_MEANING =>
"""
- We tend to find stories and patterns even when looking at sparse data
- We fill in characteristics from stereotypes, generalities, and prior histories
- We imagine things and people we’re familiar with or fond of as better
- We simplify probabilities and numbers to make them easier to think about
- We project our current mindset and assumptions onto the past and future
- We think we know what other people are thinking
""",
BiasCategory.NOT_ENOUGH_MEANING =>
"""
- We tend to find stories and patterns even when looking at sparse data
- We fill in characteristics from stereotypes, generalities, and prior histories
- We imagine things and people we’re familiar with or fond of as better
- We simplify probabilities and numbers to make them easier to think about
- We project our current mindset and assumptions onto the past and future
- We think we know what other people are thinking
""",
BiasCategory.NEED_TO_ACT_FAST =>
"""
- We favor simple-looking options and complete information over complex, ambiguous options
- To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions
- To get things done, we tend to complete things we’ve invested time & energy in
- To stay focused, we favor the immediate, relatable thing in front of us
- To act, we must be confident we can make an impact and feel what we do is important
""",
BiasCategory.NEED_TO_ACT_FAST =>
"""
- We favor simple-looking options and complete information over complex, ambiguous options
- To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions
- To get things done, we tend to complete things we’ve invested time & energy in
- To stay focused, we favor the immediate, relatable thing in front of us
- To act, we must be confident we can make an impact and feel what we do is important
""",
_ => string.Empty,
};
_ => string.Empty,
};
}
}
@@ -6,50 +6,56 @@ public static class DataSourceSecurityExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceSecurityExtensions).Namespace, nameof(DataSourceSecurityExtensions));
public static string ToSelectionText(this DataSourceSecurity security) => security switch
extension(DataSourceSecurity security)
{
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"),
public string ToSelectionText() => security switch
{
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"),
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
_ => TB("Unknown security policy")
};
public static string ToInfoText(this DataSourceSecurity security) => security switch
{
DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."),
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
_ => TB("Unknown security policy")
};
/// <summary>
/// The data security which demands what the one held and the one arriving demand together.
/// </summary>
/// <remarks>
/// Data which may only be used with self-hosted providers keeps them as the only choice, no matter
/// what arrives later: the data was seen. Data which may be used with any provider marks the holder
/// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED
/// demands nothing and changes nothing. The order does not matter.
/// </remarks>
/// <param name="held">The data security held so far.</param>
/// <param name="arriving">What the data arriving demands.</param>
/// <returns>The tightened data security; never looser than either of the two.</returns>
public static DataSourceSecurity TightenedBy(this DataSourceSecurity held, DataSourceSecurity arriving) => (held, arriving) switch
{
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED,
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY,
_ => held,
};
_ => TB("Unknown security policy")
};
public static TextColor GetColor(this DataSourceSecurity security) => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
public string ToInfoText() => security switch
{
DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."),
_ => TextColor.ERROR
};
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
_ => TB("Unknown security policy")
};
public TextColor GetColor() => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
_ => TextColor.ERROR
};
}
/// <param name="held">The data security held so far.</param>
extension(DataSourceSecurity held)
{
/// <summary>
/// The data security which demands what the one held and the one arriving demand together.
/// </summary>
/// <remarks>
/// Data which may only be used with self-hosted providers keeps them as the only choice, no matter
/// what arrives later: the data was seen. Data which may be used with any provider marks the holder
/// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED
/// demands nothing and changes nothing. The order does not matter.
/// </remarks>
/// <param name="arriving">What the data arriving demands.</param>
/// <returns>The tightened data security; never looser than either of the two.</returns>
public DataSourceSecurity TightenedBy(DataSourceSecurity arriving) => (held, arriving) switch
{
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED,
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY,
_ => held,
};
}
}
@@ -9,18 +9,21 @@ public static class DataSourceTypeExtension
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceTypeExtension).Namespace, nameof(DataSourceTypeExtension));
/// <summary>
/// Get the display name of the data source type.
/// </summary>
/// <param name="type">The data source type.</param>
/// <returns>The display name of the data source type.</returns>
public static string GetDisplayName(this DataSourceType type) => type switch
extension(DataSourceType type)
{
DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
DataSourceType.ERI_V1 => TB("External ERI Server (v1)"),
DataSourceType.MAILBOX => TB("Mailbox"),
/// <summary>
/// Get the display name of the data source type.
/// </summary>
/// <returns>The display name of the data source type.</returns>
public string GetDisplayName() => type switch
{
DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
DataSourceType.ERI_V1 => TB("External ERI Server (v1)"),
DataSourceType.MAILBOX => TB("Mailbox"),
_ => TB("None"),
};
_ => TB("None"),
};
}
}
@@ -6,11 +6,14 @@ public static class LangBehaviorExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LangBehaviorExtensions).Namespace, nameof(LangBehaviorExtensions));
public static string Name(this LangBehavior langBehavior) => langBehavior switch
extension(LangBehavior langBehavior)
{
LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."),
LangBehavior.MANUAL => TB("Choose the language manually."),
public string Name() => langBehavior switch
{
LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."),
LangBehavior.MANUAL => TB("Choose the language manually."),
_ => TB("Unknown option")
};
_ => TB("Unknown option")
};
}
}
@@ -6,31 +6,34 @@ public static class MailboxMaxAgeExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxMaxAgeExtensions).Namespace, nameof(MailboxMaxAgeExtensions));
public static string GetName(this MailboxMaxAge maxAge) => maxAge switch
{
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"),
MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"),
MailboxMaxAge.ALL => TB("All mails"),
_ => TB("Unknown period"),
};
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="maxAge">The period.</param>
/// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns>
public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch
extension(MailboxMaxAge maxAge)
{
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24),
MailboxMaxAge.ALL => null,
public string GetName() => maxAge switch
{
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"),
MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"),
MailboxMaxAge.ALL => TB("All mails"),
// The shortest period also stands in for a value this version does not know, as it does in the settings:
_ => now.AddMonths(-3),
};
_ => TB("Unknown period"),
};
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns>
public DateTimeOffset? GetReceivedSince(DateTimeOffset now) => maxAge switch
{
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24),
MailboxMaxAge.ALL => null,
// The shortest period also stands in for a value this version does not know, as it does in the settings:
_ => now.AddMonths(-3),
};
}
}
@@ -16,23 +16,26 @@ public static class MailboxTransportSecurityExtensions
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxTransportSecurityExtensions).Namespace, nameof(MailboxTransportSecurityExtensions));
public static string GetName(this MailboxTransportSecurity transportSecurity) => transportSecurity switch
extension(MailboxTransportSecurity transportSecurity)
{
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
public string GetName() => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
_ => TB("Unknown encryption"),
};
_ => TB("Unknown encryption"),
};
/// <summary>
/// The port the encryption usually comes with.
/// </summary>
/// <returns>The port, or null when there is no usual one.</returns>
public static int? GetUsualPort(this MailboxTransportSecurity transportSecurity) => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT,
MailboxTransportSecurity.STARTTLS => STARTTLS_PORT,
/// <summary>
/// The port the encryption usually comes with.
/// </summary>
/// <returns>The port, or null when there is no usual one.</returns>
public int? GetUsualPort() => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT,
MailboxTransportSecurity.STARTTLS => STARTTLS_PORT,
_ => null,
};
_ => null,
};
}
}
@@ -6,31 +6,34 @@ public static class OutboundDataRequirementExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions));
/// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary>
/// <remarks>
/// One text for the tool selection and for a call which was turned down, so the user reads the
/// same in both places, and the model can pass it on. It names the source which demands the
/// requirement, and the setting as the user finds it labeled. The level is the one the setting
/// had when the content was read: a change made later does not loosen the chat.<br/><br/>
/// The text speaks of content the chat holds, not of content it read: the content may come from
/// another chat or an assistant, which sent it here along with its restrictions.
/// </remarks>
/// <param name="requirement">What the chat demands.</param>
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param>
/// <returns>The text for the user.</returns>
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch
extension(OutboundDataRequirement requirement)
{
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes),
OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()),
OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources),
/// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary>
/// <remarks>
/// One text for the tool selection and for a call which was turned down, so the user reads the
/// same in both places, and the model can pass it on. It names the source which demands the
/// requirement, and the setting as the user finds it labeled. The level is the one the setting
/// had when the content was read: a change made later does not loosen the chat.<br/><br/>
/// The text speaks of content the chat holds, not of content it read: the content may come from
/// another chat or an assistant, which sent it here along with its restrictions.
/// </remarks>
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param>
/// <returns>The text for the user.</returns>
public string GetToolBlockedMessage(Data settings) => requirement.Origin switch
{
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes),
OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()),
OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources),
// NONE keeps no tool from running, since nothing restricts such a chat. What is left is an
// origin this version does not know:
_ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
};
// NONE keeps no tool from running, since nothing restricts such a chat. What is left is an
// origin this version does not know:
_ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
};
}
private static string GetMailboxBlockedMessage(OutboundDataRequirement requirement, IEnumerable<DataSourceMailbox> mailboxes)
{
@@ -6,52 +6,54 @@ public static class OutboundDataRestrictionExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions));
/// <summary>
/// The stricter of two restrictions.
/// </summary>
/// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks>
/// <param name="restriction">The one restriction.</param>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other)
/// <param name="restriction">The restriction, as it was read.</param>
extension(OutboundDataRestriction restriction)
{
var knownRestriction = restriction.OrStrictestWhenUnknown();
var knownOther = other.OrStrictestWhenUnknown();
return knownOther < knownRestriction ? knownOther : knownRestriction;
/// <summary>
/// The stricter of two restrictions.
/// </summary>
/// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public OutboundDataRestriction StricterOf(OutboundDataRestriction other)
{
var knownRestriction = restriction.OrStrictestWhenUnknown();
var knownOther = other.OrStrictestWhenUnknown();
return knownOther < knownRestriction ? knownOther : knownRestriction;
}
/// <summary>
/// The restriction as this version applies it.
/// </summary>
/// <remarks>
/// The settings file and a saved chat may hold a number which names no member, e.g., one written
/// by hand, and the settings read it all the same. Compared as it is, such a number would rank
/// above UNRESTRICTED and lift every restriction it meets.
/// </remarks>
/// <returns>The restriction itself, or the strictest one when this version does not know it.</returns>
public OutboundDataRestriction OrStrictestWhenUnknown() => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public string GetName() => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
OutboundDataRestriction.UNRESTRICTED => TB("No restriction"),
_ => TB("Unknown restriction"),
};
/// <summary>
/// What a chat may still do on this level, once internal content entered it.
/// </summary>
public string GetDescription() => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."),
OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."),
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
}
/// <summary>
/// The restriction as this version applies it.
/// </summary>
/// <remarks>
/// The settings file and a saved chat may hold a number which names no member, e.g., one written
/// by hand, and the settings read it all the same. Compared as it is, such a number would rank
/// above UNRESTRICTED and lift every restriction it meets.
/// </remarks>
/// <param name="restriction">The restriction as it was read.</param>
/// <returns>The restriction itself, or the strictest one when this version does not know it.</returns>
public static OutboundDataRestriction OrStrictestWhenUnknown(this OutboundDataRestriction restriction) => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public static string GetName(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
OutboundDataRestriction.UNRESTRICTED => TB("No restriction"),
_ => TB("Unknown restriction"),
};
/// <summary>
/// What a chat may still do on this level, once internal content entered it.
/// </summary>
public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."),
OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."),
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
}
@@ -6,46 +6,49 @@ public static class PreviewFeaturesExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PreviewFeaturesExtensions).Namespace, nameof(PreviewFeaturesExtensions));
public static string GetPreviewDescription(this PreviewFeatures feature) => feature switch
{
PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"),
PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"),
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"),
_ => TB("Unknown preview feature")
};
/// <summary>
/// Check if the preview feature is released or not.
/// </summary>
/// <remarks>
/// This metadata is necessary because we cannot remove the enum values from the list.
/// Otherwise, we could not deserialize old settings files that may contain these values.
/// </remarks>
/// <param name="feature">The preview feature to check.</param>
/// <returns>True when the preview feature is released, false otherwise.</returns>
public static bool IsReleased(this PreviewFeatures feature) => feature switch
extension(PreviewFeatures feature)
{
PreviewFeatures.PRE_READ_PDF_2025 => true,
PreviewFeatures.PRE_PLUGINS_2025 => true,
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true,
public string GetPreviewDescription() => feature switch
{
PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"),
PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"),
_ => false
};
public static bool IsEnabled(this PreviewFeatures feature, SettingsManager settingsManager)
{
if(feature.IsReleased())
return true;
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"),
_ => TB("Unknown preview feature")
};
/// <summary>
/// Check if the preview feature is released or not.
/// </summary>
/// <remarks>
/// This metadata is necessary because we cannot remove the enum values from the list.
/// Otherwise, we could not deserialize old settings files that may contain these values.
/// </remarks>
/// <returns>True when the preview feature is released, false otherwise.</returns>
public bool IsReleased() => feature switch
{
PreviewFeatures.PRE_READ_PDF_2025 => true,
PreviewFeatures.PRE_PLUGINS_2025 => true,
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true,
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature);
_ => false
};
public bool IsEnabled(SettingsManager settingsManager)
{
if(feature.IsReleased())
return true;
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature);
}
}
}
@@ -2,48 +2,51 @@ namespace AIStudio.Settings.DataModel;
public static class PreviewVisibilityExtensions
{
public static IList<PreviewFeatures> GetPreviewFeatures(this PreviewVisibility visibility)
extension(PreviewVisibility visibility)
{
var features = new List<PreviewFeatures>();
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
public IList<PreviewFeatures> GetPreviewFeatures()
{
}
var features = new List<PreviewFeatures>();
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
{
}
if (visibility >= PreviewVisibility.BETA)
{
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1);
features.Add(PreviewFeatures.PRE_RAG_2024);
features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026);
}
if (visibility >= PreviewVisibility.BETA)
{
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1);
features.Add(PreviewFeatures.PRE_RAG_2024);
features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026);
}
if (visibility >= PreviewVisibility.ALPHA)
{
}
if (visibility >= PreviewVisibility.ALPHA)
{
}
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_MAILBOXES_2026);
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_MAILBOXES_2026);
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
}
return features;
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
public HashSet<PreviewFeatures> FilterPreviewFeatures(HashSet<PreviewFeatures> enabledFeatures)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
}
var filteredFeatures = new HashSet<PreviewFeatures>();
var previewFeatures = visibility.GetPreviewFeatures();
foreach (var feature in enabledFeatures)
{
if (previewFeatures.Contains(feature))
filteredFeatures.Add(feature);
}
return features;
}
public static HashSet<PreviewFeatures> FilterPreviewFeatures(this PreviewVisibility visibility, HashSet<PreviewFeatures> enabledFeatures)
{
var filteredFeatures = new HashSet<PreviewFeatures>();
var previewFeatures = visibility.GetPreviewFeatures();
foreach (var feature in enabledFeatures)
{
if (previewFeatures.Contains(feature))
filteredFeatures.Add(feature);
return filteredFeatures;
}
return filteredFeatures;
}
}
@@ -2,20 +2,26 @@ namespace AIStudio.Settings.DataModel.PreviousModels;
public static class ProviderV4Extensions
{
public static List<AIStudio.Settings.Provider> MigrateFromV4ToV5(this IEnumerable<Provider> providers)
extension(IEnumerable<Provider> providers)
{
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList();
public List<AIStudio.Settings.Provider> MigrateFromV4ToV5()
{
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList();
}
}
public static AIStudio.Settings.Provider MigrateFromV4ToV5(this Provider provider) => new()
extension(Provider provider)
{
Num = provider.Num,
Id = provider.Id,
InstanceName = provider.InstanceName,
UsedLLMProvider = provider.UsedProvider,
Model = provider.Model,
IsSelfHosted = provider.IsSelfHosted,
Hostname = provider.Hostname,
Host = provider.Host,
};
public AIStudio.Settings.Provider MigrateFromV4ToV5() => new()
{
Num = provider.Num,
Id = provider.Id,
InstanceName = provider.InstanceName,
UsedLLMProvider = provider.UsedProvider,
Model = provider.Model,
IsSelfHosted = provider.IsSelfHosted,
Hostname = provider.Hostname,
Host = provider.Host,
};
}
}
@@ -6,11 +6,14 @@ public static class SendToChatDataSourceBehaviorExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SendToChatDataSourceBehaviorExtensions).Namespace, nameof(SendToChatDataSourceBehaviorExtensions));
public static string Description(this SendToChatDataSourceBehavior behavior) => behavior switch
extension(SendToChatDataSourceBehavior behavior)
{
SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"),
SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"),
public string Description() => behavior switch
{
SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"),
SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"),
_ => TB("Unknown behavior"),
};
_ => TB("Unknown behavior"),
};
}
}
@@ -2,16 +2,19 @@ namespace AIStudio.Settings.DataModel;
public static class StartPageExtensions
{
public static string ToRoute(this StartPage startPage) => startPage switch
extension(StartPage startPage)
{
StartPage.HOME => string.Empty,
StartPage.CHAT => Routes.CHAT,
StartPage.ASSISTANTS => Routes.ASSISTANTS,
StartPage.INFORMATION => Routes.ABOUT,
StartPage.PLUGINS => Routes.PLUGINS,
StartPage.SUPPORTERS => Routes.SUPPORTERS,
StartPage.SETTINGS => Routes.SETTINGS,
public string ToRoute() => startPage switch
{
StartPage.HOME => string.Empty,
StartPage.CHAT => Routes.CHAT,
StartPage.ASSISTANTS => Routes.ASSISTANTS,
StartPage.INFORMATION => Routes.ABOUT,
StartPage.PLUGINS => Routes.PLUGINS,
StartPage.SUPPORTERS => Routes.SUPPORTERS,
StartPage.SETTINGS => Routes.SETTINGS,
_ => string.Empty,
};
_ => string.Empty,
};
}
}
@@ -6,12 +6,15 @@ public static class ThemesExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ThemesExtensions).Namespace, nameof(ThemesExtensions));
public static string GetName(this Themes theme) => theme switch
extension(Themes theme)
{
Themes.SYSTEM => TB("Synchronized with the operating system settings"),
Themes.LIGHT => TB("Always use light theme"),
Themes.DARK => TB("Always use dark theme"),
public string GetName() => theme switch
{
Themes.SYSTEM => TB("Synchronized with the operating system settings"),
Themes.LIGHT => TB("Always use light theme"),
Themes.DARK => TB("Always use dark theme"),
_ => TB("Unknown setting"),
};
_ => TB("Unknown setting"),
};
}
}
@@ -6,24 +6,27 @@ public static class TranscriptionOpusBitrateExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(TranscriptionOpusBitrateExtensions).Namespace, nameof(TranscriptionOpusBitrateExtensions));
public static string GetName(this TranscriptionOpusBitrate bitrate) => bitrate switch
extension(TranscriptionOpusBitrate bitrate)
{
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"),
TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"),
_ => TB("Unknown"),
};
public string GetName() => bitrate switch
{
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"),
TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"),
_ => TB("Unknown"),
};
public static uint GetBitsPerSecond(this TranscriptionOpusBitrate bitrate) => bitrate switch
{
TranscriptionOpusBitrate.KBPS_32 => 32_000,
TranscriptionOpusBitrate.KBPS_64 => 64_000,
TranscriptionOpusBitrate.KBPS_128 => 128_000,
TranscriptionOpusBitrate.KBPS_256 => 256_000,
public uint GetBitsPerSecond() => bitrate switch
{
TranscriptionOpusBitrate.KBPS_32 => 32_000,
TranscriptionOpusBitrate.KBPS_64 => 64_000,
TranscriptionOpusBitrate.KBPS_128 => 128_000,
TranscriptionOpusBitrate.KBPS_256 => 256_000,
// A value we do not know must never mean the lowest quality: that is how quiet passages
// went missing from transcripts in the first place. Fall back to the recommended bitrate.
_ => 128_000,
};
// A value we do not know must never mean the lowest quality: that is how quiet passages
// went missing from transcripts in the first place. Fall back to the recommended bitrate.
_ => 128_000,
};
}
}
@@ -2,20 +2,23 @@ namespace AIStudio.Settings;
public static class DataSourceOrderExtensions
{
/// <summary>
/// Sorts data sources in a fixed order: by their number, then by their ID.
/// </summary>
/// <remarks>
/// The same data sources always come in the same order, however the settings list them, the
/// checks return them, or an agent selects them. Semantic Search offers them this way, because
/// the providers cache a request from its beginning, and the tools are part of that beginning,
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks>
/// <param name="dataSources">The data sources to sort.</param>
/// <returns>The data sources in their fixed order.</returns>
public static IReadOnlyList<IDataSource> InFixedOrder(this IEnumerable<IDataSource> dataSources) => dataSources
.OrderBy(dataSource => dataSource.Num)
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal)
.ToList();
extension(IEnumerable<IDataSource> dataSources)
{
/// <summary>
/// Sorts data sources in a fixed order: by their number, then by their ID.
/// </summary>
/// <remarks>
/// The same data sources always come in the same order, however the settings list them, the
/// checks return them, or an agent selects them. Semantic Search offers them this way, because
/// the providers cache a request from its beginning, and the tools are part of that beginning,
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks>
/// <returns>The data sources in their fixed order.</returns>
public IReadOnlyList<IDataSource> InFixedOrder() => dataSources
.OrderBy(dataSource => dataSource.Num)
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal)
.ToList();
}
}
@@ -30,26 +30,28 @@ public static class DataSourceOutboundDataExtensions
? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)
: organizationMinimum;
/// <summary>
/// The least strict level this data source may have.
/// </summary>
/// <param name="dataSource">The data source.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The least strict level allowed.</returns>
public static OutboundDataRestriction GetMinimumOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) =>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum);
extension(IDataSource dataSource)
{
/// <summary>
/// The least strict level this data source may have.
/// </summary>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The least strict level allowed.</returns>
public OutboundDataRestriction GetMinimumOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum);
/// <summary>
/// Where a chat may still send data, once passages of this data source reached it.
/// </summary>
/// <remarks>
/// The level of the data source, tightened to the least strict one it may have. This holds at
/// runtime, not only in the dialog: a data source stored before, or one a configuration plugin
/// provides, is never edited, so its stored level may be less strict.
/// </remarks>
/// <param name="dataSource">The data source.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The level which applies.</returns>
public static OutboundDataRestriction GetEffectiveOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) =>
dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum));
/// <summary>
/// Where a chat may still send data, once passages of this data source reached it.
/// </summary>
/// <remarks>
/// The level of the data source, tightened to the least strict one it may have. This holds at
/// runtime, not only in the dialog: a data source stored before, or one a configuration plugin
/// provides, is never edited, so its stored level may be less strict.
/// </remarks>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The level which applies.</returns>
public OutboundDataRestriction GetEffectiveOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum));
}
}
@@ -5,160 +5,184 @@ namespace AIStudio.Settings;
public static class DataSourceSecurityTrustExtensions
{
public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager)
extension(Provider provider)
{
if (provider == Provider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager settingsManager)
{
if (provider is NoProvider)
return false;
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
}
public static ConfidenceLevel GetConfidenceLevel(this Provider provider, SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this EmbeddingProvider provider, SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this TranscriptionProvider provider, SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this IProvider provider, SettingsManager settingsManager)
{
if (provider is NoProvider)
return ConfidenceLevel.NONE;
return provider.Provider.GetConfidence(settingsManager).Level;
}
public static bool AllowsDataSourceAccess(this Provider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public static bool AllowsDataSourceAccess(this IProvider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public static bool AllowsDataSourceSecurity(this Provider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public static bool AllowsDataSourceSecurity(this IProvider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public static bool AllowsDataSourceSecurity(this bool usingTrustedProvider, DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
{
DataSourceSecurity.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false,
};
public static bool AllowsDataSourceConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel requiredConfidenceLevel)
{
if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true;
return providerConfidenceLevel >= requiredConfidenceLevel;
}
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// Only the levels from very low to high qualify. NONE lets every provider through, cf.
/// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or
/// below the level of a provider nobody rated, so even such a provider would meet them.
/// </remarks>
public static bool IsAllowedMailboxConfidence(this ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
/// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary>
/// <remarks>
/// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every
/// provider, whether it was never chosen or edited by hand in the settings file. Every provider
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails.
/// </remarks>
public static bool AllowsMailboxConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel mailboxConfidenceLevel)
{
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false;
return providerConfidenceLevel >= mailboxConfidenceLevel;
}
public static ConfidenceLevel GetRequiredConfidenceLevel(this IEnumerable<IDataSource> dataSources)
{
var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
if (dataSource.ConfidenceLevel > requiredConfidenceLevel)
requiredConfidenceLevel = dataSource.ConfidenceLevel;
return requiredConfidenceLevel;
}
public static DataSourceSecurity GetRequiredSecurityPolicy(this IEnumerable<IDataSource> dataSources)
{
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED)
return DataSourceSecurity.NOT_SPECIFIED;
if (provider == Provider.NONE)
return false;
if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED)
requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
return requiredSecurityPolicy;
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
extension(EmbeddingProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
extension(TranscriptionProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
extension(IProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider is NoProvider)
return false;
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
}
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider is NoProvider)
return ConfidenceLevel.NONE;
return provider.Provider.GetConfidence(settingsManager).Level;
}
public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
}
extension(bool usingTrustedProvider)
{
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
{
DataSourceSecurity.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false,
};
}
extension(ConfidenceLevel providerConfidenceLevel)
{
public bool AllowsDataSourceConfidenceLevel(ConfidenceLevel requiredConfidenceLevel)
{
if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true;
return providerConfidenceLevel >= requiredConfidenceLevel;
}
/// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary>
/// <remarks>
/// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every
/// provider, whether it was never chosen or edited by hand in the settings file. Every provider
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails.
/// </remarks>
public bool AllowsMailboxConfidenceLevel(ConfidenceLevel mailboxConfidenceLevel)
{
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false;
return providerConfidenceLevel >= mailboxConfidenceLevel;
}
}
extension(ConfidenceLevel confidenceLevel)
{
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// Only the levels from very low to high qualify. NONE lets every provider through, cf.
/// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or
/// below the level of a provider nobody rated, so even such a provider would meet them.
/// </remarks>
public bool IsAllowedMailboxConfidence() => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
}
extension(IEnumerable<IDataSource> dataSources)
{
public ConfidenceLevel GetRequiredConfidenceLevel()
{
var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
if (dataSource.ConfidenceLevel > requiredConfidenceLevel)
requiredConfidenceLevel = dataSource.ConfidenceLevel;
return requiredConfidenceLevel;
}
public DataSourceSecurity GetRequiredSecurityPolicy()
{
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
{
if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED)
return DataSourceSecurity.NOT_SPECIFIED;
if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED)
requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED;
}
return requiredSecurityPolicy;
}
}
private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager)
{
@@ -6,38 +6,41 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <summary>
/// Get the effective reasoning indicator state for the configured provider instance.
/// </summary>
/// <remarks>
/// Two answers meet here, and they answer different questions. What a model is able to do comes
/// from the rules; what this person asked for comes from the parameters they wrote into their
/// own provider. A model which thinks unless told otherwise stops showing the indicator when a
/// parameter turns it off, and a model which can be asked to think shows it only once one does.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The effective reasoning indicator state.</returns>
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider)
extension(Provider provider)
{
var reasoning = provider.GetModelProfile().Reasoning;
if (reasoning is ReasoningSupport.ALWAYS)
return ReasoningIndicatorState.ALWAYS_ON;
var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters);
if (reasoning is ReasoningSupport.ON_BY_DEFAULT)
/// <summary>
/// Get the effective reasoning indicator state for the configured provider instance.
/// </summary>
/// <remarks>
/// Two answers meet here, and they answer different questions. What a model is able to do comes
/// from the rules; what this person asked for comes from the parameters they wrote into their
/// own provider. A model which thinks unless told otherwise stops showing the indicator when a
/// parameter turns it off, and a model which can be asked to think shows it only once one does.
/// </remarks>
/// <returns>The effective reasoning indicator state.</returns>
public ReasoningIndicatorState GetReasoningIndicatorState()
{
return configured switch
var reasoning = provider.GetModelProfile().Reasoning;
if (reasoning is ReasoningSupport.ALWAYS)
return ReasoningIndicatorState.ALWAYS_ON;
var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters);
if (reasoning is ReasoningSupport.ON_BY_DEFAULT)
{
ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE,
ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED,
return configured switch
{
ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE,
ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED,
_ => ReasoningIndicatorState.DEFAULT_ON,
};
_ => ReasoningIndicatorState.DEFAULT_ON,
};
}
if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED)
return ReasoningIndicatorState.CONFIGURED;
return ReasoningIndicatorState.NONE;
}
if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED)
return ReasoningIndicatorState.CONFIGURED;
return ReasoningIndicatorState.NONE;
}
}
@@ -7,132 +7,136 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <summary>
/// Everything the app knows about the model this provider instance is configured with.
/// </summary>
/// <remarks>
/// The one door to that question. Behind it stand the links of the chain, in the order they
/// win: what the person said about their own installation, then what the installation itself
/// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model.</returns>
public static ModelProfile GetModelProfile(this Provider provider)
extension(Provider provider)
{
var automatic = provider.GetAutomaticModelProfile();
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
/// <summary>
/// Everything the app knows about the model this provider instance is configured with.
/// </summary>
/// <remarks>
/// The one door to that question. Behind it stand the links of the chain, in the order they
/// win: what the person said about their own installation, then what the installation itself
/// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything.
/// </remarks>
/// <returns>The profile of the configured model.</returns>
public ModelProfile GetModelProfile()
{
var automatic = provider.GetAutomaticModelProfile();
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
}
/// <summary>
/// Everything known about the configured model except what the person themselves switched.
/// </summary>
/// <remarks>
/// This is what happens when somebody fills in nothing, which is why the expert dialog shows it
/// as the automatic answer. It has to include what the provider reported: a person who leaves
/// the window empty gets the number their own engine stated, and a placeholder showing them a
/// different one would be a promise the app does not keep.
/// </remarks>
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
public ModelProfile GetAutomaticModelProfile()
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
}
/// <summary>
/// Get whether the model used by the configured provider accepts images as input.
/// </summary>
/// <remarks>
/// Two capabilities express image input, one for a single image and one for several. Anything that
/// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it.
///
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator
/// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks>
/// <returns><c>true</c> when the model accepts image input.</returns>
public bool SupportsImageInput()
{
var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
}
}
/// <summary>
/// Everything known about the configured model except what the person themselves switched.
/// </summary>
/// <remarks>
/// This is what happens when somebody fills in nothing, which is why the expert dialog shows it
/// as the automatic answer. It has to include what the provider reported: a person who leaves
/// the window empty gets the number their own engine stated, and a placeholder showing them a
/// different one would be a promise the app does not keep.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
public static ModelProfile GetAutomaticModelProfile(this Provider provider)
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
}
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own installation.
/// </summary>
/// <remarks>
/// The answer to the model as such, which is the same for everybody who uses that name at that
/// provider -- and therefore the answer the registry caches. What one particular installation
/// says about it is asked one link further up, where the instance is known.
///
/// The assumed profile fills in where no rule stated a single capability. It fills in the
/// capabilities only: a modifier may well have said what the model is made for without any rule
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
/// model rather than turning into a chat model with an assumption attached.
/// </remarks>
/// <param name="provider">The LLM provider the model is reached through.</param>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public static ModelProfile GetModelProfile(this LLMProviders provider, Model model)
extension(LLMProviders provider)
{
//
// Without a provider there is nothing to reach the model through, and an empty name is what
// a provider reports before anybody picked one. Neither is a model we could assume anything
// about, so neither gets the assumption.
//
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id))
return ModelProfile.UNKNOWN;
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own installation.
/// </summary>
/// <remarks>
/// The answer to the model as such, which is the same for everybody who uses that name at that
/// provider -- and therefore the answer the registry caches. What one particular installation
/// says about it is asked one link further up, where the instance is known.
///
/// The assumed profile fills in where no rule stated a single capability. It fills in the
/// capabilities only: a modifier may well have said what the model is made for without any rule
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
/// model rather than turning into a chat model with an assumption attached.
/// </remarks>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public ModelProfile GetModelProfile(Model model)
{
//
// Without a provider there is nothing to reach the model through, and an empty name is what
// a provider reports before anybody picked one. Neither is a model we could assume anything
// about, so neither gets the assumption.
//
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id))
return ModelProfile.UNKNOWN;
var stated = ModelRegistry.Shared.Profile(provider, model.Id);
return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated;
var stated = ModelRegistry.Shared.Profile(provider, model.Id);
return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated;
}
}
/// <summary>
/// Get whether the model used by the configured provider accepts images as input.
/// </summary>
/// <remarks>
/// Two capabilities express image input, one for a single image and one for several. Anything that
/// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it.
///
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator
/// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider)
/// <param name="model">The model to check.</param>
extension(Model model)
{
var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
/// <summary>
/// Checks whether this model can be used for chatting.
/// </summary>
/// <remarks>
/// What a model can do and what it is made for used to be two questions answered by two pieces
/// of code, each walking the same name with rules of its own. They disagreed: a model like
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
/// come out of the same rules now, which is why this takes the provider -- the same name means
/// different things depending on who serves it, and only the provider knows how to unwrap it.
///
/// The direction of the answer is deliberate. Everything not recognized as something else is a
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
/// paying for it. Getting it wrong the other way would hide a model.
/// </remarks>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
public bool IsChatModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary>
/// Checks whether this model creates embeddings.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns>
public bool IsEmbeddingModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary>
/// Checks whether this model transcribes audio.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns>
public bool IsTranscriptionModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary>
/// Checks whether this model generates images.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns>
public bool IsImageModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
}
/// <summary>
/// Checks whether this model can be used for chatting.
/// </summary>
/// <remarks>
/// What a model can do and what it is made for used to be two questions answered by two pieces
/// of code, each walking the same name with rules of its own. They disagreed: a model like
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
/// come out of the same rules now, which is why this takes the provider -- the same name means
/// different things depending on who serves it, and only the provider knows how to unwrap it.
///
/// The direction of the answer is deliberate. Everything not recognized as something else is a
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
/// paying for it. Getting it wrong the other way would hide a model.
/// </remarks>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary>
/// Checks whether this model creates embeddings.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns>
public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary>
/// Checks whether this model transcribes audio.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns>
public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary>
/// Checks whether this model generates images.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns>
public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
}