Let a configured chat template carry tools and data sources

This commit is contained in:
Thorsten Sommer 2026-09-21 10:11:39 +02:00
parent c95cc5bacc
commit 6a2a8c4e9d
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
2 changed files with 161 additions and 0 deletions

View File

@ -1034,6 +1034,56 @@ CONFIG["CHAT_TEMPLATES"] = {}
-- }
-- }
-- An example chat template which preselects tools and data sources:
-- Both are optional and independent of each other. Leaving a field out is not the same as
-- leaving it empty:
--
-- ToolIds omitted -> the chat starts with the tools set as its default
-- ToolIds = {} -> the chat starts with no tools at all
-- DataSourceOptions omitted -> the chat starts with the data source defaults
-- DataSourceOptions = { ... } -> the chat starts with exactly what this table says
--
-- Both are a preselection, not a limit: users change either of them in the chat as usual.
-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000002",
-- ["Name"] = "Intranet Research",
-- ["SystemPrompt"] = "You are <Company Name>'s research assistant. Answer from our own documents and say where each answer comes from.",
-- ["AllowProfileUsage"] = true,
--
-- -- Optional: the tools a chat with this template starts with, by tool ID.
-- -- A tool ID unknown to the installation is ignored, and so is a tool your
-- -- organization switched off. A tool has to meet the confidence requirements of the
-- -- provider in use, so it may stay unavailable even though this template names it.
-- -- Tool IDs include: web_search, read_web_page
-- ["ToolIds"] = {
-- "read_web_page",
-- },
--
-- -- Optional: the data source options a chat with this template starts with.
-- -- Every field inside is optional as well. DisableDataSources defaults to false here,
-- -- because writing this table at all says that the template wants data sources; the
-- -- other three default to false and an empty list.
-- ["DataSourceOptions"] = {
-- -- Set to true to start the chat with data sources switched off.
-- ["DisableDataSources"] = false,
--
-- -- Let an agent choose the fitting data sources for each question. When true,
-- -- PreselectedDataSourceIds is not used.
-- ["AutomaticDataSourceSelection"] = false,
--
-- -- Let an agent check whether the retrieved data fits the question.
-- ["AutomaticValidation"] = true,
--
-- -- Must contain IDs from CONFIG["DATA_SOURCES"] or user-configured data sources.
-- -- IDs from another configuration of your organization work as well: they are
-- -- resolved against every known data source, not only against the ones defined
-- -- here. IDs that resolve to nothing are ignored.
-- ["PreselectedDataSourceIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- },
-- },
-- }
-- Introduction texts shown as expansion panels on the welcome page:
CONFIG["INTRODUCTIONS"] = {}

View File

