@konneal/engine 0.2.56 → 0.2.59
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-5XMD22SJ.js → ask-7J4A3PSP.js} +2 -1
- package/dist/certificates-CUGV2FIW.js +19 -0
- package/dist/certificates.d.ts +5 -0
- package/dist/chunk-FHOOFPJW.js +133 -0
- package/dist/{chunk-ZVMQHCNL.js → chunk-TZOED2YR.js} +56 -112
- package/dist/tools.d.ts +18 -0
- package/dist/worker_public/src/index.js +6 -5
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +28 -2
- package/workers/worker_public/src/certificates.ts +13 -2
- package/workers/worker_public/src/tools.ts +55 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TZOED2YR.js";
|
|
4
4
|
import "./chunk-TPV5GPY2.js";
|
|
5
5
|
import "./chunk-SPKPH54J.js";
|
|
6
6
|
import "./chunk-A3QHHUN5.js";
|
|
7
7
|
import "./chunk-LVZTWUVJ.js";
|
|
8
8
|
import "./chunk-2PCUAJJO.js";
|
|
9
|
+
import "./chunk-FHOOFPJW.js";
|
|
9
10
|
import "./chunk-3FYJM7LH.js";
|
|
10
11
|
export {
|
|
11
12
|
handleAsk
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRegisterQuery,
|
|
3
|
+
certificateLinks,
|
|
4
|
+
isRegisterShaped,
|
|
5
|
+
queryFamily,
|
|
6
|
+
registerNote,
|
|
7
|
+
registerTokens,
|
|
8
|
+
searchRegister
|
|
9
|
+
} from "./chunk-FHOOFPJW.js";
|
|
10
|
+
import "./chunk-3FYJM7LH.js";
|
|
11
|
+
export {
|
|
12
|
+
buildRegisterQuery,
|
|
13
|
+
certificateLinks,
|
|
14
|
+
isRegisterShaped,
|
|
15
|
+
queryFamily,
|
|
16
|
+
registerNote,
|
|
17
|
+
registerTokens,
|
|
18
|
+
searchRegister
|
|
19
|
+
};
|
package/dist/certificates.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface RegisterRow {
|
|
|
5
5
|
model: string;
|
|
6
6
|
year: string;
|
|
7
7
|
status: string;
|
|
8
|
+
pdf_key?: string | null;
|
|
8
9
|
}
|
|
9
10
|
/** A question is register-shaped when it asks about certification
|
|
10
11
|
* standing of an identifiable model or holder — the deterministic
|
|
@@ -37,3 +38,7 @@ export declare function searchRegister(db: any, query: string): Promise<{
|
|
|
37
38
|
tokens: string[];
|
|
38
39
|
} | null>;
|
|
39
40
|
export declare function registerNote(rows: RegisterRow[]): string;
|
|
41
|
+
/** Each certificate's location rides the note as a link when its PDF
|
|
42
|
+
* is in the R2 plane (TODO 7) — the user gets the document, not just
|
|
43
|
+
* the fact. */
|
|
44
|
+
export declare function certificateLinks(rows: RegisterRow[]): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
P
|
|
3
|
+
} from "./chunk-3FYJM7LH.js";
|
|
4
|
+
|
|
5
|
+
// workers/worker_public/src/certificates.ts
|
|
6
|
+
function isRegisterShaped(query) {
|
|
7
|
+
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were|have|has|hold|holds|possess|carry|got)\b/i.test(query);
|
|
8
|
+
}
|
|
9
|
+
function queryFamily(query) {
|
|
10
|
+
const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
|
|
11
|
+
return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
|
|
12
|
+
}
|
|
13
|
+
function registerTokens(query) {
|
|
14
|
+
const stop = /* @__PURE__ */ new Set([
|
|
15
|
+
"the",
|
|
16
|
+
"a",
|
|
17
|
+
"an",
|
|
18
|
+
"is",
|
|
19
|
+
"are",
|
|
20
|
+
"was",
|
|
21
|
+
"were",
|
|
22
|
+
"still",
|
|
23
|
+
"currently",
|
|
24
|
+
"in",
|
|
25
|
+
"on",
|
|
26
|
+
"for",
|
|
27
|
+
"of",
|
|
28
|
+
"and",
|
|
29
|
+
"or",
|
|
30
|
+
"certificate",
|
|
31
|
+
"certificates",
|
|
32
|
+
"certified",
|
|
33
|
+
"certification",
|
|
34
|
+
"register",
|
|
35
|
+
"status",
|
|
36
|
+
"valid",
|
|
37
|
+
"suspended",
|
|
38
|
+
"revoked",
|
|
39
|
+
"recommendation",
|
|
40
|
+
"per",
|
|
41
|
+
"under",
|
|
42
|
+
"by",
|
|
43
|
+
"with",
|
|
44
|
+
"what",
|
|
45
|
+
"which",
|
|
46
|
+
"who",
|
|
47
|
+
"does",
|
|
48
|
+
"do",
|
|
49
|
+
"load",
|
|
50
|
+
"cell",
|
|
51
|
+
"model",
|
|
52
|
+
"manufacturer",
|
|
53
|
+
"holder",
|
|
54
|
+
"company",
|
|
55
|
+
"r",
|
|
56
|
+
"how",
|
|
57
|
+
"to",
|
|
58
|
+
"list",
|
|
59
|
+
"show"
|
|
60
|
+
]);
|
|
61
|
+
const tokens = [];
|
|
62
|
+
const publisherWord = P().publisher.name.toLowerCase();
|
|
63
|
+
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
64
|
+
if (w.length < 2) continue;
|
|
65
|
+
if (stop.has(w.toLowerCase())) continue;
|
|
66
|
+
if (w.toLowerCase() === publisherWord) continue;
|
|
67
|
+
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
68
|
+
if (!tokens.includes(w)) tokens.push(w);
|
|
69
|
+
}
|
|
70
|
+
return tokens.slice(0, 6);
|
|
71
|
+
}
|
|
72
|
+
function buildRegisterQuery(tokens) {
|
|
73
|
+
const params = [];
|
|
74
|
+
const clauses = tokens.map((t) => {
|
|
75
|
+
if (/^\d+$/.test(t)) {
|
|
76
|
+
const padded = `% ${t.toLowerCase()} %`;
|
|
77
|
+
params.push(t, t, padded);
|
|
78
|
+
const n1 = params.length - 2;
|
|
79
|
+
const n2 = params.length;
|
|
80
|
+
return `(model = ?${n1} OR num = ?${n1} OR (' ' || REPLACE(REPLACE(REPLACE(LOWER(model), '-', ' '), ',', ' '), '.', ' ') || ' ') LIKE ?${n2})`;
|
|
81
|
+
}
|
|
82
|
+
params.push(`%${t}%`);
|
|
83
|
+
const n = params.length;
|
|
84
|
+
return `(holder LIKE ?${n} OR model LIKE ?${n} OR num LIKE ?${n})`;
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
sql: `SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
|
|
88
|
+
params
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function searchRegister(db, query) {
|
|
92
|
+
if (!isRegisterShaped(query)) return null;
|
|
93
|
+
const tokens = registerTokens(query);
|
|
94
|
+
if (!tokens.length) return null;
|
|
95
|
+
const built = buildRegisterQuery(tokens);
|
|
96
|
+
let { sql, params } = built;
|
|
97
|
+
const family = queryFamily(query);
|
|
98
|
+
if (family) {
|
|
99
|
+
sql = sql.replace(" WHERE ", " WHERE family = ?0 AND ");
|
|
100
|
+
params.unshift(family.replace(/^[A-Za-z]+/, ""));
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const res = await db.prepare(sql).bind(...params).all();
|
|
104
|
+
return { rows: res.results ?? [], tokens };
|
|
105
|
+
} catch {
|
|
106
|
+
return { rows: [], tokens };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function registerNote(rows) {
|
|
110
|
+
if (!rows.length) {
|
|
111
|
+
return `Certificate register: NO certificate matching the asked holder or model appears in the ${P().publisher.name}-CS register snapshot. State plainly that no such certificate is in this register, and that the register is a snapshot rather than the live certification status.`;
|
|
112
|
+
}
|
|
113
|
+
const lines = rows.map((r) => `- ${r.num}: holder ${r.holder}, model "${r.model}"${r.year ? `, issued ${r.year}` : ""} \u2014 status ${r.status}`);
|
|
114
|
+
return [
|
|
115
|
+
"Certificate register \u2014 the following certificates matched the asked holder or model. Quote the certificate number, the holder and the status verbatim; the register is a snapshot, so qualify any statement about current certification accordingly.",
|
|
116
|
+
...lines
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
function certificateLinks(rows) {
|
|
120
|
+
const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.oimlsmart.org/cert-pdf/${r.pdf_key}`);
|
|
121
|
+
return links.length ? `The certificates' documents:
|
|
122
|
+
${links.join("\n")}` : "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
isRegisterShaped,
|
|
127
|
+
queryFamily,
|
|
128
|
+
registerTokens,
|
|
129
|
+
buildRegisterQuery,
|
|
130
|
+
searchRegister,
|
|
131
|
+
registerNote,
|
|
132
|
+
certificateLinks
|
|
133
|
+
};
|
|
@@ -64,6 +64,11 @@ import {
|
|
|
64
64
|
roleModel,
|
|
65
65
|
sha256Hex
|
|
66
66
|
} from "./chunk-2PCUAJJO.js";
|
|
67
|
+
import {
|
|
68
|
+
certificateLinks,
|
|
69
|
+
registerNote,
|
|
70
|
+
searchRegister
|
|
71
|
+
} from "./chunk-FHOOFPJW.js";
|
|
67
72
|
import {
|
|
68
73
|
P
|
|
69
74
|
} from "./chunk-3FYJM7LH.js";
|
|
@@ -1864,119 +1869,37 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
|
|
|
1864
1869
|
return `a:${indexVersion}:g${gen}:${ns}:${queryHash}`;
|
|
1865
1870
|
}
|
|
1866
1871
|
|
|
1867
|
-
// workers/worker_public/src/
|
|
1868
|
-
function
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
"is",
|
|
1881
|
-
"are",
|
|
1882
|
-
"was",
|
|
1883
|
-
"were",
|
|
1884
|
-
"still",
|
|
1885
|
-
"currently",
|
|
1886
|
-
"in",
|
|
1887
|
-
"on",
|
|
1888
|
-
"for",
|
|
1889
|
-
"of",
|
|
1890
|
-
"and",
|
|
1891
|
-
"or",
|
|
1892
|
-
"certificate",
|
|
1893
|
-
"certificates",
|
|
1894
|
-
"certified",
|
|
1895
|
-
"certification",
|
|
1896
|
-
"register",
|
|
1897
|
-
"status",
|
|
1898
|
-
"valid",
|
|
1899
|
-
"suspended",
|
|
1900
|
-
"revoked",
|
|
1901
|
-
"recommendation",
|
|
1902
|
-
"per",
|
|
1903
|
-
"under",
|
|
1904
|
-
"by",
|
|
1905
|
-
"with",
|
|
1906
|
-
"what",
|
|
1907
|
-
"which",
|
|
1908
|
-
"who",
|
|
1909
|
-
"does",
|
|
1910
|
-
"do",
|
|
1911
|
-
"load",
|
|
1912
|
-
"cell",
|
|
1913
|
-
"model",
|
|
1914
|
-
"manufacturer",
|
|
1915
|
-
"holder",
|
|
1916
|
-
"company",
|
|
1917
|
-
"r",
|
|
1918
|
-
"how",
|
|
1919
|
-
"to",
|
|
1920
|
-
"list",
|
|
1921
|
-
"show"
|
|
1922
|
-
]);
|
|
1923
|
-
const tokens2 = [];
|
|
1924
|
-
const publisherWord = P().publisher.name.toLowerCase();
|
|
1925
|
-
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
1926
|
-
if (w.length < 2) continue;
|
|
1927
|
-
if (stop.has(w.toLowerCase())) continue;
|
|
1928
|
-
if (w.toLowerCase() === publisherWord) continue;
|
|
1929
|
-
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
1930
|
-
if (!tokens2.includes(w)) tokens2.push(w);
|
|
1931
|
-
}
|
|
1932
|
-
return tokens2.slice(0, 6);
|
|
1933
|
-
}
|
|
1934
|
-
function buildRegisterQuery(tokens2) {
|
|
1935
|
-
const params = [];
|
|
1936
|
-
const clauses = tokens2.map((t) => {
|
|
1937
|
-
if (/^\d+$/.test(t)) {
|
|
1938
|
-
const padded = `% ${t.toLowerCase()} %`;
|
|
1939
|
-
params.push(t, t, padded);
|
|
1940
|
-
const n1 = params.length - 2;
|
|
1941
|
-
const n2 = params.length;
|
|
1942
|
-
return `(model = ?${n1} OR num = ?${n1} OR (' ' || REPLACE(REPLACE(REPLACE(LOWER(model), '-', ' '), ',', ' '), '.', ' ') || ' ') LIKE ?${n2})`;
|
|
1943
|
-
}
|
|
1944
|
-
params.push(`%${t}%`);
|
|
1945
|
-
const n = params.length;
|
|
1946
|
-
return `(holder LIKE ?${n} OR model LIKE ?${n} OR num LIKE ?${n})`;
|
|
1947
|
-
});
|
|
1948
|
-
return {
|
|
1949
|
-
sql: `SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
|
|
1950
|
-
params
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
|
-
async function searchRegister(db, query) {
|
|
1954
|
-
if (!isRegisterShaped(query)) return null;
|
|
1955
|
-
const tokens2 = registerTokens(query);
|
|
1956
|
-
if (!tokens2.length) return null;
|
|
1957
|
-
const built = buildRegisterQuery(tokens2);
|
|
1958
|
-
let { sql, params } = built;
|
|
1959
|
-
const family = queryFamily(query);
|
|
1960
|
-
if (family) {
|
|
1961
|
-
sql = sql.replace(" WHERE ", " WHERE family = ?0 AND ");
|
|
1962
|
-
params.unshift(family);
|
|
1963
|
-
}
|
|
1964
|
-
try {
|
|
1965
|
-
const res = await db.prepare(sql).bind(...params).all();
|
|
1966
|
-
return { rows: res.results ?? [], tokens: tokens2 };
|
|
1967
|
-
} catch {
|
|
1968
|
-
return { rows: [], tokens: tokens2 };
|
|
1872
|
+
// workers/worker_public/src/tools.ts
|
|
1873
|
+
function parseToolCall(text) {
|
|
1874
|
+
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
1875
|
+
if (!m) return null;
|
|
1876
|
+
let args = {};
|
|
1877
|
+
if (m[2]) {
|
|
1878
|
+
try {
|
|
1879
|
+
args = JSON.parse(m[2]);
|
|
1880
|
+
} catch {
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
1884
|
+
return null;
|
|
1969
1885
|
}
|
|
1886
|
+
return { name: m[1].toLowerCase(), args };
|
|
1970
1887
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1888
|
+
var TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
|
|
1889
|
+
TOOL register_search {"query": "manufacturer and model to look up"}
|
|
1890
|
+
The worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known \u2014 including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.`;
|
|
1891
|
+
async function runTool(db, call) {
|
|
1892
|
+
if (call.name !== "register_search") return null;
|
|
1893
|
+
const query = String(call.args?.query ?? "").trim().slice(0, 160);
|
|
1894
|
+
if (!query) return null;
|
|
1895
|
+
const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-CUGV2FIW.js");
|
|
1896
|
+
const reg = await searchRegister2(db, query);
|
|
1897
|
+
const output = reg?.rows?.length ? registerNote2(reg.rows) : `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
1898
|
+
return { name: call.name, query, output };
|
|
1899
|
+
}
|
|
1900
|
+
function toolNote(r) {
|
|
1901
|
+
return `The ${r.name} tool returned, for the query "${r.query}":
|
|
1902
|
+
${r.output}`;
|
|
1980
1903
|
}
|
|
1981
1904
|
|
|
1982
1905
|
// workers/worker_public/prompts/nameplate.md
|
|
@@ -2391,7 +2314,7 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2391
2314
|
}
|
|
2392
2315
|
}
|
|
2393
2316
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
2394
|
-
const regNote = register ? registerNote(register.rows) : void 0;
|
|
2317
|
+
const regNote = register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : void 0;
|
|
2395
2318
|
if (understanding?.intent === "conversational") {
|
|
2396
2319
|
const queryHash2 = await sha256Hex(q.query);
|
|
2397
2320
|
const messages2 = [
|
|
@@ -2743,6 +2666,27 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2743
2666
|
budget
|
|
2744
2667
|
);
|
|
2745
2668
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
2669
|
+
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
2670
|
+
try {
|
|
2671
|
+
const probe = await env.AI.run(model, {
|
|
2672
|
+
messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
|
|
2673
|
+
|
|
2674
|
+
${q.query}` }],
|
|
2675
|
+
max_tokens: 300,
|
|
2676
|
+
reasoning_effort: "low"
|
|
2677
|
+
});
|
|
2678
|
+
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
2679
|
+
const call = parseToolCall(probeText);
|
|
2680
|
+
if (call) {
|
|
2681
|
+
const result = await runTool(env.DB, call);
|
|
2682
|
+
if (result) {
|
|
2683
|
+
messages.push({ role: "system", content: toolNote(result) });
|
|
2684
|
+
console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
} catch {
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2746
2690
|
if (withImage) {
|
|
2747
2691
|
const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
|
|
2748
2692
|
const last = messages[messages.length - 1];
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ToolCall {
|
|
2
|
+
name: string;
|
|
3
|
+
args: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolResult {
|
|
6
|
+
name: string;
|
|
7
|
+
query: string;
|
|
8
|
+
output: string;
|
|
9
|
+
}
|
|
10
|
+
/** The strict protocol: a line `TOOL register_search {"query": "…"}`.
|
|
11
|
+
* Plain regex parse — no JSON mode needed, tolerant of surrounding
|
|
12
|
+
* prose (the model may talk around the call). */
|
|
13
|
+
export declare function parseToolCall(text: string): ToolCall | null;
|
|
14
|
+
export declare const TOOL_DECLARATION = "You may use one tool before answering, by writing a single line:\nTOOL register_search {\"query\": \"manufacturer and model to look up\"}\nThe worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known \u2014 including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.";
|
|
15
|
+
export declare function runTool(db: any, call: ToolCall): Promise<ToolResult | null>;
|
|
16
|
+
/** The attributed injection: the answer model sees what the tool
|
|
17
|
+
* returned, for the string it asked — never a standing claim. */
|
|
18
|
+
export declare function toolNote(r: ToolResult): string;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
handleSearch
|
|
3
|
-
} from "../../chunk-FLNX4O7C.js";
|
|
4
1
|
import {
|
|
5
2
|
bindModelNode,
|
|
6
3
|
checkQuoteAnchors,
|
|
@@ -9,7 +6,10 @@ import {
|
|
|
9
6
|
modelGroundingBlock,
|
|
10
7
|
scoreFaithfulness,
|
|
11
8
|
scoreJudge
|
|
12
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-TZOED2YR.js";
|
|
10
|
+
import {
|
|
11
|
+
handleSearch
|
|
12
|
+
} from "../../chunk-FLNX4O7C.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
cfBlobs,
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
sha256Hex,
|
|
64
64
|
today
|
|
65
65
|
} from "../../chunk-2PCUAJJO.js";
|
|
66
|
+
import "../../chunk-FHOOFPJW.js";
|
|
66
67
|
import {
|
|
67
68
|
P,
|
|
68
69
|
setProfile
|
|
@@ -958,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
958
959
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
959
960
|
body: JSON.stringify({ ...args, stream: false })
|
|
960
961
|
});
|
|
961
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
962
|
+
const res = name === "ask" ? await (await import("../../ask-7J4A3PSP.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
|
|
962
963
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
963
964
|
});
|
|
964
965
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
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.59",
|
|
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",
|
|
@@ -39,7 +39,8 @@ export type { Env };
|
|
|
39
39
|
import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
|
|
40
40
|
import { clientIp, checkQuota, telemetry } from "./quota";
|
|
41
41
|
import { graphExpand, editionNote } from "./graph";
|
|
42
|
-
import { registerNote, searchRegister } from "./certificates";
|
|
42
|
+
import { registerNote, searchRegister, certificateLinks } from "./certificates";
|
|
43
|
+
import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
|
|
43
44
|
import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
|
|
44
45
|
import { P } from "./profile.ts";
|
|
45
46
|
|
|
@@ -636,7 +637,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
636
637
|
}
|
|
637
638
|
}
|
|
638
639
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
639
|
-
const regNote = register ? registerNote(register.rows) : undefined;
|
|
640
|
+
const regNote = register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : undefined;
|
|
640
641
|
|
|
641
642
|
// Conversational route, decided by query UNDERSTANDING (any language, any
|
|
642
643
|
// phrasing) — not string matching. No retrieval: nothing in the corpus
|
|
@@ -1147,6 +1148,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1147
1148
|
budget,
|
|
1148
1149
|
);
|
|
1149
1150
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
1151
|
+
// the agent loop (the owner's 2026-09-28 direction): ONE bounded tool
|
|
1152
|
+
// round, armed only where a tool can help. The model may call
|
|
1153
|
+
// register_search through the strict line protocol; the worker
|
|
1154
|
+
// executes it deterministically and the ATTRIBUTED result joins the
|
|
1155
|
+
// messages — what the tool returned, for the string it asked.
|
|
1156
|
+
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
1157
|
+
try {
|
|
1158
|
+
const probe = await (env as any).AI.run(model, {
|
|
1159
|
+
messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}\n\n${q.query}` }],
|
|
1160
|
+
max_tokens: 300,
|
|
1161
|
+
reasoning_effort: "low",
|
|
1162
|
+
});
|
|
1163
|
+
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
1164
|
+
const call = parseToolCall(probeText);
|
|
1165
|
+
if (call) {
|
|
1166
|
+
const result = await runTool(env.DB, call);
|
|
1167
|
+
if (result) {
|
|
1168
|
+
messages.push({ role: "system", content: toolNote(result) });
|
|
1169
|
+
console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
} catch {
|
|
1173
|
+
/* the loop is optional — the answer proceeds without the tool */
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1150
1176
|
if (withImage) {
|
|
1151
1177
|
// the user's own image rides on the question message — retrieval stays
|
|
1152
1178
|
// text-driven; the answer model reads the image as question context.
|
|
@@ -15,6 +15,7 @@ export interface RegisterRow {
|
|
|
15
15
|
model: string;
|
|
16
16
|
year: string;
|
|
17
17
|
status: string;
|
|
18
|
+
pdf_key?: string | null;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/** A question is register-shaped when it asks about certification
|
|
@@ -83,7 +84,7 @@ export function buildRegisterQuery(tokens: string[]): { sql: string; params: str
|
|
|
83
84
|
return `(holder LIKE ?${n} OR model LIKE ?${n} OR num LIKE ?${n})`;
|
|
84
85
|
});
|
|
85
86
|
return {
|
|
86
|
-
sql: `SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
|
|
87
|
+
sql: `SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
|
|
87
88
|
params,
|
|
88
89
|
};
|
|
89
90
|
}
|
|
@@ -96,8 +97,10 @@ export async function searchRegister(db: any, query: string): Promise<{ rows: Re
|
|
|
96
97
|
let { sql, params } = built;
|
|
97
98
|
const family = queryFamily(query);
|
|
98
99
|
if (family) {
|
|
100
|
+
// the register's family column is the DIGITS ONLY ("60", "105") —
|
|
101
|
+
// the letter+padding form ("R60") never matched a single row
|
|
99
102
|
sql = sql.replace(" WHERE ", " WHERE family = ?0 AND ");
|
|
100
|
-
params.unshift(family);
|
|
103
|
+
params.unshift(family.replace(/^[A-Za-z]+/, ""));
|
|
101
104
|
}
|
|
102
105
|
try {
|
|
103
106
|
const res = await db.prepare(sql).bind(...params).all();
|
|
@@ -118,3 +121,11 @@ export function registerNote(rows: RegisterRow[]): string {
|
|
|
118
121
|
...lines,
|
|
119
122
|
].join("\n");
|
|
120
123
|
}
|
|
124
|
+
/** Each certificate's location rides the note as a link when its PDF
|
|
125
|
+
* is in the R2 plane (TODO 7) — the user gets the document, not just
|
|
126
|
+
* the fact. */
|
|
127
|
+
export function certificateLinks(rows: RegisterRow[]): string {
|
|
128
|
+
const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.oimlsmart.org/cert-pdf/${r.pdf_key}`);
|
|
129
|
+
return links.length ? `The certificates' documents:\n${links.join("\n")}` : "";
|
|
130
|
+
}
|
|
131
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The agent interface's tool plane (the owner's 2026-09-28 direction): a
|
|
2
|
+
// bounded tool loop on the serving path. The model may invoke named
|
|
3
|
+
// tools through a strict line protocol; the worker executes each
|
|
4
|
+
// deterministically and the result rides back ATTRIBUTED — every tool
|
|
5
|
+
// output is phrased as what the tool returned, and the searched string
|
|
6
|
+
// is always visible in the answer. One round per ask, armed only where
|
|
7
|
+
// a tool can help (the certificate intent, v1).
|
|
8
|
+
|
|
9
|
+
export interface ToolCall {
|
|
10
|
+
name: string;
|
|
11
|
+
args: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolResult {
|
|
15
|
+
name: string;
|
|
16
|
+
query: string;
|
|
17
|
+
output: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The strict protocol: a line `TOOL register_search {"query": "…"}`.
|
|
21
|
+
* Plain regex parse — no JSON mode needed, tolerant of surrounding
|
|
22
|
+
* prose (the model may talk around the call). */
|
|
23
|
+
export function parseToolCall(text: string): ToolCall | null {
|
|
24
|
+
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? "")); // line-anchored, case-sensitive: prose never trips it
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
let args: Record<string, unknown> = {};
|
|
27
|
+
if (m[2]) {
|
|
28
|
+
try { args = JSON.parse(m[2]); } catch { return null; }
|
|
29
|
+
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
30
|
+
return null; // a malformed call (opened, never closed) is not a call
|
|
31
|
+
}
|
|
32
|
+
return { name: m[1].toLowerCase(), args };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
|
|
36
|
+
TOOL register_search {"query": "manufacturer and model to look up"}
|
|
37
|
+
The worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known — including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.`;
|
|
38
|
+
|
|
39
|
+
export async function runTool(db: any, call: ToolCall): Promise<ToolResult | null> {
|
|
40
|
+
if (call.name !== "register_search") return null;
|
|
41
|
+
const query = String(call.args?.query ?? "").trim().slice(0, 160);
|
|
42
|
+
if (!query) return null;
|
|
43
|
+
const { searchRegister, registerNote } = await import("./certificates");
|
|
44
|
+
const reg = await searchRegister(db, query);
|
|
45
|
+
const output = reg?.rows?.length
|
|
46
|
+
? registerNote(reg.rows)
|
|
47
|
+
: `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
48
|
+
return { name: call.name, query, output };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The attributed injection: the answer model sees what the tool
|
|
52
|
+
* returned, for the string it asked — never a standing claim. */
|
|
53
|
+
export function toolNote(r: ToolResult): string {
|
|
54
|
+
return `The ${r.name} tool returned, for the query "${r.query}":\n${r.output}`;
|
|
55
|
+
}
|