2024-07-14 21:46:17 +02:00
using AIStudio.Chat ;
using AIStudio.Provider ;
using AIStudio.Settings ;
2026-02-01 14:50:19 +01:00
using AIStudio.Dialogs.Settings ;
2026-07-03 15:02:20 +02:00
using AIStudio.Tools.AIJobs ;
using AIStudio.Tools.AssistantSessions ;
2026-07-15 12:53:30 +02:00
using AIStudio.Tools.Media ;
2025-02-15 15:41:12 +01:00
using AIStudio.Tools.Services ;
2026-09-04 15:48:07 +02:00
using AIStudio.Tools.ToolCallingSystem ;
2024-07-14 21:46:17 +02:00
using Microsoft.AspNetCore.Components ;
2025-01-01 15:49:27 +01:00
using MudBlazor.Utilities ;
using Timer = System . Timers . Timer ;
2024-09-01 20:10:03 +02:00
2025-03-12 11:31:01 +01:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2024-08-21 08:30:01 +02:00
namespace AIStudio.Assistants ;
2024-07-14 21:46:17 +02:00
2025-04-27 09:06:05 +02:00
public abstract partial class AssistantBase < TSettings > : AssistantLowerBase where TSettings : IComponent
2024-07-14 21:46:17 +02:00
{
2025-03-12 11:31:01 +01:00
[Inject]
private IDialogService DialogService { get ; init ; } = null !;
2024-07-14 21:46:17 +02:00
[Inject]
protected IJSRuntime JsRuntime { get ; init ; } = null !;
2026-08-02 21:41:12 +02:00
2024-08-13 08:57:58 +02:00
[Inject]
2024-09-01 20:10:03 +02:00
protected RustService RustService { get ; init ; } = null !;
2026-09-04 15:48:07 +02:00
[Inject]
protected ToolRegistry ToolRegistry { get ; init ; } = null !;
2024-08-13 08:57:58 +02:00
2024-08-18 12:32:18 +02:00
[Inject]
protected NavigationManager NavigationManager { get ; init ; } = null !;
2024-09-01 20:10:03 +02:00
[Inject]
2025-03-12 11:31:01 +01:00
protected ILogger < AssistantBase < TSettings >> Logger { get ; init ; } = null !;
2024-09-01 20:10:03 +02:00
2024-09-15 12:30:07 +02:00
[Inject]
private MudTheme ColorTheme { get ; init ; } = null !;
2026-07-03 15:02:20 +02:00
[Inject]
protected AssistantSessionService AssistantSessionService { get ; init ; } = null !;
/// <summary>
/// Gets the job service used to run assistant-created chats independently from the assistant UI.
/// </summary>
[Inject]
protected AIJobService AIJobService { get ; init ; } = null !;
2026-07-15 12:53:30 +02:00
[Inject]
protected MediaTranscriptionService MediaTranscriptionService { get ; init ; } = null !;
2024-09-15 12:30:07 +02:00
2024-07-14 21:46:17 +02:00
protected abstract string Title { get ; }
protected abstract string Description { get ; }
protected abstract string SystemPrompt { get ; }
2026-02-01 14:50:19 +01:00
protected abstract Tools . Components Component { get ; }
2024-09-04 15:44:23 +02:00
2026-07-03 15:02:20 +02:00
protected virtual Func < string > Result2Copy => () => this . ResultingContentBlock is null ? string . Empty : this . ResultingContentBlock . Content switch
2024-08-23 08:59:30 +02:00
{
ContentText textBlock => textBlock . Text ,
_ => string . Empty ,
};
2024-08-18 21:48:35 +02:00
2025-01-01 15:49:27 +01:00
protected abstract void ResetForm ();
2024-08-18 21:48:35 +02:00
protected abstract bool MightPreselectValues ();
2024-07-14 21:46:17 +02:00
2024-09-11 23:08:02 +02:00
protected abstract string SubmitText { get ; }
protected abstract Func < Task > SubmitAction { get ; }
2025-03-12 11:31:01 +01:00
2024-09-11 23:08:02 +02:00
protected virtual bool SubmitDisabled => false ;
2024-07-14 21:46:17 +02:00
private protected virtual RenderFragment ? Body => null ;
2026-07-05 17:48:00 +02:00
private protected virtual RenderFragment ? AfterSubmitContent => null ;
private protected virtual RenderFragment ? BelowSubmitContent => null ;
2024-08-13 08:57:58 +02:00
protected virtual bool ShowResult => true ;
2025-01-01 15:49:27 +01:00
protected virtual bool ShowEntireChatThread => false ;
2024-09-08 21:01:51 +02:00
protected virtual bool AllowProfiles => true ;
2024-09-09 15:08:16 +02:00
protected virtual bool ShowProfileSelection => true ;
2024-08-13 20:53:09 +02:00
protected virtual bool ShowDedicatedProgress => false ;
2024-10-28 15:41:00 +01:00
protected virtual bool ShowSendTo => true ;
protected virtual bool ShowCopyResult => true ;
protected virtual bool ShowReset => true ;
2024-08-13 08:57:58 +02:00
2026-04-16 16:12:45 +02:00
protected virtual string? SendToChatVisibleUserPromptPrefix => null ;
protected virtual string? SendToChatVisibleUserPromptContent => null ;
protected virtual string? SendToChatVisibleUserPromptText
{
get
{
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptPrefix ))
return null ;
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptContent ))
return this . SendToChatVisibleUserPromptPrefix ;
return $"""
{this.SendToChatVisibleUserPromptPrefix}
{this.SendToChatVisibleUserPromptContent}
""" ;
}
}
protected virtual ChatThread ConvertToChatThread => this . CreateSendToChatThread ();
2024-08-18 21:48:35 +02:00
2026-04-15 19:40:53 +02:00
private protected virtual RenderFragment ? HeaderActions => null ;
private protected virtual RenderFragment ? AfterResultContent => null ;
2024-08-18 12:32:18 +02:00
protected virtual IReadOnlyList < IButtonData > FooterButtons => [];
2026-02-01 14:50:19 +01:00
protected virtual bool HasSettingsPanel => typeof ( TSettings ) != typeof ( NoSettingsPanel );
2024-07-16 10:28:13 +02:00
2026-09-04 15:48:07 +02:00
protected HashSet < string > SelectedToolIds = [];
2026-07-03 15:02:20 +02:00
private readonly Timer formChangeTimer = new ( TimeSpan . FromSeconds ( 1.6 ));
2026-09-04 15:48:07 +02:00
2026-04-16 16:48:47 +02:00
protected MudForm ? Form ;
protected CancellationTokenSource ? CancellationTokenSource ;
2026-07-03 15:02:20 +02:00
private bool isDisposed ;
private AssistantSessionKey assistantSessionKey ;
2026-07-15 12:53:30 +02:00
private MediaImportOwner CurrentMediaImportOwner => MediaImportOwner . ForAssistant ( this . assistantSessionKey );
2026-07-03 15:02:20 +02:00
private Guid ? assistantSessionId ;
private AssistantSessionSnapshot ? pendingRenderedAssistantSessionSnapshot ;
2025-04-26 18:55:23 +02:00
2026-07-03 15:02:20 +02:00
/// <summary>
/// Gets whether the Blazor component instance has already been disposed.
/// </summary>
protected bool IsAssistantComponentDisposed => this . isDisposed ;
/// <summary>
/// Gets whether this component has attached an assistant session snapshot.
/// </summary>
protected bool HasAssistantSession => this . assistantSessionId is not null ;
2026-07-15 12:53:30 +02:00
/// <summary>Gets whether this assistant currently owns active media work.</summary>
protected bool IsMediaImportBusy => this . MediaTranscriptionService . IsBusy ( this . CurrentMediaImportOwner );
2026-07-03 15:02:20 +02:00
/// <summary>
/// Gets the assistant-specific identifier used to distinguish session slots.
/// </summary>
protected virtual string AssistantSessionInstanceId => this . GetType (). FullName ?? this . Component . ToString ();
2024-07-14 21:46:17 +02:00
#region Overrides of ComponentBase
2024-09-04 15:44:23 +02:00
protected override async Task OnInitializedAsync ()
{
2026-07-15 12:53:30 +02:00
this . MediaTranscriptionService . StateChanged += this . OnMediaImportStateChanged ;
2025-04-27 16:13:15 +02:00
await base . OnInitializedAsync ();
2026-03-14 10:37:31 +01:00
if (! this . SettingsManager . IsAssistantVisible ( this . Component , assistantName : this . Title ))
{
this . Logger . LogInformation ( "Assistant '{AssistantTitle}' is hidden. Redirecting to the assistants overview." , this . Title );
this . NavigationManager . NavigateTo ( Routes . ASSISTANTS );
return ;
}
2025-04-27 16:13:15 +02:00
2025-01-01 15:49:27 +01:00
this . formChangeTimer . AutoReset = false ;
2026-08-25 18:53:09 +02:00
//
// Mind the missing async here: a timer hands its elapsed event to a thread pool thread, where an
// async handler has nobody to hand its exception to. Such an exception is not merely unobserved,
// it is unhandled, and it takes the app down with it. Observing the task keeps it contained.
//
this . formChangeTimer . Elapsed += ( _ , _ ) =>
2025-01-01 15:49:27 +01:00
{
this . formChangeTimer . Stop ();
2026-09-16 13:02:56 +02:00
this . InvokeAsync ( this . OnFormChange ). Observe ( $"{nameof(AssistantBase<TSettings>)}: handling a form change" );
2025-01-01 15:49:27 +01:00
};
2024-09-04 15:44:23 +02:00
this . MightPreselectValues ();
2026-04-16 16:48:47 +02:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( this . Component );
this . CurrentProfile = this . SettingsManager . GetPreselectedProfile ( this . Component );
this . CurrentChatTemplate = this . SettingsManager . GetPreselectedChatTemplate ( this . Component );
2026-09-04 15:48:07 +02:00
this . SelectedToolIds = this . SettingsManager . GetDefaultToolIds ( this . Component );
2026-08-11 20:14:21 +02:00
await this . OnDefaultsAppliedAsync ();
2026-07-03 15:02:20 +02:00
this . assistantSessionKey = new ( this . Component , this . AssistantSessionInstanceId );
await this . AttachAssistantSessionIfAvailable ();
2026-07-15 12:53:30 +02:00
await this . ConsumeMediaOutcomeAsync ();
2024-09-04 15:44:23 +02:00
}
2024-07-15 16:56:28 +02:00
protected override async Task OnParametersSetAsync ()
{
// Configure the spellchecking for the user input:
this . SettingsManager . InjectSpellchecking ( USER_INPUT_ATTRIBUTES );
await base . OnParametersSetAsync ();
}
2024-07-14 21:46:17 +02:00
protected override async Task OnAfterRenderAsync ( bool firstRender )
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if ( firstRender )
2026-04-16 16:48:47 +02:00
this . Form ?. ResetValidation ();
2026-07-03 15:02:20 +02:00
if ( this . pendingRenderedAssistantSessionSnapshot is { } snapshot )
{
this . pendingRenderedAssistantSessionSnapshot = null ;
await this . OnAssistantSessionRenderedAsync ( snapshot );
}
2024-07-14 21:46:17 +02:00
await base . OnAfterRenderAsync ( firstRender );
}
#endregion
2024-09-11 23:08:02 +02:00
2025-04-27 16:13:15 +02:00
private string TB ( string fallbackEN ) => this . T ( fallbackEN , typeof ( AssistantBase < TSettings >). Namespace , nameof ( AssistantBase < TSettings >));
2026-06-21 11:52:02 +02:00
private string SubmitButtonStyle => this . SettingsManager . ConfigurationData . Confidence . ShowProviderConfidence ? this . ProviderSettings . UsedLLMProvider . GetConfidence ( this . SettingsManager ). StyleBorder ( this . SettingsManager ) : string . Empty ;
2026-03-14 10:37:31 +01:00
private IReadOnlyList < Tools . Components > VisibleSendToAssistants => Enum . GetValues < AIStudio . Tools . Components >()
. Where ( this . CanSendToAssistant )
. OrderBy ( component => component . Name (). Length )
. ToArray ();
2024-07-14 21:46:17 +02:00
2024-07-16 20:06:04 +02:00
protected string? ValidatingProvider ( AIStudio . Settings . Provider provider )
{
2024-09-13 21:50:00 +02:00
if ( provider . UsedLLMProvider == LLMProviders . NONE )
2025-05-17 14:22:10 +02:00
return this . TB ( "Please select a provider." );
2024-07-16 20:06:04 +02:00
return null ;
}
2025-01-01 15:49:27 +01:00
2025-04-26 18:55:23 +02:00
private async Task Start ()
{
2026-09-04 15:48:07 +02:00
await this . RefreshProviderSelectionFromConfigurationAsync ();
if ( this . ProviderSettings == Settings . Provider . NONE )
return ;
2026-07-15 12:53:30 +02:00
if ( this . MediaTranscriptionService . IsBusy ( this . CurrentMediaImportOwner ))
return ;
2026-07-03 15:02:20 +02:00
var activeSession = this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey );
if ( activeSession ?. IsActive ?? false )
{
await this . AttachAssistantSession ( activeSession , restoreClientOnlyContent : true );
return ;
}
this . CancellationTokenSource = new ();
this . IsProcessing = true ;
var startedSession = await this . AssistantSessionService . TryBeginAsync ( this . assistantSessionKey , this . Title , this . CancellationTokenSource , this . ChatThread , this . CaptureAssistantSessionState (), this );
if ( startedSession . IsActive is not true || startedSession . Key != this . assistantSessionKey )
{
this . CancellationTokenSource . Dispose ();
this . CancellationTokenSource = null ;
return ;
}
this . assistantSessionId = startedSession . SessionId ;
await this . RefreshAssistantUIAsync ();
var sessionStatus = AssistantSessionStatus . COMPLETED ;
var errorMessage = string . Empty ;
try
2025-04-26 18:55:23 +02:00
{
await this . SubmitAction ();
2026-07-03 15:02:20 +02:00
if ( this . CancellationTokenSource ?. IsCancellationRequested ?? false )
sessionStatus = AssistantSessionStatus . CANCELED ;
}
catch ( OperationCanceledException )
{
sessionStatus = AssistantSessionStatus . CANCELED ;
}
catch ( ProviderRequestException e )
{
sessionStatus = AssistantSessionStatus . FAILED ;
errorMessage = e . UserMessage ;
this . Logger . LogError ( e , "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'" , this . Title , e . StatusCode , e . ReasonPhrase , e . ResponseBody );
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . CloudOff , e . UserMessage ));
}
catch ( Exception e )
{
sessionStatus = AssistantSessionStatus . FAILED ;
errorMessage = e . Message ;
this . Logger . LogError ( e , "The assistant session '{AssistantTitle}' failed." , this . Title );
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Stream , string . Format ( this . TB ( "The assistant failed. The message is: '{0}'" ), e . Message )));
}
finally
{
this . IsProcessing = false ;
var sessionCancellationTokenSource = this . CancellationTokenSource ;
this . CancellationTokenSource = null ;
if ( this . assistantSessionId is { } sessionId )
{
await this . AssistantSessionService . CompleteAsync ( this . assistantSessionKey , sessionId , sessionStatus , errorMessage , this . ChatThread , this . CaptureAssistantSessionState (), this );
if (! this . isDisposed )
_ = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey );
}
sessionCancellationTokenSource ?. Dispose ();
await this . RefreshAssistantUIAsync ();
2025-04-26 18:55:23 +02:00
}
}
2025-01-01 15:49:27 +01:00
private void TriggerFormChange ( FormFieldChangedEventArgs _ )
{
this . formChangeTimer . Stop ();
this . formChangeTimer . Start ();
}
2024-07-16 20:06:04 +02:00
2025-01-01 15:49:27 +01:00
/// <summary>
/// This method is called after any form field has changed.
/// </summary>
/// <remarks>
/// This method is called after a delay of 1.6 seconds. This is to prevent
/// the method from being called too often. This method is called after
/// the user has stopped typing or selecting options.
/// </remarks>
protected virtual Task OnFormChange () => Task . CompletedTask ;
2026-08-11 20:14:21 +02:00
/// <summary>
/// Allows assistants to finish asynchronous work after their configured defaults were applied.
/// </summary>
protected virtual Task OnDefaultsAppliedAsync () => Task . CompletedTask ;
2025-01-01 15:49:27 +01:00
/// <summary>
/// Add an issue to the UI.
/// </summary>
/// <param name="issue">The issue to add.</param>
protected void AddInputIssue ( string issue )
{
2026-07-03 15:02:20 +02:00
Array . Resize ( ref this . InputIssues , this . InputIssues . Length + 1 );
this . InputIssues [^ 1 ] = issue ;
2026-04-16 16:48:47 +02:00
this . InputIsValid = false ;
2026-08-25 18:53:09 +02:00
this . RefreshAssistantUIAsync (). Observe ( $"{nameof(AssistantBase<TSettings>)}: rendering an added input issue" );
2025-01-01 15:49:27 +01:00
}
2026-02-01 14:50:19 +01:00
/// <summary>
/// Clear all input issues.
/// </summary>
protected void ClearInputIssues ()
{
2026-07-03 15:02:20 +02:00
this . InputIssues = [];
2026-04-16 16:48:47 +02:00
this . InputIsValid = true ;
2026-08-25 18:53:09 +02:00
this . RefreshAssistantUIAsync (). Observe ( $"{nameof(AssistantBase<TSettings>)}: rendering cleared input issues" );
2026-02-01 14:50:19 +01:00
}
2025-01-01 15:49:27 +01:00
2024-07-14 21:46:17 +02:00
protected void CreateChatThread ()
{
2026-04-16 16:48:47 +02:00
this . ChatThread = new ()
2024-07-14 21:46:17 +02:00
{
2026-01-18 20:36:04 +01:00
IncludeDateTime = false ,
2026-04-16 16:48:47 +02:00
SelectedProvider = this . ProviderSettings . Id ,
SelectedProfile = this . AllowProfiles ? this . CurrentProfile . Id : Profile . NO_PROFILE . Id ,
2025-01-02 14:50:54 +01:00
SystemPrompt = this . SystemPrompt ,
2024-07-14 21:46:17 +02:00
WorkspaceId = Guid . Empty ,
ChatId = Guid . NewGuid (),
2025-05-17 14:22:10 +02:00
Name = string . Format ( this . TB ( "Assistant - {0}" ), this . Title ),
2024-07-14 21:46:17 +02:00
Blocks = [],
2026-09-04 15:48:07 +02:00
RuntimeComponent = this . Component ,
2024-07-14 21:46:17 +02:00
};
}
2024-10-28 15:41:00 +01:00
protected Guid CreateChatThread ( Guid workspaceId , string name )
{
var chatId = Guid . NewGuid ();
2026-04-16 16:48:47 +02:00
this . ChatThread = new ()
2024-10-28 15:41:00 +01:00
{
2026-01-18 20:36:04 +01:00
IncludeDateTime = false ,
2026-04-16 16:48:47 +02:00
SelectedProvider = this . ProviderSettings . Id ,
SelectedProfile = this . AllowProfiles ? this . CurrentProfile . Id : Profile . NO_PROFILE . Id ,
2025-01-02 14:50:54 +01:00
SystemPrompt = this . SystemPrompt ,
2024-10-28 15:41:00 +01:00
WorkspaceId = workspaceId ,
ChatId = chatId ,
Name = name ,
Blocks = [],
2026-09-04 15:48:07 +02:00
RuntimeComponent = this . Component ,
2024-10-28 15:41:00 +01:00
};
return chatId ;
}
2026-02-01 14:50:19 +01:00
2026-09-04 15:48:07 +02:00
private Task RefreshProviderSelectionFromConfigurationAsync ()
{
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( this . Component , this . ProviderSettings . Id );
return Task . CompletedTask ;
}
2026-02-01 14:50:19 +01:00
protected virtual void ResetProviderAndProfileSelection ()
{
2026-04-16 16:48:47 +02:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( this . Component );
this . CurrentProfile = this . SettingsManager . GetPreselectedProfile ( this . Component );
this . CurrentChatTemplate = this . SettingsManager . GetPreselectedChatTemplate ( this . Component );
2026-09-04 15:48:07 +02:00
this . SelectedToolIds = this . SettingsManager . GetDefaultToolIds ( this . Component );
}
/// <summary>
/// The tools this assistant runs with when its own rules name them, instead of asking the user.
/// </summary>
/// <remarks>
/// Null is the normal case: the user picks the tools. An assistant whose configuration already
/// says which tools belong to a run — a document analysis policy, for instance — returns them
/// here. Its tool selection then disappears from the footer, because there is nothing left to
/// choose: whoever wrote the policy has decided, and a user working with a policy rolled out by
/// their organization gets it as configured.
/// </remarks>
protected virtual IReadOnlySet < string >? AssistantManagedToolIds => null ;
/// <summary>
/// The tools this assistant may hand to a model with the provider it currently uses.
/// </summary>
/// <remarks>
/// Whether the tools come from the assistant's own rules or from the user, the provider filter
/// always has the last word: a tool asking for more confidence than the selected provider has
/// never reaches the model, no matter who put it on the list. That filter belongs here rather
/// than into the stored selection, because a provider with too little confidence must not cost
/// the user a tool for good.
/// </remarks>
protected HashSet < string > GetRunnableToolIds ()
{
if ( this . AssistantManagedToolIds is not null )
return this . ToolRegistry . FilterToolIdsForProvider ( this . ProviderSettings , this . AssistantManagedToolIds );
// What the user cannot see, the assistant does not use:
if (! this . SettingsManager . IsToolSelectionVisible ( this . Component ))
return [];
return this . ToolRegistry . FilterToolIdsForProvider ( this . ProviderSettings , this . SelectedToolIds );
}
/// <summary>
/// Takes over a changed tool selection, no matter where the user made it.
/// </summary>
/// <remarks>
/// The footer offers one; an assistant may instead put the tools next to the setting they
/// belong to, as the batch processing does with its instructions. Both end up here.
/// </remarks>
protected Task SelectedToolIdsChanged ( HashSet < string > updatedToolIds )
{
this . SelectedToolIds = ToolSelectionRules . NormalizeSelection ( updatedToolIds );
return Task . CompletedTask ;
2026-02-01 14:50:19 +01:00
}
2024-07-14 21:46:17 +02:00
2025-12-30 18:30:32 +01:00
protected DateTimeOffset AddUserRequest ( string request , bool hideContentFromUser = false , params List < FileAttachment > attachments )
2024-07-14 21:46:17 +02:00
{
var time = DateTimeOffset . Now ;
2026-04-16 16:48:47 +02:00
this . LastUserPrompt = new ContentText
2025-02-10 18:49:22 +01:00
{
Text = request ,
2025-12-30 18:30:32 +01:00
FileAttachments = attachments ,
2025-02-10 18:49:22 +01:00
};
2026-04-16 16:48:47 +02:00
this . ChatThread !. Blocks . Add ( new ContentBlock
2024-07-14 21:46:17 +02:00
{
Time = time ,
ContentType = ContentType . TEXT ,
2024-10-28 15:41:00 +01:00
HideFromUser = hideContentFromUser ,
2024-07-14 21:46:17 +02:00
Role = ChatRole . USER ,
2026-04-16 16:48:47 +02:00
Content = this . LastUserPrompt ,
2024-07-14 21:46:17 +02:00
});
return time ;
}
2025-01-01 15:49:27 +01:00
protected async Task < string > AddAIResponseAsync ( DateTimeOffset time , bool hideContentFromUser = false )
2024-07-14 21:46:17 +02:00
{
2026-04-16 16:48:47 +02:00
var manageCancellationLocally = this . CancellationTokenSource is null ;
this . CancellationTokenSource ??= new CancellationTokenSource ();
2025-05-17 19:17:29 +02:00
2024-07-14 21:46:17 +02:00
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true ,
};
2026-07-03 15:02:20 +02:00
aiText . StreamingEvent = async () =>
{
await this . CheckpointAssistantSession ();
await this . RefreshAssistantUIAsync ();
};
aiText . StreamingDone = async () =>
{
await this . CheckpointAssistantSession ();
await this . RefreshAssistantUIAsync ();
};
this . ResultingContentBlock = new ContentBlock
2024-07-14 21:46:17 +02:00
{
Time = time ,
ContentType = ContentType . TEXT ,
Role = ChatRole . AI ,
Content = aiText ,
2025-01-01 15:49:27 +01:00
HideFromUser = hideContentFromUser ,
2024-07-14 21:46:17 +02:00
};
2024-11-23 13:04:02 +01:00
2026-04-16 16:48:47 +02:00
if ( this . ChatThread is not null )
2024-11-23 13:04:02 +01:00
{
2026-07-03 15:02:20 +02:00
this . ChatThread . Blocks . Add ( this . ResultingContentBlock );
2026-04-16 16:48:47 +02:00
this . ChatThread . SelectedProvider = this . ProviderSettings . Id ;
2026-09-04 15:48:07 +02:00
this . ChatThread . RuntimeComponent = this . Component ;
this . ChatThread . SelectedToolIds = [.. this . SelectedToolIds ];
this . ChatThread . RuntimeSelectedToolIds = this . GetRunnableToolIds ();
this . ChatThread . RuntimeToolsAreAssistantManaged = this . AssistantManagedToolIds is not null ;
2024-11-23 13:04:02 +01:00
}
2026-07-03 15:02:20 +02:00
this . IsProcessing = true ;
await this . CheckpointAssistantSession ();
await this . RefreshAssistantUIAsync ();
2025-04-26 18:55:23 +02:00
2026-05-25 17:32:54 +02:00
try
{
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
this . ChatThread = await aiText . CreateFromProviderAsync ( this . ProviderSettings . CreateProvider (), this . ProviderSettings . Model , this . LastUserPrompt , this . ChatThread , this . CancellationTokenSource !. Token );
// Return the AI response:
return aiText . Text ;
}
catch ( ProviderRequestException e )
2025-05-17 19:17:29 +02:00
{
2026-05-25 17:32:54 +02:00
this . Logger . LogError ( e , "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'" , this . Title , e . StatusCode , e . ReasonPhrase , e . ResponseBody );
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . CloudOff , e . UserMessage ));
2026-07-03 15:02:20 +02:00
if ( this . ResultingContentBlock is not null && string . IsNullOrWhiteSpace ( aiText . Text ))
2026-05-25 17:32:54 +02:00
{
2026-07-03 15:02:20 +02:00
this . ChatThread ?. Blocks . Remove ( this . ResultingContentBlock );
this . ResultingContentBlock = null ;
2026-05-25 17:32:54 +02:00
}
return string . Empty ;
2025-05-17 19:17:29 +02:00
}
2026-05-25 17:32:54 +02:00
finally
{
2026-07-03 15:02:20 +02:00
this . IsProcessing = this . assistantSessionId is not null && ( this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey )?. IsActive ?? false );
await this . CheckpointAssistantSession ();
await this . RefreshAssistantUIAsync ();
2025-05-17 19:17:29 +02:00
2026-05-25 17:32:54 +02:00
if ( manageCancellationLocally )
{
this . CancellationTokenSource ?. Dispose ();
this . CancellationTokenSource = null ;
}
2026-08-23 21:15:37 +02:00
//
// The handlers above close over this assistant, and the content stays in the chat
// thread. The stream is over by now, so nothing has to listen to it anymore:
//
aiText . ResetStreamingHandlers ();
2026-05-25 17:32:54 +02:00
}
2024-08-13 08:57:58 +02:00
}
2026-07-03 15:02:20 +02:00
/// <summary>
/// Starts the current assistant chat thread as a regular background-capable chat generation job.
/// </summary>
/// <remarks>
/// Use this when an assistant creates a chat and hands it over to the chat page instead of
/// rendering the answer inside the assistant UI.
/// </remarks>
/// <param name="time">The timestamp to use for the AI response block.</param>
/// <param name="hideContentFromUser">Whether the AI response block should be hidden from the user.</param>
/// <param name="isForeground">Whether the chat job should start as the current foreground job.</param>
/// <returns>A task that completes after the chat job was registered.</returns>
protected async Task StartChatGenerationJobAsync ( DateTimeOffset time , bool hideContentFromUser = false , bool isForeground = true )
{
if ( this . ChatThread is null )
return ;
var aiText = new ContentText
{
InitialRemoteWait = true ,
};
this . ResultingContentBlock = new ContentBlock
{
Time = time ,
ContentType = ContentType . TEXT ,
Role = ChatRole . AI ,
Content = aiText ,
HideFromUser = hideContentFromUser ,
};
this . ChatThread . Blocks . Add ( this . ResultingContentBlock );
this . ChatThread . SelectedProvider = this . ProviderSettings . Id ;
await this . CheckpointAssistantSession ();
await this . AIJobService . TryStartChatGenerationAsync ( new ChatGenerationRequest
{
ChatThread = this . ChatThread ,
AIText = aiText ,
LastUserPrompt = this . LastUserPrompt ,
ProviderSettings = this . ProviderSettings ,
IsForeground = isForeground ,
});
}
2024-08-13 08:57:58 +02:00
2026-08-11 20:14:21 +02:00
private Task CancelStreaming () => this . CancelAssistantSessionAsync ();
/// <summary>
/// Requests cancellation of the active assistant session.
/// </summary>
/// <remarks>
/// Derived assistants should use this method instead of accessing their local
/// cancellation token source. A component which reattaches after navigation
/// does not own that source, while the session service still does.
/// </remarks>
/// <returns>A task that completes after cancellation was requested.</returns>
protected Task CancelAssistantSessionAsync () => this . AssistantSessionService . CancelAsync ( this . assistantSessionKey , this );
2025-03-17 18:00:26 +01:00
2024-08-23 08:59:30 +02:00
protected async Task CopyToClipboard ()
2024-08-13 20:53:09 +02:00
{
2026-08-02 21:41:12 +02:00
await this . RustService . CopyText2Clipboard ( this . Result2Copy ());
2024-08-13 20:53:09 +02:00
}
2026-04-16 16:12:45 +02:00
private ChatThread CreateSendToChatThread ()
{
2026-04-16 16:48:47 +02:00
var originalChatThread = this . ChatThread ?? new ChatThread ();
2026-04-16 16:12:45 +02:00
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptText ))
return originalChatThread with
{
SystemPrompt = SystemPrompts . DEFAULT ,
};
var earliestBlock = originalChatThread . Blocks . MinBy ( x => x . Time );
var visiblePromptTime = earliestBlock is null
? DateTimeOffset . Now
: earliestBlock . Time == DateTimeOffset . MinValue
? earliestBlock . Time
: earliestBlock . Time . AddTicks (- 1 );
var transferredBlocks = originalChatThread . Blocks
. Select ( block => block . Role is ChatRole . USER
? block . DeepClone ( changeHideState : true )
: block . DeepClone ())
. ToList ();
transferredBlocks . Insert ( 0 , new ContentBlock
{
Time = visiblePromptTime ,
ContentType = ContentType . TEXT ,
HideFromUser = false ,
Role = ChatRole . USER ,
Content = new ContentText
{
Text = this . SendToChatVisibleUserPromptText ,
},
});
return originalChatThread with
{
SystemPrompt = SystemPrompts . DEFAULT ,
Blocks = transferredBlocks ,
};
}
2024-08-13 20:53:09 +02:00
2024-08-13 08:57:58 +02:00
private static string? GetButtonIcon ( string icon )
{
if ( string . IsNullOrWhiteSpace ( icon ))
return null ;
return icon ;
2024-07-14 21:46:17 +02:00
}
2024-08-18 12:32:18 +02:00
2025-03-12 11:31:01 +01:00
protected async Task OpenSettingsDialog ()
{
2026-02-01 14:50:19 +01:00
if (! this . HasSettingsPanel )
return ;
2025-03-12 11:31:01 +01:00
var dialogParameters = new DialogParameters ();
2025-03-16 15:19:17 +01:00
await this . DialogService . ShowAsync < TSettings >( null , dialogParameters , DialogOptions . FULLSCREEN );
2025-03-12 11:31:01 +01:00
}
2026-07-03 15:02:20 +02:00
protected async Task SendToAssistant ( Tools . Components destination , SendToButton sendToButton )
2024-08-18 12:32:18 +02:00
{
2026-03-14 10:37:31 +01:00
if (! this . CanSendToAssistant ( destination ))
2026-07-03 15:02:20 +02:00
return ;
2024-11-02 11:55:09 +01:00
2024-10-28 15:41:00 +01:00
var contentToSend = sendToButton == default ? string . Empty : sendToButton . UseResultingContentBlockData switch
2024-08-18 12:32:18 +02:00
{
2024-08-18 21:48:35 +02:00
false => sendToButton . GetText (),
2026-07-03 15:02:20 +02:00
true => this . ResultingContentBlock ?. Content switch
2024-08-18 12:32:18 +02:00
{
ContentText textBlock => textBlock . Text ,
_ => string . Empty ,
},
};
2024-08-22 21:27:16 +02:00
var sendToData = destination . GetData ();
2026-08-02 21:41:12 +02:00
if ( destination . HasSingleSessionSlot () && this . AssistantSessionService . GetSnapshots (). Any ( snapshot => snapshot . IsActive && snapshot . Key . Component == destination ))
2026-07-03 15:02:20 +02:00
{
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . Apps , this . TB ( "This assistant is already running. AI Studio opens the running session instead." )));
this . NavigationManager . NavigateTo ( sendToData . Route );
return ;
}
2026-08-02 21:41:12 +02:00
// Only components with a single session slot may be cleared as a group. The visual briefing
// assistant keys its sessions per briefing, so clearing by component would discard the
// status of every stored briefing instead of the one we are about to open.
if ( destination . HasSingleSessionSlot ())
2026-07-03 15:02:20 +02:00
await this . AssistantSessionService . ClearInactiveSessionsForComponentAsync ( destination );
2024-08-18 21:48:35 +02:00
switch ( destination )
{
2024-09-04 15:44:23 +02:00
case Tools . Components . CHAT :
2026-04-15 19:40:53 +02:00
if ( sendToButton . SendToChatAsInput )
MessageBus . INSTANCE . DeferMessage ( this , Event . SEND_TO_CHAT_INPUT , contentToSend );
else
{
var convertedChatThread = this . ConvertToChatThread ;
2026-04-16 16:48:47 +02:00
convertedChatThread = convertedChatThread with { SelectedProvider = this . ProviderSettings . Id };
2026-08-27 16:19:35 +02:00
MessageBus . INSTANCE . DeferMessage ( this , sendToData . Event , new ChatStartRequest ( convertedChatThread ));
2026-04-15 19:40:53 +02:00
}
2024-08-18 21:48:35 +02:00
break ;
default :
2024-08-22 21:27:16 +02:00
MessageBus . INSTANCE . DeferMessage ( this , sendToData . Event , contentToSend );
2024-08-18 21:48:35 +02:00
break ;
}
2024-08-22 21:27:16 +02:00
this . NavigationManager . NavigateTo ( sendToData . Route );
2024-08-18 12:32:18 +02:00
}
2026-03-14 10:37:31 +01:00
private bool CanSendToAssistant ( Tools . Components component )
{
if (! component . AllowSendTo ())
return false ;
2026-08-02 21:41:12 +02:00
return this . SettingsManager . IsAssistantVisible (
component ,
withLogging : false ,
requiredPreviewFeature : component . RequiredPreviewFeature ());
2026-03-14 10:37:31 +01:00
}
2024-08-18 21:48:35 +02:00
private async Task InnerResetForm ()
{
2026-07-15 12:53:30 +02:00
if (( this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey )?. IsActive ?? false )
|| this . MediaTranscriptionService . IsBusy ( this . CurrentMediaImportOwner ))
2026-07-03 15:02:20 +02:00
return ;
await this . AssistantSessionService . ClearAsync ( this . assistantSessionKey );
2026-07-15 12:53:30 +02:00
this . MediaTranscriptionService . ClearOwnerState ( this . CurrentMediaImportOwner );
2026-07-03 15:02:20 +02:00
this . assistantSessionId = null ;
2026-08-06 12:13:08 +02:00
this . ChatThread = null ;
this . LastUserPrompt = null ;
2026-07-03 15:02:20 +02:00
this . ResultingContentBlock = null ;
2026-04-16 16:48:47 +02:00
this . ProviderSettings = Settings . Provider . NONE ;
2024-08-18 21:48:35 +02:00
2026-08-06 12:13:08 +02:00
await this . JsRuntime . ClearDiv ( BEFORE_RESULT_DIV_ID );
2024-08-21 08:30:01 +02:00
await this . JsRuntime . ClearDiv ( RESULT_DIV_ID );
2024-08-18 21:48:35 +02:00
await this . JsRuntime . ClearDiv ( AFTER_RESULT_DIV_ID );
2025-01-01 15:49:27 +01:00
this . ResetForm ();
2026-02-01 14:50:19 +01:00
this . ResetProviderAndProfileSelection ();
2026-08-11 20:14:21 +02:00
await this . OnDefaultsAppliedAsync ();
2024-08-18 21:48:35 +02:00
2026-04-16 16:48:47 +02:00
this . InputIsValid = false ;
2026-07-03 15:02:20 +02:00
this . InputIssues = [];
2024-08-18 21:48:35 +02:00
2026-04-16 16:48:47 +02:00
this . Form ?. ResetValidation ();
2026-07-03 15:02:20 +02:00
await this . RefreshAssistantUIAsync ();
2026-04-16 16:48:47 +02:00
this . Form ?. ResetValidation ();
2024-08-18 21:48:35 +02:00
}
2024-09-15 12:30:07 +02:00
private string GetResetColor () => this . SettingsManager . IsDarkMode switch
{
true => $"background-color: #804000" ,
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).Warning.Value}" ,
};
private string GetSendToColor () => this . SettingsManager . IsDarkMode switch
{
true => $"background-color: #004080" ,
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}" ,
};
2025-04-27 09:06:05 +02:00
#region Overrides of MSGComponentBase
2024-09-15 12:30:07 +02:00
2025-04-27 09:06:05 +02:00
protected override void DisposeResources ()
2024-09-15 12:30:07 +02:00
{
2026-07-15 12:53:30 +02:00
this . MediaTranscriptionService . StateChanged -= this . OnMediaImportStateChanged ;
2026-07-03 15:02:20 +02:00
this . isDisposed = true ;
2025-08-15 21:16:51 +02:00
try
{
this . formChangeTimer . Stop ();
this . formChangeTimer . Dispose ();
}
catch
{
// ignore
}
base . DisposeResources ();
2024-09-15 12:30:07 +02:00
}
2026-07-15 12:53:30 +02:00
/// <summary>Refreshes assistant actions when the shared import lane changes.</summary>
private void OnMediaImportStateChanged ( MediaImportOwner owner )
{
if ( owner == this . CurrentMediaImportOwner )
2026-08-25 18:53:09 +02:00
this . InvokeAsync ( async () =>
2026-07-15 12:53:30 +02:00
{
await this . ConsumeMediaOutcomeAsync ();
this . StateHasChanged ();
2026-08-25 18:53:09 +02:00
}). Observe ( $"{nameof(AssistantBase<TSettings>)}: consuming a media import outcome" );
2026-07-15 12:53:30 +02:00
}
/// <summary>Consumes a terminal media notification when this assistant is visible.</summary>
private async Task ConsumeMediaOutcomeAsync ()
{
var outcome = this . MediaTranscriptionService . TryConsumeOutcome ( this . CurrentMediaImportOwner );
if ( outcome is null )
return ;
if ( outcome . Failures . Count > 0 )
{
var message = string . Join ( Environment . NewLine , outcome . Failures . Select ( failure => $"{failure.FileName}: {failure.UserMessage}" ));
await this . MessageBus . SendError ( new ( Icons . Material . Filled . VoiceChat , message ));
}
else if ( outcome . Status is MediaImportStatus . FAILED )
{
await this . MessageBus . SendError ( new ( Icons . Material . Filled . VoiceChat , this . TB ( "The media file could not be transcribed." )));
}
if ( outcome . Warnings . Count > 0 )
{
var message = string . Join ( Environment . NewLine , outcome . Warnings . Select ( warning => $"{warning.FileName}: {warning.UserMessage}" ));
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . VoiceChat , message ));
}
if ( outcome . Status is MediaImportStatus . CANCELLED )
{
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . VoiceChat , this . TB ( "The media transcription was canceled." )));
}
}
2024-09-15 12:30:07 +02:00
#endregion
2026-07-03 15:02:20 +02:00
#region Assistant sessions
/// <summary>
/// Stores the current assistant UI and chat state in the active assistant session.
/// </summary>
/// <returns>A task that completes after the checkpoint was stored and published.</returns>
2026-08-11 20:14:21 +02:00
protected Task CheckpointAssistantSession ()
2026-07-03 15:02:20 +02:00
{
if ( this . assistantSessionId is null )
return Task . CompletedTask ;
return this . AssistantSessionService . CheckpointAsync ( this . assistantSessionKey , this . assistantSessionId . Value , this . Title , this . ChatThread , this . CaptureAssistantSessionState (), this );
}
/// <summary>
/// Allows derived assistants to restore client-only UI after a session was attached.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was attached.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionAttachedAsync ( AssistantSessionSnapshot snapshot ) => Task . CompletedTask ;
/// <summary>
/// Allows derived assistants to restore DOM-dependent client-only UI after an attached session was rendered.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was rendered.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionRenderedAsync ( AssistantSessionSnapshot snapshot ) => Task . CompletedTask ;
/// <summary>
/// Handles assistant session change events for the current assistant instance.
/// </summary>
/// <typeparam name="T">The message payload type.</typeparam>
/// <param name="sendingComponent">The component that sent the message, if any.</param>
/// <param name="triggeredEvent">The event that was triggered.</param>
/// <param name="data">The message payload.</param>
/// <returns>A task that completes after the message was processed.</returns>
protected override async Task ProcessIncomingMessage < T >( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
if ( ReferenceEquals ( sendingComponent , this ))
return ;
switch ( triggeredEvent )
{
case Event . ASSISTANT_SESSION_CHANGED :
case Event . ASSISTANT_SESSION_FINISHED :
if ( data is AssistantSessionSnapshot snapshot && snapshot . Key == this . assistantSessionKey )
{
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : triggeredEvent is Event . ASSISTANT_SESSION_FINISHED );
if ( triggeredEvent is Event . ASSISTANT_SESSION_FINISHED )
_ = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey );
}
break ;
}
}
/// <summary>
/// Attaches the component to an existing assistant session if one is available.
/// </summary>
/// <returns>A task that completes after the session was attached.</returns>
private async Task AttachAssistantSessionIfAvailable ()
{
var snapshot = this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey );
if ( snapshot ?. IsActive ?? false )
{
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : true );
return ;
}
snapshot = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey );
if ( snapshot is null )
return ;
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : true );
}
/// <summary>
/// Applies an assistant session snapshot to this component instance.
/// </summary>
/// <param name="snapshot">The snapshot to attach.</param>
/// <param name="restoreClientOnlyContent">Whether derived assistants should restore client-only UI state.</param>
/// <returns>A task that completes after the component was refreshed.</returns>
private async Task AttachAssistantSession ( AssistantSessionSnapshot snapshot , bool restoreClientOnlyContent )
{
this . assistantSessionId = snapshot . SessionId ;
this . ImportAssistantSessionState ( snapshot . State );
this . ChatThread = snapshot . ChatThread ?? this . ChatThread ;
this . IsProcessing = snapshot . IsActive ;
if (! snapshot . IsActive )
this . CancellationTokenSource = null ;
if ( restoreClientOnlyContent )
await this . OnAssistantSessionAttachedAsync ( snapshot );
if ( restoreClientOnlyContent )
this . pendingRenderedAssistantSessionSnapshot = snapshot ;
await this . RefreshAssistantUIAsync ();
}
/// <summary>
/// Refreshes the component when it is still mounted.
/// </summary>
/// <returns>A task that completes after the renderer was notified.</returns>
2026-08-11 20:14:21 +02:00
protected async Task RefreshAssistantUIAsync ()
2026-07-03 15:02:20 +02:00
{
if ( this . isDisposed )
return ;
try
{
await this . InvokeAsync ( this . StateHasChanged );
}
catch ( InvalidOperationException )
{
// The component may already have left the renderer while a background session is finishing.
}
}
/// <summary>
/// Captures the base assistant state and assistant-specific typed state values for session restore.
/// </summary>
/// <returns>A dictionary containing the current assistant state.</returns>
private Dictionary < string , IAssistantSessionSnapshotField > CaptureAssistantSessionState ()
{
var state = new AssistantSessionStateWriter ();
state . Set ( PROVIDER_SETTINGS_STATE_KEY , this . ProviderSettings );
state . Set ( INPUT_IS_VALID_STATE_KEY , this . InputIsValid );
state . Set ( CURRENT_PROFILE_STATE_KEY , this . CurrentProfile );
state . Set ( CURRENT_CHAT_TEMPLATE_STATE_KEY , this . CurrentChatTemplate );
state . Set ( CHAT_THREAD_STATE_KEY , this . ChatThread );
state . Set ( LAST_USER_PROMPT_STATE_KEY , this . LastUserPrompt );
state . Set ( RESULTING_CONTENT_BLOCK_STATE_KEY , this . ResultingContentBlock );
state . Set ( INPUT_ISSUES_STATE_KEY , this . InputIssues );
state . Set ( IS_PROCESSING_STATE_KEY , this . IsProcessing );
2026-09-04 15:48:07 +02:00
state . Set ( SELECTED_TOOL_IDS_STATE_KEY , this . SelectedToolIds );
2026-07-03 15:02:20 +02:00
this . CaptureCustomAssistantSessionState ( state );
return state . ToDictionary ();
}
/// <summary>
/// Captures assistant-specific state values.
/// </summary>
/// <param name="state">The typed state writer to update.</param>
protected virtual void CaptureCustomAssistantSessionState ( AssistantSessionStateWriter state ) { }
/// <summary>
/// Restores the base assistant state and assistant-specific typed state values from a session snapshot.
/// </summary>
/// <param name="state">The captured assistant state to import.</param>
private void ImportAssistantSessionState ( IReadOnlyDictionary < string , IAssistantSessionSnapshotField > state )
{
var reader = new AssistantSessionStateReader ( state , this . Title );
reader . Restore ( PROVIDER_SETTINGS_STATE_KEY , value => this . ProviderSettings = value );
reader . Restore ( INPUT_IS_VALID_STATE_KEY , value => this . InputIsValid = value );
reader . Restore ( CURRENT_PROFILE_STATE_KEY , value => this . CurrentProfile = value );
reader . Restore ( CURRENT_CHAT_TEMPLATE_STATE_KEY , value => this . CurrentChatTemplate = value );
reader . Restore ( CHAT_THREAD_STATE_KEY , value => this . ChatThread = value );
reader . Restore ( LAST_USER_PROMPT_STATE_KEY , value => this . LastUserPrompt = value );
reader . Restore ( RESULTING_CONTENT_BLOCK_STATE_KEY , value => this . ResultingContentBlock = value );
reader . Restore ( INPUT_ISSUES_STATE_KEY , value => this . InputIssues = value );
reader . Restore ( IS_PROCESSING_STATE_KEY , value => this . IsProcessing = value );
2026-09-04 15:48:07 +02:00
reader . Restore ( SELECTED_TOOL_IDS_STATE_KEY , value => this . SelectedToolIds = ToolSelectionRules . NormalizeSelection ( value ));
2026-07-03 15:02:20 +02:00
this . RestoreCustomAssistantSessionState ( reader );
}
/// <summary>
/// Restores assistant-specific state values.
/// </summary>
/// <param name="state">The typed state reader to read from.</param>
protected virtual void RestoreCustomAssistantSessionState ( AssistantSessionStateReader state ) { }
#endregion
2026-09-04 15:48:07 +02:00
}