@konneal/engine 0.2.98 → 0.2.100
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/dist/{ask-4G24T25V.js → ask-KP42T6DH.js} +3 -3
- package/dist/{chunk-TXPWLKCI.js → chunk-BBKAETKM.js} +77 -42
- package/dist/{chunk-BIX3SMC7.js → chunk-GYCUMFOA.js} +2 -2
- package/dist/{chunk-GY33RTEO.js → chunk-OREMF5LK.js} +1 -1
- package/dist/{chunk-A3ELAHKF.js → chunk-QNLFNEI4.js} +137 -2
- package/dist/lexical.d.ts +12 -0
- package/dist/{search-EQAOPFCC.js → search-4CBKKNBJ.js} +2 -2
- package/dist/{tools-YHEXFX6V.js → tools-ZNQY6ZMC.js} +1 -1
- package/dist/tools-content.d.ts +2 -0
- package/dist/worker_public/src/index.js +6 -6
- package/package.json +1 -1
- package/workers/worker_public/src/codecs.ts +8 -1
- package/workers/worker_public/src/lexical.ts +84 -30
- package/workers/worker_public/src/stages/conceptGraph.ts +7 -6
- package/workers/worker_public/src/stages/graphLane.ts +13 -9
- package/workers/worker_public/src/tools-content.ts +173 -1
- package/workers/worker_public/src/tools.ts +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-GYCUMFOA.js";
|
|
4
|
+
import "./chunk-QNLFNEI4.js";
|
|
5
|
+
import "./chunk-BBKAETKM.js";
|
|
6
6
|
import "./chunk-KNCRYAX3.js";
|
|
7
7
|
import "./chunk-DTW4UW2E.js";
|
|
8
8
|
import "./chunk-A3QHHUN5.js";
|
|
@@ -114,6 +114,69 @@ var STOP = /* @__PURE__ */ new Set([
|
|
|
114
114
|
"their",
|
|
115
115
|
"there"
|
|
116
116
|
]);
|
|
117
|
+
function rowMeta(r) {
|
|
118
|
+
return {
|
|
119
|
+
doc_id: String(r.doc_id ?? ""),
|
|
120
|
+
docidentifier: String(r.docidentifier ?? ""),
|
|
121
|
+
doctype: String(r.doctype ?? ""),
|
|
122
|
+
doc_number: String(r.doc_number ?? ""),
|
|
123
|
+
edition: String(r.edition ?? ""),
|
|
124
|
+
language: String(r.language ?? "en"),
|
|
125
|
+
clause_anchor: String(r.clause_anchor ?? ""),
|
|
126
|
+
clause_title: String(r.clause_title ?? ""),
|
|
127
|
+
tier: String(r.tier ?? ""),
|
|
128
|
+
corpus: String(r.corpus ?? ""),
|
|
129
|
+
text_ref: "",
|
|
130
|
+
status: String(r.status ?? "unknown"),
|
|
131
|
+
superseded_by: String(r.superseded_by ?? ""),
|
|
132
|
+
// contract v2 over the lexical lane: typed chunks arriving via BM25
|
|
133
|
+
// keep their unit identity ([[u:…]] refs, typed pin, retyping check)
|
|
134
|
+
unit_id: String(r.unit_id ?? "") || void 0,
|
|
135
|
+
block: String(r.block ?? "") || void 0
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function rowsToHits(rows) {
|
|
139
|
+
return rows.map((r) => {
|
|
140
|
+
const bm25 = typeof r.rank === "number" ? r.rank : 0;
|
|
141
|
+
return {
|
|
142
|
+
id: String(r.id),
|
|
143
|
+
score: 1 / (1 + Math.max(0, bm25)),
|
|
144
|
+
metadata: rowMeta(r),
|
|
145
|
+
text: String(r.text ?? "")
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function rowsToMatches(rows) {
|
|
150
|
+
return rows.map((r) => {
|
|
151
|
+
const bm25 = typeof r.rank === "number" ? Math.max(0, r.rank) : 0;
|
|
152
|
+
return {
|
|
153
|
+
id: String(r.id),
|
|
154
|
+
score: 1 / (1 + bm25),
|
|
155
|
+
metadata: { ...rowMeta(r), chunk_text: String(r.text ?? "") }
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async function lexicalWithin(env, query, docNumbers, k = 12) {
|
|
160
|
+
const match2 = ftsMatchQuery(query);
|
|
161
|
+
if (!match2 || !docNumbers.length) return [];
|
|
162
|
+
try {
|
|
163
|
+
const placeholders = docNumbers.map((_, i) => `?${i + 2}`).join(",");
|
|
164
|
+
const res = await env.DB.prepare(
|
|
165
|
+
`SELECT c.id, c.doc_id, c.docidentifier, c.doctype, c.doc_number, c.edition,
|
|
166
|
+
c.language, c.clause_anchor, c.clause_title, c.status, c.superseded_by,
|
|
167
|
+
c.corpus, c.tier, c.text, c.unit_id, c.block, bm25(chunks_fts) AS rank
|
|
168
|
+
FROM chunks_fts
|
|
169
|
+
JOIN chunks c ON c.rowid = chunks_fts.rowid
|
|
170
|
+
WHERE chunks_fts MATCH ?1 AND c.doc_number IN (${placeholders})
|
|
171
|
+
ORDER BY rank
|
|
172
|
+
LIMIT ?${docNumbers.length + 2}`
|
|
173
|
+
).bind(match2, ...docNumbers, k).all();
|
|
174
|
+
return rowsToMatches(res.results ?? []);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.log("lexical-within failed:", String(e).slice(0, 200));
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
117
180
|
async function lexicalPrefilter(env, query, k = LEXICAL_K) {
|
|
118
181
|
const match2 = ftsMatchQuery(query);
|
|
119
182
|
if (!match2) return [];
|
|
@@ -129,34 +192,7 @@ async function lexicalPrefilter(env, query, k = LEXICAL_K) {
|
|
|
129
192
|
LIMIT ?2`
|
|
130
193
|
).bind(match2, k).all();
|
|
131
194
|
const rows = res.results ?? [];
|
|
132
|
-
return rows
|
|
133
|
-
const meta = {
|
|
134
|
-
doc_id: String(r.doc_id ?? ""),
|
|
135
|
-
docidentifier: String(r.docidentifier ?? ""),
|
|
136
|
-
doctype: String(r.doctype ?? ""),
|
|
137
|
-
doc_number: String(r.doc_number ?? ""),
|
|
138
|
-
edition: String(r.edition ?? ""),
|
|
139
|
-
language: String(r.language ?? "en"),
|
|
140
|
-
clause_anchor: String(r.clause_anchor ?? ""),
|
|
141
|
-
clause_title: String(r.clause_title ?? ""),
|
|
142
|
-
tier: String(r.tier ?? ""),
|
|
143
|
-
corpus: String(r.corpus ?? ""),
|
|
144
|
-
text_ref: "",
|
|
145
|
-
status: String(r.status ?? "unknown"),
|
|
146
|
-
superseded_by: String(r.superseded_by ?? ""),
|
|
147
|
-
// contract v2 over the lexical lane: typed chunks arriving via BM25
|
|
148
|
-
// keep their unit identity ([[u:…]] refs, typed pin, retyping check)
|
|
149
|
-
unit_id: String(r.unit_id ?? "") || void 0,
|
|
150
|
-
block: String(r.block ?? "") || void 0
|
|
151
|
-
};
|
|
152
|
-
const bm25 = typeof r.rank === "number" ? r.rank : i;
|
|
153
|
-
return {
|
|
154
|
-
id: String(r.id),
|
|
155
|
-
score: 1 / (1 + Math.max(0, bm25)),
|
|
156
|
-
metadata: meta,
|
|
157
|
-
text: String(r.text ?? "")
|
|
158
|
-
};
|
|
159
|
-
});
|
|
195
|
+
return rowsToHits(rows);
|
|
160
196
|
} catch (e) {
|
|
161
197
|
console.log("lexical prefilter failed:", String(e).slice(0, 200));
|
|
162
198
|
return [];
|
|
@@ -21935,7 +21971,14 @@ var oimlPubid = {
|
|
|
21935
21971
|
var plainSlug = {
|
|
21936
21972
|
parse: () => null,
|
|
21937
21973
|
scanQuestion: () => null,
|
|
21938
|
-
|
|
21974
|
+
// the graph node id scheme (doc:OIML-<TYPE>-<NUM>-<EDITION>) is the
|
|
21975
|
+
// GRAPH's own identity, estate-wide — not publisher-flavored — so the
|
|
21976
|
+
// plain codec parses it too (a null here silenced the concept-graph
|
|
21977
|
+
// lane's resolution for its whole life)
|
|
21978
|
+
graphDocNumber: (nodeId) => {
|
|
21979
|
+
const m = nodeId.match(/^doc:OIML-[A-Z]-(\d+)-/);
|
|
21980
|
+
return m ? m[1] : null;
|
|
21981
|
+
},
|
|
21939
21982
|
familyOf: () => null
|
|
21940
21983
|
};
|
|
21941
21984
|
var REGISTRY = {
|
|
@@ -22477,14 +22520,10 @@ var conceptGraph = {
|
|
|
22477
22520
|
}
|
|
22478
22521
|
}
|
|
22479
22522
|
if (numbers.size) {
|
|
22480
|
-
const gc = await c.env.
|
|
22481
|
-
topK: 12,
|
|
22482
|
-
returnMetadata: "all",
|
|
22483
|
-
filter: { doc_number: { $in: [...numbers] } }
|
|
22484
|
-
});
|
|
22523
|
+
const gc = await lexicalWithin(c.env, c.rq || c.query, [...numbers], 12);
|
|
22485
22524
|
const seenIds0 = new Set(c.matches.map((m) => m.id));
|
|
22486
22525
|
let merged0 = 0;
|
|
22487
|
-
for (const m of
|
|
22526
|
+
for (const m of gc.slice(0, 6)) {
|
|
22488
22527
|
if (!seenIds0.has(m.id)) {
|
|
22489
22528
|
c.matches.push({ id: m.id, score: m.score * THRESHOLDS.conceptGraphDiscount, metadata: m.metadata });
|
|
22490
22529
|
seenIds0.add(m.id);
|
|
@@ -22502,24 +22541,20 @@ var graphLane = {
|
|
|
22502
22541
|
failure: "additive",
|
|
22503
22542
|
when: (c) => !!c.opts.graphDocNumbers?.length && c.vector.length > 0,
|
|
22504
22543
|
prefetch: (c) => {
|
|
22505
|
-
c.lane["graph-lane"] = c.env.
|
|
22506
|
-
topK: 15,
|
|
22507
|
-
returnMetadata: "all",
|
|
22508
|
-
filter: { doc_number: { $in: c.opts.graphDocNumbers } }
|
|
22509
|
-
});
|
|
22544
|
+
c.lane["graph-lane"] = lexicalWithin(c.env, c.rq || c.query, c.opts.graphDocNumbers, 15);
|
|
22510
22545
|
},
|
|
22511
22546
|
run: async (c) => {
|
|
22512
22547
|
const g = await c.lane["graph-lane"];
|
|
22513
22548
|
const seenIds = new Set(c.matches.map((m) => m.id));
|
|
22514
22549
|
let merged = 0;
|
|
22515
|
-
for (const m of (g
|
|
22550
|
+
for (const m of (g ?? []).slice(0, 10)) {
|
|
22516
22551
|
if (!seenIds.has(m.id)) {
|
|
22517
22552
|
c.matches.push({ id: m.id, score: m.score * THRESHOLDS.graphLaneDiscount, metadata: m.metadata });
|
|
22518
22553
|
seenIds.add(m.id);
|
|
22519
22554
|
merged++;
|
|
22520
22555
|
}
|
|
22521
22556
|
}
|
|
22522
|
-
console.log("graph lane:", g
|
|
22557
|
+
console.log("graph lane:", g?.length ?? 0, "hits,", merged, "merged");
|
|
22523
22558
|
}
|
|
22524
22559
|
};
|
|
22525
22560
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
tableRetyped,
|
|
19
19
|
toolNote,
|
|
20
20
|
verdictNote
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-QNLFNEI4.js";
|
|
22
22
|
import {
|
|
23
23
|
NO_CONTEXT,
|
|
24
24
|
answerQuality,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
tokenBudget,
|
|
57
57
|
understandQuery,
|
|
58
58
|
usageTotal
|
|
59
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-BBKAETKM.js";
|
|
60
60
|
import {
|
|
61
61
|
embed,
|
|
62
62
|
generateOnce,
|
|
@@ -2,7 +2,8 @@ import {
|
|
|
2
2
|
embed,
|
|
3
3
|
hasLane,
|
|
4
4
|
portIndex,
|
|
5
|
-
portModelRunner
|
|
5
|
+
portModelRunner,
|
|
6
|
+
portStore
|
|
6
7
|
} from "./chunk-KNCRYAX3.js";
|
|
7
8
|
import {
|
|
8
9
|
standardKeysFrom
|
|
@@ -920,6 +921,140 @@ var bibEntry = {
|
|
|
920
921
|
}
|
|
921
922
|
}
|
|
922
923
|
};
|
|
924
|
+
async function unitByTerm(store, term) {
|
|
925
|
+
const t = term.trim();
|
|
926
|
+
if (!t) return null;
|
|
927
|
+
const q = (where, bind) => store.prepare(`SELECT * FROM units_db WHERE ${where} LIMIT 1`).bind(...bind).all().catch(() => ({ results: [] }));
|
|
928
|
+
const exact = await q("code = ?1 OR short = ?1 OR LOWER(name_en) = LOWER(?1)", [t]);
|
|
929
|
+
let rows = exact.results ?? [];
|
|
930
|
+
if (rows.length) return rows[0];
|
|
931
|
+
const sym = await q("LOWER(json_extract(symbols, '$.ascii')) = LOWER(?1)", [t]);
|
|
932
|
+
rows = sym.results ?? [];
|
|
933
|
+
if (rows.length) return rows[0];
|
|
934
|
+
const like = await q("LOWER(short) LIKE LOWER(?1) OR LOWER(name_en) LIKE LOWER(?1)", [`%${t}%`]);
|
|
935
|
+
rows = like.results ?? [];
|
|
936
|
+
return rows[0] ?? null;
|
|
937
|
+
}
|
|
938
|
+
async function resolveWithPrefix(store, term) {
|
|
939
|
+
const direct = await unitByTerm(store, term);
|
|
940
|
+
if (direct) return { unit: direct, prefixFactor: 1, prefixName: null };
|
|
941
|
+
const t = term.trim();
|
|
942
|
+
for (let cut = 1; cut < Math.min(t.length, 3); cut++) {
|
|
943
|
+
const pSym = t.slice(0, cut);
|
|
944
|
+
const rest = t.slice(cut);
|
|
945
|
+
if (!rest) continue;
|
|
946
|
+
const pres = await store.prepare("SELECT id, name_en, symbol, base, power FROM unit_prefixes WHERE symbol = ?1 AND base IS NOT NULL AND power IS NOT NULL").bind(pSym).all().catch(() => ({ results: [] }));
|
|
947
|
+
const prefix = (pres.results ?? [])[0];
|
|
948
|
+
if (!prefix) continue;
|
|
949
|
+
const unit = await unitByTerm(store, rest);
|
|
950
|
+
if (unit) return { unit, prefixFactor: Math.pow(prefix.base, prefix.power), prefixName: prefix.name_en };
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
var unitsLookup = {
|
|
955
|
+
name: "units.lookup",
|
|
956
|
+
description: "Look up a measurement unit in the unitsdb plane: its names and symbols, its dimension, its quantity kinds, and its SI decomposition. Bind a symbol or an everyday unit name to the dataset's canonical unit before computing with it.",
|
|
957
|
+
params: [{ key: "term", required: true, description: "a unit's code, short name, symbol or plain name, e.g. m, Pa, pascal, meter per second" }],
|
|
958
|
+
audiences: ["mcp"],
|
|
959
|
+
handler: async (env, args) => {
|
|
960
|
+
const term = String(args?.term ?? "").trim();
|
|
961
|
+
if (term.length < 1) return null;
|
|
962
|
+
try {
|
|
963
|
+
const store = portStore(env);
|
|
964
|
+
const unit = await unitByTerm(store, term);
|
|
965
|
+
if (!unit) {
|
|
966
|
+
return { name: "units.lookup", query: term, output: `No unit matches "${term}" in the unitsdb plane. State this plainly.` };
|
|
967
|
+
}
|
|
968
|
+
const qtyNames = [];
|
|
969
|
+
for (const qid of JSON.parse(unit.quantity_ids || "[]")) {
|
|
970
|
+
const r = await store.prepare("SELECT name_en FROM unit_quantities WHERE id = ?1").bind(qid).all().catch(() => ({ results: [] }));
|
|
971
|
+
const n = (r.results ?? [])[0]?.name_en;
|
|
972
|
+
if (n) qtyNames.push(n);
|
|
973
|
+
}
|
|
974
|
+
const roots = JSON.parse(unit.root_units || "[]");
|
|
975
|
+
const rootNames = await Promise.all(
|
|
976
|
+
roots.map(async (r) => {
|
|
977
|
+
const row = await unitByTerm(store, r.code);
|
|
978
|
+
return `${row?.short ?? r.code}^${r.power}`;
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
const symbols = JSON.parse(unit.symbols || "null");
|
|
982
|
+
return {
|
|
983
|
+
name: "units.lookup",
|
|
984
|
+
query: term,
|
|
985
|
+
output: JSON.stringify({
|
|
986
|
+
code: unit.code,
|
|
987
|
+
name_en: unit.name_en,
|
|
988
|
+
name_fr: unit.name_fr,
|
|
989
|
+
symbol: symbols?.ascii ?? null,
|
|
990
|
+
si_root: !!unit.root,
|
|
991
|
+
unit_system: unit.unit_system,
|
|
992
|
+
dimension: unit.dimension_ascii,
|
|
993
|
+
quantities: qtyNames.slice(0, 4),
|
|
994
|
+
si_decomposition: rootNames,
|
|
995
|
+
scale: unit.scale
|
|
996
|
+
})
|
|
997
|
+
};
|
|
998
|
+
} catch {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var unitsConvert = {
|
|
1004
|
+
name: "units.convert",
|
|
1005
|
+
description: "Convert a value between two measurement units through the unitsdb plane. SI-coherent units convert exactly (prefix powers over the same dimension); a non-SI unit is stated honestly \u2014 the dataset defines a ratio scale for it, not a factor, and none is invented.",
|
|
1006
|
+
params: [
|
|
1007
|
+
{ key: "value", required: true, description: "the numeric value to convert" },
|
|
1008
|
+
{ key: "from_unit", required: true, description: "the source unit, e.g. kPa, km, m" },
|
|
1009
|
+
{ key: "to_unit", required: true, description: "the target unit, e.g. Pa, m" }
|
|
1010
|
+
],
|
|
1011
|
+
audiences: ["mcp"],
|
|
1012
|
+
handler: async (env, args) => {
|
|
1013
|
+
const value = Number(args?.value);
|
|
1014
|
+
const fromTerm = String(args?.from_unit ?? "").trim();
|
|
1015
|
+
const toTerm = String(args?.to_unit ?? "").trim();
|
|
1016
|
+
if (!Number.isFinite(value) || !fromTerm || !toTerm) return null;
|
|
1017
|
+
try {
|
|
1018
|
+
const store = portStore(env);
|
|
1019
|
+
const from = await resolveWithPrefix(store, fromTerm);
|
|
1020
|
+
const to = await resolveWithPrefix(store, toTerm);
|
|
1021
|
+
if (!from || !to) {
|
|
1022
|
+
const missing = !from ? fromTerm : toTerm;
|
|
1023
|
+
return { name: "units.convert", query: `${value} ${fromTerm} \u2192 ${toTerm}`, output: `No unit matches "${missing}" in the unitsdb plane. State this plainly.` };
|
|
1024
|
+
}
|
|
1025
|
+
if ((from.unit.dimension_ascii ?? "") !== (to.unit.dimension_ascii ?? "")) {
|
|
1026
|
+
return {
|
|
1027
|
+
name: "units.convert",
|
|
1028
|
+
query: `${value} ${fromTerm} \u2192 ${toTerm}`,
|
|
1029
|
+
output: `Not convertible: ${fromTerm} carries dimension ${from.unit.dimension_ascii ?? "unknown"} and ${toTerm} carries ${to.unit.dimension_ascii ?? "unknown"}. State this plainly.`
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const coherent = (u) => !!u.unit_system && u.unit_system.toLowerCase().startsWith("si");
|
|
1033
|
+
if (!coherent(from.unit) || !coherent(to.unit)) {
|
|
1034
|
+
const nonSi = !coherent(from.unit) ? fromTerm : toTerm;
|
|
1035
|
+
return {
|
|
1036
|
+
name: "units.convert",
|
|
1037
|
+
query: `${value} ${fromTerm} \u2192 ${toTerm}`,
|
|
1038
|
+
output: `The dataset defines no numeric factor for ${nonSi} (a non-SI unit carried on a ${from.unit.scale ?? to.unit.scale ?? "ratio"} scale) \u2014 no factor is invented. State this plainly; the caller may supply the factor itself.`
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
const result = value * from.prefixFactor / to.prefixFactor;
|
|
1042
|
+
return {
|
|
1043
|
+
name: "units.convert",
|
|
1044
|
+
query: `${value} ${fromTerm} \u2192 ${toTerm}`,
|
|
1045
|
+
output: JSON.stringify({
|
|
1046
|
+
value,
|
|
1047
|
+
from: `${from.prefixName ? from.prefixName + " " : ""}${from.unit.name_en}`,
|
|
1048
|
+
to: `${to.prefixName ? to.prefixName + " " : ""}${to.unit.name_en}`,
|
|
1049
|
+
dimension: from.unit.dimension_ascii,
|
|
1050
|
+
result
|
|
1051
|
+
})
|
|
1052
|
+
};
|
|
1053
|
+
} catch {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
923
1058
|
var glossaryLookup = {
|
|
924
1059
|
name: "glossary.lookup",
|
|
925
1060
|
description: "Look up a defined term in the terminology datasets \u2014 the concept's definition and its defining publication. Bind everyday words to the defined term before interpreting a question.",
|
|
@@ -988,7 +1123,7 @@ var certificatesSearch = {
|
|
|
988
1123
|
return { name: "certificates.search", query, output };
|
|
989
1124
|
}
|
|
990
1125
|
};
|
|
991
|
-
var TOOLS_REGISTRY = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry];
|
|
1126
|
+
var TOOLS_REGISTRY = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert];
|
|
992
1127
|
var TOOL_DECLARATION = [
|
|
993
1128
|
"You may use one tool before answering, by writing a single line:",
|
|
994
1129
|
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
package/dist/lexical.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ import type { Hit } from "./pipeline";
|
|
|
2
2
|
/** Build a safe FTS5 MATCH query from user text: alphanumeric tokens,
|
|
3
3
|
* joined with OR so jargon hits don't require full-phrase match. */
|
|
4
4
|
export declare function ftsMatchQuery(query: string): string | null;
|
|
5
|
+
/** Lexical ranking WITHIN a document set (the graph lanes' identity
|
|
6
|
+
* resolution): BM25 over the corpus's own FTS, restricted to the
|
|
7
|
+
* documents' doc_number values. This is the working substitute for
|
|
8
|
+
* Vectorize metadata filtering, which is dead on this index (measured
|
|
9
|
+
* 2026-10-05: filters return empty for values proven present). */
|
|
10
|
+
export declare function lexicalWithin(env: {
|
|
11
|
+
DB: D1Database;
|
|
12
|
+
}, query: string, docNumbers: string[], k?: number): Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
score: number;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
}[]>;
|
|
5
17
|
export declare function lexicalPrefilter(env: {
|
|
6
18
|
DB: D1Database;
|
|
7
19
|
}, query: string, k?: number): Promise<Hit[]>;
|
package/dist/tools-content.d.ts
CHANGED
|
@@ -7,4 +7,6 @@ export declare const licensedSection: ToolSpec;
|
|
|
7
7
|
export declare const verdictEvaluate: ToolSpec;
|
|
8
8
|
export declare const conditionsCheck: ToolSpec;
|
|
9
9
|
export declare const bibEntry: ToolSpec;
|
|
10
|
+
export declare const unitsLookup: ToolSpec;
|
|
11
|
+
export declare const unitsConvert: ToolSpec;
|
|
10
12
|
export declare const glossaryLookup: ToolSpec;
|
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-GYCUMFOA.js";
|
|
8
8
|
import {
|
|
9
9
|
TOOLS_REGISTRY,
|
|
10
10
|
bindModelNode,
|
|
11
11
|
modelGroundingBlock
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-QNLFNEI4.js";
|
|
13
13
|
import {
|
|
14
14
|
handleSearch
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-OREMF5LK.js";
|
|
16
16
|
import {
|
|
17
17
|
buildMessages,
|
|
18
18
|
citations,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
sessionFrom,
|
|
35
35
|
telemetry,
|
|
36
36
|
understandQuery
|
|
37
|
-
} from "../../chunk-
|
|
37
|
+
} from "../../chunk-BBKAETKM.js";
|
|
38
38
|
import {
|
|
39
39
|
cfBlobs,
|
|
40
40
|
embed,
|
|
@@ -990,13 +990,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
990
990
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
991
991
|
body: JSON.stringify({ ...args, stream: false })
|
|
992
992
|
});
|
|
993
|
-
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-
|
|
993
|
+
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-ZNQY6ZMC.js");
|
|
994
994
|
let payload;
|
|
995
995
|
if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
996
996
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
997
997
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
998
998
|
} else {
|
|
999
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
999
|
+
const res = name === "ask" ? await (await import("../../ask-KP42T6DH.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-4CBKKNBJ.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1000
1000
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1001
1001
|
}
|
|
1002
1002
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.100",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -142,7 +142,14 @@ export const oimlPubid: RefCodec = {
|
|
|
142
142
|
export const plainSlug: RefCodec = {
|
|
143
143
|
parse: () => null,
|
|
144
144
|
scanQuestion: () => null,
|
|
145
|
-
|
|
145
|
+
// the graph node id scheme (doc:OIML-<TYPE>-<NUM>-<EDITION>) is the
|
|
146
|
+
// GRAPH's own identity, estate-wide — not publisher-flavored — so the
|
|
147
|
+
// plain codec parses it too (a null here silenced the concept-graph
|
|
148
|
+
// lane's resolution for its whole life)
|
|
149
|
+
graphDocNumber: (nodeId) => {
|
|
150
|
+
const m = nodeId.match(/^doc:OIML-[A-Z]-(\d+)-/);
|
|
151
|
+
return m ? m[1] : null;
|
|
152
|
+
},
|
|
146
153
|
familyOf: () => null,
|
|
147
154
|
};
|
|
148
155
|
|
|
@@ -31,6 +31,89 @@ const STOP = new Set([
|
|
|
31
31
|
"about", "into", "than", "then", "its", "it", "their", "there",
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
|
+
function rowMeta(r: any): ChunkMeta {
|
|
35
|
+
return {
|
|
36
|
+
doc_id: String(r.doc_id ?? ""),
|
|
37
|
+
docidentifier: String(r.docidentifier ?? ""),
|
|
38
|
+
doctype: String(r.doctype ?? ""),
|
|
39
|
+
doc_number: String(r.doc_number ?? ""),
|
|
40
|
+
edition: String(r.edition ?? ""),
|
|
41
|
+
language: String(r.language ?? "en"),
|
|
42
|
+
clause_anchor: String(r.clause_anchor ?? ""),
|
|
43
|
+
clause_title: String(r.clause_title ?? ""),
|
|
44
|
+
tier: String(r.tier ?? ""),
|
|
45
|
+
corpus: String(r.corpus ?? ""),
|
|
46
|
+
text_ref: "",
|
|
47
|
+
status: String(r.status ?? "unknown"),
|
|
48
|
+
superseded_by: String(r.superseded_by ?? ""),
|
|
49
|
+
// contract v2 over the lexical lane: typed chunks arriving via BM25
|
|
50
|
+
// keep their unit identity ([[u:…]] refs, typed pin, retyping check)
|
|
51
|
+
unit_id: String(r.unit_id ?? "") || undefined,
|
|
52
|
+
block: String(r.block ?? "") || undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rowsToHits(rows: any[]): Hit[] {
|
|
57
|
+
return rows.map((r: any) => {
|
|
58
|
+
// rank is bm25 (lower better) → convert to positive score that RRF can ignore
|
|
59
|
+
// (RRF uses rank position, not score). score kept for logging only.
|
|
60
|
+
const bm25 = typeof r.rank === "number" ? r.rank : 0;
|
|
61
|
+
return {
|
|
62
|
+
id: String(r.id),
|
|
63
|
+
score: 1 / (1 + Math.max(0, bm25)),
|
|
64
|
+
metadata: rowMeta(r),
|
|
65
|
+
text: String(r.text ?? ""),
|
|
66
|
+
} satisfies Hit;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Lexical matches for the lane merges: match-shaped, with the text
|
|
71
|
+
* riding the metadata (toHits reads chunk_text out of it). */
|
|
72
|
+
function rowsToMatches(rows: any[]): { id: string; score: number; metadata: Record<string, unknown> }[] {
|
|
73
|
+
return rows.map((r: any) => {
|
|
74
|
+
const bm25 = typeof r.rank === "number" ? Math.max(0, r.rank) : 0;
|
|
75
|
+
return {
|
|
76
|
+
id: String(r.id),
|
|
77
|
+
score: 1 / (1 + bm25),
|
|
78
|
+
metadata: { ...rowMeta(r), chunk_text: String(r.text ?? "") },
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Lexical ranking WITHIN a document set (the graph lanes' identity
|
|
84
|
+
* resolution): BM25 over the corpus's own FTS, restricted to the
|
|
85
|
+
* documents' doc_number values. This is the working substitute for
|
|
86
|
+
* Vectorize metadata filtering, which is dead on this index (measured
|
|
87
|
+
* 2026-10-05: filters return empty for values proven present). */
|
|
88
|
+
export async function lexicalWithin(
|
|
89
|
+
env: { DB: D1Database },
|
|
90
|
+
query: string,
|
|
91
|
+
docNumbers: string[],
|
|
92
|
+
k = 12,
|
|
93
|
+
): Promise<{ id: string; score: number; metadata: Record<string, unknown> }[]> {
|
|
94
|
+
const match = ftsMatchQuery(query);
|
|
95
|
+
if (!match || !docNumbers.length) return [];
|
|
96
|
+
try {
|
|
97
|
+
const placeholders = docNumbers.map((_, i) => `?${i + 2}`).join(",");
|
|
98
|
+
const res = await env.DB.prepare(
|
|
99
|
+
`SELECT c.id, c.doc_id, c.docidentifier, c.doctype, c.doc_number, c.edition,
|
|
100
|
+
c.language, c.clause_anchor, c.clause_title, c.status, c.superseded_by,
|
|
101
|
+
c.corpus, c.tier, c.text, c.unit_id, c.block, bm25(chunks_fts) AS rank
|
|
102
|
+
FROM chunks_fts
|
|
103
|
+
JOIN chunks c ON c.rowid = chunks_fts.rowid
|
|
104
|
+
WHERE chunks_fts MATCH ?1 AND c.doc_number IN (${placeholders})
|
|
105
|
+
ORDER BY rank
|
|
106
|
+
LIMIT ?${docNumbers.length + 2}`,
|
|
107
|
+
)
|
|
108
|
+
.bind(match, ...docNumbers, k)
|
|
109
|
+
.all();
|
|
110
|
+
return rowsToMatches(res.results ?? []);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.log("lexical-within failed:", String(e).slice(0, 200));
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
34
117
|
export async function lexicalPrefilter(env: { DB: D1Database }, query: string, k = LEXICAL_K): Promise<Hit[]> {
|
|
35
118
|
const match = ftsMatchQuery(query);
|
|
36
119
|
if (!match) return [];
|
|
@@ -49,36 +132,7 @@ export async function lexicalPrefilter(env: { DB: D1Database }, query: string, k
|
|
|
49
132
|
.bind(match, k)
|
|
50
133
|
.all();
|
|
51
134
|
const rows = res.results ?? [];
|
|
52
|
-
return rows
|
|
53
|
-
const meta: ChunkMeta = {
|
|
54
|
-
doc_id: String(r.doc_id ?? ""),
|
|
55
|
-
docidentifier: String(r.docidentifier ?? ""),
|
|
56
|
-
doctype: String(r.doctype ?? ""),
|
|
57
|
-
doc_number: String(r.doc_number ?? ""),
|
|
58
|
-
edition: String(r.edition ?? ""),
|
|
59
|
-
language: String(r.language ?? "en"),
|
|
60
|
-
clause_anchor: String(r.clause_anchor ?? ""),
|
|
61
|
-
clause_title: String(r.clause_title ?? ""),
|
|
62
|
-
tier: String(r.tier ?? ""),
|
|
63
|
-
corpus: String(r.corpus ?? ""),
|
|
64
|
-
text_ref: "",
|
|
65
|
-
status: String(r.status ?? "unknown"),
|
|
66
|
-
superseded_by: String(r.superseded_by ?? ""),
|
|
67
|
-
// contract v2 over the lexical lane: typed chunks arriving via BM25
|
|
68
|
-
// keep their unit identity ([[u:…]] refs, typed pin, retyping check)
|
|
69
|
-
unit_id: String(r.unit_id ?? "") || undefined,
|
|
70
|
-
block: String(r.block ?? "") || undefined,
|
|
71
|
-
};
|
|
72
|
-
// rank is bm25 (lower better) → convert to positive score that RRF can ignore
|
|
73
|
-
// (RRF uses rank position, not score). score kept for logging only.
|
|
74
|
-
const bm25 = typeof r.rank === "number" ? r.rank : i;
|
|
75
|
-
return {
|
|
76
|
-
id: String(r.id),
|
|
77
|
-
score: 1 / (1 + Math.max(0, bm25)),
|
|
78
|
-
metadata: meta,
|
|
79
|
-
text: String(r.text ?? ""),
|
|
80
|
-
} satisfies Hit;
|
|
81
|
-
});
|
|
135
|
+
return rowsToHits(rows);
|
|
82
136
|
} catch (e) {
|
|
83
137
|
console.log("lexical prefilter failed:", String(e).slice(0, 200));
|
|
84
138
|
return [];
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { THRESHOLDS } from "../config.ts";
|
|
21
21
|
import type { Stage } from "./types.ts";
|
|
22
22
|
import { refCodec } from "../codecs.ts";
|
|
23
|
+
import { lexicalWithin } from "../lexical.ts";
|
|
23
24
|
|
|
24
25
|
export const conceptGraph: Stage = {
|
|
25
26
|
name: "concept-graph",
|
|
@@ -49,14 +50,14 @@ export const conceptGraph: Stage = {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
if (numbers.size) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// identity resolution: the linked documents' query-relevant
|
|
54
|
+
// candidates rank by the corpus's own BM25 within the set — the
|
|
55
|
+
// index filter this lane used returned empty for its whole life
|
|
56
|
+
// (Vectorize metadata filtering is dead on this index)
|
|
57
|
+
const gc = await lexicalWithin(c.env, c.rq || c.query, [...numbers], 12);
|
|
57
58
|
const seenIds0 = new Set(c.matches.map((m: any) => m.id));
|
|
58
59
|
let merged0 = 0;
|
|
59
|
-
for (const m of
|
|
60
|
+
for (const m of gc.slice(0, 6)) {
|
|
60
61
|
if (!seenIds0.has(m.id)) {
|
|
61
62
|
c.matches.push({ id: m.id, score: m.score * THRESHOLDS.conceptGraphDiscount, metadata: m.metadata });
|
|
62
63
|
seenIds0.add(m.id);
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Graph lane (additive): the caller-resolved document numbers (relaton
|
|
2
|
-
// family/successor resolution done in index.ts) merge their
|
|
3
|
-
// candidates at a discount.
|
|
2
|
+
// family/successor resolution done in index.ts) merge their
|
|
3
|
+
// query-relevant candidates at a discount.
|
|
4
|
+
//
|
|
5
|
+
// Identity resolution, not index filtering: the candidates rank by the
|
|
6
|
+
// corpus's own BM25 WITHIN the document set (lexicalWithin) — Vectorize
|
|
7
|
+
// metadata filtering is dead on this index (measured 2026-10-05:
|
|
8
|
+
// filters return empty for values proven present), so the filter this
|
|
9
|
+
// lane used for its whole life merged nothing. The lexical rows carry
|
|
10
|
+
// their own text and metadata; no index round trip is needed at all.
|
|
4
11
|
import { THRESHOLDS } from "../config.ts";
|
|
12
|
+
import { lexicalWithin } from "../lexical.ts";
|
|
5
13
|
import type { Stage } from "./types.ts";
|
|
6
14
|
|
|
7
15
|
export const graphLane: Stage = {
|
|
@@ -9,23 +17,19 @@ export const graphLane: Stage = {
|
|
|
9
17
|
failure: "additive",
|
|
10
18
|
when: (c) => !!c.opts.graphDocNumbers?.length && c.vector.length > 0,
|
|
11
19
|
prefetch: (c) => {
|
|
12
|
-
c.lane["graph-lane"] = c.env.
|
|
13
|
-
topK: 15,
|
|
14
|
-
returnMetadata: "all",
|
|
15
|
-
filter: { doc_number: { $in: c.opts.graphDocNumbers } },
|
|
16
|
-
});
|
|
20
|
+
c.lane["graph-lane"] = lexicalWithin(c.env, c.rq || c.query, c.opts.graphDocNumbers!, 15);
|
|
17
21
|
},
|
|
18
22
|
run: async (c) => {
|
|
19
23
|
const g = (await c.lane["graph-lane"]!) as any;
|
|
20
24
|
const seenIds = new Set(c.matches.map((m: any) => m.id));
|
|
21
25
|
let merged = 0;
|
|
22
|
-
for (const m of (g
|
|
26
|
+
for (const m of (g ?? []).slice(0, 10)) {
|
|
23
27
|
if (!seenIds.has(m.id)) {
|
|
24
28
|
c.matches.push({ id: m.id, score: m.score * THRESHOLDS.graphLaneDiscount, metadata: m.metadata });
|
|
25
29
|
seenIds.add(m.id);
|
|
26
30
|
merged++;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
console.log("graph lane:", g
|
|
33
|
+
console.log("graph lane:", g?.length ?? 0, "hits,", merged, "merged");
|
|
30
34
|
},
|
|
31
35
|
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// tool-selection quality degrades with menu size.
|
|
8
8
|
import type { ToolSpec } from "./tools.ts";
|
|
9
9
|
import { resolveBlocks } from "./refs.ts";
|
|
10
|
-
import { portIndex, portModelRunner, hasLane } from "./env.ts";
|
|
10
|
+
import { portIndex, portModelRunner, hasLane, portStore } from "./env.ts";
|
|
11
11
|
import { embed } from "./ai.ts";
|
|
12
12
|
import { THRESHOLDS } from "./config.ts";
|
|
13
13
|
import { standardKeysFrom } from "./requestScope.ts";
|
|
@@ -428,6 +428,178 @@ export const bibEntry: ToolSpec = {
|
|
|
428
428
|
},
|
|
429
429
|
};
|
|
430
430
|
|
|
431
|
+
|
|
432
|
+
// The unitsdb plane (the owner's 2026-10-05 direction): measurement
|
|
433
|
+
// units as machine-shaped data — the dataset's typed projection in D1
|
|
434
|
+
// (units_db / unit_quantities / unit_prefixes), served as tools agents
|
|
435
|
+
// compute with. Conversion is honest about its scope: SI-coherent
|
|
436
|
+
// units convert through prefix powers; the dataset defines no numeric
|
|
437
|
+
// factors for non-SI units (foot, pound) — a ratio scale is not a
|
|
438
|
+
// factor — and the tool says so instead of inventing one.
|
|
439
|
+
|
|
440
|
+
interface UnitRow {
|
|
441
|
+
code: string;
|
|
442
|
+
short: string | null;
|
|
443
|
+
name_en: string | null;
|
|
444
|
+
name_fr: string | null;
|
|
445
|
+
root: number;
|
|
446
|
+
symbols: string | null;
|
|
447
|
+
root_units: string | null;
|
|
448
|
+
dimension_id: string | null;
|
|
449
|
+
dimension_ascii: string | null;
|
|
450
|
+
quantity_ids: string | null;
|
|
451
|
+
unit_system: string | null;
|
|
452
|
+
scale: string | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function unitByTerm(store: any, term: string): Promise<UnitRow | null> {
|
|
456
|
+
const t = term.trim();
|
|
457
|
+
if (!t) return null;
|
|
458
|
+
const q = (where: string, bind: unknown[]) =>
|
|
459
|
+
store.prepare(`SELECT * FROM units_db WHERE ${where} LIMIT 1`).bind(...bind).all().catch(() => ({ results: [] }));
|
|
460
|
+
const exact = await q("code = ?1 OR short = ?1 OR LOWER(name_en) = LOWER(?1)", [t]);
|
|
461
|
+
let rows = (exact.results ?? []) as UnitRow[];
|
|
462
|
+
if (rows.length) return rows[0]!;
|
|
463
|
+
const sym = await q("LOWER(json_extract(symbols, '$.ascii')) = LOWER(?1)", [t]);
|
|
464
|
+
rows = (sym.results ?? []) as UnitRow[];
|
|
465
|
+
if (rows.length) return rows[0]!;
|
|
466
|
+
const like = await q("LOWER(short) LIKE LOWER(?1) OR LOWER(name_en) LIKE LOWER(?1)", [`%${t}%`]);
|
|
467
|
+
rows = (like.results ?? []) as UnitRow[];
|
|
468
|
+
return rows[0] ?? null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A prefixed input ("kPa", "km"): the first symbols name a prefix,
|
|
472
|
+
* the remainder a unit — the dataset does not enumerate prefixed
|
|
473
|
+
* variants, they are derived. Returns the factor the prefix carries. */
|
|
474
|
+
async function resolveWithPrefix(store: any, term: string): Promise<{ unit: UnitRow; prefixFactor: number; prefixName: string | null } | null> {
|
|
475
|
+
const direct = await unitByTerm(store, term);
|
|
476
|
+
if (direct) return { unit: direct, prefixFactor: 1, prefixName: null };
|
|
477
|
+
const t = term.trim();
|
|
478
|
+
for (let cut = 1; cut < Math.min(t.length, 3); cut++) {
|
|
479
|
+
const pSym = t.slice(0, cut);
|
|
480
|
+
const rest = t.slice(cut);
|
|
481
|
+
if (!rest) continue;
|
|
482
|
+
const pres = await store
|
|
483
|
+
.prepare("SELECT id, name_en, symbol, base, power FROM unit_prefixes WHERE symbol = ?1 AND base IS NOT NULL AND power IS NOT NULL")
|
|
484
|
+
.bind(pSym)
|
|
485
|
+
.all()
|
|
486
|
+
.catch(() => ({ results: [] }));
|
|
487
|
+
const prefix = (pres.results ?? [])[0] as { id: string; name_en: string | null; symbol: string; base: number; power: number } | undefined;
|
|
488
|
+
if (!prefix) continue;
|
|
489
|
+
const unit = await unitByTerm(store, rest);
|
|
490
|
+
if (unit) return { unit, prefixFactor: Math.pow(prefix.base, prefix.power), prefixName: prefix.name_en };
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export const unitsLookup: ToolSpec = {
|
|
496
|
+
name: "units.lookup",
|
|
497
|
+
description:
|
|
498
|
+
"Look up a measurement unit in the unitsdb plane: its names and symbols, its dimension, its quantity kinds, and its SI decomposition. Bind a symbol or an everyday unit name to the dataset's canonical unit before computing with it.",
|
|
499
|
+
params: [{ key: "term", required: true, description: "a unit's code, short name, symbol or plain name, e.g. m, Pa, pascal, meter per second" }],
|
|
500
|
+
audiences: ["mcp"],
|
|
501
|
+
handler: async (env, args) => {
|
|
502
|
+
const term = String(args?.term ?? "").trim();
|
|
503
|
+
if (term.length < 1) return null;
|
|
504
|
+
try {
|
|
505
|
+
const store = portStore(env);
|
|
506
|
+
const unit = await unitByTerm(store, term);
|
|
507
|
+
if (!unit) {
|
|
508
|
+
return { name: "units.lookup", query: term, output: `No unit matches "${term}" in the unitsdb plane. State this plainly.` };
|
|
509
|
+
}
|
|
510
|
+
const qtyNames: string[] = [];
|
|
511
|
+
for (const qid of JSON.parse(unit.quantity_ids || "[]") as string[]) {
|
|
512
|
+
const r = await store.prepare("SELECT name_en FROM unit_quantities WHERE id = ?1").bind(qid).all().catch(() => ({ results: [] }));
|
|
513
|
+
const n = ((r.results ?? [])[0] as any)?.name_en;
|
|
514
|
+
if (n) qtyNames.push(n);
|
|
515
|
+
}
|
|
516
|
+
const roots: { code: string; power: number }[] = JSON.parse(unit.root_units || "[]");
|
|
517
|
+
const rootNames = await Promise.all(
|
|
518
|
+
roots.map(async (r) => {
|
|
519
|
+
const row = await unitByTerm(store, r.code);
|
|
520
|
+
return `${row?.short ?? r.code}^${r.power}`;
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
const symbols = JSON.parse(unit.symbols || "null");
|
|
524
|
+
return {
|
|
525
|
+
name: "units.lookup",
|
|
526
|
+
query: term,
|
|
527
|
+
output: JSON.stringify({
|
|
528
|
+
code: unit.code,
|
|
529
|
+
name_en: unit.name_en,
|
|
530
|
+
name_fr: unit.name_fr,
|
|
531
|
+
symbol: symbols?.ascii ?? null,
|
|
532
|
+
si_root: !!unit.root,
|
|
533
|
+
unit_system: unit.unit_system,
|
|
534
|
+
dimension: unit.dimension_ascii,
|
|
535
|
+
quantities: qtyNames.slice(0, 4),
|
|
536
|
+
si_decomposition: rootNames,
|
|
537
|
+
scale: unit.scale,
|
|
538
|
+
}),
|
|
539
|
+
};
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
export const unitsConvert: ToolSpec = {
|
|
547
|
+
name: "units.convert",
|
|
548
|
+
description:
|
|
549
|
+
"Convert a value between two measurement units through the unitsdb plane. SI-coherent units convert exactly (prefix powers over the same dimension); a non-SI unit is stated honestly — the dataset defines a ratio scale for it, not a factor, and none is invented.",
|
|
550
|
+
params: [
|
|
551
|
+
{ key: "value", required: true, description: "the numeric value to convert" },
|
|
552
|
+
{ key: "from_unit", required: true, description: "the source unit, e.g. kPa, km, m" },
|
|
553
|
+
{ key: "to_unit", required: true, description: "the target unit, e.g. Pa, m" },
|
|
554
|
+
],
|
|
555
|
+
audiences: ["mcp"],
|
|
556
|
+
handler: async (env, args) => {
|
|
557
|
+
const value = Number(args?.value);
|
|
558
|
+
const fromTerm = String(args?.from_unit ?? "").trim();
|
|
559
|
+
const toTerm = String(args?.to_unit ?? "").trim();
|
|
560
|
+
if (!Number.isFinite(value) || !fromTerm || !toTerm) return null;
|
|
561
|
+
try {
|
|
562
|
+
const store = portStore(env);
|
|
563
|
+
const from = await resolveWithPrefix(store, fromTerm);
|
|
564
|
+
const to = await resolveWithPrefix(store, toTerm);
|
|
565
|
+
if (!from || !to) {
|
|
566
|
+
const missing = !from ? fromTerm : toTerm;
|
|
567
|
+
return { name: "units.convert", query: `${value} ${fromTerm} → ${toTerm}`, output: `No unit matches "${missing}" in the unitsdb plane. State this plainly.` };
|
|
568
|
+
}
|
|
569
|
+
if ((from.unit.dimension_ascii ?? "") !== (to.unit.dimension_ascii ?? "")) {
|
|
570
|
+
return {
|
|
571
|
+
name: "units.convert",
|
|
572
|
+
query: `${value} ${fromTerm} → ${toTerm}`,
|
|
573
|
+
output: `Not convertible: ${fromTerm} carries dimension ${from.unit.dimension_ascii ?? "unknown"} and ${toTerm} carries ${to.unit.dimension_ascii ?? "unknown"}. State this plainly.`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const coherent = (u: UnitRow) => !!u.unit_system && u.unit_system.toLowerCase().startsWith("si");
|
|
577
|
+
if (!coherent(from.unit) || !coherent(to.unit)) {
|
|
578
|
+
const nonSi = !coherent(from.unit) ? fromTerm : toTerm;
|
|
579
|
+
return {
|
|
580
|
+
name: "units.convert",
|
|
581
|
+
query: `${value} ${fromTerm} → ${toTerm}`,
|
|
582
|
+
output: `The dataset defines no numeric factor for ${nonSi} (a non-SI unit carried on a ${from.unit.scale ?? to.unit.scale ?? "ratio"} scale) — no factor is invented. State this plainly; the caller may supply the factor itself.`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const result = (value * from.prefixFactor) / to.prefixFactor;
|
|
586
|
+
return {
|
|
587
|
+
name: "units.convert",
|
|
588
|
+
query: `${value} ${fromTerm} → ${toTerm}`,
|
|
589
|
+
output: JSON.stringify({
|
|
590
|
+
value,
|
|
591
|
+
from: `${from.prefixName ? from.prefixName + " " : ""}${from.unit.name_en}`,
|
|
592
|
+
to: `${to.prefixName ? to.prefixName + " " : ""}${to.unit.name_en}`,
|
|
593
|
+
dimension: from.unit.dimension_ascii,
|
|
594
|
+
result,
|
|
595
|
+
}),
|
|
596
|
+
};
|
|
597
|
+
} catch {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
431
603
|
export const glossaryLookup: ToolSpec = {
|
|
432
604
|
name: "glossary.lookup",
|
|
433
605
|
description:
|
|
@@ -64,9 +64,9 @@ const certificatesSearch: ToolSpec = {
|
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
import { unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry } from "./tools-content.ts";
|
|
67
|
+
import { unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert } from "./tools-content.ts";
|
|
68
68
|
|
|
69
|
-
export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry];
|
|
69
|
+
export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert];
|
|
70
70
|
|
|
71
71
|
/** The agent loop's protocol declaration, GENERATED from the registry —
|
|
72
72
|
* the model's tool vocabulary is always the registry's, never a
|