@konneal/engine 0.2.56 → 0.2.57
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-26DGSVFW.js} +2 -1
- package/dist/certificates-DZJJLRGU.js +17 -0
- package/dist/{chunk-ZVMQHCNL.js → chunk-MNEUEHO7.js} +54 -111
- package/dist/chunk-Z6JAGZAM.js +127 -0
- 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 +26 -0
- 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-MNEUEHO7.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-Z6JAGZAM.js";
|
|
9
10
|
import "./chunk-3FYJM7LH.js";
|
|
10
11
|
export {
|
|
11
12
|
handleAsk
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRegisterQuery,
|
|
3
|
+
isRegisterShaped,
|
|
4
|
+
queryFamily,
|
|
5
|
+
registerNote,
|
|
6
|
+
registerTokens,
|
|
7
|
+
searchRegister
|
|
8
|
+
} from "./chunk-Z6JAGZAM.js";
|
|
9
|
+
import "./chunk-3FYJM7LH.js";
|
|
10
|
+
export {
|
|
11
|
+
buildRegisterQuery,
|
|
12
|
+
isRegisterShaped,
|
|
13
|
+
queryFamily,
|
|
14
|
+
registerNote,
|
|
15
|
+
registerTokens,
|
|
16
|
+
searchRegister
|
|
17
|
+
};
|
|
@@ -64,6 +64,10 @@ import {
|
|
|
64
64
|
roleModel,
|
|
65
65
|
sha256Hex
|
|
66
66
|
} from "./chunk-2PCUAJJO.js";
|
|
67
|
+
import {
|
|
68
|
+
registerNote,
|
|
69
|
+
searchRegister
|
|
70
|
+
} from "./chunk-Z6JAGZAM.js";
|
|
67
71
|
import {
|
|
68
72
|
P
|
|
69
73
|
} from "./chunk-3FYJM7LH.js";
|
|
@@ -1864,119 +1868,37 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
|
|
|
1864
1868
|
return `a:${indexVersion}:g${gen}:${ns}:${queryHash}`;
|
|
1865
1869
|
}
|
|
1866
1870
|
|
|
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 };
|
|
1871
|
+
// workers/worker_public/src/tools.ts
|
|
1872
|
+
function parseToolCall(text) {
|
|
1873
|
+
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
1874
|
+
if (!m) return null;
|
|
1875
|
+
let args = {};
|
|
1876
|
+
if (m[2]) {
|
|
1877
|
+
try {
|
|
1878
|
+
args = JSON.parse(m[2]);
|
|
1879
|
+
} catch {
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
1883
|
+
return null;
|
|
1969
1884
|
}
|
|
1885
|
+
return { name: m[1].toLowerCase(), args };
|
|
1970
1886
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1887
|
+
var TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
|
|
1888
|
+
TOOL register_search {"query": "manufacturer and model to look up"}
|
|
1889
|
+
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.`;
|
|
1890
|
+
async function runTool(db, call) {
|
|
1891
|
+
if (call.name !== "register_search") return null;
|
|
1892
|
+
const query = String(call.args?.query ?? "").trim().slice(0, 160);
|
|
1893
|
+
if (!query) return null;
|
|
1894
|
+
const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-DZJJLRGU.js");
|
|
1895
|
+
const reg = await searchRegister2(db, query);
|
|
1896
|
+
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.`;
|
|
1897
|
+
return { name: call.name, query, output };
|
|
1898
|
+
}
|
|
1899
|
+
function toolNote(r) {
|
|
1900
|
+
return `The ${r.name} tool returned, for the query "${r.query}":
|
|
1901
|
+
${r.output}`;
|
|
1980
1902
|
}
|
|
1981
1903
|
|
|
1982
1904
|
// workers/worker_public/prompts/nameplate.md
|
|
@@ -2743,6 +2665,27 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2743
2665
|
budget
|
|
2744
2666
|
);
|
|
2745
2667
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
2668
|
+
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
2669
|
+
try {
|
|
2670
|
+
const probe = await env.AI.run(model, {
|
|
2671
|
+
messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
|
|
2672
|
+
|
|
2673
|
+
${q.query}` }],
|
|
2674
|
+
max_tokens: 300,
|
|
2675
|
+
reasoning_effort: "low"
|
|
2676
|
+
});
|
|
2677
|
+
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
2678
|
+
const call = parseToolCall(probeText);
|
|
2679
|
+
if (call) {
|
|
2680
|
+
const result = await runTool(env.DB, call);
|
|
2681
|
+
if (result) {
|
|
2682
|
+
messages.push({ role: "system", content: toolNote(result) });
|
|
2683
|
+
console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
} catch {
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2746
2689
|
if (withImage) {
|
|
2747
2690
|
const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
|
|
2748
2691
|
const last = messages[messages.length - 1];
|
|
@@ -0,0 +1,127 @@
|
|
|
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 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);
|
|
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
|
+
|
|
120
|
+
export {
|
|
121
|
+
isRegisterShaped,
|
|
122
|
+
queryFamily,
|
|
123
|
+
registerTokens,
|
|
124
|
+
buildRegisterQuery,
|
|
125
|
+
searchRegister,
|
|
126
|
+
registerNote
|
|
127
|
+
};
|
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-MNEUEHO7.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-Z6JAGZAM.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-26DGSVFW.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.57",
|
|
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",
|
|
@@ -40,6 +40,7 @@ import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./
|
|
|
40
40
|
import { clientIp, checkQuota, telemetry } from "./quota";
|
|
41
41
|
import { graphExpand, editionNote } from "./graph";
|
|
42
42
|
import { registerNote, searchRegister } 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
|
|
|
@@ -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.
|
|
@@ -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
|
+
}
|