@semiont/jobs 0.5.10 → 0.5.12
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/index.d.ts +51 -2
- package/dist/index.js +127 -21
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +243 -108
- package/dist/worker-main.js.map +1 -1
- package/package.json +9 -9
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,29 @@ interface GenerationParams {
|
|
|
103
103
|
* generation coverage is text-only for now and the gap is surfaced, not hidden.
|
|
104
104
|
*/
|
|
105
105
|
outputMediaType?: SupportedMediaType;
|
|
106
|
+
/**
|
|
107
|
+
* What the model is asked to DO — the prompt's framing verb. Canonical values map
|
|
108
|
+
* to tested framings; any other string is used verbatim as the framing instruction
|
|
109
|
+
* (loud degrade: the worker warns, never silently falls back to 'resource').
|
|
110
|
+
* Default 'resource'. See .plans/YIELD-STRUCTURE.md.
|
|
111
|
+
*/
|
|
112
|
+
task?: 'resource' | 'answer' | 'summary' | (string & {});
|
|
113
|
+
/**
|
|
114
|
+
* How the output is internally segmented — text-bearing shape, subordinate to
|
|
115
|
+
* `outputMediaType` (never its peer). Canonical values map to tested guidance; any
|
|
116
|
+
* other string becomes a freeform "Organize the output as: …" instruction (loud
|
|
117
|
+
* degrade). UNSET means NO structure directive is emitted — the task framing and
|
|
118
|
+
* the model determine shape. See .plans/YIELD-STRUCTURE.md D2/D5.
|
|
119
|
+
*/
|
|
120
|
+
structure?: 'prose' | 'sections' | 'chat' | (string & {});
|
|
121
|
+
/**
|
|
122
|
+
* Ask the model to cite: emit `[[<id>]]` transport tokens after each claim, using
|
|
123
|
+
* the ids the context embedding provides (CONTEXT-IDENTIFIERS). The worker
|
|
124
|
+
* validates each id against the embedded context (unknown ids are dropped
|
|
125
|
+
* loudly), strips the tokens from the stored content, and mints W3C linking
|
|
126
|
+
* annotations on the derived resource. See .plans/INLINE-CITATIONS.md.
|
|
127
|
+
*/
|
|
128
|
+
cite?: boolean;
|
|
106
129
|
}
|
|
107
130
|
/**
|
|
108
131
|
* Highlight detection job parameters
|
|
@@ -179,7 +202,8 @@ interface DetectionResult {
|
|
|
179
202
|
* Generation job progress
|
|
180
203
|
*/
|
|
181
204
|
interface YieldProgress {
|
|
182
|
-
|
|
205
|
+
/** The two real generation transitions — LLM call running, then persisting. */
|
|
206
|
+
stage: 'generating' | 'creating';
|
|
183
207
|
percentage: number;
|
|
184
208
|
message?: string;
|
|
185
209
|
}
|
|
@@ -476,6 +500,30 @@ declare class FsJobQueue implements JobQueue {
|
|
|
476
500
|
}>;
|
|
477
501
|
}
|
|
478
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Citation-token resolver (INLINE-CITATIONS P1).
|
|
505
|
+
*
|
|
506
|
+
* Under `cite`, the generation prompt asks the model to emit `[[<resourceId>]]` /
|
|
507
|
+
* `[[<resourceId>/<annotationId>]]` transport tokens after each claim, citing ids
|
|
508
|
+
* the context embedding put in front of it (CONTEXT-IDENTIFIERS). Tokens are
|
|
509
|
+
* TRANSPORT, not content: this resolver parses them, validates each id against the
|
|
510
|
+
* ids actually present in the embedded context (the hallucination guard — an
|
|
511
|
+
* unknown id is dropped loudly, never silently linked), STRIPS them from the
|
|
512
|
+
* content before upload, and returns claim-span citations the worker mints as W3C
|
|
513
|
+
* linking annotations on the derived resource. See .plans/INLINE-CITATIONS.md.
|
|
514
|
+
*/
|
|
515
|
+
|
|
516
|
+
interface GenerationCitation {
|
|
517
|
+
/** The cited source resource (validated present in the embedded context). */
|
|
518
|
+
resourceId: string;
|
|
519
|
+
/** The contributing annotation, when the cited excerpt was annotation-derived. */
|
|
520
|
+
annotationId?: string;
|
|
521
|
+
/** Claim span in the FINAL (token-stripped) content; substring(start, end) === exact. */
|
|
522
|
+
start: number;
|
|
523
|
+
end: number;
|
|
524
|
+
exact: string;
|
|
525
|
+
}
|
|
526
|
+
|
|
479
527
|
/**
|
|
480
528
|
* Job Processors
|
|
481
529
|
*
|
|
@@ -510,6 +558,7 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
|
|
|
510
558
|
content: string;
|
|
511
559
|
title: string;
|
|
512
560
|
format: SupportedMediaType;
|
|
561
|
+
citations: GenerationCitation[];
|
|
513
562
|
result: GenerationResult;
|
|
514
563
|
}>;
|
|
515
564
|
|
|
@@ -637,7 +686,7 @@ declare class AnnotationDetection {
|
|
|
637
686
|
* generate German content from an English source resource. See
|
|
638
687
|
* `types.ts` "Locale conventions" for the full discussion.
|
|
639
688
|
*/
|
|
640
|
-
declare function generateResourceFromTopic(topic: string, entityTypes: string[], client: InferenceClient, logger: Logger, userPrompt?: string, locale?: string, context?: GatheredContext, temperature?: number, maxTokens?: number, sourceLanguage?: string, outputMediaType?: SupportedMediaType): Promise<{
|
|
689
|
+
declare function generateResourceFromTopic(topic: string, entityTypes: string[], client: InferenceClient, logger: Logger, userPrompt?: string, locale?: string, context?: GatheredContext, temperature?: number, maxTokens?: number, sourceLanguage?: string, outputMediaType?: SupportedMediaType, task?: string, structure?: string, cite?: boolean): Promise<{
|
|
641
690
|
title: string;
|
|
642
691
|
content: string;
|
|
643
692
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -1068,7 +1068,13 @@ Example output:
|
|
|
1068
1068
|
function getLanguageName(locale) {
|
|
1069
1069
|
return getLocaleEnglishName(locale) || locale;
|
|
1070
1070
|
}
|
|
1071
|
-
|
|
1071
|
+
var RESOURCE_CONTENT_CAP = 4e3;
|
|
1072
|
+
var SEMANTIC_MATCH_LIMIT = 3;
|
|
1073
|
+
var SEMANTIC_MATCH_CHARS = 240;
|
|
1074
|
+
function idLabel(resourceId, annotationId) {
|
|
1075
|
+
return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
|
|
1076
|
+
}
|
|
1077
|
+
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false) {
|
|
1072
1078
|
logger.debug("Generating resource from topic", {
|
|
1073
1079
|
topicPreview: topic.substring(0, 100),
|
|
1074
1080
|
entityTypes,
|
|
@@ -1077,7 +1083,10 @@ async function generateResourceFromTopic(topic, entityTypes, client, logger, use
|
|
|
1077
1083
|
sourceLanguage,
|
|
1078
1084
|
hasContext: !!context,
|
|
1079
1085
|
temperature,
|
|
1080
|
-
maxTokens
|
|
1086
|
+
maxTokens,
|
|
1087
|
+
outputMediaType,
|
|
1088
|
+
task,
|
|
1089
|
+
structure
|
|
1081
1090
|
});
|
|
1082
1091
|
const finalTemperature = temperature ?? 0.7;
|
|
1083
1092
|
const finalMaxTokens = maxTokens ?? 500;
|
|
@@ -1098,7 +1107,7 @@ The source resource and embedded context are in ${getLanguageName(sourceLanguage
|
|
|
1098
1107
|
if (focus.kind === "annotation") {
|
|
1099
1108
|
const parts = [];
|
|
1100
1109
|
parts.push(`- Annotation motivation: ${focus.annotation.motivation}`);
|
|
1101
|
-
parts.push(`- Source resource: ${focus.sourceResource.name}`);
|
|
1110
|
+
parts.push(`- Source resource: ${focus.sourceResource.name} ${idLabel(mainResourceId)}`);
|
|
1102
1111
|
const { motivation, body } = focus.annotation;
|
|
1103
1112
|
if (motivation === "commenting" || motivation === "assessing") {
|
|
1104
1113
|
const bodyItem = Array.isArray(body) ? body[0] : body;
|
|
@@ -1124,8 +1133,7 @@ ${after ? `${after}...` : ""}
|
|
|
1124
1133
|
`;
|
|
1125
1134
|
}
|
|
1126
1135
|
} else {
|
|
1127
|
-
const
|
|
1128
|
-
const parts = [`- Resource: ${focus.resource.name}`];
|
|
1136
|
+
const parts = [`- Resource: ${focus.resource.name} ${idLabel(mainResourceId)}`];
|
|
1129
1137
|
if (focus.summary) parts.push(`- Summary: ${focus.summary}`);
|
|
1130
1138
|
if (focus.suggestedReferences && focus.suggestedReferences.length > 0) {
|
|
1131
1139
|
parts.push(`- Suggested references: ${focus.suggestedReferences.join(", ")}`);
|
|
@@ -1158,11 +1166,11 @@ ${blocks}
|
|
|
1158
1166
|
const views = deriveViews(context.graph, mainResourceId, focalAnnotationId);
|
|
1159
1167
|
const parts = [];
|
|
1160
1168
|
if (views.connections.length > 0) {
|
|
1161
|
-
const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""}`).join(", ");
|
|
1169
|
+
const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""} ${idLabel(c.resourceId)}`).join(", ");
|
|
1162
1170
|
parts.push(`- Connected resources: ${connList}`);
|
|
1163
1171
|
}
|
|
1164
1172
|
if (views.citedByCount > 0) {
|
|
1165
|
-
const citedNames = views.citedBy.map((c) => c.resourceName).join(", ");
|
|
1173
|
+
const citedNames = views.citedBy.map((c) => `${c.resourceName} ${idLabel(c.resourceId)}`).join(", ");
|
|
1166
1174
|
parts.push(`- This resource is cited by ${views.citedByCount} other resource${views.citedByCount > 1 ? "s" : ""}${citedNames ? `: ${citedNames}` : ""}`);
|
|
1167
1175
|
}
|
|
1168
1176
|
if (views.siblingEntityTypes.length > 0) {
|
|
@@ -1182,25 +1190,52 @@ ${parts.join("\n")}`;
|
|
|
1182
1190
|
let semanticContextSection = "";
|
|
1183
1191
|
const similar = context?.semanticContext?.similar ?? [];
|
|
1184
1192
|
if (similar.length > 0) {
|
|
1185
|
-
const lines = [...similar].sort((a, b) => b.score - a.score).slice(0,
|
|
1193
|
+
const lines = [...similar].sort((a, b) => b.score - a.score).slice(0, SEMANTIC_MATCH_LIMIT).map((m) => `- ${idLabel(m.resourceId, m.annotationId)} (${m.score.toFixed(2)}) ${m.text.slice(0, SEMANTIC_MATCH_CHARS)}`);
|
|
1186
1194
|
semanticContextSection = `
|
|
1187
1195
|
|
|
1188
1196
|
Related passages from the knowledge base:
|
|
1189
1197
|
${lines.join("\n")}`;
|
|
1198
|
+
}
|
|
1199
|
+
let leadLine;
|
|
1200
|
+
if (task === "resource") {
|
|
1201
|
+
leadLine = `Generate a concise, informative resource about "${topic}".`;
|
|
1202
|
+
} else if (task === "answer") {
|
|
1203
|
+
leadLine = `Answer the following question directly and concisely, grounded in the provided context: "${topic}"`;
|
|
1204
|
+
} else if (task === "summary") {
|
|
1205
|
+
leadLine = `Write a concise summary of "${topic}".`;
|
|
1206
|
+
} else {
|
|
1207
|
+
logger.warn("Unknown task \u2014 using it verbatim as the framing instruction", { task });
|
|
1208
|
+
leadLine = `${task}
|
|
1209
|
+
Topic: "${topic}"`;
|
|
1190
1210
|
}
|
|
1191
1211
|
const isPlainText = outputMediaType === "text/plain";
|
|
1192
|
-
|
|
1212
|
+
let structureRequirement = "";
|
|
1213
|
+
let titleRequirement = "";
|
|
1214
|
+
if (structure === "sections") {
|
|
1215
|
+
structureRequirement = isPlainText ? "\n- Organize the content into titled sections with well-structured paragraphs" : "\n- Organize the content into titled sections (## Section) with well-structured paragraphs";
|
|
1216
|
+
if (!isPlainText) {
|
|
1217
|
+
titleRequirement = "\n- Start with a clear heading (# Title)";
|
|
1218
|
+
}
|
|
1219
|
+
} else if (structure === "prose") {
|
|
1220
|
+
structureRequirement = "\n- Write flowing, well-structured paragraphs with no section headings";
|
|
1221
|
+
} else if (structure === "chat") {
|
|
1222
|
+
structureRequirement = "\n- Structure the content as a conversational chat transcript \u2014 a sequence of alternating, speaker-labeled turns (no section headings)";
|
|
1223
|
+
} else if (structure) {
|
|
1224
|
+
logger.warn("Unknown structure \u2014 passing it through as freeform organization guidance", { structure });
|
|
1225
|
+
structureRequirement = `
|
|
1226
|
+
- Organize the output as: ${structure}`;
|
|
1227
|
+
}
|
|
1228
|
+
const citeRequirement = cite ? "\n- Ground every claim in the provided context. Immediately after each claim, cite its source by emitting [[<id>]], where <id> is an id shown in square brackets in the context above (for a passage labeled [abc], emit [[abc]]). Cite only ids that appear in the context." : "";
|
|
1193
1229
|
const formatRequirements = isPlainText ? `- Write the response as plain text \u2014 no formatting markup (no #, *, backticks, headings, or links)
|
|
1194
|
-
- Begin with the title on its own first line` : `-
|
|
1195
|
-
- Use markdown formatting
|
|
1230
|
+
- Begin with the title on its own first line` : `- Use markdown formatting
|
|
1196
1231
|
- Write the response as markdown`;
|
|
1197
|
-
const prompt =
|
|
1198
|
-
${
|
|
1199
|
-
${
|
|
1232
|
+
const prompt = `${leadLine}
|
|
1233
|
+
${userPrompt ? `Instruction: ${userPrompt}` : ""}
|
|
1234
|
+
${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
|
|
1200
1235
|
|
|
1201
1236
|
Requirements:
|
|
1202
|
-
- Aim for approximately ${finalMaxTokens} tokens of content
|
|
1203
|
-
- Be factual and informative
|
|
1237
|
+
- Aim for approximately ${finalMaxTokens} tokens of content
|
|
1238
|
+
- Be factual and informative${structureRequirement}${titleRequirement}${citeRequirement}
|
|
1204
1239
|
${formatRequirements}`;
|
|
1205
1240
|
const parseResponse = (response2) => {
|
|
1206
1241
|
let content = response2.trim();
|
|
@@ -1239,6 +1274,67 @@ ${formatRequirements}`;
|
|
|
1239
1274
|
});
|
|
1240
1275
|
return result;
|
|
1241
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
// src/workers/generation/citation-resolver.ts
|
|
1279
|
+
var CITATION_TOKEN = /\[\[([^\s[\]/]+)(?:\/([^\s[\]/]+))?\]\]/g;
|
|
1280
|
+
function collectContextResourceIds(context) {
|
|
1281
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1282
|
+
if (!context) return ids;
|
|
1283
|
+
const { focus } = context;
|
|
1284
|
+
ids.add(focus.kind === "annotation" ? focus.sourceResource["@id"] : focus.resource["@id"]);
|
|
1285
|
+
for (const node of context.graph.nodes) {
|
|
1286
|
+
if (node.type === "resource") ids.add(node.id);
|
|
1287
|
+
}
|
|
1288
|
+
for (const m of context.semanticContext?.similar ?? []) ids.add(m.resourceId);
|
|
1289
|
+
if (focus.kind === "resource") {
|
|
1290
|
+
for (const id of Object.keys(focus.content?.related ?? {})) ids.add(id);
|
|
1291
|
+
}
|
|
1292
|
+
return ids;
|
|
1293
|
+
}
|
|
1294
|
+
function resolveCitationTokens(content, validResourceIds, logger) {
|
|
1295
|
+
const citations = [];
|
|
1296
|
+
let clean = "";
|
|
1297
|
+
let last = 0;
|
|
1298
|
+
for (const match of content.matchAll(CITATION_TOKEN)) {
|
|
1299
|
+
const token = match[0];
|
|
1300
|
+
const citedResourceId = match[1];
|
|
1301
|
+
const annotationId = match[2];
|
|
1302
|
+
clean += content.slice(last, match.index).replace(/[ \t]+$/, "");
|
|
1303
|
+
last = match.index + token.length;
|
|
1304
|
+
if (!validResourceIds.has(citedResourceId)) {
|
|
1305
|
+
logger.warn("Citation token references an id absent from the provided context \u2014 dropped", {
|
|
1306
|
+
resourceId: citedResourceId
|
|
1307
|
+
});
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const end = clean.length;
|
|
1311
|
+
let start = 0;
|
|
1312
|
+
for (let i = end - 2; i >= 0; i--) {
|
|
1313
|
+
const ch = clean[i];
|
|
1314
|
+
if (ch === "." || ch === "!" || ch === "?" || ch === "\n") {
|
|
1315
|
+
start = i + 1;
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
while (start < end && /\s/.test(clean[start])) start++;
|
|
1320
|
+
const exact = clean.slice(start, end);
|
|
1321
|
+
if (exact.length === 0) {
|
|
1322
|
+
logger.warn("Citation token has no preceding claim text \u2014 dropped", {
|
|
1323
|
+
resourceId: citedResourceId
|
|
1324
|
+
});
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
citations.push({
|
|
1328
|
+
resourceId: citedResourceId,
|
|
1329
|
+
...annotationId ? { annotationId } : {},
|
|
1330
|
+
start,
|
|
1331
|
+
end,
|
|
1332
|
+
exact
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
clean += content.slice(last);
|
|
1336
|
+
return { content: clean, citations };
|
|
1337
|
+
}
|
|
1242
1338
|
function toMatch(r) {
|
|
1243
1339
|
return {
|
|
1244
1340
|
exact: r.exact,
|
|
@@ -1527,10 +1623,9 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1527
1623
|
`Unsupported outputMediaType for generation: ${outputMediaType}. Generation produces ${GENERATABLE_MEDIA_TYPES.join(" or ")}.`
|
|
1528
1624
|
);
|
|
1529
1625
|
}
|
|
1530
|
-
onProgress(20, "Fetching context...", "fetching");
|
|
1531
1626
|
const title = params.title ?? "Untitled";
|
|
1532
1627
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
1533
|
-
onProgress(
|
|
1628
|
+
onProgress(5, "Generating resource...", "generating");
|
|
1534
1629
|
const generated = await generateResourceFromTopic(
|
|
1535
1630
|
title,
|
|
1536
1631
|
entityTypes,
|
|
@@ -1542,13 +1637,24 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1542
1637
|
params.temperature,
|
|
1543
1638
|
params.maxTokens,
|
|
1544
1639
|
params.sourceLanguage,
|
|
1545
|
-
outputMediaType
|
|
1640
|
+
outputMediaType,
|
|
1641
|
+
params.task,
|
|
1642
|
+
params.structure,
|
|
1643
|
+
params.cite
|
|
1546
1644
|
);
|
|
1547
|
-
|
|
1645
|
+
let content = generated.content;
|
|
1646
|
+
let citations = [];
|
|
1647
|
+
if (params.cite === true) {
|
|
1648
|
+
const resolved = resolveCitationTokens(content, collectContextResourceIds(params.context), logger);
|
|
1649
|
+
content = resolved.content;
|
|
1650
|
+
citations = resolved.citations;
|
|
1651
|
+
}
|
|
1652
|
+
onProgress(95, "Creating resource...", "creating");
|
|
1548
1653
|
return {
|
|
1549
|
-
content
|
|
1654
|
+
content,
|
|
1550
1655
|
title: generated.title ?? title,
|
|
1551
1656
|
format: outputMediaType,
|
|
1657
|
+
citations,
|
|
1552
1658
|
result: {
|
|
1553
1659
|
resourceId: "",
|
|
1554
1660
|
resourceName: generated.title ?? title
|