mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 02:33:38 +00:00
Let the chat template of a launcher decide its tools and data sources
This commit is contained in:
parent
b498c8e2e1
commit
845a967480
@ -51,4 +51,20 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
@* A chat template wins over what is chosen here, so the form says so before somebody picks
|
||||
something which would never take effect: *@
|
||||
@if (this.SelectedChatTemplate.DataSourceOptions is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
|
||||
@if (this.SelectedChatTemplate.ToolIds is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@T("The chosen chat template brings tools of its own, and those win over a selection made here.")
|
||||
</MudText>
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
@ -81,6 +83,19 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
/// </summary>
|
||||
private bool OpensTemporaryChat => string.IsNullOrWhiteSpace(this.WorkspaceName);
|
||||
|
||||
/// <summary>
|
||||
/// The chat template the launcher would open its chat with, as far as it is known here.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With "use chat default" chosen, this is whichever template the chat options name right now,
|
||||
/// and that may well be another one by the time somebody opens the launcher. The form therefore
|
||||
/// only says what such a template brings along instead of disabling the fields below it: a field
|
||||
/// which locks itself behind the user's back is worse than a sentence explaining the situation.
|
||||
/// </remarks>
|
||||
private ChatTemplate SelectedChatTemplate => string.IsNullOrWhiteSpace(this.ChatTemplateId)
|
||||
? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT)
|
||||
: this.SettingsManager.GetChatTemplateById(this.ChatTemplateId);
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
@ -102,10 +102,49 @@ public record ChatTemplate(
|
||||
{
|
||||
if(this.Num == uint.MaxValue)
|
||||
return string.Empty;
|
||||
|
||||
|
||||
return this.SystemPrompt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whose tools a chat started by a launcher begins with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher may name tools itself and may choose a chat template which names tools as well.
|
||||
/// When both do, the template wins as a whole — the same rule as for the data sources, so that
|
||||
/// nobody has to remember two of them.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherToolIds">The tools the launcher names itself, or null when it names none.</param>
|
||||
/// <returns>The tools to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for it.</returns>
|
||||
public static (IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped) ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds)
|
||||
{
|
||||
if (chatTemplate.ToolIds is not { } templateToolIds)
|
||||
return (launcherToolIds, false);
|
||||
|
||||
return (templateToolIds, launcherToolIds is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whose data source options a chat started by a launcher begins with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The two sides are not equally expressive: a launcher can only ever say "these sources, picked
|
||||
/// by hand", while a chat template carries the whole options and can also say "let an agent pick
|
||||
/// them for each message". Mixing them field by field would produce something neither of them
|
||||
/// asked for, so the template wins as a whole.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherOptions">The options built from the data sources the launcher names, or null when it names none.</param>
|
||||
/// <returns>The options to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for them.</returns>
|
||||
public static (DataSourceOptions? Options, bool LauncherChoiceDropped) ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions)
|
||||
{
|
||||
if (chatTemplate.DataSourceOptions is not { } templateOptions)
|
||||
return (launcherOptions, false);
|
||||
|
||||
return (templateOptions.CreateCopy(), launcherOptions is not null);
|
||||
}
|
||||
|
||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
||||
{
|
||||
template = NO_CHAT_TEMPLATE;
|
||||
|
||||
@ -45,7 +45,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
profile = Profile.NO_PROFILE;
|
||||
}
|
||||
|
||||
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(providerResult.Provider, launchConfiguration.DataSourceIds);
|
||||
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(assistantPlugin, providerResult.Provider, chatTemplate, launchConfiguration.DataSourceIds);
|
||||
var dataSourceOptions = dataSourceOptionsResult.Options;
|
||||
if (dataSourceOptions is null)
|
||||
return new(null, dataSourceOptionsResult.ErrorMessage);
|
||||
@ -75,15 +75,21 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
}
|
||||
}
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds);
|
||||
if (toolChoice.LauncherChoiceDropped)
|
||||
logger.LogWarning(
|
||||
"Assistant plugin '{PluginName}' selects the tools '{LauncherToolIds}', but its chat template '{ChatTemplateName}' names tools of its own. The chat starts with the tools of that template.",
|
||||
assistantPlugin.Name, string.Join(", ", launchConfiguration.ToolIds!), chatTemplate.GetSafeName());
|
||||
|
||||
//
|
||||
// Only the tools the user could have switched on themselves. A launcher may name one whose
|
||||
// Only the tools the user could have switched on themselves. Either side may name one whose
|
||||
// settings are incomplete — an unconfigured web search, say — and starting the chat with it
|
||||
// enabled would show a state the user cannot produce by hand and cannot fix from the chat.
|
||||
// Null keeps the chat's own defaults, which is what a launcher without tools wants.
|
||||
//
|
||||
var selectedToolIds = launchConfiguration.ToolIds is null
|
||||
var selectedToolIds = toolChoice.ToolIds is null
|
||||
? null
|
||||
: await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, launchConfiguration.ToolIds);
|
||||
: await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, toolChoice.ToolIds);
|
||||
|
||||
var chatThread = new ChatThread
|
||||
{
|
||||
@ -101,7 +107,12 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
Blocks = chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : chatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(),
|
||||
};
|
||||
|
||||
return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: launchConfiguration.DataSourceIds is not null), string.Empty);
|
||||
//
|
||||
// Whoever decided these options — the chat template or the launcher — decided them for this
|
||||
// chat. Without saying so, the chat page would replace them with the chat defaults again:
|
||||
//
|
||||
var dataSourcesWereChosen = chatTemplate.DataSourceOptions is not null || launchConfiguration.DataSourceIds is not null;
|
||||
return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty);
|
||||
}
|
||||
|
||||
private (ProviderSettings Provider, bool IsExplicit, string ErrorMessage) ResolveProvider(Guid? providerId)
|
||||
@ -167,64 +178,119 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
: new(chatTemplate, string.Empty);
|
||||
}
|
||||
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(ProviderSettings provider, IReadOnlyList<Guid>? dataSourceIds)
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList<Guid>? launcherDataSourceIds)
|
||||
{
|
||||
if (dataSourceIds is null)
|
||||
//
|
||||
// The launcher names data sources as plain IDs, and the options around them are always the
|
||||
// same ones. Building them here turns its choice into the same kind of thing the chat
|
||||
// template carries, which is what lets one rule decide between the two.
|
||||
//
|
||||
DataSourceOptions? launcherOptions = null;
|
||||
if (launcherDataSourceIds is not null)
|
||||
{
|
||||
var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions;
|
||||
launcherOptions = new DataSourceOptions
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = standardOptions.AutomaticValidation,
|
||||
PreselectedDataSourceIds = launcherDataSourceIds.Select(dataSourceId => dataSourceId.ToString()).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(chatTemplate, launcherOptions);
|
||||
if (optionsChoice.LauncherChoiceDropped)
|
||||
logger.LogWarning(
|
||||
"Assistant plugin '{PluginName}' selects the data sources '{LauncherDataSourceIds}', but its chat template '{ChatTemplateName}' brings data source options of its own. The chat starts with the data sources of that template.",
|
||||
assistantPlugin.Name, string.Join(", ", launcherDataSourceIds!), chatTemplate.GetSafeName());
|
||||
|
||||
// Neither side says anything, so the chat starts the way it would start on its own:
|
||||
if (optionsChoice.Options is not { } chosenOptions)
|
||||
return new(settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(), string.Empty);
|
||||
|
||||
return await this.CheckChosenDataSourcesAsync(provider, chosenOptions, chatTemplate.DataSourceOptions is null ? null : chatTemplate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the chosen data sources exist and may be used with the provider of the chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Opening a launcher is one click, so a source which is gone or not permitted has to be said
|
||||
/// out loud instead of being dropped quietly: nobody would see what the chat is missing. Which
|
||||
/// of the two sides chose the sources changes nothing but the wording — and that wording is the
|
||||
/// only place where the user learns which of them to go and fix.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The provider the launched chat runs with.</param>
|
||||
/// <param name="chosenOptions">The options the chat is about to start with.</param>
|
||||
/// <param name="originChatTemplate">The chat template the options came from, or null when the launcher named the sources itself.</param>
|
||||
/// <returns>The checked options, or null and a message saying why no chat was created.</returns>
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate)
|
||||
{
|
||||
//
|
||||
// There is nothing to check when data sources are switched off, and nothing to check either
|
||||
// when an agent picks them: that choice is made per message in the chat, exactly as it is
|
||||
// for a chat template the user picks by hand.
|
||||
//
|
||||
if (chosenOptions.DisableDataSources || chosenOptions.AutomaticDataSourceSelection || chosenOptions.PreselectedDataSourceIds.Count == 0)
|
||||
return new(chosenOptions, string.Empty);
|
||||
|
||||
//
|
||||
// Deciding which data sources are permitted needs an effective provider. Without one,
|
||||
// the check below would report every requested source as unavailable, which would hide
|
||||
// the actual cause from the user:
|
||||
//
|
||||
if (provider == ProviderSettings.NONE)
|
||||
return new(null, TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."));
|
||||
return new(null, originChatTemplate is null
|
||||
? TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.")
|
||||
: string.Format(TB("The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."), originChatTemplate.GetSafeName()));
|
||||
|
||||
var requestedDataSources = new List<IDataSource>(dataSourceIds.Count);
|
||||
foreach (var dataSourceId in dataSourceIds)
|
||||
var requestedDataSources = new List<IDataSource>(chosenOptions.PreselectedDataSourceIds.Count);
|
||||
foreach (var dataSourceId in chosenOptions.PreselectedDataSourceIds)
|
||||
{
|
||||
// Data sources have no lookup helper in the settings manager, so we match their ids
|
||||
// the same way the rest of the app does:
|
||||
var dataSourceIdText = dataSourceId.ToString();
|
||||
var dataSource = settingsManager.ConfigurationData.DataSources.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Id, dataSourceIdText, StringComparison.OrdinalIgnoreCase));
|
||||
string.Equals(candidate.Id, dataSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (dataSource is null)
|
||||
return new(null, string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId));
|
||||
return new(null, originChatTemplate is null
|
||||
? string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId)
|
||||
: string.Format(TB("The chat template '{0}' references data source '{1}', but that data source does not exist."), originChatTemplate.GetSafeName(), dataSourceId));
|
||||
|
||||
requestedDataSources.Add(dataSource);
|
||||
}
|
||||
|
||||
//
|
||||
// The options the launched chat will run under. We build them here already, because the
|
||||
// data-source check depends on them: they decide which agent providers take part, and an
|
||||
// agent with too little confidence makes a data source unavailable.
|
||||
// The IDs are written back from the sources they resolved to: one of them may be spelled in
|
||||
// another case than the source itself, and the chat matches its preselection literally.
|
||||
//
|
||||
var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions;
|
||||
var launchedDataSourceOptions = new DataSourceOptions
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = standardOptions.AutomaticValidation,
|
||||
PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(),
|
||||
};
|
||||
chosenOptions.PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList();
|
||||
|
||||
IReadOnlyList<IDataSource> availableDataSources;
|
||||
try
|
||||
{
|
||||
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, launchedDataSourceOptions, requestedDataSources);
|
||||
//
|
||||
// The options the launched chat will run under are what this check runs against: they
|
||||
// decide which agent providers take part, and an agent with too little confidence makes
|
||||
// a data source unavailable.
|
||||
//
|
||||
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, chosenOptions, requestedDataSources);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "The data sources configured by an assistant chat launcher could not be checked.");
|
||||
return new(null, TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created."));
|
||||
logger.LogError(exception, "The data sources an assistant chat launcher would start its chat with could not be checked.");
|
||||
return new(null, originChatTemplate is null
|
||||
? TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created.")
|
||||
: string.Format(TB("The data sources selected by the chat template '{0}' could not be checked. No chat was created."), originChatTemplate.GetSafeName()));
|
||||
}
|
||||
|
||||
var availableSelectedIds = availableDataSources.Select(source => source.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var unavailableDataSources = requestedDataSources.Where(source => !availableSelectedIds.Contains(source.Id)).Select(source => source.Name).ToList();
|
||||
if (unavailableDataSources.Count > 0)
|
||||
return new(null, string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources)));
|
||||
return new(null, originChatTemplate is null
|
||||
? string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources))
|
||||
: string.Format(TB("The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}"), originChatTemplate.GetSafeName(), string.Join(", ", unavailableDataSources)));
|
||||
|
||||
return new(launchedDataSourceOptions, string.Empty);
|
||||
return new(chosenOptions, string.Empty);
|
||||
}
|
||||
}
|
||||
165
app/Tests/Settings/ChatTemplatePrecedenceTests.cs
Normal file
165
app/Tests/Settings/ChatTemplatePrecedenceTests.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks who decides the tools and data sources when a direct chat launcher and the chat template
|
||||
/// it opens its chat with both name some.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Either side can be filled in without knowing about the other, so all four combinations happen.
|
||||
/// The rule is deliberately the same for tools and for data sources: the chat template wins as a
|
||||
/// whole, because it is the only one of the two which can also leave the choice of sources to an
|
||||
/// agent, and a field-by-field mix of both would be something neither of them asked for.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ChatTemplatePrecedenceTests
|
||||
{
|
||||
[Test]
|
||||
public void WhenNeitherSideSaysAnythingTheChatDefaultsStay()
|
||||
{
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(NewTemplate(), null);
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(NewTemplate(), null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.Null, "Nobody named any tool, so the chat has to keep using the tools of its own default.");
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False);
|
||||
Assert.That(optionsChoice.Options, Is.Null, "Nobody named any data source, so the chat has to keep using its own default options.");
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALauncherAloneDecidesForItself()
|
||||
{
|
||||
var template = NewTemplate();
|
||||
var launcherOptions = NewLauncherOptions("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, launcherOptions);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "web_search" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False, "Nothing was dropped here, so nothing may be reported as dropped either.");
|
||||
Assert.That(optionsChoice.Options, Is.SameAs(launcherOptions));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ATemplateAloneDecidesForItself()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
ToolIds = ["read_web_page"],
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, null);
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False);
|
||||
Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" }));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenBothSidesSpeakTheTemplateWinsAndTheLossIsReported()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
ToolIds = ["read_web_page"],
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.True, "The tools of the launcher are gone, and only this flag can make the log say so.");
|
||||
Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" }));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ATemplateWhichWantsNoToolsWinsJustTheSame()
|
||||
{
|
||||
var template = NewTemplate() with { ToolIds = [] };
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.Empty, "An empty selection is the statement that this template wants no tools, which is as much of a statement as naming one.");
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheAgenticSelectionOfATemplateSurvivesALauncherWithItsOwnSources()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true, PreselectedDataSourceIds = [] },
|
||||
};
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(optionsChoice.Options!.AutomaticDataSourceSelection, Is.True, "Letting an agent pick the sources is the one thing a launcher cannot express, so it is exactly what must not be overwritten by one.");
|
||||
Assert.That(optionsChoice.Options.PreselectedDataSourceIds, Is.Empty);
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheChosenOptionsAreACopyRatherThanTheOnesOfTheTemplate()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null);
|
||||
optionsChoice.Options!.PreselectedDataSourceIds.Clear();
|
||||
|
||||
Assert.That(template.DataSourceOptions!.PreselectedDataSourceIds, Is.Not.Empty, "The launched chat goes on to change these options, and the template is a setting of the user which must not change with it.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A template which says nothing about tools or data sources.
|
||||
/// </summary>
|
||||
private static ChatTemplate NewTemplate() => new()
|
||||
{
|
||||
Num = 1,
|
||||
Id = "33333333-3333-3333-3333-333333333333",
|
||||
Name = "Intranet Research",
|
||||
SystemPrompt = "You are a research assistant.",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The options a launcher which names data sources ends up with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher has no switches of its own: it names sources, and the rest is always this. Which
|
||||
/// is the whole reason the chat template wins whenever both of them speak.
|
||||
/// </remarks>
|
||||
private static DataSourceOptions NewLauncherOptions(params string[] dataSourceIds) => new()
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = false,
|
||||
PreselectedDataSourceIds = [..dataSourceIds],
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user