@semiont/jobs 0.5.10 → 0.5.11

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 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
@@ -476,6 +499,30 @@ declare class FsJobQueue implements JobQueue {
476
499
  }>;
477
500
  }
478
501
 
502
+ /**
503
+ * Citation-token resolver (INLINE-CITATIONS P1).
504
+ *
505
+ * Under `cite`, the generation prompt asks the model to emit `[[<resourceId>]]` /
506
+ * `[[<resourceId>/<annotationId>]]` transport tokens after each claim, citing ids
507
+ * the context embedding put in front of it (CONTEXT-IDENTIFIERS). Tokens are
508
+ * TRANSPORT, not content: this resolver parses them, validates each id against the
509
+ * ids actually present in the embedded context (the hallucination guard — an
510
+ * unknown id is dropped loudly, never silently linked), STRIPS them from the
511
+ * content before upload, and returns claim-span citations the worker mints as W3C
512
+ * linking annotations on the derived resource. See .plans/INLINE-CITATIONS.md.
513
+ */
514
+
515
+ interface GenerationCitation {
516
+ /** The cited source resource (validated present in the embedded context). */
517
+ resourceId: string;
518
+ /** The contributing annotation, when the cited excerpt was annotation-derived. */
519
+ annotationId?: string;
520
+ /** Claim span in the FINAL (token-stripped) content; substring(start, end) === exact. */
521
+ start: number;
522
+ end: number;
523
+ exact: string;
524
+ }
525
+
479
526
  /**
480
527
  * Job Processors
481
528
  *
@@ -510,6 +557,7 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
510
557
  content: string;
511
558
  title: string;
512
559
  format: SupportedMediaType;
560
+ citations: GenerationCitation[];
513
561
  result: GenerationResult;
514
562
  }>;
515
563
 
@@ -637,7 +685,7 @@ declare class AnnotationDetection {
637
685
  * generate German content from an English source resource. See
638
686
  * `types.ts` "Locale conventions" for the full discussion.
639
687
  */
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<{
688
+ 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
689
  title: string;
642
690
  content: string;
643
691
  }>;
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
- async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown") {
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 RESOURCE_CONTENT_CAP = 4e3;
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, 3).map((m) => `- (${m.score.toFixed(2)}) ${m.text.slice(0, 240)}`);
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
- const structureGuidance = !isPlainText && finalMaxTokens >= 1e3 ? "organized into titled sections (## Section) with well-structured paragraphs" : "organized into well-structured paragraphs";
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` : `- Start with a clear heading (# Title)
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 = `Generate a concise, informative resource about "${topic}".
1198
- ${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}
1199
- ${userPrompt ? `Additional context: ${userPrompt}` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
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, ${structureGuidance}
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,
@@ -1542,13 +1638,24 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
1542
1638
  params.temperature,
1543
1639
  params.maxTokens,
1544
1640
  params.sourceLanguage,
1545
- outputMediaType
1641
+ outputMediaType,
1642
+ params.task,
1643
+ params.structure,
1644
+ params.cite
1546
1645
  );
1646
+ let content = generated.content;
1647
+ let citations = [];
1648
+ if (params.cite === true) {
1649
+ const resolved = resolveCitationTokens(content, collectContextResourceIds(params.context), logger);
1650
+ content = resolved.content;
1651
+ citations = resolved.citations;
1652
+ }
1547
1653
  onProgress(85, "Creating resource...", "creating");
1548
1654
  return {
1549
- content: generated.content,
1655
+ content,
1550
1656
  title: generated.title ?? title,
1551
1657
  format: outputMediaType,
1658
+ citations,
1552
1659
  result: {
1553
1660
  resourceId: "",
1554
1661
  resourceName: generated.title ?? title