@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/worker-main.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
|
|
1
|
+
import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, isGenerationJobParams, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
|
|
2
2
|
import { anchoredTextStoreOverTransport, deriveStorageUri, extractPdfTextLayer, EXTRACTORS, calculateChecksum, withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
|
|
3
|
+
import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
|
|
3
4
|
import { execFileSync } from 'child_process';
|
|
4
5
|
import { existsSync, readFileSync, mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
5
6
|
import { homedir, hostname, tmpdir } from 'os';
|
|
6
7
|
import { join } from 'path';
|
|
7
8
|
import { generateAnnotationId } from '@semiont/event-sourcing';
|
|
8
|
-
import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
|
|
9
9
|
import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
10
10
|
import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
|
|
11
11
|
import { createInferenceClient } from '@semiont/inference';
|
|
@@ -9348,10 +9348,18 @@ function asJobParams(params) {
|
|
|
9348
9348
|
}
|
|
9349
9349
|
return params;
|
|
9350
9350
|
}
|
|
9351
|
-
|
|
9352
|
-
// src/workers/inference-call.ts
|
|
9353
9351
|
var INFERENCE_TIMEOUT_MS = 10 * 6e4;
|
|
9354
|
-
|
|
9352
|
+
var INFERENCE_HEARTBEAT_MS = 15e3;
|
|
9353
|
+
function spanned(client, kind, maxTokens, work) {
|
|
9354
|
+
return withSpan(`inference:${kind}`, work, {
|
|
9355
|
+
attrs: {
|
|
9356
|
+
"inference.provider": client.type,
|
|
9357
|
+
"inference.model": client.modelId,
|
|
9358
|
+
"inference.max_tokens": maxTokens
|
|
9359
|
+
}
|
|
9360
|
+
});
|
|
9361
|
+
}
|
|
9362
|
+
async function withTimeout(work, label, onHeartbeat) {
|
|
9355
9363
|
let timer;
|
|
9356
9364
|
const timedOut = new Promise((_, reject) => {
|
|
9357
9365
|
timer = setTimeout(() => {
|
|
@@ -9361,6 +9369,16 @@ async function withTimeout(work, label) {
|
|
|
9361
9369
|
}, INFERENCE_TIMEOUT_MS);
|
|
9362
9370
|
timer.unref?.();
|
|
9363
9371
|
});
|
|
9372
|
+
let heartbeat;
|
|
9373
|
+
if (onHeartbeat) {
|
|
9374
|
+
heartbeat = setInterval(() => {
|
|
9375
|
+
try {
|
|
9376
|
+
onHeartbeat();
|
|
9377
|
+
} catch {
|
|
9378
|
+
}
|
|
9379
|
+
}, INFERENCE_HEARTBEAT_MS);
|
|
9380
|
+
heartbeat.unref?.();
|
|
9381
|
+
}
|
|
9364
9382
|
try {
|
|
9365
9383
|
return await Promise.race([work, timedOut]);
|
|
9366
9384
|
} catch (err) {
|
|
@@ -9369,19 +9387,22 @@ async function withTimeout(work, label) {
|
|
|
9369
9387
|
throw err;
|
|
9370
9388
|
} finally {
|
|
9371
9389
|
clearTimeout(timer);
|
|
9390
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
9372
9391
|
}
|
|
9373
9392
|
}
|
|
9374
|
-
function
|
|
9375
|
-
return withTimeout(
|
|
9376
|
-
client.
|
|
9377
|
-
`${client.type}:${client.modelId}
|
|
9378
|
-
|
|
9393
|
+
function boundedGenerateWithMetadata(client, prompt, maxTokens, temperature, onHeartbeat) {
|
|
9394
|
+
return spanned(client, "text", maxTokens, () => withTimeout(
|
|
9395
|
+
client.generateTextWithMetadata(prompt, maxTokens, temperature),
|
|
9396
|
+
`${client.type}:${client.modelId}`,
|
|
9397
|
+
onHeartbeat
|
|
9398
|
+
));
|
|
9379
9399
|
}
|
|
9380
|
-
function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema) {
|
|
9381
|
-
return withTimeout(
|
|
9400
|
+
function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema, onHeartbeat) {
|
|
9401
|
+
return spanned(client, "structured", maxTokens, () => withTimeout(
|
|
9382
9402
|
client.generateStructured(prompt, maxTokens, temperature, elementSchema),
|
|
9383
|
-
`${client.type}:${client.modelId}
|
|
9384
|
-
|
|
9403
|
+
`${client.type}:${client.modelId}`,
|
|
9404
|
+
onHeartbeat
|
|
9405
|
+
));
|
|
9385
9406
|
}
|
|
9386
9407
|
|
|
9387
9408
|
// src/workers/detection/detection-chunking.ts
|
|
@@ -9912,7 +9933,7 @@ function assertNotTruncated(response, motivation, chunk, totalChunks, outputBudg
|
|
|
9912
9933
|
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.`);
|
|
9913
9934
|
}
|
|
9914
9935
|
}
|
|
9915
|
-
async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse,
|
|
9936
|
+
async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onActivity) {
|
|
9916
9937
|
const limits = await client.limits();
|
|
9917
9938
|
const scaffoldTokens = estimateTokens(buildPrompt(""));
|
|
9918
9939
|
const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
|
|
@@ -9924,12 +9945,14 @@ async function detectInChunks(client, content, buildPrompt, temperature, motivat
|
|
|
9924
9945
|
buildPrompt(chunks[i]),
|
|
9925
9946
|
outputBudget,
|
|
9926
9947
|
temperature,
|
|
9927
|
-
elementSchema
|
|
9948
|
+
elementSchema,
|
|
9949
|
+
// Still alive, same position (a long single call is otherwise silent).
|
|
9950
|
+
() => onActivity?.(i, chunks.length)
|
|
9928
9951
|
);
|
|
9929
9952
|
assertNotTruncated(response, motivation, i + 1, chunks.length, outputBudget);
|
|
9930
9953
|
collected.push(...parse(response.items));
|
|
9931
9954
|
if (i < chunks.length - 1) {
|
|
9932
|
-
|
|
9955
|
+
onActivity?.(i + 1, chunks.length);
|
|
9933
9956
|
}
|
|
9934
9957
|
}
|
|
9935
9958
|
return collected;
|
|
@@ -9943,7 +9966,7 @@ var AnnotationDetection = class {
|
|
|
9943
9966
|
* (source-resource locale). See `types.ts` "Locale conventions" for the
|
|
9944
9967
|
* full discussion.
|
|
9945
9968
|
*/
|
|
9946
|
-
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage,
|
|
9969
|
+
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
9947
9970
|
return detectInChunks(
|
|
9948
9971
|
client,
|
|
9949
9972
|
content,
|
|
@@ -9952,7 +9975,7 @@ var AnnotationDetection = class {
|
|
|
9952
9975
|
"comment",
|
|
9953
9976
|
COMMENT_ELEMENT_SCHEMA,
|
|
9954
9977
|
(items) => MotivationParsers.parseComments(items, content),
|
|
9955
|
-
|
|
9978
|
+
onActivity
|
|
9956
9979
|
);
|
|
9957
9980
|
}
|
|
9958
9981
|
/**
|
|
@@ -9962,7 +9985,7 @@ var AnnotationDetection = class {
|
|
|
9962
9985
|
* applies, used in the prompt so the LLM analyzes non-English source
|
|
9963
9986
|
* correctly.
|
|
9964
9987
|
*/
|
|
9965
|
-
static async detectHighlights(content, client, instructions, density, sourceLanguage,
|
|
9988
|
+
static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity) {
|
|
9966
9989
|
return detectInChunks(
|
|
9967
9990
|
client,
|
|
9968
9991
|
content,
|
|
@@ -9971,7 +9994,7 @@ var AnnotationDetection = class {
|
|
|
9971
9994
|
"highlight",
|
|
9972
9995
|
HIGHLIGHT_ELEMENT_SCHEMA,
|
|
9973
9996
|
(items) => MotivationParsers.parseHighlights(items, content),
|
|
9974
|
-
|
|
9997
|
+
onActivity
|
|
9975
9998
|
);
|
|
9976
9999
|
}
|
|
9977
10000
|
/**
|
|
@@ -9981,7 +10004,7 @@ var AnnotationDetection = class {
|
|
|
9981
10004
|
* (annotation body locale). `sourceLanguage` is the locale of the content
|
|
9982
10005
|
* being analyzed (source-resource locale).
|
|
9983
10006
|
*/
|
|
9984
|
-
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage,
|
|
10007
|
+
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
|
|
9985
10008
|
return detectInChunks(
|
|
9986
10009
|
client,
|
|
9987
10010
|
content,
|
|
@@ -9990,7 +10013,7 @@ var AnnotationDetection = class {
|
|
|
9990
10013
|
"assessment",
|
|
9991
10014
|
ASSESSMENT_ELEMENT_SCHEMA,
|
|
9992
10015
|
(items) => MotivationParsers.parseAssessments(items, content),
|
|
9993
|
-
|
|
10016
|
+
onActivity
|
|
9994
10017
|
);
|
|
9995
10018
|
}
|
|
9996
10019
|
/**
|
|
@@ -10005,7 +10028,7 @@ var AnnotationDetection = class {
|
|
|
10005
10028
|
* identifiers, not LLM-generated text — so it's consumed at the body-stamp
|
|
10006
10029
|
* site, not here.
|
|
10007
10030
|
*/
|
|
10008
|
-
static async detectTags(content, client, schema, category, sourceLanguage,
|
|
10031
|
+
static async detectTags(content, client, schema, category, sourceLanguage, onActivity) {
|
|
10009
10032
|
const categoryInfo = schema.tags.find((t) => t.name === category);
|
|
10010
10033
|
if (!categoryInfo) {
|
|
10011
10034
|
throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
|
|
@@ -10027,7 +10050,7 @@ var AnnotationDetection = class {
|
|
|
10027
10050
|
"tag",
|
|
10028
10051
|
TAG_ELEMENT_SCHEMA,
|
|
10029
10052
|
(items) => MotivationParsers.parseTags(items),
|
|
10030
|
-
|
|
10053
|
+
onActivity
|
|
10031
10054
|
);
|
|
10032
10055
|
return MotivationParsers.validateTagOffsets(parsedTags, content, category);
|
|
10033
10056
|
}
|
|
@@ -10043,7 +10066,7 @@ var ENTITY_ELEMENT_SCHEMA = {
|
|
|
10043
10066
|
required: ["exact", "entityType"],
|
|
10044
10067
|
additionalProperties: false
|
|
10045
10068
|
};
|
|
10046
|
-
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage,
|
|
10069
|
+
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage, onActivity) {
|
|
10047
10070
|
const entityTypesDescription = entityTypes.map((et) => {
|
|
10048
10071
|
if (typeof et === "string") {
|
|
10049
10072
|
return et;
|
|
@@ -10110,7 +10133,10 @@ Example output:
|
|
|
10110
10133
|
outputBudget,
|
|
10111
10134
|
0.3,
|
|
10112
10135
|
// Lower temperature for more consistent extraction
|
|
10113
|
-
ENTITY_ELEMENT_SCHEMA
|
|
10136
|
+
ENTITY_ELEMENT_SCHEMA,
|
|
10137
|
+
// Still alive, same position: a long single call would otherwise emit
|
|
10138
|
+
// nothing at all between start and finish.
|
|
10139
|
+
() => onActivity?.(i, chunks.length)
|
|
10114
10140
|
);
|
|
10115
10141
|
logger2.debug("Got entity extraction response", {
|
|
10116
10142
|
chunk: i + 1,
|
|
@@ -10135,7 +10161,7 @@ Example output:
|
|
|
10135
10161
|
}
|
|
10136
10162
|
}
|
|
10137
10163
|
if (i < chunks.length - 1) {
|
|
10138
|
-
|
|
10164
|
+
onActivity?.(i + 1, chunks.length);
|
|
10139
10165
|
}
|
|
10140
10166
|
}
|
|
10141
10167
|
return collected;
|
|
@@ -10149,6 +10175,7 @@ var SEMANTIC_MATCH_CHARS = 240;
|
|
|
10149
10175
|
function idLabel(resourceId, annotationId) {
|
|
10150
10176
|
return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
|
|
10151
10177
|
}
|
|
10178
|
+
var DEFAULT_MAX_TOKENS = 500;
|
|
10152
10179
|
async function generateResourceFromTopic(topic, entityTypes, client, logger2, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false, repair) {
|
|
10153
10180
|
logger2.debug("Generating resource from topic", {
|
|
10154
10181
|
topicPreview: topic.substring(0, 100),
|
|
@@ -10164,7 +10191,7 @@ async function generateResourceFromTopic(topic, entityTypes, client, logger2, us
|
|
|
10164
10191
|
structure
|
|
10165
10192
|
});
|
|
10166
10193
|
const finalTemperature = temperature ?? 0.7;
|
|
10167
|
-
const finalMaxTokens = maxTokens ??
|
|
10194
|
+
const finalMaxTokens = maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
10168
10195
|
const languageInstruction = locale && locale !== "en" ? `
|
|
10169
10196
|
|
|
10170
10197
|
IMPORTANT: Write the entire resource in ${getLanguageName(locale)}.` : "";
|
|
@@ -10191,6 +10218,9 @@ The source resource and embedded context are in ${getLanguageName(sourceLanguage
|
|
|
10191
10218
|
parts.push(`- ${label}: ${bodyItem.value}`);
|
|
10192
10219
|
}
|
|
10193
10220
|
}
|
|
10221
|
+
if (focus.userHint) {
|
|
10222
|
+
parts.push(`- User hint (steers what to generate): ${focus.userHint}`);
|
|
10223
|
+
}
|
|
10194
10224
|
annotationSection = `
|
|
10195
10225
|
|
|
10196
10226
|
Annotation context:
|
|
@@ -10349,16 +10379,16 @@ ${formatRequirements}`;
|
|
|
10349
10379
|
temperature: finalTemperature,
|
|
10350
10380
|
maxTokens: finalMaxTokens
|
|
10351
10381
|
});
|
|
10352
|
-
const response = await
|
|
10353
|
-
logger2.debug("Got response from inference", { responseLength: response.length });
|
|
10354
|
-
const result = parseResponse(response);
|
|
10382
|
+
const response = await boundedGenerateWithMetadata(client, prompt, finalMaxTokens, finalTemperature);
|
|
10383
|
+
logger2.debug("Got response from inference", { responseLength: response.text.length, stopReason: response.stopReason });
|
|
10384
|
+
const result = parseResponse(response.text);
|
|
10355
10385
|
logger2.debug("Parsed response", {
|
|
10356
10386
|
hasTitle: !!result.title,
|
|
10357
10387
|
titleLength: result.title?.length,
|
|
10358
10388
|
hasContent: !!result.content,
|
|
10359
10389
|
contentLength: result.content?.length
|
|
10360
10390
|
});
|
|
10361
|
-
return result;
|
|
10391
|
+
return { ...result, truncated: response.stopReason === "max_tokens" };
|
|
10362
10392
|
}
|
|
10363
10393
|
var PINNED_CREATION_TIMESTAMP = 17e8;
|
|
10364
10394
|
var MAX_COMPILE_REPAIRS = 2;
|
|
@@ -10571,30 +10601,39 @@ function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation,
|
|
|
10571
10601
|
};
|
|
10572
10602
|
}
|
|
10573
10603
|
async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
10574
|
-
|
|
10575
|
-
onProgress(
|
|
10604
|
+
const echo = detectionEcho(params);
|
|
10605
|
+
onProgress(10, { code: "loading" }, echo);
|
|
10606
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
10576
10607
|
const highlights = await AnnotationDetection.detectHighlights(
|
|
10577
10608
|
content,
|
|
10578
10609
|
inferenceClient,
|
|
10579
10610
|
params.instructions,
|
|
10580
10611
|
params.density,
|
|
10581
10612
|
params.sourceLanguage,
|
|
10582
|
-
//
|
|
10583
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
10613
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
10614
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
10584
10615
|
);
|
|
10585
|
-
onProgress(60,
|
|
10616
|
+
onProgress(60, { code: "creating-annotations", count: highlights.length }, echo);
|
|
10586
10617
|
const annotations = dedupeAnnotations(highlights.map(
|
|
10587
10618
|
(h) => buildAnnotation("highlighting", h)
|
|
10588
10619
|
));
|
|
10589
|
-
onProgress(100,
|
|
10620
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "highlight" }, echo);
|
|
10590
10621
|
return {
|
|
10591
10622
|
annotations,
|
|
10592
|
-
result: { highlightsFound: highlights.length, highlightsCreated: annotations.length }
|
|
10623
|
+
result: { kind: "highlight-annotation", highlightsFound: highlights.length, highlightsCreated: annotations.length }
|
|
10593
10624
|
};
|
|
10594
10625
|
}
|
|
10626
|
+
function detectionEcho(p) {
|
|
10627
|
+
const requestParams = [];
|
|
10628
|
+
if (p.instructions?.trim()) requestParams.push({ label: "instructions", value: p.instructions.trim() });
|
|
10629
|
+
if (p.tone?.trim()) requestParams.push({ label: "tone", value: p.tone.trim() });
|
|
10630
|
+
if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
|
|
10631
|
+
return requestParams.length > 0 ? { requestParams } : {};
|
|
10632
|
+
}
|
|
10595
10633
|
async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
10596
|
-
|
|
10597
|
-
onProgress(
|
|
10634
|
+
const echo = detectionEcho(params);
|
|
10635
|
+
onProgress(10, { code: "loading" }, echo);
|
|
10636
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
10598
10637
|
const comments = await AnnotationDetection.detectComments(
|
|
10599
10638
|
content,
|
|
10600
10639
|
inferenceClient,
|
|
@@ -10603,10 +10642,10 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
10603
10642
|
params.density,
|
|
10604
10643
|
params.language,
|
|
10605
10644
|
params.sourceLanguage,
|
|
10606
|
-
//
|
|
10607
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
10645
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
10646
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
10608
10647
|
);
|
|
10609
|
-
onProgress(60,
|
|
10648
|
+
onProgress(60, { code: "creating-annotations", count: comments.length }, echo);
|
|
10610
10649
|
const bodyLanguage = params.language ?? "en";
|
|
10611
10650
|
const annotations = dedupeAnnotations(comments.map(
|
|
10612
10651
|
(c) => (
|
|
@@ -10618,15 +10657,16 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
10618
10657
|
])
|
|
10619
10658
|
)
|
|
10620
10659
|
));
|
|
10621
|
-
onProgress(100,
|
|
10660
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "comment" }, echo);
|
|
10622
10661
|
return {
|
|
10623
10662
|
annotations,
|
|
10624
|
-
result: { commentsFound: comments.length, commentsCreated: annotations.length }
|
|
10663
|
+
result: { kind: "comment-annotation", commentsFound: comments.length, commentsCreated: annotations.length }
|
|
10625
10664
|
};
|
|
10626
10665
|
}
|
|
10627
10666
|
async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
10628
|
-
|
|
10629
|
-
onProgress(
|
|
10667
|
+
const echo = detectionEcho(params);
|
|
10668
|
+
onProgress(10, { code: "loading" }, echo);
|
|
10669
|
+
onProgress(30, { code: "analyzing" }, echo);
|
|
10630
10670
|
const assessments = await AnnotationDetection.detectAssessments(
|
|
10631
10671
|
content,
|
|
10632
10672
|
inferenceClient,
|
|
@@ -10635,10 +10675,10 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
10635
10675
|
params.density,
|
|
10636
10676
|
params.language,
|
|
10637
10677
|
params.sourceLanguage,
|
|
10638
|
-
//
|
|
10639
|
-
(completed, total) => onProgress(30 + Math.round(completed / total * 30),
|
|
10678
|
+
// Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
|
|
10679
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
|
|
10640
10680
|
);
|
|
10641
|
-
onProgress(60,
|
|
10681
|
+
onProgress(60, { code: "creating-annotations", count: assessments.length }, echo);
|
|
10642
10682
|
const bodyLanguage = params.language ?? "en";
|
|
10643
10683
|
const annotations = dedupeAnnotations(assessments.map(
|
|
10644
10684
|
(a) => (
|
|
@@ -10657,33 +10697,35 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
10657
10697
|
})
|
|
10658
10698
|
)
|
|
10659
10699
|
));
|
|
10660
|
-
onProgress(100,
|
|
10700
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "assessment" }, echo);
|
|
10661
10701
|
return {
|
|
10662
10702
|
annotations,
|
|
10663
|
-
result: { assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
|
|
10703
|
+
result: { kind: "assessment-annotation", assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
|
|
10664
10704
|
};
|
|
10665
10705
|
}
|
|
10666
10706
|
async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2) {
|
|
10667
10707
|
const entityTypeNames = params.entityTypes.map(String);
|
|
10668
|
-
const requestParams = [{ label: "
|
|
10669
|
-
const
|
|
10708
|
+
const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
|
|
10709
|
+
const completedItems = [];
|
|
10670
10710
|
let totalFound = 0;
|
|
10671
10711
|
let totalEmitted = 0;
|
|
10672
10712
|
let errors = 0;
|
|
10673
10713
|
const allAnnotations = [];
|
|
10674
|
-
onProgress(10,
|
|
10714
|
+
onProgress(10, { code: "loading" }, { requestParams });
|
|
10675
10715
|
const bodyLanguage = params.language ?? "en";
|
|
10676
10716
|
for (let i = 0; i < entityTypeNames.length; i++) {
|
|
10677
10717
|
const entityTypeName = entityTypeNames[i];
|
|
10678
10718
|
if (!entityTypeName) continue;
|
|
10679
10719
|
const pct = 20 + Math.round(i / entityTypeNames.length * 60);
|
|
10680
|
-
onProgress(pct,
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10720
|
+
onProgress(pct, { code: "detecting-entities", entityType: entityTypeName }, {
|
|
10721
|
+
// One vocabulary for "what is in flight" (CLEAN-PROGRESS D2): the entity
|
|
10722
|
+
// type is KB data, `kind` is the code the client localizes around it.
|
|
10723
|
+
current: { kind: "entity-type", value: entityTypeName },
|
|
10724
|
+
processed: i,
|
|
10725
|
+
total: entityTypeNames.length,
|
|
10684
10726
|
entitiesFound: totalFound,
|
|
10685
10727
|
entitiesEmitted: totalEmitted,
|
|
10686
|
-
|
|
10728
|
+
completedItems: [...completedItems],
|
|
10687
10729
|
requestParams
|
|
10688
10730
|
});
|
|
10689
10731
|
const extractedEntities = await extractEntities(
|
|
@@ -10693,25 +10735,28 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
10693
10735
|
params.includeDescriptiveReferences ?? false,
|
|
10694
10736
|
logger2,
|
|
10695
10737
|
params.sourceLanguage,
|
|
10696
|
-
//
|
|
10697
|
-
//
|
|
10698
|
-
//
|
|
10699
|
-
//
|
|
10738
|
+
// Liveness: fires at chunk boundaries AND every ~15 s while a single
|
|
10739
|
+
// inference call is in flight (DETECTION-HEARTBEAT). Progress feeds the
|
|
10740
|
+
// stall watchdog, the janitor, AND the client's inter-emission timeout,
|
|
10741
|
+
// so a long single-chunk call must not be silent. Percentage
|
|
10742
|
+
// interpolates within this entity type's band of the 20–80 range; a
|
|
10743
|
+
// heartbeat repeats the current position rather than inventing an
|
|
10744
|
+
// advance.
|
|
10700
10745
|
(completed, total) => {
|
|
10701
10746
|
const interpolated = 20 + Math.round((i + completed / total) / entityTypeNames.length * 60);
|
|
10702
|
-
onProgress(interpolated,
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10747
|
+
onProgress(interpolated, { code: "detecting-entities", entityType: entityTypeName }, {
|
|
10748
|
+
current: { kind: "entity-type", value: entityTypeName },
|
|
10749
|
+
processed: i,
|
|
10750
|
+
total: entityTypeNames.length,
|
|
10706
10751
|
entitiesFound: totalFound,
|
|
10707
10752
|
entitiesEmitted: totalEmitted,
|
|
10708
|
-
|
|
10753
|
+
completedItems: [...completedItems],
|
|
10709
10754
|
requestParams
|
|
10710
10755
|
});
|
|
10711
10756
|
}
|
|
10712
10757
|
);
|
|
10713
10758
|
totalFound += extractedEntities.length;
|
|
10714
|
-
|
|
10759
|
+
completedItems.push({ value: entityTypeName, foundCount: extractedEntities.length });
|
|
10715
10760
|
const unresolvedBody = [
|
|
10716
10761
|
{ type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
|
|
10717
10762
|
];
|
|
@@ -10742,36 +10787,49 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
10742
10787
|
}
|
|
10743
10788
|
}
|
|
10744
10789
|
const annotations = dedupeAnnotations(allAnnotations);
|
|
10745
|
-
onProgress(100,
|
|
10790
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "reference" }, { requestParams });
|
|
10746
10791
|
return {
|
|
10747
10792
|
annotations,
|
|
10748
|
-
result: { totalFound, totalEmitted: annotations.length, errors }
|
|
10793
|
+
result: { kind: "reference-annotation", totalFound, totalEmitted: annotations.length, errors }
|
|
10749
10794
|
};
|
|
10750
10795
|
}
|
|
10751
10796
|
async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
|
|
10752
|
-
onProgress(10,
|
|
10753
|
-
onProgress(30,
|
|
10797
|
+
onProgress(10, { code: "loading" });
|
|
10798
|
+
onProgress(30, { code: "analyzing-tags" });
|
|
10754
10799
|
const allTags = [];
|
|
10800
|
+
const completedItems = [];
|
|
10755
10801
|
for (let c = 0; c < params.categories.length; c++) {
|
|
10756
10802
|
const category = params.categories[c];
|
|
10803
|
+
const position = () => ({
|
|
10804
|
+
current: { kind: "category", value: category },
|
|
10805
|
+
processed: c,
|
|
10806
|
+
total: params.categories.length,
|
|
10807
|
+
completedItems: [...completedItems]
|
|
10808
|
+
});
|
|
10809
|
+
onProgress(
|
|
10810
|
+
30 + Math.round(c / params.categories.length * 30),
|
|
10811
|
+
{ code: "analyzing-tags" },
|
|
10812
|
+
position()
|
|
10813
|
+
);
|
|
10757
10814
|
const categoryTags = await AnnotationDetection.detectTags(
|
|
10758
10815
|
content,
|
|
10759
10816
|
inferenceClient,
|
|
10760
10817
|
params.schema,
|
|
10761
10818
|
category,
|
|
10762
10819
|
params.sourceLanguage,
|
|
10763
|
-
//
|
|
10764
|
-
//
|
|
10820
|
+
// Liveness (chunk boundaries + in-flight heartbeat): this category's
|
|
10821
|
+
// slice of the 30–60 band.
|
|
10765
10822
|
(completed, total) => onProgress(
|
|
10766
10823
|
30 + Math.round((c + completed / total) / params.categories.length * 30),
|
|
10767
|
-
|
|
10768
|
-
|
|
10824
|
+
{ code: "analyzing-tags" },
|
|
10825
|
+
position()
|
|
10769
10826
|
)
|
|
10770
10827
|
);
|
|
10828
|
+
completedItems.push({ value: category, foundCount: categoryTags.length });
|
|
10771
10829
|
allTags.push(...categoryTags);
|
|
10772
10830
|
}
|
|
10773
10831
|
const tags = allTags;
|
|
10774
|
-
onProgress(60,
|
|
10832
|
+
onProgress(60, { code: "creating-tag-annotations", count: tags.length });
|
|
10775
10833
|
const bodyLanguage = params.language ?? "en";
|
|
10776
10834
|
const annotations = dedupeAnnotations(tags.map((t) => {
|
|
10777
10835
|
const category = t.category ?? "unknown";
|
|
@@ -10786,10 +10844,10 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
10786
10844
|
const category = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
|
|
10787
10845
|
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
10788
10846
|
}
|
|
10789
|
-
onProgress(100,
|
|
10847
|
+
onProgress(100, { code: "complete-created", count: annotations.length, kind: "tag" });
|
|
10790
10848
|
return {
|
|
10791
10849
|
annotations,
|
|
10792
|
-
result: { tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
10850
|
+
result: { kind: "tag-annotation", tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
10793
10851
|
};
|
|
10794
10852
|
}
|
|
10795
10853
|
function assertWithinOutputBudget(byteLength) {
|
|
@@ -10809,7 +10867,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10809
10867
|
const title = params.title ?? "Untitled";
|
|
10810
10868
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
10811
10869
|
if (outputMediaType === "application/pdf") {
|
|
10812
|
-
onProgress(5,
|
|
10870
|
+
onProgress(5, { code: "generating-resource" });
|
|
10813
10871
|
const validIds = params.cite === true ? collectContextResourceIds(params.context) : null;
|
|
10814
10872
|
let generated2 = await generateResourceFromTopic(
|
|
10815
10873
|
title,
|
|
@@ -10836,7 +10894,17 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10836
10894
|
}
|
|
10837
10895
|
let compiled = compileTypst(source);
|
|
10838
10896
|
let repairs = 0;
|
|
10839
|
-
while ("error" in compiled
|
|
10897
|
+
while ("error" in compiled) {
|
|
10898
|
+
if (generated2.truncated) {
|
|
10899
|
+
throw new Error(
|
|
10900
|
+
`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}`
|
|
10901
|
+
);
|
|
10902
|
+
}
|
|
10903
|
+
if (repairs >= MAX_COMPILE_REPAIRS) {
|
|
10904
|
+
throw new Error(
|
|
10905
|
+
`Typst compilation failed after ${MAX_COMPILE_REPAIRS} repair attempts: ${compiled.error}`
|
|
10906
|
+
);
|
|
10907
|
+
}
|
|
10840
10908
|
repairs++;
|
|
10841
10909
|
logger2.warn("Typst compile failed \u2014 feeding the error back for repair", {
|
|
10842
10910
|
attempt: repairs,
|
|
@@ -10868,25 +10936,23 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10868
10936
|
}
|
|
10869
10937
|
compiled = compileTypst(source);
|
|
10870
10938
|
}
|
|
10871
|
-
if ("error" in compiled) {
|
|
10872
|
-
throw new Error(
|
|
10873
|
-
`Typst compilation failed after ${MAX_COMPILE_REPAIRS} repair attempts: ${compiled.error}`
|
|
10874
|
-
);
|
|
10875
|
-
}
|
|
10876
10939
|
assertWithinOutputBudget(compiled.pdf.byteLength);
|
|
10877
|
-
onProgress(95,
|
|
10940
|
+
onProgress(95, { code: "creating-resource" });
|
|
10941
|
+
onProgress(100, { code: "complete-generated", truncated: generated2.truncated });
|
|
10878
10942
|
return {
|
|
10879
10943
|
content: compiled.pdf,
|
|
10880
|
-
title
|
|
10944
|
+
title,
|
|
10881
10945
|
format: outputMediaType,
|
|
10882
10946
|
citations: citations2,
|
|
10883
10947
|
result: {
|
|
10948
|
+
kind: "generation",
|
|
10884
10949
|
resourceId: "",
|
|
10885
|
-
resourceName:
|
|
10950
|
+
resourceName: title,
|
|
10951
|
+
truncated: generated2.truncated
|
|
10886
10952
|
}
|
|
10887
10953
|
};
|
|
10888
10954
|
}
|
|
10889
|
-
onProgress(5,
|
|
10955
|
+
onProgress(5, { code: "generating-resource" });
|
|
10890
10956
|
const generated = await generateResourceFromTopic(
|
|
10891
10957
|
title,
|
|
10892
10958
|
entityTypes,
|
|
@@ -10910,17 +10976,20 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10910
10976
|
content = resolved.content;
|
|
10911
10977
|
citations = resolved.citations;
|
|
10912
10978
|
}
|
|
10913
|
-
onProgress(95,
|
|
10979
|
+
onProgress(95, { code: "creating-resource" });
|
|
10914
10980
|
const artifact = new TextEncoder().encode(content);
|
|
10915
10981
|
assertWithinOutputBudget(artifact.byteLength);
|
|
10982
|
+
onProgress(100, { code: "complete-generated", truncated: generated.truncated });
|
|
10916
10983
|
return {
|
|
10917
10984
|
content: artifact,
|
|
10918
|
-
title
|
|
10985
|
+
title,
|
|
10919
10986
|
format: outputMediaType,
|
|
10920
10987
|
citations,
|
|
10921
10988
|
result: {
|
|
10989
|
+
kind: "generation",
|
|
10922
10990
|
resourceId: "",
|
|
10923
|
-
resourceName:
|
|
10991
|
+
resourceName: title,
|
|
10992
|
+
truncated: generated.truncated
|
|
10924
10993
|
}
|
|
10925
10994
|
};
|
|
10926
10995
|
}
|
|
@@ -10935,7 +11004,7 @@ async function prepareDetection(mediaType, session, resourceId, userId, generato
|
|
|
10935
11004
|
key: calculateChecksum(bytes),
|
|
10936
11005
|
store
|
|
10937
11006
|
});
|
|
10938
|
-
if ("declined"
|
|
11007
|
+
if (extracted.kind === "declined") return extracted;
|
|
10939
11008
|
if (!extracted.text.trim()) return { declined: "empty" };
|
|
10940
11009
|
const items = extracted.items;
|
|
10941
11010
|
if (items && items.length > 0) {
|
|
@@ -10950,13 +11019,18 @@ async function prepareDetection(mediaType, session, resourceId, userId, generato
|
|
|
10950
11019
|
buildAnnotation: (motivation, match, body) => buildTextAnnotation(extracted.text, resourceId, userId, generator, motivation, match, body)
|
|
10951
11020
|
};
|
|
10952
11021
|
}
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
}
|
|
11022
|
+
function referenceIdOf(job) {
|
|
11023
|
+
if (job.type === "generation") {
|
|
11024
|
+
const context = job.params.context;
|
|
11025
|
+
const focus = context?.focus;
|
|
11026
|
+
if (focus?.kind === "annotation" && typeof focus.annotation?.id === "string") {
|
|
11027
|
+
return focus.annotation.id;
|
|
11028
|
+
}
|
|
11029
|
+
return void 0;
|
|
11030
|
+
}
|
|
11031
|
+
const ref = job.params.referenceId;
|
|
11032
|
+
return typeof ref === "string" ? ref : void 0;
|
|
11033
|
+
}
|
|
10960
11034
|
async function emitEvent(session, channel, payload) {
|
|
10961
11035
|
await session.client.transport.emit(channel, payload);
|
|
10962
11036
|
}
|
|
@@ -10973,7 +11047,7 @@ function startWorkerProcess(config) {
|
|
|
10973
11047
|
handleJob(adapter, config, job).catch((error) => {
|
|
10974
11048
|
const message = error instanceof Error ? error.message : String(error);
|
|
10975
11049
|
logger2.error("Job failed", { jobId: job.jobId, error: message, stack: error instanceof Error ? error.stack : void 0 });
|
|
10976
|
-
const failAnnotationId = job
|
|
11050
|
+
const failAnnotationId = referenceIdOf(job);
|
|
10977
11051
|
if (isJobType(job.type)) {
|
|
10978
11052
|
emitEvent(session, "job:fail", {
|
|
10979
11053
|
resourceId: job.resourceId,
|
|
@@ -11022,7 +11096,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11022
11096
|
}
|
|
11023
11097
|
const jobType = job.type;
|
|
11024
11098
|
const resourceId$1 = resourceId(job.resourceId);
|
|
11025
|
-
const annotationId = job
|
|
11099
|
+
const annotationId = referenceIdOf(job);
|
|
11026
11100
|
const lifecycleBase = {
|
|
11027
11101
|
resourceId: resourceId$1,
|
|
11028
11102
|
jobId,
|
|
@@ -11038,7 +11112,11 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11038
11112
|
if (jobType !== "generation") {
|
|
11039
11113
|
const descriptor = await session.client.browse.resource(resourceId$1).fresh();
|
|
11040
11114
|
const mediaType = getPrimaryMediaType(descriptor);
|
|
11041
|
-
const source = await
|
|
11115
|
+
const source = await withSpan(
|
|
11116
|
+
"detection:prepare",
|
|
11117
|
+
() => prepareDetection(mediaType ?? "", session, resourceId$1, userId, generator, config.anchoredTextStore),
|
|
11118
|
+
{ attrs: { "resource.id": resourceId$1, "media.type": mediaType ?? "unknown" } }
|
|
11119
|
+
);
|
|
11042
11120
|
if ("declined" in source) {
|
|
11043
11121
|
if (source.declined === "no-extractor") {
|
|
11044
11122
|
throw new Error(`Cannot run ${jobType} on resource ${resourceId$1}: media type '${mediaType ?? "unknown"}' has no extractable text to analyze`);
|
|
@@ -11046,9 +11124,9 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11046
11124
|
await emitEvent(session, "job:complete", {
|
|
11047
11125
|
...lifecycleBase,
|
|
11048
11126
|
result: {
|
|
11127
|
+
kind: "declined",
|
|
11049
11128
|
declined: true,
|
|
11050
|
-
reason: source.declined
|
|
11051
|
-
message: DECLINE_MESSAGES[source.declined]
|
|
11129
|
+
reason: source.declined
|
|
11052
11130
|
}
|
|
11053
11131
|
});
|
|
11054
11132
|
adapter.completeJob();
|
|
@@ -11056,13 +11134,12 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11056
11134
|
}
|
|
11057
11135
|
ready = source;
|
|
11058
11136
|
}
|
|
11059
|
-
const onProgress = (percentage, message,
|
|
11137
|
+
const onProgress = (percentage, message, extra) => {
|
|
11060
11138
|
adapter.touchActivity();
|
|
11061
11139
|
emitEvent(session, "job:report-progress", {
|
|
11062
11140
|
...lifecycleBase,
|
|
11063
11141
|
percentage,
|
|
11064
11142
|
progress: {
|
|
11065
|
-
stage,
|
|
11066
11143
|
percentage,
|
|
11067
11144
|
message,
|
|
11068
11145
|
...annotationId ? { annotationId } : {},
|
|
@@ -11153,6 +11230,11 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11153
11230
|
});
|
|
11154
11231
|
adapter.completeJob();
|
|
11155
11232
|
} else if (jobType === "generation") {
|
|
11233
|
+
if (!isGenerationJobParams(job.params)) {
|
|
11234
|
+
throw new Error(
|
|
11235
|
+
`generation job ${job.jobId}: params do not satisfy GenerationJobParams (title, storageUri, and context are required)`
|
|
11236
|
+
);
|
|
11237
|
+
}
|
|
11156
11238
|
const genResult = await processGenerationJob(
|
|
11157
11239
|
inferenceClient,
|
|
11158
11240
|
job.params,
|
|
@@ -11160,6 +11242,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11160
11242
|
config.logger
|
|
11161
11243
|
);
|
|
11162
11244
|
const genParams = job.params;
|
|
11245
|
+
const genReferenceId = referenceIdOf(job);
|
|
11163
11246
|
const storageUri = deriveStorageUri(genResult.title, genResult.format);
|
|
11164
11247
|
const { resourceId: newResourceId } = await session.client.yield.resource({
|
|
11165
11248
|
name: genResult.title,
|
|
@@ -11167,13 +11250,13 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11167
11250
|
format: genResult.format,
|
|
11168
11251
|
storageUri,
|
|
11169
11252
|
sourceResourceId: resourceId$1,
|
|
11170
|
-
...
|
|
11253
|
+
...genReferenceId ? { sourceAnnotationId: genReferenceId } : {},
|
|
11171
11254
|
...genParams.prompt ? { generationPrompt: genParams.prompt } : {},
|
|
11172
11255
|
...genParams.language ? { language: genParams.language } : {},
|
|
11173
11256
|
...genParams.entityTypes && genParams.entityTypes.length > 0 ? { entityTypes: genParams.entityTypes } : {},
|
|
11174
11257
|
generator
|
|
11175
11258
|
});
|
|
11176
|
-
if (!
|
|
11259
|
+
if (!genReferenceId) {
|
|
11177
11260
|
const { annotation: provenanceRef } = assembleAnnotation(
|
|
11178
11261
|
{
|
|
11179
11262
|
motivation: "linking",
|
|
@@ -11237,7 +11320,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11237
11320
|
}
|
|
11238
11321
|
await emitEvent(session, "job:complete", {
|
|
11239
11322
|
...lifecycleBase,
|
|
11240
|
-
result: { resourceId: newResourceId, resourceName: genResult.title }
|
|
11323
|
+
result: { kind: "generation", resourceId: newResourceId, resourceName: genResult.title, truncated: genResult.result.truncated }
|
|
11241
11324
|
});
|
|
11242
11325
|
adapter.completeJob();
|
|
11243
11326
|
} else {
|