Merge branch 'main' into custom-tokenizer

This commit is contained in:
PaulKoudelka committed 2026-04-24 12:07:22 +02:00
commit e3cb7e9734
165 files changed
+4739 -1738

No files matched your search

@@ -11,6 +11,7 @@ public enum ConfigurableAssistant
GRAMMAR_SPELLING_ASSISTANT,
ICON_FINDER_ASSISTANT,
REWRITE_ASSISTANT,
PROMPT_OPTIMIZER_ASSISTANT,
TRANSLATION_ASSISTANT,
AGENDA_ASSISTANT,
CODING_ASSISTANT,
@@ -114,6 +114,8 @@ public sealed class Data
public DataDocumentAnalysis DocumentAnalysis { get; init; } = new();
public DataMandatoryInformation MandatoryInformation { get; init; } = new();
public DataTextSummarizer TextSummarizer { get; init; } = new();
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
@@ -129,6 +131,8 @@ public sealed class Data
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
public DataRewriteImprove RewriteImprove { get; init; } = new();
public DataPromptOptimizer PromptOptimizer { get; init; } = new();
public DataEMail EMail { get; init; } = new();
@@ -0,0 +1,117 @@
using System.Security.Cryptography;
using System.Text;
using Lua;
namespace AIStudio.Settings.DataModel;
public sealed record DataMandatoryInfo
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataMandatoryInfo>();
/// <summary>
/// The stable ID of the mandatory info.
/// </summary>
public string Id { get; private init; } = string.Empty;
/// <summary>
/// The ID of the enterprise configuration plugin that provides this info.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty;
/// <summary>
/// The title shown to the user.
/// </summary>
public string Title { get; private init; } = string.Empty;
/// <summary>
/// The configured version string shown to the user. A changed version triggers re-acceptance
/// and allows the UI to distinguish a new version from a content-only change.
/// </summary>
public string VersionText { get; private init; } = string.Empty;
/// <summary>
/// The Markdown content shown to the user.
/// </summary>
public string Markdown { get; private init; } = string.Empty;
/// <summary>
/// The label of the acceptance button.
/// </summary>
public string AcceptButtonText { get; private init; } = string.Empty;
/// <summary>
/// The label of the reject button.
/// </summary>
public string RejectButtonText { get; private init; } = string.Empty;
/// <summary>
/// The current hash used to determine whether the user needs to re-accept the info.
/// </summary>
public string AcceptanceHash { get; private init; } = string.Empty;
private static string CreateAcceptanceHash(string versionText, string title, string markdown)
{
var content = $"Version:{versionText}\nTitle:{title}\nMarkdown:{markdown}";
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash);
}
public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataMandatoryInfo mandatoryInfo)
{
mandatoryInfo = new DataMandatoryInfo();
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid ID. The ID must be a valid GUID.", idx);
return false;
}
if (!table.TryGetValue("Title", out var titleValue) || !titleValue.TryRead<string>(out var title) || string.IsNullOrWhiteSpace(title))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Title field.", idx);
return false;
}
if (!table.TryGetValue("Version", out var versionValue) || !versionValue.TryRead<string>(out var versionText) || string.IsNullOrWhiteSpace(versionText))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Version field.", idx);
return false;
}
if (!table.TryGetValue("Markdown", out var markdownValue) || !markdownValue.TryRead<string>(out var markdown) || string.IsNullOrWhiteSpace(markdown))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Markdown field.", idx);
return false;
}
if (!table.TryGetValue("AcceptButtonText", out var acceptButtonValue) || !acceptButtonValue.TryRead<string>(out var acceptButtonText) || string.IsNullOrWhiteSpace(acceptButtonText))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid AcceptButtonText field.", idx);
return false;
}
if (!table.TryGetValue("RejectButtonText", out var rejectButtonValue) || !rejectButtonValue.TryRead<string>(out var rejectButtonText) || string.IsNullOrWhiteSpace(rejectButtonText))
{
LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid RejectButtonText field.", idx);
return false;
}
var normalizedMarkdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown);
var acceptanceHash = CreateAcceptanceHash(versionText, title, normalizedMarkdown);
mandatoryInfo = new DataMandatoryInfo
{
Id = id.ToString(),
Title = title,
VersionText = versionText,
Markdown = normalizedMarkdown,
AcceptButtonText = acceptButtonText,
RejectButtonText = rejectButtonText,
EnterpriseConfigurationPluginId = configPluginId,
AcceptanceHash = acceptanceHash,
};
return true;
}
}
@@ -0,0 +1,29 @@
namespace AIStudio.Settings.DataModel;
public sealed record DataMandatoryInfoAcceptance
{
/// <summary>
/// The ID of the mandatory info that was accepted.
/// </summary>
public string InfoId { get; init; } = string.Empty;
/// <summary>
/// The accepted version string.
/// </summary>
public string AcceptedVersion { get; init; } = string.Empty;
/// <summary>
/// The accepted hash of the mandatory info content.
/// </summary>
public string AcceptedHash { get; init; } = string.Empty;
/// <summary>
/// The UTC time of the acceptance.
/// </summary>
public DateTimeOffset AcceptedAtUtc { get; init; }
/// <summary>
/// The plugin that provided the accepted info at the time of acceptance.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
}
@@ -0,0 +1,24 @@
namespace AIStudio.Settings.DataModel;
public sealed class DataMandatoryInformation
{
/// <summary>
/// Persisted user acceptances for configured mandatory infos.
/// </summary>
public List<DataMandatoryInfoAcceptance> Acceptances { get; set; } = [];
public DataMandatoryInfoAcceptance? FindAcceptance(string infoId)
{
return this.Acceptances.LastOrDefault(acceptance => string.Equals(acceptance.InfoId, infoId, StringComparison.OrdinalIgnoreCase));
}
public bool RemoveLeftOverAcceptances(IEnumerable<DataMandatoryInfo> mandatoryInfos)
{
var validInfoIds = mandatoryInfos
.Select(info => info.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var removedCount = this.Acceptances.RemoveAll(acceptance => !validInfoIds.Contains(acceptance.InfoId));
return removedCount > 0;
}
}
@@ -0,0 +1,36 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataPromptOptimizer
{
/// <summary>
/// Preselect prompt optimizer options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// Preselect the target language?
/// </summary>
public CommonLanguages PreselectedTargetLanguage { get; set; } = CommonLanguages.AS_IS;
/// <summary>
/// Preselect a custom target language when "Other" is selected?
/// </summary>
public string PreselectedOtherLanguage { get; set; } = string.Empty;
/// <summary>
/// Preselect important aspects for the optimization.
/// </summary>
public string PreselectedImportantAspects { get; set; } = string.Empty;
/// <summary>
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
/// <summary>
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
@@ -35,6 +35,28 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Check for Qwen 3.6 family:
if(modelName.StartsWith("qwen3.6"))
return
[
Capability.TEXT_INPUT, Capability.VIDEO_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Check for the 3.0 VL models:
if(modelName.IndexOf("-vl-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Check for Qwen 3:
if(modelName.StartsWith("qwen3"))
return
@@ -45,15 +67,6 @@ public static partial class ProviderExtensions
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("-vl-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
// QwQ models:
@@ -19,24 +19,68 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Mistral large latest:
if (modelName.IndexOf("mistral-large-latest") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral large:
if (modelName.IndexOf("mistral-large-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral medium latest:
if (modelName.IndexOf("mistral-medium-latest") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral medium:
if (modelName.IndexOf("mistral-medium-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral small latest:
if (modelName.IndexOf("mistral-small-latest") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
@@ -45,8 +89,10 @@ public static partial class ProviderExtensions
if (modelName.IndexOf("mistral-small-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
@@ -113,6 +113,18 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Check for Qwen 3.6 family:
if(modelName.IndexOf("qwen3.6") is not -1)
return
[
Capability.TEXT_INPUT, Capability.VIDEO_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("-vl-") is not -1)
return [
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
@@ -150,9 +162,49 @@ public static partial class ProviderExtensions
modelName.IndexOf("mistral-large-3") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("mistral-small-4") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("mistral-small-3") is not -1 ||
modelName.IndexOf("mistral-small-4") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("mistral-small-") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
@@ -305,4 +357,4 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
}
}
}
@@ -304,6 +304,43 @@ public sealed class SettingsManager
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE;
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetChatProviderForLoadedChat(string? chatProviderId = null)
{
var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT);
bool IsSelectableProvider(Provider provider) =>
provider != Provider.NONE
&& provider.UsedLLMProvider != LLMProviders.NONE
&& provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel;
Provider? FindProviderById(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
return null;
var provider = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId);
return provider is not null && IsSelectableProvider(provider) ? provider : null;
}
var chatProvider = FindProviderById(chatProviderId);
if (chatProvider is not null)
return chatProvider;
var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions
? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider)
: null;
if (defaultChatProvider is not null)
return defaultChatProvider;
var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider);
if (defaultAppProvider is not null)
return defaultAppProvider;
var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList();
return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE;
}
public Profile GetPreselectedProfile(Tools.Components component)
{
var preselection = component.GetProfilePreselection(this);