@semiont/jobs 0.5.8 → 0.5.10
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 +26 -6
- package/dist/index.js +238 -231
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +291 -278
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
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, deriveViews, 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
|
|
714
|
-
let
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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.
|
|
952
|
-
|
|
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.
|
|
964
|
-
|
|
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.
|
|
976
|
-
|
|
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.
|
|
1006
|
-
|
|
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,41 +1028,47 @@ 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
|
-
|
|
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))
|
|
1046
|
+
});
|
|
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)
|
|
1102
1051
|
});
|
|
1103
|
-
|
|
1052
|
+
throw new Error("Failed to parse entity extraction response: expected a JSON array");
|
|
1104
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;
|
|
1108
1070
|
}
|
|
1109
|
-
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage) {
|
|
1071
|
+
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown") {
|
|
1110
1072
|
logger.debug("Generating resource from topic", {
|
|
1111
1073
|
topicPreview: topic.substring(0, 100),
|
|
1112
1074
|
entityTypes,
|
|
@@ -1126,61 +1088,95 @@ IMPORTANT: Write the entire resource in ${getLanguageName(locale)}.` : "";
|
|
|
1126
1088
|
|
|
1127
1089
|
The source resource and embedded context are in ${getLanguageName(sourceLanguage)}.` : "";
|
|
1128
1090
|
let annotationSection = "";
|
|
1091
|
+
let contextSection = "";
|
|
1092
|
+
let resourceSection = "";
|
|
1093
|
+
let graphSection = "";
|
|
1129
1094
|
if (context) {
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1095
|
+
const { focus } = context;
|
|
1096
|
+
const mainResourceId = focus.kind === "annotation" ? focus.sourceResource["@id"] : focus.resource["@id"];
|
|
1097
|
+
const focalAnnotationId = focus.kind === "annotation" ? focus.annotation.id : void 0;
|
|
1098
|
+
if (focus.kind === "annotation") {
|
|
1099
|
+
const parts = [];
|
|
1100
|
+
parts.push(`- Annotation motivation: ${focus.annotation.motivation}`);
|
|
1101
|
+
parts.push(`- Source resource: ${focus.sourceResource.name}`);
|
|
1102
|
+
const { motivation, body } = focus.annotation;
|
|
1103
|
+
if (motivation === "commenting" || motivation === "assessing") {
|
|
1104
|
+
const bodyItem = Array.isArray(body) ? body[0] : body;
|
|
1105
|
+
if (bodyItem && "value" in bodyItem && bodyItem.value) {
|
|
1106
|
+
const label = motivation === "commenting" ? "Comment" : "Assessment";
|
|
1107
|
+
parts.push(`- ${label}: ${bodyItem.value}`);
|
|
1108
|
+
}
|
|
1139
1109
|
}
|
|
1140
|
-
|
|
1141
|
-
annotationSection = `
|
|
1110
|
+
annotationSection = `
|
|
1142
1111
|
|
|
1143
1112
|
Annotation context:
|
|
1144
1113
|
${parts.join("\n")}`;
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const { before, selected, after } = context.sourceContext;
|
|
1149
|
-
contextSection = `
|
|
1114
|
+
if (focus.selected) {
|
|
1115
|
+
const { before, text, after } = focus.selected;
|
|
1116
|
+
contextSection = `
|
|
1150
1117
|
|
|
1151
1118
|
Source document context:
|
|
1152
1119
|
---
|
|
1153
1120
|
${before ? `...${before}` : ""}
|
|
1154
|
-
**[${
|
|
1121
|
+
**[${text}]**
|
|
1155
1122
|
${after ? `${after}...` : ""}
|
|
1156
1123
|
---
|
|
1157
1124
|
`;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
const RESOURCE_CONTENT_CAP = 4e3;
|
|
1128
|
+
const parts = [`- Resource: ${focus.resource.name}`];
|
|
1129
|
+
if (focus.summary) parts.push(`- Summary: ${focus.summary}`);
|
|
1130
|
+
if (focus.suggestedReferences && focus.suggestedReferences.length > 0) {
|
|
1131
|
+
parts.push(`- Suggested references: ${focus.suggestedReferences.join(", ")}`);
|
|
1132
|
+
}
|
|
1133
|
+
resourceSection = `
|
|
1134
|
+
|
|
1135
|
+
Resource context:
|
|
1136
|
+
${parts.join("\n")}`;
|
|
1137
|
+
if (focus.content?.main) {
|
|
1138
|
+
resourceSection += `
|
|
1139
|
+
|
|
1140
|
+
Resource content:
|
|
1141
|
+
---
|
|
1142
|
+
${focus.content.main.slice(0, RESOURCE_CONTENT_CAP)}
|
|
1143
|
+
---`;
|
|
1144
|
+
}
|
|
1145
|
+
const related = Object.entries(focus.content?.related ?? {});
|
|
1146
|
+
if (related.length > 0) {
|
|
1147
|
+
const blocks = related.map(([id, text]) => `[${id}]
|
|
1148
|
+
${text.slice(0, RESOURCE_CONTENT_CAP)}`).join("\n\n");
|
|
1149
|
+
resourceSection += `
|
|
1150
|
+
|
|
1151
|
+
Related resource content:
|
|
1152
|
+
---
|
|
1153
|
+
${blocks}
|
|
1154
|
+
---`;
|
|
1155
|
+
}
|
|
1178
1156
|
}
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1157
|
+
if (mainResourceId) {
|
|
1158
|
+
const views = deriveViews(context.graph, mainResourceId, focalAnnotationId);
|
|
1159
|
+
const parts = [];
|
|
1160
|
+
if (views.connections.length > 0) {
|
|
1161
|
+
const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""}`).join(", ");
|
|
1162
|
+
parts.push(`- Connected resources: ${connList}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (views.citedByCount > 0) {
|
|
1165
|
+
const citedNames = views.citedBy.map((c) => c.resourceName).join(", ");
|
|
1166
|
+
parts.push(`- This resource is cited by ${views.citedByCount} other resource${views.citedByCount > 1 ? "s" : ""}${citedNames ? `: ${citedNames}` : ""}`);
|
|
1167
|
+
}
|
|
1168
|
+
if (views.siblingEntityTypes.length > 0) {
|
|
1169
|
+
parts.push(`- Related entity types in this document: ${views.siblingEntityTypes.join(", ")}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (context.inferredRelationshipSummary) {
|
|
1172
|
+
parts.push(`- Relationship summary: ${context.inferredRelationshipSummary}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (parts.length > 0) {
|
|
1175
|
+
graphSection = `
|
|
1181
1176
|
|
|
1182
1177
|
Knowledge graph context:
|
|
1183
1178
|
${parts.join("\n")}`;
|
|
1179
|
+
}
|
|
1184
1180
|
}
|
|
1185
1181
|
}
|
|
1186
1182
|
let semanticContextSection = "";
|
|
@@ -1192,17 +1188,20 @@ ${parts.join("\n")}`;
|
|
|
1192
1188
|
Related passages from the knowledge base:
|
|
1193
1189
|
${lines.join("\n")}`;
|
|
1194
1190
|
}
|
|
1195
|
-
const
|
|
1191
|
+
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";
|
|
1193
|
+
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
|
|
1196
|
+
- Write the response as markdown`;
|
|
1196
1197
|
const prompt = `Generate a concise, informative resource about "${topic}".
|
|
1197
1198
|
${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}
|
|
1198
|
-
${userPrompt ? `Additional context: ${userPrompt}` : ""}${annotationSection}${contextSection}${
|
|
1199
|
+
${userPrompt ? `Additional context: ${userPrompt}` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
|
|
1199
1200
|
|
|
1200
1201
|
Requirements:
|
|
1201
|
-
- Start with a clear heading (# Title)
|
|
1202
1202
|
- Aim for approximately ${finalMaxTokens} tokens of content, ${structureGuidance}
|
|
1203
1203
|
- Be factual and informative
|
|
1204
|
-
|
|
1205
|
-
- Write the response as markdown`;
|
|
1204
|
+
${formatRequirements}`;
|
|
1206
1205
|
const parseResponse = (response2) => {
|
|
1207
1206
|
let content = response2.trim();
|
|
1208
1207
|
if (content.startsWith("```markdown") || content.startsWith("```md")) {
|
|
@@ -1521,6 +1520,13 @@ async function processTagJob(content, inferenceClient, params, userId, generator
|
|
|
1521
1520
|
};
|
|
1522
1521
|
}
|
|
1523
1522
|
async function processGenerationJob(inferenceClient, params, onProgress, logger) {
|
|
1523
|
+
const GENERATABLE_MEDIA_TYPES = ["text/markdown", "text/plain"];
|
|
1524
|
+
const outputMediaType = params.outputMediaType ?? "text/markdown";
|
|
1525
|
+
if (!GENERATABLE_MEDIA_TYPES.includes(outputMediaType)) {
|
|
1526
|
+
throw new Error(
|
|
1527
|
+
`Unsupported outputMediaType for generation: ${outputMediaType}. Generation produces ${GENERATABLE_MEDIA_TYPES.join(" or ")}.`
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1524
1530
|
onProgress(20, "Fetching context...", "fetching");
|
|
1525
1531
|
const title = params.title ?? "Untitled";
|
|
1526
1532
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
@@ -1535,13 +1541,14 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1535
1541
|
params.context,
|
|
1536
1542
|
params.temperature,
|
|
1537
1543
|
params.maxTokens,
|
|
1538
|
-
params.sourceLanguage
|
|
1544
|
+
params.sourceLanguage,
|
|
1545
|
+
outputMediaType
|
|
1539
1546
|
);
|
|
1540
1547
|
onProgress(85, "Creating resource...", "creating");
|
|
1541
1548
|
return {
|
|
1542
1549
|
content: generated.content,
|
|
1543
1550
|
title: generated.title ?? title,
|
|
1544
|
-
format:
|
|
1551
|
+
format: outputMediaType,
|
|
1545
1552
|
result: {
|
|
1546
1553
|
resourceId: "",
|
|
1547
1554
|
resourceName: generated.title ?? title
|