@semiont/jobs 0.5.26 → 0.5.28
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/README.md +13 -12
- package/dist/index.d.ts +35 -72
- package/dist/index.js +165 -96
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +198 -115
- package/dist/worker-main.js.map +1 -1
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@ import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { jobId, deriveViews, reconcileSelector, GENERATABLE_MEDIA_TYPES, estimateTokens, chunkText, isObject, isString, getLocaleEnglishName } from '@semiont/core';
|
|
5
|
+
import { withSpan } from '@semiont/observability';
|
|
5
6
|
import { execFileSync } from 'child_process';
|
|
6
7
|
import { tmpdir } from 'os';
|
|
7
8
|
import { withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
|
|
8
9
|
import '@semiont/event-sourcing';
|
|
9
|
-
import '@semiont/observability';
|
|
10
10
|
import '@semiont/sdk';
|
|
11
11
|
import '@semiont/http-transport';
|
|
12
12
|
|
|
@@ -426,10 +426,18 @@ function isFailedJob(job) {
|
|
|
426
426
|
function isCancelledJob(job) {
|
|
427
427
|
return job.status === "cancelled";
|
|
428
428
|
}
|
|
429
|
-
|
|
430
|
-
// src/workers/inference-call.ts
|
|
431
429
|
var INFERENCE_TIMEOUT_MS = 10 * 6e4;
|
|
432
|
-
|
|
430
|
+
var INFERENCE_HEARTBEAT_MS = 15e3;
|
|
431
|
+
function spanned(client, kind, maxTokens, work) {
|
|
432
|
+
return withSpan(`inference:${kind}`, work, {
|
|
433
|
+
attrs: {
|
|
434
|
+
"inference.provider": client.type,
|
|
435
|
+
"inference.model": client.modelId,
|
|
436
|
+
"inference.max_tokens": maxTokens
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async function withTimeout(work, label, onHeartbeat) {
|
|
433
441
|
let timer;
|
|
434
442
|
const timedOut = new Promise((_, reject) => {
|
|
435
443
|
timer = setTimeout(() => {
|
|
@@ -439,6 +447,16 @@ async function withTimeout(work, label) {
|
|
|
439
447
|
}, INFERENCE_TIMEOUT_MS);
|
|
440
448
|
timer.unref?.();
|
|
441
449
|
});
|
|
450
|
+
let heartbeat;
|
|
451
|
+
if (onHeartbeat) {
|
|
452
|
+
heartbeat = setInterval(() => {
|
|
453
|
+
try {
|
|
454
|
+
onHeartbeat();
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}, INFERENCE_HEARTBEAT_MS);
|
|
458
|
+
heartbeat.unref?.();
|
|
459
|
+
}
|
|
442
460
|
try {
|
|
443
461
|
return await Promise.race([work, timedOut]);
|
|
444
462
|
} catch (err) {
|
|
@@ -447,19 +465,22 @@ async function withTimeout(work, label) {
|
|
|
447
465
|
throw err;
|
|
448
466
|
} finally {
|
|
449
467
|
clearTimeout(timer);
|
|
468
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
450
469
|
}
|
|
451
470
|
}
|
|
452
|
-
function
|
|
453
|
-
return withTimeout(
|
|
454
|
-
client.
|
|
455
|
-
`${client.type}:${client.modelId}
|
|
456
|
-
|
|
471
|
+
function boundedGenerateWithMetadata(client, prompt, maxTokens, temperature, onHeartbeat) {
|
|
472
|
+
return spanned(client, "text", maxTokens, () => withTimeout(
|
|
473
|
+
client.generateTextWithMetadata(prompt, maxTokens, temperature),
|
|
474
|
+
`${client.type}:${client.modelId}`,
|
|
475
|
+
onHeartbeat
|
|
476
|
+
));
|
|
457
477
|
}
|
|
458
|
-
function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema) {
|
|
459
|
-
return withTimeout(
|
|
478
|
+
function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema, onHeartbeat) {
|
|
479
|
+
return spanned(client, "structured", maxTokens, () => withTimeout(
|
|
460
480
|
client.generateStructured(prompt, maxTokens, temperature, elementSchema),
|
|
461
|
-
`${client.type}:${client.modelId}
|
|
462
|
-
|
|
481
|
+
`${client.type}:${client.modelId}`,
|
|
482
|
+
onHeartbeat
|
|
483
|
+
));
|
|
463
484
|
}
|
|
464
485
|
|
|
465
486
|
// src/workers/detection/detection-chunking.ts
|
|
@@ -990,7 +1011,7 @@ function assertNotTruncated(response, motivation, chunk, totalChunks, outputBudg
|
|
|
990
1011
|
throw new Error(`${motivation} detection response truncated (max_tokens) on chunk ${chunk}/${totalChunks} despite the derived output budget of ${outputBudget} tokens \u2014 failing the job rather than under-reporting annotations.`);
|
|
991
1012
|
}
|
|
992
1013
|
}
|
|
993
|
-
async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse,
|
|
1014
|
+
async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onActivity) {
|
|
994
1015
|
const limits = await client.limits();
|
|
995
1016
|
const scaffoldTokens = estimateTokens(buildPrompt(""));
|
|
996
1017
|
const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
|
|
@@ -1002,12 +1023,14 @@ async function detectInChunks(client, content, buildPrompt, temperature, motivat
|
|
|
1002
1023
|
buildPrompt(chunks[i]),
|
|
1003
1024
|
outputBudget,
|
|
1004
1025
|
temperature,
|
|
1005
|
-
elementSchema
|
|
1026
|
+
elementSchema,
|
|
1027
|
+
// Still alive, same position (a long single call is otherwise silent).
|
|
1028
|
+
() => onActivity?.(i, chunks.length)
|
|
1006
1029
|
);
|
|
1007
1030
|
assertNotTruncated(response, motivation, i + 1, chunks.length, outputBudget);
|
|
1008
1031
|
collected.push(...parse(response.items));
|
|
1009
1032
|
if (i < chunks.length - 1) {
|
|
1010
|
-
|
|
1033
|
+
onActivity?.(i + 1, chunks.length);
|
|
1011
1034
|
}
|
|
1012
1035
|
}
|
|
1013
1036
|
return collected;
|
|
@@ -1021,7 +1044,7 @@ var AnnotationDetection = class {
|
|
|
1021
1044
|
* (source-resource locale). See `types.ts` "Locale conventions" for the
|
|
1022
1045
|
* full discussion.
|
|
1023
1046
|
*/
|
|
1024
|
-
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage,
|
|
1047
|
+
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
1025
1048
|
return detectInChunks(
|
|
1026
1049
|
client,
|
|
1027
1050
|
content,
|
|
@@ -1030,7 +1053,7 @@ var AnnotationDetection = class {
|
|
|
1030
1053
|
"comment",
|
|
1031
1054
|
COMMENT_ELEMENT_SCHEMA,
|
|
1032
1055
|
(items) => MotivationParsers.parseComments(items, content),
|
|
1033
|
-
|
|
1056
|
+
onActivity
|
|
1034
1057
|
);
|
|
1035
1058
|
}
|
|
1036
1059
|
/**
|
|
@@ -1040,7 +1063,7 @@ var AnnotationDetection = class {
|
|
|
1040
1063
|
* applies, used in the prompt so the LLM analyzes non-English source
|
|
1041
1064
|
* correctly.
|
|
1042
1065
|
*/
|
|
1043
|
-
static async detectHighlights(content, client, instructions, density, sourceLanguage,
|
|
1066
|
+
static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity) {
|
|
1044
1067
|
return detectInChunks(
|
|
1045
1068
|
client,
|
|
1046
1069
|
content,
|
|
@@ -1049,7 +1072,7 @@ var AnnotationDetection = class {
|
|
|
1049
1072
|
"highlight",
|
|
1050
1073
|
HIGHLIGHT_ELEMENT_SCHEMA,
|
|
1051
1074
|
(items) => MotivationParsers.parseHighlights(items, content),
|
|
1052
|
-
|
|
1075
|
+
onActivity
|
|
1053
1076
|
);
|
|
1054
1077
|
}
|
|
1055
1078
|
/**
|
|
@@ -1059,7 +1082,7 @@ var AnnotationDetection = class {
|
|
|
1059
1082
|
* (annotation body locale). `sourceLanguage` is the locale of the content
|
|
1060
1083
|
* being analyzed (source-resource locale).
|
|
1061
1084
|
*/
|
|
1062
|
-
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage,
|
|
1085
|
+
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
1063
1086
|
return detectInChunks(
|
|
1064
1087
|
client,
|
|
1065
1088
|
content,
|
|
@@ -1068,7 +1091,7 @@ var AnnotationDetection = class {
|
|
|
1068
1091
|
"assessment",
|
|
1069
1092
|
ASSESSMENT_ELEMENT_SCHEMA,
|
|
1070
1093
|
(items) => MotivationParsers.parseAssessments(items, content),
|
|
1071
|
-
|
|
1094
|
+
onActivity
|
|
1072
1095
|
);
|
|
1073
1096
|
}
|
|
1074
1097
|
/**
|
|
@@ -1083,7 +1106,7 @@ var AnnotationDetection = class {
|
|
|
1083
1106
|
* identifiers, not LLM-generated text — so it's consumed at the body-stamp
|
|
1084
1107
|
* site, not here.
|
|
1085
1108
|
*/
|
|
1086
|
-
static async detectTags(content, client, schema, category, sourceLanguage,
|
|
1109
|
+
static async detectTags(content, client, schema, category, sourceLanguage, onActivity) {
|
|
1087
1110
|
const categoryInfo = schema.tags.find((t) => t.name === category);
|
|
1088
1111
|
if (!categoryInfo) {
|
|
1089
1112
|
throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
|
|
@@ -1105,7 +1128,7 @@ var AnnotationDetection = class {
|
|
|
1105
1128
|
"tag",
|
|
1106
1129
|
TAG_ELEMENT_SCHEMA,
|
|
1107
1130
|
(items) => MotivationParsers.parseTags(items),
|
|
1108
|
-
|
|
1131
|
+
onActivity
|
|
1109
1132
|
);
|
|
1110
1133
|
return MotivationParsers.validateTagOffsets(parsedTags, content, category);
|
|
1111
1134
|
}
|
|
@@ -1121,7 +1144,7 @@ var ENTITY_ELEMENT_SCHEMA = {
|
|
|
1121
1144
|
required: ["exact", "entityType"],
|
|
1122
1145
|
additionalProperties: false
|
|
1123
1146
|
};
|
|
1124
|
-
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage,
|
|
1147
|
+
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onActivity) {
|
|
1125
1148
|
const entityTypesDescription = entityTypes.map((et) => {
|
|
1126
1149
|
if (typeof et === "string") {
|
|
1127
1150
|
return et;
|
|
@@ -1188,7 +1211,10 @@ Example output:
|
|
|
1188
1211
|
outputBudget,
|
|
1189
1212
|
0.3,
|
|
1190
1213
|
// Lower temperature for more consistent extraction
|
|
1191
|
-
ENTITY_ELEMENT_SCHEMA
|
|
1214
|
+
ENTITY_ELEMENT_SCHEMA,
|
|
1215
|
+
// Still alive, same position: a long single call would otherwise emit
|
|
1216
|
+
// nothing at all between start and finish.
|
|
1217
|
+
() => onActivity?.(i, chunks.length)
|
|
1192
1218
|
);
|
|
1193
1219
|
logger.debug("Got entity extraction response", {
|
|
1194
1220
|
chunk: i + 1,
|
|
@@ -1213,7 +1239,7 @@ Example output:
|
|
|
1213
1239
|
}
|
|
1214
1240
|
}
|
|
1215
1241
|
if (i < chunks.length - 1) {
|
|
1216
|
-
|
|
1242
|
+
onActivity?.(i + 1, chunks.length);
|
|
1217
1243
|
}
|
|
1218
1244
|
}
|
|
1219
1245
|
return collected;
|
|
@@ -1227,6 +1253,7 @@ var SEMANTIC_MATCH_CHARS = 240;
|
|
|
1227
1253
|
function idLabel(resourceId, annotationId) {
|
|
1228
1254
|
return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
|
|
1229
1255
|
}
|
|
1256
|
+
var DEFAULT_MAX_TOKENS = 500;
|
|
1230
1257
|
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false, repair) {
|
|
1231
1258
|
logger.debug("Generating resource from topic", {
|
|
1232
1259
|
topicPreview: topic.substring(0, 100),
|
|
@@ -1242,7 +1269,7 @@ async function generateResourceFromTopic(topic, entityTypes, client, logger, use
|
|
|
1242
1269
|
structure
|
|
1243
1270
|
});
|
|
1244
1271
|
const finalTemperature = temperature ?? 0.7;
|
|
1245
|
-
const finalMaxTokens = maxTokens ??
|
|
1272
|
+
const finalMaxTokens = maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1246
1273
|
const languageInstruction = locale && locale !== "en" ? `
|
|
1247
1274
|
|
|
1248
1275
|
IMPORTANT: Write the entire resource in ${getLanguageName(locale)}.` : "";
|
|
@@ -1269,6 +1296,9 @@ The source resource and embedded context are in ${getLanguageName(sourceLanguage
|
|
|
1269
1296
|
parts.push(`- ${label}: ${bodyItem.value}`);
|
|
1270
1297
|
}
|
|
1271
1298
|
}
|
|
1299
|
+
if (focus.userHint) {
|
|
1300
|
+
parts.push(`- User hint (steers what to generate): ${focus.userHint}`);
|
|
1301
|
+
}
|
|
1272
1302
|
annotationSection = `
|
|
1273
1303
|
|
|
1274
1304
|
Annotation context:
|
|
@@ -1427,16 +1457,16 @@ ${formatRequirements}`;
|
|
|
1427
1457
|
temperature: finalTemperature,
|
|
1428
1458
|
maxTokens: finalMaxTokens
|
|
1429
1459
|
});
|
|
1430
|
-
const response = await
|
|
1431
|
-
logger.debug("Got response from inference", { responseLength: response.length });
|
|
1432
|
-
const result = parseResponse(response);
|
|
1460
|
+
const response = await boundedGenerateWithMetadata(client, prompt, finalMaxTokens, finalTemperature);
|
|
1461
|
+
logger.debug("Got response from inference", { responseLength: response.text.length, stopReason: response.stopReason });
|
|
1462
|
+
const result = parseResponse(response.text);
|
|
1433
1463
|
logger.debug("Parsed response", {
|
|
1434
1464
|
hasTitle: !!result.title,
|
|
1435
1465
|
titleLength: result.title?.length,
|
|
1436
1466
|
hasContent: !!result.content,
|
|
1437
1467
|
contentLength: result.content?.length
|
|
1438
1468
|
});
|
|
1439
|
-
return result;
|
|
1469
|
+
return { ...result, truncated: response.stopReason === "max_tokens" };
|
|
1440
1470
|
}
|
|
1441
1471
|
var PINNED_CREATION_TIMESTAMP = 17e8;
|
|
1442
1472
|
var MAX_COMPILE_REPAIRS = 2;
|
|
@@ -1557,30 +1587,39 @@ function dedupeAnnotations(annotations) {
|
|
|
1557
1587
|
return out;
|
|
1558
1588
|
}
|
|
1559
1589
|
async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1560
|
-
|
|
1561
|
-
onProgress(
|
|
1590
|
+
const echo = detectionEcho(params);
|
|
1591
|
+
onProgress(10, { code: "loading" }, echo);
|
|
1592
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
1562
1593
|
const highlights = await AnnotationDetection.detectHighlights(
|
|
1563
1594
|
content,
|
|
1564
1595
|
inferenceClient,
|
|
1565
1596
|
params.instructions,
|
|
1566
1597
|
params.density,
|
|
1567
1598
|
params.sourceLanguage,
|
|
1568
|
-
//
|
|
1569
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
1599
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1600
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1570
1601
|
);
|
|
1571
|
-
onProgress(60,
|
|
1602
|
+
onProgress(60, { code: "creating-annotations", count: highlights.length }, echo);
|
|
1572
1603
|
const annotations = dedupeAnnotations(highlights.map(
|
|
1573
1604
|
(h) => buildAnnotation("highlighting", h)
|
|
1574
1605
|
));
|
|
1575
|
-
onProgress(100,
|
|
1606
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "highlight" }, echo);
|
|
1576
1607
|
return {
|
|
1577
1608
|
annotations,
|
|
1578
|
-
result: { highlightsFound: highlights.length, highlightsCreated: annotations.length }
|
|
1609
|
+
result: { kind: "highlight-annotation", highlightsFound: highlights.length, highlightsCreated: annotations.length }
|
|
1579
1610
|
};
|
|
1580
1611
|
}
|
|
1612
|
+
function detectionEcho(p) {
|
|
1613
|
+
const requestParams = [];
|
|
1614
|
+
if (p.instructions?.trim()) requestParams.push({ label: "instructions", value: p.instructions.trim() });
|
|
1615
|
+
if (p.tone?.trim()) requestParams.push({ label: "tone", value: p.tone.trim() });
|
|
1616
|
+
if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
|
|
1617
|
+
return requestParams.length > 0 ? { requestParams } : {};
|
|
1618
|
+
}
|
|
1581
1619
|
async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1582
|
-
|
|
1583
|
-
onProgress(
|
|
1620
|
+
const echo = detectionEcho(params);
|
|
1621
|
+
onProgress(10, { code: "loading" }, echo);
|
|
1622
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
1584
1623
|
const comments = await AnnotationDetection.detectComments(
|
|
1585
1624
|
content,
|
|
1586
1625
|
inferenceClient,
|
|
@@ -1589,10 +1628,10 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
1589
1628
|
params.density,
|
|
1590
1629
|
params.language,
|
|
1591
1630
|
params.sourceLanguage,
|
|
1592
|
-
//
|
|
1593
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
1631
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1632
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1594
1633
|
);
|
|
1595
|
-
onProgress(60,
|
|
1634
|
+
onProgress(60, { code: "creating-annotations", count: comments.length }, echo);
|
|
1596
1635
|
const bodyLanguage = params.language ?? "en";
|
|
1597
1636
|
const annotations = dedupeAnnotations(comments.map(
|
|
1598
1637
|
(c) => (
|
|
@@ -1604,15 +1643,16 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
1604
1643
|
])
|
|
1605
1644
|
)
|
|
1606
1645
|
));
|
|
1607
|
-
onProgress(100,
|
|
1646
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "comment" }, echo);
|
|
1608
1647
|
return {
|
|
1609
1648
|
annotations,
|
|
1610
|
-
result: { commentsFound: comments.length, commentsCreated: annotations.length }
|
|
1649
|
+
result: { kind: "comment-annotation", commentsFound: comments.length, commentsCreated: annotations.length }
|
|
1611
1650
|
};
|
|
1612
1651
|
}
|
|
1613
1652
|
async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1614
|
-
|
|
1615
|
-
onProgress(
|
|
1653
|
+
const echo = detectionEcho(params);
|
|
1654
|
+
onProgress(10, { code: "loading" }, echo);
|
|
1655
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
1616
1656
|
const assessments = await AnnotationDetection.detectAssessments(
|
|
1617
1657
|
content,
|
|
1618
1658
|
inferenceClient,
|
|
@@ -1621,10 +1661,10 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
1621
1661
|
params.density,
|
|
1622
1662
|
params.language,
|
|
1623
1663
|
params.sourceLanguage,
|
|
1624
|
-
//
|
|
1625
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
1664
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
1665
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
1626
1666
|
);
|
|
1627
|
-
onProgress(60,
|
|
1667
|
+
onProgress(60, { code: "creating-annotations", count: assessments.length }, echo);
|
|
1628
1668
|
const bodyLanguage = params.language ?? "en";
|
|
1629
1669
|
const annotations = dedupeAnnotations(assessments.map(
|
|
1630
1670
|
(a) => (
|
|
@@ -1643,33 +1683,35 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
1643
1683
|
})
|
|
1644
1684
|
)
|
|
1645
1685
|
));
|
|
1646
|
-
onProgress(100,
|
|
1686
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "assessment" }, echo);
|
|
1647
1687
|
return {
|
|
1648
1688
|
annotations,
|
|
1649
|
-
result: { assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
|
|
1689
|
+
result: { kind: "assessment-annotation", assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
|
|
1650
1690
|
};
|
|
1651
1691
|
}
|
|
1652
1692
|
async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger) {
|
|
1653
1693
|
const entityTypeNames = params.entityTypes.map(String);
|
|
1654
|
-
const requestParams = [{ label: "
|
|
1655
|
-
const
|
|
1694
|
+
const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
|
|
1695
|
+
const completedItems = [];
|
|
1656
1696
|
let totalFound = 0;
|
|
1657
1697
|
let totalEmitted = 0;
|
|
1658
1698
|
let errors = 0;
|
|
1659
1699
|
const allAnnotations = [];
|
|
1660
|
-
onProgress(10,
|
|
1700
|
+
onProgress(10, { code: "loading" }, { requestParams });
|
|
1661
1701
|
const bodyLanguage = params.language ?? "en";
|
|
1662
1702
|
for (let i = 0; i < entityTypeNames.length; i++) {
|
|
1663
1703
|
const entityTypeName = entityTypeNames[i];
|
|
1664
1704
|
if (!entityTypeName) continue;
|
|
1665
1705
|
const pct = 20 + Math.round(i / entityTypeNames.length * 60);
|
|
1666
|
-
onProgress(pct,
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1706
|
+
onProgress(pct, { code: "detecting-entities", entityType: entityTypeName }, {
|
|
1707
|
+
// One vocabulary for "what is in flight" (CLEAN-PROGRESS D2): the entity
|
|
1708
|
+
// type is KB data, `kind` is the code the client localizes around it.
|
|
1709
|
+
current: { kind: "entity-type", value: entityTypeName },
|
|
1710
|
+
processed: i,
|
|
1711
|
+
total: entityTypeNames.length,
|
|
1670
1712
|
entitiesFound: totalFound,
|
|
1671
1713
|
entitiesEmitted: totalEmitted,
|
|
1672
|
-
|
|
1714
|
+
completedItems: [...completedItems],
|
|
1673
1715
|
requestParams
|
|
1674
1716
|
});
|
|
1675
1717
|
const extractedEntities = await extractEntities(
|
|
@@ -1679,25 +1721,28 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1679
1721
|
params.includeDescriptiveReferences ?? false,
|
|
1680
1722
|
logger,
|
|
1681
1723
|
params.sourceLanguage,
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1724
|
+
// Liveness: fires at chunk boundaries AND every ~15 s while a single
|
|
1725
|
+
// inference call is in flight (DETECTION-HEARTBEAT). Progress feeds the
|
|
1726
|
+
// stall watchdog, the janitor, AND the client's inter-emission timeout,
|
|
1727
|
+
// so a long single-chunk call must not be silent. Percentage
|
|
1728
|
+
// interpolates within this entity type's band of the 20–80 range; a
|
|
1729
|
+
// heartbeat repeats the current position rather than inventing an
|
|
1730
|
+
// advance.
|
|
1686
1731
|
(completed, total) => {
|
|
1687
1732
|
const interpolated = 20 + Math.round((i + completed / total) / entityTypeNames.length * 60);
|
|
1688
|
-
onProgress(interpolated,
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1733
|
+
onProgress(interpolated, { code: "detecting-entities", entityType: entityTypeName }, {
|
|
1734
|
+
current: { kind: "entity-type", value: entityTypeName },
|
|
1735
|
+
processed: i,
|
|
1736
|
+
total: entityTypeNames.length,
|
|
1692
1737
|
entitiesFound: totalFound,
|
|
1693
1738
|
entitiesEmitted: totalEmitted,
|
|
1694
|
-
|
|
1739
|
+
completedItems: [...completedItems],
|
|
1695
1740
|
requestParams
|
|
1696
1741
|
});
|
|
1697
1742
|
}
|
|
1698
1743
|
);
|
|
1699
1744
|
totalFound += extractedEntities.length;
|
|
1700
|
-
|
|
1745
|
+
completedItems.push({ value: entityTypeName, foundCount: extractedEntities.length });
|
|
1701
1746
|
const unresolvedBody = [
|
|
1702
1747
|
{ type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
|
|
1703
1748
|
];
|
|
@@ -1728,36 +1773,49 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1728
1773
|
}
|
|
1729
1774
|
}
|
|
1730
1775
|
const annotations = dedupeAnnotations(allAnnotations);
|
|
1731
|
-
onProgress(100,
|
|
1776
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "reference" }, { requestParams });
|
|
1732
1777
|
return {
|
|
1733
1778
|
annotations,
|
|
1734
|
-
result: { totalFound, totalEmitted: annotations.length, errors }
|
|
1779
|
+
result: { kind: "reference-annotation", totalFound, totalEmitted: annotations.length, errors }
|
|
1735
1780
|
};
|
|
1736
1781
|
}
|
|
1737
1782
|
async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
1738
|
-
onProgress(10,
|
|
1739
|
-
onProgress(30,
|
|
1783
|
+
onProgress(10, { code: "loading" });
|
|
1784
|
+
onProgress(30, { code: "analyzing-tags" });
|
|
1740
1785
|
const allTags = [];
|
|
1786
|
+
const completedItems = [];
|
|
1741
1787
|
for (let c = 0; c < params.categories.length; c++) {
|
|
1742
1788
|
const category = params.categories[c];
|
|
1789
|
+
const position = () => ({
|
|
1790
|
+
current: { kind: "category", value: category },
|
|
1791
|
+
processed: c,
|
|
1792
|
+
total: params.categories.length,
|
|
1793
|
+
completedItems: [...completedItems]
|
|
1794
|
+
});
|
|
1795
|
+
onProgress(
|
|
1796
|
+
30 + Math.round(c / params.categories.length * 30),
|
|
1797
|
+
{ code: "analyzing-tags" },
|
|
1798
|
+
position()
|
|
1799
|
+
);
|
|
1743
1800
|
const categoryTags = await AnnotationDetection.detectTags(
|
|
1744
1801
|
content,
|
|
1745
1802
|
inferenceClient,
|
|
1746
1803
|
params.schema,
|
|
1747
1804
|
category,
|
|
1748
1805
|
params.sourceLanguage,
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1806
|
+
// Liveness (chunk boundaries + in-flight heartbeat): this category's
|
|
1807
|
+
// slice of the 30–60 band.
|
|
1751
1808
|
(completed, total) => onProgress(
|
|
1752
1809
|
30 + Math.round((c + completed / total) / params.categories.length * 30),
|
|
1753
|
-
|
|
1754
|
-
|
|
1810
|
+
{ code: "analyzing-tags" },
|
|
1811
|
+
position()
|
|
1755
1812
|
)
|
|
1756
1813
|
);
|
|
1814
|
+
completedItems.push({ value: category, foundCount: categoryTags.length });
|
|
1757
1815
|
allTags.push(...categoryTags);
|
|
1758
1816
|
}
|
|
1759
1817
|
const tags = allTags;
|
|
1760
|
-
onProgress(60,
|
|
1818
|
+
onProgress(60, { code: "creating-tag-annotations", count: tags.length });
|
|
1761
1819
|
const bodyLanguage = params.language ?? "en";
|
|
1762
1820
|
const annotations = dedupeAnnotations(tags.map((t) => {
|
|
1763
1821
|
const category = t.category ?? "unknown";
|
|
@@ -1772,10 +1830,10 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
1772
1830
|
const category = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
|
|
1773
1831
|
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
1774
1832
|
}
|
|
1775
|
-
onProgress(100,
|
|
1833
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "tag" });
|
|
1776
1834
|
return {
|
|
1777
1835
|
annotations,
|
|
1778
|
-
result: { tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
1836
|
+
result: { kind: "tag-annotation", tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
1779
1837
|
};
|
|
1780
1838
|
}
|
|
1781
1839
|
function assertWithinOutputBudget(byteLength) {
|
|
@@ -1795,7 +1853,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1795
1853
|
const title = params.title ?? "Untitled";
|
|
1796
1854
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
1797
1855
|
if (outputMediaType === "application/pdf") {
|
|
1798
|
-
onProgress(5,
|
|
1856
|
+
onProgress(5, { code: "generating-resource" });
|
|
1799
1857
|
const validIds = params.cite === true ? collectContextResourceIds(params.context) : null;
|
|
1800
1858
|
let generated2 = await generateResourceFromTopic(
|
|
1801
1859
|
title,
|
|
@@ -1822,7 +1880,17 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1822
1880
|
}
|
|
1823
1881
|
let compiled = compileTypst(source);
|
|
1824
1882
|
let repairs = 0;
|
|
1825
|
-
while ("error" in compiled
|
|
1883
|
+
while ("error" in compiled) {
|
|
1884
|
+
if (generated2.truncated) {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
`Generation stopped at the maxTokens ceiling (${params.maxTokens ?? DEFAULT_MAX_TOKENS} tokens) and the cut-off Typst source does not compile \u2014 repair cannot help; raise maxTokens. Compile error: ${compiled.error}`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
if (repairs >= MAX_COMPILE_REPAIRS) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`Typst compilation failed after ${MAX_COMPILE_REPAIRS} repair attempts: ${compiled.error}`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1826
1894
|
repairs++;
|
|
1827
1895
|
logger.warn("Typst compile failed \u2014 feeding the error back for repair", {
|
|
1828
1896
|
attempt: repairs,
|
|
@@ -1854,25 +1922,23 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1854
1922
|
}
|
|
1855
1923
|
compiled = compileTypst(source);
|
|
1856
1924
|
}
|
|
1857
|
-
if ("error" in compiled) {
|
|
1858
|
-
throw new Error(
|
|
1859
|
-
`Typst compilation failed after ${MAX_COMPILE_REPAIRS} repair attempts: ${compiled.error}`
|
|
1860
|
-
);
|
|
1861
|
-
}
|
|
1862
1925
|
assertWithinOutputBudget(compiled.pdf.byteLength);
|
|
1863
|
-
onProgress(95,
|
|
1926
|
+
onProgress(95, { code: "creating-resource" });
|
|
1927
|
+
onProgress(100, { code: "complete-generated", truncated: generated2.truncated });
|
|
1864
1928
|
return {
|
|
1865
1929
|
content: compiled.pdf,
|
|
1866
|
-
title
|
|
1930
|
+
title,
|
|
1867
1931
|
format: outputMediaType,
|
|
1868
1932
|
citations: citations2,
|
|
1869
1933
|
result: {
|
|
1934
|
+
kind: "generation",
|
|
1870
1935
|
resourceId: "",
|
|
1871
|
-
resourceName:
|
|
1936
|
+
resourceName: title,
|
|
1937
|
+
truncated: generated2.truncated
|
|
1872
1938
|
}
|
|
1873
1939
|
};
|
|
1874
1940
|
}
|
|
1875
|
-
onProgress(5,
|
|
1941
|
+
onProgress(5, { code: "generating-resource" });
|
|
1876
1942
|
const generated = await generateResourceFromTopic(
|
|
1877
1943
|
title,
|
|
1878
1944
|
entityTypes,
|
|
@@ -1896,17 +1962,20 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1896
1962
|
content = resolved.content;
|
|
1897
1963
|
citations = resolved.citations;
|
|
1898
1964
|
}
|
|
1899
|
-
onProgress(95,
|
|
1965
|
+
onProgress(95, { code: "creating-resource" });
|
|
1900
1966
|
const artifact = new TextEncoder().encode(content);
|
|
1901
1967
|
assertWithinOutputBudget(artifact.byteLength);
|
|
1968
|
+
onProgress(100, { code: "complete-generated", truncated: generated.truncated });
|
|
1902
1969
|
return {
|
|
1903
1970
|
content: artifact,
|
|
1904
|
-
title
|
|
1971
|
+
title,
|
|
1905
1972
|
format: outputMediaType,
|
|
1906
1973
|
citations,
|
|
1907
1974
|
result: {
|
|
1975
|
+
kind: "generation",
|
|
1908
1976
|
resourceId: "",
|
|
1909
|
-
resourceName:
|
|
1977
|
+
resourceName: title,
|
|
1978
|
+
truncated: generated.truncated
|
|
1910
1979
|
}
|
|
1911
1980
|
};
|
|
1912
1981
|
}
|