Refactored configuration objects (#538)
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2025-08-26 10:59:56 +02:00 committed by GitHub
parent be11efed67
commit c1826426d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 723 additions and 472 deletions

View File

@ -54,7 +54,7 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
#region Implementation of IAgent #region Implementation of IAgent
public abstract AIStudio.Settings.Provider? ProviderSettings { get; set; } public abstract AIStudio.Settings.Provider ProviderSettings { get; set; }
public abstract Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData); public abstract Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData);
@ -103,10 +103,9 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time) protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time)
{ {
if(this.ProviderSettings is null) if(this.ProviderSettings == Settings.Provider.NONE)
return; return;
var providerSettings = this.ProviderSettings.Value;
var aiText = new ContentText var aiText = new ContentText
{ {
// We have to wait for the remote // We have to wait for the remote
@ -127,6 +126,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), providerSettings.Model, lastUserPrompt, thread); await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(this.Logger), this.ProviderSettings.Model, lastUserPrompt, thread);
} }
} }

View File

@ -86,7 +86,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
"""; """;
/// <inheritdoc /> /// <inheritdoc />
public override Settings.Provider? ProviderSettings { get; set; } public override Settings.Provider ProviderSettings { get; set; } = Settings.Provider.NONE;
/// <summary> /// <summary>
/// The data source selection agent does not work with context. Use /// The data source selection agent does not work with context. Use
@ -141,6 +141,11 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
// We start with the provider currently selected by the user: // We start with the provider currently selected by the user:
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true); var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true);
if (agentProvider == Settings.Provider.NONE)
{
logger.LogWarning("No provider is selected for the agent. The agent cannot select data sources.");
return [];
}
// Assign the provider settings to the agent: // Assign the provider settings to the agent:
logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()})."); logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");

View File

@ -71,7 +71,7 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
"""; """;
/// <inheritdoc /> /// <inheritdoc />
public override Settings.Provider? ProviderSettings { get; set; } public override Settings.Provider ProviderSettings { get; set; } = Settings.Provider.NONE;
/// <summary> /// <summary>
/// The retrieval context validation agent does not work with context. Use /// The retrieval context validation agent does not work with context. Use
@ -133,6 +133,11 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
{ {
// We start with the provider currently selected by the user: // We start with the provider currently selected by the user:
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, provider.Id, true); var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, provider.Id, true);
if (agentProvider == Settings.Provider.NONE)
{
logger.LogWarning("No provider is selected for the agent.");
return;
}
// Assign the provider settings to the agent: // Assign the provider settings to the agent:
logger.LogInformation($"The agent for the retrieval context validation uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()})."); logger.LogInformation($"The agent for the retrieval context validation uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");

View File

@ -11,7 +11,7 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
#region Overrides of AgentBase #region Overrides of AgentBase
public override AIStudio.Settings.Provider? ProviderSettings { get; set; } public override AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
protected override Type Type => Type.SYSTEM; protected override Type Type => Type.SYSTEM;

View File

@ -12,7 +12,7 @@ public interface IAgent
/// <summary> /// <summary>
/// The provider to use for this agent. /// The provider to use for this agent.
/// </summary> /// </summary>
public AIStudio.Settings.Provider? ProviderSettings { get; set; } public Settings.Provider ProviderSettings { get; set; }
/// <summary> /// <summary>
/// Processes a chat thread (i.e., context) and returns the updated thread. /// Processes a chat thread (i.e., context) and returns the updated thread.

View File

@ -85,7 +85,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected virtual IReadOnlyList<IButtonData> FooterButtons => []; protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
protected AIStudio.Settings.Provider providerSettings; protected AIStudio.Settings.Provider providerSettings = Settings.Provider.NONE;
protected MudForm? form; protected MudForm? form;
protected bool inputIsValid; protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE; protected Profile currentProfile = Profile.NO_PROFILE;
@ -352,7 +352,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
private async Task InnerResetForm() private async Task InnerResetForm()
{ {
this.resultingContentBlock = null; this.resultingContentBlock = null;
this.providerSettings = default; this.providerSettings = Settings.Provider.NONE;
await this.JsRuntime.ClearDiv(RESULT_DIV_ID); await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID); await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);

View File

@ -5440,11 +5440,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464
-- The SETTINGS table does not exist or is not a valid table. -- The SETTINGS table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table."
-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist."
-- The CONFIG table does not exist or is not a valid table. -- The CONFIG table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table."
-- The LLM_PROVIDERS table does not exist or is not a valid table. -- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist."
-- The field IETF_TAG does not exist or is not a valid string. -- The field IETF_TAG does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string."

View File

@ -20,7 +20,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
public EventCallback<ChatThread?> ChatThreadChanged { get; set; } public EventCallback<ChatThread?> ChatThreadChanged { get; set; }
[Parameter] [Parameter]
public AIStudio.Settings.Provider Provider { get; set; } public AIStudio.Settings.Provider Provider { get; set; } = AIStudio.Settings.Provider.NONE;
[Parameter] [Parameter]
public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; } public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; }
@ -634,7 +634,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
default: default:
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER: case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
if(this.Provider == default) if(this.Provider == AIStudio.Settings.Provider.NONE)
{ {
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
await this.ProviderChanged.InvokeAsync(this.Provider); await this.ProviderChanged.InvokeAsync(this.Provider);
@ -797,7 +797,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
break; break;
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER: case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
if(this.Provider == default) if(this.Provider == AIStudio.Settings.Provider.NONE)
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break; break;
} }

