@konneal/engine 0.2.52 → 0.2.54
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-27TMC7PV.js → ask-3TAC56RC.js} +1 -1
- package/dist/{chunk-SU4FO7TN.js → chunk-OPJXHEZU.js} +37 -5
- package/dist/openapi-surface.gen.d.ts +1 -1
- package/dist/openapi-types.d.ts +0 -89
- package/dist/worker_public/src/index.js +13 -53
- package/package.json +1 -1
- package/workers/worker_public/migrations/0018_messages_attachment.sql +4 -0
- package/workers/worker_public/openapi.yaml +0 -52
- package/workers/worker_public/schema.sql +3 -0
- package/workers/worker_public/src/ask.ts +49 -4
- package/workers/worker_public/src/conversations.ts +12 -4
- package/workers/worker_public/src/index.ts +1 -38
- package/workers/worker_public/src/openapi-surface.gen.ts +0 -12
|
@@ -2181,13 +2181,31 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2181
2181
|
if (body?.image != null && !userImage) {
|
|
2182
2182
|
return err(400, "invalid_image", "image must be a data URL (data:image/png|jpeg|webp|gif;base64,\u2026) up to 6 MB");
|
|
2183
2183
|
}
|
|
2184
|
+
let storedImage = null;
|
|
2185
|
+
const attachIds = Array.isArray(body?.attachment_ids) ? body.attachment_ids.filter((x) => typeof x === "string" && /^[0-9a-f-]{36}$/i.test(x)).slice(0, 2) : [];
|
|
2186
|
+
if (attachIds.length && env.DB && member) {
|
|
2187
|
+
for (const aid of attachIds) {
|
|
2188
|
+
const row = await env.DB.prepare("SELECT mime, r2_key FROM attachments WHERE id = ?1 AND sub = ?2").bind(aid, member.sub).first();
|
|
2189
|
+
if (!row) continue;
|
|
2190
|
+
try {
|
|
2191
|
+
const obj = await env.CHAT_UPLOADS.get(row.r2_key);
|
|
2192
|
+
if (!obj) continue;
|
|
2193
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
|
|
2194
|
+
storedImage = `data:${row.mime};base64,${b64}`;
|
|
2195
|
+
break;
|
|
2196
|
+
} catch {
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
const effectiveImage = userImage ?? storedImage;
|
|
2184
2201
|
const budget = num(env, "INPUT_TOKEN_BUDGET", LIMITS.inputTokenBudget);
|
|
2185
2202
|
const { kept: keptHistory, overflow } = splitHistory(history, budget);
|
|
2186
2203
|
const summary = overflow.length >= 2 ? await summarizeHistory(env, MODELS.understand, overflow) ?? void 0 : void 0;
|
|
2187
2204
|
let retrieved;
|
|
2188
2205
|
const fresh = freshRequested(body);
|
|
2189
2206
|
const gen = await corpusGen(env.CACHE);
|
|
2190
|
-
const
|
|
2207
|
+
const withImage = effectiveImage || storedImage;
|
|
2208
|
+
const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
|
|
2191
2209
|
const wantsStream = body?.stream === true || tier === "anon" && body?.stream !== false;
|
|
2192
2210
|
if (cached) {
|
|
2193
2211
|
telemetry(env, ctx, tier, "ask", null, true, (cached.value.answer ?? "").length, cached.value.query_hash, q.lang, "exact", telemetryMeta());
|
|
@@ -2204,6 +2222,21 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2204
2222
|
const warmQuery = retrievalQuery(q.query, prev);
|
|
2205
2223
|
const warmEmbed = embedWarm(env, warmQuery);
|
|
2206
2224
|
const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
|
|
2225
|
+
if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
|
|
2226
|
+
const prior = await env.DB.prepare(
|
|
2227
|
+
"SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1"
|
|
2228
|
+
).bind(conversationId, member.sub).first();
|
|
2229
|
+
if (prior) {
|
|
2230
|
+
try {
|
|
2231
|
+
const obj = await env.CHAT_UPLOADS.get(prior.r2_key);
|
|
2232
|
+
if (obj) {
|
|
2233
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
|
|
2234
|
+
storedImage = `data:${prior.mime};base64,${b64}`;
|
|
2235
|
+
}
|
|
2236
|
+
} catch {
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2207
2240
|
let convEntities = [];
|
|
2208
2241
|
if (conversationId) {
|
|
2209
2242
|
try {
|
|
@@ -2651,18 +2684,18 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2651
2684
|
budget
|
|
2652
2685
|
);
|
|
2653
2686
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
2654
|
-
if (
|
|
2687
|
+
if (withImage) {
|
|
2655
2688
|
const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
|
|
2656
2689
|
const last = messages[messages.length - 1];
|
|
2657
2690
|
const note = wantsMarkings ? "\n\n(The user attached a photo with this question. First transcribe every inscription you can actually read in the image \u2014 names, model references, accuracy classes, numeric values with their units, certificate or approval numbers \u2014 quoting them verbatim. Then answer the question from the numbered passages, citing the requirement each interpretation rests on. If an inscription is unreadable, say so; never invent a marking.)" : "\n\n(The user attached an image with this question; interpret it directly when answering.)";
|
|
2658
2691
|
if (Array.isArray(last.content)) {
|
|
2659
2692
|
const textPart = last.content.find((p) => p.type === "text");
|
|
2660
2693
|
if (textPart) textPart.text += note;
|
|
2661
|
-
last.content = [...last.content, { type: "image_url", image_url: { url:
|
|
2694
|
+
last.content = [...last.content, { type: "image_url", image_url: { url: withImage } }];
|
|
2662
2695
|
} else {
|
|
2663
2696
|
last.content = [
|
|
2664
2697
|
{ type: "text", text: last.content + note },
|
|
2665
|
-
{ type: "image_url", image_url: { url:
|
|
2698
|
+
{ type: "image_url", image_url: { url: withImage } }
|
|
2666
2699
|
];
|
|
2667
2700
|
}
|
|
2668
2701
|
console.log("user image attached to generation");
|
|
@@ -2875,7 +2908,6 @@ export {
|
|
|
2875
2908
|
handleMemories,
|
|
2876
2909
|
scoreFaithfulness,
|
|
2877
2910
|
checkQuoteAnchors,
|
|
2878
|
-
standardForDocNumber,
|
|
2879
2911
|
bindModelNode,
|
|
2880
2912
|
modelGroundingBlock,
|
|
2881
2913
|
scoreJudge,
|
|
@@ -3,7 +3,7 @@ export interface OpenApiRoute {
|
|
|
3
3
|
pattern: string;
|
|
4
4
|
operationId: string;
|
|
5
5
|
}
|
|
6
|
-
export type OpenApiOperationId = "askAnonymous" | "askKeyed" | "search" | "searchKeyed" | "
|
|
6
|
+
export type OpenApiOperationId = "askAnonymous" | "askKeyed" | "search" | "searchKeyed" | "verify" | "verifyKeyed" | "research" | "researchKeyed" | "mcp" | "datasets" | "keyUsage" | "health" | "listConversations" | "createConversation" | "getConversation" | "renameConversation" | "deleteConversation" | "appendMessage" | "shareConversation" | "getShared" | "uploadAttachment" | "listAttachments" | "getAttachment" | "deleteAttachment" | "listMemories" | "createMemory" | "deleteMemory" | "listProjects" | "createProject" | "deleteProject" | "listProjectFiles" | "attachProjectFile" | "detachProjectFile" | "laneQuery" | "laneKeyed" | "feedback" | "adminEnrich" | "adminEnrichAlias" | "adminSection" | "adminSectionAlias" | "adminVectors" | "adminCaption" | "adminJudge" | "adminJudgeAlias" | "adminRevokeKey" | "authMe" | "authLogin" | "authCallback" | "authLogout" | "authLogoutLink" | "adminStats" | "adminListKeys" | "adminCreateKey";
|
|
7
7
|
export declare const OPENAPI_SURFACE: readonly (Omit<OpenApiRoute, "operationId"> & {
|
|
8
8
|
operationId: OpenApiOperationId;
|
|
9
9
|
})[];
|
package/dist/openapi-types.d.ts
CHANGED
|
@@ -86,43 +86,6 @@ export interface paths {
|
|
|
86
86
|
patch?: never;
|
|
87
87
|
trace?: never;
|
|
88
88
|
};
|
|
89
|
-
"/api/absence": {
|
|
90
|
-
parameters: {
|
|
91
|
-
query?: never;
|
|
92
|
-
header?: never;
|
|
93
|
-
path?: never;
|
|
94
|
-
cookie?: never;
|
|
95
|
-
};
|
|
96
|
-
get?: never;
|
|
97
|
-
put?: never;
|
|
98
|
-
/**
|
|
99
|
-
* Request a provable absence certificate
|
|
100
|
-
* @description Enumerates the entire machine-readable model of the named standard and reports how many nodes were examined and whether any matched the topic. A verdict of absent is an enumeration certificate that states its scope, not a statement that a search found nothing.
|
|
101
|
-
*/
|
|
102
|
-
post: operations["absence"];
|
|
103
|
-
delete?: never;
|
|
104
|
-
options?: never;
|
|
105
|
-
head?: never;
|
|
106
|
-
patch?: never;
|
|
107
|
-
trace?: never;
|
|
108
|
-
};
|
|
109
|
-
"/v1/absence": {
|
|
110
|
-
parameters: {
|
|
111
|
-
query?: never;
|
|
112
|
-
header?: never;
|
|
113
|
-
path?: never;
|
|
114
|
-
cookie?: never;
|
|
115
|
-
};
|
|
116
|
-
get?: never;
|
|
117
|
-
put?: never;
|
|
118
|
-
/** Request a provable absence certificate with an API key */
|
|
119
|
-
post: operations["absenceKeyed"];
|
|
120
|
-
delete?: never;
|
|
121
|
-
options?: never;
|
|
122
|
-
head?: never;
|
|
123
|
-
patch?: never;
|
|
124
|
-
trace?: never;
|
|
125
|
-
};
|
|
126
89
|
"/api/verify": {
|
|
127
90
|
parameters: {
|
|
128
91
|
query?: never;
|
|
@@ -1089,58 +1052,6 @@ export interface operations {
|
|
|
1089
1052
|
};
|
|
1090
1053
|
};
|
|
1091
1054
|
};
|
|
1092
|
-
absence: {
|
|
1093
|
-
parameters: {
|
|
1094
|
-
query?: never;
|
|
1095
|
-
header?: never;
|
|
1096
|
-
path?: never;
|
|
1097
|
-
cookie?: never;
|
|
1098
|
-
};
|
|
1099
|
-
requestBody: {
|
|
1100
|
-
content: {
|
|
1101
|
-
"application/json": {
|
|
1102
|
-
/** @description The publication, for example R 60. */
|
|
1103
|
-
standard: string;
|
|
1104
|
-
/** @description The topic whose absence is to be proven. */
|
|
1105
|
-
topic: string;
|
|
1106
|
-
};
|
|
1107
|
-
};
|
|
1108
|
-
};
|
|
1109
|
-
responses: {
|
|
1110
|
-
/** @description The enumeration certificate. */
|
|
1111
|
-
200: {
|
|
1112
|
-
headers: {
|
|
1113
|
-
[name: string]: unknown;
|
|
1114
|
-
};
|
|
1115
|
-
content?: never;
|
|
1116
|
-
};
|
|
1117
|
-
};
|
|
1118
|
-
};
|
|
1119
|
-
absenceKeyed: {
|
|
1120
|
-
parameters: {
|
|
1121
|
-
query?: never;
|
|
1122
|
-
header?: never;
|
|
1123
|
-
path?: never;
|
|
1124
|
-
cookie?: never;
|
|
1125
|
-
};
|
|
1126
|
-
requestBody: {
|
|
1127
|
-
content: {
|
|
1128
|
-
"application/json": {
|
|
1129
|
-
standard: string;
|
|
1130
|
-
topic: string;
|
|
1131
|
-
};
|
|
1132
|
-
};
|
|
1133
|
-
};
|
|
1134
|
-
responses: {
|
|
1135
|
-
/** @description The enumeration certificate. */
|
|
1136
|
-
200: {
|
|
1137
|
-
headers: {
|
|
1138
|
-
[name: string]: unknown;
|
|
1139
|
-
};
|
|
1140
|
-
content?: never;
|
|
1141
|
-
};
|
|
1142
|
-
};
|
|
1143
|
-
};
|
|
1144
1055
|
verify: {
|
|
1145
1056
|
parameters: {
|
|
1146
1057
|
query?: never;
|
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
handleMemories,
|
|
9
9
|
modelGroundingBlock,
|
|
10
10
|
scoreFaithfulness,
|
|
11
|
-
scoreJudge
|
|
12
|
-
|
|
13
|
-
} from "../../chunk-SU4FO7TN.js";
|
|
11
|
+
scoreJudge
|
|
12
|
+
} from "../../chunk-OPJXHEZU.js";
|
|
14
13
|
import {
|
|
15
14
|
buildMessages,
|
|
16
15
|
cfBlobs,
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
handleLogin,
|
|
26
25
|
handleLogout,
|
|
27
26
|
handleMe,
|
|
28
|
-
namedDocumentIn,
|
|
29
27
|
opCfg,
|
|
30
28
|
opMemberCanWrite,
|
|
31
29
|
opTokenMember,
|
|
@@ -103,11 +101,11 @@ async function handleConversations(env, sub, req, route) {
|
|
|
103
101
|
let msgs;
|
|
104
102
|
try {
|
|
105
103
|
msgs = await env.DB.prepare(
|
|
106
|
-
"SELECT id, role, content, citations, model, context_applied, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC"
|
|
104
|
+
"SELECT id, role, content, citations, model, context_applied, attachment_id, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC"
|
|
107
105
|
).bind(id).all();
|
|
108
106
|
} catch {
|
|
109
107
|
msgs = await env.DB.prepare(
|
|
110
|
-
"SELECT id, role, content, citations, model, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC"
|
|
108
|
+
"SELECT id, role, content, citations, model, attachment_id, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC"
|
|
111
109
|
).bind(id).all();
|
|
112
110
|
}
|
|
113
111
|
return json({
|
|
@@ -151,6 +149,12 @@ async function handleAppendMessage(env, sub, req, convId) {
|
|
|
151
149
|
if (role !== "user" && role !== "assistant" || !content.trim() || content.length > LIMITS.maxOutputTokens * 4) {
|
|
152
150
|
return err(400, "invalid_input", "role (user|assistant) and content are required");
|
|
153
151
|
}
|
|
152
|
+
let attachmentId = null;
|
|
153
|
+
if (typeof body?.attachment_id === "string" && body.attachment_id) {
|
|
154
|
+
const owned = await env.DB.prepare("SELECT id FROM attachments WHERE id = ?1 AND sub = ?2").bind(body.attachment_id, sub).first();
|
|
155
|
+
if (!owned) return err(404, "not_found", "No such attachment");
|
|
156
|
+
attachmentId = body.attachment_id;
|
|
157
|
+
}
|
|
154
158
|
let citations2 = null;
|
|
155
159
|
if (body?.citations != null) {
|
|
156
160
|
if (!Array.isArray(body.citations) || body.citations.length > 16) {
|
|
@@ -166,8 +170,8 @@ async function handleAppendMessage(env, sub, req, convId) {
|
|
|
166
170
|
try {
|
|
167
171
|
await env.DB.batch([
|
|
168
172
|
env.DB.prepare(
|
|
169
|
-
"INSERT INTO messages (id, conversation_id, role, content, citations, model, context_applied, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)"
|
|
170
|
-
).bind(mid, convId, role, content, citations2, typeof body?.model === "string" ? body.model.slice(0, 80) : null, applied ? JSON.stringify(applied) : null, now),
|
|
173
|
+
"INSERT INTO messages (id, conversation_id, role, content, citations, model, context_applied, attachment_id, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)"
|
|
174
|
+
).bind(mid, convId, role, content, citations2, typeof body?.model === "string" ? body.model.slice(0, 80) : null, applied ? JSON.stringify(applied) : null, attachmentId, now),
|
|
171
175
|
env.DB.prepare("UPDATE conversations SET updated_at = ?1 WHERE id = ?2 AND sub = ?3").bind(now, convId, sub)
|
|
172
176
|
]);
|
|
173
177
|
} catch (e) {
|
|
@@ -954,7 +958,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
954
958
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
955
959
|
body: JSON.stringify({ ...args, stream: false })
|
|
956
960
|
});
|
|
957
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
961
|
+
const res = name === "ask" ? await (await import("../../ask-3TAC56RC.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
|
|
958
962
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
959
963
|
});
|
|
960
964
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
|
@@ -1014,16 +1018,6 @@ var OPENAPI_SURFACE = [
|
|
|
1014
1018
|
"pattern": "/v1/search",
|
|
1015
1019
|
"operationId": "searchKeyed"
|
|
1016
1020
|
},
|
|
1017
|
-
{
|
|
1018
|
-
"method": "POST",
|
|
1019
|
-
"pattern": "/api/absence",
|
|
1020
|
-
"operationId": "absence"
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
"method": "POST",
|
|
1024
|
-
"pattern": "/v1/absence",
|
|
1025
|
-
"operationId": "absenceKeyed"
|
|
1026
|
-
},
|
|
1027
1021
|
{
|
|
1028
1022
|
"method": "POST",
|
|
1029
1023
|
"pattern": "/api/verify",
|
|
@@ -1450,38 +1444,6 @@ async function adminStatsRoute(c) {
|
|
|
1450
1444
|
pruned: "telemetry >90d"
|
|
1451
1445
|
}, 200, corsHeaders(req));
|
|
1452
1446
|
}
|
|
1453
|
-
async function absenceRoute(c) {
|
|
1454
|
-
const { env, req } = c;
|
|
1455
|
-
const t = await tierFor(c);
|
|
1456
|
-
if (t instanceof Response) return t;
|
|
1457
|
-
const body = await readJson(req);
|
|
1458
|
-
const namedStd = namedDocumentIn(String(body?.standard ?? ""));
|
|
1459
|
-
const standard = standardForDocNumber(namedStd?.doc_number ?? String(body?.standard ?? "").trim());
|
|
1460
|
-
const topic = String(body?.topic ?? "").trim().toLowerCase();
|
|
1461
|
-
if (!standard || !topic) return err(400, "invalid_input", "standard and topic are required");
|
|
1462
|
-
try {
|
|
1463
|
-
const nodes = (await env.DB.prepare("SELECT node_id, kind, name, content FROM model_nodes WHERE standard = ?1").bind(standard).all()).results ?? [];
|
|
1464
|
-
const tokens = topic.split(/\s+/).filter((t2) => t2.length > 2);
|
|
1465
|
-
const matches = [];
|
|
1466
|
-
for (const n of nodes) {
|
|
1467
|
-
const hay = `${n.name ?? ""} ${n.content ?? ""}`.toLowerCase();
|
|
1468
|
-
if (tokens.some((tok) => hay.includes(tok))) {
|
|
1469
|
-
matches.push({ node_id: n.node_id, kind: n.kind });
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
const chunks = await env.DB.prepare("SELECT COUNT(*) AS n FROM chunks WHERE corpus = 'smart-model' AND (docidentifier LIKE ?1 OR doc_id LIKE ?2)").bind(`%${body?.standard}%`, `%${body?.standard}%`).first();
|
|
1473
|
-
return json({
|
|
1474
|
-
standard,
|
|
1475
|
-
topic,
|
|
1476
|
-
enumerated: { model_nodes: nodes.length, smart_model_chunks: chunks?.n ?? 0 },
|
|
1477
|
-
matches: matches.slice(0, 20),
|
|
1478
|
-
verdict: matches.length === 0 ? "absent" : "present",
|
|
1479
|
-
scope: `the machine-readable model of ${standard} (all model nodes) \u2014 the enumeration is exhaustive over that scope; prose outside the modeled families is not claimed`
|
|
1480
|
-
});
|
|
1481
|
-
} catch (e) {
|
|
1482
|
-
return err(502, "absence_failed", String(e).slice(0, 200));
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
1447
|
async function verifyRoute(c) {
|
|
1486
1448
|
const { env, req } = c;
|
|
1487
1449
|
const t = await tierFor(c);
|
|
@@ -1721,8 +1683,6 @@ var OPENAPI_HANDLERS = {
|
|
|
1721
1683
|
askKeyed: askRoute,
|
|
1722
1684
|
search: searchRoute,
|
|
1723
1685
|
searchKeyed: searchRoute,
|
|
1724
|
-
absence: absenceRoute,
|
|
1725
|
-
absenceKeyed: absenceRoute,
|
|
1726
1686
|
verify: verifyRoute,
|
|
1727
1687
|
verifyKeyed: verifyRoute,
|
|
1728
1688
|
research: researchRoute,
|
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.54",
|
|
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",
|
|
@@ -178,58 +178,6 @@ paths:
|
|
|
178
178
|
responses:
|
|
179
179
|
'200':
|
|
180
180
|
description: The ranked hits.
|
|
181
|
-
/api/absence:
|
|
182
|
-
post:
|
|
183
|
-
tags: [Evidence]
|
|
184
|
-
summary: Request a provable absence certificate
|
|
185
|
-
description: >-
|
|
186
|
-
Enumerates the entire machine-readable model of the named standard and
|
|
187
|
-
reports how many nodes were examined and whether any matched the
|
|
188
|
-
topic. A verdict of absent is an enumeration certificate that
|
|
189
|
-
states its scope, not a statement that a search found nothing.
|
|
190
|
-
operationId: absence
|
|
191
|
-
requestBody:
|
|
192
|
-
required: true
|
|
193
|
-
content:
|
|
194
|
-
application/json:
|
|
195
|
-
schema:
|
|
196
|
-
type: object
|
|
197
|
-
required: [standard, topic]
|
|
198
|
-
properties:
|
|
199
|
-
standard:
|
|
200
|
-
type: string
|
|
201
|
-
description: The publication, for example R 60.
|
|
202
|
-
topic:
|
|
203
|
-
type: string
|
|
204
|
-
description: The topic whose absence is to be proven.
|
|
205
|
-
examples:
|
|
206
|
-
packaging:
|
|
207
|
-
value:
|
|
208
|
-
standard: R 60
|
|
209
|
-
topic: packaging
|
|
210
|
-
responses:
|
|
211
|
-
'200':
|
|
212
|
-
description: The enumeration certificate.
|
|
213
|
-
/v1/absence:
|
|
214
|
-
post:
|
|
215
|
-
tags: [Evidence]
|
|
216
|
-
summary: Request a provable absence certificate with an API key
|
|
217
|
-
operationId: absenceKeyed
|
|
218
|
-
security:
|
|
219
|
-
- bearerAuth: []
|
|
220
|
-
requestBody:
|
|
221
|
-
required: true
|
|
222
|
-
content:
|
|
223
|
-
application/json:
|
|
224
|
-
schema:
|
|
225
|
-
type: object
|
|
226
|
-
required: [standard, topic]
|
|
227
|
-
properties:
|
|
228
|
-
standard: { type: string }
|
|
229
|
-
topic: { type: string }
|
|
230
|
-
responses:
|
|
231
|
-
'200':
|
|
232
|
-
description: The enumeration certificate.
|
|
233
181
|
/api/verify:
|
|
234
182
|
post:
|
|
235
183
|
tags: [Evidence]
|
|
@@ -258,3 +258,6 @@ CREATE TABLE IF NOT EXISTS attachments (
|
|
|
258
258
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
259
259
|
);
|
|
260
260
|
CREATE INDEX IF NOT EXISTS idx_attachments_sub ON attachments (sub);
|
|
261
|
+
|
|
262
|
+
-- Tier-2 image reuse: the user message's stored attachment
|
|
263
|
+
ALTER TABLE messages ADD COLUMN attachment_id TEXT;
|
|
@@ -379,6 +379,31 @@ async function handleAsk(
|
|
|
379
379
|
if (body?.image != null && !userImage) {
|
|
380
380
|
return err(400, "invalid_image", "image must be a data URL (data:image/png|jpeg|webp|gif;base64,…) up to 6 MB");
|
|
381
381
|
}
|
|
382
|
+
// Tier-2 image reuse (TODO.new-era/8): stored attachments materialize
|
|
383
|
+
// as the question's image. Explicit attachment_ids win (member-gated,
|
|
384
|
+
// ownership-checked, at most one — the multimodal shape rule); with
|
|
385
|
+
// none given, the conversation's most recent image re-attaches itself,
|
|
386
|
+
// so "identify this" works turns after the upload.
|
|
387
|
+
let storedImage: string | null = null;
|
|
388
|
+
const attachIds: string[] = Array.isArray(body?.attachment_ids)
|
|
389
|
+
? body.attachment_ids.filter((x: unknown) => typeof x === "string" && /^[0-9a-f-]{36}$/i.test(x)).slice(0, 2)
|
|
390
|
+
: [];
|
|
391
|
+
if (attachIds.length && env.DB && member) {
|
|
392
|
+
for (const aid of attachIds) {
|
|
393
|
+
const row = await env.DB.prepare("SELECT mime, r2_key FROM attachments WHERE id = ?1 AND sub = ?2").bind(aid, member.sub).first<{ mime: string; r2_key: string }>();
|
|
394
|
+
if (!row) continue;
|
|
395
|
+
try {
|
|
396
|
+
const obj = await (env as any).CHAT_UPLOADS.get(row.r2_key);
|
|
397
|
+
if (!obj) continue;
|
|
398
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
|
|
399
|
+
storedImage = `data:${row.mime};base64,${b64}`;
|
|
400
|
+
break; // the multimodal shape rule: one image per question
|
|
401
|
+
} catch {
|
|
402
|
+
/* the object's gone — ask without it */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const effectiveImage = userImage ?? storedImage;
|
|
382
407
|
// history compaction: turns beyond the budget slice are summarized into a
|
|
383
408
|
// continuity block (below) instead of silently dropped
|
|
384
409
|
const budget = num(env as any, "INPUT_TOKEN_BUDGET", LIMITS.inputTokenBudget);
|
|
@@ -395,7 +420,8 @@ async function handleAsk(
|
|
|
395
420
|
// answer caches; corpus surgery bumps it (scripts/invalidate_answer_
|
|
396
421
|
// cache.py) and old-generation entries miss (oimlsmart/rag#72)
|
|
397
422
|
const gen = await corpusGen(env.CACHE);
|
|
398
|
-
const
|
|
423
|
+
const withImage = effectiveImage || storedImage;
|
|
424
|
+
const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
|
|
399
425
|
const wantsStream = body?.stream === true || (tier === "anon" && body?.stream !== false);
|
|
400
426
|
|
|
401
427
|
if (cached) {
|
|
@@ -424,6 +450,25 @@ async function handleAsk(
|
|
|
424
450
|
// (present when the client passes conversation_id) make pronoun
|
|
425
451
|
// follow-ups O(1) — "it / the 2017 one" resolve against the map
|
|
426
452
|
const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
|
|
453
|
+
|
|
454
|
+
// Tier-2 continuation: no new image, but the conversation carries one —
|
|
455
|
+
// its most recent stored image re-attaches to this question
|
|
456
|
+
if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
|
|
457
|
+
const prior = await env.DB.prepare(
|
|
458
|
+
"SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1",
|
|
459
|
+
).bind(conversationId, member.sub).first<{ aid: string; mime: string; r2_key: string }>();
|
|
460
|
+
if (prior) {
|
|
461
|
+
try {
|
|
462
|
+
const obj = await (env as any).CHAT_UPLOADS.get(prior.r2_key);
|
|
463
|
+
if (obj) {
|
|
464
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
|
|
465
|
+
storedImage = `data:${prior.mime};base64,${b64}`;
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
/* the object's gone */
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
427
472
|
let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
428
473
|
if (conversationId) {
|
|
429
474
|
try {
|
|
@@ -1081,7 +1126,7 @@ async function handleAsk(
|
|
|
1081
1126
|
budget,
|
|
1082
1127
|
);
|
|
1083
1128
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
1084
|
-
if (
|
|
1129
|
+
if (withImage) {
|
|
1085
1130
|
// the user's own image rides on the question message — retrieval stays
|
|
1086
1131
|
// text-driven; the answer model reads the image as question context.
|
|
1087
1132
|
// A markings/nameplate/certificate photo gets a TRANSCRIPTION note:
|
|
@@ -1096,11 +1141,11 @@ async function handleAsk(
|
|
|
1096
1141
|
if (Array.isArray(last.content)) {
|
|
1097
1142
|
const textPart = last.content.find((p: any) => p.type === "text");
|
|
1098
1143
|
if (textPart) textPart.text += note;
|
|
1099
|
-
last.content = [...last.content, { type: "image_url", image_url: { url:
|
|
1144
|
+
last.content = [...last.content, { type: "image_url", image_url: { url: withImage } }] as unknown as string;
|
|
1100
1145
|
} else {
|
|
1101
1146
|
last.content = [
|
|
1102
1147
|
{ type: "text", text: last.content + note },
|
|
1103
|
-
{ type: "image_url", image_url: { url:
|
|
1148
|
+
{ type: "image_url", image_url: { url: withImage } },
|
|
1104
1149
|
] as unknown as string;
|
|
1105
1150
|
}
|
|
1106
1151
|
console.log("user image attached to generation");
|
|
@@ -66,13 +66,13 @@ export async function handleConversations(
|
|
|
66
66
|
let msgs: any;
|
|
67
67
|
try {
|
|
68
68
|
msgs = await env.DB.prepare(
|
|
69
|
-
"SELECT id, role, content, citations, model, context_applied, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
|
|
69
|
+
"SELECT id, role, content, citations, model, context_applied, attachment_id, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
|
|
70
70
|
)
|
|
71
71
|
.bind(id)
|
|
72
72
|
.all();
|
|
73
73
|
} catch {
|
|
74
74
|
msgs = await env.DB.prepare(
|
|
75
|
-
"SELECT id, role, content, citations, model, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
|
|
75
|
+
"SELECT id, role, content, citations, model, attachment_id, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
|
|
76
76
|
)
|
|
77
77
|
.bind(id)
|
|
78
78
|
.all();
|
|
@@ -124,6 +124,14 @@ export async function handleAppendMessage(env: any, sub: string, req: Request, c
|
|
|
124
124
|
if ((role !== "user" && role !== "assistant") || !content.trim() || content.length > LIMITS.maxOutputTokens * 4) {
|
|
125
125
|
return err(400, "invalid_input", "role (user|assistant) and content are required");
|
|
126
126
|
}
|
|
127
|
+
// Tier-2 image reuse: the message may carry a stored attachment id —
|
|
128
|
+
// the member's own, validated against the attachments table
|
|
129
|
+
let attachmentId: string | null = null;
|
|
130
|
+
if (typeof body?.attachment_id === "string" && body.attachment_id) {
|
|
131
|
+
const owned = await env.DB.prepare("SELECT id FROM attachments WHERE id = ?1 AND sub = ?2").bind(body.attachment_id, sub).first();
|
|
132
|
+
if (!owned) return err(404, "not_found", "No such attachment");
|
|
133
|
+
attachmentId = body.attachment_id;
|
|
134
|
+
}
|
|
127
135
|
let citations: string | null = null;
|
|
128
136
|
if (body?.citations != null) {
|
|
129
137
|
if (!Array.isArray(body.citations) || body.citations.length > 16) {
|
|
@@ -142,8 +150,8 @@ export async function handleAppendMessage(env: any, sub: string, req: Request, c
|
|
|
142
150
|
try {
|
|
143
151
|
await env.DB.batch([
|
|
144
152
|
env.DB.prepare(
|
|
145
|
-
"INSERT INTO messages (id, conversation_id, role, content, citations, model, context_applied, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
|
146
|
-
).bind(mid, convId, role, content, citations, typeof body?.model === "string" ? body.model.slice(0, 80) : null, applied ? JSON.stringify(applied) : null, now),
|
|
153
|
+
"INSERT INTO messages (id, conversation_id, role, content, citations, model, context_applied, attachment_id, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
|
154
|
+
).bind(mid, convId, role, content, citations, typeof body?.model === "string" ? body.model.slice(0, 80) : null, applied ? JSON.stringify(applied) : null, attachmentId, now),
|
|
147
155
|
env.DB.prepare("UPDATE conversations SET updated_at = ?1 WHERE id = ?2 AND sub = ?3").bind(now, convId, sub),
|
|
148
156
|
]);
|
|
149
157
|
} catch (e) {
|
|
@@ -13,8 +13,7 @@ import { embed } from "./ai";
|
|
|
13
13
|
import { ftsMatchQuery } from "./lexical";
|
|
14
14
|
import { scoreFaithfulness } from "./faithfulness";
|
|
15
15
|
import { checkQuoteAnchors } from "./anchors";
|
|
16
|
-
import {
|
|
17
|
-
import { standardForDocNumber, bindModelNode, modelGroundingBlock } from "./modelplane";
|
|
16
|
+
import { bindModelNode, modelGroundingBlock } from "./modelplane";
|
|
18
17
|
import { refCodec } from "./codecs";
|
|
19
18
|
import { entitlementScope, standardKeysFrom } from "./requestScope";
|
|
20
19
|
import type { Env } from "./env";
|
|
@@ -261,40 +260,6 @@ async function adminStatsRoute(c: RouteContext): Promise<Response> {
|
|
|
261
260
|
}, 200, corsHeaders(req));
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
// ── Provable absence (TODO.era3/02): deterministic enumeration proof ──
|
|
265
|
-
async function absenceRoute(c: RouteContext): Promise<Response> {
|
|
266
|
-
const { env, req } = c;
|
|
267
|
-
const t = await tierFor(c);
|
|
268
|
-
if (t instanceof Response) return t;
|
|
269
|
-
const body = await readJson(req);
|
|
270
|
-
const namedStd = namedDocumentIn(String(body?.standard ?? ""));
|
|
271
|
-
const standard = standardForDocNumber(namedStd?.doc_number ?? String(body?.standard ?? "").trim());
|
|
272
|
-
const topic = String(body?.topic ?? "").trim().toLowerCase();
|
|
273
|
-
if (!standard || !topic) return err(400, "invalid_input", "standard and topic are required");
|
|
274
|
-
try {
|
|
275
|
-
const nodes = (await env.DB.prepare("SELECT node_id, kind, name, content FROM model_nodes WHERE standard = ?1").bind(standard).all()).results ?? [];
|
|
276
|
-
const tokens = topic.split(/\s+/).filter((t: string) => t.length > 2);
|
|
277
|
-
const matches: unknown[] = [];
|
|
278
|
-
for (const n of nodes as any[]) {
|
|
279
|
-
const hay = `${n.name ?? ""} ${n.content ?? ""}`.toLowerCase();
|
|
280
|
-
if (tokens.some((tok: string) => hay.includes(tok))) {
|
|
281
|
-
matches.push({ node_id: n.node_id, kind: n.kind });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const chunks = (await env.DB.prepare("SELECT COUNT(*) AS n FROM chunks WHERE corpus = 'smart-model' AND (docidentifier LIKE ?1 OR doc_id LIKE ?2)").bind(`%${body?.standard}%`, `%${body?.standard}%`).first()) as any;
|
|
285
|
-
return json({
|
|
286
|
-
standard,
|
|
287
|
-
topic,
|
|
288
|
-
enumerated: { model_nodes: nodes.length, smart_model_chunks: chunks?.n ?? 0 },
|
|
289
|
-
matches: matches.slice(0, 20),
|
|
290
|
-
verdict: matches.length === 0 ? "absent" : "present",
|
|
291
|
-
scope: `the machine-readable model of ${standard} (all model nodes) — the enumeration is exhaustive over that scope; prose outside the modeled families is not claimed`,
|
|
292
|
-
});
|
|
293
|
-
} catch (e) {
|
|
294
|
-
return err(502, "absence_failed", String(e).slice(0, 200));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
263
|
// ── Self-verification (TODO.era3/03): the deterministic battery, exposed ──
|
|
299
264
|
async function verifyRoute(c: RouteContext): Promise<Response> {
|
|
300
265
|
const { env, req } = c;
|
|
@@ -595,8 +560,6 @@ const OPENAPI_HANDLERS: Record<OpenApiOperationId, RouteHandler> = {
|
|
|
595
560
|
askKeyed: askRoute,
|
|
596
561
|
search: searchRoute,
|
|
597
562
|
searchKeyed: searchRoute,
|
|
598
|
-
absence: absenceRoute,
|
|
599
|
-
absenceKeyed: absenceRoute,
|
|
600
563
|
verify: verifyRoute,
|
|
601
564
|
verifyKeyed: verifyRoute,
|
|
602
565
|
research: researchRoute,
|
|
@@ -11,8 +11,6 @@ export type OpenApiOperationId =
|
|
|
11
11
|
| "askKeyed"
|
|
12
12
|
| "search"
|
|
13
13
|
| "searchKeyed"
|
|
14
|
-
| "absence"
|
|
15
|
-
| "absenceKeyed"
|
|
16
14
|
| "verify"
|
|
17
15
|
| "verifyKeyed"
|
|
18
16
|
| "research"
|
|
@@ -84,16 +82,6 @@ export const OPENAPI_SURFACE: readonly (Omit<OpenApiRoute, "operationId"> & { op
|
|
|
84
82
|
"pattern": "/v1/search",
|
|
85
83
|
"operationId": "searchKeyed"
|
|
86
84
|
},
|
|
87
|
-
{
|
|
88
|
-
"method": "POST",
|
|
89
|
-
"pattern": "/api/absence",
|
|
90
|
-
"operationId": "absence"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"method": "POST",
|
|
94
|
-
"pattern": "/v1/absence",
|
|
95
|
-
"operationId": "absenceKeyed"
|
|
96
|
-
},
|
|
97
85
|
{
|
|
98
86
|
"method": "POST",
|
|
99
87
|
"pattern": "/api/verify",
|