Private
Public Access
1
0

Use editable default values with {placeholders} for AI prompts

Instead of showing defaults as greyed-out placeholder text, populate
textareas with the actual default prompt text so users can directly
edit it. Add {categories}, {examples}, and {transactions} placeholders
so users control where dynamic data is inserted in the prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 20:26:07 -05:00
parent e742b0fbe7
commit d49ad4aca7
2 changed files with 50 additions and 22 deletions
+15 -11
View File
@@ -470,8 +470,10 @@
<v-card-title>Prompt Templates</v-card-title> <v-card-title>Prompt Templates</v-card-title>
<v-card-text> <v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4"> <p class="text-body-2 text-medium-emphasis mb-4">
Customize the prompts sent to the AI model. Leave empty to use built-in defaults. Customize the prompts sent to the AI model. Use placeholders to control where dynamic data is inserted:
The category list is always appended to the system prompt, and the transaction list is always appended to the user prompt. <strong>{categories}</strong> = category list,
<strong>{examples}</strong> = recent categorized transactions,
<strong>{transactions}</strong> = uncategorized transactions to classify.
</p> </p>
<v-textarea <v-textarea
@@ -479,8 +481,6 @@
label="System Prompt" label="System Prompt"
variant="outlined" variant="outlined"
rows="8" rows="8"
:placeholder="defaultSystemPrompt"
persistent-placeholder
class="mb-4" class="mb-4"
/> />
@@ -488,9 +488,7 @@
v-model="aiSettings.userPromptTemplate" v-model="aiSettings.userPromptTemplate"
label="User Prompt Template" label="User Prompt Template"
variant="outlined" variant="outlined"
rows="3" rows="5"
:placeholder="defaultUserPromptTemplate"
persistent-placeholder
class="mb-4" class="mb-4"
/> />
@@ -867,21 +865,27 @@ Rules:
- "confidence" must be a number between 0 and 1 - "confidence" must be a number between 0 and 1
- Include one entry for every transaction - Include one entry for every transaction
Available categories:` Available categories:
{categories}`
const defaultUserPromptTemplate = 'Classify these uncategorized transactions:' const defaultUserPromptTemplate = `{examples}Classify these uncategorized transactions:
{transactions}`
function resetPromptsToDefaults() { function resetPromptsToDefaults() {
aiSettings.systemPrompt = null aiSettings.systemPrompt = defaultSystemPrompt
aiSettings.userPromptTemplate = null aiSettings.userPromptTemplate = defaultUserPromptTemplate
} }
async function loadAiSettings() { async function loadAiSettings() {
try { try {
const { data } = await aiCategorizationApi.getSettings() const { data } = await aiCategorizationApi.getSettings()
Object.assign(aiSettings, data) Object.assign(aiSettings, data)
aiSettings.systemPrompt = aiSettings.systemPrompt || defaultSystemPrompt
aiSettings.userPromptTemplate = aiSettings.userPromptTemplate || defaultUserPromptTemplate
} catch { } catch {
// Not configured yet — defaults are fine // Not configured yet — defaults are fine
aiSettings.systemPrompt = defaultSystemPrompt
aiSettings.userPromptTemplate = defaultUserPromptTemplate
} }
} }
@@ -35,9 +35,14 @@ public class AiCategorizationService : IAiCategorizationService
- Include one entry for every transaction - Include one entry for every transaction
Available categories: Available categories:
{categories}
"""; """;
internal const string DefaultUserPromptTemplate = "Classify these uncategorized transactions:"; internal const string DefaultUserPromptTemplate =
"""
{examples}Classify these uncategorized transactions:
{transactions}
""";
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
@@ -233,45 +238,64 @@ public class AiCategorizationService : IAiCategorizationService
private static string BuildSystemMessage(AiCategorizationSettings settings, List<Category> categories) private static string BuildSystemMessage(AiCategorizationSettings settings, List<Category> categories)
{ {
var sb = new StringBuilder(); var prompt = settings.SystemPrompt ?? DefaultSystemPrompt;
sb.AppendLine(settings.SystemPrompt ?? DefaultSystemPrompt);
var sb = new StringBuilder();
foreach (var category in categories) foreach (var category in categories)
{ {
var parentInfo = category.Parent != null ? $" (under \"{category.Parent.Name}\")" : ""; var parentInfo = category.Parent != null ? $" (under \"{category.Parent.Name}\")" : "";
sb.AppendLine($"- \"{category.Name}\"{parentInfo}"); sb.AppendLine($"- \"{category.Name}\"{parentInfo}");
} }
var categoryList = sb.ToString();
return sb.ToString(); return prompt.Contains("{categories}")
? prompt.Replace("{categories}", categoryList)
: prompt + "\n" + categoryList;
} }
private static string BuildUserMessage(AiCategorizationSettings settings, List<Transaction> recentCategorized, List<Transaction> batch) private static string BuildUserMessage(AiCategorizationSettings settings, List<Transaction> recentCategorized, List<Transaction> batch)
{ {
var sb = new StringBuilder(); var prompt = settings.UserPromptTemplate ?? DefaultUserPromptTemplate;
// Build examples block
var examplesSb = new StringBuilder();
if (recentCategorized.Count > 0) if (recentCategorized.Count > 0)
{ {
sb.AppendLine("Recent categorized transactions (for reference):"); examplesSb.AppendLine("Recent categorized transactions (for reference):");
for (int i = 0; i < recentCategorized.Count; i++) for (int i = 0; i < recentCategorized.Count; i++)
{ {
var t = recentCategorized[i]; var t = recentCategorized[i];
var categoryName = t.Category?.Parent != null var categoryName = t.Category?.Parent != null
? $"{t.Category.Parent.Name} > {t.Category.Name}" ? $"{t.Category.Parent.Name} > {t.Category.Name}"
: t.Category?.Name ?? "Unknown"; : t.Category?.Name ?? "Unknown";
sb.AppendLine($"{i + 1}. \"{t.PayeeName}\" (${t.Amount:F2}) -> \"{categoryName}\""); examplesSb.AppendLine($"{i + 1}. \"{t.PayeeName}\" (${t.Amount:F2}) -> \"{categoryName}\"");
} }
sb.AppendLine(); examplesSb.AppendLine();
} }
var examples = examplesSb.ToString();
sb.AppendLine(settings.UserPromptTemplate ?? DefaultUserPromptTemplate); // Build transactions block
var txnSb = new StringBuilder();
for (int i = 0; i < batch.Count; i++) for (int i = 0; i < batch.Count; i++)
{ {
var t = batch[i]; var t = batch[i];
var type = t.Amount >= 0 ? "credit" : "debit"; var type = t.Amount >= 0 ? "credit" : "debit";
sb.AppendLine($"{i}. \"{t.PayeeName}\" (${t.Amount:F2}, {type}, {t.Date:yyyy-MM-dd}, memo: \"{t.Memo}\")"); txnSb.AppendLine($"{i}. \"{t.PayeeName}\" (${t.Amount:F2}, {type}, {t.Date:yyyy-MM-dd}, memo: \"{t.Memo}\")");
} }
var transactions = txnSb.ToString();
return sb.ToString(); var result = prompt;
if (result.Contains("{examples}"))
result = result.Replace("{examples}", examples);
else
result = examples + result;
if (result.Contains("{transactions}"))
result = result.Replace("{transactions}", transactions);
else
result = result + "\n" + transactions;
return result;
} }
private List<TransactionClassification> ParseClassifications( private List<TransactionClassification> ParseClassifications(