2026-08-27 16:19:35 +02:00
using AIStudio.Chat ;
using AIStudio.Settings ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem ;
using AIStudio.Tools.PluginSystem.Assistants ;
2026-09-04 15:48:07 +02:00
using AIStudio.Tools.ToolCallingSystem ;
2026-08-27 16:19:35 +02:00
using ProviderSettings = AIStudio . Settings . Provider ;
namespace AIStudio.Tools.Services ;
2026-09-04 15:48:07 +02:00
public sealed class DirectChatService ( SettingsManager settingsManager , DataSourceService dataSourceService , ToolRegistry toolRegistry , ILogger < DirectChatService > logger )
2026-08-27 16:19:35 +02:00
{
private static string TB ( string fallbackEn ) => I18N . I . T ( fallbackEn , typeof ( DirectChatService ). Namespace , nameof ( DirectChatService ));
public async Task < DirectChatStartResult > TryCreateAssistantChatAsync ( PluginAssistants assistantPlugin )
{
if ( assistantPlugin . ChatLaunchConfiguration is not { } launchConfiguration )
return new ( null , TB ( "The assistant plugin does not contain a valid chat launch configuration." ));
var providerResult = this . ResolveProvider ( launchConfiguration . ProviderId );
if ( providerResult . IsExplicit && providerResult . Provider == ProviderSettings . NONE )
return new ( null , providerResult . ErrorMessage );
2026-09-07 16:09:00 +02:00
var profileResult = this . ResolveProfiles ( launchConfiguration . ProfileIds );
var profiles = profileResult . Profiles ;
if ( profiles is null )
2026-08-27 16:19:35 +02:00
return new ( null , profileResult . ErrorMessage );
var chatTemplateResult = this . ResolveChatTemplate ( launchConfiguration . ChatTemplateId );
var chatTemplate = chatTemplateResult . ChatTemplate ;
if ( chatTemplate is null )
return new ( null , chatTemplateResult . ErrorMessage );
//
// A chat template that forbids profiles wins over a configured profile: the chat disables
// 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.
//
2026-09-07 16:09:00 +02:00
if (! chatTemplate . AllowProfileUsage && profiles . Count > 0 )
2026-08-27 16:19:35 +02:00
{
logger . LogWarning (
2026-09-07 16:09:00 +02:00
"Assistant plugin '{PluginName}' selects profiles, but its chat template '{ChatTemplateName}' does not allow profiles. The chat starts without profiles." ,
assistantPlugin . Name , chatTemplate . GetSafeName ());
2026-08-27 16:19:35 +02:00
2026-09-07 16:09:00 +02:00
profiles = [];
2026-08-27 16:19:35 +02:00
}
var dataSourceOptionsResult = await this . ResolveDataSourceOptionsAsync ( providerResult . Provider , launchConfiguration . DataSourceIds );
var dataSourceOptions = dataSourceOptionsResult . Options ;
if ( dataSourceOptions is null )
return new ( null , dataSourceOptionsResult . ErrorMessage );
Guid workspaceId ;
try
{
workspaceId = await WorkspaceBehaviour . ResolveOrCreateWorkspaceIdByNameAsync ( launchConfiguration . WorkspaceName );
}
catch ( Exception exception )
{
logger . LogError ( exception , "Assistant plugin '{PluginName}' could not resolve or create workspace '{WorkspaceName}'." , assistantPlugin . Name , launchConfiguration . WorkspaceName );
return new ( null , string . Format ( TB ( "The workspace '{0}' could not be opened or created." ), launchConfiguration . WorkspaceName ));
}
if ( workspaceId == Guid . Empty )
{
logger . LogWarning ( "Assistant plugin '{PluginName}' could not resolve or create workspace '{WorkspaceName}'." , assistantPlugin . Name , launchConfiguration . WorkspaceName );
return new ( null , string . Format ( TB ( "The workspace '{0}' could not be opened or created." ), launchConfiguration . WorkspaceName ));
}
2026-09-04 15:48:07 +02:00
//
// Only the tools the user could have switched on themselves. A launcher 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
? null
: await toolRegistry . FilterSelectableToolIdsAsync ( Components . CHAT , launchConfiguration . ToolIds );
2026-08-27 16:19:35 +02:00
var chatThread = new ChatThread
{
IncludeDateTime = true ,
SelectedProvider = providerResult . Provider == ProviderSettings . NONE ? string . Empty : providerResult . Provider . Id ,
2026-09-07 16:09:00 +02:00
SelectedProfileIds = profiles . Select ( profile => profile . Id ). ToHashSet ( StringComparer . OrdinalIgnoreCase ),
2026-08-27 16:19:35 +02:00
SelectedChatTemplate = chatTemplate . Id ,
2026-09-04 15:48:07 +02:00
// The provider confidence is checked later, when the chat sends a message:
SelectedToolIds = selectedToolIds ,
2026-08-27 16:19:35 +02:00
SystemPrompt = SystemPrompts . DEFAULT ,
WorkspaceId = workspaceId ,
ChatId = Guid . NewGuid (),
Name = assistantPlugin . AssistantTitle ,
DataSourceOptions = dataSourceOptions ,
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 );
}
private ( ProviderSettings Provider , bool IsExplicit , string ErrorMessage ) ResolveProvider ( Guid ? providerId )
{
//
// The launcher does not name a provider, so it wants the chat defaults. We resolve them
// exactly like the chat does when it loads a chat without a provider. When no default can
// be determined, we do not fail: the chat opens with an empty provider selection and the
// user picks a provider there, just like for any other new chat.
//
if ( providerId is null )
return new ( settingsManager . GetChatProviderForLoadedChat (), false , string . Empty );
//
// GetProviderById does not apply any confidence filtering, so we check the provider
// ourselves afterwards, exactly as its documentation demands:
//
var provider = settingsManager . GetProviderById ( providerId . Value . ToString ());
if ( provider == ProviderSettings . NONE )
return new ( ProviderSettings . NONE , true , string . Format ( TB ( "The assistant chat launcher references provider '{0}', but that provider does not exist." ), providerId ));
if (! settingsManager . IsProviderConfident ( provider , Components . CHAT ))
return new ( ProviderSettings . NONE , true , string . Format ( TB ( "The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level." ), provider . InstanceName ));
return new ( provider , true , string . Empty );
}
2026-09-07 16:09:00 +02:00
private ( IReadOnlyList < Profile >? Profiles , string ErrorMessage ) ResolveProfiles ( IReadOnlyList < Guid >? profileIds )
2026-08-27 16:19:35 +02:00
{
2026-09-07 16:09:00 +02:00
if ( profileIds is null )
return new ( settingsManager . GetPreselectedProfiles ( Components . CHAT ), string . Empty );
2026-08-27 16:19:35 +02:00
2026-09-07 16:09:00 +02:00
if ( profileIds . Count == 0 )
return new ([], string . Empty );
2026-08-27 16:19:35 +02:00
2026-09-07 16:09:00 +02:00
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 );
2026-08-27 16:19:35 +02:00
}
private ( ChatTemplate ? ChatTemplate , string ErrorMessage ) ResolveChatTemplate ( Guid ? chatTemplateId )
{
if ( chatTemplateId is null )
return new ( settingsManager . GetPreselectedChatTemplate ( Components . CHAT ), string . Empty );
// The launcher explicitly wants no chat template:
if ( chatTemplateId == Guid . Empty )
return new ( ChatTemplate . NO_CHAT_TEMPLATE , string . Empty );
//
// We already handled the empty GUID above, so GetChatTemplateById returning the
// no-template entry here can only mean that the referenced template is gone:
//
var chatTemplate = settingsManager . GetChatTemplateById ( chatTemplateId . Value . ToString ());
return chatTemplate == ChatTemplate . NO_CHAT_TEMPLATE
? new ( null , string . Format ( TB ( "The assistant chat launcher references chat template '{0}', but that template does not exist." ), chatTemplateId ))
: new ( chatTemplate , string . Empty );
}
private async Task <( DataSourceOptions ? Options , string ErrorMessage )> ResolveDataSourceOptionsAsync ( ProviderSettings provider , IReadOnlyList < Guid >? dataSourceIds )
{
if ( dataSourceIds is null )
return new ( settingsManager . ConfigurationData . Chat . PreselectedDataSourceOptions . CreateCopy (), 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." ));
var requestedDataSources = new List < IDataSource >( dataSourceIds . Count );
foreach ( var dataSourceId in dataSourceIds )
{
// 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 ));
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 ));
requestedDataSources . Add ( dataSource );
}
IReadOnlyList < IDataSource > availableDataSources ;
try
{
availableDataSources = await dataSourceService . GetAllowedDataSources ( provider , 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." ));
}
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 )));
var standardOptions = settingsManager . ConfigurationData . Chat . PreselectedDataSourceOptions ;
return new ( new ()
{
DisableDataSources = false ,
AutomaticDataSourceSelection = false ,
AutomaticValidation = standardOptions . AutomaticValidation ,
PreselectedDataSourceIds = requestedDataSources . Select ( source => source . Id ). ToList (),
}, string . Empty );
}
2026-09-07 16:09:00 +02:00
}