@konneal/engine 0.2.54 → 0.2.56
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-3TAC56RC.js → ask-5XMD22SJ.js} +1 -1
- package/dist/{chunk-OPJXHEZU.js → chunk-ZVMQHCNL.js} +70 -11
- package/dist/nameplate-parse.d.ts +8 -0
- package/dist/nameplate.d.ts +5 -0
- package/dist/prompts/nameplate.md +8 -0
- package/dist/worker_public/src/index.js +2 -2
- package/package.json +1 -1
- package/workers/worker_public/prompts/nameplate.md +8 -0
- package/workers/worker_public/src/ask.ts +37 -16
- package/workers/worker_public/src/certificates.ts +1 -1
- package/workers/worker_public/src/nameplate-parse.ts +29 -0
- package/workers/worker_public/src/nameplate.ts +33 -0
|
@@ -1866,7 +1866,7 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
|
|
|
1866
1866
|
|
|
1867
1867
|
// workers/worker_public/src/certificates.ts
|
|
1868
1868
|
function isRegisterShaped(query) {
|
|
1869
|
-
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were)\b/i.test(query);
|
|
1869
|
+
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);
|
|
1870
1870
|
}
|
|
1871
1871
|
function queryFamily(query) {
|
|
1872
1872
|
const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
|
|
@@ -1979,6 +1979,53 @@ function registerNote(rows) {
|
|
|
1979
1979
|
].join("\n");
|
|
1980
1980
|
}
|
|
1981
1981
|
|
|
1982
|
+
// workers/worker_public/prompts/nameplate.md
|
|
1983
|
+
var nameplate_default = `You read instrument nameplates. From the photograph, extract the
|
|
1984
|
+
manufacturer's name exactly as printed and the model designation exactly
|
|
1985
|
+
as printed. Answer with ONLY a JSON object on one line:
|
|
1986
|
+
|
|
1987
|
+
{"manufacturer": "\u2026", "model": "\u2026"}
|
|
1988
|
+
|
|
1989
|
+
If the photograph shows no manufacturer or no model, use null for the
|
|
1990
|
+
missing value. Never guess beyond what is printed.
|
|
1991
|
+
`;
|
|
1992
|
+
|
|
1993
|
+
// workers/worker_public/src/nameplate-parse.ts
|
|
1994
|
+
function parseNameplate(text) {
|
|
1995
|
+
const m = /\{[^{}]*\}/.exec(String(text ?? ""));
|
|
1996
|
+
if (!m) return null;
|
|
1997
|
+
try {
|
|
1998
|
+
const j = JSON.parse(m[0]);
|
|
1999
|
+
const clean = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null;
|
|
2000
|
+
const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
|
|
2001
|
+
return out.manufacturer || out.model ? out : null;
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function nameplateRegisterQuery(np, question) {
|
|
2007
|
+
const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
|
|
2008
|
+
return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// workers/worker_public/src/nameplate.ts
|
|
2012
|
+
async function extractNameplate(ai, model, image) {
|
|
2013
|
+
try {
|
|
2014
|
+
const res = await ai.run(model, {
|
|
2015
|
+
messages: [
|
|
2016
|
+
{ role: "system", content: nameplate_default.trimEnd() },
|
|
2017
|
+
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] }
|
|
2018
|
+
],
|
|
2019
|
+
max_tokens: 512,
|
|
2020
|
+
reasoning_effort: "low"
|
|
2021
|
+
});
|
|
2022
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
2023
|
+
return parseNameplate(text ?? "");
|
|
2024
|
+
} catch {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1982
2029
|
// workers/worker_public/src/ask.ts
|
|
1983
2030
|
function userImageDataUrl(body) {
|
|
1984
2031
|
const img = body?.image;
|
|
@@ -2222,20 +2269,22 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2222
2269
|
const warmQuery = retrievalQuery(q.query, prev);
|
|
2223
2270
|
const warmEmbed = embedWarm(env, warmQuery);
|
|
2224
2271
|
const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2272
|
+
try {
|
|
2273
|
+
if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
|
|
2274
|
+
const prior = await env.DB.prepare(
|
|
2275
|
+
"SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id JOIN conversations c ON c.id = m.conversation_id AND c.sub = ?2 WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1"
|
|
2276
|
+
).bind(conversationId, member.sub).first();
|
|
2277
|
+
if (prior) {
|
|
2231
2278
|
const obj = await env.CHAT_UPLOADS.get(prior.r2_key);
|
|
2232
2279
|
if (obj) {
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2280
|
+
const bytes = new Uint8Array(await obj.arrayBuffer());
|
|
2281
|
+
let bin = "";
|
|
2282
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2283
|
+
storedImage = `data:${prior.mime};base64,${btoa(bin)}`;
|
|
2235
2284
|
}
|
|
2236
|
-
} catch {
|
|
2237
2285
|
}
|
|
2238
2286
|
}
|
|
2287
|
+
} catch {
|
|
2239
2288
|
}
|
|
2240
2289
|
let convEntities = [];
|
|
2241
2290
|
if (conversationId) {
|
|
@@ -2330,7 +2379,17 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2330
2379
|
console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
|
|
2331
2380
|
const graphDocNumbers = await graphExpand(env, understanding);
|
|
2332
2381
|
const eNote = await editionNote(env, understanding);
|
|
2333
|
-
|
|
2382
|
+
let register = await searchRegister(env.DB, q.query);
|
|
2383
|
+
if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
|
|
2384
|
+
const np = await extractNameplate(env.AI, MODELS.member, withImage);
|
|
2385
|
+
if (np?.manufacturer) {
|
|
2386
|
+
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
|
|
2387
|
+
if (bridged?.rows?.length) {
|
|
2388
|
+
register = bridged;
|
|
2389
|
+
console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "\u2192", bridged.rows.length, "register rows");
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2334
2393
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
2335
2394
|
const regNote = register ? registerNote(register.rows) : void 0;
|
|
2336
2395
|
if (understanding?.intent === "conversational") {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Nameplate {
|
|
2
|
+
manufacturer: string | null;
|
|
3
|
+
model: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseNameplate(text: string): Nameplate | null;
|
|
6
|
+
/** The register query the extraction feeds: the nameplate's tokens lead,
|
|
7
|
+
* the question's family (if named) scopes. */
|
|
8
|
+
export declare function nameplateRegisterQuery(np: Nameplate, question: string): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
|
|
2
|
+
export type { Nameplate };
|
|
3
|
+
export { nameplateRegisterQuery };
|
|
4
|
+
export declare function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null>;
|
|
5
|
+
export declare const nameplateModel: () => "@cf/zai-org/glm-5.3-flash";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
You read instrument nameplates. From the photograph, extract the
|
|
2
|
+
manufacturer's name exactly as printed and the model designation exactly
|
|
3
|
+
as printed. Answer with ONLY a JSON object on one line:
|
|
4
|
+
|
|
5
|
+
{"manufacturer": "…", "model": "…"}
|
|
6
|
+
|
|
7
|
+
If the photograph shows no manufacturer or no model, use null for the
|
|
8
|
+
missing value. Never guess beyond what is printed.
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelGroundingBlock,
|
|
10
10
|
scoreFaithfulness,
|
|
11
11
|
scoreJudge
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-ZVMQHCNL.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
cfBlobs,
|
|
@@ -958,7 +958,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
958
958
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
959
959
|
body: JSON.stringify({ ...args, stream: false })
|
|
960
960
|
});
|
|
961
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
961
|
+
const res = name === "ask" ? await (await import("../../ask-5XMD22SJ.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
|
|
962
962
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
963
963
|
});
|
|
964
964
|
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.56",
|
|
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",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
You read instrument nameplates. From the photograph, extract the
|
|
2
|
+
manufacturer's name exactly as printed and the model designation exactly
|
|
3
|
+
as printed. Answer with ONLY a JSON object on one line:
|
|
4
|
+
|
|
5
|
+
{"manufacturer": "…", "model": "…"}
|
|
6
|
+
|
|
7
|
+
If the photograph shows no manufacturer or no model, use null for the
|
|
8
|
+
missing value. Never guess beyond what is printed.
|
|
@@ -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 { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
|
|
43
44
|
import { P } from "./profile.ts";
|
|
44
45
|
|
|
45
46
|
/** User-uploaded image for multimodal questions: a data URL
|
|
@@ -452,24 +453,30 @@ async function handleAsk(
|
|
|
452
453
|
const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
|
|
453
454
|
|
|
454
455
|
// Tier-2 continuation: no new image, but the conversation carries one —
|
|
455
|
-
// its most recent stored image re-attaches to this question
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
456
|
+
// its most recent stored image re-attaches to this question. The query
|
|
457
|
+
// scopes by the CONVERSATION'S OWNER (the messages table carries no
|
|
458
|
+
// sub; ownership rides the conversation row) and the whole resolution
|
|
459
|
+
// fails open: an optional feature never takes the ask down.
|
|
460
|
+
try {
|
|
461
|
+
if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
|
|
462
|
+
const prior = await env.DB.prepare(
|
|
463
|
+
"SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id JOIN conversations c ON c.id = m.conversation_id AND c.sub = ?2 WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1",
|
|
464
|
+
).bind(conversationId, member.sub).first<{ aid: string; mime: string; r2_key: string }>();
|
|
465
|
+
if (prior) {
|
|
466
|
+
const obj = await (env as any).CHAT_UPLOADS.get(prior.r2_key);
|
|
467
|
+
if (obj) {
|
|
468
|
+
const bytes = new Uint8Array(await obj.arrayBuffer());
|
|
469
|
+
let bin = "";
|
|
470
|
+
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
471
|
+
storedImage = `data:${prior.mime};base64,${btoa(bin)}`;
|
|
472
|
+
}
|
|
469
473
|
}
|
|
470
474
|
}
|
|
475
|
+
} catch {
|
|
476
|
+
/* the reuse path is optional — the ask answers without the image */
|
|
471
477
|
}
|
|
472
|
-
|
|
478
|
+
|
|
479
|
+
let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
473
480
|
if (conversationId) {
|
|
474
481
|
try {
|
|
475
482
|
const rows = await env.DB.prepare("SELECT entity, kind FROM conversation_entities WHERE conversation_id = ?1 LIMIT 12").bind(conversationId).all();
|
|
@@ -613,7 +620,21 @@ async function handleAsk(
|
|
|
613
620
|
// register-shaped question queries the D1 table directly and the rows
|
|
614
621
|
// join the prompt as an authoritative note — exact matches, never a
|
|
615
622
|
// similarity guess about certification standing
|
|
616
|
-
|
|
623
|
+
let register = await searchRegister(env.DB, q.query);
|
|
624
|
+
// the nameplate bridge (TODO.new-era/6's image lane): a certificate
|
|
625
|
+
// question WITH a photograph carries its manufacturer in the pixels —
|
|
626
|
+
// one vision call extracts it, and those tokens re-query the register
|
|
627
|
+
// so the answer cites the actual certificate rows
|
|
628
|
+
if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
|
|
629
|
+
const np = await extractNameplate((env as any).AI, MODELS.member, withImage);
|
|
630
|
+
if (np?.manufacturer) {
|
|
631
|
+
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
|
|
632
|
+
if (bridged?.rows?.length) {
|
|
633
|
+
register = bridged;
|
|
634
|
+
console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "→", bridged.rows.length, "register rows");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
617
638
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
618
639
|
const regNote = register ? registerNote(register.rows) : undefined;
|
|
619
640
|
|
|
@@ -22,7 +22,7 @@ export interface RegisterRow {
|
|
|
22
22
|
* trigger the citation-graph notes already use (query-shaped notes,
|
|
23
23
|
* never a router). */
|
|
24
24
|
export function isRegisterShaped(query: string): boolean {
|
|
25
|
-
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were)\b/i.test(query);
|
|
25
|
+
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);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Search tokens: words that can identify a holder or a model — words of
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The nameplate bridge's pure half (TODO.new-era/6's image lane): parse
|
|
2
|
+
// the vision call's answer into {manufacturer, model}. Lives apart from
|
|
3
|
+
// the AI-call module so the plain-node suites can exercise it (the
|
|
4
|
+
// prompt is a bundled .md, which only esbuild can load).
|
|
5
|
+
|
|
6
|
+
export interface Nameplate {
|
|
7
|
+
manufacturer: string | null;
|
|
8
|
+
model: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseNameplate(text: string): Nameplate | null {
|
|
12
|
+
const m = /\{[^{}]*\}/.exec(String(text ?? ""));
|
|
13
|
+
if (!m) return null;
|
|
14
|
+
try {
|
|
15
|
+
const j = JSON.parse(m[0]) as { manufacturer?: unknown; model?: unknown };
|
|
16
|
+
const clean = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null);
|
|
17
|
+
const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
|
|
18
|
+
return out.manufacturer || out.model ? out : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The register query the extraction feeds: the nameplate's tokens lead,
|
|
25
|
+
* the question's family (if named) scopes. */
|
|
26
|
+
export function nameplateRegisterQuery(np: Nameplate, question: string): string {
|
|
27
|
+
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(" ");
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The nameplate bridge (TODO.new-era/6, the image lane): a certificate
|
|
2
|
+
// question that arrives WITH a photograph carries its manufacturer only
|
|
3
|
+
// in the pixels. A single low-effort vision call extracts the printed
|
|
4
|
+
// manufacturer and model, and those tokens feed the certificate
|
|
5
|
+
// register's D1 lane — so "does this load cell's manufacturer hold the
|
|
6
|
+
// certificate" answers with the actual register rows instead of advice.
|
|
7
|
+
// The shape rules are the 2026-09-09 multimodal findings: the image
|
|
8
|
+
// rides its OWN short user message, never a long passage.
|
|
9
|
+
import nameplatePrompt from "../prompts/nameplate.md";
|
|
10
|
+
import { parseNameplate, nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
|
|
11
|
+
import { MODELS } from "./config.ts";
|
|
12
|
+
|
|
13
|
+
export type { Nameplate };
|
|
14
|
+
export { nameplateRegisterQuery };
|
|
15
|
+
|
|
16
|
+
export async function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null> {
|
|
17
|
+
try {
|
|
18
|
+
const res: any = await ai.run(model, {
|
|
19
|
+
messages: [
|
|
20
|
+
{ role: "system", content: nameplatePrompt.trimEnd() },
|
|
21
|
+
{ role: "user", content: [{ type: "image_url", image_url: { url: image } }] },
|
|
22
|
+
],
|
|
23
|
+
max_tokens: 512,
|
|
24
|
+
reasoning_effort: "low",
|
|
25
|
+
});
|
|
26
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
27
|
+
return parseNameplate(text ?? "");
|
|
28
|
+
} catch {
|
|
29
|
+
return null; // the bridge is optional — the ask never depends on it
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const nameplateModel = () => MODELS.member;
|