plasalid 0.5.7 → 0.6.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 +9 -9
- package/dist/accounts/taxonomy.d.ts +1 -1
- package/dist/accounts/taxonomy.js +2 -2
- package/dist/ai/agent.d.ts +8 -9
- package/dist/ai/agent.js +21 -20
- package/dist/ai/errors.d.ts +16 -0
- package/dist/ai/errors.js +47 -0
- package/dist/ai/personas.d.ts +1 -1
- package/dist/ai/personas.js +69 -66
- package/dist/ai/prompt-sections.d.ts +4 -5
- package/dist/ai/prompt-sections.js +11 -11
- package/dist/ai/providers/anthropic.js +10 -4
- package/dist/ai/providers/openai.js +70 -56
- package/dist/ai/redactor.js +77 -51
- package/dist/ai/system-prompt.d.ts +2 -3
- package/dist/ai/system-prompt.js +5 -5
- package/dist/ai/tools/common.js +13 -5
- package/dist/ai/tools/index.js +15 -15
- package/dist/ai/tools/ingest.d.ts +2 -2
- package/dist/ai/tools/ingest.js +210 -87
- package/dist/ai/tools/merchants.js +27 -12
- package/dist/ai/tools/read.js +36 -20
- package/dist/ai/tools/record.js +79 -19
- package/dist/ai/tools/resolve.d.ts +2 -0
- package/dist/ai/tools/resolve.js +195 -0
- package/dist/ai/tools/types.d.ts +5 -7
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/record.js +4 -2
- package/dist/cli/commands/resolve.d.ts +2 -0
- package/dist/cli/commands/resolve.js +13 -0
- package/dist/cli/commands/scan.js +18 -22
- package/dist/cli/commands/status.js +4 -2
- package/dist/cli/index.js +9 -9
- package/dist/cli/ink/hooks/useFooterText.js +1 -1
- package/dist/cli/ink/hooks/useTextInput.js +60 -69
- package/dist/cli/ink/scan_dashboard.d.ts +2 -2
- package/dist/cli/ink/scan_dashboard.js +3 -3
- package/dist/cli/setup.js +6 -3
- package/dist/cli/ux.js +1 -1
- package/dist/db/queries/account-balance.d.ts +140 -0
- package/dist/db/queries/account-balance.js +355 -0
- package/dist/db/queries/account_balance.d.ts +0 -1
- package/dist/db/queries/account_balance.js +0 -10
- package/dist/db/queries/action-log.d.ts +29 -0
- package/dist/db/queries/action-log.js +27 -0
- package/dist/db/queries/action_log.d.ts +1 -1
- package/dist/db/queries/concerns.d.ts +10 -0
- package/dist/db/queries/concerns.js +21 -0
- package/dist/db/queries/transactions.d.ts +3 -22
- package/dist/db/queries/transactions.js +4 -5
- package/dist/db/queries/unknowns.d.ts +62 -0
- package/dist/db/queries/unknowns.js +114 -0
- package/dist/db/schema.js +3 -3
- package/dist/resolver/pipeline.d.ts +16 -0
- package/dist/resolver/pipeline.js +38 -0
- package/dist/resolver/prompts.d.ts +8 -0
- package/dist/resolver/prompts.js +26 -0
- package/dist/scanner/account-mutex.d.ts +1 -0
- package/dist/scanner/account-mutex.js +16 -0
- package/dist/scanner/buffer.d.ts +10 -10
- package/dist/scanner/buffer.js +15 -15
- package/dist/scanner/concurrency.d.ts +10 -7
- package/dist/scanner/concurrency.js +3 -16
- package/dist/scanner/decrypt-queue.d.ts +57 -0
- package/dist/scanner/decrypt-queue.js +114 -0
- package/dist/scanner/decrypt_queue.js +56 -38
- package/dist/scanner/detectors/correlations.d.ts +2 -0
- package/dist/scanner/detectors/correlations.js +51 -0
- package/dist/scanner/detectors/duplicates.d.ts +2 -0
- package/dist/scanner/detectors/duplicates.js +75 -0
- package/dist/scanner/detectors/index.d.ts +18 -0
- package/dist/scanner/detectors/index.js +39 -0
- package/dist/scanner/detectors/recurrences.d.ts +2 -0
- package/dist/scanner/detectors/recurrences.js +49 -0
- package/dist/scanner/detectors/similar_accounts.d.ts +2 -0
- package/dist/scanner/detectors/similar_accounts.js +64 -0
- package/dist/scanner/detectors/similarities.d.ts +2 -0
- package/dist/scanner/detectors/similarities.js +73 -0
- package/dist/scanner/detectors/types.d.ts +16 -0
- package/dist/scanner/detectors/types.js +1 -0
- package/dist/scanner/inspectors/correlations.d.ts +2 -0
- package/dist/scanner/inspectors/correlations.js +47 -0
- package/dist/scanner/inspectors/duplicates.d.ts +2 -0
- package/dist/scanner/inspectors/duplicates.js +75 -0
- package/dist/scanner/inspectors/index.d.ts +19 -0
- package/dist/scanner/inspectors/index.js +39 -0
- package/dist/scanner/inspectors/recurrences.d.ts +2 -0
- package/dist/scanner/inspectors/recurrences.js +49 -0
- package/dist/scanner/inspectors/similarities.d.ts +2 -0
- package/dist/scanner/inspectors/similarities.js +73 -0
- package/dist/scanner/inspectors/types.d.ts +16 -0
- package/dist/scanner/inspectors/types.js +1 -0
- package/dist/scanner/pdf-unlock.js +3 -1
- package/dist/scanner/pipeline.d.ts +6 -4
- package/dist/scanner/pipeline.js +63 -102
- package/dist/scanner/prompts.js +2 -2
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMemories } from "./memory.js";
|
|
2
|
-
import { getAccountBalances } from "../db/queries/
|
|
2
|
+
import { getAccountBalances, } from "../db/queries/account-balance.js";
|
|
3
3
|
import { stripControls } from "./sanitize.js";
|
|
4
4
|
/**
|
|
5
5
|
* Small, single-purpose renderers that produce one Markdown-ish section each.
|
|
@@ -21,7 +21,7 @@ export function renderTodayHuman() {
|
|
|
21
21
|
year: "numeric",
|
|
22
22
|
})}.`;
|
|
23
23
|
}
|
|
24
|
-
/** ISO date for scan/
|
|
24
|
+
/** ISO date for scan/resolve ("Today is 2026-03-05."). */
|
|
25
25
|
export function renderTodayIso() {
|
|
26
26
|
return `Today is ${new Date().toISOString().slice(0, 10)}.`;
|
|
27
27
|
}
|
|
@@ -51,7 +51,7 @@ export function renderChatChartOrEmpty(db, name) {
|
|
|
51
51
|
return `## Accounts on file\n${rows.join("\n")}`;
|
|
52
52
|
}
|
|
53
53
|
function renderHierarchical(balances, withBalance) {
|
|
54
|
-
const byId = new Map(balances.map(b => [b.id, b]));
|
|
54
|
+
const byId = new Map(balances.map((b) => [b.id, b]));
|
|
55
55
|
const depthCache = new Map();
|
|
56
56
|
const depth = (id) => {
|
|
57
57
|
if (depthCache.has(id))
|
|
@@ -65,16 +65,16 @@ function renderHierarchical(balances, withBalance) {
|
|
|
65
65
|
depthCache.set(id, d);
|
|
66
66
|
return d;
|
|
67
67
|
};
|
|
68
|
-
return balances.map(a => formatAccountRow(a, withBalance, depth(a.id)));
|
|
68
|
+
return balances.map((a) => formatAccountRow(a, withBalance, depth(a.id)));
|
|
69
69
|
}
|
|
70
70
|
export function renderMemories(db, opts) {
|
|
71
71
|
const all = getMemories(db);
|
|
72
72
|
const filtered = opts.filterCategories
|
|
73
|
-
? all.filter(m => opts.filterCategories.includes(m.category))
|
|
73
|
+
? all.filter((m) => opts.filterCategories.includes(m.category))
|
|
74
74
|
: all;
|
|
75
75
|
if (filtered.length === 0)
|
|
76
76
|
return null;
|
|
77
|
-
const lines = filtered.map(m => formatMemoryLine(m, opts.showCategory));
|
|
77
|
+
const lines = filtered.map((m) => formatMemoryLine(m, opts.showCategory));
|
|
78
78
|
return `## ${opts.header}\n${lines.join("\n")}`;
|
|
79
79
|
}
|
|
80
80
|
export function renderScope(opts) {
|
|
@@ -83,14 +83,12 @@ export function renderScope(opts) {
|
|
|
83
83
|
`- account: ${opts.accountId ?? "all"}`,
|
|
84
84
|
`- from: ${opts.from ?? "all time"}`,
|
|
85
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
86
|
].join("\n");
|
|
90
87
|
}
|
|
91
88
|
/** Chat user context */
|
|
92
89
|
export function renderUserContext(name, contextMd) {
|
|
93
|
-
const body = contextMd ??
|
|
90
|
+
const body = contextMd ??
|
|
91
|
+
`(No personal context on file yet. ${name} can edit ~/.plasalid/context.md to add family, income, or other facts.)`;
|
|
94
92
|
return `## About ${name}\n${body}`;
|
|
95
93
|
}
|
|
96
94
|
/** Internal formatters */
|
|
@@ -98,7 +96,9 @@ function formatAccountRow(a, withBalance, depth = 0) {
|
|
|
98
96
|
const indent = " ".repeat(depth);
|
|
99
97
|
const subtype = a.subtype ? `/${a.subtype}` : "";
|
|
100
98
|
const base = `- ${indent}${a.id} | ${a.name} | ${a.type}${subtype}`;
|
|
101
|
-
return withBalance
|
|
99
|
+
return withBalance
|
|
100
|
+
? `${base} | balance ${a.balance.toFixed(2)} ${a.currency}`
|
|
101
|
+
: base;
|
|
102
102
|
}
|
|
103
103
|
function formatMemoryLine(m, showCategory) {
|
|
104
104
|
return showCategory
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { classifyProviderError } from "../errors.js";
|
|
2
3
|
export function createAnthropicProvider(opts) {
|
|
3
4
|
const client = new Anthropic(opts.baseURL
|
|
4
5
|
? { apiKey: opts.apiKey, baseURL: opts.baseURL }
|
|
@@ -17,10 +18,15 @@ export function createAnthropicProvider(opts) {
|
|
|
17
18
|
if (params.thinking) {
|
|
18
19
|
apiParams.thinking = params.thinking;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
let response;
|
|
22
|
+
try {
|
|
23
|
+
response = await client.messages.create(apiParams, {
|
|
24
|
+
signal: params.signal,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
classifyProviderError(e, params.signal);
|
|
29
|
+
}
|
|
24
30
|
const content = [];
|
|
25
31
|
for (const block of response.content) {
|
|
26
32
|
if (block.type === "thinking")
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { classifyProviderError } from "../errors.js";
|
|
3
|
+
function isMaxTokensRejection(e) {
|
|
4
|
+
const err = e;
|
|
5
|
+
return err.status === 400 && (err.message?.includes("max_tokens") ?? false);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Some OpenAI-compatible endpoints (older models, Ollama, vLLM) accept `max_tokens`;
|
|
9
|
+
* newer OpenAI models require `max_completion_tokens`. Try the former, fall back on a
|
|
10
|
+
* 400 that explicitly names the parameter.
|
|
11
|
+
*/
|
|
12
|
+
async function createCompletionWithTokenFallback(client, body, options) {
|
|
13
|
+
const base = {
|
|
14
|
+
model: body.model,
|
|
15
|
+
messages: body.messages,
|
|
16
|
+
tools: body.tools,
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
return await client.chat.completions.create({ ...base, max_tokens: body.maxTokens }, options);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (isMaxTokensRejection(e)) {
|
|
23
|
+
return await client.chat.completions.create({ ...base, max_completion_tokens: body.maxTokens }, options);
|
|
24
|
+
}
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
2
28
|
export function createOpenAICompatibleProvider(opts) {
|
|
3
29
|
const client = new OpenAI({
|
|
4
30
|
apiKey: opts.apiKey,
|
|
@@ -8,63 +34,54 @@ export function createOpenAICompatibleProvider(opts) {
|
|
|
8
34
|
name: "openai-compatible",
|
|
9
35
|
supportsThinking: false,
|
|
10
36
|
async sendMessage(params) {
|
|
11
|
-
const messages = convertMessages(params.system, params.messages);
|
|
12
37
|
const tools = convertTools(params.tools);
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
const body = {
|
|
39
|
+
model: params.model,
|
|
40
|
+
maxTokens: params.maxTokens,
|
|
41
|
+
messages: convertMessages(params.system, params.messages),
|
|
42
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
43
|
+
};
|
|
15
44
|
let response;
|
|
16
45
|
try {
|
|
17
|
-
response = await client.
|
|
18
|
-
model: params.model,
|
|
19
|
-
max_tokens: params.maxTokens,
|
|
20
|
-
messages,
|
|
21
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
22
|
-
}, { signal: params.signal });
|
|
46
|
+
response = await createCompletionWithTokenFallback(client, body, { signal: params.signal });
|
|
23
47
|
}
|
|
24
48
|
catch (e) {
|
|
25
|
-
|
|
26
|
-
response = await client.chat.completions.create({
|
|
27
|
-
model: params.model,
|
|
28
|
-
max_completion_tokens: params.maxTokens,
|
|
29
|
-
messages,
|
|
30
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
31
|
-
}, { signal: params.signal });
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
throw e;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const choice = response.choices[0];
|
|
38
|
-
if (!choice) {
|
|
39
|
-
return { content: [], stopReason: "end_turn" };
|
|
40
|
-
}
|
|
41
|
-
const content = [];
|
|
42
|
-
if (choice.message.content) {
|
|
43
|
-
content.push({ type: "text", text: choice.message.content });
|
|
49
|
+
classifyProviderError(e, params.signal);
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
for (const tc of choice.message.tool_calls) {
|
|
47
|
-
if (tc.type !== "function")
|
|
48
|
-
continue;
|
|
49
|
-
content.push({
|
|
50
|
-
type: "tool_use",
|
|
51
|
-
id: tc.id,
|
|
52
|
-
name: tc.function.name,
|
|
53
|
-
input: parseArguments(tc.function.arguments),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
58
|
-
return {
|
|
59
|
-
content,
|
|
60
|
-
stopReason: hasToolCalls ? "tool_use" : "end_turn",
|
|
61
|
-
usage: response.usage
|
|
62
|
-
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
63
|
-
: undefined,
|
|
64
|
-
};
|
|
51
|
+
return normalizeResponse(response);
|
|
65
52
|
},
|
|
66
53
|
};
|
|
67
54
|
}
|
|
55
|
+
function normalizeResponse(response) {
|
|
56
|
+
const choice = response.choices[0];
|
|
57
|
+
if (!choice) {
|
|
58
|
+
return { content: [], stopReason: "end_turn" };
|
|
59
|
+
}
|
|
60
|
+
const content = [];
|
|
61
|
+
if (choice.message.content) {
|
|
62
|
+
content.push({ type: "text", text: choice.message.content });
|
|
63
|
+
}
|
|
64
|
+
if (choice.message.tool_calls) {
|
|
65
|
+
for (const tc of choice.message.tool_calls) {
|
|
66
|
+
if (tc.type !== "function")
|
|
67
|
+
continue;
|
|
68
|
+
content.push({
|
|
69
|
+
type: "tool_use",
|
|
70
|
+
id: tc.id,
|
|
71
|
+
name: tc.function.name,
|
|
72
|
+
input: parseArguments(tc.function.arguments),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
77
|
+
return {
|
|
78
|
+
content,
|
|
79
|
+
stopReason: hasToolCalls ? "tool_use" : "end_turn",
|
|
80
|
+
usage: response.usage
|
|
81
|
+
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
82
|
+
: undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
68
85
|
function convertMessages(system, messages) {
|
|
69
86
|
const result = [
|
|
70
87
|
{ role: "system", content: system },
|
|
@@ -104,14 +121,11 @@ function convertMessages(system, messages) {
|
|
|
104
121
|
.join("\n");
|
|
105
122
|
const toolCalls = blocks
|
|
106
123
|
.filter((b) => b.type === "tool_use")
|
|
107
|
-
.map((
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
113
|
-
};
|
|
114
|
-
});
|
|
124
|
+
.map((tu) => ({
|
|
125
|
+
id: tu.id,
|
|
126
|
+
type: "function",
|
|
127
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
128
|
+
}));
|
|
115
129
|
result.push({
|
|
116
130
|
role: "assistant",
|
|
117
131
|
content: textParts || null,
|
package/dist/ai/redactor.js
CHANGED
|
@@ -1,75 +1,101 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
2
|
import { readContext } from "./context.js";
|
|
3
|
+
const SECTION_RULES = [
|
|
4
|
+
{
|
|
5
|
+
heading: "Family",
|
|
6
|
+
token: "[PARTNER]",
|
|
7
|
+
stripParen: true,
|
|
8
|
+
skipIfUser: true,
|
|
9
|
+
patterns: [
|
|
10
|
+
/^(?:partner|spouse|wife|husband|child|kid|son|daughter|dependent)[:\s]+(.+)/i,
|
|
11
|
+
/^([\p{Lu}\p{Lo}][\p{L}\s]+)/u,
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
heading: "Income",
|
|
16
|
+
token: "[EMPLOYER]",
|
|
17
|
+
patterns: [
|
|
18
|
+
/(?:employer|works? (?:at|for)|employed (?:at|by))[:\s]+([A-Z][\w\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/i,
|
|
19
|
+
/\bfrom ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/,
|
|
20
|
+
/\bat ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/,
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
// Patterns for numeric / identifier PII commonly found in Thai financial data.
|
|
25
|
+
const NUMERIC_PII_PATTERNS = [
|
|
26
|
+
// Thai national ID with dashes: 1-2345-67890-12-3
|
|
27
|
+
[/\b\d-\d{4}-\d{5}-\d{2}-\d\b/g, "[NATID]"],
|
|
28
|
+
// Thai national ID without dashes (13 digits) — must precede the generic ACCT pattern.
|
|
29
|
+
[/\b\d{13}\b/g, "[NATID]"],
|
|
30
|
+
// Thai mobile numbers: 0[689]xxxxxxxx (10 digits starting 06/08/09)
|
|
31
|
+
[/\b0[689]\d{8}\b/g, "[PHONE]"],
|
|
32
|
+
// 16-digit credit card (with optional separators)
|
|
33
|
+
[/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "[CARD]"],
|
|
34
|
+
// 10–12 digit account / routing numbers at a word boundary
|
|
35
|
+
[/\b\d{10,12}\b(?=\s|$|[,.])/g, "[ACCT]"],
|
|
36
|
+
];
|
|
37
|
+
function extractSectionLines(context, heading) {
|
|
38
|
+
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n##|$)`);
|
|
39
|
+
const match = context.match(re);
|
|
40
|
+
if (!match)
|
|
41
|
+
return [];
|
|
42
|
+
return match[1]
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter((l) => l.trim().startsWith("-"))
|
|
45
|
+
.map((l) => l.replace(/^-\s*/, "").trim())
|
|
46
|
+
.filter((text) => text.length > 0 && !text.startsWith("("));
|
|
47
|
+
}
|
|
48
|
+
function applyRule(rule, context, userName, push) {
|
|
49
|
+
for (const line of extractSectionLines(context, rule.heading)) {
|
|
50
|
+
if (rule.skipIfUser && line.toLowerCase() === userName.toLowerCase())
|
|
51
|
+
continue;
|
|
52
|
+
for (const pattern of rule.patterns) {
|
|
53
|
+
const match = line.match(pattern);
|
|
54
|
+
if (!match)
|
|
55
|
+
continue;
|
|
56
|
+
let name = match[1].trim();
|
|
57
|
+
if (rule.stripParen)
|
|
58
|
+
name = name.replace(/\s*\(.*\)/, "").trim();
|
|
59
|
+
if (!name)
|
|
60
|
+
break;
|
|
61
|
+
if (rule.skipIfUser && name.toLowerCase() === userName.toLowerCase())
|
|
62
|
+
break;
|
|
63
|
+
push(name, rule.token);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
3
68
|
function buildRedactions() {
|
|
4
69
|
const entries = [];
|
|
5
70
|
const seen = new Set();
|
|
6
|
-
|
|
71
|
+
const push = (real, token) => {
|
|
7
72
|
const trimmed = real.trim();
|
|
8
|
-
if (trimmed.length < 2
|
|
73
|
+
if (trimmed.length < 2)
|
|
74
|
+
return;
|
|
75
|
+
const key = trimmed.toLowerCase();
|
|
76
|
+
if (seen.has(key))
|
|
9
77
|
return;
|
|
10
|
-
seen.add(
|
|
78
|
+
seen.add(key);
|
|
11
79
|
entries.push({ real: trimmed, token });
|
|
12
|
-
}
|
|
80
|
+
};
|
|
13
81
|
const userName = config.userName;
|
|
14
82
|
if (userName && userName !== "User") {
|
|
15
|
-
|
|
83
|
+
push(userName, "[USER]");
|
|
16
84
|
const parts = userName.split(/\s+/);
|
|
17
85
|
if (parts.length > 1) {
|
|
18
|
-
|
|
19
|
-
|
|
86
|
+
push(parts[0], "[USER_FIRST]");
|
|
87
|
+
push(parts[parts.length - 1], "[USER_LAST]");
|
|
20
88
|
}
|
|
21
89
|
}
|
|
22
90
|
const context = readContext();
|
|
23
91
|
if (context) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const lines = familyMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
|
|
27
|
-
for (const line of lines) {
|
|
28
|
-
const text = line.replace(/^-\s*/, "").trim();
|
|
29
|
-
if (!text || text.startsWith("(") || text.toLowerCase() === userName.toLowerCase())
|
|
30
|
-
continue;
|
|
31
|
-
const nameMatch = text.match(/^(?:partner|spouse|wife|husband|child|kid|son|daughter|dependent)[:\s]+(.+)/i)
|
|
32
|
-
|| text.match(/^([\p{Lu}\p{Lo}][\p{L}\s]+)/u);
|
|
33
|
-
if (nameMatch) {
|
|
34
|
-
const name = nameMatch[1].replace(/\s*\(.*\)/, "").trim();
|
|
35
|
-
if (name && name.toLowerCase() !== userName.toLowerCase()) {
|
|
36
|
-
add(name, "[PARTNER]");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const incomeMatch = context.match(/## Income\n([\s\S]*?)(?=\n##|$)/);
|
|
42
|
-
if (incomeMatch) {
|
|
43
|
-
const lines = incomeMatch[1].split("\n").filter(l => l.trim().startsWith("-"));
|
|
44
|
-
for (const line of lines) {
|
|
45
|
-
const text = line.replace(/^-\s*/, "").trim();
|
|
46
|
-
if (!text || text.startsWith("("))
|
|
47
|
-
continue;
|
|
48
|
-
const employerMatch = text.match(/(?:employer|works? (?:at|for)|employed (?:at|by))[:\s]+([A-Z][\w\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/i)
|
|
49
|
-
|| text.match(/\bfrom ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/)
|
|
50
|
-
|| text.match(/\bat ([A-Z][A-Za-z\s&.,-]+?)(?:\s*[-–—|,;(\n]|$)/);
|
|
51
|
-
if (employerMatch) {
|
|
52
|
-
add(employerMatch[1].trim(), "[EMPLOYER]");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
92
|
+
for (const rule of SECTION_RULES) {
|
|
93
|
+
applyRule(rule, context, userName, push);
|
|
55
94
|
}
|
|
56
95
|
}
|
|
57
96
|
entries.sort((a, b) => b.real.length - a.real.length);
|
|
58
97
|
return entries;
|
|
59
98
|
}
|
|
60
|
-
// Patterns for numeric / identifier PII commonly found in Thai financial data.
|
|
61
|
-
const NUMERIC_PII_PATTERNS = [
|
|
62
|
-
// Thai national ID with dashes: 1-2345-67890-12-3
|
|
63
|
-
[/\b\d-\d{4}-\d{5}-\d{2}-\d\b/g, "[NATID]"],
|
|
64
|
-
// Thai national ID without dashes (13 digits) — must precede the generic ACCT pattern.
|
|
65
|
-
[/\b\d{13}\b/g, "[NATID]"],
|
|
66
|
-
// Thai mobile numbers: 0[689]xxxxxxxx (10 digits starting 06/08/09)
|
|
67
|
-
[/\b0[689]\d{8}\b/g, "[PHONE]"],
|
|
68
|
-
// 16-digit credit card (with optional separators)
|
|
69
|
-
[/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "[CARD]"],
|
|
70
|
-
// 10–12 digit account / routing numbers at a word boundary
|
|
71
|
-
[/\b\d{10,12}\b(?=\s|$|[,.])/g, "[ACCT]"],
|
|
72
|
-
];
|
|
73
99
|
export function redact(text) {
|
|
74
100
|
const redactions = buildRedactions();
|
|
75
101
|
let result = text;
|
|
@@ -2,11 +2,10 @@ import type Database from "libsql";
|
|
|
2
2
|
export interface ScanPromptOptions {
|
|
3
3
|
fileName: string;
|
|
4
4
|
}
|
|
5
|
-
export interface
|
|
5
|
+
export interface ResolvePromptOptions {
|
|
6
6
|
accountId?: string;
|
|
7
7
|
from?: string;
|
|
8
8
|
to?: string;
|
|
9
|
-
dryRun: boolean;
|
|
10
9
|
}
|
|
11
10
|
export interface RecordPromptOptions {
|
|
12
11
|
utterance: string;
|
|
@@ -19,6 +18,6 @@ export interface RecordPromptOptions {
|
|
|
19
18
|
* shuffle the array.
|
|
20
19
|
*/
|
|
21
20
|
export declare function buildChatSystemPrompt(db: Database.Database): string;
|
|
22
|
-
export declare function
|
|
21
|
+
export declare function buildResolveSystemPrompt(db: Database.Database, opts: ResolvePromptOptions): string;
|
|
23
22
|
export declare function buildRecordSystemPrompt(db: Database.Database, opts: RecordPromptOptions): string;
|
|
24
23
|
export declare function buildScanSystemPrompt(db: Database.Database, opts: ScanPromptOptions): string;
|
package/dist/ai/system-prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
2
|
import { readContext } from "./context.js";
|
|
3
|
-
import { chatPersona, SCAN_PERSONA,
|
|
3
|
+
import { chatPersona, SCAN_PERSONA, RESOLVE_PERSONA, RECORD_PERSONA } from "./personas.js";
|
|
4
4
|
import { getThaiTaxonomyHint } from "../accounts/taxonomy.js";
|
|
5
5
|
import { renderChartOfAccounts, renderChatChartOrEmpty, renderMemories, renderScope, renderTodayHuman, renderTodayIso, renderUserContext, } from "./prompt-sections.js";
|
|
6
6
|
/**
|
|
@@ -23,11 +23,11 @@ export function buildChatSystemPrompt(db) {
|
|
|
23
23
|
}),
|
|
24
24
|
]);
|
|
25
25
|
}
|
|
26
|
-
export function
|
|
26
|
+
export function buildResolveSystemPrompt(db, opts) {
|
|
27
27
|
return joinSections([
|
|
28
|
-
|
|
28
|
+
RESOLVE_PERSONA,
|
|
29
29
|
renderTodayIso(),
|
|
30
|
-
renderChartOfAccounts(db, { withBalance: true, emptyState: "
|
|
30
|
+
renderChartOfAccounts(db, { withBalance: true, emptyState: "resolve" }),
|
|
31
31
|
renderScope(opts),
|
|
32
32
|
renderMemories(db, {
|
|
33
33
|
header: "Rules you've already learned (apply directly; do not re-ask the user)",
|
|
@@ -56,7 +56,7 @@ export function buildScanSystemPrompt(db, opts) {
|
|
|
56
56
|
`## File context\nFile: ${opts.fileName}`,
|
|
57
57
|
`## Taxonomy hints\n${getThaiTaxonomyHint()}`,
|
|
58
58
|
renderMemories(db, {
|
|
59
|
-
header: "Rules you've already learned (apply silently before raising
|
|
59
|
+
header: "Rules you've already learned (apply silently before raising an unknown)",
|
|
60
60
|
filterCategories: ["scanning_hint", "general"],
|
|
61
61
|
showCategory: false,
|
|
62
62
|
}),
|
package/dist/ai/tools/common.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { saveMemory, getMemories } from "../memory.js";
|
|
2
|
-
import { getAccountBalances } from "../../db/queries/
|
|
2
|
+
import { getAccountBalances } from "../../db/queries/account-balance.js";
|
|
3
3
|
import { formatAmount } from "../../currency.js";
|
|
4
4
|
import { sanitizeForPrompt, sanitizeForPromptCell } from "../sanitize.js";
|
|
5
5
|
import { ACCOUNT_TYPE_DESCRIPTIONS } from "../../accounts/taxonomy.js";
|
|
@@ -11,7 +11,11 @@ const DEFS = [
|
|
|
11
11
|
input_schema: {
|
|
12
12
|
type: "object",
|
|
13
13
|
properties: {
|
|
14
|
-
type: {
|
|
14
|
+
type: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: ACCOUNT_TYPES,
|
|
17
|
+
description: "Filter by account type.",
|
|
18
|
+
},
|
|
15
19
|
},
|
|
16
20
|
required: [],
|
|
17
21
|
},
|
|
@@ -23,7 +27,11 @@ const DEFS = [
|
|
|
23
27
|
type: "object",
|
|
24
28
|
properties: {
|
|
25
29
|
content: { type: "string", description: "What to remember." },
|
|
26
|
-
category: {
|
|
30
|
+
category: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Category: general, scanning_hint, preference, life_event.",
|
|
33
|
+
default: "general",
|
|
34
|
+
},
|
|
27
35
|
},
|
|
28
36
|
required: ["content"],
|
|
29
37
|
},
|
|
@@ -46,7 +54,7 @@ async function execute(db, name, input, _ctx) {
|
|
|
46
54
|
if (accounts.length === 0)
|
|
47
55
|
return "No accounts in the chart of accounts yet.";
|
|
48
56
|
return accounts
|
|
49
|
-
.map(a => {
|
|
57
|
+
.map((a) => {
|
|
50
58
|
const meta = [];
|
|
51
59
|
if (a.bank_name)
|
|
52
60
|
meta.push(sanitizeForPrompt(a.bank_name));
|
|
@@ -70,7 +78,7 @@ async function execute(db, name, input, _ctx) {
|
|
|
70
78
|
if (memories.length === 0)
|
|
71
79
|
return "No memories saved yet.";
|
|
72
80
|
return memories
|
|
73
|
-
.map(m => `[${m.category}] ${sanitizeForPrompt(m.content)} (saved ${m.created_at})`)
|
|
81
|
+
.map((m) => `[${m.category}] ${sanitizeForPrompt(m.content)} (saved ${m.created_at})`)
|
|
74
82
|
.join("\n");
|
|
75
83
|
}
|
|
76
84
|
default:
|
package/dist/ai/tools/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { commonTools } from "./common.js";
|
|
2
2
|
import { readTools } from "./read.js";
|
|
3
|
-
import { accountIngestTools,
|
|
3
|
+
import { accountIngestTools, scanUnknownTools, resolveIngestTools } from "./ingest.js";
|
|
4
4
|
import { scanTools } from "./scan.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveTools } from "./resolve.js";
|
|
6
6
|
import { recordTools } from "./record.js";
|
|
7
7
|
import { merchantTools } from "./merchants.js";
|
|
8
8
|
/**
|
|
@@ -11,17 +11,17 @@ import { merchantTools } from "./merchants.js";
|
|
|
11
11
|
* central switch.
|
|
12
12
|
*
|
|
13
13
|
* `accountIngestTools` (create_account / update_account_metadata /
|
|
14
|
-
* record_transaction) ships with scan,
|
|
15
|
-
* shared write primitives. `
|
|
16
|
-
* record uses `clarify` from `recordTools` for transient prompts,
|
|
17
|
-
* `ask_user` from `
|
|
18
|
-
* `merchantTools` ships with scan,
|
|
14
|
+
* record_transaction) ships with scan, resolve, and record — they're the
|
|
15
|
+
* shared write primitives. `scanUnknownTools` (note_unknown) is scan-only;
|
|
16
|
+
* record uses `clarify` from `recordTools` for transient prompts, resolve uses
|
|
17
|
+
* `ask_user` from `resolveIngestTools` for resolve-in-place clarifications.
|
|
18
|
+
* `merchantTools` ships with scan, resolve, and record so any write profile can
|
|
19
19
|
* upsert / look up / re-cache merchants alongside the posting flow.
|
|
20
20
|
*/
|
|
21
21
|
const PROFILES = {
|
|
22
|
-
scan: [commonTools, accountIngestTools,
|
|
22
|
+
scan: [commonTools, accountIngestTools, scanUnknownTools, scanTools, merchantTools],
|
|
23
23
|
chat: [commonTools, readTools],
|
|
24
|
-
|
|
24
|
+
resolve: [commonTools, readTools, accountIngestTools, resolveIngestTools, resolveTools, merchantTools],
|
|
25
25
|
record: [commonTools, readTools, accountIngestTools, recordTools, merchantTools],
|
|
26
26
|
};
|
|
27
27
|
export function getToolDefinitions(profile) {
|
|
@@ -32,10 +32,10 @@ export async function executeTool(db, name, input, ctx) {
|
|
|
32
32
|
commonTools,
|
|
33
33
|
readTools,
|
|
34
34
|
accountIngestTools,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
scanUnknownTools,
|
|
36
|
+
resolveIngestTools,
|
|
37
37
|
scanTools,
|
|
38
|
-
|
|
38
|
+
resolveTools,
|
|
39
39
|
recordTools,
|
|
40
40
|
merchantTools,
|
|
41
41
|
]) {
|
|
@@ -50,10 +50,10 @@ export const TOOL_LABELS = {
|
|
|
50
50
|
...commonTools.LABELS,
|
|
51
51
|
...readTools.LABELS,
|
|
52
52
|
...accountIngestTools.LABELS,
|
|
53
|
-
...
|
|
54
|
-
...
|
|
53
|
+
...scanUnknownTools.LABELS,
|
|
54
|
+
...resolveIngestTools.LABELS,
|
|
55
55
|
...scanTools.LABELS,
|
|
56
|
-
...
|
|
56
|
+
...resolveTools.LABELS,
|
|
57
57
|
...recordTools.LABELS,
|
|
58
58
|
...merchantTools.LABELS,
|
|
59
59
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ToolModule } from "./types.js";
|
|
2
2
|
export declare const accountIngestTools: ToolModule;
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
3
|
+
export declare const scanUnknownTools: ToolModule;
|
|
4
|
+
export declare const resolveIngestTools: ToolModule;
|