@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.
Files changed (35) hide show
  1. package/dist/CloudStorage/providers/AutotagAzureBlob.d.ts +4 -0
  2. package/dist/CloudStorage/providers/AutotagAzureBlob.d.ts.map +1 -1
  3. package/dist/CloudStorage/providers/AutotagAzureBlob.js +13 -5
  4. package/dist/CloudStorage/providers/AutotagAzureBlob.js.map +1 -1
  5. package/dist/CloudStorage/providers/AutotagCloudStorage.d.ts +8 -8
  6. package/dist/CloudStorage/providers/AutotagCloudStorage.js +17 -17
  7. package/dist/Engine/generic/AutotagBaseEngine.d.ts +100 -13
  8. package/dist/Engine/generic/AutotagBaseEngine.d.ts.map +1 -1
  9. package/dist/Engine/generic/AutotagBaseEngine.js +232 -85
  10. package/dist/Engine/generic/AutotagBaseEngine.js.map +1 -1
  11. package/dist/Engine/generic/FieldPathResolver.d.ts +35 -2
  12. package/dist/Engine/generic/FieldPathResolver.d.ts.map +1 -1
  13. package/dist/Engine/generic/FieldPathResolver.js +53 -9
  14. package/dist/Engine/generic/FieldPathResolver.js.map +1 -1
  15. package/dist/Engine/generic/RunBudget.d.ts +22 -0
  16. package/dist/Engine/generic/RunBudget.d.ts.map +1 -1
  17. package/dist/Engine/generic/RunBudget.js +40 -8
  18. package/dist/Engine/generic/RunBudget.js.map +1 -1
  19. package/dist/Engine/generic/ScopeContextResolver.d.ts +4 -0
  20. package/dist/Engine/generic/ScopeContextResolver.d.ts.map +1 -1
  21. package/dist/Engine/generic/ScopeContextResolver.js +10 -2
  22. package/dist/Engine/generic/ScopeContextResolver.js.map +1 -1
  23. package/dist/Entity/generic/AutotagEntity.d.ts +12 -12
  24. package/dist/Entity/generic/AutotagEntity.js +24 -24
  25. package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.d.ts +8 -0
  26. package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.d.ts.map +1 -1
  27. package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.js +23 -7
  28. package/dist/LocalFileSystem/generic/AutotagLocalFileSystem.js.map +1 -1
  29. package/dist/RSSFeed/generic/AutotagRSSFeed.d.ts +6 -6
  30. package/dist/RSSFeed/generic/AutotagRSSFeed.js +12 -12
  31. package/dist/Websites/generic/AutotagWebsite.d.ts +11 -0
  32. package/dist/Websites/generic/AutotagWebsite.d.ts.map +1 -1
  33. package/dist/Websites/generic/AutotagWebsite.js +24 -8
  34. package/dist/Websites/generic/AutotagWebsite.js.map +1 -1
  35. 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, AIModelRunner } from '@memberjunction/ai-prompts';
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._ContentTypeAttributes = [];
59
- this._ContentSourceTypeParams = [];
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._ContentTypeAttributes; }
123
+ get ContentTypeAttributes() { return this._contentTypeAttributes; }
124
124
  /** All content source type params, cached at startup */
125
- get ContentSourceTypeParams() { return this._ContentSourceTypeParams; }
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: '_ContentTypeAttributes',
134
+ PropertyName: '_contentTypeAttributes',
135
135
  },