@ -1,6 +1,7 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using SharedTools;
@ -26,6 +27,29 @@ public record ChatTemplate(
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false)
{
}
/// <summary>
/// The tools this template preselects for a chat started with it.
/// </summary>
/// <remarks>
/// Null means the template says nothing about tools, so the chat starts with the tools chosen
/// as its default in the app settings. An empty set is the opposite statement: this template
/// wants no tools at all, whatever that default says.<br/><br/>
/// A preselection, not a limit: the user changes the selection in the chat as usual, and a
/// tool still has to meet the confidence requirements of the provider in use.
/// </remarks>
public HashSet<string>? ToolIds { get; init; }
/// <summary>
/// The data source options a chat started with this template begins with.
/// </summary>
/// <remarks>
/// Null means the template says nothing, so the chat starts with the data source defaults from
/// the app settings. Anything else is the template's own answer, and it carries more than a
/// list of sources: whether data sources are used at all, whether an agent picks them, and
/// whether the retrieved data is validated.
/// </remarks>
public DataSourceOptions? DataSourceOptions { get; init; }
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
@ -41,6 +65,8 @@ public record ChatTemplate(
ExampleConversation = [],
FileAttachments = [],
AllowProfileUsage = true,
ToolIds = null,
DataSourceOptions = null,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
};
@ -121,6 +147,8 @@ public record ChatTemplate(
ExampleConversation = ParseExampleConversation(idx, table),
FileAttachments = fileAttachments,
AllowProfileUsage = allowProfileUsage,
ToolIds = ParseToolIds(idx, table),
DataSourceOptions = ParseDataSourceOptions(idx, table),
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = configPluginId,
};
@ -175,6 +203,89 @@ public record ChatTemplate(
return exampleConversation;
}
/// <remarks>
/// A missing list and an empty one mean different things here, so an empty one must not fall
/// back to null: the template then states that it wants no tools. The assistant plugins reject
/// an empty list instead, because there it carries no meaning at all.
/// </remarks>
private static HashSet<string>? ParseToolIds(int idx, LuaTable table)
{
if (!table.TryGetValue("ToolIds", out var toolIdsValue) || !toolIdsValue.TryRead<LuaTable>(out var toolIdsTable))
return null;
var toolIds = new HashSet<string>(StringComparer.Ordinal);
var numToolIds = toolIdsTable.ArrayLength;
for (var toolNum = 1; toolNum <= numToolIds; toolNum++)
{
if (!toolIdsTable[toolNum].TryRead<string>(out var toolId) || string.IsNullOrWhiteSpace(toolId))
{
LOGGER.LogWarning("The ToolIds entry {ToolNum} in chat template {IdxChatTemplate} is not a valid tool ID and will be ignored.", toolNum, idx);
continue;
}
toolIds.Add(toolId.Trim());
}
return toolIds;
}
private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table)
{
if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead<LuaTable>(out var optionsTable))
return null;
//
// Writing this table at all is already the statement that the template wants data sources,
// hence the switch starts enabled here. Everywhere else in the app, data sources start
// switched off.
//
var disableDataSources = false;
if (optionsTable.TryGetValue("DisableDataSources", out var disableValue) && disableValue.TryRead<bool>(out var disable))
disableDataSources = disable;
var automaticSelection = false;
if (optionsTable.TryGetValue("AutomaticDataSourceSelection", out var automaticSelectionValue) && automaticSelectionValue.TryRead<bool>(out var automaticSelectionFlag))
automaticSelection = automaticSelectionFlag;
var automaticValidation = false;
if (optionsTable.TryGetValue("AutomaticValidation", out var automaticValidationValue) && automaticValidationValue.TryRead<bool>(out var automaticValidationFlag))
automaticValidation = automaticValidationFlag;
return new DataSourceOptions
{
DisableDataSources = disableDataSources,
AutomaticDataSourceSelection = automaticSelection,
AutomaticValidation = automaticValidation,
PreselectedDataSourceIds = ParsePreselectedDataSourceIds(idx, optionsTable),
};
}
/// <remarks>
/// The IDs stay strings instead of being parsed as GUIDs: a data source of another
/// configuration may carry an ID which is none, and rejecting it here would make it
/// unreferenceable for no gain.
/// </remarks>
private static List<string> ParsePreselectedDataSourceIds(int idx, LuaTable optionsTable)
{
var dataSourceIds = new List<string>();
if (!optionsTable.TryGetValue("PreselectedDataSourceIds", out var idsValue) || !idsValue.TryRead<LuaTable>(out var idsTable))
return dataSourceIds;
var numIds = idsTable.ArrayLength;
for (var idNum = 1; idNum <= numIds; idNum++)
{
if (!idsTable[idNum].TryRead<string>(out var dataSourceId) || string.IsNullOrWhiteSpace(dataSourceId))
{
LOGGER.LogWarning("The PreselectedDataSourceIds entry {IdNum} in chat template {IdxChatTemplate} is not a valid data source ID and will be ignored.", idNum, idx);
continue;
}
dataSourceIds.Add(dataSourceId.Trim());
}
return dataSourceIds;
}
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table, string pluginPath)
{
var fileAttachments = new List<FileAttachment>();