View File

@ -42,6 +42,9 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
foreach (var providerId in this.Data) foreach (var providerId in this.Data)
{ {
var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value); var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value);
if (provider is null)
continue;
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return providerId; yield return providerId;
} }

View File

@ -13,7 +13,7 @@ public partial class ProviderSelection : MSGComponentBase
public AssistantBase<NoComponent>? AssistantBase { get; set; } public AssistantBase<NoComponent>? AssistantBase { get; set; }
[Parameter] [Parameter]
public AIStudio.Settings.Provider ProviderSettings { get; set; } public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
[Parameter] [Parameter]
public EventCallback<AIStudio.Settings.Provider> ProviderSettingsChanged { get; set; } public EventCallback<AIStudio.Settings.Provider> ProviderSettingsChanged { get; set; }
@ -32,7 +32,8 @@ public partial class ProviderSelection : MSGComponentBase
{ {
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);
foreach (var provider in this.SettingsManager.ConfigurationData.Providers) foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) if (provider.UsedLLMProvider != LLMProviders.NONE)
yield return provider; if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return provider;
} }
} }

View File

@ -20,7 +20,7 @@ public partial class ReadWebContent : MSGComponentBase
public EventCallback<string> ContentChanged { get; set; } public EventCallback<string> ContentChanged { get; set; }
[Parameter] [Parameter]
public AIStudio.Settings.Provider ProviderSettings { get; set; } public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
[Parameter] [Parameter]
public bool AgentIsRunning { get; set; } public bool AgentIsRunning { get; set; }
@ -34,7 +34,7 @@ public partial class ReadWebContent : MSGComponentBase
[Parameter] [Parameter]
public bool PreselectContentCleanerAgent { get; set; } public bool PreselectContentCleanerAgent { get; set; }
private Process<ReadWebContentSteps> process = Process<ReadWebContentSteps>.INSTANCE; private readonly Process<ReadWebContentSteps> process = Process<ReadWebContentSteps>.INSTANCE;
private ProcessStepValue processStep; private ProcessStepValue processStep;
private bool showWebContentReader; private bool showWebContentReader;
@ -43,7 +43,7 @@ public partial class ReadWebContent : MSGComponentBase
private bool urlIsValid; private bool urlIsValid;
private bool isProviderValid; private bool isProviderValid;
private AIStudio.Settings.Provider providerSettings; private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -86,7 +86,7 @@ public partial class ReadWebContent : MSGComponentBase
this.StateHasChanged(); this.StateHasChanged();
markdown = this.HTMLParser.ParseToMarkdown(html); markdown = this.HTMLParser.ParseToMarkdown(html);
if (this.useContentCleanerAgent) if (this.useContentCleanerAgent && this.providerSettings != AIStudio.Settings.Provider.NONE)
{ {
this.AgentTextContentCleaner.ProviderSettings = this.providerSettings; this.AgentTextContentCleaner.ProviderSettings = this.providerSettings;
var additionalData = new Dictionary<string, string> var additionalData = new Dictionary<string, string>
@ -149,7 +149,7 @@ public partial class ReadWebContent : MSGComponentBase
private string? ValidateProvider(bool shouldUseAgent) private string? ValidateProvider(bool shouldUseAgent)
{ {
if(shouldUseAgent && this.providerSettings == default) if(shouldUseAgent && this.providerSettings == AIStudio.Settings.Provider.NONE)
{ {
this.isProviderValid = false; this.isProviderValid = false;
return T("Please select a provider to use the cleanup agent."); return T("Please select a provider to use the cleanup agent.");

View File

@ -54,6 +54,9 @@ public partial class SettingsPanelProviders : SettingsPanelBase
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task EditLLMProvider(AIStudio.Settings.Provider provider) private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
{ {
if(provider == AIStudio.Settings.Provider.NONE)
return;
if (provider.IsEnterpriseConfiguration) if (provider.IsEnterpriseConfiguration)
return; return;

View File

@ -21,7 +21,7 @@ public partial class Chat : MSGComponentBase
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
private ChatThread? chatThread; private ChatThread? chatThread;
private AIStudio.Settings.Provider providerSettings; private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
private bool workspaceOverlayVisible; private bool workspaceOverlayVisible;
private string currentWorkspaceName = string.Empty; private string currentWorkspaceName = string.Empty;
private Workspaces? workspaces; private Workspaces? workspaces;

View File

@ -18,7 +18,7 @@ public partial class Writer : MSGComponentBase
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500)); private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
private MudTextField<string> textField = null!; private MudTextField<string> textField = null!;
private AIStudio.Settings.Provider providerSettings; private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
private ChatThread? chatThread; private ChatThread? chatThread;
private bool isStreaming; private bool isStreaming;
private string userInput = string.Empty; private string userInput = string.Empty;

View File

@ -5442,11 +5442,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464
-- The SETTINGS table does not exist or is not a valid table. -- The SETTINGS table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "Die Tabelle SETTINGS existiert nicht oder ist keine gültige Tabelle." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "Die Tabelle SETTINGS existiert nicht oder ist keine gültige Tabelle."
-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "Mindestens ein konfigurierter LLM-Anbieter ist ungültig oder konnte nicht verarbeitet werden, oder die Tabelle LLM_PROVIDERS existiert nicht."
-- The CONFIG table does not exist or is not a valid table. -- The CONFIG table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "Die Tabelle CONFIG existiert nicht oder ist keine gültige Tabelle." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "Die Tabelle CONFIG existiert nicht oder ist keine gültige Tabelle."
-- The LLM_PROVIDERS table does not exist or is not a valid table. -- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "Die Tabelle LLM_PROVIDERS existiert nicht oder ist keine gültige Tabelle." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "Mindestens eine konfigurierte Chat-Vorlage ist ungültig oder konnte nicht verarbeitet werden, oder die Tabelle CHAT_TEMPLATES existiert nicht."
-- The field IETF_TAG does not exist or is not a valid string. -- The field IETF_TAG does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "Das Feld IETF_TAG existiert nicht oder ist keine gültige Zeichenkette." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "Das Feld IETF_TAG existiert nicht oder ist keine gültige Zeichenkette."

View File

@ -5442,11 +5442,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464
-- The SETTINGS table does not exist or is not a valid table. -- The SETTINGS table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table."
-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist."
-- The CONFIG table does not exist or is not a valid table. -- The CONFIG table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table."
-- The LLM_PROVIDERS table does not exist or is not a valid table. -- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist."
-- The field IETF_TAG does not exist or is not a valid string. -- The field IETF_TAG does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string."

View File

@ -1,9 +1,20 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings; namespace AIStudio.Settings;
public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, string PredefinedUserPrompt, List<ContentBlock> ExampleConversation, bool AllowProfileUsage, bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default) public record ChatTemplate(
uint Num,
string Id,
string Name,
string SystemPrompt,
string PredefinedUserPrompt,
List<ContentBlock> ExampleConversation,
bool AllowProfileUsage,
bool IsEnterpriseConfiguration = false,
Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject
{ {
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false) public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false)
{ {
@ -11,6 +22,8 @@ public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
private static readonly ILogger<ChatTemplate> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatTemplate>();
public static readonly ChatTemplate NO_CHAT_TEMPLATE = new() public static readonly ChatTemplate NO_CHAT_TEMPLATE = new()
{ {
Name = TB("Use no chat template"), Name = TB("Use no chat template"),
@ -41,4 +54,96 @@ public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt
return this.SystemPrompt; return this.SystemPrompt;
} }
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
{
template = NO_CHAT_TEMPLATE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid ID. The ID must be a valid GUID.");
return false;
}
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid name.");
return false;
}
if (!table.TryGetValue("SystemPrompt", out var sysPromptValue) || !sysPromptValue.TryRead<string>(out var systemPrompt))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid system prompt.");
return false;
}
var predefinedUserPrompt = string.Empty;
if (table.TryGetValue("PredefinedUserPrompt", out var preUserValue) && preUserValue.TryRead<string>(out var preUser))
predefinedUserPrompt = preUser;
var allowProfileUsage = false;
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
allowProfileUsage = allow;
template = new ChatTemplate
{
Num = 0,
Id = id.ToString(),
Name = name,
SystemPrompt = systemPrompt,
PredefinedUserPrompt = predefinedUserPrompt,
ExampleConversation = ParseExampleConversation(idx, table),
AllowProfileUsage = allowProfileUsage,
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = configPluginId,
};
return true;
}
private static List<ContentBlock> ParseExampleConversation(int idx, LuaTable table)
{
var exampleConversation = new List<ContentBlock>();
if (!table.TryGetValue("ExampleConversation", out var exConvValue) || !exConvValue.TryRead<LuaTable>(out var exConvTable))
return exampleConversation;
var numBlocks = exConvTable.ArrayLength;
for (var j = 1; j <= numBlocks; j++)
{
var blockValue = exConvTable[j];
if (!blockValue.TryRead<LuaTable>(out var blockTable))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} is not a valid table.");
continue;
}
if (!blockTable.TryGetValue("Role", out var roleValue) || !roleValue.TryRead<string>(out var roleText) || !Enum.TryParse<ChatRole>(roleText, true, out var parsedRole))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid role.");
continue;
}
if (!blockTable.TryGetValue("Content", out var contentValue) || !contentValue.TryRead<string>(out var content))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid content message.");
continue;
}
if (string.IsNullOrWhiteSpace(content))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} contains an empty content message.");
continue;
}
exampleConversation.Add(new ContentBlock
{
Time = DateTimeOffset.UtcNow,
Role = parsedRole,
Content = new ContentText { Text = content },
ContentType = ContentType.TEXT,
HideFromUser = true,
});
}
return exampleConversation;
}
} }