136
136
  {
137
137
  Type: 'entity',
138
138
  EntityName: 'MJ: Content Source Type Params',
139
- PropertyName: '_ContentSourceTypeParams',
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.saveProcessRun(processRunParams, contextUser);
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.promptAndRetrieveResultsFromLLM(params, contextUser);
349
- await this.saveLLMResults(LLMResults, contextUser);
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.BridgeContentItemTagToTaxonomy(contentItemTag, parentTagName, ctxUser);
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 BridgeContentItemTagToTaxonomy(contentItemTag, parentTagName, contextUser) {
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 promptAndRetrieveResultsFromLLM(params, contextUser) {
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.chunkExtractedText(params.text, tokenLimit);
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.processChunkWithPromptRunner(prompt, params, chunks[ci], LLMResults, contextUser);
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 processChunkWithPromptRunner(prompt, params, chunk, LLMResults, contextUser) {
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
- async saveLLMResults(LLMResults, contextUser) {
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.saveResultsToContentItemAttribute(LLMResults, contextUser);
776
- await this.saveContentItemTags(LLMResults.contentItemID, LLMResults, contextUser);
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.deleteInvalidContentItem(LLMResults.contentItemID, contextUser);
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
- async deleteInvalidContentItem(contentItemID, contextUser) {
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 chunkExtractedText(text, tokenLimit) {
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 saveContentItemTags(contentItemID, LLMResults, contextUser) {
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 saveResultsToContentItemAttribute(LLMResults, contextUser) {
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 getAllContentSources(contextUser, contentSourceTypeID) {
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 getContentSourceParams(contentSource, contextUser) {
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.castValueAsCorrectType(contentSourceParam.Value, params.type);
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._ContentSourceTypeParams.find(p => UUIDsEqual(p.ID, contentSourceTypeParamID));
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.castValueAsCorrectType(result.DefaultValue ?? '', params.type);
1062
+ params.value = this.CastValueAsCorrectType(result.DefaultValue ?? '', params.type);
1024
1063
  return params;
1025
1064
  }
1026
- castValueAsCorrectType(value, type) {
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.stringToBoolean(value);
1070
+ return this.StringToBoolean(value);
1032
1071
  case 'string':
1033
1072
  return value;
1034
1073
  case 'string[]':
1035
- return this.parseStringArray(value);
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
- stringToBoolean(str) {
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
- parseStringArray(value) {
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 convertLastRunDateToTimezone(lastRunDate) {
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 getContentSourceLastRunDate(contentSourceID, contextUser) {
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.convertLastRunDateToTimezone(lastRunDate);
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._ContentTypeAttributes.filter(a => UUIDsEqual(a.ContentTypeID, contentTypeID));
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 getChecksumFromURL(url) {
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
- async getChecksumFromText(text) {
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
- async getContentItemIDFromURL(contentSourceParams, contextUser) {
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 saveProcessRun(processRunParams, contextUser) {
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 parsePDF(dataBuffer) {
1313
+ async ParsePDF(dataBuffer) {
1239
1314
  const dataPDF = await pdfParse(dataBuffer);
1240
1315
  return dataPDF.text;
1241
1316
  }
1242
- async parseDOCX(dataBuffer) {
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
- async parseHTML(data) {
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
- async parseFileFromPath(filePath) {
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.parsePDF(dataBuffer);
1349
+ return this.ParsePDF(dataBuffer);
1263
1350
  case 'docx':
1264
- return this.parseDOCX(dataBuffer);
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 AIModelRunner to create AIPromptRun records for each embedding batch,
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 AIModelRunner for each embedding batch to create AIPromptRun records with
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 AIModelRunner tracking
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 modelRunner = new AIModelRunner();
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 AIModelRunner to embed texts with AIPromptRun tracking
1365
- const runResult = await modelRunner.RunEmbedding({
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 AIModelRunner tracking.
1401
- * Returns undefined if the prompt is not found (AIModelRunner will fall back to
1402
- * the first active Embedding-type prompt).
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 AIModelRunner find the first active Embedding prompt
1410
- LogStatus('[Autotag] "Content Embedding" prompt not found — AIModelRunner will use default embedding prompt');
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 AIModelRunner().RunEmbedding({
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
- async loadContentSourceAndTypeMaps(items, _contextUser) {
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 = this.khEngine.GetVectorIndexByID(vectorIndexID);
2345
+ const vectorIndex = AIEngine.Instance.GetVectorIndexByID(vectorIndexID);
2197
2346
  if (!vectorIndex) {
2198
- throw new Error(`Vector index ${vectorIndexID} not found in KnowledgeHubMetadataEngine cache`);
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 = this.khEngine.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
- // The 3rd-party index is addressed by ExternalID (the provider-side name); Name is the MJ display label.
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
- /** Create a BaseEmbeddings instance for a given driver class */
2273
- createEmbeddingInstance(driverClass) {
2274
- // No pre-flight key check, deliberately — the same call the EntityDocument pipeline already
2275
- // makes (`entityVectorSync.ts`), for the reason documented there: an empty key is legitimate
2276
- // for local-only drivers (LocalEmbedding runs ONNX in-process and does `super(apiKey || 'local')`),
2277
- // and for a cloud driver that genuinely needs one the constructor or the first inference call
2278
- // raises a real provider-level auth error, which is more actionable than a guard here.
2279
- // Gating up front made local embedding models unusable from this pipeline without inventing a
2280
- // meaningless AI_VENDOR_API_KEY__LocalEmbedding.
2281
- const apiKey = GetAIAPIKey(driverClass);
2282
- const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseEmbeddings, driverClass, apiKey || '');
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 modelRunner = new AIModelRunner();
3048
+ const embeddingRunner = new AIEmbeddingRunner();
2902
3049
  const embeddingPromptID = this.resolveEmbeddingPromptID();
2903
- const runResult = await modelRunner.RunEmbedding({
3050
+ const runResult = await embeddingRunner.RunEmbedding({
2904
3051
  ModelID: infra.embeddingModelID,
2905
3052
  Texts: [truncated],
2906
3053
  PromptID: embeddingPromptID ?? undefined,