2026-11-06 17:44:58 +01:00
using AIStudio.Assistants ;
2026-11-06 12:12:32 +01:00
using AIStudio.Chat ;
using AIStudio.Components ;
using AIStudio.Settings ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.Services ;
using Microsoft.AspNetCore.Components ;
namespace AIStudio.Dialogs ;
/// <summary>
/// Shows the system prompt a chat sends, part by part, each under the name of where it comes from.
/// </summary>
/// <remarks>
2026-11-06 17:44:58 +01:00
/// Nobody types this prompt as it is sent. A chat template or an assistant whose conversation the chat
/// continues replaces the default, a profile, the
2026-11-06 12:12:32 +01:00
/// retrieved data of a data source, and the policy of the selected tools are added to it, and the
/// date goes in front. Each part stands under its own heading, in the order of the prompt, so a
/// person does not have to work out from the text alone where a paragraph came from.<br/><br/>
///
/// The parts are those ChatThread.BuildSystemPrompt recorded, so the dialog decides nothing about
/// them. Only the retrieved data needs a word of its own: AI Studio works it out anew whenever a
/// message is sent, see AISrcSelWithRetCtxVal.ProcessAsync. What the dialog shows of it was sent with
/// the last message, and the next one gets what the search finds then. Before the first search,
/// e.g., in a new chat or one just loaded, there is nothing to show, but the chat may still search.
/// Two more things stand where they would be: a selected profile which the chat template does not
/// allow, and the instruction the tool calling loop adds once the limit of tool calls is reached.
/// </remarks>
public partial class SystemPromptDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get ; set ; } = null !;
[Inject]
private RustService RustService { get ; init ; } = null !;
/// <summary>
/// The system prompt, together with the parts it was assembled from.
/// </summary>
[Parameter]
public PreparedSystemPrompt Prompt { get ; set ; } = null !;
/// <summary>
/// The display names of the tools whose usage policy is part of the prompt.
/// </summary>
[Parameter]
public IReadOnlyList < string > ToolNames { get ; set ; } = [];
/// <summary>
/// How the next message searches the data sources of the chat, or null when it searches none.
/// </summary>
[Parameter]
public DataSourceRetrievalMode ? RetrievalMode { get ; set ; }
private IReadOnlyList < Section > sections = [];
#region Overrides of ComponentBase
protected override void OnParametersSet ()
{
this . sections = this . BuildSections ();
base . OnParametersSet ();
}
#endregion
/// <summary>
/// Lists the parts of the prompt in its order, with what stands where nothing was added.
/// </summary>
/// <remarks>
/// Retrieved data follows the base prompt or the chat template, and a profile follows the
/// retrieved data. So a chat which has not retrieved anything yet, but searches its data sources,
/// says so right after the base prompt, and a profile which the chat template switched off is
/// named where it would otherwise stand.
/// </remarks>
private IReadOnlyList < Section > BuildSections ()
{
var result = new List < Section >( this . Prompt . Parts . Count + 2 );
var hasRetrievedData = this . Prompt . Parts . Any ( part => part . Kind is SystemPromptPartKind . RETRIEVED_DATA );
foreach ( var part in this . Prompt . Parts )
{
result . Add ( this . SectionOf ( part ));
switch ( part . Kind )
{
case SystemPromptPartKind . BASE_PROMPT or SystemPromptPartKind . CHAT_TEMPLATE when ! hasRetrievedData :
if ( this . NoticeWithoutRetrievedData is { Length : > 0 } notice )
result . Add ( new ( T ( "Data from your data sources" ), string . Empty , notice ));
this . AddSwitchedOffProfile ( result );
break ;
case SystemPromptPartKind . RETRIEVED_DATA :
this . AddSwitchedOffProfile ( result );
break ;
}
}
return result ;
}
private Section SectionOf ( SystemPromptPart part ) => part . Kind switch
{
SystemPromptPartKind . DATE_TIME => new ( T ( "Current date and time" ), part . Text , string . Empty ),
2026-11-06 17:44:58 +01:00
// A prompt which an assistant wrote for the chat is not the one it created its result with, which the heading must not hide:
SystemPromptPartKind . BASE_PROMPT when part . Origin is { Source : ChatSystemPromptSource . CUSTOM } origin => new ( string . Format ( T ( "Chat prompt of the assistant '{0}'" ), origin . AssistantName ), part . Text , string . Format ( T ( "The assistant '{0}' wrote these instructions for your follow-up questions in this chat. They differ from the instructions with which the assistant created its result." ), origin . AssistantName )),
SystemPromptPartKind . BASE_PROMPT when part . Origin is { } origin => new ( string . Format ( T ( "System prompt of the assistant '{0}'" ), origin . AssistantName ), part . Text , string . Empty ),
2026-11-06 12:12:32 +01:00
SystemPromptPartKind . BASE_PROMPT when part . Text == SystemPrompts . DEFAULT => new ( T ( "Default prompt of AI Studio" ), part . Text , string . Empty ),
SystemPromptPartKind . BASE_PROMPT => new ( T ( "System prompt of this chat" ), part . Text , string . Empty ),
SystemPromptPartKind . CHAT_TEMPLATE => new ( string . Format ( T ( "Chat template '{0}'" ), part . SourceName ), part . Text , string . Empty ),
SystemPromptPartKind . RETRIEVED_DATA => new ( T ( "Data from your data sources" ), part . Text , this . NoticeWithRetrievedData ),
SystemPromptPartKind . PROFILE => new ( string . Format ( T ( "Profile '{0}'" ), part . SourceName ), part . Text , string . Empty ),
SystemPromptPartKind . TOOL_INSTRUCTIONS => new ( string . Format ( T ( "Instructions for your tools: {0}" ), string . Join ( ", " , this . ToolNames )), part . Text , T ( "When the AI reaches the limit of tool calls for one answer, AI Studio adds one more instruction at the end: to answer with what the tools have returned so far." )),
_ => new ( string . Empty , part . Text , string . Empty ),
};
private void AddSwitchedOffProfile ( List < Section > result )
{
if ( this . Prompt . SwitchedOffProfile == Profile . NO_PROFILE )
return ;
result . Add ( new ( string . Format ( T ( "Profile '{0}'" ), this . Prompt . SwitchedOffProfile . Name ), string . Empty , T ( "The chat template does not allow profiles, so your selected profile is not part of the prompt." )));
}
private string NoticeWithRetrievedData => this . RetrievalMode is DataSourceRetrievalMode . EVERY_MESSAGE
? T ( "AI Studio retrieved this data for your last message. When you send your next message, AI Studio searches your data sources again and replaces this data with what it finds then." )
: T ( "AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew." );
private string NoticeWithoutRetrievedData => this . RetrievalMode switch
{
DataSourceRetrievalMode . EVERY_MESSAGE => T ( "When you send your next message, AI Studio searches your data sources and adds what it finds here." ),
DataSourceRetrievalMode . SEMANTIC_SEARCH => T ( "The AI searches your data sources itself whenever a question calls for it. What it finds reaches the AI as the result of that search, not as part of this prompt." ),
_ => string . Empty ,
};
private async Task CopyCompletePrompt () => await this . RustService . CopyText2Clipboard ( this . Prompt . Text );
private void Close () => this . MudDialog . Close ();
/// <summary>
/// One part of the prompt as the dialog shows it.
/// </summary>
/// <param name="Heading">Where the part comes from.</param>
/// <param name="Text">The text of the part, or empty when only the notice stands there.</param>
/// <param name="Notice">What the user should know about the part, or empty when there is nothing to say.</param>
private sealed record Section ( string Heading , string Text , string Notice );
}