View File

@ -1,26 +0,0 @@
using System.Linq.Expressions;
namespace AIStudio.Settings;
public static class ExpressionExtensions
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions));
public static MemberExpression GetMemberExpression<TIn, TOut>(this Expression<Func<TIn, TOut>> expression)
{
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;
default:
LOGGER.LogError($"Expression '{expression}' is not a valid property expression.");
throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression));
}
}
}

View File

@ -2,6 +2,10 @@ using System.Text.Json.Serialization;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Provider.HuggingFace; using AIStudio.Provider.HuggingFace;
using AIStudio.Tools.PluginSystem;
using Lua;
using Host = AIStudio.Provider.SelfHosted.Host; using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings; namespace AIStudio.Settings;
@ -16,7 +20,7 @@ namespace AIStudio.Settings;
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param> /// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param> /// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
/// <param name="Model">The LLM model to use for chat.</param> /// <param name="Model">The LLM model to use for chat.</param>
public readonly record struct Provider( public sealed record Provider(
uint Num, uint Num,
string Id, string Id,
string InstanceName, string InstanceName,
@ -27,8 +31,24 @@ public readonly record struct Provider(
Guid EnterpriseConfigurationPluginId = default, Guid EnterpriseConfigurationPluginId = default,
string Hostname = "http://localhost:1234", string Hostname = "http://localhost:1234",
Host Host = Host.NONE, Host Host = Host.NONE,
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId
{ {
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
public static readonly Provider NONE = new();
public Provider() : this(
0,
Guid.Empty.ToString(),
string.Empty,
LLMProviders.NONE,
default,
false,
false,
Guid.Empty)
{
}
#region Overrides of ValueType #region Overrides of ValueType
/// <summary> /// <summary>
@ -57,4 +77,95 @@ public readonly record struct Provider(
public string SecretName => this.InstanceName; public string SecretName => this.InstanceName;
#endregion #endregion
#region Implementation of IConfigurationObject
public override string Name
{
get => this.InstanceName;
init => this.InstanceName = value;
}
#endregion
public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider)
{
provider = NONE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID.");
return false;
}
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name.");
return false;
}
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value.");
return false;
}
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value.");
return false;
}
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname.");
return false;
}
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table.");
return false;
}
if (!TryReadModelTable(idx, modelTable, out var model))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration.");
return false;
}
provider = new Provider
{
Num = 0,
Id = id.ToString(),
InstanceName = instanceName,
UsedLLMProvider = usedLLMProvider,
Model = model,
IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED,
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = configPluginId,
Hostname = hostname,
Host = host
};
return true;
}
private static bool TryReadModelTable(int idx, LuaTable table, out Model model)
{
model = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID.");
return false;
}
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name.");
return false;
}
model = new(id, displayName);
return true;
}
} }

