@chatpanel/pii 0.2.4 → 0.2.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/pii",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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/pii-detect.js CHANGED
@@ -116,7 +116,13 @@ async function detectViaEndpoint(text, det, signal, fetchImpl) {
116
116
 
117
117
  async function detectViaOpenAI(text, det, signal, fetchImpl) {
118
118
  const base = String(det.url || '').replace(/\/$/, '');
119
- const url = /\/chat\/completions$/.test(base) ? base : `${base}/v1/chat/completions`;
119
+ // Build the chat URL the SAME way the chat path does. An OpenAI-compatible baseUrl
120
+ // already ends in /v1 (Ollama, OpenRouter, NVIDIA, OpenAI…) → only add
121
+ // /chat/completions (appending /v1/chat/completions would 404 on /v1/v1/…). A bare
122
+ // host gets /v1/chat/completions; a full chat URL is used as-is.
123
+ const url = /\/chat\/completions$/.test(base) ? base
124
+ : /\/v\d+$/.test(base) ? `${base}/chat/completions`
125
+ : `${base}/v1/chat/completions`;
120
126
  const res = await fetchImpl(url, {
121
127
  method: 'POST',
122
128
  headers: { 'Content-Type': 'application/json', ...(det.apiKey ? { Authorization: `Bearer ${det.apiKey}` } : {}) },
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
- 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
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
  }