@chatpanel/pii 0.2.3 → 0.2.5
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/package.json +1 -1
- package/tool-harness.js +24 -0
- package/tool-rank.js +42 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/pii",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "The canonical ChatPanel privacy engine — reversible PII redaction + pseudonymization with local entity detection. Pure, dependency-free ESM shared by the ChatPanel extension, gateway, and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/tool-harness.js
CHANGED
|
@@ -41,6 +41,30 @@ export function restoreToolArgs(value, vault) {
|
|
|
41
41
|
return value;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// System-prompt note that tells the model how to behave around placeholders when
|
|
45
|
+
// tools are armed. WITHOUT this, privacy-aware models (Codex, Claude) recognize a
|
|
46
|
+
// [[LOCATION_1]] token as redacted and REFUSE to use it for a lookup ("I can't see
|
|
47
|
+
// your real city") — the opposite of what we want. Weak models call the tool blindly
|
|
48
|
+
// and it works (the harness restores the real value), so the note levels them up.
|
|
49
|
+
export function placeholderToolNote({ toolData = 'real' } = {}) {
|
|
50
|
+
const intro =
|
|
51
|
+
'PRIVACY PLACEHOLDERS: some values in this conversation are tokens like [[PERSON_1]], '
|
|
52
|
+
+ '[[LOCATION_1]], [[ORG_1]] that stand in for the user\'s real private data. ';
|
|
53
|
+
const remote = toolData === 'redactRemote'
|
|
54
|
+
? 'When you call a LOCAL tool the placeholder is automatically replaced with the real '
|
|
55
|
+
+ 'value before the tool runs; REMOTE (MCP) tools deliberately receive the placeholder '
|
|
56
|
+
+ 'to keep private data off third-party servers. '
|
|
57
|
+
: 'When you call ANY tool, these placeholders are AUTOMATICALLY replaced with the real '
|
|
58
|
+
+ 'values before the tool executes — the tool receives the TRUE value and returns correct '
|
|
59
|
+
+ 'results. ';
|
|
60
|
+
const rules =
|
|
61
|
+
'So: CALL THE TOOL using the placeholder exactly as written, as if it were the real value. '
|
|
62
|
+
+ 'Do NOT say you cannot see the real value, do NOT ask the user to re-type it, and do NOT '
|
|
63
|
+
+ 'refuse on privacy grounds — the lookup will work. The real values are restored in your '
|
|
64
|
+
+ 'final answer automatically, so write your answer using the placeholders too.';
|
|
65
|
+
return intro + remote + rules;
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
export function makeToolHarness({ vault = null, toolData = 'real', redactOpts = null, redactResults = true } = {}) {
|
|
45
69
|
const on = !!vault; // privacy enabled for this turn?
|
|
46
70
|
const redactRemote = toolData === 'redactRemote';
|
package/tool-rank.js
CHANGED
|
@@ -19,6 +19,11 @@ const STOP = new Set([
|
|
|
19
19
|
const defName = (s) => (s && s.name) || '';
|
|
20
20
|
const defDesc = (s) => (s && s.description) || '';
|
|
21
21
|
|
|
22
|
+
// Names of GENERAL entry-point tools — preferred when a query doesn't pin a specific
|
|
23
|
+
// tool. Matches the tool segment (after the server prefix): e.g. ...__wikipedia_search,
|
|
24
|
+
// ...__ask_pipeworx, ...__get_summary, ...__search_wikipedia.
|
|
25
|
+
const GENERAL_TOOL_RE = /(?:^|_)(search|ask|lookup|find|answer|summary|wiki)(?:_|$)/i;
|
|
26
|
+
|
|
22
27
|
// Returns specs scored + sorted most-relevant first, as [{ s, i, n }] (i = original
|
|
23
28
|
// index, n = score). Stable for ties (preserves original order).
|
|
24
29
|
export function scoreToolSpecs(specs, query, { name = defName, description = defDesc } = {}) {
|
|
@@ -37,6 +42,12 @@ export function scoreToolSpecs(specs, query, { name = defName, description = def
|
|
|
37
42
|
for (const part of names[i].split(/[^a-z0-9]+/)) {
|
|
38
43
|
if (part.length > 2 && q.includes(part)) n += 2 + idf(part); // tool explicitly named
|
|
39
44
|
}
|
|
45
|
+
// General-purpose ENTRY-POINT tools (search / ask / lookup / get-summary / answer)
|
|
46
|
+
// are the right default when the query doesn't keyword-match a specific tool —
|
|
47
|
+
// e.g. "which state is Seattle in" → a wikipedia SEARCH/ASK tool, not one of 20
|
|
48
|
+
// dataset-query tools. A small tie-breaker boost (below a real keyword match) so
|
|
49
|
+
// those generic tools win when nothing else distinguishes them.
|
|
50
|
+
if (GENERAL_TOOL_RE.test(names[i])) n += 1.5;
|
|
40
51
|
return n;
|
|
41
52
|
};
|
|
42
53
|
return list.map((s, i) => ({ s, i, n: score(i) })).sort((a, b) => (b.n - a.n) || (a.i - b.i));
|
|
@@ -51,12 +62,41 @@ export function rankToolSpecs(specs, query, accessors) {
|
|
|
51
62
|
// retaining everything that does (e.g. local page/history tools). `cap` therefore
|
|
52
63
|
// bounds the NARROWABLE (MCP) tools; kept tools ride along free. Returns the list
|
|
53
64
|
// unchanged when there's no cap or the narrowable set already fits.
|
|
65
|
+
// The MCP server a tool belongs to: mcp_<server>__<tool> → "mcp_<server>". Tools
|
|
66
|
+
// without that shape are their own "server" (never grouped together).
|
|
67
|
+
function serverKey(n) {
|
|
68
|
+
const s = String(n || '');
|
|
69
|
+
const i = s.indexOf('__');
|
|
70
|
+
return i > 0 ? s.slice(0, i) : s;
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
export function narrowSpecs(specs, query, { cap = 0, keep, name = defName, description = defDesc } = {}) {
|
|
55
74
|
const list = specs || [];
|
|
56
75
|
if (!cap || cap < 1) return list;
|
|
57
76
|
const kept = keep ? list.filter(keep) : [];
|
|
58
77
|
const rest = keep ? list.filter((s) => !kept.includes(s)) : list;
|
|
59
78
|
if (rest.length <= cap) return list;
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
// SERVER-DIVERSE selection: rank all narrowable tools, then pick ROUND-ROBIN across
|
|
80
|
+
// servers — each server's best tool first, then seconds, … up to `cap`. This keeps
|
|
81
|
+
// a relevant server (e.g. wikipedia) from being crowded out of the top-K by another
|
|
82
|
+
// server that happens to have many tools. Servers are visited best-first (the order
|
|
83
|
+
// their top-ranked tool appears in the global ranking).
|
|
84
|
+
const ranked = rankToolSpecs(rest, query, { name, description });
|
|
85
|
+
const queues = new Map(); // serverKey -> [tools] in rank order (insertion = best-first)
|
|
86
|
+
for (const s of ranked) {
|
|
87
|
+
const k = serverKey(name(s));
|
|
88
|
+
if (!queues.has(k)) queues.set(k, []);
|
|
89
|
+
queues.get(k).push(s);
|
|
90
|
+
}
|
|
91
|
+
const lanes = [...queues.values()];
|
|
92
|
+
const chosen = new Set();
|
|
93
|
+
for (let round = 0; chosen.size < cap; round++) {
|
|
94
|
+
let advanced = false;
|
|
95
|
+
for (const lane of lanes) {
|
|
96
|
+
if (chosen.size >= cap) break;
|
|
97
|
+
if (lane.length > round) { chosen.add(lane[round]); advanced = true; }
|
|
98
|
+
}
|
|
99
|
+
if (!advanced) break;
|
|
100
|
+
}
|
|
101
|
+
return list.filter((s) => kept.includes(s) || chosen.has(s)); // preserve original order
|
|
62
102
|
}
|