Add file attachment support to chat templates

This commit is contained in:
Thorsten Sommer 2026-01-01 15:57:27 +01:00
parent 3b76b06d5d
commit 4ee7bbdaf7
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
13 changed files with 136 additions and 19 deletions

View File

@ -2443,6 +2443,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile
-- Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs."
-- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments"
-- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
@ -2482,6 +2485,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3016903701"] = "The nam
-- Image content
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3094908719"] = "Image content"
-- You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template."
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats."

View File

@ -36,6 +36,15 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public bool UseSmallForm { get; set; }
/// <summary>
/// When true, validate media file types before attaching. Default is true. That means that
/// the user cannot attach unsupported media file types when the provider or model does not
/// support them. Set it to false in order to disable this validation. This is useful for places
/// where the user might want to prepare a template.
/// </summary>
[Parameter]
public bool ValidateMediaFileTypes { get; set; } = true;
[Parameter]
public AIStudio.Settings.Provider? Provider { get; set; }
@ -117,7 +126,7 @@ public partial class AttachDocuments : MSGComponentBase
foreach (var path in paths)
{
if(!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.Provider))
if(!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
continue;
this.DocumentPaths.Add(FileAttachment.FromPath(path));
@ -161,7 +170,7 @@ public partial class AttachDocuments : MSGComponentBase
if (!File.Exists(selectedFilePath))
continue;
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.Provider))
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.ValidateMediaFileTypes, this.Provider))
continue;
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));

View File

@ -79,7 +79,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Get the preselected chat template:
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
// Apply template's file attachments, if any:
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment);
//
// Check for deferred messages of the kind 'SEND_TO_CHAT',
// aka the user sends an assistant result to the chat:
@ -328,7 +332,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.currentChatTemplate = chatTemplate;
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
// Apply template's file attachments (replaces existing):
this.chatDocumentPaths.Clear();
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment);
if(this.ChatThread is null)
return;
@ -677,9 +686,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
};
}
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
// Apply template's file attachments:
this.chatDocumentPaths.Clear();
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment);
// Now, we have to reset the data source options as well:
this.ApplyStandardDataSourceOptions();

View File

@ -77,7 +77,21 @@
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your predefined user input.")"
/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("File Attachments")
</MudText>
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template.")
</MudJustifiedText>
<AttachDocuments
Name="ChatTemplateFileAttachments"
@bind-DocumentPaths="@this.fileAttachments"
UseSmallForm="false"
CatchAllDocuments="false"
ValidateMediaFileTypes="false"
/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Profile Usage")
</MudText>

View File

@ -50,7 +50,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
[Parameter]
public IReadOnlyCollection<ContentBlock> ExampleConversation { get; init; } = [];
[Parameter]
[Parameter]
public IReadOnlyCollection<FileAttachment> FileAttachments { get; init; } = [];
[Parameter]
public bool AllowProfileUsage { get; set; } = true;
[Parameter]
@ -71,6 +74,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
private bool dataIsValid;
private List<ContentBlock> dataExampleConversation = [];
private HashSet<FileAttachment> fileAttachments = [];
private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty;
private bool isInlineEditOnGoing;
@ -95,6 +99,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
{
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
this.dataExampleConversation = this.ExampleConversation.Select(n => n.DeepClone()).ToList();
this.fileAttachments = [..this.FileAttachments];
}
if (this.CreateFromExistingChatThread && this.ExistingChatThread is not null)
@ -128,6 +133,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
SystemPrompt = this.DataSystemPrompt,
PredefinedUserPrompt = this.PredefinedUserPrompt,
ExampleConversation = this.dataExampleConversation,
FileAttachments = [..this.fileAttachments],
AllowProfileUsage = this.AllowProfileUsage,
EnterpriseConfigurationPluginId = Guid.Empty,

View File

@ -65,6 +65,7 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
{ x => x.PredefinedUserPrompt, chatTemplate.PredefinedUserPrompt },
{ x => x.IsEditing, true },
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
{ x => x.FileAttachments, chatTemplate.FileAttachments },
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
};

View File

@ -125,6 +125,33 @@ CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
}
}
-- An example chat template with file attachments:
-- This template automatically attaches specified files when the user selects it.
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "00000000-0000-0000-0000-000000000001",
["Name"] = "Document Analysis Template",
["SystemPrompt"] = "You are an expert document analyst. Please analyze the attached documents and provide insights.",
["PredefinedUserPrompt"] = "Please analyze the attached company guidelines and summarize the key points.",
["AllowProfileUsage"] = true,
-- Optional: Pre-attach files that will be automatically included when using this template.
-- These files will be loaded when the user selects this chat template.
-- Note: File paths must be absolute paths and accessible to all users.
["FileAttachments"] = {
"G:\\Company\\Documents\\Guidelines.pdf",
"G:\\Company\\Documents\\CompanyPolicies.docx"
},
["ExampleConversation"] = {
{
["Role"] = "USER",
["Content"] = "I have attached the company documents for analysis."
},
{
["Role"] = "AI",
["Content"] = "Thank you. I'll analyze the documents and provide a comprehensive summary."
}
}
}
-- Profiles for this configuration:
CONFIG["PROFILES"] = {}

View File