View File

@ -215,18 +215,18 @@ public sealed class SettingsManager
return this.ConfigurationData.Providers[0]; return this.ConfigurationData.Providers[0];
// Is there a current provider with a sufficiently high confidence level? // Is there a current provider with a sufficiently high confidence level?
Provider currentProvider = default; var currentProvider = Provider.NONE;
if (currentProviderId is not null && !string.IsNullOrWhiteSpace(currentProviderId)) if (currentProviderId is not null && !string.IsNullOrWhiteSpace(currentProviderId))
{ {
var currentProviderProbe = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == currentProviderId); var currentProviderProbe = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == currentProviderId);
if (currentProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) if (currentProviderProbe is not null && currentProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
currentProvider = currentProviderProbe; currentProvider = currentProviderProbe;
} }
// Is there a component-preselected provider with a sufficiently high confidence level? // Is there a component-preselected provider with a sufficiently high confidence level?
Provider preselectedProvider = default; var preselectedProvider = Provider.NONE;
var preselectedProviderProbe = component.PreselectedProvider(this); var preselectedProviderProbe = component.PreselectedProvider(this);
if(preselectedProviderProbe != default && preselectedProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) if(preselectedProviderProbe != Provider.NONE && preselectedProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
preselectedProvider = preselectedProviderProbe; preselectedProvider = preselectedProviderProbe;
// //
@ -234,14 +234,14 @@ public sealed class SettingsManager
// and the preselected provider is available and has a confidence level // and the preselected provider is available and has a confidence level
// that is high enough. // that is high enough.
// //
if(usePreselectionBeforeCurrentProvider && preselectedProvider != default) if(usePreselectionBeforeCurrentProvider && preselectedProvider != Provider.NONE)
return preselectedProvider; return preselectedProvider;
// //
// Case: The current provider is available and has a confidence level that is // Case: The current provider is available and has a confidence level that is
// high enough. // high enough.
// //
if(currentProvider != default) if(currentProvider != Provider.NONE)
return currentProvider; return currentProvider;
// //
@ -250,11 +250,11 @@ public sealed class SettingsManager
// level that is high enough. The preselected provider is available and // level that is high enough. The preselected provider is available and
// has a confidence level that is high enough. // has a confidence level that is high enough.
// //
if(preselectedProvider != default) if(preselectedProvider != Provider.NONE)
return preselectedProvider; return preselectedProvider;
// When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it: // When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it:
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel); return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE;
} }
public Profile GetPreselectedProfile(Tools.Components component) public Profile GetPreselectedProfile(Tools.Components component)

View File

@ -89,32 +89,37 @@ public static class ComponentsExtensions
}; };
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager)
{ {
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : default, var preselectedProvider = component switch
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : default, {
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : default, Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : default, Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : default, Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : default, Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : default, Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : default, Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : default, Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : default, Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default, Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default, Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : default, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null,
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : default, Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : default, Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : default, Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null,
_ => default, _ => Settings.Provider.NONE,
}; };
return preselectedProvider ?? Settings.Provider.NONE;
}
public static Profile PreselectedProfile(this Components component, SettingsManager settingsManager) => component switch public static Profile PreselectedProfile(this Components component, SettingsManager settingsManager) => component switch
{ {

View File

@ -0,0 +1,96 @@
using System.Linq.Expressions;
using System.Numerics;
using System.Reflection;
namespace AIStudio.Tools;
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)
{
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;
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>
{
// 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);
}
}
}

