@memberjunction/content-autotagging 6.2.0-edge.0 → 6.2.0-edge.2
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/CloudStorage/providers/AutotagAzureBlob.d.ts +4 -0
- package/dist/CloudStorage/providers/AutotagAzureBlob.d.ts.map +1 -1
- package/dist/CloudStorage/providers/AutotagAzureBlob.js +13 -5
- package/dist/CloudStorage/providers/AutotagAzureBlob.js.map +1 -1
- package/dist/CloudStorage/providers/AutotagCloudStorage.d.ts +8 -8
- package/dist/CloudStorage/providers/AutotagCloudStorage.js +17 -17
- package/dist/Engine/generic/AutotagBaseEngine.d.ts +100 -13
- package/dist/Engine/generic/AutotagBaseEngine.d.ts.map +1 -1
- package/dist/Engine/generic/AutotagBaseEngine.js +232 -85
- package/dist/Engine/generic/AutotagBaseEngine.js.map +1 -1
- package/dist/Engine/generic/FieldPathResolver.d.ts +35 -2
- package/dist/Engine/generic/FieldPathResolver.d.ts.map +1 -1
- package/dist/Engine/generic/FieldPathResolver.js +53 -9
- package/dist/Engine/generic/FieldPathResolver.js.map +1 -1
- package/dist/Engine/generic/RunBudget.d.ts +22 -0
- package/dist/Engine/generic/RunBudget.d.ts.map +1 -1
- package/dist/Engine/generic/RunBudget.js +40 -8
- package/dist/Engine/generic/RunBudget.js.map +1 -1
- package/dist/Engine/generic/ScopeContextResolver.d.ts +4 -0
- package/dist/Engine/generic/ScopeContextResolver.d.ts.map +1 -1
- package/dist/Engine/generic/ScopeContextResolver.js +10 -2
- package/dist/Engine/generic/ScopeContextResolver.js.map +1 -1
- package/dist/Entity/generic/AutotagEntity.d.ts +12 -12
- package/dist/Entity/generic/AutotagEntity.js +24 -24
- package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.d.ts +8 -0
- package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.d.ts.map +1 -1
- package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.js +23 -7
- package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.js.map +1 -1
- package/dist/RSSFeed/generic/AutotagRSSFeed.d.ts +6 -6
- package/dist/RSSFeed/generic/AutotagRSSFeed.js +12 -12
- package/dist/Websites/generic/AutotagWebsite.d.ts +11 -0
- package/dist/Websites/generic/AutotagWebsite.d.ts.map +1 -1
- package/dist/Websites/generic/AutotagWebsite.js +24 -8
- package/dist/Websites/generic/AutotagWebsite.js.map +1 -1
- package/package.json +19 -19
|
@@ -21,7 +21,7 @@ import * as cheerio from 'cheerio';
|
|
|
21
21
|
import crypto from 'crypto';
|
|
22
22
|
import { BaseEmbeddings, GetAIAPIKey } from '@memberjunction/ai';
|
|
23
23
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
24
|
-
import { AIPromptRunner,
|
|
24
|
+
import { AIPromptRunner, AIEmbeddingRunner } from '@memberjunction/ai-prompts';
|
|
25
25
|
import { AIPromptParams } from '@memberjunction/ai-core-plus';
|
|
26
26
|
import { FIXED_WINDOW_SEGMENTER_KEY, ResolveSegmenter, } from '@memberjunction/ai-segmentation';
|
|
27
27
|
import { VectorDBBase } from '@memberjunction/ai-vectordb';
|
|
@@ -55,8 +55,8 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
55
55
|
constructor() {
|
|
56
56
|
super(...arguments);
|
|
57
57
|
// Cached metadata unique to this engine — loaded by BaseEngine.Config()
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
58
|
+
this._contentTypeAttributes = [];
|
|
59
|
+
this._contentSourceTypeParams = [];
|
|
60
60
|
/**
|
|
61
61
|
* Optional taxonomy JSON string to inject into the autotagging prompt.
|
|
62
62
|
* Set by the caller (e.g., AutotagEntity) before calling ExtractTextAndProcessWithLLM.
|
|
@@ -120,9 +120,9 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
120
120
|
/** All content file types — delegated to KnowledgeHubMetadataEngine */
|
|
121
121
|
get ContentFileTypes() { return this.khEngine.ContentFileTypes; }
|
|
122
122
|
/** All content type attributes, cached at startup */
|
|
123
|
-
get ContentTypeAttributes() { return this.
|
|
123
|
+
get ContentTypeAttributes() { return this._contentTypeAttributes; }
|
|
124
124
|
/** All content source type params, cached at startup */
|
|
125
|
-
get ContentSourceTypeParams() { return this.
|
|
125
|
+
get ContentSourceTypeParams() { return this._contentSourceTypeParams; }
|
|
126
126
|
async Config(forceRefresh, contextUser, provider) {
|
|
127
127
|
// Content Types, Content Source Types, and Content File Types are delegated to
|
|
128
128
|
// KnowledgeHubMetadataEngine (avoid redundant loading). Only cache entities unique to this engine.
|
|
@@ -131,12 +131,12 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
131
131
|
{
|
|
132
132
|
Type: 'entity',
|
|
133
133
|
EntityName: 'MJ: Content Type Attributes',
|
|
134
|
-
PropertyName: '
|
|
134
|
+
PropertyName: '_contentTypeAttributes',
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
Type: 'entity',
|
|
138
138
|
EntityName: 'MJ: Content Source Type Params',
|
|
139
|
-
PropertyName: '
|
|
139
|
+
PropertyName: '_contentSourceTypeParams',
|
|
140
140
|
},
|
|
141
141
|
];
|
|
142
142
|
await this.Load(configs, provider, forceRefresh, contextUser);
|
|
@@ -231,6 +231,9 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
231
231
|
buffer = [];
|
|
232
232
|
batchNum++;
|
|
233
233
|
anyItemsProcessed = true;
|
|
234
|
+
// The source's classification config and the type's model / tag limits are read from the
|
|
235
|
+
// KnowledgeHub cache per item; catch a source or type created after it loaded.
|
|
236
|
+
await this.ensureItemMetadataCached(batch, contextUser);
|
|
234
237
|
// Rate limit before each batch of parallel LLM calls.
|
|
235
238
|
await this.LLMRateLimiter.Acquire();
|
|
236
239
|
let batchOk = 0;
|
|
@@ -317,7 +320,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
317
320
|
processRunParams.startTime = new Date();
|
|
318
321
|
processRunParams.endTime = new Date();
|
|
319
322
|
processRunParams.numItemsProcessed = totalProcessed - resumeOffset;
|
|
320
|
-
await this.
|
|
323
|
+
await this.SaveProcessRun(processRunParams, contextUser);
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
/**
|
|
@@ -345,8 +348,8 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
345
348
|
// A8: Update tagging status to Processing
|
|
346
349
|
await this.updateContentItemTaggingStatus(params.contentItemID, 'Processing', contextUser);
|
|
347
350
|
try {
|
|
348
|
-
const LLMResults = await this.
|
|
349
|
-
await this.
|
|
351
|
+
const LLMResults = await this.PromptAndRetrieveResultsFromLLM(params, contextUser);
|
|
352
|
+
await this.SaveLLMResults(LLMResults, contextUser);
|
|
350
353
|
// A8: Update tagging status to Complete
|
|
351
354
|
await this.updateContentItemTaggingStatus(params.contentItemID, 'Complete', contextUser);
|
|
352
355
|
}
|
|
@@ -445,7 +448,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
445
448
|
}
|
|
446
449
|
// Set up the bridge callback — fires after each ContentItemTag is saved
|
|
447
450
|
this.OnContentItemTagSaved = async (contentItemTag, parentTagName, ctxUser) => {
|
|
448
|
-
await this.
|
|
451
|
+
await this.bridgeContentItemTagToTaxonomy(contentItemTag, parentTagName, ctxUser);
|
|
449
452
|
};
|
|
450
453
|
LogStatus(`[TaxonomyBridge] Bridge callback installed`);
|
|
451
454
|
}
|
|
@@ -509,7 +512,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
509
512
|
* - For Entity sources: tags the original entity record (e.g., Products row)
|
|
510
513
|
* - For non-Entity sources (RSS, Website, etc.): tags the ContentItem itself
|
|
511
514
|
*/
|
|
512
|
-
async
|
|
515
|
+
async bridgeContentItemTagToTaxonomy(contentItemTag, parentTagName, contextUser) {
|
|
513
516
|
try {
|
|
514
517
|
// If the LLM suggested a parent tag, resolve it first (through the same
|
|
515
518
|
// mutex, to prevent duplicate parents under concurrent batch processing)
|
|
@@ -673,14 +676,14 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
673
676
|
// extension keys ride along in the same JSON and are surfaced via the extended interface.
|
|
674
677
|
return source.ConfigurationObject ?? null;
|
|
675
678
|
}
|
|
676
|
-
async
|
|
679
|
+
async PromptAndRetrieveResultsFromLLM(params, contextUser) {
|
|
677
680
|
await AIEngine.Instance.Config(false, contextUser);
|
|
678
681
|
// Resolve the effective classification context once per item (async lookup);
|
|
679
682
|
// buildPromptData reads it from params for each chunk.
|
|
680
683
|
params.classificationContext = await this.resolveClassificationContext(params, contextUser);
|
|
681
684
|
const prompt = this.getAutotagPrompt();
|
|
682
685
|
const tokenLimit = this.resolveTokenLimit(params.modelID);
|
|
683
|
-
const chunks = await this.
|
|
686
|
+
const chunks = await this.ChunkExtractedText(params.text, tokenLimit);
|
|
684
687
|
if (chunks.length === 0 || (chunks.length === 1 && (!chunks[0] || chunks[0].trim().length === 0))) {
|
|
685
688
|
LogError(`[Autotag] No text to process for item ${params.contentItemID}`);
|
|
686
689
|
return {};
|
|
@@ -689,7 +692,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
689
692
|
const startTime = new Date();
|
|
690
693
|
for (let ci = 0; ci < chunks.length; ci++) {
|
|
691
694
|
try {
|
|
692
|
-
LLMResults = await this.
|
|
695
|
+
LLMResults = await this.ProcessChunkWithPromptRunner(prompt, params, chunks[ci], LLMResults, contextUser);
|
|
693
696
|
}
|
|
694
697
|
catch (chunkError) {
|
|
695
698
|
LogError(`[Autotag] Chunk ${ci + 1}/${chunks.length} failed for item ${params.contentItemID}: ${chunkError instanceof Error ? chunkError.message : String(chunkError)}`);
|
|
@@ -700,6 +703,10 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
700
703
|
LLMResults.contentItemID = params.contentItemID;
|
|
701
704
|
return LLMResults;
|
|
702
705
|
}
|
|
706
|
+
/** @deprecated Use {@link PromptAndRetrieveResultsFromLLM}. */
|
|
707
|
+
async promptAndRetrieveResultsFromLLM(params, contextUser) {
|
|
708
|
+
return this.PromptAndRetrieveResultsFromLLM(params, contextUser);
|
|
709
|
+
}
|
|
703
710
|
/**
|
|
704
711
|
* Resolves the input token limit for chunking. Uses the model specified by modelID if available,
|
|
705
712
|
* otherwise falls back to a conservative default.
|
|
@@ -719,7 +726,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
719
726
|
* Uses the prompt's configured model by default. If ContentType.AIModelID is set,
|
|
720
727
|
* it is passed as a runtime model override via AIPromptParams.override.
|
|
721
728
|
*/
|
|
722
|
-
async
|
|
729
|
+
async ProcessChunkWithPromptRunner(prompt, params, chunk, LLMResults, contextUser) {
|
|
723
730
|
const promptParams = new AIPromptParams();
|
|
724
731
|
promptParams.prompt = prompt;
|
|
725
732
|
promptParams.contextUser = contextUser;
|
|
@@ -770,25 +777,37 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
770
777
|
}
|
|
771
778
|
return LLMResults;
|
|
772
779
|
}
|
|
773
|
-
|
|
780
|
+
/** @deprecated Use {@link ProcessChunkWithPromptRunner}. */
|
|
781
|
+
async processChunkWithPromptRunner(prompt, params, chunk, LLMResults, contextUser) {
|
|
782
|
+
return this.ProcessChunkWithPromptRunner(prompt, params, chunk, LLMResults, contextUser);
|
|
783
|
+
}
|
|
784
|
+
async SaveLLMResults(LLMResults, contextUser) {
|
|
774
785
|
if (LLMResults.isValidContent === true) {
|
|
775
|
-
await this.
|
|
776
|
-
await this.
|
|
786
|
+
await this.SaveResultsToContentItemAttribute(LLMResults, contextUser);
|
|
787
|
+
await this.SaveContentItemTags(LLMResults.contentItemID, LLMResults, contextUser);
|
|
777
788
|
}
|
|
778
789
|
else if (LLMResults.isValidContent === false) {
|
|
779
790
|
LogStatus(`[Autotag] LLM judged content item ${LLMResults.contentItemID} INVALID — deleting it. Title: ${String(LLMResults.title ?? '')} | Reason/description: ${String(LLMResults.description ?? LLMResults.reason ?? '')}`.slice(0, 600));
|
|
780
|
-
await this.
|
|
791
|
+
await this.DeleteInvalidContentItem(LLMResults.contentItemID, contextUser);
|
|
781
792
|
}
|
|
782
793
|
else {
|
|
783
794
|
LogError(`[Autotag] Unexpected LLM format for item ${LLMResults.contentItemID} — isValidContent missing. Keys: ${Object.keys(LLMResults).join(', ')}`);
|
|
784
795
|
}
|
|
785
796
|
}
|
|
786
|
-
|
|
797
|
+
/** @deprecated Use {@link SaveLLMResults}. */
|
|
798
|
+
async saveLLMResults(LLMResults, contextUser) {
|
|
799
|
+
return this.SaveLLMResults(LLMResults, contextUser);
|
|
800
|
+
}
|
|
801
|
+
async DeleteInvalidContentItem(contentItemID, contextUser) {
|
|
787
802
|
const md = this.ProviderToUse;
|
|
788
803
|
const contentItem = await md.GetEntityObject('MJ: Content Items', contextUser);
|
|
789
804
|
await contentItem.Load(contentItemID);
|
|
790
805
|
await contentItem.Delete();
|
|
791
806
|
}
|
|
807
|
+
/** @deprecated Use {@link DeleteInvalidContentItem}. */
|
|
808
|
+
async deleteInvalidContentItem(contentItemID, contextUser) {
|
|
809
|
+
return this.DeleteInvalidContentItem(contentItemID, contextUser);
|
|
810
|
+
}
|
|
792
811
|
/**
|
|
793
812
|
* Registration key of the segmentation strategy this engine uses.
|
|
794
813
|
*
|
|
@@ -827,7 +846,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
827
846
|
* ({@link MAX_EMBEDDING_TOKENS}) — these two chunk sites feed different consumers and
|
|
828
847
|
* must not be collapsed into one call. Tagging chunks are transient and never persisted.
|
|
829
848
|
*/
|
|
830
|
-
async
|
|
849
|
+
async ChunkExtractedText(text, tokenLimit) {
|
|
831
850
|
try {
|
|
832
851
|
const maxChunkTokens = Math.ceil(tokenLimit / 1.5);
|
|
833
852
|
// Short-circuit: text that already fits is passed through verbatim, preserving its
|
|
@@ -847,6 +866,10 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
847
866
|
return [text];
|
|
848
867
|
}
|
|
849
868
|
}
|
|
869
|
+
/** @deprecated Use {@link ChunkExtractedText}. */
|
|
870
|
+
async chunkExtractedText(text, tokenLimit) {
|
|
871
|
+
return this.ChunkExtractedText(text, tokenLimit);
|
|
872
|
+
}
|
|
850
873
|
/**
|
|
851
874
|
* Simple character-based chunking as fallback
|
|
852
875
|
*/
|
|
@@ -867,7 +890,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
867
890
|
* After each tag is saved, invokes the OnContentItemTagSaved callback (if set)
|
|
868
891
|
* for taxonomy bridge processing.
|
|
869
892
|
*/
|
|
870
|
-
async
|
|
893
|
+
async SaveContentItemTags(contentItemID, LLMResults, contextUser) {
|
|
871
894
|
const md = this.ProviderToUse;
|
|
872
895
|
const keywords = LLMResults.keywords;
|
|
873
896
|
if (!keywords || !Array.isArray(keywords))
|
|
@@ -926,11 +949,15 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
926
949
|
}));
|
|
927
950
|
}
|
|
928
951
|
}
|
|
952
|
+
/** @deprecated Use {@link SaveContentItemTags}. */
|
|
953
|
+
async saveContentItemTags(contentItemID, LLMResults, contextUser) {
|
|
954
|
+
return this.SaveContentItemTags(contentItemID, LLMResults, contextUser);
|
|
955
|
+
}
|
|
929
956
|
/**
|
|
930
957
|
* Saves LLM-extracted attributes to the database.
|
|
931
958
|
* Updates content item name/description, then creates attribute records for other fields.
|
|
932
959
|
*/
|
|
933
|
-
async
|
|
960
|
+
async SaveResultsToContentItemAttribute(LLMResults, contextUser) {
|
|
934
961
|
const md = this.ProviderToUse;
|
|
935
962
|
const contentItemID = LLMResults.contentItemID;
|
|
936
963
|
const skipKeys = new Set(['keywords', 'processStartTime', 'processEndTime', 'contentItemID', 'isValidContent', AutotagBaseEngine_1.AI_PROMPT_RUN_ID_KEY]);
|
|
@@ -964,17 +991,25 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
964
991
|
}));
|
|
965
992
|
}
|
|
966
993
|
}
|
|
994
|
+
/** @deprecated Use {@link SaveResultsToContentItemAttribute}. */
|
|
995
|
+
async saveResultsToContentItemAttribute(LLMResults, contextUser) {
|
|
996
|
+
return this.SaveResultsToContentItemAttribute(LLMResults, contextUser);
|
|
997
|
+
}
|
|
967
998
|
/**
|
|
968
999
|
* Retrieves all content sources for a given content source type.
|
|
969
1000
|
* Throws if no sources are found.
|
|
970
1001
|
*/
|
|
971
|
-
async
|
|
1002
|
+
async GetAllContentSources(contextUser, contentSourceTypeID) {
|
|
972
1003
|
const sources = await this.GetAllContentSourcesSafe(contextUser, contentSourceTypeID);
|
|
973
1004
|
if (sources.length === 0) {
|
|
974
1005
|
throw new Error(`No content sources found for content source type with ID '${contentSourceTypeID}'`);
|
|
975
1006
|
}
|
|
976
1007
|
return sources;
|
|
977
1008
|
}
|
|
1009
|
+
/** @deprecated Use {@link GetAllContentSources}. */
|
|
1010
|
+
async getAllContentSources(contextUser, contentSourceTypeID) {
|
|
1011
|
+
return this.GetAllContentSources(contextUser, contentSourceTypeID);
|
|
1012
|
+
}
|
|
978
1013
|
/**
|
|
979
1014
|
* Retrieves all content sources for a given content source type.
|
|
980
1015
|
* Returns an empty array (instead of throwing) when no sources are configured.
|
|
@@ -989,7 +1024,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
989
1024
|
}
|
|
990
1025
|
return sourceType.ID;
|
|
991
1026
|
}
|
|
992
|
-
async
|
|
1027
|
+
async GetContentSourceParams(contentSource, contextUser) {
|
|
993
1028
|
const contentSourceParams = new Map();
|
|
994
1029
|
const rv = new RunView();
|
|
995
1030
|
const results = await rv.RunView({
|
|
@@ -1002,7 +1037,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1002
1037
|
const params = this.GetDefaultContentSourceTypeParams(contentSourceParam.ContentSourceTypeParamID);
|
|
1003
1038
|
params.contentSourceID = contentSource.ID;
|
|
1004
1039
|
if (contentSourceParam.Value) {
|
|
1005
|
-
params.value = this.
|
|
1040
|
+
params.value = this.CastValueAsCorrectType(contentSourceParam.Value, params.type);
|
|
1006
1041
|
}
|
|
1007
1042
|
contentSourceParams.set(params.name, params.value);
|
|
1008
1043
|
}
|
|
@@ -1012,50 +1047,70 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1012
1047
|
}
|
|
1013
1048
|
return contentSourceParams;
|
|
1014
1049
|
}
|
|
1050
|
+
/** @deprecated Use {@link GetContentSourceParams}. */
|
|
1051
|
+
async getContentSourceParams(contentSource, contextUser) {
|
|
1052
|
+
return this.GetContentSourceParams(contentSource, contextUser);
|
|
1053
|
+
}
|
|
1015
1054
|
GetDefaultContentSourceTypeParams(contentSourceTypeParamID) {
|
|
1016
|
-
const result = this.
|
|
1055
|
+
const result = this._contentSourceTypeParams.find(p => UUIDsEqual(p.ID, contentSourceTypeParamID));
|
|
1017
1056
|
if (!result) {
|
|
1018
1057
|
throw new Error(`Content Source Type Param with ID '${contentSourceTypeParamID}' not found in cached metadata`);
|
|
1019
1058
|
}
|
|
1020
1059
|
const params = new ContentSourceTypeParams();
|
|
1021
1060
|
params.name = result.Name;
|
|
1022
1061
|
params.type = result.Type.toLowerCase();
|
|
1023
|
-
params.value = this.
|
|
1062
|
+
params.value = this.CastValueAsCorrectType(result.DefaultValue ?? '', params.type);
|
|
1024
1063
|
return params;
|
|
1025
1064
|
}
|
|
1026
|
-
|
|
1065
|
+
CastValueAsCorrectType(value, type) {
|
|
1027
1066
|
switch (type) {
|
|
1028
1067
|
case 'number':
|
|
1029
1068
|
return parseInt(value, 10);
|
|
1030
1069
|
case 'boolean':
|
|
1031
|
-
return this.
|
|
1070
|
+
return this.StringToBoolean(value);
|
|
1032
1071
|
case 'string':
|
|
1033
1072
|
return value;
|
|
1034
1073
|
case 'string[]':
|
|
1035
|
-
return this.
|
|
1074
|
+
return this.ParseStringArray(value);
|
|
1036
1075
|
case 'regexp':
|
|
1037
1076
|
return new RegExp(value.replace(/\\\\/g, '\\'));
|
|
1038
1077
|
default:
|
|
1039
1078
|
return value;
|
|
1040
1079
|
}
|
|
1041
1080
|
}
|
|
1042
|
-
|
|
1081
|
+
/** @deprecated Use {@link CastValueAsCorrectType}. */
|
|
1082
|
+
castValueAsCorrectType(value, type) {
|
|
1083
|
+
return this.CastValueAsCorrectType(value, type);
|
|
1084
|
+
}
|
|
1085
|
+
StringToBoolean(str) {
|
|
1043
1086
|
return str === 'true';
|
|
1044
1087
|
}
|
|
1045
|
-
|
|
1088
|
+
/** @deprecated Use {@link StringToBoolean}. */
|
|
1089
|
+
stringToBoolean(str) {
|
|
1090
|
+
return this.StringToBoolean(str);
|
|
1091
|
+
}
|
|
1092
|
+
ParseStringArray(value) {
|
|
1046
1093
|
return JSON.parse(value);
|
|
1047
1094
|
}
|
|
1095
|
+
/** @deprecated Use {@link ParseStringArray}. */
|
|
1096
|
+
parseStringArray(value) {
|
|
1097
|
+
return this.ParseStringArray(value);
|
|
1098
|
+
}
|
|
1048
1099
|
/**
|
|
1049
1100
|
* Converts a run date to the user's local timezone.
|
|
1050
1101
|
*/
|
|
1051
|
-
async
|
|
1102
|
+
async ConvertLastRunDateToTimezone(lastRunDate) {
|
|
1052
1103
|
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1053
1104
|
return toZonedTime(lastRunDate, userTimeZone);
|
|
1054
1105
|
}
|
|
1106
|
+
/** @deprecated Use {@link ConvertLastRunDateToTimezone}. */
|
|
1107
|
+
async convertLastRunDateToTimezone(lastRunDate) {
|
|
1108
|
+
return this.ConvertLastRunDateToTimezone(lastRunDate);
|
|
1109
|
+
}
|
|
1055
1110
|
/**
|
|
1056
1111
|
* Retrieves the last run date for a content source. Returns epoch date if no runs exist.
|
|
1057
1112
|
*/
|
|
1058
|
-
async
|
|
1113
|
+
async GetContentSourceLastRunDate(contentSourceID, contextUser) {
|
|
1059
1114
|
const rv = new RunView();
|
|
1060
1115
|
// Exclude 'Running' status to avoid using the current in-progress run's
|
|
1061
1116
|
// start time as the cutoff — that would cause the provider to skip all records.
|
|
@@ -1067,13 +1122,17 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1067
1122
|
}, contextUser);
|
|
1068
1123
|
if (results.Success && results.Results.length) {
|
|
1069
1124
|
const lastRunDate = results.Results[0].__mj_CreatedAt;
|
|
1070
|
-
return this.
|
|
1125
|
+
return this.ConvertLastRunDateToTimezone(lastRunDate);
|
|
1071
1126
|
}
|
|
1072
1127
|
if (results.Success) {
|
|
1073
1128
|
return new Date(0);
|
|
1074
1129
|
}
|
|
1075
1130
|
throw new Error(`Failed to retrieve last run date for content source with ID ${contentSourceID}`);
|
|
1076
1131
|
}
|
|
1132
|
+
/** @deprecated Use {@link GetContentSourceLastRunDate}. */
|
|
1133
|
+
async getContentSourceLastRunDate(contentSourceID, contextUser) {
|
|
1134
|
+
return this.GetContentSourceLastRunDate(contentSourceID, contextUser);
|
|
1135
|
+
}
|
|
1077
1136
|
GetContentItemParams(contentTypeID) {
|
|
1078
1137
|
const contentType = this.khEngine.GetContentTypeByID(contentTypeID);
|
|
1079
1138
|
if (!contentType) {
|
|
@@ -1107,7 +1166,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1107
1166
|
return fileType.Name;
|
|
1108
1167
|
}
|
|
1109
1168
|
GetAdditionalContentTypePrompt(contentTypeID) {
|
|
1110
|
-
const attrs = this.
|
|
1169
|
+
const attrs = this._contentTypeAttributes.filter(a => UUIDsEqual(a.ContentTypeID, contentTypeID));
|
|
1111
1170
|
if (attrs.length === 0)
|
|
1112
1171
|
return '';
|
|
1113
1172
|
return attrs.map(attr => `${attr.Prompt}. The data must be included in the above described JSON file in this key-value format: { "${attr.Name}": (value of ${attr.Name} here)}`).join('\n');
|
|
@@ -1118,15 +1177,23 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1118
1177
|
const sourceTypeName = this.GetContentSourceTypeName(contentSourceParams.ContentSourceTypeID);
|
|
1119
1178
|
return `${contentTypeName} in ${fileTypeName} format obtained from a ${sourceTypeName} source`;
|
|
1120
1179
|
}
|
|
1121
|
-
async
|
|
1180
|
+
async GetChecksumFromURL(url) {
|
|
1122
1181
|
const response = await HttpGet(url, { ResponseType: 'text' });
|
|
1123
1182
|
const content = String(response.Data);
|
|
1124
1183
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
1125
1184
|
}
|
|
1126
|
-
|
|
1185
|
+
/** @deprecated Use {@link GetChecksumFromURL}. */
|
|
1186
|
+
async getChecksumFromURL(url) {
|
|
1187
|
+
return this.GetChecksumFromURL(url);
|
|
1188
|
+
}
|
|
1189
|
+
async GetChecksumFromText(text) {
|
|
1127
1190
|
return crypto.createHash('sha256').update(text).digest('hex');
|
|
1128
1191
|
}
|
|
1129
|
-
|
|
1192
|
+
/** @deprecated Use {@link GetChecksumFromText}. */
|
|
1193
|
+
async getChecksumFromText(text) {
|
|
1194
|
+
return this.GetChecksumFromText(text);
|
|
1195
|
+
}
|
|
1196
|
+
async GetContentItemIDFromURL(contentSourceParams, contextUser) {
|
|
1130
1197
|
const url = contentSourceParams.URL;
|
|
1131
1198
|
const rv = new RunView();
|
|
1132
1199
|
const results = await rv.RunView({
|
|
@@ -1139,10 +1206,14 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1139
1206
|
}
|
|
1140
1207
|
throw new Error(`Content item with URL ${url} not found`);
|
|
1141
1208
|
}
|
|
1209
|
+
/** @deprecated Use {@link GetContentItemIDFromURL}. */
|
|
1210
|
+
async getContentItemIDFromURL(contentSourceParams, contextUser) {
|
|
1211
|
+
return this.GetContentItemIDFromURL(contentSourceParams, contextUser);
|
|
1212
|
+
}
|
|
1142
1213
|
/**
|
|
1143
1214
|
* Saves process run metadata to the database (backward-compatible simple version).
|
|
1144
1215
|
*/
|
|
1145
|
-
async
|
|
1216
|
+
async SaveProcessRun(processRunParams, contextUser) {
|
|
1146
1217
|
const md = this.ProviderToUse;
|
|
1147
1218
|
const processRun = await md.GetEntityObject('MJ: Content Process Runs', contextUser);
|
|
1148
1219
|
processRun.NewRecord();
|
|
@@ -1154,6 +1225,10 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1154
1225
|
processRun.StartedByUserID = contextUser.ID;
|
|
1155
1226
|
await processRun.Save();
|
|
1156
1227
|
}
|
|
1228
|
+
/** @deprecated Use {@link SaveProcessRun}. */
|
|
1229
|
+
async saveProcessRun(processRunParams, contextUser) {
|
|
1230
|
+
return this.SaveProcessRun(processRunParams, contextUser);
|
|
1231
|
+
}
|
|
1157
1232
|
/**
|
|
1158
1233
|
* Create a new ContentProcessRun record for batched pipeline execution.
|
|
1159
1234
|
* Returns the entity so the caller can update cursor/status as batches complete.
|
|
@@ -1235,15 +1310,23 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1235
1310
|
}),
|
|
1236
1311
|
};
|
|
1237
1312
|
}
|
|
1238
|
-
async
|
|
1313
|
+
async ParsePDF(dataBuffer) {
|
|
1239
1314
|
const dataPDF = await pdfParse(dataBuffer);
|
|
1240
1315
|
return dataPDF.text;
|
|
1241
1316
|
}
|
|
1242
|
-
|
|
1317
|
+
/** @deprecated Use {@link ParsePDF}. */
|
|
1318
|
+
async parsePDF(dataBuffer) {
|
|
1319
|
+
return this.ParsePDF(dataBuffer);
|
|
1320
|
+
}
|
|
1321
|
+
async ParseDOCX(dataBuffer) {
|
|
1243
1322
|
const dataDOCX = await officeparser.parseOffice(dataBuffer);
|
|
1244
1323
|
return dataDOCX.toText();
|
|
1245
1324
|
}
|
|
1246
|
-
|
|
1325
|
+
/** @deprecated Use {@link ParseDOCX}. */
|
|
1326
|
+
async parseDOCX(dataBuffer) {
|
|
1327
|
+
return this.ParseDOCX(dataBuffer);
|
|
1328
|
+
}
|
|
1329
|
+
async ParseHTML(data) {
|
|
1247
1330
|
try {
|
|
1248
1331
|
const $ = cheerio.load(data);
|
|
1249
1332
|
$('script, style, nav, footer, header, .hidden').remove();
|
|
@@ -1254,18 +1337,26 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1254
1337
|
throw e;
|
|
1255
1338
|
}
|
|
1256
1339
|
}
|
|
1257
|
-
|
|
1340
|
+
/** @deprecated Use {@link ParseHTML}. */
|
|
1341
|
+
async parseHTML(data) {
|
|
1342
|
+
return this.ParseHTML(data);
|
|
1343
|
+
}
|
|
1344
|
+
async ParseFileFromPath(filePath) {
|
|
1258
1345
|
const dataBuffer = await fs.promises.readFile(filePath);
|
|
1259
1346
|
const fileExtension = filePath.split('.').pop()?.toLowerCase();
|
|
1260
1347
|
switch (fileExtension) {
|
|
1261
1348
|
case 'pdf':
|
|
1262
|
-
return this.
|
|
1349
|
+
return this.ParsePDF(dataBuffer);
|
|
1263
1350
|
case 'docx':
|
|
1264
|
-
return this.
|
|
1351
|
+
return this.ParseDOCX(dataBuffer);
|
|
1265
1352
|
default:
|
|
1266
1353
|
throw new Error(`File type '${fileExtension}' not supported`);
|
|
1267
1354
|
}
|
|
1268
1355
|
}
|
|
1356
|
+
/** @deprecated Use {@link ParseFileFromPath}. */
|
|
1357
|
+
async parseFileFromPath(filePath) {
|
|
1358
|
+
return this.ParseFileFromPath(filePath);
|
|
1359
|
+
}
|
|
1269
1360
|
// ---- Direct Vectorization ----
|
|
1270
1361
|
/**
|
|
1271
1362
|
* Embeds content items and upserts them to the appropriate vector index.
|
|
@@ -1274,7 +1365,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1274
1365
|
* (first active VectorIndex). Each group is processed in configurable batches with
|
|
1275
1366
|
* parallel upserts within each batch.
|
|
1276
1367
|
*
|
|
1277
|
-
* Uses
|
|
1368
|
+
* Uses AIEmbeddingRunner to create AIPromptRun records for each embedding batch,
|
|
1278
1369
|
* enabling token/cost tracking and linking to ContentProcessRunDetail records.
|
|
1279
1370
|
*
|
|
1280
1371
|
* @param items - content items to vectorize
|
|
@@ -1317,21 +1408,21 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1317
1408
|
}
|
|
1318
1409
|
/**
|
|
1319
1410
|
* Process a single infrastructure group: embed texts in batches and upsert to vector DB.
|
|
1320
|
-
* Uses
|
|
1411
|
+
* Uses AIEmbeddingRunner for each embedding batch to create AIPromptRun records with
|
|
1321
1412
|
* token/cost tracking. Upserts within each batch run in parallel for throughput.
|
|
1322
1413
|
*
|
|
1323
1414
|
* @param items - content items in this infrastructure group
|
|
1324
1415
|
* @param infra - resolved embedding + vector DB infrastructure
|
|
1325
1416
|
* @param tagMap - pre-loaded tags for metadata enrichment
|
|
1326
1417
|
* @param batchSize - number of items per embedding batch
|
|
1327
|
-
* @param contextUser - current user for
|
|
1418
|
+
* @param contextUser - current user for AIEmbeddingRunner tracking
|
|
1328
1419
|
* @param onBatchComplete - callback invoked after each batch with item count
|
|
1329
1420
|
* @returns count of vectorized items and collected AIPromptRun IDs
|
|
1330
1421
|
*/
|
|
1331
1422
|
async vectorizeGroup(items, infra, tagMap, batchSize, contextUser, pathResolver, onBatchComplete) {
|
|
1332
1423
|
let vectorized = 0;
|
|
1333
1424
|
const promptRunIDs = [];
|
|
1334
|
-
const
|
|
1425
|
+
const embeddingRunner = new AIEmbeddingRunner();
|
|
1335
1426
|
// Provider directives are built for every item BEFORE any embedding spend. A driver may
|
|
1336
1427
|
// reject a record from BuildProviderDirectives (e.g. a mandatory routing value its config
|
|
1337
1428
|
// requires — such as a tenant-isolation field — is missing); rejected items are marked
|
|
@@ -1361,8 +1452,8 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1361
1452
|
const texts = allChunks.map(c => c.text);
|
|
1362
1453
|
// Rate limit embedding API call
|
|
1363
1454
|
await this.EmbeddingRateLimiter.Acquire(texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0));
|
|
1364
|
-
// Use
|
|
1365
|
-
const runResult = await
|
|
1455
|
+
// Use AIEmbeddingRunner to embed texts with AIPromptRun tracking
|
|
1456
|
+
const runResult = await embeddingRunner.RunEmbedding({
|
|
1366
1457
|
Texts: texts,
|
|
1367
1458
|
ModelID: infra.embeddingModelID,
|
|
1368
1459
|
PromptID: embeddingPromptID,
|
|
@@ -1397,17 +1488,17 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
1397
1488
|
return { vectorized, promptRunIDs };
|
|
1398
1489
|
}
|
|
1399
1490
|
/**
|
|
1400
|
-
* Resolve the "Content Embedding" prompt ID from AIEngine for
|
|
1401
|
-
* Returns undefined if the prompt is not found (
|
|
1402
|
-
*
|
|
1491
|
+
* Resolve the "Content Embedding" prompt ID from AIEngine for AIEmbeddingRunner tracking.
|
|
1492
|
+
* Returns undefined if the prompt is not found (AIEmbeddingRunner then uses the first
|
|
1493
|
+
* active Embedding-type prompt).
|
|
1403
1494
|
*/
|
|
1404
1495
|
resolveEmbeddingPromptID() {
|
|
1405
1496
|
const prompt = AIEngine.Instance.Prompts.find(p => p.Name === 'Content Embedding' && p.Status === 'Active');
|
|
1406
1497
|
if (prompt) {
|
|
1407
1498
|
return prompt.ID;
|
|
1408
1499
|
}
|
|
1409
|
-
// Fall back: let
|
|
1410
|
-
LogStatus('[Autotag] "Content Embedding" prompt not found —
|
|
1500
|
+
// Fall back: let AIEmbeddingRunner find the first active Embedding prompt
|
|
1501
|
+
LogStatus('[Autotag] "Content Embedding" prompt not found — AIEmbeddingRunner will use default embedding prompt');
|
|
1411
1502
|
return undefined;
|
|
1412
1503
|
}
|
|
1413
1504
|
/**
|
|
@@ -2034,7 +2125,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2034
2125
|
async embedAndPersistChunkBatch(embeddable, infra, tagMap, contextUser, stats, directivesByItem) {
|
|
2035
2126
|
const texts = embeddable.map(e => e.chunk.text);
|
|
2036
2127
|
await this.EmbeddingRateLimiter.Acquire(texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0));
|
|
2037
|
-
const runResult = await new
|
|
2128
|
+
const runResult = await new AIEmbeddingRunner().RunEmbedding({
|
|
2038
2129
|
Texts: texts,
|
|
2039
2130
|
ModelID: infra.embeddingModelID,
|
|
2040
2131
|
PromptID: this.resolveEmbeddingPromptID(),
|
|
@@ -2112,8 +2203,15 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2112
2203
|
/**
|
|
2113
2204
|
* Load content source and content type records for all unique source/type IDs
|
|
2114
2205
|
* referenced by the given items. Returns maps keyed by normalized ID.
|
|
2115
|
-
|
|
2116
|
-
|
|
2206
|
+
*
|
|
2207
|
+
* Every vectorization entry point (VectorizeContentItems, PurgeDeletedChunks,
|
|
2208
|
+
* EmbedPendingChunks, vector dedup) builds its maps here, so this is where a stale
|
|
2209
|
+
* KnowledgeHub cache is caught — see {@link ensureItemMetadataCached}. The per-item storage
|
|
2210
|
+
* config ({@link resolveItemVectorStorageConfig}) reads the same cache later in the pass, so
|
|
2211
|
+
* it sees the reloaded rows too.
|
|
2212
|
+
*/
|
|
2213
|
+
async loadContentSourceAndTypeMaps(items, contextUser) {
|
|
2214
|
+
await this.ensureItemMetadataCached(items, contextUser);
|
|
2117
2215
|
const sourceIdSet = new Set(items.map(i => NormalizeUUID(i.ContentSourceID)));
|
|
2118
2216
|
const typeIdSet = new Set(items.map(i => NormalizeUUID(i.ContentTypeID)));
|
|
2119
2217
|
// Use KH engine cached data instead of RunView calls
|
|
@@ -2131,6 +2229,57 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2131
2229
|
}
|
|
2132
2230
|
return { sourceMap, typeMap };
|
|
2133
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Make sure the KnowledgeHub cache holds every content source and content type these items
|
|
2234
|
+
* reference, reloading it ONCE when it does not.
|
|
2235
|
+
*
|
|
2236
|
+
* ── A MISS HERE MEANS THE CACHE IS STALE ────────────────────────────────────────────────────
|
|
2237
|
+
* Both ids are required foreign keys on the item, so the rows exist; a miss means the cache
|
|
2238
|
+
* loaded before they did. That is the normal state of a long-running worker: unless Redis
|
|
2239
|
+
* cross-server cache sync is configured, nothing tells this process about a source another
|
|
2240
|
+
* process created after it booted. And the source row is not decoration. It carries the item's
|
|
2241
|
+
* routing (its EmbeddingModelID / VectorIndexID override) and its storage config
|
|
2242
|
+
* (VectorIDStrategy, ChunkTextStorage, VectorMetadata, VectorEntityName). Read from a stale
|
|
2243
|
+
* cache, every one of those silently falls back to the content type's values or the hard-coded
|
|
2244
|
+
* defaults, so the item is embedded into the wrong index with the wrong vector ids until the
|
|
2245
|
+
* worker restarts, and its later re-runs then write different ids.
|
|
2246
|
+
*
|
|
2247
|
+
* One `Config(true)` reloads all six KnowledgeHub sets in a single batched, cache-bypassing
|
|
2248
|
+
* round trip. It runs only on a miss, so a warm cache costs nothing beyond the set lookups, and
|
|
2249
|
+
* at most once per call however many items miss.
|
|
2250
|
+
*
|
|
2251
|
+
* Never makes things worse: a failed reload is logged and the pass continues on whatever the
|
|
2252
|
+
* cache holds, which is exactly the pre-reload behavior (content type → default cascade).
|
|
2253
|
+
*/
|
|
2254
|
+
async ensureItemMetadataCached(items, contextUser) {
|
|
2255
|
+
const uncached = this.findUncachedItemMetadata(items);
|
|
2256
|
+
if (uncached.length === 0)
|
|
2257
|
+
return;
|
|
2258
|
+
LogStatus(`[Autotag] KnowledgeHub cache is missing ${uncached.length} row(s) these items reference (created after it loaded) — reloading it once: ${uncached.join(', ')}`);
|
|
2259
|
+
try {
|
|
2260
|
+
await this.khEngine.Config(true, contextUser, this.ProviderToUse);
|
|
2261
|
+
}
|
|
2262
|
+
catch (e) {
|
|
2263
|
+
LogError(`[Autotag] KnowledgeHub cache reload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2264
|
+
}
|
|
2265
|
+
const stillUncached = this.findUncachedItemMetadata(items);
|
|
2266
|
+
if (stillUncached.length > 0) {
|
|
2267
|
+
LogError(`[Autotag] ${stillUncached.length} row(s) still absent from the KnowledgeHub cache after a reload: ${stillUncached.join(', ')} — items referencing them fall back to their content type / default configuration`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/** The distinct content source / content type ids these items reference that the KnowledgeHub cache does not hold. */
|
|
2271
|
+
findUncachedItemMetadata(items) {
|
|
2272
|
+
const uncached = new Set();
|
|
2273
|
+
for (const item of items) {
|
|
2274
|
+
if (item.ContentSourceID && !this.khEngine.GetContentSourceByID(item.ContentSourceID)) {
|
|
2275
|
+
uncached.add(`content source ${NormalizeUUID(item.ContentSourceID)}`);
|
|
2276
|
+
}
|
|
2277
|
+
if (item.ContentTypeID && !this.khEngine.GetContentTypeByID(item.ContentTypeID)) {
|
|
2278
|
+
uncached.add(`content type ${NormalizeUUID(item.ContentTypeID)}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return [...uncached];
|
|
2282
|
+
}
|
|
2134
2283
|
/**
|
|
2135
2284
|
* Resolve the (embeddingModelID, vectorIndexID) pair for a content item using
|
|
2136
2285
|
* the cascade: ContentSource override -> ContentType default -> null (global fallback).
|
|
@@ -2193,9 +2342,9 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2193
2342
|
* Looks up the vector index by ID and the embedding model from AIEngine.
|
|
2194
2343
|
*/
|
|
2195
2344
|
async buildVectorInfrastructure(embeddingModelID, vectorIndexID, _contextUser) {
|
|
2196
|
-
const vectorIndex =
|
|
2345
|
+
const vectorIndex = AIEngine.Instance.GetVectorIndexByID(vectorIndexID);
|
|
2197
2346
|
if (!vectorIndex) {
|
|
2198
|
-
throw new Error(`Vector index ${vectorIndexID} not found in
|
|
2347
|
+
throw new Error(`Vector index ${vectorIndexID} not found in AIEngine cache`);
|
|
2199
2348
|
}
|
|
2200
2349
|
return this.createInfrastructureFromIndex(vectorIndex, embeddingModelID);
|
|
2201
2350
|
}
|
|
@@ -2203,7 +2352,7 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2203
2352
|
* Fallback: resolve infrastructure from the first available VectorIndex (original behavior).
|
|
2204
2353
|
*/
|
|
2205
2354
|
async getDefaultVectorInfrastructure(_contextUser) {
|
|
2206
|
-
const vectorIndexes =
|
|
2355
|
+
const vectorIndexes = AIEngine.Instance.VectorIndexes;
|
|
2207
2356
|
if (vectorIndexes.length === 0) {
|
|
2208
2357
|
throw new Error('No vector indexes found — create one in the Configuration tab first');
|
|
2209
2358
|
}
|
|
@@ -2225,13 +2374,14 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2225
2374
|
const aiModel = this.findEmbeddingModel(embeddingModelID);
|
|
2226
2375
|
const driverClass = aiModel.DriverClass;
|
|
2227
2376
|
const embeddingModelName = aiModel.APIName ?? aiModel.Name;
|
|
2228
|
-
|
|
2229
|
-
const externalIndexName = vectorIndex.ExternalID?.trim() || vectorIndex.Name;
|
|
2377
|
+
const externalIndexName = AIEngine.Instance.GetProviderIndexName(vectorIndex);
|
|
2230
2378
|
LogStatus(`VectorizeContentItems: USING embedding model "${aiModel.Name}" (${driverClass}), vector DB "${vectorDBClassKey}", index "${externalIndexName}" (Vector Index "${vectorIndex.Name}")`);
|
|
2231
|
-
const embedding = this.createEmbeddingInstance(driverClass);
|
|
2232
2379
|
const vectorDB = this.createVectorDBInstance(vectorDBClassKey);
|
|
2380
|
+
const legacyEmbedding = this.legacyEmbeddingAccessor(driverClass);
|
|
2233
2381
|
return {
|
|
2234
|
-
embedding
|
|
2382
|
+
get embedding() {
|
|
2383
|
+
return legacyEmbedding();
|
|
2384
|
+
},
|
|
2235
2385
|
vectorDB,
|
|
2236
2386
|
indexName: externalIndexName,
|
|
2237
2387
|
embeddingModelName,
|
|
@@ -2269,20 +2419,17 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2269
2419
|
}
|
|
2270
2420
|
return aiModel;
|
|
2271
2421
|
}
|
|
2272
|
-
/**
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
if (!instance)
|
|
2284
|
-
throw new Error(`Failed to create embedding instance for ${driverClass}`);
|
|
2285
|
-
return instance;
|
|
2422
|
+
/**
|
|
2423
|
+
* Backs the deprecated `ResolvedVectorInfrastructure.embedding`: builds the driver the way this
|
|
2424
|
+
* engine did before AIEmbeddingRunner, on first read only. Nothing in MJ reads it, so normally
|
|
2425
|
+
* nothing is built. An empty key is fine for keyless drivers such as LocalEmbedding.
|
|
2426
|
+
*/
|
|
2427
|
+
legacyEmbeddingAccessor(driverClass) {
|
|
2428
|
+
let driver;
|
|
2429
|
+
return () => {
|
|
2430
|
+
driver ??= MJGlobal.Instance.ClassFactory.CreateInstance(BaseEmbeddings, driverClass, GetAIAPIKey(driverClass) || '') ?? undefined;
|
|
2431
|
+
return driver;
|
|
2432
|
+
};
|
|
2286
2433
|
}
|
|
2287
2434
|
/**
|
|
2288
2435
|
* Create a VectorDBBase instance for a given class key, wired for colocated storage where the
|
|
@@ -2898,9 +3045,9 @@ let AutotagBaseEngine = class AutotagBaseEngine extends BaseEngine {
|
|
|
2898
3045
|
const text = contentItem.Text.trim();
|
|
2899
3046
|
const truncated = text.length > 8000 ? text.substring(0, 8000) : text;
|
|
2900
3047
|
await this.EmbeddingRateLimiter.Acquire(Math.ceil(truncated.length / 4));
|
|
2901
|
-
const
|
|
3048
|
+
const embeddingRunner = new AIEmbeddingRunner();
|
|
2902
3049
|
const embeddingPromptID = this.resolveEmbeddingPromptID();
|
|
2903
|
-
const runResult = await
|
|
3050
|
+
const runResult = await embeddingRunner.RunEmbedding({
|
|
2904
3051
|
ModelID: infra.embeddingModelID,
|
|
2905
3052
|
Texts: [truncated],
|
|
2906
3053
|
PromptID: embeddingPromptID ?? undefined,
|