@memberjunction/server 5.24.0 → 5.26.0

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 (62) hide show
  1. package/dist/agents/skip-sdk.d.ts +12 -0
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +70 -1
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/config.d.ts +70 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +21 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/generated/generated.d.ts +498 -0
  10. package/dist/generated/generated.d.ts.map +1 -1
  11. package/dist/generated/generated.js +2755 -0
  12. package/dist/generated/generated.js.map +1 -1
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +18 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/resolvers/ArtifactFileResolver.d.ts +15 -0
  18. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -0
  19. package/dist/resolvers/ArtifactFileResolver.js +74 -0
  20. package/dist/resolvers/ArtifactFileResolver.js.map +1 -0
  21. package/dist/resolvers/AutotagPipelineResolver.d.ts +13 -0
  22. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  23. package/dist/resolvers/AutotagPipelineResolver.js +103 -3
  24. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  25. package/dist/resolvers/CacheStatsResolver.d.ts +31 -0
  26. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -0
  27. package/dist/resolvers/CacheStatsResolver.js +181 -0
  28. package/dist/resolvers/CacheStatsResolver.js.map +1 -0
  29. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  30. package/dist/resolvers/FileResolver.js +12 -32
  31. package/dist/resolvers/FileResolver.js.map +1 -1
  32. package/dist/resolvers/GeoResolver.d.ts +58 -0
  33. package/dist/resolvers/GeoResolver.d.ts.map +1 -0
  34. package/dist/resolvers/GeoResolver.js +302 -0
  35. package/dist/resolvers/GeoResolver.js.map +1 -0
  36. package/dist/resolvers/RunAIAgentResolver.d.ts +13 -1
  37. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  38. package/dist/resolvers/RunAIAgentResolver.js +115 -20
  39. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  40. package/dist/resolvers/SearchKnowledgeResolver.d.ts +21 -80
  41. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  42. package/dist/resolvers/SearchKnowledgeResolver.js +129 -604
  43. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  44. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts +19 -0
  45. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -0
  46. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +149 -0
  47. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -0
  48. package/package.json +66 -63
  49. package/src/__tests__/search-knowledge-tags.test.ts +177 -337
  50. package/src/__tests__/skip-sdk-organic-keys.test.ts +274 -0
  51. package/src/agents/skip-sdk.ts +83 -2
  52. package/src/config.ts +24 -0
  53. package/src/generated/generated.ts +1902 -1
  54. package/src/index.ts +18 -2
  55. package/src/resolvers/ArtifactFileResolver.ts +71 -0
  56. package/src/resolvers/AutotagPipelineResolver.ts +118 -4
  57. package/src/resolvers/CacheStatsResolver.ts +142 -0
  58. package/src/resolvers/FileResolver.ts +12 -41
  59. package/src/resolvers/GeoResolver.ts +258 -0
  60. package/src/resolvers/RunAIAgentResolver.ts +137 -23
  61. package/src/resolvers/SearchKnowledgeResolver.ts +114 -715
  62. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +138 -0