@ -2445,6 +2445,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profiln
-- Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Fügen Sie Nachrichten einer Beispiel-Konversation hinzu (Nutzereingabe, gefolgt von einer Antwort des Assistenten), um das gewünschte Interaktionsmuster zu demonstrieren. Diese Beispiele helfen der KI, ihre Erwartungen zu verstehen, indem Sie das korrekte Format, den Stil und den Inhalt von Antworten zeigen, bevor tatsächliche Nutzereingaben erfolgen."
-- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateianhänge"
-- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle"
@ -2484,6 +2487,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3016903701"] = "Der Nam
-- Image content
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3094908719"] = "Bildinhalt"
-- You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "Sie können Dateien anhängen, die bei Verwendung dieser Chat-Vorlage automatisch einbezogen werden. Diese Dateien werden der ersten Nachricht hinzugefügt, die in einem Chat gesendet wird, der diese Vorlage verwendet."
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Sie unsicher, welchen System-Prompt Sie verwenden sollen? Sie können mit dem Standard-System-Prompt beginnen, den AI Studio für alle Chats verwendet."
@ -5196,9 +5202,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
-- no model selected
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgewählt"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
-- Navigation never expands, but there are tooltips
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Die Navigationsleiste wird nie ausgeklappt, aber es gibt Tooltips"

View File

@ -2445,6 +2445,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile
-- Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs."
-- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments"
-- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
@ -2484,6 +2487,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3016903701"] = "The nam
-- Image content
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3094908719"] = "Image content"
-- You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template."
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats."
@ -5196,9 +5202,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
-- no model selected
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
-- Navigation never expands, but there are tooltips
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"

View File

@ -12,13 +12,14 @@ public record ChatTemplate(
string SystemPrompt,
string PredefinedUserPrompt,
List<ContentBlock> ExampleConversation,
List<FileAttachment> FileAttachments,
bool AllowProfileUsage,
bool IsEnterpriseConfiguration = false,
Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject
{
private const string USE_NO_CHAT_TEMPLATE_TEXT = "Use no chat template";
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false)
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false)
{
}
@ -34,6 +35,7 @@ public record ChatTemplate(
Id = Guid.Empty.ToString(),
Num = uint.MaxValue,
ExampleConversation = [],
FileAttachments = [],
AllowProfileUsage = true,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
@ -102,7 +104,9 @@ public record ChatTemplate(
var allowProfileUsage = false;
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
allowProfileUsage = allow;
var fileAttachments = ParseFileAttachments(idx, table);
template = new ChatTemplate
{
Num = 0,
@ -111,6 +115,7 @@ public record ChatTemplate(
SystemPrompt = systemPrompt,
PredefinedUserPrompt = predefinedUserPrompt,
ExampleConversation = ParseExampleConversation(idx, table),
FileAttachments = fileAttachments,
AllowProfileUsage = allowProfileUsage,
IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = configPluginId,
@ -165,4 +170,26 @@ public record ChatTemplate(
return exampleConversation;
}
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table)
{
var fileAttachments = new List<FileAttachment>();
if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead<LuaTable>(out var fileAttTable))
return fileAttachments;
var numAttachments = fileAttTable.ArrayLength;
for (var attachmentNum = 1; attachmentNum <= numAttachments; attachmentNum++)
{
var attachmentValue = fileAttTable[attachmentNum];
if (!attachmentValue.TryRead<string>(out var filePath))
{
LOGGER.LogWarning("The FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} is not a valid string.", attachmentNum, idx);
continue;
}
fileAttachments.Add(FileAttachment.FromPath(filePath));
}
return fileAttachments;
}
}

View File

@ -32,15 +32,16 @@ public static class FileExtensionValidation
/// </summary>
ATTACHING_CONTENT,
}
/// <summary>
/// Validates the file extension and sends appropriate MessageBus notifications when invalid.
/// </summary>
/// <param name="useCae">The validation use case.</param>
/// <param name="filePath">The file path to validate.</param>
/// <param name="validateMediaFileTypes">Whether to validate media file types against provider capabilities.</param>
/// <param name="provider">The selected provider.</param>
/// <returns>True if valid, false if invalid (error/warning already sent via MessageBus).</returns>
public static async Task<bool> IsExtensionValidWithNotifyAsync(UseCase useCae, string filePath, Settings.Provider? provider = null)
public static async Task<bool> IsExtensionValidWithNotifyAsync(UseCase useCae, string filePath, bool validateMediaFileTypes = true, Settings.Provider? provider = null)
{
var ext = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant();
if(FileTypeFilter.Executables.FilterExtensions.Contains(ext))
@ -63,6 +64,10 @@ public static class FileExtensionValidation
TB("Images are not supported at this place")));
return false;
// In this use case, we don't validate the provider capabilities:
case UseCase.ATTACHING_CONTENT when !validateMediaFileTypes:
return true;
// In this use case, we can check the provider capabilities:
case UseCase.ATTACHING_CONTENT when capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) ||
capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT):

View File

@ -1 +0,0 @@
# v0.10.1, build 231 (2026-01-xx xx:xx UTC)

View File

@ -0,0 +1,3 @@
# v26.1.1, build 231 (2026-01-xx xx:xx UTC)
- Added the option to attach files, including images, to chat templates. You can also define templates with file attachments through a configuration plugin. These file attachments arent copied—theyre re-read every time. That means the AI will pick up any updates you make to those files.
- Improved the app versioning. Starting in 2026, each version number includes the year, followed by the month. The last digit shows the release number for that month. For example, version `26.1.1` is the first release in January 2026.