@konneal/engine 0.2.75 → 0.2.77
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-AJJCWGW6.js → ask-DKYIVPNV.js} +2 -2
- package/dist/{certificates-DNRUOYRF.js → certificates-YFL4K7O3.js} +3 -1
- package/dist/certificates.d.ts +4 -0
- package/dist/{chunk-VGVTIPFW.js → chunk-7AQM3GSO.js} +18 -0
- package/dist/{chunk-XJPJO376.js → chunk-GOYFWC3N.js} +47 -27
- package/dist/nameplate-parse.d.ts +4 -2
- package/dist/prompts/nameplate.md +10 -6
- package/dist/worker_public/src/index.js +3 -3
- package/package.json +1 -1
- package/workers/worker_public/prompts/nameplate.md +10 -6
- package/workers/worker_public/src/certificates.ts +28 -0
- package/workers/worker_public/src/nameplate-parse.ts +12 -6
- package/workers/worker_public/src/nameplate.ts +33 -16
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GOYFWC3N.js";
|
|
4
4
|
import "./chunk-7HWJOFTQ.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-
|
|
9
|
+
import "./chunk-7AQM3GSO.js";
|
|
10
10
|
import "./chunk-3FYJM7LH.js";
|
|
11
11
|
export {
|
|
12
12
|
handleAsk
|
|
@@ -2,16 +2,18 @@ import {
|
|
|
2
2
|
buildRegisterQuery,
|
|
3
3
|
certificateLinks,
|
|
4
4
|
isRegisterShaped,
|
|
5
|
+
printedCertificateNumber,
|
|
5
6
|
queryFamily,
|
|
6
7
|
registerNote,
|
|
7
8
|
registerTokens,
|
|
8
9
|
searchRegister
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-7AQM3GSO.js";
|
|
10
11
|
import "./chunk-3FYJM7LH.js";
|
|
11
12
|
export {
|
|
12
13
|
buildRegisterQuery,
|
|
13
14
|
certificateLinks,
|
|
14
15
|
isRegisterShaped,
|
|
16
|
+
printedCertificateNumber,
|
|
15
17
|
queryFamily,
|
|
16
18
|
registerNote,
|
|
17
19
|
registerTokens,
|
package/dist/certificates.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export declare function isRegisterShaped(query: string): boolean;
|
|
|
20
20
|
* search filters to it, so a question about R 60 never presents R 76
|
|
21
21
|
* rows. */
|
|
22
22
|
export declare function queryFamily(query: string): string | null;
|
|
23
|
+
/** A PRINTED certificate number on a nameplate ("R76/2006-A-GB1-18.08")
|
|
24
|
+
* names the exact register row — it must not be shredded into word
|
|
25
|
+
* tokens that compete with it. */
|
|
26
|
+
export declare function printedCertificateNumber(query: string): string | null;
|
|
23
27
|
export declare function registerTokens(query: string): string[];
|
|
24
28
|
/** The register query for a token set. Numeric tokens (a model number
|
|
25
29
|
* like "190") must match as a STANDALONE value — `LIKE '%190%'` also
|
|
@@ -10,6 +10,10 @@ function queryFamily(query) {
|
|
|
10
10
|
const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
|
|
11
11
|
return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
|
|
12
12
|
}
|
|
13
|
+
function printedCertificateNumber(query) {
|
|
14
|
+
const m = /\b([RDMB]\s?-?\d{2,3}\s?\/\s?\d{4}\s?-\s?[A-Z0-9]+\s?-\s?[^\s,;]+(?:\s?\.\s?\d+)?)/i.exec(query);
|
|
15
|
+
return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
|
|
16
|
+
}
|
|
13
17
|
function registerTokens(query) {
|
|
14
18
|
const stop = /* @__PURE__ */ new Set([
|
|
15
19
|
"the",
|
|
@@ -90,6 +94,19 @@ function buildRegisterQuery(tokens) {
|
|
|
90
94
|
}
|
|
91
95
|
async function searchRegister(db, query, force = false) {
|
|
92
96
|
if (!force && !isRegisterShaped(query)) return null;
|
|
97
|
+
const printed = printedCertificateNumber(query);
|
|
98
|
+
if (printed) {
|
|
99
|
+
try {
|
|
100
|
+
const seq = printed.replace(/\.\d+$/, "");
|
|
101
|
+
const res = await db.prepare(`SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE REPLACE(num, ' ', '') = ?1 OR REPLACE(num, ' ', '') LIKE ?2 || '%' ORDER BY num LIMIT 6`).bind(printed, seq).all();
|
|
102
|
+
if ((res.results ?? []).length) {
|
|
103
|
+
console.log(`register-search: printed-number hit ${printed} \u2192 ${(res.results ?? []).length} rows`);
|
|
104
|
+
return { rows: res.results ?? [], tokens: [printed] };
|
|
105
|
+
}
|
|
106
|
+
console.log(`register-search: printed number ${printed} missed the register \u2014 falling back to tokens`);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
93
110
|
const tokens = registerTokens(query);
|
|
94
111
|
if (!tokens.length) return null;
|
|
95
112
|
const built = buildRegisterQuery(tokens);
|
|
@@ -129,6 +146,7 @@ ${links.join("\n")}` : "";
|
|
|
129
146
|
export {
|
|
130
147
|
isRegisterShaped,
|
|
131
148
|
queryFamily,
|
|
149
|
+
printedCertificateNumber,
|
|
132
150
|
registerTokens,
|
|
133
151
|
buildRegisterQuery,
|
|
134
152
|
searchRegister,
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
certificateLinks,
|
|
73
73
|
registerNote,
|
|
74
74
|
searchRegister
|
|
75
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-7AQM3GSO.js";
|
|
76
76
|
import {
|
|
77
77
|
P
|
|
78
78
|
} from "./chunk-3FYJM7LH.js";
|
|
@@ -1896,7 +1896,7 @@ async function runTool(db, call) {
|
|
|
1896
1896
|
if (call.name !== "register_search") return null;
|
|
1897
1897
|
const query = String(call.args?.query ?? "").trim().slice(0, 160);
|
|
1898
1898
|
if (!query) return null;
|
|
1899
|
-
const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-
|
|
1899
|
+
const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-YFL4K7O3.js");
|
|
1900
1900
|
const reg = await searchRegister2(db, query, true);
|
|
1901
1901
|
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.`;
|
|
1902
1902
|
return { name: call.name, query, output };
|
|
@@ -2020,14 +2020,18 @@ ${matched.map((m) => `- ${m.text}`).join("\n")}`);
|
|
|
2020
2020
|
}
|
|
2021
2021
|
|
|
2022
2022
|
// workers/worker_public/prompts/nameplate.md
|
|
2023
|
-
var nameplate_default = `You read instrument nameplates. From the photograph, extract
|
|
2024
|
-
manufacturer's name exactly as printed and the model designation exactly
|
|
2025
|
-
as printed. Answer with ONLY a JSON object on one line:
|
|
2023
|
+
var nameplate_default = `You read instrument nameplates. From the photograph, extract:
|
|
2026
2024
|
|
|
2027
|
-
{"manufacturer": "\u2026", "model": "\u2026"}
|
|
2025
|
+
{"manufacturer": "\u2026", "model": "\u2026", "certificate_number": "\u2026"}
|
|
2028
2026
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2027
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
2028
|
+
- model: the model designation exactly as printed, else null.
|
|
2029
|
+
- certificate_number: the OIML certificate number if one is printed
|
|
2030
|
+
(it looks like R76/2006-A-GB1-18.08 \u2014 a family, an edition year, an
|
|
2031
|
+
authority code, a year and a sequence), exactly as printed, else null.
|
|
2032
|
+
|
|
2033
|
+
Answer with ONLY the JSON object on one line. Never guess beyond what is
|
|
2034
|
+
printed.
|
|
2031
2035
|
`;
|
|
2032
2036
|
|
|
2033
2037
|
// workers/worker_public/src/nameplate-parse.ts
|
|
@@ -2037,36 +2041,52 @@ function parseNameplate(text) {
|
|
|
2037
2041
|
try {
|
|
2038
2042
|
const j = JSON.parse(m[0]);
|
|
2039
2043
|
const clean = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null;
|
|
2040
|
-
const out = {
|
|
2041
|
-
|
|
2044
|
+
const out = {
|
|
2045
|
+
manufacturer: clean(j.manufacturer),
|
|
2046
|
+
model: clean(j.model),
|
|
2047
|
+
certificate_number: clean(j.certificate_number)
|
|
2048
|
+
};
|
|
2049
|
+
return out.manufacturer || out.model || out.certificate_number ? out : null;
|
|
2042
2050
|
} catch {
|
|
2043
2051
|
return null;
|
|
2044
2052
|
}
|
|
2045
2053
|
}
|
|
2046
2054
|
function nameplateRegisterQuery(np, question) {
|
|
2047
2055
|
const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
|
|
2048
|
-
return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
2056
|
+
return [np.certificate_number, np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
2049
2057
|
}
|
|
2050
2058
|
|
|
2051
2059
|
// workers/worker_public/src/nameplate.ts
|
|
2052
2060
|
async function extractNameplate(ai, model, image) {
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2061
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2062
|
+
try {
|
|
2063
|
+
const res = await ai.run(model, {
|
|
2064
|
+
messages: [
|
|
2065
|
+
{ role: "system", content: nameplate_default.trimEnd() },
|
|
2066
|
+
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] }
|
|
2067
|
+
],
|
|
2068
|
+
// the budget rises with the retry's effort — elevated effort under
|
|
2069
|
+
// a flat budget starves the answer on this model (measured,
|
|
2070
|
+
// 2026-09-18: 5/38 at medium with the flat budget); the
|
|
2071
|
+
// temperature is a READING temperature — a transcription task at
|
|
2072
|
+
// default sampling hallucinated brands on a low-res plate
|
|
2073
|
+
// (observed live 2026-10-03: "Tektronix" on a CAS label)
|
|
2074
|
+
max_tokens: attempt === 0 ? 512 : 1536,
|
|
2075
|
+
reasoning_effort: attempt === 0 ? "low" : "medium",
|
|
2076
|
+
temperature: 0.15
|
|
2077
|
+
});
|
|
2078
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
2079
|
+
const np = parseNameplate(text ?? "");
|
|
2080
|
+
if (np) {
|
|
2081
|
+
console.log(`nameplate-extract: ${np.manufacturer} / ${np.model}${attempt ? " (on retry)" : ""}`);
|
|
2082
|
+
return np;
|
|
2083
|
+
}
|
|
2084
|
+
console.log(`nameplate-extract: no parse on attempt ${attempt + 1} (${(text ?? "").slice(0, 120)})`);
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
console.log(`nameplate-extract: FAILED attempt ${attempt + 1} ${String(e).slice(0, 160)}`);
|
|
2087
|
+
}
|
|
2069
2088
|
}
|
|
2089
|
+
return null;
|
|
2070
2090
|
}
|
|
2071
2091
|
|
|
2072
2092
|
// workers/worker_public/src/ask.ts
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export interface Nameplate {
|
|
2
2
|
manufacturer: string | null;
|
|
3
3
|
model: string | null;
|
|
4
|
+
certificate_number?: string | null;
|
|
4
5
|
}
|
|
5
6
|
export declare function parseNameplate(text: string): Nameplate | null;
|
|
6
|
-
/** The register query the extraction feeds:
|
|
7
|
-
* the
|
|
7
|
+
/** The register query the extraction feeds: a PRINTED CERTIFICATE NUMBER
|
|
8
|
+
* leads (it names the exact row), then the nameplate's tokens, then the
|
|
9
|
+
* question's family (if named) scopes. */
|
|
8
10
|
export declare function nameplateRegisterQuery(np: Nameplate, question: string): string;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
You read instrument nameplates. From the photograph, extract
|
|
2
|
-
manufacturer's name exactly as printed and the model designation exactly
|
|
3
|
-
as printed. Answer with ONLY a JSON object on one line:
|
|
1
|
+
You read instrument nameplates. From the photograph, extract:
|
|
4
2
|
|
|
5
|
-
{"manufacturer": "…", "model": "…"}
|
|
3
|
+
{"manufacturer": "…", "model": "…", "certificate_number": "…"}
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
6
|
+
- model: the model designation exactly as printed, else null.
|
|
7
|
+
- certificate_number: the OIML certificate number if one is printed
|
|
8
|
+
(it looks like R76/2006-A-GB1-18.08 — a family, an edition year, an
|
|
9
|
+
authority code, a year and a sequence), exactly as printed, else null.
|
|
10
|
+
|
|
11
|
+
Answer with ONLY the JSON object on one line. Never guess beyond what is
|
|
12
|
+
printed.
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
modelGroundingBlock,
|
|
7
7
|
scoreFaithfulness,
|
|
8
8
|
scoreJudge
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-GOYFWC3N.js";
|
|
10
10
|
import {
|
|
11
11
|
handleSearch
|
|
12
12
|
} from "../../chunk-KO6DRI7C.js";
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
sha256Hex,
|
|
64
64
|
today
|
|
65
65
|
} from "../../chunk-2PCUAJJO.js";
|
|
66
|
-
import "../../chunk-
|
|
66
|
+
import "../../chunk-7AQM3GSO.js";
|
|
67
67
|
import {
|
|
68
68
|
P,
|
|
69
69
|
setProfile
|
|
@@ -959,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
959
959
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
960
960
|
body: JSON.stringify({ ...args, stream: false })
|
|
961
961
|
});
|
|
962
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
962
|
+
const res = name === "ask" ? await (await import("../../ask-DKYIVPNV.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
|
|
963
963
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
964
964
|
});
|
|
965
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.77",
|
|
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",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
You read instrument nameplates. From the photograph, extract
|
|
2
|
-
manufacturer's name exactly as printed and the model designation exactly
|
|
3
|
-
as printed. Answer with ONLY a JSON object on one line:
|
|
1
|
+
You read instrument nameplates. From the photograph, extract:
|
|
4
2
|
|
|
5
|
-
{"manufacturer": "…", "model": "…"}
|
|
3
|
+
{"manufacturer": "…", "model": "…", "certificate_number": "…"}
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
6
|
+
- model: the model designation exactly as printed, else null.
|
|
7
|
+
- certificate_number: the OIML certificate number if one is printed
|
|
8
|
+
(it looks like R76/2006-A-GB1-18.08 — a family, an edition year, an
|
|
9
|
+
authority code, a year and a sequence), exactly as printed, else null.
|
|
10
|
+
|
|
11
|
+
Answer with ONLY the JSON object on one line. Never guess beyond what is
|
|
12
|
+
printed.
|
|
@@ -38,6 +38,14 @@ export function queryFamily(query: string): string | null {
|
|
|
38
38
|
return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** A PRINTED certificate number on a nameplate ("R76/2006-A-GB1-18.08")
|
|
42
|
+
* names the exact register row — it must not be shredded into word
|
|
43
|
+
* tokens that compete with it. */
|
|
44
|
+
export function printedCertificateNumber(query: string): string | null {
|
|
45
|
+
const m = /\b([RDMB]\s?-?\d{2,3}\s?\/\s?\d{4}\s?-\s?[A-Z0-9]+\s?-\s?[^\s,;]+(?:\s?\.\s?\d+)?)/i.exec(query);
|
|
46
|
+
return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export function registerTokens(query: string): string[] {
|
|
42
50
|
const stop = new Set([
|
|
43
51
|
"the", "a", "an", "is", "are", "was", "were", "still", "currently", "in", "on", "for", "of", "and", "or",
|
|
@@ -93,6 +101,26 @@ export async function searchRegister(db: any, query: string, force = false): Pro
|
|
|
93
101
|
// the shaping gate exists for ASK questions (is this a register
|
|
94
102
|
// question?); a TOOL invocation already decided — skip the gate
|
|
95
103
|
if (!force && !isRegisterShaped(query)) return null;
|
|
104
|
+
// a PRINTED certificate number names the row: query it first, and only
|
|
105
|
+
// fall back to the token arms when it misses (an extraction may drop a
|
|
106
|
+
// character; the prefix up to the sequence still scopes the family)
|
|
107
|
+
const printed = printedCertificateNumber(query);
|
|
108
|
+
if (printed) {
|
|
109
|
+
try {
|
|
110
|
+
const seq = printed.replace(/\.\d+$/, "");
|
|
111
|
+
const res = await db
|
|
112
|
+
.prepare(`SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE REPLACE(num, ' ', '') = ?1 OR REPLACE(num, ' ', '') LIKE ?2 || '%' ORDER BY num LIMIT 6`)
|
|
113
|
+
.bind(printed, seq)
|
|
114
|
+
.all();
|
|
115
|
+
if ((res.results ?? []).length) {
|
|
116
|
+
console.log(`register-search: printed-number hit ${printed} → ${(res.results ?? []).length} rows`);
|
|
117
|
+
return { rows: (res.results ?? []) as RegisterRow[], tokens: [printed] };
|
|
118
|
+
}
|
|
119
|
+
console.log(`register-search: printed number ${printed} missed the register — falling back to tokens`);
|
|
120
|
+
} catch {
|
|
121
|
+
// fall through to the token arms
|
|
122
|
+
}
|
|
123
|
+
}
|
|
96
124
|
const tokens = registerTokens(query);
|
|
97
125
|
if (!tokens.length) return null;
|
|
98
126
|
const built = buildRegisterQuery(tokens);
|
|
@@ -6,24 +6,30 @@
|
|
|
6
6
|
export interface Nameplate {
|
|
7
7
|
manufacturer: string | null;
|
|
8
8
|
model: string | null;
|
|
9
|
+
certificate_number?: string | null;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function parseNameplate(text: string): Nameplate | null {
|
|
12
13
|
const m = /\{[^{}]*\}/.exec(String(text ?? ""));
|
|
13
14
|
if (!m) return null;
|
|
14
15
|
try {
|
|
15
|
-
const j = JSON.parse(m[0]) as { manufacturer?: unknown; model?: unknown };
|
|
16
|
+
const j = JSON.parse(m[0]) as { manufacturer?: unknown; model?: unknown; certificate_number?: unknown };
|
|
16
17
|
const clean = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null);
|
|
17
|
-
const out = {
|
|
18
|
-
|
|
18
|
+
const out = {
|
|
19
|
+
manufacturer: clean(j.manufacturer),
|
|
20
|
+
model: clean(j.model),
|
|
21
|
+
certificate_number: clean(j.certificate_number),
|
|
22
|
+
};
|
|
23
|
+
return out.manufacturer || out.model || out.certificate_number ? out : null;
|
|
19
24
|
} catch {
|
|
20
25
|
return null;
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
/** The register query the extraction feeds:
|
|
25
|
-
* the
|
|
29
|
+
/** The register query the extraction feeds: a PRINTED CERTIFICATE NUMBER
|
|
30
|
+
* leads (it names the exact row), then the nameplate's tokens, then the
|
|
31
|
+
* question's family (if named) scopes. */
|
|
26
32
|
export function nameplateRegisterQuery(np: Nameplate, question: string): string {
|
|
27
33
|
const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
|
|
28
|
-
return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
34
|
+
return [np.certificate_number, np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
29
35
|
}
|
|
@@ -14,23 +14,40 @@ export type { Nameplate };
|
|
|
14
14
|
export { nameplateRegisterQuery };
|
|
15
15
|
|
|
16
16
|
export async function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
// one retry, decided on the RESULT not the error: the observed live
|
|
18
|
+
// failure (2026-10-03) is a structurally valid response carrying null
|
|
19
|
+
// fields — the same image the answer model read perfectly a moment
|
|
20
|
+
// later. A transient vision-call miss must not strand a certificate
|
|
21
|
+
// question with no register rows.
|
|
22
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
const res: any = await ai.run(model, {
|
|
25
|
+
messages: [
|
|
26
|
+
{ role: "system", content: nameplatePrompt.trimEnd() },
|
|
27
|
+
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] },
|
|
28
|
+
],
|
|
29
|
+
// the budget rises with the retry's effort — elevated effort under
|
|
30
|
+
// a flat budget starves the answer on this model (measured,
|
|
31
|
+
// 2026-09-18: 5/38 at medium with the flat budget); the
|
|
32
|
+
// temperature is a READING temperature — a transcription task at
|
|
33
|
+
// default sampling hallucinated brands on a low-res plate
|
|
34
|
+
// (observed live 2026-10-03: "Tektronix" on a CAS label)
|
|
35
|
+
max_tokens: attempt === 0 ? 512 : 1536,
|
|
36
|
+
reasoning_effort: attempt === 0 ? "low" : "medium",
|
|
37
|
+
temperature: 0.15,
|
|
38
|
+
});
|
|
39
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
40
|
+
const np = parseNameplate(text ?? "");
|
|
41
|
+
if (np) {
|
|
42
|
+
console.log(`nameplate-extract: ${np.manufacturer} / ${np.model}${attempt ? " (on retry)" : ""}`);
|
|
43
|
+
return np;
|
|
44
|
+
}
|
|
45
|
+
console.log(`nameplate-extract: no parse on attempt ${attempt + 1} (${(text ?? "").slice(0, 120)})`);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.log(`nameplate-extract: FAILED attempt ${attempt + 1} ${String(e).slice(0, 160)}`);
|
|
48
|
+
}
|
|
33
49
|
}
|
|
50
|
+
return null; // the bridge is optional — the ask never depends on it
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
export const nameplateModel = () => MODELS.member;
|