View File

@ -0,0 +1,10 @@
using System.Numerics;
namespace AIStudio.Tools;
/// <summary>
/// Represents the result of an increment operation. It encapsulates whether the operation
/// was successful and the increased value.
/// </summary>
/// <typeparam name="TOut">The type of the incremented value, constrained to implement the IBinaryInteger interface.</typeparam>
public sealed record IncrementResult<TOut>(bool Success, TOut UpdatedValue) where TOut : IBinaryInteger<TOut>;

View File

@ -0,0 +1,14 @@
namespace AIStudio.Tools;
public enum IncrementType
{
/// <summary>
/// Increments the value before returning it. So, the incremented value is returned.
/// </summary>
PRE,
/// <summary>
/// Increments the value after returning it. So, the original value is returned.
/// </summary>
POST,
}

View File

@ -0,0 +1,23 @@
namespace AIStudio.Tools.PluginSystem;
public abstract record ConfigurationBaseObject : IConfigurationObject
{
#region Implementation of IConfigurationObject
/// <inheritdoc />
public abstract string Id { get; init; }
/// <inheritdoc />
public abstract uint Num { get; init; }
/// <inheritdoc />
public abstract string Name { get; init; }
/// <inheritdoc />
public abstract bool IsEnterpriseConfiguration { get; init; }
/// <inheritdoc />
public abstract Guid EnterpriseConfigurationPluginId { get; init; }
#endregion
}

View File

@ -0,0 +1,32 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents a configuration object, such as a chat template or a LLM provider.
/// </summary>
public interface IConfigurationObject
{
/// <summary>
/// The unique ID of the configuration object.
/// </summary>
public string Id { get; }
/// <summary>
/// The continuous number of the configuration object.
/// </summary>
public uint Num { get; }
/// <summary>
/// The name of the configuration object.
/// </summary>
public string Name { get; }
/// <summary>
/// Is this configuration object an enterprise configuration?
/// </summary>
public bool IsEnterpriseConfiguration { get; }
/// <summary>
/// The ID of the enterprise configuration plugin.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; }
}

View File

@ -0,0 +1,26 @@
namespace AIStudio.Tools.PluginSystem;
public sealed record NoConfigurationObject : ConfigurationBaseObject
{
public static readonly NoConfigurationObject INSTANCE = new();
private NoConfigurationObject()
{
this.Id = Guid.Empty.ToString();
this.Name = "No Configuration";
}
#region Overrides of ConfigurationBaseObject
public override string Id { get; init; }
public override uint Num { get; init; }
public override string Name { get; init; }
public override bool IsEnterpriseConfiguration { get; init; }
public override Guid EnterpriseConfigurationPluginId { get; init; }
#endregion
}

View File

