@konneal/engine 0.2.76 → 0.2.78
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-PK75QJ66.js → ask-IANJ4PXT.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-64G44J7U.js → chunk-JIN6Y4PD.js} +37 -13
- package/dist/nameplate-parse.d.ts +4 -2
- package/dist/prompts/nameplate.md +17 -6
- package/dist/worker_public/src/index.js +3 -3
- package/package.json +1 -1
- package/workers/worker_public/prompts/nameplate.md +17 -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 +10 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JIN6Y4PD.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,25 @@ ${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.
|
|
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. Work in two steps.
|
|
2026
2024
|
|
|
2027
|
-
|
|
2025
|
+
Step 1 \u2014 transcribe: list every inscription you can actually read on the
|
|
2026
|
+
photograph, verbatim: names, model designations, serials, accuracy-class
|
|
2027
|
+
markings, capacities with units, and any certificate or approval number
|
|
2028
|
+
(it looks like R76/2006-A-GB1-18.08 \u2014 a family, an edition year, an
|
|
2029
|
+
authority code, a year and a sequence).
|
|
2028
2030
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
+
Step 2 \u2014 answer with ONLY a JSON object on one line:
|
|
2032
|
+
|
|
2033
|
+
{"manufacturer": "\u2026", "model": "\u2026", "certificate_number": "\u2026"}
|
|
2034
|
+
|
|
2035
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
2036
|
+
- model: the model designation exactly as printed, else null.
|
|
2037
|
+
- certificate_number: the OIML certificate number if one is printed,
|
|
2038
|
+
exactly as printed, else null.
|
|
2039
|
+
|
|
2040
|
+
Never guess beyond what is printed; a low-quality photograph yields nulls,
|
|
2041
|
+
not inventions.
|
|
2031
2042
|
`;
|
|
2032
2043
|
|
|
2033
2044
|
// workers/worker_public/src/nameplate-parse.ts
|
|
@@ -2037,15 +2048,19 @@ function parseNameplate(text) {
|
|
|
2037
2048
|
try {
|
|
2038
2049
|
const j = JSON.parse(m[0]);
|
|
2039
2050
|
const clean = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null;
|
|
2040
|
-
const out = {
|
|
2041
|
-
|
|
2051
|
+
const out = {
|
|
2052
|
+
manufacturer: clean(j.manufacturer),
|
|
2053
|
+
model: clean(j.model),
|
|
2054
|
+
certificate_number: clean(j.certificate_number)
|
|
2055
|
+
};
|
|
2056
|
+
return out.manufacturer || out.model || out.certificate_number ? out : null;
|
|
2042
2057
|
} catch {
|
|
2043
2058
|
return null;
|
|
2044
2059
|
}
|
|
2045
2060
|
}
|
|
2046
2061
|
function nameplateRegisterQuery(np, question) {
|
|
2047
2062
|
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(" ");
|
|
2063
|
+
return [np.certificate_number, np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
2049
2064
|
}
|
|
2050
2065
|
|
|
2051
2066
|
// workers/worker_public/src/nameplate.ts
|
|
@@ -2055,13 +2070,22 @@ async function extractNameplate(ai, model, image) {
|
|
|
2055
2070
|
const res = await ai.run(model, {
|
|
2056
2071
|
messages: [
|
|
2057
2072
|
{ role: "system", content: nameplate_default.trimEnd() },
|
|
2073
|
+
// the question rides along: the transcription-first framing that
|
|
2074
|
+
// succeeds in the answer path works better WITH its context than
|
|
2075
|
+
// as an isolated extract-a-JSON task (the isolated form returned
|
|
2076
|
+
// nulls on plates the answer model read every time)
|
|
2077
|
+
{ role: "user", content: "This photograph accompanies the user's certificate question. Transcribe what you can read, then answer with the JSON object." },
|
|
2058
2078
|
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] }
|
|
2059
2079
|
],
|
|
2060
2080
|
// the budget rises with the retry's effort — elevated effort under
|
|
2061
2081
|
// a flat budget starves the answer on this model (measured,
|
|
2062
|
-
// 2026-09-18: 5/38 at medium with the flat budget)
|
|
2082
|
+
// 2026-09-18: 5/38 at medium with the flat budget); the
|
|
2083
|
+
// temperature is a READING temperature — a transcription task at
|
|
2084
|
+
// default sampling hallucinated brands on a low-res plate
|
|
2085
|
+
// (observed live 2026-10-03: "Tektronix" on a CAS label)
|
|
2063
2086
|
max_tokens: attempt === 0 ? 512 : 1536,
|
|
2064
|
-
reasoning_effort: attempt === 0 ? "low" : "medium"
|
|
2087
|
+
reasoning_effort: attempt === 0 ? "low" : "medium",
|
|
2088
|
+
temperature: 0.15
|
|
2065
2089
|
});
|
|
2066
2090
|
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
2067
2091
|
const np = parseNameplate(text ?? "");
|
|
@@ -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,19 @@
|
|
|
1
|
-
You read instrument nameplates.
|
|
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. Work in two steps.
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
Step 1 — transcribe: list every inscription you can actually read on the
|
|
4
|
+
photograph, verbatim: names, model designations, serials, accuracy-class
|
|
5
|
+
markings, capacities with units, and any certificate or approval number
|
|
6
|
+
(it looks like R76/2006-A-GB1-18.08 — a family, an edition year, an
|
|
7
|
+
authority code, a year and a sequence).
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
Step 2 — answer with ONLY a JSON object on one line:
|
|
10
|
+
|
|
11
|
+
{"manufacturer": "…", "model": "…", "certificate_number": "…"}
|
|
12
|
+
|
|
13
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
14
|
+
- model: the model designation exactly as printed, else null.
|
|
15
|
+
- certificate_number: the OIML certificate number if one is printed,
|
|
16
|
+
exactly as printed, else null.
|
|
17
|
+
|
|
18
|
+
Never guess beyond what is printed; a low-quality photograph yields nulls,
|
|
19
|
+
not inventions.
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
modelGroundingBlock,
|
|
7
7
|
scoreFaithfulness,
|
|
8
8
|
scoreJudge
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-JIN6Y4PD.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-IANJ4PXT.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.78",
|
|
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,19 @@
|
|
|
1
|
-
You read instrument nameplates.
|
|
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. Work in two steps.
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
Step 1 — transcribe: list every inscription you can actually read on the
|
|
4
|
+
photograph, verbatim: names, model designations, serials, accuracy-class
|
|
5
|
+
markings, capacities with units, and any certificate or approval number
|
|
6
|
+
(it looks like R76/2006-A-GB1-18.08 — a family, an edition year, an
|
|
7
|
+
authority code, a year and a sequence).
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
Step 2 — answer with ONLY a JSON object on one line:
|
|
10
|
+
|
|
11
|
+
{"manufacturer": "…", "model": "…", "certificate_number": "…"}
|
|
12
|
+
|
|
13
|
+
- manufacturer: the manufacturer's name exactly as printed, else null.
|
|
14
|
+
- model: the model designation exactly as printed, else null.
|
|
15
|
+
- certificate_number: the OIML certificate number if one is printed,
|
|
16
|
+
exactly as printed, else null.
|
|
17
|
+
|
|
18
|
+
Never guess beyond what is printed; a low-quality photograph yields nulls,
|
|
19
|
+
not inventions.
|
|
@@ -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
|
}
|
|
@@ -24,13 +24,22 @@ export async function extractNameplate(ai: any, model: string, image: string): P
|
|
|
24
24
|
const res: any = await ai.run(model, {
|
|
25
25
|
messages: [
|
|
26
26
|
{ role: "system", content: nameplatePrompt.trimEnd() },
|
|
27
|
+
// the question rides along: the transcription-first framing that
|
|
28
|
+
// succeeds in the answer path works better WITH its context than
|
|
29
|
+
// as an isolated extract-a-JSON task (the isolated form returned
|
|
30
|
+
// nulls on plates the answer model read every time)
|
|
31
|
+
{ role: "user", content: "This photograph accompanies the user's certificate question. Transcribe what you can read, then answer with the JSON object." },
|
|
27
32
|
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] },
|
|
28
33
|
],
|
|
29
34
|
// the budget rises with the retry's effort — elevated effort under
|
|
30
35
|
// a flat budget starves the answer on this model (measured,
|
|
31
|
-
// 2026-09-18: 5/38 at medium with the flat budget)
|
|
36
|
+
// 2026-09-18: 5/38 at medium with the flat budget); the
|
|
37
|
+
// temperature is a READING temperature — a transcription task at
|
|
38
|
+
// default sampling hallucinated brands on a low-res plate
|
|
39
|
+
// (observed live 2026-10-03: "Tektronix" on a CAS label)
|
|
32
40
|
max_tokens: attempt === 0 ? 512 : 1536,
|
|
33
41
|
reasoning_effort: attempt === 0 ? "low" : "medium",
|
|
42
|
+
temperature: 0.15,
|
|
34
43
|
});
|
|
35
44
|
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
36
45
|
const np = parseNameplate(text ?? "");
|