plasalid 0.3.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -43
- package/dist/accounts/taxonomy.d.ts +1 -1
- package/dist/accounts/taxonomy.js +2 -2
- package/dist/ai/agent.d.ts +19 -5
- package/dist/ai/agent.js +26 -6
- package/dist/ai/memory.d.ts +14 -5
- package/dist/ai/memory.js +12 -0
- package/dist/ai/personas.d.ts +11 -0
- package/dist/ai/personas.js +193 -0
- package/dist/ai/prompt-sections.d.ts +49 -0
- package/dist/ai/prompt-sections.js +107 -0
- package/dist/ai/system-prompt.d.ts +14 -3
- package/dist/ai/system-prompt.js +59 -165
- package/dist/ai/thinking.js +1 -1
- package/dist/ai/tools/common.js +2 -5
- package/dist/ai/tools/index.js +32 -7
- package/dist/ai/tools/ingest.d.ts +3 -1
- package/dist/ai/tools/ingest.js +372 -124
- package/dist/ai/tools/merchants.d.ts +2 -0
- package/dist/ai/tools/merchants.js +117 -0
- package/dist/ai/tools/read.js +57 -24
- package/dist/ai/tools/record.d.ts +2 -0
- package/dist/ai/tools/record.js +188 -0
- package/dist/ai/tools/review.d.ts +2 -0
- package/dist/ai/tools/review.js +359 -0
- package/dist/ai/tools/scan.js +5 -3
- package/dist/ai/tools/types.d.ts +33 -4
- package/dist/cli/commands/accounts.js +33 -25
- package/dist/cli/commands/record.d.ts +4 -0
- package/dist/cli/commands/record.js +119 -0
- package/dist/cli/commands/revert.js +1 -1
- package/dist/cli/commands/review.d.ts +2 -0
- package/dist/cli/commands/review.js +15 -0
- package/dist/cli/commands/scan.d.ts +4 -2
- package/dist/cli/commands/scan.js +143 -19
- package/dist/cli/commands/status.js +6 -9
- package/dist/cli/commands/transactions.js +36 -41
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.js +7 -2
- package/dist/cli/index.js +28 -13
- package/dist/cli/ink/scan_dashboard.d.ts +38 -0
- package/dist/cli/ink/scan_dashboard.js +62 -0
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js +2 -8
- package/dist/cli/ux.d.ts +2 -1
- package/dist/cli/ux.js +36 -2
- package/dist/currency.d.ts +3 -0
- package/dist/currency.js +12 -1
- package/dist/db/queries/account_balance.d.ts +84 -4
- package/dist/db/queries/account_balance.js +239 -20
- package/dist/db/queries/action_log.d.ts +29 -0
- package/dist/db/queries/action_log.js +27 -0
- package/dist/db/queries/concerns.d.ts +50 -0
- package/dist/db/queries/concerns.js +91 -0
- package/dist/db/queries/journal.d.ts +75 -8
- package/dist/db/queries/journal.js +131 -19
- package/dist/db/queries/merchants.d.ts +42 -0
- package/dist/db/queries/merchants.js +120 -0
- package/dist/db/queries/recurrences.d.ts +33 -0
- package/dist/db/queries/recurrences.js +128 -0
- package/dist/db/queries/search.d.ts +5 -4
- package/dist/db/queries/search.js +16 -12
- package/dist/db/queries/transactions.d.ts +167 -0
- package/dist/db/queries/transactions.js +320 -0
- package/dist/db/schema.js +74 -9
- package/dist/reviewer/pipeline.d.ts +18 -0
- package/dist/reviewer/pipeline.js +46 -0
- package/dist/reviewer/prompts.d.ts +12 -0
- package/dist/reviewer/prompts.js +22 -0
- package/dist/scanner/account_mutex.d.ts +1 -0
- package/dist/scanner/account_mutex.js +16 -0
- package/dist/scanner/buffer.d.ts +51 -0
- package/dist/scanner/buffer.js +63 -0
- package/dist/scanner/concurrency.d.ts +14 -0
- package/dist/scanner/concurrency.js +31 -0
- package/dist/scanner/decrypt_queue.d.ts +57 -0
- package/dist/scanner/decrypt_queue.js +96 -0
- package/dist/scanner/pipeline.d.ts +47 -18
- package/dist/scanner/pipeline.js +247 -97
- package/dist/scanner/prompts.js +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getMemories } from "./memory.js";
|
|
2
|
+
import { getAccountBalances } from "../db/queries/account_balance.js";
|
|
3
|
+
import { stripControls } from "./sanitize.js";
|
|
4
|
+
/**
|
|
5
|
+
* Small, single-purpose renderers that produce one Markdown-ish section each.
|
|
6
|
+
* Builders compose them; each helper either returns a string or null (omit).
|
|
7
|
+
*
|
|
8
|
+
* Style:
|
|
9
|
+
* - No accumulation (`let prompt = …; prompt += …`).
|
|
10
|
+
* - No per-call branching the caller can't see — section options stay tiny.
|
|
11
|
+
* - String building stays in the helper; the builder only chooses *which*
|
|
12
|
+
* helpers to call and *in what order*.
|
|
13
|
+
*/
|
|
14
|
+
/** Date headers */
|
|
15
|
+
/** Long-form date for chat ("Today is Friday, March 5, 2026."). */
|
|
16
|
+
export function renderTodayHuman() {
|
|
17
|
+
return `Today is ${new Date().toLocaleDateString("en-GB", {
|
|
18
|
+
weekday: "long",
|
|
19
|
+
month: "long",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
year: "numeric",
|
|
22
|
+
})}.`;
|
|
23
|
+
}
|
|
24
|
+
/** ISO date for scan/review ("Today is 2026-03-05."). */
|
|
25
|
+
export function renderTodayIso() {
|
|
26
|
+
return `Today is ${new Date().toISOString().slice(0, 10)}.`;
|
|
27
|
+
}
|
|
28
|
+
export function renderChartOfAccounts(db, opts) {
|
|
29
|
+
const balances = getAccountBalances(db);
|
|
30
|
+
if (balances.length === 0) {
|
|
31
|
+
const empty = opts.emptyState === "scan"
|
|
32
|
+
? "(empty — you may need to create accounts; remember to pass parent_id under one of asset/liability/income/expense/equity)"
|
|
33
|
+
: "(empty)";
|
|
34
|
+
return `## Current chart of accounts\n${empty}`;
|
|
35
|
+
}
|
|
36
|
+
const rows = renderHierarchical(balances, opts.withBalance);
|
|
37
|
+
return `## Current chart of accounts\n${rows.join("\n")}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Chat's chart section has a different empty-state shape — it replaces the
|
|
41
|
+
* whole "## Current chart of accounts" header with a user-facing call to
|
|
42
|
+
* action that mentions the user by name. Worth its own helper instead of
|
|
43
|
+
* branching the generic one.
|
|
44
|
+
*/
|
|
45
|
+
export function renderChatChartOrEmpty(db, name) {
|
|
46
|
+
const balances = getAccountBalances(db);
|
|
47
|
+
if (balances.length === 0) {
|
|
48
|
+
return `No accounts have been scanned yet. ${name} should drop files into ~/.plasalid/data/ and run \`plasalid scan\`.`;
|
|
49
|
+
}
|
|
50
|
+
const rows = renderHierarchical(balances, true);
|
|
51
|
+
return `## Accounts on file\n${rows.join("\n")}`;
|
|
52
|
+
}
|
|
53
|
+
function renderHierarchical(balances, withBalance) {
|
|
54
|
+
const byId = new Map(balances.map(b => [b.id, b]));
|
|
55
|
+
const depthCache = new Map();
|
|
56
|
+
const depth = (id) => {
|
|
57
|
+
if (depthCache.has(id))
|
|
58
|
+
return depthCache.get(id);
|
|
59
|
+
const node = byId.get(id);
|
|
60
|
+
if (!node || !node.parent_id) {
|
|
61
|
+
depthCache.set(id, 0);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const d = depth(node.parent_id) + 1;
|
|
65
|
+
depthCache.set(id, d);
|
|
66
|
+
return d;
|
|
67
|
+
};
|
|
68
|
+
return balances.map(a => formatAccountRow(a, withBalance, depth(a.id)));
|
|
69
|
+
}
|
|
70
|
+
export function renderMemories(db, opts) {
|
|
71
|
+
const all = getMemories(db);
|
|
72
|
+
const filtered = opts.filterCategories
|
|
73
|
+
? all.filter(m => opts.filterCategories.includes(m.category))
|
|
74
|
+
: all;
|
|
75
|
+
if (filtered.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const lines = filtered.map(m => formatMemoryLine(m, opts.showCategory));
|
|
78
|
+
return `## ${opts.header}\n${lines.join("\n")}`;
|
|
79
|
+
}
|
|
80
|
+
export function renderScope(opts) {
|
|
81
|
+
return [
|
|
82
|
+
"## Scope",
|
|
83
|
+
`- account: ${opts.accountId ?? "all"}`,
|
|
84
|
+
`- from: ${opts.from ?? "all time"}`,
|
|
85
|
+
`- to: ${opts.to ?? "now"}`,
|
|
86
|
+
`- dry run: ${opts.dryRun
|
|
87
|
+
? "yes — write tools will not mutate the DB"
|
|
88
|
+
: "no — write tools will mutate the DB after confirmation"}`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
/** Chat user context */
|
|
92
|
+
export function renderUserContext(name, contextMd) {
|
|
93
|
+
const body = contextMd ?? `(No personal context on file yet. ${name} can edit ~/.plasalid/context.md to add family, income, or other facts.)`;
|
|
94
|
+
return `## About ${name}\n${body}`;
|
|
95
|
+
}
|
|
96
|
+
/** Internal formatters */
|
|
97
|
+
function formatAccountRow(a, withBalance, depth = 0) {
|
|
98
|
+
const indent = " ".repeat(depth);
|
|
99
|
+
const subtype = a.subtype ? `/${a.subtype}` : "";
|
|
100
|
+
const base = `- ${indent}${a.id} | ${a.name} | ${a.type}${subtype}`;
|
|
101
|
+
return withBalance ? `${base} | balance ${a.balance.toFixed(2)} ${a.currency}` : base;
|
|
102
|
+
}
|
|
103
|
+
function formatMemoryLine(m, showCategory) {
|
|
104
|
+
return showCategory
|
|
105
|
+
? `- [${m.category}] ${stripControls(m.content)}`
|
|
106
|
+
: `- ${stripControls(m.content)}`;
|
|
107
|
+
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type Database from "libsql";
|
|
2
|
-
export declare function buildChatSystemPrompt(db: Database.Database): string;
|
|
3
2
|
export interface ScanPromptOptions {
|
|
4
3
|
fileName: string;
|
|
5
4
|
}
|
|
6
|
-
export interface
|
|
5
|
+
export interface ReviewPromptOptions {
|
|
7
6
|
accountId?: string;
|
|
8
7
|
from?: string;
|
|
9
8
|
to?: string;
|
|
10
9
|
dryRun: boolean;
|
|
11
10
|
}
|
|
12
|
-
export
|
|
11
|
+
export interface RecordPromptOptions {
|
|
12
|
+
utterance: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Builders
|
|
16
|
+
*
|
|
17
|
+
* Each builder is a list of sections in render order. No accumulation, no
|
|
18
|
+
* inline string assembly. To edit a section, change the helper; to reorder,
|
|
19
|
+
* shuffle the array.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildChatSystemPrompt(db: Database.Database): string;
|
|
22
|
+
export declare function buildReviewSystemPrompt(db: Database.Database, opts: ReviewPromptOptions): string;
|
|
23
|
+
export declare function buildRecordSystemPrompt(db: Database.Database, opts: RecordPromptOptions): string;
|
|
13
24
|
export declare function buildScanSystemPrompt(db: Database.Database, opts: ScanPromptOptions): string;
|
package/dist/ai/system-prompt.js
CHANGED
|
@@ -1,174 +1,68 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
-
import { getMemories } from "./memory.js";
|
|
3
2
|
import { readContext } from "./context.js";
|
|
4
|
-
import {
|
|
5
|
-
import { getAccountBalances } from "../db/queries/account_balance.js";
|
|
3
|
+
import { chatPersona, SCAN_PERSONA, REVIEW_PERSONA, RECORD_PERSONA } from "./personas.js";
|
|
6
4
|
import { getThaiTaxonomyHint } from "../accounts/taxonomy.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Be concise. 2-4 sentences for simple questions. Skip preamble like "Great question!" or "Let me look that up." Just answer.
|
|
16
|
-
- Warm but direct. Celebrate wins genuinely. Flag problems without sugarcoating.
|
|
17
|
-
|
|
18
|
-
## How you work
|
|
19
|
-
1. Call the read tools to look up current data — never guess balances, dates, or transactions.
|
|
20
|
-
2. Connect the dots. Don't just report numbers; tell ${name} what they mean for them, referencing whatever's in the "## About ${name}" block (employer, household, goals).
|
|
21
|
-
3. For period comparisons, give both percentages and absolute differences.
|
|
22
|
-
4. End with a next step when it helps. A good partner always has a suggestion.
|
|
23
|
-
5. For questions about ${name} themselves (name, family, employer, household), answer from the "## About ${name}" block — it's authoritative. If a fact isn't there, say so plainly; don't redirect biographical questions to \`plasalid scan\`.
|
|
24
|
-
6. Default currency is THB unless an account is explicitly in another. Don't mix currencies.
|
|
25
|
-
|
|
26
|
-
## Output rules
|
|
27
|
-
- Reply in the dominant language of ${name}'s message; default to English when mixed or ambiguous.
|
|
28
|
-
- Markdown sparingly: **bold** for figures, simple bullets, no code blocks.
|
|
29
|
-
- No emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, arrows-as-emoji). Use plain words.
|
|
30
|
-
- No tables — no markdown \`|\` tables, no ASCII grids, no pipe-delimited rows. The TUI breaks them. Use prose, dashes, or numbered lists.
|
|
31
|
-
- If the data needed to answer isn't in the database, say so plainly and suggest \`plasalid scan\` when relevant.`;
|
|
32
|
-
}
|
|
33
|
-
const SCAN_PERSONA = `You are Plasalid's scanner. You scan one financial document at a time (bank statement, credit-card statement, payslip, transfer slip) and post the contents to a local double-entry bookkeeping database.
|
|
34
|
-
|
|
35
|
-
Rules:
|
|
36
|
-
1. Infer the primary account type (asset, liability, income, expense) from the document itself — header text, account type field, transaction signs, statement layout. Do not rely on the filename or directory.
|
|
37
|
-
2. Every transaction must become a balanced \`record_journal_entry\` call. Total debits must equal total credits.
|
|
38
|
-
3. Account-type conventions:
|
|
39
|
-
- **Asset** (e.g. bank, cash): DEBIT increases, CREDIT decreases.
|
|
40
|
-
- **Liability** (e.g. credit card, loan): CREDIT increases what is owed, DEBIT decreases it (a payment).
|
|
41
|
-
- **Income**: CREDIT increases.
|
|
42
|
-
- **Expense**: DEBIT increases.
|
|
43
|
-
4. Dates: convert Buddhist Era → Gregorian by subtracting 543 from the year. Store as YYYY-MM-DD.
|
|
44
|
-
5. Default currency is THB. Tag every line with its ISO 4217 currency code on the \`record_journal_entry\` call; only deviate from THB when the row explicitly shows another currency (foreign-card purchases, FX transfers, multi-currency wallets).
|
|
45
|
-
6. Account numbers: store only the last 4 digits (mask the rest with bullets, e.g. \`••1234\`). Never persist the full account number.
|
|
46
|
-
7. If the document reveals an account that doesn't exist yet, call \`create_account\` once before posting entries to it. Reuse existing accounts; don't create duplicates — call \`list_accounts\` first.
|
|
47
|
-
8. Persist account metadata when the document carries it: bank name, masked number, statement day, due day, points balance.
|
|
48
|
-
9. If you are unsure about a row (ambiguous category, missing date, unclear sign), call \`ask_user\` instead of guessing.
|
|
49
|
-
10. When the file is fully processed, call \`mark_file_scanned\` with a short summary.
|
|
50
|
-
|
|
51
|
-
Common Thai statement patterns to expect:
|
|
52
|
-
- Bank statements list incoming, outgoing with running balance.
|
|
53
|
-
- Credit-card statements list a statement balance, minimum payment, due date, statement-cut date, and per-transaction rows.
|
|
54
|
-
- Payslips list gross salary, tax, social-security, and net pay.
|
|
55
|
-
- Transfer slips (PromptPay / mobile banking) show source account, destination account, amount, and a reference number.
|
|
56
|
-
|
|
57
|
-
Pick a stable account id format: \`<type>:<bank>-<subtype>-<last4>\`, e.g. \`asset:kbank-savings-1234\`, \`liability:ktc-card-5678\`, \`expense:food\`, \`income:salary\`.
|
|
58
|
-
|
|
59
|
-
How to phrase ask_user when you're unsure about a row:
|
|
60
|
-
- Frame each question as a complete sentence with enough context: include the date, the amount (formatted as ฿N,NNN.NN), and the row's description.
|
|
61
|
-
- Never reference accounts by their internal id (\`a:…\`). Use the human account name (e.g. "KBank Savings ••8745") in the question.
|
|
62
|
-
- Always include "Skip — leave as is" as one of the options.
|
|
63
|
-
|
|
64
|
-
Output formatting: use plain ASCII numbers (\`1.\`, \`2.\`, \`3.\`) for any lists. Never use Unicode circled digits (①②③). Never use emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, etc.) — use plain words.`;
|
|
5
|
+
import { renderChartOfAccounts, renderChatChartOrEmpty, renderMemories, renderScope, renderTodayHuman, renderTodayIso, renderUserContext, } from "./prompt-sections.js";
|
|
6
|
+
/**
|
|
7
|
+
* Builders
|
|
8
|
+
*
|
|
9
|
+
* Each builder is a list of sections in render order. No accumulation, no
|
|
10
|
+
* inline string assembly. To edit a section, change the helper; to reorder,
|
|
11
|
+
* shuffle the array.
|
|
12
|
+
*/
|
|
65
13
|
export function buildChatSystemPrompt(db) {
|
|
66
|
-
const memories = getMemories(db);
|
|
67
|
-
const context = readContext();
|
|
68
14
|
const name = config.userName;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (memories.length > 0) {
|
|
92
|
-
prompt += `\n\n## Things to remember about ${name}\n`;
|
|
93
|
-
prompt += memories
|
|
94
|
-
.map((m) => `- [${m.category}] ${stripControls(m.content)}`)
|
|
95
|
-
.join("\n");
|
|
96
|
-
}
|
|
97
|
-
return prompt;
|
|
15
|
+
return joinSections([
|
|
16
|
+
chatPersona(name),
|
|
17
|
+
renderTodayHuman(),
|
|
18
|
+
renderUserContext(name, readContext()),
|
|
19
|
+
renderChatChartOrEmpty(db, name),
|
|
20
|
+
renderMemories(db, {
|
|
21
|
+
header: `Things to remember about ${name}`,
|
|
22
|
+
showCategory: true,
|
|
23
|
+
}),
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
export function buildReviewSystemPrompt(db, opts) {
|
|
27
|
+
return joinSections([
|
|
28
|
+
REVIEW_PERSONA,
|
|
29
|
+
renderTodayIso(),
|
|
30
|
+
renderChartOfAccounts(db, { withBalance: true, emptyState: "review" }),
|
|
31
|
+
renderScope(opts),
|
|
32
|
+
renderMemories(db, {
|
|
33
|
+
header: "Rules you've already learned (apply directly; do not re-ask the user)",
|
|
34
|
+
showCategory: true,
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
98
37
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- Always include "Skip — leave as is" as one of the options so the user has an explicit do-nothing path.
|
|
112
|
-
- Example:
|
|
113
|
-
Avoid: \`prompt: "Merge these?"\`
|
|
114
|
-
Prefer: \`prompt: "Two ฿350 lunch entries dated 2026-01-15 and 2026-01-17, both posted to KBank Savings ••8745 → Food. Merge into one?"\`, \`options: ["Yes — delete the 01-17 entry", "Yes — delete the 01-15 entry", "Skip — leave as is"]\`
|
|
115
|
-
|
|
116
|
-
How to write the mark_reconcile_done summary:
|
|
117
|
-
- Write a short user-facing report of what actually changed, in plain language. The user does not know what the internal detectors are.
|
|
118
|
-
- Report counts and actions, never internal grouping. Examples:
|
|
119
|
-
- "Merged 3 duplicate transactions. Renamed 1 account. No other changes."
|
|
120
|
-
- "No duplicates found. Skipped 2 unused accounts on your instruction."
|
|
121
|
-
- Never reference internal detector names (\`find_duplicate_entries\`, "Group 1", "Group 1-5", "candidate group N"). Those are tool internals.
|
|
122
|
-
- Keep the summary to one to three sentences. Lead with what was applied; tail with what was skipped or deferred.
|
|
123
|
-
|
|
124
|
-
Output formatting:
|
|
125
|
-
- Use plain ASCII numbers (\`1.\`, \`2.\`, \`3.\`) for any lists or option numbering you generate. Never use Unicode circled digits (①②③).
|
|
126
|
-
- Never use emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, etc.) — use plain words.
|
|
127
|
-
- Always reply in English.
|
|
128
|
-
- Be brief in prose; the user is reviewing in real time and wants to confirm fast.`;
|
|
129
|
-
export function buildReconcileSystemPrompt(db, opts) {
|
|
130
|
-
const balances = getAccountBalances(db);
|
|
131
|
-
const memories = getMemories(db);
|
|
132
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
133
|
-
let prompt = `${RECONCILE_PERSONA}\n\nToday is ${today}.\n\n## Current chart of accounts\n`;
|
|
134
|
-
prompt +=
|
|
135
|
-
balances.length === 0
|
|
136
|
-
? "(empty)"
|
|
137
|
-
: balances
|
|
138
|
-
.map((a) => `- ${a.id} | ${a.name} | ${a.type}${a.subtype ? `/${a.subtype}` : ""} | balance ${a.balance.toFixed(2)} ${a.currency}`)
|
|
139
|
-
.join("\n");
|
|
140
|
-
prompt += `\n\n## Scope`;
|
|
141
|
-
prompt += `\n- account: ${opts.accountId ?? "all"}`;
|
|
142
|
-
prompt += `\n- from: ${opts.from ?? "all time"}`;
|
|
143
|
-
prompt += `\n- to: ${opts.to ?? "now"}`;
|
|
144
|
-
prompt += `\n- dry run: ${opts.dryRun ? "yes — write tools will not mutate the DB" : "no — write tools will mutate the DB after confirmation"}`;
|
|
145
|
-
if (memories.length > 0) {
|
|
146
|
-
prompt += `\n\n## Saved memories (apply where relevant)\n`;
|
|
147
|
-
prompt += memories
|
|
148
|
-
.map((m) => `- [${m.category}] ${stripControls(m.content)}`)
|
|
149
|
-
.join("\n");
|
|
150
|
-
}
|
|
151
|
-
return prompt;
|
|
38
|
+
export function buildRecordSystemPrompt(db, opts) {
|
|
39
|
+
return joinSections([
|
|
40
|
+
RECORD_PERSONA,
|
|
41
|
+
renderTodayIso(),
|
|
42
|
+
renderChartOfAccounts(db, { withBalance: true, emptyState: "scan" }),
|
|
43
|
+
`## What the user said\n> ${opts.utterance.replace(/\n/g, " ")}`,
|
|
44
|
+
renderMemories(db, {
|
|
45
|
+
header: "Rules you've already learned (apply silently)",
|
|
46
|
+
filterCategories: ["scanning_hint", "general", "preference"],
|
|
47
|
+
showCategory: false,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
152
50
|
}
|
|
153
51
|
export function buildScanSystemPrompt(db, opts) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.map((m) => `- ${stripControls(m.content)}`)
|
|
171
|
-
.join("\n");
|
|
172
|
-
}
|
|
173
|
-
return prompt;
|
|
52
|
+
return joinSections([
|
|
53
|
+
SCAN_PERSONA,
|
|
54
|
+
renderTodayIso(),
|
|
55
|
+
renderChartOfAccounts(db, { withBalance: false, emptyState: "scan" }),
|
|
56
|
+
`## File context\nFile: ${opts.fileName}`,
|
|
57
|
+
`## Taxonomy hints\n${getThaiTaxonomyHint()}`,
|
|
58
|
+
renderMemories(db, {
|
|
59
|
+
header: "Rules you've already learned (apply silently before raising a concern)",
|
|
60
|
+
filterCategories: ["scanning_hint", "general"],
|
|
61
|
+
showCategory: false,
|
|
62
|
+
}),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
/** Drop null/empty sections, join the rest with a blank line. */
|
|
66
|
+
function joinSections(sections) {
|
|
67
|
+
return sections.filter((s) => !!s).join("\n\n");
|
|
174
68
|
}
|
package/dist/ai/thinking.js
CHANGED
package/dist/ai/tools/common.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { saveMemory, getMemories } from "../memory.js";
|
|
2
2
|
import { getAccountBalances } from "../../db/queries/account_balance.js";
|
|
3
|
-
import {
|
|
3
|
+
import { formatAmount } from "../../currency.js";
|
|
4
4
|
import { sanitizeForPrompt, sanitizeForPromptCell } from "../sanitize.js";
|
|
5
5
|
import { ACCOUNT_TYPE_DESCRIPTIONS } from "../../accounts/taxonomy.js";
|
|
6
6
|
const ACCOUNT_TYPES = Object.keys(ACCOUNT_TYPE_DESCRIPTIONS);
|
|
7
|
-
function formatTHB(amount) {
|
|
8
|
-
return formatCurrencyAmount(amount, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
9
|
-
}
|
|
10
7
|
const DEFS = [
|
|
11
8
|
{
|
|
12
9
|
name: "list_accounts",
|
|
@@ -60,7 +57,7 @@ async function execute(db, name, input, _ctx) {
|
|
|
60
57
|
if (a.points_balance)
|
|
61
58
|
meta.push(`${a.points_balance} pts`);
|
|
62
59
|
const metaStr = meta.length ? ` [${meta.join(" · ")}]` : "";
|
|
63
|
-
return `${a.id} | ${sanitizeForPromptCell(a.name)} | ${a.type}${a.subtype ? `/${a.subtype}` : ""} | balance ${
|
|
60
|
+
return `${a.id} | ${sanitizeForPromptCell(a.name)} | ${a.type}${a.subtype ? `/${a.subtype}` : ""} | balance ${formatAmount(a.balance)}${metaStr}`;
|
|
64
61
|
})
|
|
65
62
|
.join("\n");
|
|
66
63
|
}
|
package/dist/ai/tools/index.js
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import { commonTools } from "./common.js";
|
|
2
2
|
import { readTools } from "./read.js";
|
|
3
|
-
import {
|
|
3
|
+
import { accountIngestTools, scanConcernTools, reviewIngestTools } from "./ingest.js";
|
|
4
4
|
import { scanTools } from "./scan.js";
|
|
5
|
-
import {
|
|
5
|
+
import { reviewTools } from "./review.js";
|
|
6
|
+
import { recordTools } from "./record.js";
|
|
7
|
+
import { merchantTools } from "./merchants.js";
|
|
6
8
|
/**
|
|
7
9
|
* Profile composition. Each profile is the union of one or more tool modules;
|
|
8
10
|
* the dispatcher iterates every module on each tool call so we never need a
|
|
9
11
|
* central switch.
|
|
12
|
+
*
|
|
13
|
+
* `accountIngestTools` (create_account / update_account_metadata /
|
|
14
|
+
* record_transaction) ships with scan, review, and record — they're the
|
|
15
|
+
* shared write primitives. `scanConcernTools` (note_concern) is scan-only;
|
|
16
|
+
* record uses `clarify` from `recordTools` for transient prompts, review uses
|
|
17
|
+
* `ask_user` from `reviewIngestTools` for resolve-in-place clarifications.
|
|
18
|
+
* `merchantTools` ships with scan, review, and record so any write profile can
|
|
19
|
+
* upsert / look up / re-cache merchants alongside the posting flow.
|
|
10
20
|
*/
|
|
11
21
|
const PROFILES = {
|
|
12
|
-
scan: [commonTools,
|
|
22
|
+
scan: [commonTools, accountIngestTools, scanConcernTools, scanTools, merchantTools],
|
|
13
23
|
chat: [commonTools, readTools],
|
|
14
|
-
|
|
24
|
+
review: [commonTools, readTools, accountIngestTools, reviewIngestTools, reviewTools, merchantTools],
|
|
25
|
+
record: [commonTools, readTools, accountIngestTools, recordTools, merchantTools],
|
|
15
26
|
};
|
|
16
27
|
export function getToolDefinitions(profile) {
|
|
17
28
|
return PROFILES[profile].flatMap(m => m.DEFS);
|
|
18
29
|
}
|
|
19
30
|
export async function executeTool(db, name, input, ctx) {
|
|
20
|
-
for (const mod of [
|
|
31
|
+
for (const mod of [
|
|
32
|
+
commonTools,
|
|
33
|
+
readTools,
|
|
34
|
+
accountIngestTools,
|
|
35
|
+
scanConcernTools,
|
|
36
|
+
reviewIngestTools,
|
|
37
|
+
scanTools,
|
|
38
|
+
reviewTools,
|
|
39
|
+
recordTools,
|
|
40
|
+
merchantTools,
|
|
41
|
+
]) {
|
|
21
42
|
const result = await mod.execute(db, name, input, ctx);
|
|
22
43
|
if (result !== undefined)
|
|
23
44
|
return result;
|
|
@@ -28,7 +49,11 @@ export async function executeTool(db, name, input, ctx) {
|
|
|
28
49
|
export const TOOL_LABELS = {
|
|
29
50
|
...commonTools.LABELS,
|
|
30
51
|
...readTools.LABELS,
|
|
31
|
-
...
|
|
52
|
+
...accountIngestTools.LABELS,
|
|
53
|
+
...scanConcernTools.LABELS,
|
|
54
|
+
...reviewIngestTools.LABELS,
|
|
32
55
|
...scanTools.LABELS,
|
|
33
|
-
...
|
|
56
|
+
...reviewTools.LABELS,
|
|
57
|
+
...recordTools.LABELS,
|
|
58
|
+
...merchantTools.LABELS,
|
|
34
59
|
};
|