@semiont/jobs 0.5.25 → 0.5.26

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
@@ -579,6 +579,7 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
579
579
  * NOTE: These are static utility methods without logger access.
580
580
  * Console statements kept for debugging - consider adding logger parameter in future.
581
581
  */
582
+
582
583
  /**
583
584
  * Represents a detected comment with validated position
584
585
  */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { join } from 'path';
4
- import { jobId, deriveViews, reconcileSelector, GENERATABLE_MEDIA_TYPES, estimateTokens, chunkText, isObject, isString, getLocaleEnglishName, isArray } from '@semiont/core';
4
+ import { jobId, deriveViews, reconcileSelector, GENERATABLE_MEDIA_TYPES, estimateTokens, chunkText, isObject, isString, getLocaleEnglishName } from '@semiont/core';
5
5
  import { execFileSync } from 'child_process';
6
6
  import { tmpdir } from 'os';
7
7
  import { withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
@@ -449,15 +449,15 @@ async function withTimeout(work, label) {
449
449
  clearTimeout(timer);
450
450
  }
451
451
  }
452
- function boundedGenerate(client, prompt, maxTokens, temperature, options) {
452
+ function boundedGenerate(client, prompt, maxTokens, temperature) {
453
453
  return withTimeout(
454
- client.generateText(prompt, maxTokens, temperature, options),
454
+ client.generateText(prompt, maxTokens, temperature),
455
455
  `${client.type}:${client.modelId}`
456
456
  );
457
457
  }
458
- function boundedGenerateWithMetadata(client, prompt, maxTokens, temperature, options) {
458
+ function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema) {
459
459
  return withTimeout(
460
- client.generateTextWithMetadata(prompt, maxTokens, temperature, options),
460
+ client.generateStructured(prompt, maxTokens, temperature, elementSchema),
461
461
  `${client.type}:${client.modelId}`
462
462
  );
463
463
  }
@@ -791,32 +791,57 @@ Example format:
791
791
  return prompt;
792
792
  }
793
793
  };
