@chatpanel/pii 0.1.1 → 0.2.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/index.js +2 -0
- package/package.json +4 -2
- package/tool-rank.js +62 -0
package/index.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// 'chatpanel-pii/pii-redact.js' deterministic redact/restore + vault
|
|
9
9
|
// 'chatpanel-pii/pii-detect.js' local NER / LLM entity detection
|
|
10
10
|
// 'chatpanel-pii/pipeline.js' pure turn orchestration + tier/scope gating
|
|
11
|
+
// 'chatpanel-pii/tool-rank.js' deterministic tool narrowing (auto mode)
|
|
11
12
|
|
|
12
13
|
export * from './pii-redact.js';
|
|
13
14
|
export * from './pii-detect.js';
|
|
14
15
|
export * from './pipeline.js';
|
|
16
|
+
export * from './tool-rank.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/pii",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./pii-redact.js": "./pii-redact.js",
|
|
10
10
|
"./pii-detect.js": "./pii-detect.js",
|
|
11
|
-
"./pipeline.js": "./pipeline.js"
|
|
11
|
+
"./pipeline.js": "./pipeline.js",
|
|
12
|
+
"./tool-rank.js": "./tool-rank.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"index.js",
|
|
15
16
|
"pii-redact.js",
|
|
16
17
|
"pii-detect.js",
|
|
17
18
|
"pipeline.js",
|
|
19
|
+
"tool-rank.js",
|
|
18
20
|
"LICENSE",
|
|
19
21
|
"README.md"
|
|
20
22
|
],
|
package/tool-rank.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Deterministic, model-free tool ranking — shared by the extension's side panel
|
|
2
|
+
// and the gateway, so "auto mode" narrows the same way everywhere (single source
|
|
3
|
+
// of truth, per the no-duplication rule).
|
|
4
|
+
//
|
|
5
|
+
// Ranks tool specs by lexical relevance to a query, weighting each query word by
|
|
6
|
+
// INVERSE DOCUMENT FREQUENCY across the toolset: a distinctive word like "wiki"
|
|
7
|
+
// (in 1–2 tools) counts far more than a common one like "search" (in many) — so
|
|
8
|
+
// "use wiki search" ranks the Wikipedia tool above generic search tools instead
|
|
9
|
+
// of tying them. Latency-sensitive: pure string ops, runs on every turn, no model
|
|
10
|
+
// call. Generic over the spec shape via name/description accessors (the extension
|
|
11
|
+
// uses { name, description }; the gateway uses OpenAI's { function: { name, … } }).
|
|
12
|
+
|
|
13
|
+
const STOP = new Set([
|
|
14
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'use', 'can', 'you', 'your', 'please',
|
|
15
|
+
'about', 'from', 'what', 'who', 'how', 'are', 'was', 'will', 'just', 'tell', 'find',
|
|
16
|
+
'get', 'into', 'them', 'they', 'their', 'name', 'one', 'but', 'not', 'all',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const defName = (s) => (s && s.name) || '';
|
|
20
|
+
const defDesc = (s) => (s && s.description) || '';
|
|
21
|
+
|
|
22
|
+
// Returns specs scored + sorted most-relevant first, as [{ s, i, n }] (i = original
|
|
23
|
+
// index, n = score). Stable for ties (preserves original order).
|
|
24
|
+
export function scoreToolSpecs(specs, query, { name = defName, description = defDesc } = {}) {
|
|
25
|
+
const q = String(query || '').toLowerCase();
|
|
26
|
+
const words = [...new Set(q.split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w)))];
|
|
27
|
+
const list = [...(specs || [])];
|
|
28
|
+
const names = list.map((s) => String(name(s) || '').toLowerCase());
|
|
29
|
+
const hays = list.map((s, i) => `${names[i]} ${String(description(s) || '').toLowerCase()}`);
|
|
30
|
+
const N = list.length || 1;
|
|
31
|
+
const df = {}; // how many tools mention each query word
|
|
32
|
+
for (const w of words) df[w] = hays.reduce((n, h) => n + (h.includes(w) ? 1 : 0), 0);
|
|
33
|
+
const idf = (w) => Math.log(1 + N / (1 + (df[w] || 0))); // rarer → higher weight
|
|
34
|
+
const score = (i) => {
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const w of words) if (hays[i].includes(w)) n += idf(w);
|
|
37
|
+
for (const part of names[i].split(/[^a-z0-9]+/)) {
|
|
38
|
+
if (part.length > 2 && q.includes(part)) n += 2 + idf(part); // tool explicitly named
|
|
39
|
+
}
|
|
40
|
+
return n;
|
|
41
|
+
};
|
|
42
|
+
return list.map((s, i) => ({ s, i, n: score(i) })).sort((a, b) => (b.n - a.n) || (a.i - b.i));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Rank tool specs most-relevant first (stable for ties).
|
|
46
|
+
export function rankToolSpecs(specs, query, accessors) {
|
|
47
|
+
return scoreToolSpecs(specs, query, accessors).map((x) => x.s);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Narrow a flat spec list to at most `cap` entries that DON'T match `keep`, always
|
|
51
|
+
// retaining everything that does (e.g. local page/history tools). `cap` therefore
|
|
52
|
+
// bounds the NARROWABLE (MCP) tools; kept tools ride along free. Returns the list
|
|
53
|
+
// unchanged when there's no cap or the narrowable set already fits.
|
|
54
|
+
export function narrowSpecs(specs, query, { cap = 0, keep, name = defName, description = defDesc } = {}) {
|
|
55
|
+
const list = specs || [];
|
|
56
|
+
if (!cap || cap < 1) return list;
|
|
57
|
+
const kept = keep ? list.filter(keep) : [];
|
|
58
|
+
const rest = keep ? list.filter((s) => !kept.includes(s)) : list;
|
|
59
|
+
if (rest.length <= cap) return list;
|
|
60
|
+
const top = new Set(rankToolSpecs(rest, query, { name, description }).slice(0, cap));
|
|
61
|
+
return list.filter((s) => kept.includes(s) || top.has(s)); // preserve original order
|
|
62
|
+
}
|