Allow chat templates in config plugins (#537)
Some checks failed
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Has been cancelled
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) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Has been cancelled
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) Has been cancelled
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) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled

This commit is contained in:
Thorsten Sommer 2025-08-18 20:40:52 +02:00 committed by GitHub
parent 7d10bb00f7
commit be11efed67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 363 additions and 28 deletions

17
.junie/guidelines.md Normal file
View File

@ -0,0 +1,17 @@
# Project Guidelines
## Repository Structure
- The repository and the app consist of a Rust project in the `runtime` folder and a .NET solution in the `app` folder.
- The .NET solution then contains 4 .NET projects:
- `Build Script` is not required for running the app; instead, it contains the build script for creating new releases, for example.
- `MindWork AI Studio` contains the actual app code.
- `SharedTools` contains types that are needed in the build script and in the app, for example.
- `SourceCodeRules` is a Roslyn analyzer project. It contains analyzers and code fixes that we use to enforce code style rules within the team.
## Changelogs
- There is a changelog in Markdown format for each version.
- All changelogs are located in the folder `app/MindWork AI Studio/wwwroot/changelog`.
- These changelogs are intended for end users, not for developers.
- Therefore, we don't mention all changes in the changelog: changes that end users wouldn't understand remain unmentioned. For complex refactorings, for example, we mention a generic point that the code quality has been improved to enhance future maintenance.
- The changelog is always written in US English.
- The changelog doesn't mention bug fixes if the bug was never shipped and users don't know about it.

View File

@ -3415,6 +3415,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Close"
-- This template is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
-- Edit Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"

View File

@ -109,7 +109,7 @@ public sealed record ChatThread
else
{
var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatThread.SelectedChatTemplate);
if(chatTemplate == default)
if(chatTemplate == null)
systemPromptTextWithChatTemplate = chatThread.SystemPrompt;
else
{

View File

@ -327,7 +327,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
{
this.currentChatTemplate = chatTemplate;
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
if(this.ChatThread is null)
return;
@ -435,7 +437,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
DataSourceOptions = this.earlyDataSourceOptions,
Name = this.ExtractThreadName(this.userInput),
Seed = this.RNG.Next(),
Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
};
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
@ -673,7 +675,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
};
}
@ -813,9 +815,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Try to select the chat template:
if (!string.IsNullOrWhiteSpace(chatChatTemplate))
{
this.currentChatTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate);
if(this.currentChatTemplate == default)
this.currentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
var selectedTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate);
this.currentChatTemplate = selectedTemplate ?? ChatTemplate.NO_CHAT_TEMPLATE;
}
}

View File

@ -54,6 +54,9 @@ public partial class SettingsPanelProviders : SettingsPanelBase
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
{
if (provider.IsEnterpriseConfiguration)
return;
var dialogParameters = new DialogParameters<ProviderDialog>
{
{ x => x.DataNum, provider.Num },

View File

@ -129,6 +129,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
PredefinedUserPrompt = this.PredefinedUserPrompt,
ExampleConversation = this.dataExampleConversation,
AllowProfileUsage = this.AllowProfileUsage,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
};
private void RemoveMessage(ContentBlock item)

View File

@ -31,6 +31,14 @@
<MudTd>@context.Num</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This template is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
}
else
{
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditChatTemplate(context)"/>
@ -39,6 +47,7 @@
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteChatTemplate(context)"/>
</MudTooltip>
</MudStack>
}
</MudTd>
</RowTemplate>
</MudTable>

View File

@ -53,6 +53,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
private async Task EditChatTemplate(ChatTemplate chatTemplate)
{
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
return;
var dialogParameters = new DialogParameters<ChatTemplateDialog>
{
{ x => x.DataNum, chatTemplate.Num },

View File

@ -69,3 +69,27 @@ CONFIG["SETTINGS"] = {}
-- Configure the user permission to add providers:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
-- Example chat templates for this configuration:
CONFIG["CHAT_TEMPLATES"] = {}
-- A simple example chat template:
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "00000000-0000-0000-0000-000000000000",
["Name"] = "<user-friendly name of the chat template>",
["SystemPrompt"] = "You are <Company Name>'s helpful AI assistant for <Department Name>. Your task is ...",
["PredefinedUserPrompt"] = "Please help me with ...",
["AllowProfileUsage"] = true,
["ExampleConversation"] = {
{
-- Allowed values are: USER, AI, SYSTEM
["Role"] = "USER",
["Content"] = "Hello! Can you help me with a quick task?"
},
{
-- Allowed values are: USER, AI, SYSTEM
["Role"] = "AI",
["Content"] = "Of course. What do you need?"
}
}
}

View File

@ -3417,6 +3417,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Schließen"
-- This template is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet."
-- Edit Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten"
@ -4584,7 +4587,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Letztes Änderungsproto
-- Choose the provider and model best suited for your current task.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Wählen Sie den Anbieter und das Modell aus, die am besten zu ihrer aktuellen Aufgabe passen."
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar."
-- Quick Start Guide

View File

@ -3417,6 +3417,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Close"
-- This template is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
-- Edit Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
@ -4584,7 +4587,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog"
-- Choose the provider and model best suited for your current task.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Choose the provider and model best suited for your current task."
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."
-- Quick Start Guide

View File

@ -3,8 +3,12 @@ using AIStudio.Tools.PluginSystem;
namespace AIStudio.Settings;
public readonly record struct ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, string PredefinedUserPrompt, List<ContentBlock> ExampleConversation, bool AllowProfileUsage)
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 ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false)
{
}
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
public static readonly ChatTemplate NO_CHAT_TEMPLATE = new()
@ -16,6 +20,8 @@ public readonly record struct ChatTemplate(uint Num, string Id, string Name, str
Num = uint.MaxValue,
ExampleConversation = [],
AllowProfileUsage = true,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
};
#region Overrides of ValueType