@ -1,21 +1,15 @@
using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Chat;
using Lua; using Lua;
using Host = AIStudio.Provider.SelfHosted.Host;
using Model = AIStudio.Provider.Model;
namespace AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.PluginSystem;
public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type)
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration));
private static readonly ILogger<PluginConfiguration> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginConfiguration>();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private readonly List<PluginConfigurationObject> configObjects = []; private List<PluginConfigurationObject> configObjects = [];
/// <summary> /// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
@ -51,317 +45,34 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
return false; return false;
} }
// // Check for the main SETTINGS table:
// ===========================================
// Configured settings
// ===========================================
//
if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead<LuaTable>(out var settingsTable)) if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead<LuaTable>(out var settingsTable))
{ {
message = TB("The SETTINGS table does not exist or is not a valid table."); message = TB("The SETTINGS table does not exist or is not a valid table.");
return false; return false;
} }
// Check for updates, and if so, how often? // Config: check for updates, and if so, how often?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateBehavior, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateBehavior, this.Id, settingsTable, dryRun);
// Allow the user to add providers? // Config: allow the user to add providers?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
// // Handle configured LLM providers:
// Configured providers: if (!PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun))
//
if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead<LuaTable>(out var providersTable))
{ {
message = TB("The LLM_PROVIDERS table does not exist or is not a valid table."); message = TB("At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist.");
return false;
}
// Handle configured chat templates:
if (!PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun))
{
message = TB("At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist.");
return false; return false;
} }
message = string.Empty; message = string.Empty;
var numberProviders = providersTable.ArrayLength;
var configuredProviders = new List<Settings.Provider>(numberProviders);
for (var i = 1; i <= numberProviders; i++)
{
var providerLuaTableValue = providersTable[i];
if (!providerLuaTableValue.TryRead<LuaTable>(out var providerLuaTable))
{
LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} is not a valid table.");
continue;
}
if(this.TryReadProviderTable(i, providerLuaTable, out var provider))
configuredProviders.Add(provider);
else
LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} does not contain a valid provider configuration.");
}
//
// Apply the configured providers to the system settings:
//
#pragma warning disable MWAIS0001
foreach (var configuredProvider in configuredProviders)
{
// The iterating variable is immutable, so we need to create a local copy:
var provider = configuredProvider;
// Store this provider in the config object list:
this.configObjects.Add(new()
{
ConfigPluginId = this.Id,
Id = Guid.Parse(provider.Id),
Type = PluginConfigurationObjectType.LLM_PROVIDER,
});
if (dryRun)
continue;
var providerIndex = SETTINGS_MANAGER.ConfigurationData.Providers.FindIndex(p => p.Id == provider.Id);
if (providerIndex > -1)
{
// Case: The provider already exists, we update it:
var existingProvider = SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex];
provider = provider with { Num = existingProvider.Num }; // Keep the original number
SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex] = provider;
}
else
{
// Case: The provider does not exist, we add it:
provider = provider with { Num = SETTINGS_MANAGER.ConfigurationData.NextProviderNum++ };
SETTINGS_MANAGER.ConfigurationData.Providers.Add(provider);
}
}
#pragma warning restore MWAIS0001
//
// Configured chat templates:
//
if (mainTable.TryGetValue("CHAT_TEMPLATES", out var templatesValue) && templatesValue.TryRead<LuaTable>(out var templatesTable))
{
var numberTemplates = templatesTable.ArrayLength;
var configuredTemplates = new List<ChatTemplate>(numberTemplates);
for (var i = 1; i <= numberTemplates; i++)
{
var templateLuaTableValue = templatesTable[i];
if (!templateLuaTableValue.TryRead<LuaTable>(out var templateLuaTable))
{
LOGGER.LogWarning($"The CHAT_TEMPLATES table at index {i} is not a valid table.");
continue;
}
if(this.TryReadChatTemplateTable(i, templateLuaTable, out var template) && template != ChatTemplate.NO_CHAT_TEMPLATE)
configuredTemplates.Add(template);
else
LOGGER.LogWarning($"The CHAT_TEMPLATES table at index {i} does not contain a valid chat template configuration.");
}
// Apply configured chat templates to the system settings:
foreach (var configuredTemplate in configuredTemplates)
{
// The iterating variable is immutable, so we need to create a local copy:
var template = configuredTemplate;
// Store this provider in the config object list:
this.configObjects.Add(new()
{
ConfigPluginId = this.Id,
Id = Guid.Parse(template.Id),
Type = PluginConfigurationObjectType.CHAT_TEMPLATE,
});
if (dryRun)
continue;
var tplIndex = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.FindIndex(t => t.Id == template.Id);
if (tplIndex > -1)
{
// Case: The template already exists, we update it:
var existingTemplate = SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex];
template = template with { Num = existingTemplate.Num };
SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex] = template;
}
else
{
// Case: The template does not exist, we add it:
template = template with { Num = SETTINGS_MANAGER.ConfigurationData.NextChatTemplateNum++ };
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Add(template);
}
}
}
return true; return true;
} }
private bool TryReadProviderTable(int idx, LuaTable table, out Settings.Provider provider)
{
provider = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID.");
return false;
}
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name.");
return false;
}
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value.");
return false;
}
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value.");
return false;
}
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname.");
return false;
}
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table.");
return false;
}
if (!this.TryReadModelTable(idx, modelTable, out var model))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration.");
return false;
}
provider = new()
{
Num = 0,
Id = id.ToString(),
InstanceName = instanceName,
UsedLLMProvider = usedLLMProvider,
Model = model,
IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED,
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = this.Id,
Hostname = hostname,
Host = host
};
return true;
}
private bool TryReadModelTable(int idx, LuaTable table, out Model model)
{
model = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID.");
return false;
}
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name.");
return false;
}
model = new(id, displayName);
return true;
}
private bool TryReadChatTemplateTable(int idx, LuaTable table, out ChatTemplate template)
{
template = ChatTemplate.NO_CHAT_TEMPLATE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid ID. The ID must be a valid GUID.");
return false;
}
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid name.");
return false;
}
if (!table.TryGetValue("SystemPrompt", out var sysPromptValue) || !sysPromptValue.TryRead<string>(out var systemPrompt))
{
LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid system prompt.");
return false;
}
var predefinedUserPrompt = string.Empty;
if (table.TryGetValue("PredefinedUserPrompt", out var preUserValue) && preUserValue.TryRead<string>(out var preUser))
predefinedUserPrompt = preUser;
var allowProfileUsage = false;
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
allowProfileUsage = allow;
template = new()
{
Num = 0,
Id = id.ToString(),
Name = name,
SystemPrompt = systemPrompt,
PredefinedUserPrompt = predefinedUserPrompt,
ExampleConversation = ParseExampleConversation(idx, table),
AllowProfileUsage = allowProfileUsage,
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = this.Id
};
return true;
}
private static List<ContentBlock> ParseExampleConversation(int idx, LuaTable table)
{
var exampleConversation = new List<ContentBlock>();
if (!table.TryGetValue("ExampleConversation", out var exConvValue) || !exConvValue.TryRead<LuaTable>(out var exConvTable))
return exampleConversation;
var numBlocks = exConvTable.ArrayLength;
for (var j = 1; j <= numBlocks; j++)
{
var blockValue = exConvTable[j];
if (!blockValue.TryRead<LuaTable>(out var blockTable))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} is not a valid table.");
continue;
}
if (!blockTable.TryGetValue("Role", out var roleValue) || !roleValue.TryRead<string>(out var roleText) || !Enum.TryParse<ChatRole>(roleText, true, out var parsedRole))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid role.");
continue;
}
if (!blockTable.TryGetValue("Content", out var contentValue) || !contentValue.TryRead<string>(out var content))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid content message.");
continue;
}
if (string.IsNullOrWhiteSpace(content))
{
LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} contains an empty content message.");
continue;
}
exampleConversation.Add(new ContentBlock
{
Time = DateTimeOffset.UtcNow,
Role = parsedRole,
Content = new ContentText { Text = content },
ContentType = ContentType.TEXT,
HideFromUser = true,
});
}
return exampleConversation;
}
} }

