@semiont/jobs 0.5.24 → 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 +16 -20
- package/dist/index.js +412 -149
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +577 -240
- package/dist/worker-main.js.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { promises } from 'fs';
|
|
1
|
+
import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { jobId, deriveViews, reconcileSelector, GENERATABLE_MEDIA_TYPES, estimateTokens, chunkText, isObject, isString, getLocaleEnglishName } from '@semiont/core';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
|
|
4
8
|
import '@semiont/event-sourcing';
|
|
5
|
-
import '@semiont/content';
|
|
6
9
|
import '@semiont/observability';
|
|
7
10
|
import '@semiont/sdk';
|
|
8
11
|
import '@semiont/http-transport';
|
|
@@ -446,18 +449,51 @@ async function withTimeout(work, label) {
|
|
|
446
449
|
clearTimeout(timer);
|
|
447
450
|
}
|
|
448
451
|
}
|
|
449
|
-
function boundedGenerate(client, prompt, maxTokens, temperature
|
|
452
|
+
function boundedGenerate(client, prompt, maxTokens, temperature) {
|
|
450
453
|
return withTimeout(
|
|
451
|
-
client.generateText(prompt, maxTokens, temperature
|
|
454
|
+
client.generateText(prompt, maxTokens, temperature),
|
|
452
455
|
`${client.type}:${client.modelId}`
|
|
453
456
|
);
|
|
454
457
|
}
|
|
455
|
-
function
|
|
458
|
+
function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema) {
|
|
456
459
|
return withTimeout(
|
|
457
|
-
client.
|
|
460
|
+
client.generateStructured(prompt, maxTokens, temperature, elementSchema),
|
|
458
461
|
`${client.type}:${client.modelId}`
|
|
459
462
|
);
|
|
460
463
|
}
|
|
464
|
+
|
|
465
|
+
// src/workers/detection/detection-chunking.ts
|
|
466
|
+
var SELECTOR_CONTEXT_CHARS = 64;
|
|
467
|
+
var OVERLAP_CHARS = SELECTOR_CONTEXT_CHARS + // prefix
|
|
468
|
+
SELECTOR_CONTEXT_CHARS + // suffix
|
|
469
|
+
2 * SELECTOR_CONTEXT_CHARS;
|
|
470
|
+
var OVERLAP_TOKENS = Math.ceil(OVERLAP_CHARS / 4);
|
|
471
|
+
function deriveDetectionBudget(limits, scaffoldTokens) {
|
|
472
|
+
const { contextTokens, maxOutputTokens } = limits;
|
|
473
|
+
const available = contextTokens - scaffoldTokens;
|
|
474
|
+
let inputBudget;
|
|
475
|
+
let outputBudget;
|
|
476
|
+
if (maxOutputTokens >= contextTokens) {
|
|
477
|
+
inputBudget = Math.floor(available / 3);
|
|
478
|
+
outputBudget = available - inputBudget;
|
|
479
|
+
} else {
|
|
480
|
+
outputBudget = maxOutputTokens;
|
|
481
|
+
inputBudget = contextTokens - outputBudget - scaffoldTokens;
|
|
482
|
+
if (inputBudget <= 0) {
|
|
483
|
+
inputBudget = Math.floor(available / 3);
|
|
484
|
+
outputBudget = available - inputBudget;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (inputBudget <= OVERLAP_TOKENS) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Inference window too small for detection: context ${contextTokens} tokens minus scaffold ${scaffoldTokens} leaves an input budget of ${inputBudget} (need > ${OVERLAP_TOKENS}). Use a model with a larger context window or reduce the prompt scaffold.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
chunking: { chunkSize: inputBudget, overlap: OVERLAP_TOKENS },
|
|
494
|
+
outputBudget
|
|
495
|
+
};
|
|
496
|
+
}
|
|
461
497
|
function languageName(tag) {
|
|
462
498
|
return getLocaleEnglishName(tag) || tag;
|
|
463
499
|
}
|
|
@@ -477,7 +513,9 @@ var MotivationPrompts = class {
|
|
|
477
513
|
/**
|
|
478
514
|
* Build a prompt for detecting comment-worthy passages
|
|
479
515
|
*
|
|
480
|
-
* @param content - The text content to analyze
|
|
516
|
+
* @param content - The text content to analyze — a chunk sized by the
|
|
517
|
+
* caller from derived provider limits; NEVER re-truncated here (a
|
|
518
|
+
* builder-level clip is silent input loss — see #738)
|
|
481
519
|
* @param instructions - Optional user-provided instructions
|
|
482
520
|
* @param tone - Optional tone guidance (e.g., "academic", "conversational")
|
|
483
521
|
* @param density - Optional target number of comments per 2000 words
|
|
@@ -498,7 +536,7 @@ ${instructions}${toneGuidance}${densityGuidance}${sourceLang}${bodyLang}
|
|
|
498
536
|
|
|
499
537
|
Text to analyze:
|
|
500
538
|
---
|
|
501
|
-
${content
|
|
539
|
+
${content}
|
|
502
540
|
---
|
|
503
541
|
|
|
504
542
|
Return a JSON array of comments. Each comment must have:
|
|
@@ -532,7 +570,7 @@ Guidelines:
|
|
|
532
570
|
|
|
533
571
|
Text to analyze:
|
|
534
572
|
---
|
|
535
|
-
${content
|
|
573
|
+
${content}
|
|
536
574
|
---
|
|
537
575
|
|
|
538
576
|
Return a JSON array of comments. Each comment should have:
|
|
@@ -553,7 +591,9 @@ Example format:
|
|
|
553
591
|
/**
|
|
554
592
|
* Build a prompt for detecting highlight-worthy passages
|
|
555
593
|
*
|
|
556
|
-
* @param content - The text content to analyze
|
|
594
|
+
* @param content - The text content to analyze — a chunk sized by the
|
|
595
|
+
* caller from derived provider limits; NEVER re-truncated here (a
|
|
596
|
+
* builder-level clip is silent input loss — see #738)
|
|
557
597
|
* @param instructions - Optional user-provided instructions
|
|
558
598
|
* @param density - Optional target number of highlights per 2000 words
|
|
559
599
|
* @returns Formatted prompt string
|
|
@@ -571,7 +611,7 @@ ${instructions}${densityGuidance}${sourceLang}
|
|
|
571
611
|
|
|
572
612
|
Text to analyze:
|
|
573
613
|
---
|
|
574
|
-
${content
|
|
614
|
+
${content}
|
|
575
615
|
---
|
|
576
616
|
|
|
577
617
|
Return a JSON array of highlights. Each highlight must have:
|
|
@@ -602,7 +642,7 @@ Guidelines:
|
|
|
602
642
|
|
|
603
643
|
Text to analyze:
|
|
604
644
|
---
|
|
605
|
-
${content
|
|
645
|
+
${content}
|
|
606
646
|
---
|
|
607
647
|
|
|
608
648
|
Return a JSON array of highlights. Each highlight should have:
|
|
@@ -622,7 +662,9 @@ Example format:
|
|
|
622
662
|
/**
|
|
623
663
|
* Build a prompt for detecting assessment-worthy passages
|
|
624
664
|
*
|
|
625
|
-
* @param content - The text content to analyze
|
|
665
|
+
* @param content - The text content to analyze — a chunk sized by the
|
|
666
|
+
* caller from derived provider limits; NEVER re-truncated here (a
|
|
667
|
+
* builder-level clip is silent input loss — see #738)
|
|
626
668
|
* @param instructions - Optional user-provided instructions
|
|
627
669
|
* @param tone - Optional tone guidance (e.g., "critical", "supportive")
|
|
628
670
|
* @param density - Optional target number of assessments per 2000 words
|
|
@@ -643,7 +685,7 @@ ${instructions}${toneGuidance}${densityGuidance}${sourceLang}${bodyLang}
|
|
|
643
685
|
|
|
644
686
|
Text to analyze:
|
|
645
687
|
---
|
|
646
|
-
${content
|
|
688
|
+
${content}
|
|
647
689
|
---
|
|
648
690
|
|
|
649
691
|
Return a JSON array of assessments. Each assessment must have:
|
|
@@ -677,7 +719,7 @@ Guidelines:
|
|
|
677
719
|
|
|
678
720
|
Text to analyze:
|
|
679
721
|
---
|
|
680
|
-
${content
|
|
722
|
+
${content}
|
|
681
723
|
---
|
|
682
724
|
|
|
683
725
|
Return a JSON array of assessments. Each assessment should have:
|
|
@@ -749,32 +791,57 @@ Example format:
|
|
|
749
791
|
return prompt;
|
|
750
792
|
}
|
|
751
793
|
};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
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
|
+
};
|
|
767
836
|
var MotivationParsers = class {
|
|
768
837
|
/**
|
|
769
|
-
*
|
|
838
|
+
* Validate and reconcile structured comment elements.
|
|
770
839
|
*
|
|
771
|
-
* @param
|
|
840
|
+
* @param parsed - Already-parsed elements from the structured surface
|
|
772
841
|
* @param content - Original content to validate offsets against
|
|
773
842
|
* @returns Array of validated comment matches
|
|
774
|
-
* @throws if the response is not a parseable JSON array
|
|
775
843
|
*/
|
|
776
|
-
static parseComments(
|
|
777
|
-
const parsed = parseJsonArray(response, "comment");
|
|
844
|
+
static parseComments(parsed, content) {
|
|
778
845
|
const valid = parsed.filter(
|
|
779
846
|
(c) => isObject(c) && isString(c.exact) && isString(c.comment) && c.comment.trim().length > 0
|
|
780
847
|
);
|
|
@@ -803,15 +870,13 @@ var MotivationParsers = class {
|
|
|
803
870
|
return validatedComments;
|
|
804
871
|
}
|
|
805
872
|
/**
|
|
806
|
-
*
|
|
873
|
+
* Validate and reconcile structured highlight elements.
|
|
807
874
|
*
|
|
808
|
-
* @param
|
|
875
|
+
* @param parsed - Already-parsed elements from the structured surface
|
|
809
876
|
* @param content - Original content to validate offsets against
|
|
810
877
|
* @returns Array of validated highlight matches
|
|
811
|
-
* @throws if the response is not a parseable JSON array
|
|
812
878
|
*/
|
|
813
|
-
static parseHighlights(
|
|
814
|
-
const parsed = parseJsonArray(response, "highlight");
|
|
879
|
+
static parseHighlights(parsed, content) {
|
|
815
880
|
const highlights = parsed.filter(
|
|
816
881
|
(h) => isObject(h) && isString(h.exact)
|
|
817
882
|
);
|
|
@@ -838,15 +903,13 @@ var MotivationParsers = class {
|
|
|
838
903
|
return validatedHighlights;
|
|
839
904
|
}
|
|
840
905
|
/**
|
|
841
|
-
*
|
|
906
|
+
* Validate and reconcile structured assessment elements.
|
|
842
907
|
*
|
|
843
|
-
* @param
|
|
908
|
+
* @param parsed - Already-parsed elements from the structured surface
|
|
844
909
|
* @param content - Original content to validate offsets against
|
|
845
910
|
* @returns Array of validated assessment matches
|
|
846
|
-
* @throws if the response is not a parseable JSON array
|
|
847
911
|
*/
|
|
848
|
-
static parseAssessments(
|
|
849
|
-
const parsed = parseJsonArray(response, "assessment");
|
|
912
|
+
static parseAssessments(parsed, content) {
|
|
850
913
|
const assessments = parsed.filter(
|
|
851
914
|
(a) => isObject(a) && isString(a.exact) && isString(a.assessment)
|
|
852
915
|
);
|
|
@@ -874,14 +937,13 @@ var MotivationParsers = class {
|
|
|
874
937
|
return validatedAssessments;
|
|
875
938
|
}
|
|
876
939
|
/**
|
|
877
|
-
*
|
|
940
|
+
* Validate structured tag elements into raw, pre-reconciliation tag inputs.
|
|
878
941
|
* Reconciliation happens in `validateTagOffsets`, which adds `start`/`end`
|
|
879
942
|
* by anchoring `exact` against the source content.
|
|
880
943
|
*
|
|
881
|
-
* @
|
|
944
|
+
* @param parsed - Already-parsed elements from the structured surface
|
|
882
945
|
*/
|
|
883
|
-
static parseTags(
|
|
884
|
-
const parsed = parseJsonArray(response, "tag");
|
|
946
|
+
static parseTags(parsed) {
|
|
885
947
|
const valid = parsed.filter(
|
|
886
948
|
(t) => isObject(t) && isString(t.exact) && t.exact.trim().length > 0
|
|
887
949
|
);
|
|
@@ -923,10 +985,32 @@ function logAnchorMethod(motivation, exact, anchorMethod) {
|
|
|
923
985
|
}
|
|
924
986
|
|
|
925
987
|
// src/workers/annotation-detection.ts
|
|
926
|
-
function assertNotTruncated(response, motivation) {
|
|
988
|
+
function assertNotTruncated(response, motivation, chunk, totalChunks, outputBudget) {
|
|
927
989
|
if (response.stopReason === "max_tokens") {
|
|
928
|
-
throw new Error(`${motivation} detection response truncated (max_tokens)
|
|
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.`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onChunk) {
|
|
994
|
+
const limits = await client.limits();
|
|
995
|
+
const scaffoldTokens = estimateTokens(buildPrompt(""));
|
|
996
|
+
const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
|
|
997
|
+
const chunks = chunkText(content, chunking);
|
|
998
|
+
const collected = [];
|
|
999
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1000
|
+
const response = await boundedGenerateStructured(
|
|
1001
|
+
client,
|
|
1002
|
+
buildPrompt(chunks[i]),
|
|
1003
|
+
outputBudget,
|
|
1004
|
+
temperature,
|
|
1005
|
+
elementSchema
|
|
1006
|
+
);
|
|
1007
|
+
assertNotTruncated(response, motivation, i + 1, chunks.length, outputBudget);
|
|
1008
|
+
collected.push(...parse(response.items));
|
|
1009
|
+
if (i < chunks.length - 1) {
|
|
1010
|
+
onChunk?.(i + 1, chunks.length);
|
|
1011
|
+
}
|
|
929
1012
|
}
|
|
1013
|
+
return collected;
|
|
930
1014
|
}
|
|
931
1015
|
var AnnotationDetection = class {
|
|
932
1016
|
/**
|
|
@@ -937,11 +1021,17 @@ var AnnotationDetection = class {
|
|
|
937
1021
|
* (source-resource locale). See `types.ts` "Locale conventions" for the
|
|
938
1022
|
* full discussion.
|
|
939
1023
|
*/
|
|
940
|
-
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1024
|
+
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onChunk) {
|
|
1025
|
+
return detectInChunks(
|
|
1026
|
+
client,
|
|
1027
|
+
content,
|
|
1028
|
+
(chunk) => MotivationPrompts.buildCommentPrompt(chunk, instructions, tone, density, language, sourceLanguage),
|
|
1029
|
+
0.4,
|
|
1030
|
+
"comment",
|
|
1031
|
+
COMMENT_ELEMENT_SCHEMA,
|
|
1032
|
+
(items) => MotivationParsers.parseComments(items, content),
|
|
1033
|
+
onChunk
|
|
1034
|
+
);
|
|
945
1035
|
}
|
|
946
1036
|
/**
|
|
947
1037
|
* Detect highlights in content.
|
|
@@ -950,11 +1040,17 @@ var AnnotationDetection = class {
|
|
|
950
1040
|
* applies, used in the prompt so the LLM analyzes non-English source
|
|
951
1041
|
* correctly.
|
|
952
1042
|
*/
|
|
953
|
-
static async detectHighlights(content, client, instructions, density, sourceLanguage) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1043
|
+
static async detectHighlights(content, client, instructions, density, sourceLanguage, onChunk) {
|
|
1044
|
+
return detectInChunks(
|
|
1045
|
+
client,
|
|
1046
|
+
content,
|
|
1047
|
+
(chunk) => MotivationPrompts.buildHighlightPrompt(chunk, instructions, density, sourceLanguage),
|
|
1048
|
+
0.3,
|
|
1049
|
+
"highlight",
|
|
1050
|
+
HIGHLIGHT_ELEMENT_SCHEMA,
|
|
1051
|
+
(items) => MotivationParsers.parseHighlights(items, content),
|
|
1052
|
+
onChunk
|
|
1053
|
+
);
|
|
958
1054
|
}
|
|
959
1055
|
/**
|
|
960
1056
|
* Detect assessments in content.
|
|
@@ -963,11 +1059,17 @@ var AnnotationDetection = class {
|
|
|
963
1059
|
* (annotation body locale). `sourceLanguage` is the locale of the content
|
|
964
1060
|
* being analyzed (source-resource locale).
|
|
965
1061
|
*/
|
|
966
|
-
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage) {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1062
|
+
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onChunk) {
|
|
1063
|
+
return detectInChunks(
|
|
1064
|
+
client,
|
|
1065
|
+
content,
|
|
1066
|
+
(chunk) => MotivationPrompts.buildAssessmentPrompt(chunk, instructions, tone, density, language, sourceLanguage),
|
|
1067
|
+
0.3,
|
|
1068
|
+
"assessment",
|
|
1069
|
+
ASSESSMENT_ELEMENT_SCHEMA,
|
|
1070
|
+
(items) => MotivationParsers.parseAssessments(items, content),
|
|
1071
|
+
onChunk
|
|
1072
|
+
);
|
|
971
1073
|
}
|
|
972
1074
|
/**
|
|
973
1075
|
* Detect tags in content for a specific category.
|
|
@@ -981,28 +1083,45 @@ var AnnotationDetection = class {
|
|
|
981
1083
|
* identifiers, not LLM-generated text — so it's consumed at the body-stamp
|
|
982
1084
|
* site, not here.
|
|
983
1085
|
*/
|
|
984
|
-
static async detectTags(content, client, schema, category, sourceLanguage) {
|
|
1086
|
+
static async detectTags(content, client, schema, category, sourceLanguage, onChunk) {
|
|
985
1087
|
const categoryInfo = schema.tags.find((t) => t.name === category);
|
|
986
1088
|
if (!categoryInfo) {
|
|
987
1089
|
throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
|
|
988
1090
|
}
|
|
989
|
-
const
|
|
1091
|
+
const parsedTags = await detectInChunks(
|
|
1092
|
+
client,
|
|
990
1093
|
content,
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1094
|
+
(chunk) => MotivationPrompts.buildTagPrompt(
|
|
1095
|
+
chunk,
|
|
1096
|
+
category,
|
|
1097
|
+
schema.name,
|
|
1098
|
+
schema.description,
|
|
1099
|
+
schema.domain,
|
|
1100
|
+
categoryInfo.description,
|
|
1101
|
+
categoryInfo.examples,
|
|
1102
|
+
sourceLanguage
|
|
1103
|
+
),
|
|
1104
|
+
0.2,
|
|
1105
|
+
"tag",
|
|
1106
|
+
TAG_ELEMENT_SCHEMA,
|
|
1107
|
+
(items) => MotivationParsers.parseTags(items),
|
|
1108
|
+
onChunk
|
|
998
1109
|
);
|
|
999
|
-
const response = await boundedGenerateWithMetadata(client, prompt, 4e3, 0.2, { format: "json" });
|
|
1000
|
-
assertNotTruncated(response, "tag");
|
|
1001
|
-
const parsedTags = MotivationParsers.parseTags(response.text);
|
|
1002
1110
|
return MotivationParsers.validateTagOffsets(parsedTags, content, category);
|
|
1003
1111
|
}
|
|
1004
1112
|
};
|
|
1005
|
-
|
|
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
|
+
};
|
|
1124
|
+
async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onChunk) {
|
|
1006
1125
|
const entityTypesDescription = entityTypes.map((et) => {
|
|
1007
1126
|
if (typeof et === "string") {
|
|
1008
1127
|
return et;
|
|
@@ -1034,11 +1153,11 @@ Find direct mentions only (names, proper nouns). Do not include pronouns or desc
|
|
|
1034
1153
|
const sourceLangGuidance = sourceLanguage ? `
|
|
1035
1154
|
Source text language: ${getLocaleEnglishName(sourceLanguage) || sourceLanguage}.
|
|
1036
1155
|
` : "";
|
|
1037
|
-
const
|
|
1156
|
+
const buildPrompt = (text) => `Identify entity references in the following text. Look for mentions of: ${entityTypesDescription}.
|
|
1038
1157
|
${descriptiveReferenceGuidance}${sourceLangGuidance}
|
|
1039
1158
|
Text to analyze:
|
|
1040
1159
|
"""
|
|
1041
|
-
${
|
|
1160
|
+
${text}
|
|
1042
1161
|
"""
|
|
1043
1162
|
|
|
1044
1163
|
Respond with a JSON array of entities found. Each entity should have:
|
|
@@ -1051,59 +1170,53 @@ If no entities are found, respond with an empty array [].
|
|
|
1051
1170
|
|
|
1052
1171
|
Example output:
|
|
1053
1172
|
[{"exact":"Alice","entityType":"Person","prefix":"","suffix":" went to"},{"exact":"Paris","entityType":"Location","prefix":"went to ","suffix":" yesterday"}]`;
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
entities = JSON.parse(response.text.trim());
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
logger.error("Failed to parse entity extraction response", {
|
|
1081
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1082
|
-
response: response.text.slice(0, 500)
|
|
1083
|
-
});
|
|
1084
|
-
throw new Error("Failed to parse entity extraction response", {
|
|
1085
|
-
cause: error instanceof Error ? error : new Error(String(error))
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
if (!isArray(entities)) {
|
|
1089
|
-
logger.error("Failed to parse entity extraction response: expected a JSON array", {
|
|
1090
|
-
response: response.text.slice(0, 500)
|
|
1173
|
+
const limits = await client.limits();
|
|
1174
|
+
const scaffoldTokens = estimateTokens(buildPrompt(""));
|
|
1175
|
+
const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
|
|
1176
|
+
const chunks = chunkText(exact, chunking);
|
|
1177
|
+
logger.debug("Sending entity extraction request", {
|
|
1178
|
+
entityTypes: entityTypesDescription,
|
|
1179
|
+
chunks: chunks.length,
|
|
1180
|
+
chunkSizeTokens: chunking.chunkSize,
|
|
1181
|
+
outputBudget
|
|
1182
|
+
});
|
|
1183
|
+
const collected = [];
|
|
1184
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1185
|
+
const response = await boundedGenerateStructured(
|
|
1186
|
+
client,
|
|
1187
|
+
buildPrompt(chunks[i]),
|
|
1188
|
+
outputBudget,
|
|
1189
|
+
0.3,
|
|
1190
|
+
// Lower temperature for more consistent extraction
|
|
1191
|
+
ENTITY_ELEMENT_SCHEMA
|
|
1192
|
+
);
|
|
1193
|
+
logger.debug("Got entity extraction response", {
|
|
1194
|
+
chunk: i + 1,
|
|
1195
|
+
chunks: chunks.length,
|
|
1196
|
+
items: response.items.length
|
|
1091
1197
|
});
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const ok = isObject(e) && isString(e.exact) && isString(e.entityType);
|
|
1097
|
-
if (!ok) {
|
|
1098
|
-
logger.debug("Dropped malformed LLM entity", { entity: e });
|
|
1198
|
+
if (response.stopReason === "max_tokens") {
|
|
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.`;
|
|
1200
|
+
logger.error(errorMsg, { items: response.items.length });
|
|
1201
|
+
throw new Error(errorMsg);
|
|
1099
1202
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1203
|
+
for (const e of response.items) {
|
|
1204
|
+
if (isObject(e) && isString(e.exact) && isString(e.entityType)) {
|
|
1205
|
+
collected.push({
|
|
1206
|
+
exact: e.exact,
|
|
1207
|
+
entityType: e.entityType,
|
|
1208
|
+
...isString(e.prefix) ? { prefix: e.prefix } : {},
|
|
1209
|
+
...isString(e.suffix) ? { suffix: e.suffix } : {}
|
|
1210
|
+
});
|
|
1211
|
+
} else {
|
|
1212
|
+
logger.debug("Dropped malformed LLM entity", { entity: e });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (i < chunks.length - 1) {
|
|
1216
|
+
onChunk?.(i + 1, chunks.length);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return collected;
|
|
1107
1220
|
}
|
|
1108
1221
|
function getLanguageName(locale) {
|
|
1109
1222
|
return getLocaleEnglishName(locale) || locale;
|
|
@@ -1114,7 +1227,7 @@ var SEMANTIC_MATCH_CHARS = 240;
|
|
|
1114
1227
|
function idLabel(resourceId, annotationId) {
|
|
1115
1228
|
return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
|
|
1116
1229
|
}
|
|
1117
|
-
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false) {
|
|
1230
|
+
async function generateResourceFromTopic(topic, entityTypes, client, logger, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false, repair) {
|
|
1118
1231
|
logger.debug("Generating resource from topic", {
|
|
1119
1232
|
topicPreview: topic.substring(0, 100),
|
|
1120
1233
|
entityTypes,
|
|
@@ -1230,11 +1343,12 @@ ${parts.join("\n")}`;
|
|
|
1230
1343
|
let semanticContextSection = "";
|
|
1231
1344
|
const similar = context?.semanticContext?.similar ?? [];
|
|
1232
1345
|
if (similar.length > 0) {
|
|
1233
|
-
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)}`);
|
|
1346
|
+
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.machineRead ? " [OCR]" : ""} ${m.text.slice(0, SEMANTIC_MATCH_CHARS)}`);
|
|
1347
|
+
const ocrNote = similar.some((m) => m.machineRead) ? "\nPassages marked [OCR] were read from scanned images by character recognition; treat their exact wording and numbers as uncertain, and say so if you rely on one." : "";
|
|
1234
1348
|
semanticContextSection = `
|
|
1235
1349
|
|
|
1236
1350
|
Related passages from the knowledge base:
|
|
1237
|
-
${lines.join("\n")}`;
|
|
1351
|
+
${lines.join("\n")}${ocrNote}`;
|
|
1238
1352
|
}
|
|
1239
1353
|
let leadLine;
|
|
1240
1354
|
if (task === "resource") {
|
|
@@ -1249,11 +1363,12 @@ ${lines.join("\n")}`;
|
|
|
1249
1363
|
Topic: "${topic}"`;
|
|
1250
1364
|
}
|
|
1251
1365
|
const isPlainText = outputMediaType === "text/plain";
|
|
1366
|
+
const isPdf = outputMediaType === "application/pdf";
|
|
1252
1367
|
let structureRequirement = "";
|
|
1253
1368
|
let titleRequirement = "";
|
|
1254
1369
|
if (structure === "sections") {
|
|
1255
|
-
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";
|
|
1256
|
-
if (!isPlainText) {
|
|
1370
|
+
structureRequirement = isPdf ? "\n- Organize the content into titled sections (= Heading) with well-structured paragraphs" : isPlainText ? "\n- Organize the content into titled sections with well-structured paragraphs" : "\n- Organize the content into titled sections (## Section) with well-structured paragraphs";
|
|
1371
|
+
if (!isPlainText && !isPdf) {
|
|
1257
1372
|
titleRequirement = "\n- Start with a clear heading (# Title)";
|
|
1258
1373
|
}
|
|
1259
1374
|
} else if (structure === "prose") {
|
|
@@ -1266,11 +1381,20 @@ Topic: "${topic}"`;
|
|
|
1266
1381
|
- Organize the output as: ${structure}`;
|
|
1267
1382
|
}
|
|
1268
1383
|
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." : "";
|
|
1269
|
-
const formatRequirements =
|
|
1384
|
+
const formatRequirements = isPdf ? `- Write the response as Typst markup (the Typst typesetting language \u2014 not markdown, not LaTeX)
|
|
1385
|
+
- Headings are written as = Heading (deeper levels == Subheading); everything else is plain prose paragraphs
|
|
1386
|
+
- Do not emit markdown syntax or code fences` : isPlainText ? `- Write the response as plain text \u2014 no formatting markup (no #, *, backticks, headings, or links)
|
|
1270
1387
|
- Begin with the title on its own first line` : `- Use markdown formatting
|
|
1271
1388
|
- Write the response as markdown`;
|
|
1389
|
+
const repairSection = repair ? `
|
|
1390
|
+
|
|
1391
|
+
Your previous attempt failed to compile. Fix the error and return the complete corrected document \u2014 full source, not a diff.
|
|
1392
|
+
Compile error:
|
|
1393
|
+
${repair.error}
|
|
1394
|
+
Previous source:
|
|
1395
|
+
${repair.source}` : "";
|
|
1272
1396
|
const prompt = `${leadLine}
|
|
1273
|
-
${userPrompt ? `Instruction: ${userPrompt}` : ""}
|
|
1397
|
+
${userPrompt ? `Instruction: ${userPrompt}` : ""}${repairSection}
|
|
1274
1398
|
${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
|
|
1275
1399
|
|
|
1276
1400
|
Requirements:
|
|
@@ -1279,7 +1403,7 @@ Requirements:
|
|
|
1279
1403
|
${formatRequirements}`;
|
|
1280
1404
|
const parseResponse = (response2) => {
|
|
1281
1405
|
let content = response2.trim();
|
|
1282
|
-
if (content.startsWith("```markdown") || content.startsWith("```md")) {
|
|
1406
|
+
if (content.startsWith("```markdown") || content.startsWith("```md") || content.startsWith("```typst")) {
|
|
1283
1407
|
content = content.slice(content.indexOf("\n") + 1);
|
|
1284
1408
|
const endIndex = content.lastIndexOf("```");
|
|
1285
1409
|
if (endIndex !== -1) {
|
|
@@ -1314,6 +1438,29 @@ ${formatRequirements}`;
|
|
|
1314
1438
|
});
|
|
1315
1439
|
return result;
|
|
1316
1440
|
}
|
|
1441
|
+
var PINNED_CREATION_TIMESTAMP = 17e8;
|
|
1442
|
+
var MAX_COMPILE_REPAIRS = 2;
|
|
1443
|
+
function compileTypst(source) {
|
|
1444
|
+
const dir = mkdtempSync(join(tmpdir(), "typst-"));
|
|
1445
|
+
try {
|
|
1446
|
+
const inFile = join(dir, "doc.typ");
|
|
1447
|
+
const outFile = join(dir, "doc.pdf");
|
|
1448
|
+
writeFileSync(inFile, source);
|
|
1449
|
+
try {
|
|
1450
|
+
execFileSync(
|
|
1451
|
+
"typst",
|
|
1452
|
+
["compile", "--creation-timestamp", String(PINNED_CREATION_TIMESTAMP), inFile, outFile],
|
|
1453
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
1454
|
+
);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
const stderr = err.stderr;
|
|
1457
|
+
return { error: stderr?.length ? stderr.toString("utf8") : String(err) };
|
|
1458
|
+
}
|
|
1459
|
+
return { pdf: new Uint8Array(readFileSync(outFile)) };
|
|
1460
|
+
} finally {
|
|
1461
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1317
1464
|
|
|
1318
1465
|
// src/workers/generation/citation-resolver.ts
|
|
1319
1466
|
var CITATION_TOKEN = /\[\[([^\s[\]/]+)(?:\/([^\s[\]/]+))?\]\]/g;
|
|
@@ -1417,7 +1564,9 @@ async function processHighlightJob(content, inferenceClient, params, buildAnnota
|
|
|
1417
1564
|
inferenceClient,
|
|
1418
1565
|
params.instructions,
|
|
1419
1566
|
params.density,
|
|
1420
|
-
params.sourceLanguage
|
|
1567
|
+
params.sourceLanguage,
|
|
1568
|
+
// Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
|
|
1569
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
|
|
1421
1570
|
);
|
|
1422
1571
|
onProgress(60, `Creating ${highlights.length} annotations...`, "creating");
|
|
1423
1572
|
const annotations = dedupeAnnotations(highlights.map(
|
|
@@ -1439,7 +1588,9 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
|
|
|
1439
1588
|
params.tone,
|
|
1440
1589
|
params.density,
|
|
1441
1590
|
params.language,
|
|
1442
|
-
params.sourceLanguage
|
|
1591
|
+
params.sourceLanguage,
|
|
1592
|
+
// Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
|
|
1593
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
|
|
1443
1594
|
);
|
|
1444
1595
|
onProgress(60, `Creating ${comments.length} annotations...`, "creating");
|
|
1445
1596
|
const bodyLanguage = params.language ?? "en";
|
|
@@ -1469,7 +1620,9 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
|
|
|
1469
1620
|
params.tone,
|
|
1470
1621
|
params.density,
|
|
1471
1622
|
params.language,
|
|
1472
|
-
params.sourceLanguage
|
|
1623
|
+
params.sourceLanguage,
|
|
1624
|
+
// Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
|
|
1625
|
+
(completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
|
|
1473
1626
|
);
|
|
1474
1627
|
onProgress(60, `Creating ${assessments.length} annotations...`, "creating");
|
|
1475
1628
|
const bodyLanguage = params.language ?? "en";
|
|
@@ -1525,7 +1678,23 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
|
|
|
1525
1678
|
inferenceClient,
|
|
1526
1679
|
params.includeDescriptiveReferences ?? false,
|
|
1527
1680
|
logger,
|
|
1528
|
-
params.sourceLanguage
|
|
1681
|
+
params.sourceLanguage,
|
|
1682
|
+
// Chunk-boundary heartbeat: progress is the worker's liveness signal
|
|
1683
|
+
// (stall watchdog + backend janitor), so multi-chunk extraction must
|
|
1684
|
+
// emit between inference calls. Percentage interpolates within this
|
|
1685
|
+
// entity type's band of the 20–80 range.
|
|
1686
|
+
(completed, total) => {
|
|
1687
|
+
const interpolated = 20 + Math.round((i + completed / total) / entityTypeNames.length * 60);
|
|
1688
|
+
onProgress(interpolated, `Detecting ${entityTypeName} entities...`, "analyzing", {
|
|
1689
|
+
currentEntityType: entityTypeName,
|
|
1690
|
+
processedEntityTypes: i,
|
|
1691
|
+
totalEntityTypes: entityTypeNames.length,
|
|
1692
|
+
entitiesFound: totalFound,
|
|
1693
|
+
entitiesEmitted: totalEmitted,
|
|
1694
|
+
completedEntityTypes: [...completedEntityTypes],
|
|
1695
|
+
requestParams
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1529
1698
|
);
|
|
1530
1699
|
totalFound += extractedEntities.length;
|
|
1531
1700
|
completedEntityTypes.push({ entityType: entityTypeName, foundCount: extractedEntities.length });
|
|
@@ -1569,13 +1738,21 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
1569
1738
|
onProgress(10, "Loading resource...", "analyzing");
|
|
1570
1739
|
onProgress(30, "Analyzing text for tags...", "analyzing");
|
|
1571
1740
|
const allTags = [];
|
|
1572
|
-
for (
|
|
1741
|
+
for (let c = 0; c < params.categories.length; c++) {
|
|
1742
|
+
const category = params.categories[c];
|
|
1573
1743
|
const categoryTags = await AnnotationDetection.detectTags(
|
|
1574
1744
|
content,
|
|
1575
1745
|
inferenceClient,
|
|
1576
1746
|
params.schema,
|
|
1577
1747
|
category,
|
|
1578
|
-
params.sourceLanguage
|
|
1748
|
+
params.sourceLanguage,
|
|
1749
|
+
// Chunk-boundary heartbeat (liveness): interpolate within this
|
|
1750
|
+
// category's slice of the 30–60 band.
|
|
1751
|
+
(completed, total) => onProgress(
|
|
1752
|
+
30 + Math.round((c + completed / total) / params.categories.length * 30),
|
|
1753
|
+
"Analyzing text for tags...",
|
|
1754
|
+
"analyzing"
|
|
1755
|
+
)
|
|
1579
1756
|
);
|
|
1580
1757
|
allTags.push(...categoryTags);
|
|
1581
1758
|
}
|
|
@@ -1601,8 +1778,14 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
|
|
|
1601
1778
|
result: { tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
|
|
1602
1779
|
};
|
|
1603
1780
|
}
|
|
1781
|
+
function assertWithinOutputBudget(byteLength) {
|
|
1782
|
+
if (!withinByteBudget(byteLength)) {
|
|
1783
|
+
throw new Error(
|
|
1784
|
+
`Generated artifact exceeds the output byte budget: ${byteLength} bytes > ${MAX_PDF_BYTES}. Refusing a runaway generation.`
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1604
1788
|
async function processGenerationJob(inferenceClient, params, onProgress, logger) {
|
|
1605
|
-
const GENERATABLE_MEDIA_TYPES = ["text/markdown", "text/plain"];
|
|
1606
1789
|
const outputMediaType = params.outputMediaType ?? "text/markdown";
|
|
1607
1790
|
if (!GENERATABLE_MEDIA_TYPES.includes(outputMediaType)) {
|
|
1608
1791
|
throw new Error(
|
|
@@ -1611,6 +1794,84 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1611
1794
|
}
|
|
1612
1795
|
const title = params.title ?? "Untitled";
|
|
1613
1796
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
1797
|
+
if (outputMediaType === "application/pdf") {
|
|
1798
|
+
onProgress(5, "Generating resource...", "generating");
|
|
1799
|
+
const validIds = params.cite === true ? collectContextResourceIds(params.context) : null;
|
|
1800
|
+
let generated2 = await generateResourceFromTopic(
|
|
1801
|
+
title,
|
|
1802
|
+
entityTypes,
|
|
1803
|
+
inferenceClient,
|
|
1804
|
+
logger,
|
|
1805
|
+
params.prompt,
|
|
1806
|
+
params.language,
|
|
1807
|
+
params.context,
|
|
1808
|
+
params.temperature,
|
|
1809
|
+
params.maxTokens,
|
|
1810
|
+
params.sourceLanguage,
|
|
1811
|
+
outputMediaType,
|
|
1812
|
+
params.task,
|
|
1813
|
+
params.structure,
|
|
1814
|
+
params.cite
|
|
1815
|
+
);
|
|
1816
|
+
let source = generated2.content;
|
|
1817
|
+
let citations2 = [];
|
|
1818
|
+
if (validIds) {
|
|
1819
|
+
const resolved = resolveCitationTokens(generated2.content, validIds, logger);
|
|
1820
|
+
source = resolved.content;
|
|
1821
|
+
citations2 = resolved.citations;
|
|
1822
|
+
}
|
|
1823
|
+
let compiled = compileTypst(source);
|
|
1824
|
+
let repairs = 0;
|
|
1825
|
+
while ("error" in compiled && repairs < MAX_COMPILE_REPAIRS) {
|
|
1826
|
+
repairs++;
|
|
1827
|
+
logger.warn("Typst compile failed \u2014 feeding the error back for repair", {
|
|
1828
|
+
attempt: repairs,
|
|
1829
|
+
error: compiled.error.slice(0, 500)
|
|
1830
|
+
});
|
|
1831
|
+
generated2 = await generateResourceFromTopic(
|
|
1832
|
+
title,
|
|
1833
|
+
entityTypes,
|
|
1834
|
+
inferenceClient,
|
|
1835
|
+
logger,
|
|
1836
|
+
params.prompt,
|
|
1837
|
+
params.language,
|
|
1838
|
+
params.context,
|
|
1839
|
+
params.temperature,
|
|
1840
|
+
params.maxTokens,
|
|
1841
|
+
params.sourceLanguage,
|
|
1842
|
+
outputMediaType,
|
|
1843
|
+
params.task,
|
|
1844
|
+
params.structure,
|
|
1845
|
+
params.cite,
|
|
1846
|
+
{ source, error: compiled.error }
|
|
1847
|
+
);
|
|
1848
|
+
if (validIds) {
|
|
1849
|
+
const resolved = resolveCitationTokens(generated2.content, validIds, logger);
|
|
1850
|
+
source = resolved.content;
|
|
1851
|
+
citations2 = resolved.citations;
|
|
1852
|
+
} else {
|
|
1853
|
+
source = generated2.content;
|
|
1854
|
+
}
|
|
1855
|
+
compiled = compileTypst(source);
|
|
1856
|
+
}
|
|
1857
|
+
if ("error" in compiled) {
|
|
1858
|
+
throw new Error(
|
|
1859
|
+
`Typst compilation failed after ${MAX_COMPILE_REPAIRS} repair attempts: ${compiled.error}`
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
assertWithinOutputBudget(compiled.pdf.byteLength);
|
|
1863
|
+
onProgress(95, "Creating resource...", "creating");
|
|
1864
|
+
return {
|
|
1865
|
+
content: compiled.pdf,
|
|
1866
|
+
title: generated2.title ?? title,
|
|
1867
|
+
format: outputMediaType,
|
|
1868
|
+
citations: citations2,
|
|
1869
|
+
result: {
|
|
1870
|
+
resourceId: "",
|
|
1871
|
+
resourceName: generated2.title ?? title
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1614
1875
|
onProgress(5, "Generating resource...", "generating");
|
|
1615
1876
|
const generated = await generateResourceFromTopic(
|
|
1616
1877
|
title,
|
|
@@ -1636,8 +1897,10 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
|
|
|
1636
1897
|
citations = resolved.citations;
|
|
1637
1898
|
}
|
|
1638
1899
|
onProgress(95, "Creating resource...", "creating");
|
|
1900
|
+
const artifact = new TextEncoder().encode(content);
|
|
1901
|
+
assertWithinOutputBudget(artifact.byteLength);
|
|
1639
1902
|
return {
|
|
1640
|
-
content,
|
|
1903
|
+
content: artifact,
|
|
1641
1904
|
title: generated.title ?? title,
|
|
1642
1905
|
format: outputMediaType,
|
|
1643
1906
|
citations,
|