Multiple Profiles can now be selected

This commit is contained in:
Peer Hogeterp committed 2026-09-07 16:09:00 +02:00
1 parent d043fbc8f0
commit cefc5464d5
89 files changed
+1360 -503

No files matched your search

@@ -207,23 +207,23 @@ public static class ComponentsExtensions
{
var storedValue = component switch
{
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfileIds : null,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfileIds : null,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfileIds : null,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfileIds : null,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfileIds : null,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfileIds : null,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfileIds : null,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfileIds : null,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfileIds,
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty,
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfileIds : null,
// The Document Analysis Assistant does not have a preselected profile at the component level.
// The profile is selected per policy instead. We do this inside the Document Analysis Assistant component:
Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id,
Components.DOCUMENT_ANALYSIS_ASSISTANT => [],
_ => string.Empty,
_ => null,
};
return ProfilePreselection.FromStoredValue(storedValue);
@@ -236,4 +236,4 @@ public static class ComponentsExtensions
_ => ChatTemplate.NO_CHAT_TEMPLATE,
};
}
}
@@ -1,4 +1,5 @@
namespace AIStudio.Tools.PluginSystem.Assistants;
/// <param name="ProfileIds">The exact profiles to use, an empty list for none, or null for chat defaults.</param>
/// <param name="ToolIds">The tools preselected for the chat, or null when the launcher names none.</param>
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, Guid? ProfileId, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds, IReadOnlyList<string>? ToolIds);
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, IReadOnlyList<Guid>? ProfileIds, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds, IReadOnlyList<string>? ToolIds);
@@ -169,14 +169,20 @@ public static class DirectChatLauncherLuaWriter
builder.AppendLine($" [\"WorkspaceName\"] = \"{Escape(definition.Launch.WorkspaceName.Trim())}\",");
//
// Omitted IDs mean "use the chat defaults", while an empty GUID explicitly selects no
// profile or no chat template. An empty provider GUID has no such meaning and is invalid:
// Omitted IDs mean "use the chat defaults", while an empty profile list or chat-template
// GUID explicitly selects none. An empty provider GUID has no such meaning and is invalid:
//
if (definition.Launch.ProviderId is { } providerId && providerId != Guid.Empty)
builder.AppendLine($" [\"ProviderId\"] = \"{providerId}\",");
if (definition.Launch.ProfileId is { } profileId)
builder.AppendLine($" [\"ProfileId\"] = \"{profileId}\",");
if (definition.Launch.ProfileIds is { } profileIds)
{
builder.AppendLine(" [\"ProfileIds\"] = {");
foreach (var profileId in profileIds)
builder.AppendLine($" \"{profileId}\",");
builder.AppendLine(" },");
}
if (definition.Launch.ChatTemplateId is { } chatTemplateId)
builder.AppendLine($" [\"ChatTemplateId\"] = \"{chatTemplateId}\",");
@@ -236,4 +242,4 @@ public static class DirectChatLauncherLuaWriter
.Replace("\r", "\\r", StringComparison.Ordinal)
.Replace("\n", "\\n", StringComparison.Ordinal)
.Replace("\t", "\\t", StringComparison.Ordinal);
}
}
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
using Lua;
@@ -241,13 +242,13 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
}
if (!TryReadOptionalGuid(assistantTable, "ProviderId", false, out var providerId, out message) ||
!TryReadOptionalGuid(assistantTable, "ProfileId", true, out var profileId, out message) ||
!TryReadOptionalProfileIds(assistantTable, out var profileIds, out message) ||
!TryReadOptionalGuid(assistantTable, "ChatTemplateId", true, out var chatTemplateId, out message) ||
!TryReadOptionalDataSourceIds(assistantTable, out var dataSourceIds, out message) ||
!TryReadOptionalToolIds(assistantTable, out var toolIds, out message))
return false;
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileId, chatTemplateId, dataSourceIds, toolIds);
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileIds, chatTemplateId, dataSourceIds, toolIds);
return true;
@@ -309,6 +310,56 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
return true;
}
private static bool TryReadOptionalProfileIds(LuaTable assistantTable, out IReadOnlyList<Guid>? profileIds, out string message)
{
profileIds = null;
message = string.Empty;
var hasLegacyId = assistantTable.TryGetValue("ProfileId", out _);
var hasIds = assistantTable.TryGetValue("ProfileIds", out var profileIdsValue);
if (hasLegacyId && hasIds)
{
message = TB("The ASSISTANT table contains both ProfileId and ProfileIds. Use only one of them.");
return false;
}
if (hasLegacyId)
{
if (!TryReadOptionalGuid(assistantTable, "ProfileId", true, out var profileId, out message))
return false;
profileIds = profileId == Guid.Empty ? [] : profileId is { } id ? [id] : null;
return true;
}
if (!hasIds)
return true;
if (!profileIdsValue.TryRead<LuaTable>(out var profileIdsTable))
{
message = TB("The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs.");
return false;
}
var parsedIds = new List<Guid>(profileIdsTable.ArrayLength);
var uniqueIds = new HashSet<Guid>();
for (var index = 1; index <= profileIdsTable.ArrayLength; index++)
{
if (!profileIdsTable[index].TryRead<string>(out var idText) ||
!Guid.TryParse(idText, out var parsedId) ||
parsedId == Guid.Empty ||
!uniqueIds.Add(parsedId))
{
message = TB("The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs.");
return false;
}
parsedIds.Add(parsedId);
}
profileIds = parsedIds.ToImmutableArray();
return true;
}
/// <summary>
/// Reads the tools an assistant names: the ones a launcher preselects for its chat, or the ones
/// the assistant itself runs with.
@@ -383,12 +434,14 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
InitializeState(this.RootComponent.Children, assistantState);
var input = assistantState.ToLuaTable(this.RootComponent?.Children ?? []);
input["profiles"] = new LuaTable();
input["profile"] = new LuaTable
{
["Name"] = string.Empty,
["NeedToKnow"] = string.Empty,
["Actions"] = string.Empty,
["Num"] = 0,
["Id"] = Profile.NO_PROFILE.Id,
["Name"] = Profile.NO_PROFILE.Name,
["NeedToKnow"] = Profile.NO_PROFILE.NeedToKnow,
["Actions"] = Profile.NO_PROFILE.Actions,
["Num"] = Profile.NO_PROFILE.Num,
};
var prompt = await this.TryBuildPromptAsync(input, cancellationToken);
@@ -210,6 +210,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
if (!TryValidateMinimumProviderConfidenceConfiguration(settingsTable, out message))
return false;
if (!TryValidateProfilePreselectionConfiguration(settingsTable, out message))
return false;
this.DeclaredSettingsCount = CountDeclaredSettings(settingsTable);
// Config: check for updates, and if so, how often?
@@ -346,13 +349,29 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected profile?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected profiles? The singular name remains a compatibility alias.
var appProfilesValid = settingsTable.TryGetValue("DataApp.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfileIds(x => x.App, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataApp.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfileIds(x => x.App, x => x.PreselectedProfileIds, "DataApp.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!appProfilesValid)
{
message = TB("The configured app profile preselection is invalid.");
return false;
}
// Config: preselected chat options?
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProfile, this.Id, settingsTable, dryRun);
var chatProfilesValid = settingsTable.TryGetValue("DataChat.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfilePreselection(x => x.Chat, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataChat.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfilePreselection(x => x.Chat, x => x.PreselectedProfileIds, "DataChat.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!chatProfilesValid)
{
message = TB("The configured chat profile preselection is invalid.");
return false;
}
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedChatTemplate, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourcesDisabled, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, this.Id, settingsTable, dryRun);
@@ -360,6 +379,16 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourceIds, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.SendToChatDataSourceBehavior, this.Id, settingsTable, dryRun);
var visualBriefingProfilesValid = settingsTable.TryGetValue("DataVisualBriefing.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfilePreselection(x => x.VisualBriefing, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataVisualBriefing.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfilePreselection(x => x.VisualBriefing, x => x.PreselectedProfileIds, "DataVisualBriefing.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!visualBriefingProfilesValid)
{
message = TB("The configured visual briefing profile preselection is invalid.");
return false;
}
// Config: Batch Processing Assistant defaults?
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.InputDirectory, this.Id, settingsTable, dryRun);
@@ -391,6 +420,27 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
return true;
}
private static bool TryValidateProfilePreselectionConfiguration(LuaTable settings, out string message)
{
foreach (var settingPrefix in new[] { "DataApp", "DataChat", "DataVisualBriefing" })
{
var singularName = $"{settingPrefix}.PreselectedProfile";
var pluralName = $"{settingPrefix}.PreselectedProfileIds";
if (settings.TryGetValue(singularName, out _) && settings.TryGetValue(pluralName, out _))
{
message = string.Format(TB("The SETTINGS table contains both '{0}' and '{1}'. Use only one of them."), singularName, pluralName);
return false;
}
var legacyOverrideName = $"{singularName}.AllowUserOverride";
if (settings.TryGetValue(legacyOverrideName, out var legacyOverride))
settings[$"{pluralName}.AllowUserOverride"] = legacyOverride;
}
message = string.Empty;
return true;
}
private static bool TryValidateMinimumProviderConfidenceConfiguration(LuaTable settingsTable, out string message)
{
const string SETTING_NAME = "DataTools.MinimumProviderConfidenceByToolId";
@@ -5,8 +5,8 @@ namespace AIStudio.Tools.Services;
/// </summary>
/// <param name="WorkspaceName">The workspace the chat is created in.</param>
/// <param name="ProviderId">The provider to preselect, or null for the chat default.</param>
/// <param name="ProfileId">The profile to preselect; the empty GUID selects no profile.</param>
/// <param name="ProfileIds">The exact profiles to use, an empty list for none, or null for chat defaults.</param>
/// <param name="ChatTemplateId">The chat template to preselect; the empty GUID selects none.</param>
/// <param name="DataSourceIds">The data sources to preselect, or null for the chat defaults.</param>
/// <param name="ToolIds">The tools to preselect, or null for the chat defaults.</param>
public sealed record AssistantBuilderChatLaunchRequest(string WorkspaceName, string? ProviderId, string? ProfileId, string? ChatTemplateId, IReadOnlyList<string>? DataSourceIds, IReadOnlyList<string>? ToolIds);
public sealed record AssistantBuilderChatLaunchRequest(string WorkspaceName, string? ProviderId, IReadOnlyList<string>? ProfileIds, string? ChatTemplateId, IReadOnlyList<string>? DataSourceIds, IReadOnlyList<string>? ToolIds);
@@ -164,7 +164,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
new(
chatLaunch.WorkspaceName.Trim(),
ParseOptionalGuid(chatLaunch.ProviderId),
ParseOptionalGuid(chatLaunch.ProfileId),
chatLaunch.ProfileIds?.Select(Guid.Parse).ToArray(),
ParseOptionalGuid(chatLaunch.ChatTemplateId),
chatLaunch.DataSourceIds?.Select(Guid.Parse).ToArray(),
chatLaunch.ToolIds));
@@ -505,9 +505,9 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
: $$"""
- Describe a direct chat launcher, not a form assistant.
- Copy the structured ChatLaunch selections faithfully into the {{TB("Chat Launcher")}}, {{TB("Workspace")}}, {{TB("Chat Configuration")}}, {{TB("Data Sources")}}, and {{TB("Tools")}} sections.
- Explain omitted provider, profile, template, data-source, or tool values as using the normal chat defaults.
- Explain omitted provider, profiles, template, data-source, or tool values as using the normal chat defaults.
- In the {{TB("Tools")}} section, say what the preselected tools let the chat do and that users may change the selection once the chat is open.
- Explain the empty profile/template GUID as explicitly selecting no profile/template.
- Explain an empty profile list as explicitly selecting no profiles and the empty template GUID as selecting no template.
- Do not propose UI components, submit behavior, BuildPrompt, or a plugin SystemPrompt for a chat launcher.
""";
@@ -619,9 +619,9 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
- Set assistant.kind to "CHAT_LAUNCHER" exactly when the revised ASSISTANT table uses LaunchBehavior = "OPEN_WORKSPACE_CHAT_BY_NAME"; otherwise set it to "FORM".
- For a form assistant, include system_prompt, submit_text, and allow_ai_studio_profiles in the JSON assistant object and omit launch. Include tool_ids exactly when the revised ASSISTANT table carries ToolIds.
- Change ASSISTANT.ToolIds only when the requested change asks for it. Use only tool IDs from the "Available tools" list in the plugin context for tools you add; never invent an ID. Drop the field entirely rather than writing an empty list.
- For a chat launcher, include launch with the exact WorkspaceName and optional ProviderId, ProfileId, ChatTemplateId, DataSourceIds, and ToolIds values from the revised ASSISTANT table; omit system_prompt, submit_text, and allow_ai_studio_profiles.
- For a chat launcher, include launch with the exact WorkspaceName and optional ProviderId, ProfileIds, ChatTemplateId, DataSourceIds, and ToolIds values from the revised ASSISTANT table; omit system_prompt, submit_text, and allow_ai_studio_profiles.
- A chat launcher must not include SystemPrompt, SubmitText, AllowProfiles, BuildPrompt, or UI in its ASSISTANT table.
- Preserve an empty profile or template GUID when it explicitly means no profile or no template. Do not emit empty provider or data-source GUIDs.
- Preserve an empty ProfileIds list when it explicitly means no profiles, and preserve an empty template GUID when it means no template. Do not emit empty provider, profile, or data-source GUIDs.
- Preserve existing behavior unless the requested change explicitly modifies it.
- Apply the requested change directly to plugin.lua; do not describe how to change it.
- Do not create companion files, new require(...) dependencies, hidden behavior, or obfuscated behavior.
@@ -754,7 +754,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
if (string.IsNullOrWhiteSpace(launch.WorkspaceName) ||
!IsOptionalGuid(launch.ProviderId, allowEmpty: false) ||
!IsOptionalGuid(launch.ProfileId, allowEmpty: true) ||
!IsOptionalGuidList(launch.ProfileIds, allowEmptyList: true) ||
!IsOptionalGuid(launch.ChatTemplateId, allowEmpty: true))
return false;
@@ -784,10 +784,14 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
if (actual is null ||
!string.Equals(requested.WorkspaceName.Trim(), actual.WorkspaceName, StringComparison.Ordinal) ||
ParseOptionalGuid(requested.ProviderId) != actual.ProviderId ||
ParseOptionalGuid(requested.ProfileId) != actual.ProfileId ||
ParseOptionalGuid(requested.ChatTemplateId) != actual.ChatTemplateId)
return false;
var requestedProfileIds = requested.ProfileIds?.Select(Guid.Parse).ToArray();
if (!(requestedProfileIds is null && actual.ProfileIds is null ||
requestedProfileIds is not null && actual.ProfileIds is not null && requestedProfileIds.SequenceEqual(actual.ProfileIds)))
return false;
var requestedDataSourceIds = requested.DataSourceIds?.Select(Guid.Parse).ToArray();
if (!(requestedDataSourceIds is null && actual.DataSourceIds is null ||
requestedDataSourceIds is not null && actual.DataSourceIds is not null &&
@@ -832,7 +836,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
var request = new AssistantBuilderChatLaunchRequest(
launch.WorkspaceName,
launch.ProviderId,
launch.ProfileId,
launch.ProfileIds,
launch.ChatTemplateId,
launch.DataSourceIds,
launch.ToolIds);
@@ -868,6 +872,11 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
private static bool IsOptionalGuid(string? value, bool allowEmpty) => value is null ||
Guid.TryParse(value, out var parsed) && (allowEmpty || parsed != Guid.Empty);
private static bool IsOptionalGuidList(IReadOnlyList<string>? values, bool allowEmptyList) => values is null ||
(allowEmptyList || values.Count > 0) &&
values.All(value => Guid.TryParse(value, out var parsed) && parsed != Guid.Empty) &&
values.Distinct(StringComparer.OrdinalIgnoreCase).Count() == values.Count;
private static Guid? ParseOptionalGuid(string? value) => value is null ? null : Guid.Parse(value);
private static string SerializeUntrustedPromptData(object value) => JsonSerializer.Serialize(value, UNTRUSTED_PROMPT_JSON_OPTIONS);
@@ -888,4 +897,4 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
private static AssistantPluginRevisionDraft RevisionFailure(string issue) => new(false, string.Empty, string.Empty, issue);
private readonly record struct AssistantContextFile(string Title, string RelativePath, bool IsRequired);
}
}
@@ -21,9 +21,9 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE)
return new(null, providerResult.ErrorMessage);
var profileResult = this.ResolveProfile(launchConfiguration.ProfileId);
var profile = profileResult.Profile;
if (profile is null)
var profileResult = this.ResolveProfiles(launchConfiguration.ProfileIds);
var profiles = profileResult.Profiles;
if (profiles is null)
return new(null, profileResult.ErrorMessage);
var chatTemplateResult = this.ResolveChatTemplate(launchConfiguration.ChatTemplateId);
@@ -36,13 +36,13 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
// its profile selection for such templates, so keeping the profile would pin one that the
// user can neither see nor change. We drop it instead of failing the whole launch.
//
if (!chatTemplate.AllowProfileUsage && profile != Profile.NO_PROFILE)
if (!chatTemplate.AllowProfileUsage && profiles.Count > 0)
{
logger.LogWarning(
"Assistant plugin '{PluginName}' selects the profile '{ProfileName}', but its chat template '{ChatTemplateName}' does not allow profiles. The chat starts without a profile.",
assistantPlugin.Name, profile.GetSafeName(), chatTemplate.GetSafeName());
"Assistant plugin '{PluginName}' selects profiles, but its chat template '{ChatTemplateName}' does not allow profiles. The chat starts without profiles.",
assistantPlugin.Name, chatTemplate.GetSafeName());
profile = Profile.NO_PROFILE;
profiles = [];
}
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(providerResult.Provider, launchConfiguration.DataSourceIds);
@@ -81,7 +81,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
{
IncludeDateTime = true,
SelectedProvider = providerResult.Provider == ProviderSettings.NONE ? string.Empty : providerResult.Provider.Id,
SelectedProfile = profile.Id,
SelectedProfileIds = profiles.Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase),
SelectedChatTemplate = chatTemplate.Id,
// The provider confidence is checked later, when the chat sends a message:
SelectedToolIds = selectedToolIds,
@@ -121,23 +121,19 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
return new(provider, true, string.Empty);
}
private (Profile? Profile, string ErrorMessage) ResolveProfile(Guid? profileId)
private (IReadOnlyList<Profile>? Profiles, string ErrorMessage) ResolveProfiles(IReadOnlyList<Guid>? profileIds)
{
if (profileId is null)
return new(settingsManager.GetPreselectedProfile(Components.CHAT), string.Empty);
if (profileIds is null)
return new(settingsManager.GetPreselectedProfiles(Components.CHAT), string.Empty);
// The launcher explicitly wants no profile:
if (profileId == Guid.Empty)
return new(Profile.NO_PROFILE, string.Empty);
if (profileIds.Count == 0)
return new([], string.Empty);
//
// We already handled the empty GUID above, so GetProfileById returning the no-profile
// entry here can only mean that the referenced profile is gone:
//
var profile = settingsManager.GetProfileById(profileId.Value.ToString());
return profile == Profile.NO_PROFILE
? new(null, string.Format(TB("The assistant chat launcher references profile '{0}', but that profile does not exist."), profileId))
: new(profile, string.Empty);
var profiles = settingsManager.ResolveProfiles(profileIds.Select(profileId => profileId.ToString()));
if (profiles.Count != profileIds.Count)
return new(null, TB("The assistant chat launcher references one or more profiles that do not exist."));
return new(profiles, string.Empty);
}
private (ChatTemplate? ChatTemplate, string ErrorMessage) ResolveChatTemplate(Guid? chatTemplateId)
@@ -212,4 +208,4 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(),
}, string.Empty);
}
}
}