- Customize the prompts sent to the AI model. Leave empty to use built-in defaults.
- The category list is always appended to the system prompt, and the transaction list is always appended to the user prompt.
+ Customize the prompts sent to the AI model. Use placeholders to control where dynamic data is inserted:
+ {categories} = category list,
+ {examples} = recent categorized transactions,
+ {transactions} = uncategorized transactions to classify.
@@ -488,9 +488,7 @@
v-model="aiSettings.userPromptTemplate"
label="User Prompt Template"
variant="outlined"
- rows="3"
- :placeholder="defaultUserPromptTemplate"
- persistent-placeholder
+ rows="5"
class="mb-4"
/>
@@ -867,21 +865,27 @@ Rules:
- "confidence" must be a number between 0 and 1
- 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() {
- aiSettings.systemPrompt = null
- aiSettings.userPromptTemplate = null
+ aiSettings.systemPrompt = defaultSystemPrompt
+ aiSettings.userPromptTemplate = defaultUserPromptTemplate
}
async function loadAiSettings() {
try {
const { data } = await aiCategorizationApi.getSettings()
Object.assign(aiSettings, data)
+ aiSettings.systemPrompt = aiSettings.systemPrompt || defaultSystemPrompt
+ aiSettings.userPromptTemplate = aiSettings.userPromptTemplate || defaultUserPromptTemplate
} catch {
// Not configured yet — defaults are fine
+ aiSettings.systemPrompt = defaultSystemPrompt
+ aiSettings.userPromptTemplate = defaultUserPromptTemplate
}
}
diff --git a/src/Purrse.Api/Services/AiCategorizationService.cs b/src/Purrse.Api/Services/AiCategorizationService.cs
index 9499232..15578cc 100644
--- a/src/Purrse.Api/Services/AiCategorizationService.cs
+++ b/src/Purrse.Api/Services/AiCategorizationService.cs
@@ -35,9 +35,14 @@ public class AiCategorizationService : IAiCategorizationService
- Include one entry for every transaction
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()
{
@@ -233,45 +238,64 @@ public class AiCategorizationService : IAiCategorizationService
private static string BuildSystemMessage(AiCategorizationSettings settings, List categories)
{
- var sb = new StringBuilder();
- sb.AppendLine(settings.SystemPrompt ?? DefaultSystemPrompt);
+ var prompt = settings.SystemPrompt ?? DefaultSystemPrompt;
+ var sb = new StringBuilder();
foreach (var category in categories)
{
var parentInfo = category.Parent != null ? $" (under \"{category.Parent.Name}\")" : "";
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 recentCategorized, List batch)
{
- var sb = new StringBuilder();
+ var prompt = settings.UserPromptTemplate ?? DefaultUserPromptTemplate;
+ // Build examples block
+ var examplesSb = new StringBuilder();
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++)
{
var t = recentCategorized[i];
var categoryName = t.Category?.Parent != null
? $"{t.Category.Parent.Name} > {t.Category.Name}"
: 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++)
{
var t = batch[i];
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 ParseClassifications(