View File

@ -270,11 +270,11 @@ public sealed class SettingsManager
public ChatTemplate GetPreselectedChatTemplate(Tools.Components component)
{
var preselection = component.PreselectedChatTemplate(this);
if (preselection != default)
if (preselection != ChatTemplate.NO_CHAT_TEMPLATE)
return preselection;
preselection = this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedChatTemplate);
return preselection != default ? preselection : ChatTemplate.NO_CHAT_TEMPLATE;
return preselection ?? ChatTemplate.NO_CHAT_TEMPLATE;
}
public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider)

View File

@ -133,8 +133,8 @@ public static class ComponentsExtensions
public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch
{
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : default,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) ?? ChatTemplate.NO_CHAT_TEMPLATE : ChatTemplate.NO_CHAT_TEMPLATE,
_ => default,
_ => ChatTemplate.NO_CHAT_TEMPLATE,
};
}

View File

@ -1,5 +1,6 @@
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Chat;
using Lua;
@ -14,6 +15,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private static readonly ILogger<PluginConfiguration> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginConfiguration>();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private readonly List<PluginConfigurationObject> configObjects = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
public async Task InitializeAsync(bool dryRun)
{
if(!this.TryProcessConfiguration(dryRun, out var issue))
@ -29,11 +37,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// <summary>
/// Tries to initialize the UI text content of the plugin.
/// </summary>
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <param name="message">The error message, when the UI text content could not be read.</param>
/// <returns>True, when the UI text content could be read successfully.</returns>
private bool TryProcessConfiguration(bool dryRun, out string message)
{
this.configObjects.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.state.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
{
@ -59,7 +69,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
//
// Configured providers
// Configured providers:
//
if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead<LuaTable>(out var providersTable))
{
@ -94,6 +104,17 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// 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)
{
@ -109,8 +130,65 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
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;
}
@ -194,4 +272,96 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
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

@ -0,0 +1,23 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents metadata for a configuration object from a configuration plugin. These are
/// complex objects such as configured LLM providers, chat templates, etc.
/// </summary>
public sealed record PluginConfigurationObject
{
/// <summary>
/// The id of the configuration plugin to which this configuration object belongs.
/// </summary>
public required Guid ConfigPluginId { get; init; } = Guid.NewGuid();
/// <summary>
/// The id of the configuration object, e.g., the id of a chat template.
/// </summary>
public required Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The type of the configuration object.
/// </summary>
public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;
}

View File

@ -0,0 +1,13 @@
namespace AIStudio.Tools.PluginSystem;
public enum PluginConfigurationObjectType
{
NONE,
UNKNOWN,
PROFILE,
DATA_SOURCE,
LLM_PROVIDER,
CHAT_TEMPLATE,
EMBEDDING_PROVIDER,
}

View File

@ -40,6 +40,8 @@ public static partial class PluginFactory
if (!await PLUGIN_LOAD_SEMAPHORE.WaitAsync(0, cancellationToken))
return;
var configObjectList = new List<PluginConfigurationObject>();
try
{
LOG.LogInformation("Start loading plugins.");
@ -112,7 +114,8 @@ public static partial class PluginFactory
}
// Start or restart all plugins:
await RestartAllPlugins(cancellationToken);
var configObjects = await RestartAllPlugins(cancellationToken);
configObjectList.AddRange(configObjects);
}
finally
{
@ -149,9 +152,51 @@ public static partial class PluginFactory
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:
//
var configuredTemplates = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.ToList();
foreach (var configuredTemplate in configuredTemplates)
{
if(!configuredTemplate.IsEnterpriseConfiguration)
continue;
var templateSourcePluginId = configuredTemplate.EnterpriseConfigurationPluginId;
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:

View File

@ -11,9 +11,10 @@ public static partial class PluginFactory
/// </summary>
public static IReadOnlyCollection<PluginBase> RunningPlugins => RUNNING_PLUGINS;
private static async Task RestartAllPlugins(CancellationToken cancellationToken = default)
private static async Task<List<PluginConfigurationObject>> RestartAllPlugins(CancellationToken cancellationToken = default)
{
LOG.LogInformation("Try to start or restart all plugins.");
var configObjects = new List<PluginConfigurationObject>();
RUNNING_PLUGINS.Clear();
//
@ -65,8 +66,13 @@ public static partial class PluginFactory
{
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION)
if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin)
{
if (plugin is PluginConfiguration configPlugin)
configObjects.AddRange(configPlugin.ConfigObjects);
RUNNING_PLUGINS.Add(plugin);
}
}
catch (Exception e)
{
LOG.LogError(e, $"An error occurred while starting the plugin: Id='{availablePlugin.Id}', Type='{availablePlugin.Type}', Name='{availablePlugin.Name}', Version='{availablePlugin.Version}'.");
@ -75,6 +81,7 @@ public static partial class PluginFactory
// Inform all components that the plugins have been reloaded or started:
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
return configObjects;
}
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)

View File

@ -1,3 +1,5 @@
# v0.9.51, build 226 (2025-08-xx xx:xx UTC)
- Added support for predefined chat templates in configuration plugins to help enterprises roll out consistent templates across the organization.
- Improved memory usage in several areas of the app.
- Improved plugin management for configuration plugins so that hot reload detects when a provider or chat template has been removed.
- Fixed a bug in various assistants where some text fields were not reset when resetting.