@semiont/jobs 0.5.8 → 0.5.9

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
@@ -503,6 +503,16 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
503
503
  result: GenerationResult;
504
504
  }>;
505
505
 
506
+ /**
507
+ * Response parsers for annotation detection motivations
508
+ *
509
+ * Provides static methods to parse and validate AI responses for each motivation type.
510
+ * Includes offset validation and correction logic.
511
+ * Extracted from worker implementations to centralize parsing logic.
512
+ *
513
+ * NOTE: These are static utility methods without logger access.
514
+ * Console statements kept for debugging - consider adding logger parameter in future.
515
+ */
506
516
  /**
507
517
  * Represents a detected comment with validated position
508
518
  */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { promises } from 'fs';
2
2
  import * as path from 'path';
3
- import { jobId, reconcileSelector, getLocaleEnglishName, didToAgent } from '@semiont/core';
3
+ import { jobId, reconcileSelector, isObject, isString, getLocaleEnglishName, didToAgent, isArray } from '@semiont/core';
4
4
  import { generateAnnotationId } from '@semiont/event-sourcing';
5
5
 
6
6
  // src/fs-job-queue.ts
@@ -710,197 +710,144 @@ Example format:
710
710
  return prompt;
711
711
  }
712
712
  };
713
- function extractObjectsFromArray(response) {
714
- let cleaned = response.trim();
715
- if (cleaned.startsWith("```")) {
716
- cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
717
- }
713
+ function parseJsonArray(response, motivation) {
714
+ let parsed;
718
715
  try {
719
- const parsed = JSON.parse(cleaned);
720
- return Array.isArray(parsed) ? parsed : [];
721
- } catch {
716
+ parsed = JSON.parse(response.trim());
717
+ } catch (error) {
718
+ console.error(`[MotivationParsers] Failed to parse AI ${motivation} response:`, error);
719
+ console.error("Raw response:", response);
720
+ throw error instanceof Error ? error : new Error(String(error));
722
721
  }
723
- const start = cleaned.indexOf("[");
724
- if (start === -1) return [];
725
- const endBracket = cleaned.lastIndexOf("]");
726
- const end = endBracket > start ? endBracket : cleaned.length;
727
- const inner = cleaned.slice(start + 1, end);
728
- const objects = [];
729
- let depth = 0;
730
- let objStart = -1;
731
- let inString = false;
732
- let escape = false;
733
- for (let i = 0; i < inner.length; i++) {
734
- const ch = inner[i];
735
- if (escape) {
736
- escape = false;
737
- continue;
738
- }
739
- if (ch === "\\") {
740
- escape = true;
741
- continue;
742
- }
743
- if (ch === '"') {
744
- inString = !inString;
745
- continue;
746
- }
747
- if (inString) continue;
748
- if (ch === "{") {
749
- if (depth === 0) objStart = i;
750
- depth++;
751
- } else if (ch === "}") {
752
- depth--;
753
- if (depth === 0 && objStart !== -1) {
754
- try {
755
- objects.push(JSON.parse(inner.slice(objStart, i + 1)));
756
- } catch {
757
- }
758
- objStart = -1;
759
- }
760
- }
722
+ if (!Array.isArray(parsed)) {
723
+ console.error(`[MotivationParsers] Expected a JSON array for ${motivation} detection, got ${typeof parsed}:`, response);
724
+ throw new Error(`Expected a JSON array for ${motivation} detection, got ${typeof parsed}`);
761
725
  }
762
- return objects;
726
+ return parsed;
763
727
  }
764
728
  var MotivationParsers = class {
765
729
  /**
766
730
  * Parse and validate AI response for comment detection
767
731
  *
768
- * @param response - Raw AI response string (may include markdown code fences)
732
+ * @param response - Raw AI response text (a JSON array)
769
733
  * @param content - Original content to validate offsets against
770
734
  * @returns Array of validated comment matches
735
+ * @throws if the response is not a parseable JSON array
771
736
  */
772
737
  static parseComments(response, content) {
773
- try {
774
- const parsed = extractObjectsFromArray(response);
775
- const valid = parsed.filter(
776
- (c) => !!c && typeof c === "object" && typeof c.exact === "string" && typeof c.comment === "string" && c.comment.trim().length > 0
777
- );
778
- console.log(`[MotivationParsers] Parsed ${valid.length} valid comments from ${parsed.length} total`);
779
- const validatedComments = [];
780
- for (const comment of valid) {
781
- const reconciled = reconcileSelector(content, {
782
- exact: comment.exact,
783
- ...typeof comment.prefix === "string" ? { prefix: comment.prefix } : {},
784
- ...typeof comment.suffix === "string" ? { suffix: comment.suffix } : {}
785
- });
786
- if (!reconciled) {
787
- console.warn(`[MotivationParsers] Dropped hallucinated comment "${comment.exact}"`);
788
- continue;
789
- }
790
- logAnchorMethod("comment", comment.exact, reconciled.anchorMethod);
791
- validatedComments.push({
792
- comment: comment.comment,
793
- exact: reconciled.exact,
794
- start: reconciled.start,
795
- end: reconciled.end,
796
- ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
797
- ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
798
- });
738
+ const parsed = parseJsonArray(response, "comment");
739
+ const valid = parsed.filter(
740
+ (c) => isObject(c) && isString(c.exact) && isString(c.comment) && c.comment.trim().length > 0
741
+ );
742
+ console.log(`[MotivationParsers] Parsed ${valid.length} valid comments from ${parsed.length} total`);
743
+ const validatedComments = [];
744
+ for (const comment of valid) {
745
+ const reconciled = reconcileSelector(content, {
746
+ exact: comment.exact,
747
+ ...typeof comment.prefix === "string" ? { prefix: comment.prefix } : {},
748
+ ...typeof comment.suffix === "string" ? { suffix: comment.suffix } : {}
749
+ });
750
+ if (!reconciled) {
751
+ console.warn(`[MotivationParsers] Dropped hallucinated comment "${comment.exact}"`);
752
+ continue;
799
753
  }
800
- return validatedComments;
801
- } catch (error) {
802
- console.error("[MotivationParsers] Failed to parse AI comment response:", error);
803
- return [];
754
+ logAnchorMethod("comment", comment.exact, reconciled.anchorMethod);
755
+ validatedComments.push({
756
+ comment: comment.comment,
757
+ exact: reconciled.exact,
758
+ start: reconciled.start,
759
+ end: reconciled.end,
760
+ ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
761
+ ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
762
+ });
804
763
  }
764
+ return validatedComments;
805
765
  }
806
766
  /**
807
767
  * Parse and validate AI response for highlight detection
808
768
  *
809
- * @param response - Raw AI response string (may include markdown code fences)
769
+ * @param response - Raw AI response text (a JSON array)
810
770
  * @param content - Original content to validate offsets against
811
771
  * @returns Array of validated highlight matches
772
+ * @throws if the response is not a parseable JSON array
812
773
  */
813
774
  static parseHighlights(response, content) {
814
- try {
815
- const parsed = extractObjectsFromArray(response);
816
- const highlights = parsed.filter(
817
- (h) => !!h && typeof h === "object" && typeof h.exact === "string"
818
- );
819
- const validatedHighlights = [];
820
- for (const highlight of highlights) {
821
- const reconciled = reconcileSelector(content, {
822
- exact: highlight.exact,
823
- ...typeof highlight.prefix === "string" ? { prefix: highlight.prefix } : {},
824
- ...typeof highlight.suffix === "string" ? { suffix: highlight.suffix } : {}
825
- });
826
- if (!reconciled) {
827
- console.warn(`[MotivationParsers] Dropped hallucinated highlight "${highlight.exact}"`);
828
- continue;
829
- }
830
- logAnchorMethod("highlight", highlight.exact, reconciled.anchorMethod);
831
- validatedHighlights.push({
832
- exact: reconciled.exact,
833
- start: reconciled.start,
834
- end: reconciled.end,
835
- ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
836
- ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
837
- });
775
+ const parsed = parseJsonArray(response, "highlight");
776
+ const highlights = parsed.filter(
777
+ (h) => isObject(h) && isString(h.exact)
778
+ );
779
+ const validatedHighlights = [];
780
+ for (const highlight of highlights) {
781
+ const reconciled = reconcileSelector(content, {
782
+ exact: highlight.exact,
783
+ ...typeof highlight.prefix === "string" ? { prefix: highlight.prefix } : {},
784
+ ...typeof highlight.suffix === "string" ? { suffix: highlight.suffix } : {}
785
+ });
786
+ if (!reconciled) {
787
+ console.warn(`[MotivationParsers] Dropped hallucinated highlight "${highlight.exact}"`);
788
+ continue;
838
789
  }
839
- return validatedHighlights;
840
- } catch (error) {
841
- console.error("[MotivationParsers] Failed to parse AI highlight response:", error);
842
- console.error("Raw response:", response);
843
- return [];
790
+ logAnchorMethod("highlight", highlight.exact, reconciled.anchorMethod);
791
+ validatedHighlights.push({
792
+ exact: reconciled.exact,
793
+ start: reconciled.start,
794
+ end: reconciled.end,
795
+ ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
796
+ ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
797
+ });
844
798
  }
799
+ return validatedHighlights;
845
800
  }
846
801
  /**
847
802
  * Parse and validate AI response for assessment detection
848
803
  *
849
- * @param response - Raw AI response string (may include markdown code fences)
804
+ * @param response - Raw AI response text (a JSON array)
850
805
  * @param content - Original content to validate offsets against
851
806
  * @returns Array of validated assessment matches
807
+ * @throws if the response is not a parseable JSON array
852
808
  */
853
809
  static parseAssessments(response, content) {
854
- try {
855
- const parsed = extractObjectsFromArray(response);
856
- const assessments = parsed.filter(
857
- (a) => !!a && typeof a === "object" && typeof a.exact === "string" && typeof a.assessment === "string"
858
- );
859
- const validatedAssessments = [];
860
- for (const assessment of assessments) {
861
- const reconciled = reconcileSelector(content, {
862
- exact: assessment.exact,
863
- ...typeof assessment.prefix === "string" ? { prefix: assessment.prefix } : {},
864
- ...typeof assessment.suffix === "string" ? { suffix: assessment.suffix } : {}
865
- });
866
- if (!reconciled) {
867
- console.warn(`[MotivationParsers] Dropped hallucinated assessment "${assessment.exact}"`);
868
- continue;
869
- }
870
- logAnchorMethod("assessment", assessment.exact, reconciled.anchorMethod);
871
- validatedAssessments.push({
872
- assessment: assessment.assessment,
873
- exact: reconciled.exact,
874
- start: reconciled.start,
875
- end: reconciled.end,
876
- ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
877
- ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
878
- });
810
+ const parsed = parseJsonArray(response, "assessment");
811
+ const assessments = parsed.filter(
812
+ (a) => isObject(a) && isString(a.exact) && isString(a.assessment)
813
+ );
814
+ const validatedAssessments = [];
815
+ for (const assessment of assessments) {
816
+ const reconciled = reconcileSelector(content, {
817
+ exact: assessment.exact,
818
+ ...typeof assessment.prefix === "string" ? { prefix: assessment.prefix } : {},
819
+ ...typeof assessment.suffix === "string" ? { suffix: assessment.suffix } : {}
820
+ });
821
+ if (!reconciled) {
822
+ console.warn(`[MotivationParsers] Dropped hallucinated assessment "${assessment.exact}"`);
823
+ continue;
879
824
  }
880
- return validatedAssessments;
881
- } catch (error) {
882
- console.error("[MotivationParsers] Failed to parse AI assessment response:", error);
883
- console.error("Raw response:", response);
884
- return [];
825
+ logAnchorMethod("assessment", assessment.exact, reconciled.anchorMethod);
826
+ validatedAssessments.push({
827
+ assessment: assessment.assessment,
828
+ exact: reconciled.exact,
829
+ start: reconciled.start,
830
+ end: reconciled.end,
831
+ ...reconciled.prefix !== void 0 ? { prefix: reconciled.prefix } : {},
832
+ ...reconciled.suffix !== void 0 ? { suffix: reconciled.suffix } : {}
833
+ });
885
834
  }
835
+ return validatedAssessments;
886
836
  }
887
837
  /**
888
838
  * Parse the LLM's tag response into raw, pre-reconciliation tag inputs.
889
839
  * Reconciliation happens in `validateTagOffsets`, which adds `start`/`end`
890
840
  * by anchoring `exact` against the source content.
841
+ *
842
+ * @throws if the response is not a parseable JSON array
891
843
  */
892
844
  static parseTags(response) {
893
- try {
894
- const parsed = extractObjectsFromArray(response);
895
- const valid = parsed.filter(
896
- (t) => !!t && typeof t === "object" && typeof t.exact === "string" && t.exact.trim().length > 0
897
- );
898
- console.log(`[MotivationParsers] Parsed ${valid.length} valid tags from ${parsed.length} total`);
899
- return valid;
900
- } catch (error) {
901
- console.error("[MotivationParsers] Failed to parse AI tag response:", error);
902
- return [];
903
- }
845
+ const parsed = parseJsonArray(response, "tag");
846
+ const valid = parsed.filter(
847
+ (t) => isObject(t) && isString(t.exact) && t.exact.trim().length > 0
848
+ );
849
+ console.log(`[MotivationParsers] Parsed ${valid.length} valid tags from ${parsed.length} total`);
850
+ return valid;
904
851
  }
905
852
  /**
906
853
  * Anchor raw tag inputs against source content and add category.
@@ -937,6 +884,11 @@ function logAnchorMethod(motivation, exact, anchorMethod) {
937
884
  }
938
885
 
939
886
  // src/workers/annotation-detection.ts
887
+ function assertNotTruncated(response, motivation) {
888
+ if (response.stopReason === "max_tokens") {
889
+ throw new Error(`${motivation} detection response truncated (max_tokens) \u2014 increase max_tokens or reduce resource size; failing the job rather than under-reporting annotations.`);
890
+ }
891
+ }
940
892
  var AnnotationDetection = class {
941
893
  /**
942
894
  * Detect comments in content.
@@ -948,8 +900,9 @@ var AnnotationDetection = class {
948
900
  */
949
901
  static async detectComments(content, client, instructions, tone, density, language, sourceLanguage) {
950
902
  const prompt = MotivationPrompts.buildCommentPrompt(content, instructions, tone, density, language, sourceLanguage);
951
- const response = await client.generateText(prompt, 3e3, 0.4, { format: "json" });
952
- return MotivationParsers.parseComments(response, content);
903
+ const response = await client.generateTextWithMetadata(prompt, 3e3, 0.4, { format: "json" });
904
+ assertNotTruncated(response, "comment");
905
+ return MotivationParsers.parseComments(response.text, content);
953
906
  }
954
907
  /**
955
908
  * Detect highlights in content.
@@ -960,8 +913,9 @@ var AnnotationDetection = class {
960
913
  */
961
914
  static async detectHighlights(content, client, instructions, density, sourceLanguage) {
962
915
  const prompt = MotivationPrompts.buildHighlightPrompt(content, instructions, density, sourceLanguage);
963
- const response = await client.generateText(prompt, 2e3, 0.3, { format: "json" });
964
- return MotivationParsers.parseHighlights(response, content);
916
+ const response = await client.generateTextWithMetadata(prompt, 2e3, 0.3, { format: "json" });
917
+ assertNotTruncated(response, "highlight");
918
+ return MotivationParsers.parseHighlights(response.text, content);
965
919
  }
966
920
  /**
967
921
  * Detect assessments in content.
@@ -972,8 +926,9 @@ var AnnotationDetection = class {
972
926
  */
973
927
  static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage) {
974
928
  const prompt = MotivationPrompts.buildAssessmentPrompt(content, instructions, tone, density, language, sourceLanguage);
975
- const response = await client.generateText(prompt, 3e3, 0.3, { format: "json" });
976
- return MotivationParsers.parseAssessments(response, content);
929
+ const response = await client.generateTextWithMetadata(prompt, 3e3, 0.3, { format: "json" });
930
+ assertNotTruncated(response, "assessment");
931
+ return MotivationParsers.parseAssessments(response.text, content);
977
932
  }
978
933
  /**
979
934
  * Detect tags in content for a specific category.
@@ -1002,8 +957,9 @@ var AnnotationDetection = class {
1002
957
  categoryInfo.examples,
1003
958
  sourceLanguage
1004
959
  );
1005
- const response = await client.generateText(prompt, 4e3, 0.2, { format: "json" });
1006
- const parsedTags = MotivationParsers.parseTags(response);
960
+ const response = await client.generateTextWithMetadata(prompt, 4e3, 0.2, { format: "json" });
961
+ assertNotTruncated(response, "tag");
962
+ const parsedTags = MotivationParsers.parseTags(response.text);
1007
963
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
1008
964
  }
1009
965
  };
@@ -1072,36 +1028,42 @@ Example output:
1072
1028
  { format: "json" }
1073
1029
  );
1074
1030
  logger.debug("Got entity extraction response", { responseLength: response.text.length });
1031
+ if (response.stopReason === "max_tokens") {
1032
+ const errorMsg = "Entity extraction response truncated (max_tokens) \u2014 increase max_tokens or reduce resource size; failing the job rather than dropping annotations.";
1033
+ logger.error(errorMsg, { responseLength: response.text.length });
1034
+ throw new Error(errorMsg);
1035
+ }
1036
+ let entities;
1075
1037
  try {
1076
- let jsonStr = response.text.trim();
1077
- if (jsonStr.startsWith("```")) {
1078
- jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
1079
- }
1080
- const entities = JSON.parse(jsonStr);
1081
- logger.debug("Parsed entities from AI response", { count: entities.length });
1082
- if (response.stopReason === "max_tokens") {
1083
- const errorMsg = `AI response truncated: Found ${entities.length} entities but response hit max_tokens limit. Increase max_tokens or reduce resource size.`;
1084
- logger.error(errorMsg);
1085
- throw new Error(errorMsg);
1086
- }
1087
- return entities.filter((e) => {
1088
- const ok = e && typeof e === "object" && typeof e.exact === "string" && typeof e.entityType === "string";
1089
- if (!ok) {
1090
- logger.debug("Dropped malformed LLM entity", { entity: e });
1091
- }
1092
- return ok;
1093
- }).map((entity) => ({
1094
- exact: entity.exact,
1095
- entityType: entity.entityType,
1096
- ...typeof entity.prefix === "string" ? { prefix: entity.prefix } : {},
1097
- ...typeof entity.suffix === "string" ? { suffix: entity.suffix } : {}
1098
- }));
1038
+ entities = JSON.parse(response.text.trim());
1099
1039
  } catch (error) {
1100
1040
  logger.error("Failed to parse entity extraction response", {
1101
- error: error instanceof Error ? error.message : String(error)
1041
+ error: error instanceof Error ? error.message : String(error),
1042
+ response: response.text.slice(0, 500)
1043
+ });
1044
+ throw new Error("Failed to parse entity extraction response", {
1045
+ cause: error instanceof Error ? error : new Error(String(error))
1102
1046
  });
1103
- return [];
1104
1047
  }
1048
+ if (!isArray(entities)) {
1049
+ logger.error("Failed to parse entity extraction response: expected a JSON array", {
1050
+ response: response.text.slice(0, 500)
1051
+ });
1052
+ throw new Error("Failed to parse entity extraction response: expected a JSON array");
1053
+ }
1054
+ logger.debug("Parsed entities from AI response", { count: entities.length });
1055
+ return entities.filter((e) => {
1056
+ const ok = isObject(e) && isString(e.exact) && isString(e.entityType);
1057
+ if (!ok) {
1058
+ logger.debug("Dropped malformed LLM entity", { entity: e });
1059
+ }
1060
+ return ok;
1061
+ }).map((entity) => ({
1062
+ exact: entity.exact,
1063
+ entityType: entity.entityType,
1064
+ ...isString(entity.prefix) ? { prefix: entity.prefix } : {},
1065
+ ...isString(entity.suffix) ? { suffix: entity.suffix } : {}
1066
+ }));
1105
1067
  }
1106
1068
  function getLanguageName(locale) {
1107
1069
  return getLocaleEnglishName(locale) || locale;