plasalid 0.4.1 → 0.5.1
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 +15 -14
- package/dist/ai/agent.d.ts +15 -2
- package/dist/ai/agent.js +21 -2
- package/dist/ai/memory.d.ts +2 -0
- package/dist/ai/memory.js +2 -2
- package/dist/ai/personas.d.ts +2 -1
- package/dist/ai/personas.js +115 -45
- package/dist/ai/prompt-sections.d.ts +5 -0
- package/dist/ai/prompt-sections.js +26 -8
- package/dist/ai/system-prompt.d.ts +11 -0
- package/dist/ai/system-prompt.js +21 -6
- package/dist/ai/thinking.js +1 -1
- package/dist/ai/tools/common.js +2 -5
- package/dist/ai/tools/index.js +28 -8
- package/dist/ai/tools/ingest.d.ts +2 -1
- package/dist/ai/tools/ingest.js +262 -151
- package/dist/ai/tools/merchants.d.ts +2 -0
- package/dist/ai/tools/merchants.js +117 -0
- package/dist/ai/tools/read.js +31 -29
- package/dist/ai/tools/record.d.ts +2 -0
- package/dist/ai/tools/record.js +188 -0
- package/dist/ai/tools/review.js +77 -80
- package/dist/ai/tools/scan.js +1 -1
- package/dist/ai/tools/types.d.ts +15 -6
- 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/scan.js +15 -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 +19 -7
- package/dist/cli/ink/hooks/useFooterText.js +2 -1
- package/dist/cli/ink/scan_dashboard.d.ts +1 -1
- package/dist/cli/ink/scan_dashboard.js +2 -2
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js +2 -8
- package/dist/currency.d.ts +3 -0
- package/dist/currency.js +12 -1
- package/dist/db/queries/account_balance.d.ts +83 -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 +10 -7
- package/dist/db/queries/concerns.js +20 -16
- package/dist/db/queries/journal.d.ts +1 -0
- package/dist/db/queries/merchants.d.ts +42 -0
- package/dist/db/queries/merchants.js +120 -0
- package/dist/db/queries/recurrences.d.ts +3 -3
- package/dist/db/queries/recurrences.js +32 -34
- 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 +51 -9
- package/dist/reviewer/pipeline.d.ts +4 -4
- package/dist/reviewer/pipeline.js +4 -4
- package/dist/reviewer/prompts.js +4 -4
- package/dist/scanner/buffer.d.ts +24 -21
- package/dist/scanner/buffer.js +18 -18
- package/dist/scanner/pipeline.d.ts +3 -2
- package/dist/scanner/pipeline.js +33 -36
- package/dist/scanner/prompts.js +3 -3
- package/package.json +2 -2
package/dist/ai/tools/ingest.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import { findAccountById } from "../../db/queries/account_balance.js";
|
|
2
|
-
import {
|
|
1
|
+
import { createAccount, updateAccountMetadata, findAccountById, } from "../../db/queries/account_balance.js";
|
|
2
|
+
import { validateTransaction, insertTransactionRows, recordTransaction, } from "../../db/queries/transactions.js";
|
|
3
|
+
import { appendAction } from "../../db/queries/action_log.js";
|
|
3
4
|
import { getConcernTarget, recordConcern, resolveConcern, } from "../../db/queries/concerns.js";
|
|
4
5
|
import { runExclusive as runAccountExclusive } from "../../scanner/account_mutex.js";
|
|
5
6
|
import { sanitizeForPrompt } from "../sanitize.js";
|
|
6
|
-
import { ACCOUNT_TYPE_DESCRIPTIONS
|
|
7
|
+
import { ACCOUNT_TYPE_DESCRIPTIONS } from "../../accounts/taxonomy.js";
|
|
7
8
|
const ACCOUNT_TYPES = Object.keys(ACCOUNT_TYPE_DESCRIPTIONS);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Account + transaction write primitives
|
|
11
|
+
*
|
|
12
|
+
* Shared by scan, review, and record. Each tool branches once on
|
|
13
|
+
* `ctx.correlationId`: when set (record path), the data write and the
|
|
14
|
+
* action_log insert run inside a single transaction so the audit row is
|
|
15
|
+
* atomic with the change. Without it (scan / review), the write goes through
|
|
16
|
+
* the existing path unchanged.
|
|
17
|
+
*/
|
|
18
|
+
const ACCOUNT_DEFS = [
|
|
15
19
|
{
|
|
16
20
|
name: "create_account",
|
|
17
|
-
description: "Create a new account in the chart of accounts
|
|
21
|
+
description: "Create a new account in the chart of accounts. Account ids are colon-paths under one of the five top-level type roots ('asset', 'liability', 'income', 'expense', 'equity'). Examples: 'asset:kbank-savings-1234', 'expense:food', 'expense:food:groceries'. Every non-root account must have a parent_id that already exists and shares its type; create intermediate parents (e.g. 'expense:food') before their leaves. Top-level roots are auto-bootstrapped on first use.",
|
|
18
22
|
input_schema: {
|
|
19
23
|
type: "object",
|
|
20
24
|
properties: {
|
|
21
|
-
id: { type: "string", description: "Stable identifier, lowercase
|
|
22
|
-
name: { type: "string", description: "Human-readable name. e.g. 'KBank Savings ••1234'." },
|
|
23
|
-
type: { type: "string", enum: ACCOUNT_TYPES, description: "Account type." },
|
|
24
|
-
|
|
25
|
+
id: { type: "string", description: "Stable colon-path identifier, lowercase. e.g. 'expense:food:groceries' or 'asset:kbank-savings-1234'." },
|
|
26
|
+
name: { type: "string", description: "Human-readable name. e.g. 'Groceries' or 'KBank Savings ••1234'." },
|
|
27
|
+
type: { type: "string", enum: ACCOUNT_TYPES, description: "Account type. Must match the parent's type." },
|
|
28
|
+
parent_id: {
|
|
29
|
+
type: ["string", "null"],
|
|
30
|
+
description: "Parent account id (the prefix before the final ':' segment). Pass null only when creating one of the five top-level type roots — and then id must equal type. Examples: id='expense:food:groceries' → parent_id='expense:food'. id='expense:food' → parent_id='expense'. id='expense' → parent_id=null.",
|
|
31
|
+
},
|
|
32
|
+
subtype: { type: "string", description: "e.g. 'bank', 'credit_card', 'salary'." },
|
|
25
33
|
bank_name: { type: "string", description: "Thai institution code from the taxonomy (e.g. KBANK, SCB, KTC)." },
|
|
26
34
|
account_number_masked: { type: "string", description: "Last 4 digits only, e.g. '••1234'." },
|
|
27
35
|
currency: { type: "string", description: "ISO 4217 code. Defaults to 'THB'.", default: "THB" },
|
|
@@ -29,12 +37,12 @@ const SCAN_DEFS = [
|
|
|
29
37
|
statement_day: { type: "number", description: "Statement-cut day of month." },
|
|
30
38
|
metadata: { type: "object", description: "Free-form extra fields (e.g. {points_program: 'KTC Forever'})." },
|
|
31
39
|
},
|
|
32
|
-
required: ["id", "name", "type"],
|
|
40
|
+
required: ["id", "name", "type", "parent_id"],
|
|
33
41
|
},
|
|
34
42
|
},
|
|
35
43
|
{
|
|
36
44
|
name: "update_account_metadata",
|
|
37
|
-
description: "Update account metadata (due day, statement day, points balance, masked number, bank).",
|
|
45
|
+
description: "Update account metadata (due day, statement day, points balance, masked number, bank). Use this — not record_transaction — when the user says things like 'set my KTC due day to 20' or 'statement day 28'.",
|
|
38
46
|
input_schema: {
|
|
39
47
|
type: "object",
|
|
40
48
|
properties: {
|
|
@@ -50,68 +58,58 @@ const SCAN_DEFS = [
|
|
|
50
58
|
},
|
|
51
59
|
},
|
|
52
60
|
{
|
|
53
|
-
name: "
|
|
54
|
-
description: "Post
|
|
61
|
+
name: "record_transaction",
|
|
62
|
+
description: "Post one balanced double-entry transaction — the right tool for any real-world event (purchase, payment, transfer, refund, salary, withdrawal). Use adjust_account_balance instead when the user is stating a current balance rather than describing a transaction. The sum of debits MUST equal the sum of credits (within one currency). Convert Buddhist-Era dates by subtracting 543. Each posting carries an ISO 4217 currency code (THB, USD, EUR, …); default to THB. Use the account's currency where set; only deviate when the source row is explicitly in another currency. When the transaction has an external counter-party, attach a `merchant` block — Plasalid dedups merchants and learns a default expense account per merchant so future statements skip re-categorization.",
|
|
55
63
|
input_schema: {
|
|
56
64
|
type: "object",
|
|
57
65
|
properties: {
|
|
58
66
|
date: { type: "string", description: "ISO Gregorian date (YYYY-MM-DD)." },
|
|
59
|
-
description: { type: "string", description: "Short human-readable description
|
|
67
|
+
description: { type: "string", description: "Short human-readable description." },
|
|
60
68
|
source_page: { type: "number", description: "Page number in the source PDF, if known." },
|
|
61
|
-
|
|
69
|
+
raw_descriptor: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "The exact statement line (the raw merchant descriptor) when posting from a PDF — preserved for alias matching and later review. Omit for manual entries and transfers.",
|
|
72
|
+
},
|
|
73
|
+
merchant: {
|
|
74
|
+
type: "object",
|
|
75
|
+
description: "Counter-party block. Omit for transfers between own accounts and pure metadata movements. When set during a scan, Plasalid upserts the merchant by canonical_name and (optionally) records the raw descriptor as an alias for future matches. Set default_account_id to teach the cache when categorization is confident.",
|
|
76
|
+
properties: {
|
|
77
|
+
canonical_name: { type: "string", description: "Normalized merchant name, Title Case. e.g. 'Starbucks', 'Amazon', 'Spotify'." },
|
|
78
|
+
alias: { type: "string", description: "The raw descriptor exactly as it appears on the statement. Plasalid normalizes and stores it so future statements skip the LLM." },
|
|
79
|
+
default_account_id: { type: "string", description: "Optional learned cache: 'this merchant's expense category is X'. Set when categorization is confident." },
|
|
80
|
+
},
|
|
81
|
+
required: ["canonical_name"],
|
|
82
|
+
},
|
|
83
|
+
merchant_id: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Pre-resolved merchant id (from the scanner's alias pre-pass). When set, the merchant block is ignored. The scanner uses this to skip re-categorizing merchants it already knows.",
|
|
86
|
+
},
|
|
87
|
+
postings: {
|
|
62
88
|
type: "array",
|
|
63
|
-
description: "Two or more
|
|
89
|
+
description: "Two or more postings that balance.",
|
|
64
90
|
items: {
|
|
65
91
|
type: "object",
|
|
66
92
|
properties: {
|
|
67
93
|
account_id: { type: "string", description: "Existing account id from list_accounts or create_account." },
|
|
68
|
-
debit: { type: "number", description: "Debit amount in this
|
|
69
|
-
credit: { type: "number", description: "Credit amount in this
|
|
70
|
-
currency: { type: "string", description: "ISO 4217 currency code for this
|
|
71
|
-
memo: { type: "string", description: "Optional per-
|
|
94
|
+
debit: { type: "number", description: "Debit amount in this posting's currency. Use 0 if this posting is a credit." },
|
|
95
|
+
credit: { type: "number", description: "Credit amount in this posting's currency. Use 0 if this posting is a debit." },
|
|
96
|
+
currency: { type: "string", description: "ISO 4217 currency code for this posting (e.g. THB, USD, EUR). Defaults to THB.", default: "THB" },
|
|
97
|
+
memo: { type: "string", description: "Optional per-posting memo." },
|
|
72
98
|
},
|
|
73
99
|
required: ["account_id"],
|
|
74
100
|
},
|
|
75
101
|
},
|
|
76
102
|
},
|
|
77
|
-
required: ["date", "description", "
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: "note_concern",
|
|
82
|
-
description: "Record a clarification request without pausing the run. Use during scan when a row is ambiguous (post your best-guess journal entry first, then call this with the entry's id), when a row is unparseable (skip the entry, call this with no entry_id), or when you have a concern about an account itself (pass account_id). The reviewer picks these up later with the full picture.",
|
|
83
|
-
input_schema: {
|
|
84
|
-
type: "object",
|
|
85
|
-
properties: {
|
|
86
|
-
prompt: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "The question or concern in a complete sentence, with date, ฿-formatted amount, and human account names. Never reference internal ids.",
|
|
89
|
-
},
|
|
90
|
-
options: {
|
|
91
|
-
type: "array",
|
|
92
|
-
description: "Optional list of candidate answers the reviewer can offer the user.",
|
|
93
|
-
items: { type: "string" },
|
|
94
|
-
},
|
|
95
|
-
entry_id: {
|
|
96
|
-
type: "string",
|
|
97
|
-
description: "Id of the journal entry this concern relates to (returned by record_journal_entry). Omit for file-level concerns about an unparseable row.",
|
|
98
|
-
},
|
|
99
|
-
account_id: {
|
|
100
|
-
type: "string",
|
|
101
|
-
description: "Id of the account this concern relates to. Set when the statement's bank name, currency, statement_day, due_day, or other metadata disagrees with the stored account, or when you suspect a new account you're about to create duplicates an existing one. Can be combined with entry_id.",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
required: ["prompt"],
|
|
103
|
+
required: ["date", "description", "postings"],
|
|
105
104
|
},
|
|
106
105
|
},
|
|
107
106
|
];
|
|
108
|
-
const
|
|
107
|
+
const ACCOUNT_LABELS = {
|
|
109
108
|
create_account: "Creating account",
|
|
110
109
|
update_account_metadata: "Updating account metadata",
|
|
111
|
-
|
|
112
|
-
note_concern: "Noting concern",
|
|
110
|
+
record_transaction: "Posting transaction",
|
|
113
111
|
};
|
|
114
|
-
async function
|
|
112
|
+
async function accountExecute(db, name, input, ctx) {
|
|
115
113
|
switch (name) {
|
|
116
114
|
case "create_account": {
|
|
117
115
|
if (ctx?.dryRun)
|
|
@@ -119,20 +117,48 @@ async function scanExecute(db, name, input, ctx) {
|
|
|
119
117
|
if (!ACCOUNT_TYPES.includes(input.type)) {
|
|
120
118
|
return `Invalid type "${input.type}". Allowed: ${ACCOUNT_TYPES.join(", ")}.`;
|
|
121
119
|
}
|
|
122
|
-
const bank = input.bank_name ? String(input.bank_name).toUpperCase() : null;
|
|
123
|
-
// Account writes serialize across concurrent scan agents so the next
|
|
124
|
-
// list_accounts call (from any agent) sees this row.
|
|
125
120
|
return await runAccountExclusive(() => {
|
|
126
121
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
const create = () => {
|
|
123
|
+
createAccount(db, {
|
|
124
|
+
id: input.id,
|
|
125
|
+
name: input.name,
|
|
126
|
+
type: input.type,
|
|
127
|
+
parent_id: input.parent_id ?? null,
|
|
128
|
+
subtype: input.subtype ?? null,
|
|
129
|
+
bank_name: input.bank_name ?? null,
|
|
130
|
+
account_number_masked: input.account_number_masked ?? null,
|
|
131
|
+
currency: input.currency,
|
|
132
|
+
due_day: input.due_day ?? null,
|
|
133
|
+
statement_day: input.statement_day ?? null,
|
|
134
|
+
metadata: input.metadata ?? null,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
if (ctx?.correlationId) {
|
|
138
|
+
const tx = db.transaction(() => {
|
|
139
|
+
create();
|
|
140
|
+
const row = findAccountById(db, input.id);
|
|
141
|
+
appendAction(db, {
|
|
142
|
+
correlation_id: ctx.correlationId,
|
|
143
|
+
command: ctx.command ?? "record",
|
|
144
|
+
user_input: ctx.userInput ?? null,
|
|
145
|
+
action_type: "create_account",
|
|
146
|
+
target_id: input.id,
|
|
147
|
+
payload: { row },
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
tx();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
create();
|
|
154
|
+
}
|
|
129
155
|
return `Account created: ${input.id} (${input.name}, ${input.type}).`;
|
|
130
156
|
}
|
|
131
157
|
catch (err) {
|
|
132
|
-
if (
|
|
158
|
+
if (err.code === "ACCOUNT_EXISTS") {
|
|
133
159
|
return `Account "${input.id}" already exists. Use update_account_metadata to modify it.`;
|
|
134
160
|
}
|
|
135
|
-
|
|
161
|
+
return `Could not create account "${input.id}": ${err.message}`;
|
|
136
162
|
}
|
|
137
163
|
});
|
|
138
164
|
}
|
|
@@ -140,109 +166,199 @@ async function scanExecute(db, name, input, ctx) {
|
|
|
140
166
|
if (ctx?.dryRun)
|
|
141
167
|
return `Would update metadata for ${input.account_id}: ${JSON.stringify(input)}`;
|
|
142
168
|
return await runAccountExclusive(() => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
try {
|
|
170
|
+
let changed = false;
|
|
171
|
+
const apply = () => {
|
|
172
|
+
const result = updateAccountMetadata(db, input.account_id, {
|
|
173
|
+
due_day: input.due_day,
|
|
174
|
+
statement_day: input.statement_day,
|
|
175
|
+
points_balance: input.points_balance,
|
|
176
|
+
account_number_masked: input.account_number_masked,
|
|
177
|
+
bank_name: input.bank_name,
|
|
178
|
+
metadata: input.metadata,
|
|
179
|
+
});
|
|
180
|
+
changed = result.changed;
|
|
181
|
+
return result;
|
|
182
|
+
};
|
|
183
|
+
if (ctx?.correlationId) {
|
|
184
|
+
const tx = db.transaction(() => {
|
|
185
|
+
const result = apply();
|
|
186
|
+
if (!result.changed)
|
|
187
|
+
return;
|
|
188
|
+
appendAction(db, {
|
|
189
|
+
correlation_id: ctx.correlationId,
|
|
190
|
+
command: ctx.command ?? "record",
|
|
191
|
+
user_input: ctx.userInput ?? null,
|
|
192
|
+
action_type: "update_account_metadata",
|
|
193
|
+
target_id: input.account_id,
|
|
194
|
+
payload: { before: result.before, after: result.after },
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
tx();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
apply();
|
|
201
|
+
}
|
|
202
|
+
return changed ? `Updated ${input.account_id}.` : "Nothing to update.";
|
|
167
203
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (String(err.message).includes("not found")) {
|
|
206
|
+
return `Account "${input.account_id}" not found.`;
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
173
209
|
}
|
|
174
|
-
if (updates.length === 0)
|
|
175
|
-
return "Nothing to update.";
|
|
176
|
-
params.push(input.account_id);
|
|
177
|
-
db.prepare(`UPDATE accounts SET ${updates.join(", ")} WHERE id = ?`).run(...params);
|
|
178
|
-
return `Updated ${input.account_id}.`;
|
|
179
210
|
});
|
|
180
211
|
}
|
|
181
|
-
case "
|
|
212
|
+
case "record_transaction": {
|
|
182
213
|
if (!ctx)
|
|
183
|
-
return "
|
|
214
|
+
return "record_transaction is only available inside an agent session.";
|
|
184
215
|
if (ctx.dryRun)
|
|
185
|
-
return `Would post
|
|
186
|
-
const
|
|
216
|
+
return `Would post transaction "${input.description}" on ${input.date}.`;
|
|
217
|
+
const txInput = {
|
|
187
218
|
date: input.date,
|
|
188
219
|
description: input.description,
|
|
189
220
|
source_file_id: ctx.fileId,
|
|
190
221
|
source_page: input.source_page ?? null,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
222
|
+
raw_descriptor: input.raw_descriptor ?? null,
|
|
223
|
+
merchant: input.merchant ?? null,
|
|
224
|
+
merchant_id: input.merchant_id ?? null,
|
|
225
|
+
postings: (input.postings || []).map((p) => ({
|
|
226
|
+
account_id: p.account_id,
|
|
227
|
+
debit: p.debit ?? 0,
|
|
228
|
+
credit: p.credit ?? 0,
|
|
229
|
+
currency: p.currency || "THB",
|
|
230
|
+
memo: p.memo ?? null,
|
|
197
231
|
})),
|
|
198
232
|
};
|
|
199
233
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
let transactionId;
|
|
235
|
+
if (ctx.buffer) {
|
|
236
|
+
transactionId = ctx.buffer.appendTransaction(txInput);
|
|
237
|
+
}
|
|
238
|
+
else if (ctx.correlationId) {
|
|
239
|
+
const validated = validateTransaction(txInput);
|
|
240
|
+
const tx = db.transaction(() => {
|
|
241
|
+
insertTransactionRows(db, validated);
|
|
242
|
+
appendAction(db, {
|
|
243
|
+
correlation_id: ctx.correlationId,
|
|
244
|
+
command: ctx.command ?? "record",
|
|
245
|
+
user_input: ctx.userInput ?? null,
|
|
246
|
+
action_type: "record_transaction",
|
|
247
|
+
target_id: validated.id,
|
|
248
|
+
payload: {
|
|
249
|
+
transaction: {
|
|
250
|
+
date: validated.date,
|
|
251
|
+
description: validated.description,
|
|
252
|
+
source_page: validated.source_page ?? null,
|
|
253
|
+
raw_descriptor: validated.raw_descriptor ?? null,
|
|
254
|
+
},
|
|
255
|
+
postings: validated.postings,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
tx();
|
|
260
|
+
transactionId = validated.id;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
transactionId = recordTransaction(db, txInput);
|
|
264
|
+
}
|
|
265
|
+
return `Posted transaction ${transactionId} (${input.date}).`;
|
|
204
266
|
}
|
|
205
267
|
catch (err) {
|
|
206
|
-
return `Could not post
|
|
268
|
+
return `Could not post transaction: ${err.message}`;
|
|
207
269
|
}
|
|
208
270
|
}
|
|
209
|
-
case "note_concern": {
|
|
210
|
-
if (!ctx)
|
|
211
|
-
return "note_concern is only available inside an agent session.";
|
|
212
|
-
const target = {
|
|
213
|
-
entry_id: input.entry_id ?? null,
|
|
214
|
-
account_id: input.account_id ?? null,
|
|
215
|
-
};
|
|
216
|
-
if (ctx.buffer) {
|
|
217
|
-
ctx.buffer.appendConcern({ ...target, prompt: input.prompt, options: input.options });
|
|
218
|
-
return `Concern noted (buffered). Continue with the next row.`;
|
|
219
|
-
}
|
|
220
|
-
const id = recordConcern(db, {
|
|
221
|
-
file_id: ctx.fileId ?? null,
|
|
222
|
-
entry_id: target.entry_id,
|
|
223
|
-
account_id: target.account_id,
|
|
224
|
-
prompt: input.prompt,
|
|
225
|
-
options: input.options,
|
|
226
|
-
});
|
|
227
|
-
return `Concern noted (${id}). Continue with the next row.`;
|
|
228
|
-
}
|
|
229
271
|
default:
|
|
230
272
|
return undefined;
|
|
231
273
|
}
|
|
232
274
|
}
|
|
233
|
-
export const
|
|
234
|
-
DEFS:
|
|
235
|
-
LABELS:
|
|
236
|
-
execute:
|
|
275
|
+
export const accountIngestTools = {
|
|
276
|
+
DEFS: ACCOUNT_DEFS,
|
|
277
|
+
LABELS: ACCOUNT_LABELS,
|
|
278
|
+
execute: accountExecute,
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Scan-only concerns
|
|
282
|
+
*
|
|
283
|
+
* `note_concern` records a clarification mid-scan without ever prompting the
|
|
284
|
+
* user — only scan needs this. Record uses `clarify` (transient prompt, no
|
|
285
|
+
* concerns-table residue); review uses `ask_user` (prompts and resolves).
|
|
286
|
+
*/
|
|
287
|
+
const CONCERN_DEFS = [
|
|
288
|
+
{
|
|
289
|
+
name: "note_concern",
|
|
290
|
+
description: "Record a clarification request without pausing the run. Use during scan when a row is ambiguous (post your best-guess transaction first, then call this with the transaction's id), when a row is unparseable (skip the transaction, call this with no transaction_id), or when you have a concern about an account itself (pass account_id). Use kind='uncategorized_expense' when posting an expense to expense:uncategorized so reviewer can group these. The reviewer picks these up later with the full picture.",
|
|
291
|
+
input_schema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
prompt: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "The question or concern in a complete sentence, with date, ฿-formatted amount, and human account names. Never reference internal ids.",
|
|
297
|
+
},
|
|
298
|
+
kind: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Optional category for the concern. Use 'uncategorized_expense' when the posting landed in expense:uncategorized; reviewer batches these into one cleanup pass.",
|
|
301
|
+
},
|
|
302
|
+
options: {
|
|
303
|
+
type: "array",
|
|
304
|
+
description: "Optional list of candidate answers the reviewer can offer the user.",
|
|
305
|
+
items: { type: "string" },
|
|
306
|
+
},
|
|
307
|
+
transaction_id: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Id of the transaction this concern relates to (returned by record_transaction). Omit for file-level concerns about an unparseable row.",
|
|
310
|
+
},
|
|
311
|
+
account_id: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "Id of the account this concern relates to. Set when the statement's bank name, currency, statement_day, due_day, or other metadata disagrees with the stored account, or when you suspect a new account you're about to create duplicates an existing one. Can be combined with transaction_id.",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ["prompt"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
const CONCERN_LABELS = {
|
|
321
|
+
note_concern: "Noting concern",
|
|
322
|
+
};
|
|
323
|
+
async function concernExecute(db, name, input, ctx) {
|
|
324
|
+
if (name !== "note_concern")
|
|
325
|
+
return undefined;
|
|
326
|
+
if (!ctx)
|
|
327
|
+
return "note_concern is only available inside an agent session.";
|
|
328
|
+
const target = {
|
|
329
|
+
transaction_id: input.transaction_id ?? null,
|
|
330
|
+
account_id: input.account_id ?? null,
|
|
331
|
+
};
|
|
332
|
+
if (ctx.buffer) {
|
|
333
|
+
ctx.buffer.appendConcern({ ...target, kind: input.kind ?? null, prompt: input.prompt, options: input.options });
|
|
334
|
+
return `Concern noted (buffered). Continue with the next row.`;
|
|
335
|
+
}
|
|
336
|
+
const id = recordConcern(db, {
|
|
337
|
+
file_id: ctx.fileId ?? null,
|
|
338
|
+
transaction_id: target.transaction_id,
|
|
339
|
+
account_id: target.account_id,
|
|
340
|
+
kind: input.kind ?? null,
|
|
341
|
+
prompt: input.prompt,
|
|
342
|
+
options: input.options,
|
|
343
|
+
});
|
|
344
|
+
return `Concern noted (${id}). Continue with the next row.`;
|
|
345
|
+
}
|
|
346
|
+
export const scanConcernTools = {
|
|
347
|
+
DEFS: CONCERN_DEFS,
|
|
348
|
+
LABELS: CONCERN_LABELS,
|
|
349
|
+
execute: concernExecute,
|
|
237
350
|
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Review-only tool definitions
|
|
353
|
+
*
|
|
354
|
+
* `ask_user` is the only interactive primitive. Scan never reaches it (the
|
|
355
|
+
* scan profile doesn't include this module), so we don't need a "scan, please
|
|
356
|
+
* don't use this" guard.
|
|
357
|
+
*/
|
|
242
358
|
const REVIEW_DEFS = [
|
|
243
359
|
{
|
|
244
360
|
name: "ask_user",
|
|
245
|
-
description: "Ask the user a clarifying question when you cannot confidently proceed. The pipeline pauses and prompts the user interactively. Available during `plasalid review`. Not exposed during `plasalid scan` — use `note_concern` instead. Pass `
|
|
361
|
+
description: "Ask the user a clarifying question when you cannot confidently proceed. The pipeline pauses and prompts the user interactively. Available during `plasalid review`. Not exposed during `plasalid scan` — use `note_concern` instead. Pass `transaction_id` / `account_id` to attach the question to the same target as a scan-noted concern. Pass `concern_id` to resolve an existing open concern in place (recommended when re-posing a scan-noted concern to the user). Pass `related_concern_ids` to apply the user's single answer to a whole group of sibling concerns at once.",
|
|
246
362
|
input_schema: {
|
|
247
363
|
type: "object",
|
|
248
364
|
properties: {
|
|
@@ -252,9 +368,9 @@ const REVIEW_DEFS = [
|
|
|
252
368
|
description: "Optional list of candidate answers.",
|
|
253
369
|
items: { type: "string" },
|
|
254
370
|
},
|
|
255
|
-
|
|
371
|
+
transaction_id: {
|
|
256
372
|
type: "string",
|
|
257
|
-
description: "Optional:
|
|
373
|
+
description: "Optional: transaction this question is about. Used to clear the transaction's has_concern flag once all its concerns close.",
|
|
258
374
|
},
|
|
259
375
|
account_id: {
|
|
260
376
|
type: "string",
|
|
@@ -296,8 +412,6 @@ async function reviewExecute(db, name, input, ctx) {
|
|
|
296
412
|
return undefined;
|
|
297
413
|
if (!ctx)
|
|
298
414
|
return "ask_user is only available inside an agent session.";
|
|
299
|
-
// Two modes: resolve an existing concern in place (concern_id supplied),
|
|
300
|
-
// or post a fresh question that becomes its own concerns row.
|
|
301
415
|
let id;
|
|
302
416
|
if (input.concern_id) {
|
|
303
417
|
id = String(input.concern_id);
|
|
@@ -307,7 +421,7 @@ async function reviewExecute(db, name, input, ctx) {
|
|
|
307
421
|
else {
|
|
308
422
|
id = recordConcern(db, {
|
|
309
423
|
file_id: ctx.fileId ?? null,
|
|
310
|
-
|
|
424
|
+
transaction_id: input.transaction_id ?? null,
|
|
311
425
|
account_id: input.account_id ?? null,
|
|
312
426
|
prompt: input.prompt,
|
|
313
427
|
options: input.options,
|
|
@@ -316,9 +430,6 @@ async function reviewExecute(db, name, input, ctx) {
|
|
|
316
430
|
if (ctx.interactive && ctx.promptUser) {
|
|
317
431
|
const answer = await ctx.promptUser(input.prompt, input.options, input.facts);
|
|
318
432
|
resolveConcern(db, id, answer);
|
|
319
|
-
// Propagate the same answer to every sibling in the group so the user
|
|
320
|
-
// isn't asked the same thing again. Skip the primary id if the agent
|
|
321
|
-
// happened to include it.
|
|
322
433
|
const siblings = Array.isArray(input.related_concern_ids) ? input.related_concern_ids : [];
|
|
323
434
|
let propagated = 0;
|
|
324
435
|
for (const sibId of siblings) {
|