View File

@ -1,3 +1,10 @@
using System.Linq.Expressions;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Lua;
namespace AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.PluginSystem;
/// <summary> /// <summary>
@ -6,6 +13,9 @@ namespace AIStudio.Tools.PluginSystem;
/// </summary> /// </summary>
public sealed record PluginConfigurationObject public sealed record PluginConfigurationObject
{ {
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<PluginConfigurationObject>();
/// <summary> /// <summary>
/// The id of the configuration plugin to which this configuration object belongs. /// The id of the configuration plugin to which this configuration object belongs.
/// </summary> /// </summary>
@ -20,4 +30,178 @@ public sealed record PluginConfigurationObject
/// The type of the configuration object. /// The type of the configuration object.
/// </summary> /// </summary>
public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE; public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;
/// <summary>
/// Parses Lua table entries into configuration objects of the specified type, populating the
/// provided list with results.
/// </summary>
/// <typeparam name="TClass">The type of configuration object to parse, which must
/// inherit from <see cref="ConfigurationBaseObject"/>.</typeparam>
/// <param name="configObjectType">The type of configuration object to process, as specified
/// in <see cref="PluginConfigurationObjectType"/>.</param>
/// <param name="configObjectSelection">An expression to retrieve existing configuration objects from
/// the main configuration data.</param>
/// <param name="nextConfigObjectNumSelection">An expression to retrieve the next available configuration
/// object number from the main configuration data.</param>
/// <param name="mainTable">The Lua table containing entries to parse into configuration objects.</param>
/// <param name="configPluginId">The unique identifier of the plugin associated with the configuration
/// objects being parsed.</param>
/// <param name="configObjects">The list to populate with the parsed configuration objects.
/// This parameter is passed by reference.</param>
/// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
/// are not persisted.</param>
/// <returns>Returns true if parsing succeeds and configuration objects are added
/// to the list; otherwise, false.</returns>
public static bool TryParse<TClass>(
PluginConfigurationObjectType configObjectType,
Expression<Func<Data, List<TClass>>> configObjectSelection,
Expression<Func<Data, uint>> nextConfigObjectNumSelection,
LuaTable mainTable,
Guid configPluginId,
ref List<PluginConfigurationObject> configObjects,
bool dryRun
) where TClass : ConfigurationBaseObject
{
var luaTableName = configObjectType switch
{
PluginConfigurationObjectType.LLM_PROVIDER => "LLM_PROVIDERS",
PluginConfigurationObjectType.CHAT_TEMPLATE => "CHAT_TEMPLATES",
PluginConfigurationObjectType.DATA_SOURCE => "DATA_SOURCES",
PluginConfigurationObjectType.EMBEDDING_PROVIDER => "EMBEDDING_PROVIDERS",
PluginConfigurationObjectType.PROFILE => "PROFILES",
_ => null,
};
if (luaTableName is null)
{
LOG.LogError($"The configuration object type '{configObjectType}' is not supported yet.");
return false;
}
if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
{
LOG.LogWarning($"The {luaTableName} table does not exist or is not a valid table.");
return false;
}
var storedObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
var numberObjects = luaTable.ArrayLength;
ThreadSafeRandom? random = null;
for (var i = 1; i <= numberObjects; i++)
{
var luaObjectTableValue = luaTable[i];
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
{
LOG.LogWarning($"The {luaObjectTable} table at index {i} is not a valid table.");
continue;
}
var (wasParsingSuccessful, configObject) = configObjectType switch
{
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
_ => (false, NoConfigurationObject.INSTANCE)
};
if (wasParsingSuccessful)
{
// Store it in the config object list:
configObjects.Add(new()
{
ConfigPluginId = configPluginId,
Id = Guid.Parse(configObject.Id),
Type = configObjectType,
});
if (dryRun)
continue;
var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
// Case: The object already exists, we update it:
if (objectIndex > -1)
{
var existingObject = storedObjects[objectIndex];
configObject = configObject with { Num = existingObject.Num };
storedObjects[objectIndex] = (TClass)configObject;
}
// Case: The object does not exist, we have to add it
else
{
if (nextConfigObjectNumSelection.TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum })
{
// Case: Increment the next number was successful
configObject = configObject with { Num = nextNum };
storedObjects.Add((TClass)configObject);
}
else
{
// Case: The next number could not be incremented, we use a random number
random ??= new ThreadSafeRandom();
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
storedObjects.Add((TClass)configObject);
LOG.LogWarning($"The next number for the configuration object '{configObject.Name}' (id={configObject.Id}) could not be incremented. Using a random number instead.");
}
}
}
else
LOG.LogWarning($"The {luaObjectTable} table at index {i} does not contain a valid chat template configuration.");
}
return true;
}
/// <summary>
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
/// </summary>
/// <typeparam name="TClass">The type of configuration object to clean up.</typeparam>
/// <param name="configObjectType">The type of configuration object to process.</param>
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
/// <param name="availablePlugins">A list of currently available plugins.</param>
/// <param name="configObjectList">A list of all existing configuration objects.</param>
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
public static bool CleanLeftOverConfigurationObjects<TClass>(
PluginConfigurationObjectType configObjectType,
Expression<Func<Data, List<TClass>>> configObjectSelection,
IList<IAvailablePlugin> availablePlugins,
IList<PluginConfigurationObject> configObjectList) where TClass : IConfigurationObject
{
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
var leftOverObjects = new List<TClass>();
foreach (var configuredObject in configuredObjects)
{
if(!configuredObject.IsEnterpriseConfiguration)
continue;
var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
if(configObjectSourcePluginId == Guid.Empty)
continue;
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
if(templateSourcePlugin is null)
{
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings.");
leftOverObjects.Add(configuredObject);
}
if(!configObjectList.Any(configObject =>
configObject.Type == configObjectType &&
configObject.ConfigPluginId == configObjectSourcePluginId &&
configObject.Id.ToString() == configuredObject.Id))
{
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings.");
leftOverObjects.Add(configuredObject);
}
}
// Remove collected items after enumeration to avoid modifying the collection during iteration:
var wasConfigurationChanged = leftOverObjects.Count > 0;
foreach (var item in leftOverObjects.Distinct())
configuredObjects.Remove(item);
return wasConfigurationChanged;
}
} }

