Added a way to see the complete system prompt of a chat (#1001)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
j-erlerandThorsten Sommer authored and GitHub committed 2026-11-06 12:12:32 +01:00
1 parent dce0b534aa
commit c37393817b
14 files changed
+815 -70

No files matched your search

@@ -4096,6 +4096,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile u
-- The selected provider is not allowed in this chat due to data security or confidence-level requirements.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2672162875"] = "The selected provider is not allowed in this chat due to data security or confidence-level requirements."
-- Show the complete system prompt the AI receives
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2931983492"] = "Show the complete system prompt the AI receives"
-- Bulleted List
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Bulleted List"
@@ -4135,6 +4138,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Heading"
-- Please select the workspace where you want to move the chat to.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Please select the workspace where you want to move the chat to."
-- Complete system prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T544159998"] = "Complete system prompt of this chat"
-- Move the chat to a workspace, or to another if it is already in one.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T636393754"] = "Move the chat to a workspace, or to another if it is already in one."
@@ -10807,6 +10813,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T406940705"] = "This i
-- Passages
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T4291613392"] = "Passages"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1014606529"] = "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."
-- Data from your data sources
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1629973648"] = "Data from your data sources"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1948716844"] = "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."
-- AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1989325201"] = "AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew."
-- Profile '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2068735836"] = "Profile '{0}'"
-- System prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2172559741"] = "System prompt of this chat"
-- Chat template '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2327622141"] = "Chat template '{0}'"
-- These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2763249726"] = "These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them."
-- this part
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2810340110"] = "this part"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T299624105"] = "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."
-- Copy the complete prompt
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3233146612"] = "Copy the complete prompt"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3448155331"] = "Close"
-- Default prompt of AI Studio
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3895504389"] = "Default prompt of AI Studio"
-- Instructions for your tools: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T472942707"] = "Instructions for your tools: {0}"
-- Current date and time
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T556236372"] = "Current date and time"
-- The chat template does not allow profiles, so your selected profile is not part of the prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T565876250"] = "The chat template does not allow profiles, so your selected profile is not part of the prompt."
-- When you send your next message, AI Studio searches your data sources and adds what it finds here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T920703436"] = "When you send your next message, AI Studio searches your data sources and adds what it finds here."
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider"
+86 -55
View File
@@ -390,43 +390,31 @@ public sealed record ChatThread
/// request which is never sent.
///
/// Nothing here writes to the thread and nothing logs, because this runs while somebody types.
///
/// Each part is recorded as it is added, in the order of the text, so that the dialog showing
/// the prompt names the parts from the decisions made here rather than repeating them.
/// </remarks>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <param name="runnableToolDefinitions">The tools which may run in this thread. Null when the thread runs without tools.</param>
/// <returns>The system prompt and what building it decided.</returns>
public PreparedSystemPrompt BuildSystemPrompt(SettingsManager settingsManager, IEnumerable<ToolDefinition>? runnableToolDefinitions = null)
{
var allowProfile = true;
var parts = new List<SystemPromptPart>();
//
// Use the information from the chat template, if provided. Otherwise, use the default system prompt
//
string systemPromptTextWithChatTemplate;
var logMessage = $"Using no chat template for chat thread '{this.Name}'.";
if (string.IsNullOrWhiteSpace(this.SelectedChatTemplate))
systemPromptTextWithChatTemplate = this.SystemPrompt;
else
{
if(!Guid.TryParse(this.SelectedChatTemplate, out var chatTemplateId))
systemPromptTextWithChatTemplate = this.SystemPrompt;
else
{
if(this.SelectedChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE.Id || chatTemplateId == Guid.Empty)
systemPromptTextWithChatTemplate = this.SystemPrompt;
else
{
var chatTemplate = settingsManager.GetChatTemplateById(this.SelectedChatTemplate);
if(chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE)
systemPromptTextWithChatTemplate = this.SystemPrompt;
else
{
logMessage = $"Using chat template '{chatTemplate.Name}' for chat thread '{this.Name}'.";
allowProfile = chatTemplate.AllowProfileUsage;
systemPromptTextWithChatTemplate = chatTemplate.ToSystemPrompt();
}
}
}
}
var chatTemplate = this.GetSelectedChatTemplate(settingsManager);
var usesChatTemplate = chatTemplate != ChatTemplate.NO_CHAT_TEMPLATE;
var allowProfile = !usesChatTemplate || chatTemplate.AllowProfileUsage;
var systemPromptTextWithChatTemplate = usesChatTemplate ? chatTemplate.ToSystemPrompt() : this.SystemPrompt;
var logMessage = usesChatTemplate
? $"Using chat template '{chatTemplate.Name}' for chat thread '{this.Name}'."
: $"Using no chat template for chat thread '{this.Name}'.";
parts.Add(usesChatTemplate
? new(SystemPromptPartKind.CHAT_TEMPLATE, systemPromptTextWithChatTemplate) { SourceName = chatTemplate.Name }
: new(SystemPromptPartKind.BASE_PROMPT, systemPromptTextWithChatTemplate));
//
// Add augmented data, if available:
@@ -436,51 +424,50 @@ public sealed record ChatThread
{
true => $"""
{systemPromptTextWithChatTemplate}
{this.AugmentedData}
""",
false => systemPromptTextWithChatTemplate,
};
if (isAugmentedDataAvailable)
parts.Add(new(SystemPromptPartKind.RETRIEVED_DATA, this.AugmentedData));
logMessage = isAugmentedDataAvailable
? $"{logMessage} Augmented data is available for the chat thread."
: $"{logMessage} No augmented data is available for the chat thread.";
//
// Add information from the profile if available and allowed:
// Add information from the profile if available and allowed. A selected profile which the
// chat template does not allow is reported, so that nobody wonders where it went:
//
string systemPromptText;
var profileNote = $"Using no profile for chat thread '{this.Name}'.";
if (string.IsNullOrWhiteSpace(this.SelectedProfile) || !allowProfile)
var profile = this.GetSelectedProfile(settingsManager);
var switchedOffProfile = Profile.NO_PROFILE;
if (profile == Profile.NO_PROFILE)
systemPromptText = systemPromptWithAugmentedData;
else if (!allowProfile)
{
systemPromptText = systemPromptWithAugmentedData;
switchedOffProfile = profile;
}
else
{
if(!Guid.TryParse(this.SelectedProfile, out var profileId))
systemPromptText = systemPromptWithAugmentedData;
else
{
if(this.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = systemPromptWithAugmentedData;
else
{
var profile = settingsManager.GetProfileById(this.SelectedProfile);
if(profile == Profile.NO_PROFILE)
systemPromptText = systemPromptWithAugmentedData;
else
{
profileNote = $"Using profile '{profile.Name}' for chat thread '{this.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
var profilePrompt = profile.ToSystemPrompt();
profileNote = $"Using profile '{profile.Name}' for chat thread '{this.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
{profile.ToSystemPrompt()}
""";
}
}
}
{profilePrompt}
""";
parts.Add(new(SystemPromptPartKind.PROFILE, profilePrompt) { SourceName = profile.Name });
}
var toolPolicy = ToolSelectionRules.BuildToolPolicyPrompt(runnableToolDefinitions ?? []);
var instructedTools = ToolSelectionRules.GetInstructedTools(runnableToolDefinitions ?? []);
var toolPolicy = ToolSelectionRules.BuildToolPolicyPrompt(instructedTools);
if (!string.IsNullOrWhiteSpace(toolPolicy))
{
systemPromptText = $"""
@@ -488,11 +475,13 @@ public sealed record ChatThread
{toolPolicy}
""";
parts.Add(new(SystemPromptPartKind.TOOL_INSTRUCTIONS, toolPolicy) { Tools = instructedTools });
}
var explanation = $"{logMessage} {profileNote}";
if(!this.IncludeDateTime)
return new(systemPromptText, systemPromptTextWithChatTemplate, allowProfile, explanation);
return new(systemPromptText, systemPromptTextWithChatTemplate, explanation, parts, switchedOffProfile);
//
// Prepend the current date and time to the system prompt:
@@ -510,7 +499,49 @@ public sealed record ChatThread
{systemPromptText}
""";
return new(withDateTime, systemPromptTextWithChatTemplate, allowProfile, explanation);
parts.Insert(0, new(SystemPromptPartKind.DATE_TIME, currentDateTime));
return new(withDateTime, systemPromptTextWithChatTemplate, explanation, parts, switchedOffProfile);
}
/// <summary>
/// The chat template selected for this thread, or ChatTemplate.NO_CHAT_TEMPLATE when there is none.
/// </summary>
/// <remarks>
/// A selection which is no ID, the ID of no chat template, or that of a chat template which no
/// longer exists counts as none, so the thread falls back to its own system prompt.
/// </remarks>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <returns>The selected chat template, or ChatTemplate.NO_CHAT_TEMPLATE.</returns>
private ChatTemplate GetSelectedChatTemplate(SettingsManager settingsManager)
{
if (string.IsNullOrWhiteSpace(this.SelectedChatTemplate) || !Guid.TryParse(this.SelectedChatTemplate, out var chatTemplateId))
return ChatTemplate.NO_CHAT_TEMPLATE;
if (this.SelectedChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE.Id || chatTemplateId == Guid.Empty)
return ChatTemplate.NO_CHAT_TEMPLATE;
return settingsManager.GetChatTemplateById(this.SelectedChatTemplate);
}
/// <summary>
/// The profile selected for this thread, or Profile.NO_PROFILE when there is none.
/// </summary>
/// <remarks>
/// The same rules as for the chat template: a selection which is no ID, the ID of no profile, or
/// that of a profile which no longer exists counts as none. Whether the chat template lets the
/// profile take part is decided by BuildSystemPrompt.
/// </remarks>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <returns>The selected profile, or Profile.NO_PROFILE.</returns>
private Profile GetSelectedProfile(SettingsManager settingsManager)
{
if (string.IsNullOrWhiteSpace(this.SelectedProfile) || !Guid.TryParse(this.SelectedProfile, out var profileId))
return Profile.NO_PROFILE;
if (this.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
return Profile.NO_PROFILE;
return settingsManager.GetProfileById(this.SelectedProfile);
}
/// <summary>
@@ -1,3 +1,5 @@
using AIStudio.Settings;
namespace AIStudio.Chat;
/// <summary>
@@ -7,13 +9,15 @@ namespace AIStudio.Chat;
/// The system prompt is not the text a person typed into it. A chat template may replace it, the
/// retrieved data of a data source is appended to it, a profile adds its own paragraph, the tool
/// policy adds another, and the current date goes in front of everything. Whoever wants to know how
/// long the next request is has to ask the same question the request does.
/// long the next request is has to ask the same question the request does, and whoever shows the
/// prompt reads its parts from here rather than repeating the decisions behind them.
/// </remarks>
/// <param name="Text">The whole system prompt, as the provider receives it.</param>
/// <param name="BasePrompt">
/// The prompt without any of the parts added around it. The thread keeps this one, so that it can
/// still say which prompt it was configured with rather than the assembled result.
/// </param>
/// <param name="ProfileIsAllowed">Whether the chat template let a profile take part.</param>
/// <param name="Explanation">What was used, in one sentence, for the log.</param>
public sealed record PreparedSystemPrompt(string Text, string BasePrompt, bool ProfileIsAllowed, string Explanation);
/// <param name="Parts">The parts the text is made of, in the order in which they stand in it.</param>
/// <param name="SwitchedOffProfile">The profile the user selected but the chat template does not let take part, or Profile.NO_PROFILE when nothing was switched off.</param>
public sealed record PreparedSystemPrompt(string Text, string BasePrompt, string Explanation, IReadOnlyList<SystemPromptPart> Parts, Profile SwitchedOffProfile);
@@ -0,0 +1,26 @@
using AIStudio.Tools.ToolCallingSystem;
namespace AIStudio.Chat;
/// <summary>
/// One part of the system prompt, as it stands in the text sent to the provider.
/// </summary>
/// <remarks>
/// Recorded by ChatThread.BuildSystemPrompt while it assembles the text, so that whoever shows the
/// prompt learns where each paragraph came from from the one place which decided it, instead of
/// working it out again.
/// </remarks>
/// <param name="Kind">Where the part comes from.</param>
/// <param name="Text">The text of the part, exactly as it stands in the system prompt.</param>
public sealed record SystemPromptPart(SystemPromptPartKind Kind, string Text)
{
/// <summary>
/// The name of the chat template or the profile the part comes from; empty for every other part.
/// </summary>
public string SourceName { get; init; } = string.Empty;
/// <summary>
/// The tools whose usage policy the part states; empty for every other part.
/// </summary>
public IReadOnlyList<ToolDefinition> Tools { get; init; } = [];
}
@@ -0,0 +1,41 @@
namespace AIStudio.Chat;
/// <summary>
/// Where a part of the system prompt comes from.
/// </summary>
/// <remarks>
/// The members are listed in the order in which the parts stand in the system prompt, see
/// ChatThread.BuildSystemPrompt.
/// </remarks>
public enum SystemPromptPartKind
{
/// <summary>
/// The current date and time, in front of everything else.
/// </summary>
DATE_TIME,
/// <summary>
/// The prompt the chat was set up with, used when no chat template replaces it.
/// </summary>
BASE_PROMPT,
/// <summary>
/// The prompt of the chat template, which replaces the one the chat was set up with.
/// </summary>
CHAT_TEMPLATE,
/// <summary>
/// What the last request retrieved from the data sources of the chat.
/// </summary>
RETRIEVED_DATA,
/// <summary>
/// What the selected profile tells about the user.
/// </summary>
PROFILE,
/// <summary>
/// How to use the tools the request offers.
/// </summary>
TOOL_INSTRUCTIONS,
}
@@ -134,6 +134,10 @@
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
<MudTooltip Text="@T("Show the complete system prompt the AI receives")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Visibility" OnClick="@this.ShowSystemPrompt"/>
</MudTooltip>
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.CanProfileBeChanged)" DisabledText="@this.ProfileDisabledText"/>
@if (this.SettingsManager.AreToolsEnabled())
@@ -1775,7 +1775,7 @@ public partial class ChatComponent : MSGComponentBase
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
this.ChatThread.RuntimeAssistantName = string.Empty;
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
this.ChatThread.RuntimeSelectedToolIds = this.GetSelectedToolIdsForRequest(this.ChatThread);
// From here on, the AI job stores the chat, while chats are stored automatically:
this.unstoredChat = null;
@@ -2451,7 +2451,7 @@ public partial class ChatComponent : MSGComponentBase
// of it would tell a person their window is empty while their first message is not.
//
var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId);
var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch);
var toolDefinitions = this.GetRunnableToolDefinitions(thread, offersSemanticSearch);
provider = this.Provider;
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
reported = thread.ReportedHistoryFor(provider.Model);
@@ -2485,6 +2485,44 @@ public partial class ChatComponent : MSGComponentBase
/// <returns>The system prompt as it would be sent.</returns>
private string BuildSystemPromptFor(ChatThread thread, IReadOnlyList<ToolDefinition> toolDefinitions) => thread.BuildSystemPrompt(this.SettingsManager, toolDefinitions).Text;
/// <summary>
/// Shows the system prompt the next request would send, and what it was assembled from.
/// </summary>
/// <remarks>
/// Before the first message there is no thread yet, and the one a new chat would start with
/// stands in for it, as it does for the token count.
///
/// The tools are asked for the way a request asks for them, not the way the token count does:
/// a dialog which promises the prompt as the AI receives it has to show the instructions as the
/// request words them, see GetOfferedToolDefinitionsAsync.
///
/// What the prompt is made of comes from ChatThread.BuildSystemPrompt, which decides it; only
/// the names of the tools are looked up here, because the thread does not know the registry.
/// How the next message searches the data sources comes along, since the retrieved data in the
/// prompt belongs to the last message, and the dialog has to say what happens with the next.
/// </remarks>
private async Task ShowSystemPrompt()
{
var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId);
var toolDefinitions = await this.GetOfferedToolDefinitionsAsync(thread);
var retrievalMode = await this.GetRetrievalModeOfNextRequestAsync();
var prepared = thread.BuildSystemPrompt(this.SettingsManager, toolDefinitions);
var toolNames = prepared.Parts
.SelectMany(part => part.Tools)
.Select(definition => this.ToolRegistry.GetImplementation(definition.ImplementationKey)?.GetDisplayName() ?? definition.Function.Name)
.Distinct(StringComparer.Ordinal)
.ToList();
var dialogParameters = new DialogParameters<SystemPromptDialog>
{
{ x => x.Prompt, prepared },
{ x => x.ToolNames, toolNames },
{ x => x.RetrievalMode, retrievalMode },
};
await this.DialogService.ShowAsync<SystemPromptDialog>(T("Complete system prompt of this chat"), dialogParameters, DialogOptions.FULLSCREEN);
}
/// <summary>
/// The tools the next request would offer the model.
/// </summary>
@@ -2509,12 +2547,12 @@ public partial class ChatComponent : MSGComponentBase
/// private hosts configured, leaves Read Web Page nothing to offer, and only the request finds
/// that out, see ReadWebPageTool.ResolveFunctionAsync.
/// </remarks>
/// <param name="thread">The thread the next request continues.</param>
/// <param name="offersSemanticSearch">Whether the next request offers Semantic Search, see OffersSemanticSearchAsync.</param>
/// <returns>The definitions of the selected tools, and of Semantic Search when it is offered.</returns>
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions(bool offersSemanticSearch)
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions(ChatThread thread, bool offersSemanticSearch)
{
var outboundDataRestriction = (this.ChatThread?.RequiredOutboundDataRestriction ?? OutboundDataRequirement.NONE).Restriction;
var definitions = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, outboundDataRestriction)
var definitions = this.GetSelectedToolIdsForRequest(thread)
.Select(this.ToolRegistry.GetDefinition)
.Where(definition => definition is not null)
.Select(definition => definition!)
@@ -2526,6 +2564,50 @@ public partial class ChatComponent : MSGComponentBase
return definitions;
}
/// <summary>
/// The tools the next request offers the model, each as that request words it.
/// </summary>
/// <remarks>
/// Asked the way the providers ask right before sending: the same selection, the same provider
/// settings and trust, the chat as the request finds it. Only then do the tools answer what
/// they offer and how they word their instructions, see ToolRegistry.GetRunnableToolsAsync.
/// So a tool which is not configured, or has nothing to offer in this chat, stays out, and
/// Read Web Page, Search Confluence, Search Mails, and Semantic Search state the rules which
/// actually apply.
///
/// Unlike GetRunnableToolDefinitions, this may ask the servers of ERI data sources, which is
/// fine when somebody opens a dialog, but not while somebody types. In the chat, the tool
/// selection is always visible, so the request may always run tools.
/// </remarks>
/// <param name="thread">The thread the next request continues.</param>
/// <returns>The definitions of the tools as the next request offers them.</returns>
private async Task<IReadOnlyList<ToolDefinition>> GetOfferedToolDefinitionsAsync(ChatThread thread)
{
var context = new ToolResolutionContext
{
Provider = this.Provider,
Component = Tools.Components.CHAT,
ProviderConfidence = this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level,
ChatThread = thread,
};
var mayRunTools = this.SettingsManager.IsToolSelectionVisible(Tools.Components.CHAT);
var offeredTools = await this.ToolRegistry.GetRunnableToolsAsync(context, this.GetSelectedToolIdsForRequest(thread), mayRunTools);
return offeredTools.Select(tool => tool.Definition).ToList();
}
/// <summary>
/// The tools the user selected, as far as the next request may offer them.
/// </summary>
/// <remarks>
/// Left out are tools the provider is not trusted enough for and tools the internal content of
/// the chat keeps back. Sending, the token count, and the system prompt dialog all start from
/// this one selection.
/// </remarks>
/// <param name="thread">The thread the next request continues.</param>
/// <returns>The IDs of the selected tools the next request may offer.</returns>
private HashSet<string> GetSelectedToolIdsForRequest(ChatThread thread) => this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, thread.RequiredOutboundDataRestriction.Restriction);
/// <summary>
/// Whether the next request offers the model Semantic Search, as far as this can be told without asking the data sources.
/// </summary>
@@ -2535,14 +2617,25 @@ public partial class ChatComponent : MSGComponentBase
/// there, the number the provider reported takes over anyway, see ChatThread.ReportedHistoryFor.
/// </remarks>
/// <returns>True when the next request offers Semantic Search, as far as can be told.</returns>
private async Task<bool> OffersSemanticSearchAsync()
private async Task<bool> OffersSemanticSearchAsync() => await this.GetRetrievalModeOfNextRequestAsync() is DataSourceRetrievalMode.SEMANTIC_SEARCH;
/// <summary>
/// How the next request searches the data sources of the chat.
/// </summary>
/// <remarks>
/// The same questions the RAG process asks when a message is sent: whether data sources are
/// switched on for the chat at all, and then how they are searched, see
/// ToolRegistry.GetEffectiveRetrievalModeAsync.
/// </remarks>
/// <returns>How the data sources are searched, or null when the next request searches none.</returns>
private async Task<DataSourceRetrievalMode?> GetRetrievalModeOfNextRequestAsync()
{
var options = this.GetCurrentDataSourceOptions();
if (!PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager) || !options.IsEnabled())
return false;
return null;
var retrievalMode = await this.ToolRegistry.GetEffectiveRetrievalModeAsync(options, this.Provider, Tools.Components.CHAT);
return retrievalMode.Mode is DataSourceRetrievalMode.SEMANTIC_SEARCH;
return retrievalMode.Mode;
}
/// <summary>
@@ -0,0 +1,32 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them.")
</MudJustifiedText>
@foreach (var section in this.sections)
{
<MudText Typo="Typo.h6" Class="mt-4 mb-2">@section.Heading</MudText>
@if (!string.IsNullOrWhiteSpace(section.Notice))
{
<MudAlert Severity="Severity.Info" Dense="@true" Class="mb-2">@section.Notice</MudAlert>
}
@if (!string.IsNullOrEmpty(section.Text))
{
<TextInfoLines Lines="1" MaxLines="12" Value="@section.Text" ClipboardTooltipSubject="@T("this part")"/>
}
}
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.CopyCompletePrompt" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ContentCopy">
@T("Copy the complete prompt")
</MudButton>
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,142 @@
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>
/// Nobody types this prompt as it is sent. A chat template replaces the default, a profile, the
/// 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),
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);
}
@@ -4098,6 +4098,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Die Profi
-- The selected provider is not allowed in this chat due to data security or confidence-level requirements.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2672162875"] = "Der ausgewählte Anbieter ist in diesem Chat aufgrund der Datensicherheit oder der Anforderungen an das Vertrauensniveau nicht zulässig."
-- Show the complete system prompt the AI receives
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2931983492"] = "Den vollständigen System-Prompt anzeigen, den die KI erhält"
-- Bulleted List
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Aufzählungszeichen"
@@ -4137,6 +4140,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Überschr
-- Please select the workspace where you want to move the chat to.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Bitte wählen Sie den Arbeitsbereich aus, in den Sie den Chat verschieben möchten."
-- Complete system prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T544159998"] = "Vollständiger System-Prompt dieses Chats"
-- Move the chat to a workspace, or to another if it is already in one.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T636393754"] = "Verschieben Sie den Chat in einen Arbeitsbereich oder in einen anderen, falls er sich bereits in einem befindet."
@@ -10809,6 +10815,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T406940705"] = "Genau
-- Passages
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T4291613392"] = "Textstellen"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1014606529"] = "Erreicht die KI bei einer Antwort die Höchstzahl an Werkzeugaufrufen, fügt AI Studio am Ende eine weitere Anweisung hinzu: mit dem zu antworten, was die Werkzeuge bis dahin geliefert haben."
-- Data from your data sources
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1629973648"] = "Daten aus Ihren Datenquellen"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1948716844"] = "Die KI durchsucht Ihre Datenquellen selbst, sobald eine Frage es erfordert. Was sie findet, erhält sie als Ergebnis dieser Suche, nicht als Teil dieses Prompts."
-- AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1989325201"] = "AI Studio hat diese Daten mit Ihrer letzten Nachricht gesendet. Wenn Sie Ihre nächste Nachricht senden, stellt AI Studio diesen Teil neu zusammen."
-- Profile '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2068735836"] = "Profil „{0}“"
-- System prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2172559741"] = "System-Prompt dieses Chats"
-- Chat template '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2327622141"] = "Chat-Vorlage „{0}“"
-- These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2763249726"] = "Dies sind die Anweisungen, die die KI vor Ihrer Unterhaltung erhält: der System-Prompt dieses Chats. AI Studio setzt ihn aus den folgenden Teilen zusammen, in der Reihenfolge, in der die KI sie liest."
-- this part
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2810340110"] = "Diesen Teil"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T299624105"] = "AI Studio hat diese Daten für Ihre letzte Nachricht abgerufen. Wenn Sie Ihre nächste Nachricht senden, durchsucht AI Studio Ihre Datenquellen erneut und ersetzt diese Daten durch das, was es dann findet."
-- Copy the complete prompt
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3233146612"] = "Vollständigen Prompt kopieren"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3448155331"] = "Schließen"
-- Default prompt of AI Studio
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3895504389"] = "Standard-Prompt von AI Studio"
-- Instructions for your tools: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T472942707"] = "Anweisungen für Ihre Werkzeuge: {0}"
-- Current date and time
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T556236372"] = "Aktuelles Datum und aktuelle Uhrzeit"
-- The chat template does not allow profiles, so your selected profile is not part of the prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T565876250"] = "Die Chat-Vorlage erlaubt keine Profile, daher ist Ihr ausgewähltes Profil nicht Teil des Prompts."
-- When you send your next message, AI Studio searches your data sources and adds what it finds here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T920703436"] = "Wenn Sie Ihre nächste Nachricht senden, durchsucht AI Studio Ihre Datenquellen und fügt hier hinzu, was es findet."
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging-Face-Inferenzanbieter"
@@ -4098,6 +4098,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile u
-- The selected provider is not allowed in this chat due to data security or confidence-level requirements.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2672162875"] = "The selected provider is not allowed in this chat due to data security or confidence-level requirements."
-- Show the complete system prompt the AI receives
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2931983492"] = "Show the complete system prompt the AI receives"
-- Bulleted List
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Bulleted List"
@@ -4137,6 +4140,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Heading"
-- Please select the workspace where you want to move the chat to.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Please select the workspace where you want to move the chat to."
-- Complete system prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T544159998"] = "Complete system prompt of this chat"
-- Move the chat to a workspace, or to another if it is already in one.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T636393754"] = "Move the chat to a workspace, or to another if it is already in one."
@@ -10809,6 +10815,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T406940705"] = "This i
-- Passages
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SOURCEPASSAGESDIALOG::T4291613392"] = "Passages"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1014606529"] = "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."
-- Data from your data sources
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1629973648"] = "Data from your data sources"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1948716844"] = "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."
-- AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T1989325201"] = "AI Studio sent this data with your last message. When you send your next message, AI Studio works out this part anew."
-- Profile '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2068735836"] = "Profile '{0}'"
-- System prompt of this chat
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2172559741"] = "System prompt of this chat"
-- Chat template '{0}'
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2327622141"] = "Chat template '{0}'"
-- These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2763249726"] = "These are the instructions the AI receives ahead of your conversation, the system prompt of this chat. AI Studio assembles it from the parts below, in the order in which the AI reads them."
-- this part
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T2810340110"] = "this part"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T299624105"] = "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."
-- Copy the complete prompt
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3233146612"] = "Copy the complete prompt"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3448155331"] = "Close"
-- Default prompt of AI Studio
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T3895504389"] = "Default prompt of AI Studio"
-- Instructions for your tools: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T472942707"] = "Instructions for your tools: {0}"
-- Current date and time
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T556236372"] = "Current date and time"
-- The chat template does not allow profiles, so your selected profile is not part of the prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T565876250"] = "The chat template does not allow profiles, so your selected profile is not part of the prompt."
-- When you send your next message, AI Studio searches your data sources and adds what it finds here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SYSTEMPROMPTDIALOG::T920703436"] = "When you send your next message, AI Studio searches your data sources and adds what it finds here."
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider"
@@ -33,12 +33,24 @@ public static class ToolSelectionRules
: null;
}
/// <summary>
/// The tools whose usage policy goes into the system prompt.
/// </summary>
/// <remarks>
/// A tool without instructions is offered all the same, but adds nothing to the system prompt.
/// BuildToolPolicyPrompt writes the policy of exactly these tools, and the system prompt names
/// them as the source of that part, so the two cannot disagree about which tools take part.
/// </remarks>
/// <param name="definitions">The tools a request offers.</param>
/// <returns>The tools with instructions, in the order given.</returns>
public static IReadOnlyList<ToolDefinition> GetInstructedTools(IEnumerable<ToolDefinition> definitions) => definitions
.Where(x => !string.IsNullOrWhiteSpace(x.SystemPromptInstructions))
.ToList();
public static string BuildToolPolicyPrompt(IEnumerable<ToolDefinition> definitions)
{
var policySections = definitions
.Select(x => (ToolName: x.Function.Name, PolicyLines: x.SystemPromptInstructions.Trim()))
.Where(x => !string.IsNullOrWhiteSpace(x.PolicyLines))
.Select(x => $"## Tool `{x.ToolName}`{Environment.NewLine}{x.PolicyLines}")
var policySections = GetInstructedTools(definitions)
.Select(x => $"## Tool `{x.Function.Name}`{Environment.NewLine}{x.SystemPromptInstructions.Trim()}")
.Distinct(StringComparer.Ordinal)
.ToList();
if (policySections.Count == 0)
@@ -6,6 +6,7 @@
- Added the Mathematics tools as a prototype: the AI can now calculate exactly and work with formulas, right on your computer. It keeps fractions exact, gives results with up to 100 digits, solves equations, differentiates and integrates, and can check whether an integral really has the value someone claims. AI Studio checks the results numerically wherever possible, and the AI tells you when a result is only approximate. To try it, open the app settings, allow preview features down to prototype, enable Mathematics, and then choose Mathematics among the tools of a chat.
- Added a way for IT departments to preselect the options of the Prompt Optimizer, such as the prompt guide of your organization, the language of the optimized prompt, and the provider. They can lock each option or let you change it.
- Added your own prompt guide to the options of the Prompt Optimizer. Choose the file once, and the assistant uses it whenever you open it or start over, always with the current content of the file.
- Added a way to see the complete system prompt of a chat: the button with the eye in the chat toolbar shows the instructions the AI receives ahead of your conversation, part by part, such as your chat template, your profile, data from your data sources, and the instructions of your tools. You can copy each part or the complete prompt. Thanks, Jens Erler (`j-erler`), for this contribution.
- Improved the documentation for IT departments, which now lives in our [wiki](https://github.com/MindWorkAI/AI-Studio/wiki/Enterprise-IT). It is sorted by task, explains the setup on Windows, Linux, and macOS, and offers complete configurations for typical needs, such as using only local models. The link in the app settings leads there, and the wiki also holds the installation guide in English and German.
- Improved the German translation throughout: we proofread all texts and corrected wrong labels as well as spelling and grammar mistakes. The texts now always address you formally and use the same terms everywhere.
- Improved the security of the connection between the app window and AI Studio.
@@ -0,0 +1,188 @@
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools.Services;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Chat;
/// <summary>
/// Checks that the system prompt reports the parts it is made of.
/// </summary>
/// <remarks>
/// The dialog which shows the system prompt names its parts from what building it decided. The text
/// itself is what the request sends and the token count measures, so recording the parts must never
/// change it: each part stands in the text exactly as recorded, in the order recorded.
/// </remarks>
[TestFixture]
public sealed class ChatThreadSystemPromptTests
{
private const string BASE_PROMPT = "You are a helpful assistant.";
private const string TEMPLATE_PROMPT = "You review contracts and point out risks.";
private const string RETRIEVED_DATA = "The following useful information will help you in processing the user prompt: the travel policy allows trains only.";
private static readonly string TEMPLATE_ID = Guid.NewGuid().ToString();
private static readonly string PROFILE_ID = Guid.NewGuid().ToString();
[Test]
public void TheBasePromptStandsAloneWithoutAnythingElse()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: true);
var prepared = Thread().BuildSystemPrompt(settings);
using (Assert.EnterMultipleScope())
{
Assert.That(prepared.Text, Is.EqualTo(BASE_PROMPT));
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
Assert.That(prepared.SwitchedOffProfile, Is.EqualTo(Profile.NO_PROFILE));
}
}
[Test]
public void EveryPartStandsInTheTextInItsOrder()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: true);
var thread = Thread();
thread.IncludeDateTime = true;
thread.SelectedChatTemplate = TEMPLATE_ID;
thread.SelectedProfile = PROFILE_ID;
thread.AugmentedData = RETRIEVED_DATA;
var tool = Tool("calculate", "Use `calculate` for every arithmetic.");
var prepared = thread.BuildSystemPrompt(settings, [tool]);
using (Assert.EnterMultipleScope())
{
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[]
{
SystemPromptPartKind.DATE_TIME,
SystemPromptPartKind.CHAT_TEMPLATE,
SystemPromptPartKind.RETRIEVED_DATA,
SystemPromptPartKind.PROFILE,
SystemPromptPartKind.TOOL_INSTRUCTIONS,
}));
var position = 0;
foreach (var part in prepared.Parts)
{
var found = prepared.Text.IndexOf(part.Text, position, StringComparison.Ordinal);
Assert.That(found, Is.GreaterThanOrEqualTo(position), $"The part {part.Kind} must stand in the text after the parts before it.");
position = found + part.Text.Length;
}
Assert.That(position, Is.EqualTo(prepared.Text.Length), "Nothing may follow the last part.");
Assert.That(prepared.Parts[1].SourceName, Is.EqualTo("Legal review"));
Assert.That(prepared.Parts[1].Text, Is.EqualTo(TEMPLATE_PROMPT));
Assert.That(prepared.Parts[2].Text, Is.EqualTo(RETRIEVED_DATA));
Assert.That(prepared.Parts[3].SourceName, Is.EqualTo("Developer"));
Assert.That(prepared.Parts[4].Tools, Is.EqualTo(new[] { tool }));
Assert.That(prepared.BasePrompt, Is.EqualTo(TEMPLATE_PROMPT));
}
}
[Test]
public void AChatTemplateWhichForbidsProfilesSwitchesTheSelectedProfileOff()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: false);
var thread = Thread();
thread.SelectedChatTemplate = TEMPLATE_ID;
thread.SelectedProfile = PROFILE_ID;
var prepared = thread.BuildSystemPrompt(settings);
using (Assert.EnterMultipleScope())
{
Assert.That(prepared.Text, Is.EqualTo(TEMPLATE_PROMPT));
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.CHAT_TEMPLATE }));
Assert.That(prepared.SwitchedOffProfile.Name, Is.EqualTo("Developer"));
}
}
[Test]
public void AChatTemplateWhichForbidsProfilesSwitchesNothingOffWithoutAProfile()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: false);
var thread = Thread();
thread.SelectedChatTemplate = TEMPLATE_ID;
thread.SelectedProfile = Profile.NO_PROFILE.Id;
var prepared = thread.BuildSystemPrompt(settings);
Assert.That(prepared.SwitchedOffProfile, Is.EqualTo(Profile.NO_PROFILE), "Nothing was selected, so the chat template switched nothing off.");
}
[Test]
public void AChatTemplateWhichNoLongerExistsLeavesTheBasePrompt()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: true);
var thread = Thread();
thread.SelectedChatTemplate = Guid.NewGuid().ToString();
var prepared = thread.BuildSystemPrompt(settings);
using (Assert.EnterMultipleScope())
{
Assert.That(prepared.Text, Is.EqualTo(BASE_PROMPT));
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
}
}
[Test]
public void AToolWithoutInstructionsAddsNoPart()
{
using var rustService = new RustService("1", "unused");
var settings = Settings(rustService, allowProfileUsage: true);
var withInstructions = Tool("calculate", "Use `calculate` for every arithmetic.");
var withoutInstructions = Tool("silent_tool", " ");
using (Assert.EnterMultipleScope())
{
var silent = Thread().BuildSystemPrompt(settings, [withoutInstructions]);
Assert.That(silent.Text, Is.EqualTo(BASE_PROMPT));
Assert.That(silent.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
var mixed = Thread().BuildSystemPrompt(settings, [withoutInstructions, withInstructions]);
Assert.That(mixed.Parts[^1].Tools, Is.EqualTo(new[] { withInstructions }), "Only a tool with instructions is a source of that part.");
Assert.That(mixed.Text, Does.Not.Contain("`silent_tool`"));
}
}
private static ChatThread Thread() => new() { SystemPrompt = BASE_PROMPT };
private static ToolDefinition Tool(string name, string instructions) => new()
{
Id = name,
ImplementationKey = name,
SystemPromptInstructions = instructions,
Function = new() { Name = name },
};
private static SettingsManager Settings(RustService rustService, bool allowProfileUsage)
{
var settings = new SettingsManager(NullLogger<SettingsManager>.Instance, rustService);
settings.ConfigurationData.ChatTemplates.Add(new ChatTemplate
{
Num = 1,
Id = TEMPLATE_ID,
Name = "Legal review",
SystemPrompt = TEMPLATE_PROMPT,
AllowProfileUsage = allowProfileUsage,
});
settings.ConfigurationData.Profiles.Add(new Profile
{
Num = 1,
Id = PROFILE_ID,
Name = "Developer",
NeedToKnow = "I write C# for a living.",
Actions = "Answer briefly.",
});
return settings;
}
}