794
- function parseJsonArray(response, motivation) {
795
- let parsed;
796
- try {
797
- parsed = JSON.parse(response.trim());
798
- } catch (error) {
799
- console.error(`[MotivationParsers] Failed to parse AI ${motivation} response:`, error);
800
- console.error("Raw response:", response);
801
- throw error instanceof Error ? error : new Error(String(error));
802
- }
803
- if (!Array.isArray(parsed)) {
804
- console.error(`[MotivationParsers] Expected a JSON array for ${motivation} detection, got ${typeof parsed}:`, response);
805
- throw new Error(`Expected a JSON array for ${motivation} detection, got ${typeof parsed}`);
806
- }
807
- return parsed;
808
- }
794
+ var COMMENT_ELEMENT_SCHEMA = {
795
+ type: "object",
796
+ properties: {
797
+ exact: { type: "string" },
798
+ prefix: { type: "string" },
799
+ suffix: { type: "string" },
800
+ comment: { type: "string" }
801
+ },
802
+ required: ["exact", "comment"],
803
+ additionalProperties: false
804
+ };
805
+ var HIGHLIGHT_ELEMENT_SCHEMA = {
806
+ type: "object",
807
+ properties: {
808
+ exact: { type: "string" },
809
+ prefix: { type: "string" },
810
+ suffix: { type: "string" }
811
+ },
812
+ required: ["exact"],
813
+ additionalProperties: false
814
+ };
815
+ var ASSESSMENT_ELEMENT_SCHEMA = {
816
+ type: "object",
817
+ properties: {
818
+ exact: { type: "string" },
819
+ prefix: { type: "string" },
820
+ suffix: { type: "string" },
821
+ assessment: { type: "string" }
822
+ },
823
+ required: ["exact", "assessment"],
824
+ additionalProperties: false
825
+ };
826
+ var TAG_ELEMENT_SCHEMA = {
827
+ type: "object",
828
+ properties: {
829
+ exact: { type: "string" },
830
+ prefix: { type: "string" },
831
+ suffix: { type: "string" }
832
+ },
833
+ required: ["exact"],
834
+ additionalProperties: false
835
+ };
809
836
  var MotivationParsers = class {
810
837
  /**
811
- * Parse and validate AI response for comment detection
838
+ * Validate and reconcile structured comment elements.
812
839
  *
813
- * @param response - Raw AI response text (a JSON array)
840
+ * @param parsed - Already-parsed elements from the structured surface
814
841
  * @param content - Original content to validate offsets against
815
842
  * @returns Array of validated comment matches
816
- * @throws if the response is not a parseable JSON array
817
843
  */
818
- static parseComments(response, content) {
819
- const parsed = parseJsonArray(response, "comment");
844
+ static parseComments(parsed, content) {
820
845
  const valid = parsed.filter(
821
846
  (c) => isObject(c) && isString(c.exact) && isString(c.comment) && c.comment.trim().length > 0
822
847
  );
@@ -845,15 +870,13 @@ var MotivationParsers = class {
845
870
  return validatedComments;
846
871
  }
847
872
  /**
848
- * Parse and validate AI response for highlight detection
873
+ * Validate and reconcile structured highlight elements.
849
874
  *
850
- * @param response - Raw AI response text (a JSON array)
875
+ * @param parsed - Already-parsed elements from the structured surface
851
876
  * @param content - Original content to validate offsets against
852
877
  * @returns Array of validated highlight matches
853
- * @throws if the response is not a parseable JSON array
854
878
  */
855
- static parseHighlights(response, content) {
856
- const parsed = parseJsonArray(response, "highlight");
879
+ static parseHighlights(parsed, content) {
857
880
  const highlights = parsed.filter(
858
881
  (h) => isObject(h) && isString(h.exact)
859
882
  );
@@ -880,15 +903,13 @@ var MotivationParsers = class {
880
903
  return validatedHighlights;
881
904
  }
882
905
  /**
883
- * Parse and validate AI response for assessment detection
906
+ * Validate and reconcile structured assessment elements.
884
907
  *
885
- * @param response - Raw AI response text (a JSON array)
908
+ * @param parsed - Already-parsed elements from the structured surface
886
909
  * @param content - Original content to validate offsets against
887
910
  * @returns Array of validated assessment matches
888
- * @throws if the response is not a parseable JSON array
889
911
  */
890
- static parseAssessments(response, content) {
891
- const parsed = parseJsonArray(response, "assessment");
912
+ static parseAssessments(parsed, content) {
892
913
  const assessments = parsed.filter(
893
914
  (a) => isObject(a) && isString(a.exact) && isString(a.assessment)
894
915
  );
@@ -916,14 +937,13 @@ var MotivationParsers = class {
916
937
  return validatedAssessments;
917
938
  }
918
939
  /**
919
- * Parse the LLM's tag response into raw, pre-reconciliation tag inputs.
940
+ * Validate structured tag elements into raw, pre-reconciliation tag inputs.
920
941
  * Reconciliation happens in `validateTagOffsets`, which adds `start`/`end`
921
942
  * by anchoring `exact` against the source content.
922
943
  *
923
- * @throws if the response is not a parseable JSON array
944
+ * @param parsed - Already-parsed elements from the structured surface
924
945
  */
925
- static parseTags(response) {
926
- const parsed = parseJsonArray(response, "tag");
946
+ static parseTags(parsed) {
927
947
  const valid = parsed.filter(
928
948
  (t) => isObject(t) && isString(t.exact) && t.exact.trim().length > 0
929
949
  );
@@ -970,22 +990,22 @@ function assertNotTruncated(response, motivation, chunk, totalChunks, outputBudg
970
990
  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.`);
971
991
  }
972
992
  }
973
- async function detectInChunks(client, content, buildPrompt, temperature, motivation, parse, onChunk) {
993
+ async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onChunk) {
974
994
  const limits = await client.limits();
975
995
  const scaffoldTokens = estimateTokens(buildPrompt(""));
976
996
  const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
977
997
  const chunks = chunkText(content, chunking);
978
998
  const collected = [];
979
999
  for (let i = 0; i < chunks.length; i++) {
980
- const response = await boundedGenerateWithMetadata(
1000
+ const response = await boundedGenerateStructured(
981
1001
  client,
982
1002
  buildPrompt(chunks[i]),
983
1003
  outputBudget,
984
1004
  temperature,
985
- { format: "json" }
1005
+ elementSchema
986
1006
  );
987
1007
  assertNotTruncated(response, motivation, i + 1, chunks.length, outputBudget);
988
- collected.push(...parse(response.text));
1008
+ collected.push(...parse(response.items));
989
1009
  if (i < chunks.length - 1) {
990
1010
  onChunk?.(i + 1, chunks.length);
991
1011
  }
@@ -1008,7 +1028,8 @@ var AnnotationDetection = class {
1008
1028
  (chunk) => MotivationPrompts.buildCommentPrompt(chunk, instructions, tone, density, language, sourceLanguage),
1009
1029
  0.4,
1010
1030
  "comment",
1011
- (text) => MotivationParsers.parseComments(text, content),
1031
+ COMMENT_ELEMENT_SCHEMA,
1032
+ (items) => MotivationParsers.parseComments(items, content),
1012
1033
  onChunk
1013
1034
  );
1014
1035
  }
@@ -1026,7 +1047,8 @@ var AnnotationDetection = class {
1026
1047
  (chunk) => MotivationPrompts.buildHighlightPrompt(chunk, instructions, density, sourceLanguage),
1027
1048
  0.3,
1028
1049
  "highlight",
1029
- (text) => MotivationParsers.parseHighlights(text, content),
1050
+ HIGHLIGHT_ELEMENT_SCHEMA,
1051
+ (items) => MotivationParsers.parseHighlights(items, content),
1030
1052
  onChunk
1031
1053
  );
1032
1054
  }
@@ -1044,7 +1066,8 @@ var AnnotationDetection = class {
1044
1066
  (chunk) => MotivationPrompts.buildAssessmentPrompt(chunk, instructions, tone, density, language, sourceLanguage),
1045
1067
  0.3,
1046
1068
  "assessment",
1047
- (text) => MotivationParsers.parseAssessments(text, content),
1069
+ ASSESSMENT_ELEMENT_SCHEMA,
1070
+ (items) => MotivationParsers.parseAssessments(items, content),
1048
1071
  onChunk
1049
1072
  );
1050
1073
  }
@@ -1080,12 +1103,24 @@ var AnnotationDetection = class {
1080
1103
  ),
1081
1104
  0.2,
1082
1105
  "tag",
1083
- (text) => MotivationParsers.parseTags(text),
1106
+ TAG_ELEMENT_SCHEMA,
1107
+ (items) => MotivationParsers.parseTags(items),
1084
1108
  onChunk
1085
1109
  );
1086
1110
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
1087
1111
  }
1088
1112
  };
1113
+ var ENTITY_ELEMENT_SCHEMA = {
1114
+ type: "object",
1115
+ properties: {
1116
+ exact: { type: "string" },
1117
+ entityType: { type: "string" },
1118
+ prefix: { type: "string" },
1119
+ suffix: { type: "string" }
1120
+ },
1121
+ required: ["exact", "entityType"],
1122
+ additionalProperties: false
1123
+ };
1089
1124
  async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onChunk) {
1090
1125
  const entityTypesDescription = entityTypes.map((et) => {
1091
1126
  if (typeof et === "string") {
@@ -1147,50 +1182,25 @@ Example output:
1147
1182
  });
1148
1183
  const collected = [];
1149
1184
  for (let i = 0; i < chunks.length; i++) {
1150
- const response = await boundedGenerateWithMetadata(
1185
+ const response = await boundedGenerateStructured(
1151
1186
  client,
1152
1187
  buildPrompt(chunks[i]),
1153
1188
  outputBudget,
1154
1189
  0.3,
1155
1190
  // Lower temperature for more consistent extraction
1156
- // Force grammar-constrained JSON output. Without this, Ollama models
1157
- // periodically emit malformed JSON (truncated brackets, mid-token
1158
- // breaks at higher token counts) which silently parse-fails into
1159
- // [] downstream. The prompt's schema (which keys, what types) still
1160
- // governs *what* the JSON contains; `format: 'json'` governs that
1161
- // it's syntactically valid.
1162
- { format: "json" }
1191
+ ENTITY_ELEMENT_SCHEMA
1163
1192
  );
1164
1193
  logger.debug("Got entity extraction response", {
1165
1194
  chunk: i + 1,
1166
1195
  chunks: chunks.length,
1167
- responseLength: response.text.length
1196
+ items: response.items.length
1168
1197
  });
1169
1198
  if (response.stopReason === "max_tokens") {
1170
1199
  const errorMsg = `Entity extraction response truncated (max_tokens) on chunk ${i + 1}/${chunks.length} despite the derived output budget of ${outputBudget} tokens \u2014 failing the job rather than dropping annotations.`;
1171
- logger.error(errorMsg, { responseLength: response.text.length });
1200
+ logger.error(errorMsg, { items: response.items.length });
1172
1201
  throw new Error(errorMsg);
1173
1202
  }
1174
- let entities;
1175
- try {
1176
- entities = JSON.parse(response.text.trim());
1177
- } catch (error) {
1178
- logger.error("Failed to parse entity extraction response", {
1179
- error: error instanceof Error ? error.message : String(error),
1180
- response: response.text.slice(0, 500)
1181
- });
1182
- throw new Error("Failed to parse entity extraction response", {
1183
- cause: error instanceof Error ? error : new Error(String(error))
1184
- });
1185
- }
1186
- if (!isArray(entities)) {
1187
- logger.error("Failed to parse entity extraction response: expected a JSON array", {
1188
- response: response.text.slice(0, 500)
1189
- });
1190
- throw new Error("Failed to parse entity extraction response: expected a JSON array");
1191
- }
1192
- logger.debug("Parsed entities from AI response", { chunk: i + 1, count: entities.length });
1193
- for (const e of entities) {
1203
+ for (const e of response.items) {
1194
1204
  if (isObject(e) && isString(e.exact) && isString(e.entityType)) {
1195
1205
  collected.push({
1196
1206
  exact: e.exact,