View File

@ -92,10 +92,10 @@ public static partial class PluginFactory
case { IsValid: false }: case { IsValid: false }:
LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix. First issue is: {plugin.Issues.FirstOrDefault()}"); LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix. First issue is: {plugin.Issues.FirstOrDefault()}");
#if DEBUG #if DEBUG
foreach (var pluginIssue in plugin.Issues) foreach (var pluginIssue in plugin.Issues)
LOG.LogError($"Plugin issue: {pluginIssue}"); LOG.LogError($"Plugin issue: {pluginIssue}");
#endif #endif
continue; continue;
case { IsMaintained: false }: case { IsMaintained: false }:
@ -125,89 +125,24 @@ public static partial class PluginFactory
// //
// ========================================================= // =========================================================
// Next, we have to clean up our settings. It is possible that a configuration plugin was removed. // Next, we have to clean up our settings. It is possible
// We have to remove the related settings as well: // that a configuration plugin was removed. We have to
// remove the related settings as well:
// ========================================================= // =========================================================
// //
var wasConfigurationChanged = false;
//
// Check LLM providers: // Check LLM providers:
// var wasConfigurationChanged = PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList);
#pragma warning disable MWAIS0001
var configuredProviders = SETTINGS_MANAGER.ConfigurationData.Providers.ToList();
foreach (var configuredProvider in configuredProviders)
{
if(!configuredProvider.IsEnterpriseConfiguration)
continue;
var providerSourcePluginId = configuredProvider.EnterpriseConfigurationPluginId;
if(providerSourcePluginId == Guid.Empty)
continue;
var providerSourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == providerSourcePluginId);
if(providerSourcePlugin is null)
{
LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is based on a plugin that is not available anymore. Removing the provider from the settings.");
SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider);
wasConfigurationChanged = true;
}
if(!configObjectList.Any(configObject =>
configObject.Type is PluginConfigurationObjectType.LLM_PROVIDER &&
configObject.ConfigPluginId == providerSourcePluginId &&
configObject.Id.ToString() == configuredProvider.Id))
{
LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is not present in the configuration plugin anymore. Removing the provider from the settings.");
SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider);
wasConfigurationChanged = true;
}
}
#pragma warning restore MWAIS0001
//
// Check chat templates: // Check chat templates:
// if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
var configuredTemplates = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.ToList(); wasConfigurationChanged = true;
foreach (var configuredTemplate in configuredTemplates)
{
if(!configuredTemplate.IsEnterpriseConfiguration)
continue;
var templateSourcePluginId = configuredTemplate.EnterpriseConfigurationPluginId; // Check for update behavior:
if(templateSourcePluginId == Guid.Empty)
continue;
var templateSourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == templateSourcePluginId);
if(templateSourcePlugin is null)
{
LOG.LogWarning($"The configured chat template '{configuredTemplate.Name}' (id={configuredTemplate.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings.");
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate);
wasConfigurationChanged = true;
}
if(!configObjectList.Any(configObject =>
configObject.Type is PluginConfigurationObjectType.CHAT_TEMPLATE &&
configObject.ConfigPluginId == templateSourcePluginId &&
configObject.Id.ToString() == configuredTemplate.Id))
{
LOG.LogWarning($"The configured chat template '{configuredTemplate.Name}' (id={configuredTemplate.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings.");
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate);
wasConfigurationChanged = true;
}
}
//
// ==========================================================
// Check all possible settings:
// ==========================================================
//
// Check for updates, and if so, how often?
if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, UpdateBehavior>(x => x.App, x => x.UpdateBehavior, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, UpdateBehavior>(x => x.App, x => x.UpdateBehavior, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; wasConfigurationChanged = true;
// Allow the user to add providers? // Check for users allowed to added providers:
if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, bool>(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, bool>(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; wasConfigurationChanged = true;

View File

@ -37,7 +37,7 @@ public sealed class DataSourceService
// does not mean anything. We cannot filter the data sources by any means. // does not mean anything. We cannot filter the data sources by any means.
// We return an empty list. Better safe than sorry. // We return an empty list. Better safe than sorry.
// //
if (selectedLLMProvider == default) if (selectedLLMProvider == Settings.Provider.NONE)
{ {
this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means."); this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means.");
return new([], []); return new([], []);