@@ -0,0 +1,258 @@
1
+ import { Resolver, Query, Arg, Ctx, ObjectType, Field, InputType, Float, Int } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { ResolverBase } from '../generic/ResolverBase.js';
4
+ import { RunView, LogError, Metadata, UserInfo } from '@memberjunction/core';
5
+ import { MJCountryEntity, MJStateProvinceEntity } from '@memberjunction/core-entities';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ @ObjectType()
12
+ export class GeoCountryResult {
13
+ @Field()
14
+ ID!: string;
15
+
16
+ @Field()
17
+ Name!: string;
18
+
19
+ @Field()
20
+ ISO2!: string;
21
+
22
+ @Field()
23
+ ISO3!: string;
24
+
25
+ @Field(() => Float, { nullable: true })
26
+ Latitude!: number | null;
27
+
28
+ @Field(() => Float, { nullable: true })
29
+ Longitude!: number | null;
30
+ }
31
+
32
+ @ObjectType()
33
+ export class GeoStateProvinceResult {
34
+ @Field()
35
+ ID!: string;
36
+
37
+ @Field()
38
+ CountryID!: string;
39
+
40
+ @Field()
41
+ Name!: string;
42
+
43
+ @Field()
44
+ Code!: string;
45
+
46
+ @Field()
47
+ ISO3166_2!: string;
48
+
49
+ @Field(() => Float, { nullable: true })
50
+ Latitude!: number | null;
51
+
52
+ @Field(() => Float, { nullable: true })
53
+ Longitude!: number | null;
54
+ }
55
+
56
+ @ObjectType()
57
+ export class GeoResolveResult {
58
+ @Field()
59
+ Success!: boolean;
60
+
61
+ @Field({ nullable: true })
62
+ CountryID?: string;
63
+
64
+ @Field({ nullable: true })
65
+ CountryName?: string;
66
+
67
+ @Field({ nullable: true })
68
+ StateProvinceID?: string;
69
+
70
+ @Field({ nullable: true })
71
+ StateProvinceName?: string;
72
+
73
+ @Field(() => Float, { nullable: true })
74
+ Latitude?: number;
75
+
76
+ @Field(() => Float, { nullable: true })
77
+ Longitude?: number;
78
+
79
+ @Field({ nullable: true })
80
+ ErrorMessage?: string;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Resolver
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * GraphQL resolver for geographic reference data resolution.
89
+ * Provides country/state text-to-reference matching via the GeoResolver service.
90
+ * Used by clients that need to resolve free-text location strings to structured
91
+ * reference data (Country/StateProvince IDs, centroids) without a full geocoding API call.
92
+ */
93
+ @Resolver()
94
+ export class GeoResolver extends ResolverBase {
95
+ private _countries: MJCountryEntity[] | null = null;
96
+ private _states: MJStateProvinceEntity[] | null = null;
97
+
98
+ /**
99
+ * Lazily load all countries into memory (~250 records).
100
+ */
101
+ private async GetCountries(contextUser: UserInfo | undefined): Promise<MJCountryEntity[]> {
102
+ if (this._countries) return this._countries;
103
+ const rv = new RunView();
104
+ const result = await rv.RunView<MJCountryEntity>({
105
+ EntityName: 'MJ: Countries',
106
+ ResultType: 'entity_object'
107
+ }, contextUser);
108
+ if (result.Success) {
109
+ this._countries = result.Results;
110
+ }
111
+ return this._countries ?? [];
112
+ }
113
+
114
+ /**
115
+ * Lazily load all state/provinces into memory (~5000 records).
116
+ */
117
+ private async GetStates(contextUser: UserInfo | undefined): Promise<MJStateProvinceEntity[]> {
118
+ if (this._states) return this._states;
119
+ const rv = new RunView();
120
+ const result = await rv.RunView<MJStateProvinceEntity>({
121
+ EntityName: 'MJ: State Provinces',
122
+ ResultType: 'entity_object'
123
+ }, contextUser);
124
+ if (result.Success) {
125
+ this._states = result.Results;
126
+ }
127
+ return this._states ?? [];
128
+ }
129
+
130
+ /**
131
+ * Resolve a free-text country string to a Country reference record.
132
+ * Matches by Name, ISO2, ISO3, or CommonAliases.
133
+ */
134
+ @Query(() => GeoResolveResult)
135
+ async ResolveCountry(
136
+ @Arg('input', () => String) input: string,
137
+ @Ctx() { userPayload }: AppContext
138
+ ): Promise<GeoResolveResult> {
139
+ try {
140
+ const user = this.GetUserFromPayload(userPayload);
141
+ const countries = await this.GetCountries(user);
142
+ const normalized = input.trim().toLowerCase();
143
+
144
+ // 1. Exact match on Name, ISO2, ISO3
145
+ const exact = countries.find(c =>
146
+ c.Name.toLowerCase() === normalized ||
147
+ c.ISO2.toLowerCase() === normalized ||
148
+ c.ISO3.toLowerCase() === normalized
149
+ );
150
+ if (exact) {
151
+ return {
152
+ Success: true,
153
+ CountryID: exact.ID,
154
+ CountryName: exact.Name,
155
+ Latitude: exact.Latitude ?? undefined,
156
+ Longitude: exact.Longitude ?? undefined
157
+ };
158
+ }
159
+
160
+ // 2. CommonAliases search
161
+ const aliasMatch = countries.find(c => {
162
+ if (!c.CommonAliases) return false;
163
+ try {
164
+ const aliases: string[] = JSON.parse(c.CommonAliases);
165
+ return aliases.some(a => a.toLowerCase() === normalized);
166
+ } catch {
167
+ return false;
168
+ }
169
+ });
170
+ if (aliasMatch) {
171
+ return {
172
+ Success: true,
173
+ CountryID: aliasMatch.ID,
174
+ CountryName: aliasMatch.Name,
175
+ Latitude: aliasMatch.Latitude ?? undefined,
176
+ Longitude: aliasMatch.Longitude ?? undefined
177
+ };
178
+ }
179
+
180
+ return { Success: false, ErrorMessage: `No country match for "${input}"` };
181
+ } catch (e: unknown) {
182
+ const msg = e instanceof Error ? e.message : String(e);
183
+ LogError(`GeoResolver.ResolveCountry error: ${msg}`);
184
+ return { Success: false, ErrorMessage: msg };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Resolve a free-text state/province string with country context.
190
+ * Country context is critical: "CA" = California (US) vs Canada (ISO2).
191
+ */
192
+ @Query(() => GeoResolveResult)
193
+ async ResolveStateProvince(
194
+ @Arg('stateInput', () => String) stateInput: string,
195
+ @Arg('countryInput', () => String) countryInput: string,
196
+ @Ctx() { userPayload }: AppContext
197
+ ): Promise<GeoResolveResult> {
198
+ try {
199
+ const user = this.GetUserFromPayload(userPayload);
200
+
201
+ // First resolve country
202
+ const countryResult = await this.ResolveCountry(countryInput, { userPayload } as AppContext);
203
+ if (!countryResult.Success || !countryResult.CountryID) {
204
+ return { Success: false, ErrorMessage: `Could not resolve country "${countryInput}"` };
205
+ }
206
+
207
+ const states = await this.GetStates(user);
208
+ const countryStates = states.filter(s => s.CountryID === countryResult.CountryID);
209
+ const normalized = stateInput.trim().toLowerCase();
210
+
211
+ // 1. Exact match on Name, Code, ISO3166_2
212
+ const exact = countryStates.find(s =>
213
+ s.Name.toLowerCase() === normalized ||
214
+ s.Code.toLowerCase() === normalized ||
215
+ s.ISO3166_2.toLowerCase() === normalized
216
+ );
217
+ if (exact) {
218
+ return {
219
+ Success: true,
220
+ CountryID: countryResult.CountryID,
221
+ CountryName: countryResult.CountryName,
222
+ StateProvinceID: exact.ID,
223
+ StateProvinceName: exact.Name,
224
+ Latitude: exact.Latitude ?? undefined,
225
+ Longitude: exact.Longitude ?? undefined
226
+ };
227
+ }
228
+
229
+ // 2. CommonAliases search
230
+ const aliasMatch = countryStates.find(s => {
231
+ if (!s.CommonAliases) return false;
232
+ try {
233
+ const aliases: string[] = JSON.parse(s.CommonAliases);
234
+ return aliases.some(a => a.toLowerCase() === normalized);
235
+ } catch {
236
+ return false;
237
+ }
238
+ });
239
+ if (aliasMatch) {
240
+ return {
241
+ Success: true,
242
+ CountryID: countryResult.CountryID,
243
+ CountryName: countryResult.CountryName,
244
+ StateProvinceID: aliasMatch.ID,
245
+ StateProvinceName: aliasMatch.Name,
246
+ Latitude: aliasMatch.Latitude ?? undefined,
247
+ Longitude: aliasMatch.Longitude ?? undefined
248
+ };
249
+ }
250
+
251
+ return { Success: false, ErrorMessage: `No state/province match for "${stateInput}" in ${countryResult.CountryName}` };
252
+ } catch (e: unknown) {
253
+ const msg = e instanceof Error ? e.message : String(e);
254
+ LogError(`GeoResolver.ResolveStateProvince error: ${msg}`);
255
+ return { Success: false, ErrorMessage: msg };
256
+ }
257
+ }
258
+ }
@@ -1,17 +1,17 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID, Int } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
- import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
- import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
3
+ import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo, IMetadataProvider } from '@memberjunction/core';
4
+ import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJConversationDetailArtifactEntity, MJArtifactVersionEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
5
5
  import { AgentRunner } from '@memberjunction/ai-agents';
6
6
  import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
8
- import { ChatMessage } from '@memberjunction/ai';
8
+ import { ChatMessage, ChatMessageContent } from '@memberjunction/ai';
9
9
  import { ResolverBase } from '../generic/ResolverBase.js';
10
10
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
11
11
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
12
12
  import { GetReadWriteProvider } from '../util.js';
13
13
  import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
14
- import { getAttachmentService } from '@memberjunction/aiengine';
14
+ import { GetAttachmentService } from '@memberjunction/aiengine';
15
15
  import { NotificationEngine } from '@memberjunction/notifications';
16
16
 
17
17
  @ObjectType()
@@ -453,7 +453,7 @@ export class RunAIAgentResolver extends ResolverBase {
453
453
 
454
454
  if (lastRunId && result.agentRun?.ID) {
455
455
  postExecutionOps.push(
456
- this.syncFeedbackRequestFromConversation(lastRunId, result.agentRun.ID, userMessage, currentUser)
456
+ this.syncFeedbackRequestFromConversation(lastRunId, result.agentRun.ID, userMessage, currentUser, p)
457
457
  );
458
458
  }
459
459
 
@@ -797,10 +797,11 @@ export class RunAIAgentResolver extends ResolverBase {
797
797
  lastRunId: string,
798
798
  newRunId: string,
799
799
  userMessage: string | undefined,
800
- contextUser: UserInfo
800
+ contextUser: UserInfo,
801
+ provider: IMetadataProvider
801
802
  ): Promise<void> {
802
803
  try {
803
- const rv = new RunView();
804
+ const rv = RunView.FromMetadataProvider(provider);
804
805
  const result = await rv.RunView<MJAIAgentRequestEntity>({
805
806
  EntityName: 'MJ: AI Agent Requests',
806
807
  ExtraFilter: `OriginatingAgentRunID='${lastRunId}' AND Status='Requested'`,
@@ -939,8 +940,7 @@ export class RunAIAgentResolver extends ResolverBase {
939
940
  // times: once in loadConversationHistoryWithAttachments (just to get conversationId),
940
941
  // once in AgentRunner (same reason), and once in createCompletionNotification. Now we
941
942
  // load it a single time and thread conversationId through the call chain.
942
- const md = new Metadata();
943
- const currentDetail = await md.GetEntityObject<MJConversationDetailEntity>(
943
+ const currentDetail = await p.GetEntityObject<MJConversationDetailEntity>(
944
944
  'MJ: Conversation Details',
945
945
  currentUser
946
946
  );
@@ -953,7 +953,8 @@ export class RunAIAgentResolver extends ResolverBase {
953
953
  const messages = await this.loadConversationHistoryWithAttachments(
954
954
  conversationId,
955
955
  currentUser,
956
- maxHistoryMessages || 20
956
+ maxHistoryMessages || 20,
957
+ p
957
958
  );
958
959
 
959
960
  // Convert to JSON string for the existing executeAIAgent method
@@ -1031,8 +1032,8 @@ export class RunAIAgentResolver extends ResolverBase {
1031
1032
  throw new Error('Unable to determine current user');
1032
1033
  }
1033
1034
 
1034
- const md = new Metadata();
1035
- const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1035
+ const p = GetReadWriteProvider(providers);
1036
+ const request = await p.GetEntityObject<MJAIAgentRequestEntity>(
1036
1037
  'MJ: AI Agent Requests',
1037
1038
  currentUser
1038
1039
  );
@@ -1096,7 +1097,7 @@ export class RunAIAgentResolver extends ResolverBase {
1096
1097
  async ReassignAgentRequest(
1097
1098
  @Arg('requestId') requestId: string,
1098
1099
  @Arg('newUserID') newUserID: string,
1099
- @Ctx() { userPayload }: AppContext,
1100
+ @Ctx() { userPayload, providers }: AppContext,
1100
1101
  @Arg('note', { nullable: true }) note?: string
1101
1102
  ): Promise<AIAgentRunResult> {
1102
1103
  const startTime = Date.now();
@@ -1106,8 +1107,8 @@ export class RunAIAgentResolver extends ResolverBase {
1106
1107
  throw new Error('Unable to determine current user');
1107
1108
  }
1108
1109
 
1109
- const md = new Metadata();
1110
- const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1110
+ const p = GetReadWriteProvider(providers);
1111
+ const request = await p.GetEntityObject<MJAIAgentRequestEntity>(
1111
1112
  'MJ: AI Agent Requests',
1112
1113
  currentUser
1113
1114
  );
@@ -1262,10 +1263,11 @@ export class RunAIAgentResolver extends ResolverBase {
1262
1263
  private async loadConversationHistoryWithAttachments(
1263
1264
  conversationId: string,
1264
1265
  contextUser: UserInfo,
1265
- maxMessages: number
1266
+ maxMessages: number,
1267
+ provider: IMetadataProvider
1266
1268
  ): Promise<ChatMessage[]> {
1267
- const rv = new RunView();
1268
- const attachmentService = getAttachmentService();
1269
+ const rv = RunView.FromMetadataProvider(provider);
1270
+ const attachmentService = GetAttachmentService();
1269
1271
 
1270
1272
  // Load recent conversation details (messages) for this conversation.
1271
1273
  // Only fetch the three fields we actually use — ID for attachment lookups,
@@ -1290,9 +1292,12 @@ export class RunAIAgentResolver extends ResolverBase {
1290
1292
  const messageIds = details.map(d => d.ID);
1291
1293
 
1292
1294
  // Batch load all attachments for these messages
1293
- const attachmentsByDetailId = await attachmentService.getAttachmentsBatch(messageIds, contextUser);
1295
+ const attachmentsByDetailId = await attachmentService.GetAttachmentsBatch(messageIds, contextUser, provider);
1296
+
1297
+ // Batch load input artifacts for these messages
1298
+ const inputArtifactsByDetailId = await this.loadInputArtifactsBatch(messageIds, contextUser, provider);
1294
1299
 
1295
- // Build ChatMessage array with attachments
1300
+ // Build ChatMessage array with attachments and input artifacts
1296
1301
  const messages: ChatMessage[] = [];
1297
1302
 
1298
1303
  for (const detail of details) {
@@ -1301,7 +1306,7 @@ export class RunAIAgentResolver extends ResolverBase {
1301
1306
 
1302
1307
  // Get attachment data with content URLs (handles both inline and FileID storage)
1303
1308
  const attachmentDataPromises = attachments.map(att =>
1304
- attachmentService.getAttachmentData(att, contextUser)
1309
+ attachmentService.GetAttachmentData(att, contextUser, provider)
1305
1310
  );
1306
1311
  const attachmentDataResults = await Promise.all(attachmentDataPromises);
1307
1312
 
@@ -1319,12 +1324,38 @@ export class RunAIAgentResolver extends ResolverBase {
1319
1324
  content: result.contentUrl
1320
1325
  }));
1321
1326
 
1327
+ // Get input artifacts for this message and convert to AttachmentData
1328
+ const inputArtifacts = inputArtifactsByDetailId.get(detail.ID) || [];
1329
+ for (const artifactVersion of inputArtifacts) {
1330
+ if (artifactVersion.ContentMode === 'File' && artifactVersion.FileID) {
1331
+ // File-backed artifact — download content and treat like a document attachment
1332
+ const fileContent = await this.downloadArtifactFileContent(artifactVersion, contextUser, provider);
1333
+ if (fileContent) {
1334
+ validAttachments.push({
1335
+ type: ConversationUtility.GetAttachmentTypeFromMime(artifactVersion.MimeType || ''),
1336
+ mimeType: artifactVersion.MimeType || 'application/octet-stream',
1337
+ fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
1338
+ sizeBytes: artifactVersion.ContentSizeBytes || undefined,
1339
+ content: fileContent
1340
+ });
1341
+ }
1342
+ } else if (artifactVersion.Content) {
1343
+ // Text artifact — include content directly as a text attachment
1344
+ validAttachments.push({
1345
+ type: 'Document' as AttachmentData['type'],
1346
+ mimeType: 'text/plain',
1347
+ fileName: artifactVersion.Name || 'artifact.txt',
1348
+ content: `[Artifact: ${artifactVersion.Name || 'Untitled'}]\n\n${artifactVersion.Content}`
1349
+ });
1350
+ }
1351
+ }
1352
+
1322
1353
  // Build message content (with or without attachments)
1323
- let content: string | ReturnType<typeof ConversationUtility.BuildChatMessageContent>;
1354
+ let content: ChatMessageContent;
1324
1355
 
1325
1356
  if (validAttachments.length > 0) {
1326
1357
  // Use ConversationUtility to build multimodal content blocks
1327
- content = ConversationUtility.BuildChatMessageContent(
1358
+ content = await ConversationUtility.BuildChatMessageContent(
1328
1359
  detail.Message || '',
1329
1360
  validAttachments
1330
1361
  );
@@ -1352,4 +1383,87 @@ export class RunAIAgentResolver extends ResolverBase {
1352
1383
  return 'user'; // Default to user
1353
1384
  }
1354
1385
 
1386
+ /**
1387
+ * Batch load input artifact versions for conversation details.
1388
+ * Returns a map of ConversationDetailID -> ArtifactVersion[]
1389
+ */
1390
+ private async loadInputArtifactsBatch(
1391
+ conversationDetailIds: string[],
1392
+ contextUser: UserInfo,
1393
+ provider: IMetadataProvider
1394
+ ): Promise<Map<string, MJArtifactVersionEntity[]>> {
1395
+ const map = new Map<string, MJArtifactVersionEntity[]>();
1396
+ if (conversationDetailIds.length === 0) return map;
1397
+
1398
+ const rv = RunView.FromMetadataProvider(provider);
1399
+ const idList = conversationDetailIds.map(id => `'${id}'`).join(',');
1400
+
1401
+ // Load ConversationDetailArtifact links with Direction='Input'
1402
+ const linksResult = await rv.RunView<MJConversationDetailArtifactEntity>({
1403
+ EntityName: 'MJ: Conversation Detail Artifacts',
1404
+ ExtraFilter: `ConversationDetailID IN (${idList}) AND Direction = 'Input'`,
1405
+ ResultType: 'entity_object'
1406
+ }, contextUser);
1407
+
1408
+ if (!linksResult.Success || !linksResult.Results || linksResult.Results.length === 0) {
1409
+ return map;
1410
+ }
1411
+
1412
+ // Load the referenced artifact versions
1413
+ const versionIds = linksResult.Results.map(l => `'${l.ArtifactVersionID}'`).join(',');
1414
+ const versionsResult = await rv.RunView<MJArtifactVersionEntity>({
1415
+ EntityName: 'MJ: Artifact Versions',
1416
+ ExtraFilter: `ID IN (${versionIds})`,
1417
+ ResultType: 'entity_object'
1418
+ }, contextUser);
1419
+
1420
+ if (!versionsResult.Success || !versionsResult.Results) return map;
1421
+
1422
+ // Build a lookup of version ID -> version entity
1423
+ const versionMap = new Map<string, MJArtifactVersionEntity>();
1424
+ for (const v of versionsResult.Results) {
1425
+ versionMap.set(v.ID, v);
1426
+ }
1427
+
1428
+ // Group by conversation detail ID
1429
+ for (const link of linksResult.Results) {
1430
+ const version = versionMap.get(link.ArtifactVersionID);
1431
+ if (version) {
1432
+ const existing = map.get(link.ConversationDetailID) || [];
1433
+ existing.push(version);
1434
+ map.set(link.ConversationDetailID, existing);
1435
+ }
1436
+ }
1437
+
1438
+ return map;
1439
+ }
1440
+
1441
+ /**
1442
+ * Download file content from an artifact version's FileID.
1443
+ * Returns base64 data URL for extraction pipeline compatibility.
1444
+ * Uses the same downloadFileContent path as ConversationAttachmentService
1445
+ * to avoid Box driver path resolution issues.
1446
+ */
1447
+ private async downloadArtifactFileContent(
1448
+ artifactVersion: MJArtifactVersionEntity,
1449
+ contextUser: UserInfo,
1450
+ provider: IMetadataProvider
1451
+ ): Promise<string | null> {
1452
+ if (!artifactVersion.FileID) return null;
1453
+
1454
+ try {
1455
+ // Use the attachment service's downloadFileContent which uses GetObject directly
1456
+ const attachmentService = GetAttachmentService();
1457
+ const buffer = await attachmentService.DownloadFileContent(artifactVersion.FileID, contextUser, provider);
1458
+ if (!buffer) return null;
1459
+
1460
+ const base64 = buffer.toString('base64');
1461
+ const mimeType = artifactVersion.MimeType || 'application/octet-stream';
1462
+ return `data:${mimeType};base64,${base64}`;
1463
+ } catch (err) {
1464
+ LogError(`Failed to download artifact file ${artifactVersion.FileID}: ${err}`);
1465
+ return null;
1466
+ }
1467
+ }
1468
+
1355
1469
  }