@memberjunction/server 5.23.0 → 5.25.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 (63) 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.map +1 -1
  6. package/dist/config.js +11 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/generated/generated.d.ts +954 -0
  9. package/dist/generated/generated.d.ts.map +1 -1
  10. package/dist/generated/generated.js +26108 -20749
  11. package/dist/generated/generated.js.map +1 -1
  12. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  13. package/dist/generic/RunViewResolver.js.map +1 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/resolvers/ArtifactFileResolver.d.ts +15 -0
  19. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -0
  20. package/dist/resolvers/ArtifactFileResolver.js +74 -0
  21. package/dist/resolvers/ArtifactFileResolver.js.map +1 -0
  22. package/dist/resolvers/AutotagPipelineResolver.d.ts +23 -1
  23. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  24. package/dist/resolvers/AutotagPipelineResolver.js +197 -13
  25. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  26. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  27. package/dist/resolvers/FetchEntityVectorsResolver.js +6 -2
  28. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  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 +34 -1
  37. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  38. package/dist/resolvers/RunAIAgentResolver.js +183 -48
  39. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  40. package/dist/resolvers/SearchKnowledgeResolver.d.ts +23 -41
  41. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  42. package/dist/resolvers/SearchKnowledgeResolver.js +133 -382
  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 +63 -63
  49. package/src/__tests__/search-knowledge-tags.test.ts +255 -0
  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 +11 -0
  53. package/src/generated/generated.ts +3690 -1
  54. package/src/generic/RunViewResolver.ts +1 -0
  55. package/src/index.ts +2 -0
  56. package/src/resolvers/ArtifactFileResolver.ts +71 -0
  57. package/src/resolvers/AutotagPipelineResolver.ts +213 -10
  58. package/src/resolvers/FetchEntityVectorsResolver.ts +6 -2
  59. package/src/resolvers/FileResolver.ts +12 -41
  60. package/src/resolvers/GeoResolver.ts +258 -0
  61. package/src/resolvers/RunAIAgentResolver.ts +229 -76
  62. package/src/resolvers/SearchKnowledgeResolver.ts +118 -462
  63. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +138 -0
@@ -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()
@@ -361,7 +361,9 @@ export class RunAIAgentResolver extends ResolverBase {
361
361
  createArtifacts: boolean = false,
362
362
  createNotification: boolean = false,
363
363
  sourceArtifactId?: string,
364
- sourceArtifactVersionId?: string
364
+ sourceArtifactVersionId?: string,
365
+ /** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
366
+ conversationId?: string
365
367
  ): Promise<AIAgentRunResult> {
366
368
  const startTime = Date.now();
367
369
 
@@ -418,6 +420,7 @@ export class RunAIAgentResolver extends ResolverBase {
418
420
  }
419
421
  }, {
420
422
  conversationDetailId: conversationDetailId, // Use existing if provided
423
+ conversationId: conversationId, // LATENCY OPT #2: pre-resolved to skip redundant load in AgentRunner
421
424
  userMessage: userMessage, // Provide user message when conversationDetailId not provided
422
425
  createArtifacts: createArtifacts || false,
423
426
  sourceArtifactId: sourceArtifactId
@@ -435,37 +438,53 @@ export class RunAIAgentResolver extends ResolverBase {
435
438
 
436
439
  const executionTime = Date.now() - startTime;
437
440
 
438
- // Sync feedback request if this is a continuation run (user responded via conversation)
441
+ // LATENCY OPTIMIZATION (Opt #6): These three post-execution operations are independent
442
+ // of each other — none reads the output of another. Previously they ran sequentially,
443
+ // adding their latencies together (~50ms total). Now they run in parallel via Promise.all,
444
+ // so we only pay the cost of the slowest one.
445
+ //
446
+ // 1. syncFeedbackRequestFromConversation — links a prior Chat-step feedback request to
447
+ // the new agent run so the conversation thread stays coherent.
448
+ // 2. sendFeedbackRequestNotification — sends an in-app/email/SMS notification when the
449
+ // agent paused for human input (Chat step).
450
+ // 3. createCompletionNotification — sends an in-app/email/SMS notification that the
451
+ // agent finished and created an artifact.
452
+ const postExecutionOps: Promise<void>[] = [];
453
+
439
454
  if (lastRunId && result.agentRun?.ID) {
440
- await this.syncFeedbackRequestFromConversation(
441
- lastRunId,
442
- result.agentRun.ID,
443
- userMessage,
444
- currentUser
455
+ postExecutionOps.push(
456
+ this.syncFeedbackRequestFromConversation(lastRunId, result.agentRun.ID, userMessage, currentUser, p)
445
457
  );
446
458
  }
447
459
 
448
- // Send notification if agent created a feedback request (Chat step)
449
460
  if (result.feedbackRequestId) {
450
- await this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload);
461
+ postExecutionOps.push(
462
+ this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload)
463
+ );
451
464
  }
452
465
 
453
- // Create notification if enabled and artifact was created successfully
454
466
  if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
455
- await this.createCompletionNotification(
456
- result.agentRun,
457
- {
458
- artifactId: artifactInfo.artifactId,
459
- versionId: artifactInfo.versionId,
460
- versionNumber: artifactInfo.versionNumber
461
- },
462
- finalConversationDetailId,
463
- currentUser,
464
- pubSub,
465
- userPayload
467
+ postExecutionOps.push(
468
+ this.createCompletionNotification(
469
+ result.agentRun,
470
+ {
471
+ artifactId: artifactInfo.artifactId,
472
+ versionId: artifactInfo.versionId,
473
+ versionNumber: artifactInfo.versionNumber
474
+ },
475
+ conversationResult.conversationId,
476
+ finalConversationDetailId,
477
+ currentUser,
478
+ pubSub,
479
+ userPayload
480
+ )
466
481
  );
467
482
  }
468
483
 
484
+ if (postExecutionOps.length > 0) {
485
+ await Promise.all(postExecutionOps);
486
+ }
487
+
469
488
  // Create sanitized payload for JSON serialization
470
489
  const sanitizedResult = this.sanitizeAgentResult(result);
471
490
  const returnResult = JSON.stringify(sanitizedResult);
@@ -680,34 +699,31 @@ export class RunAIAgentResolver extends ResolverBase {
680
699
  * Create a user notification for agent completion with artifact
681
700
  * Notification includes navigation link back to the conversation
682
701
  */
702
+ /**
703
+ * LATENCY OPTIMIZATION (Opt #2): Now accepts conversationId directly instead of
704
+ * conversationDetailId. Previously this method loaded a ConversationDetail entity
705
+ * from the DB solely to extract its ConversationID field for building a URL — a
706
+ * redundant ~50ms DB round-trip since the caller already resolved conversationId
707
+ * when loading conversation history.
708
+ */
683
709
  private async createCompletionNotification(
684
710
  agentRun: MJAIAgentRunEntityExtended,
685
711
  artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
712
+ conversationId: string,
686
713
  conversationDetailId: string,
687
714
  contextUser: UserInfo,
688
715
  pubSub: PubSubEngine,
689
716
  userPayload: UserPayload
690
717
  ): Promise<void> {
691
718
  try {
692
- const md = new Metadata();
693
-
694
719
  // Get agent info for notification message
695
720
  await AIEngine.Instance.Config(false, contextUser);
696
721
  const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, agentRun.AgentID));
697
722
  const agentName = agent?.Name || 'Agent';
698
723
 
699
- // Load conversation detail to get conversation info
700
- const detail = await md.GetEntityObject<MJConversationDetailEntity>(
701
- 'MJ: Conversation Details',
702
- contextUser
703
- );
704
- if (!(await detail.Load(conversationDetailId))) {
705
- throw new Error(`Failed to load conversation detail ${conversationDetailId}`);
706
- }
707
-
708
724
  // Build conversation URL for email/SMS templates
709
725
  const baseUrl = process.env.APP_BASE_URL || 'http://localhost:4201';
710
- const conversationUrl = `${baseUrl}/conversations/${detail.ConversationID}?artifact=${artifactInfo.artifactId}`;
726
+ const conversationUrl = `${baseUrl}/conversations/${conversationId}?artifact=${artifactInfo.artifactId}`;
711
727
 
712
728
  // Craft message based on versioning
713
729
  const message = artifactInfo.versionNumber > 1
@@ -724,7 +740,7 @@ export class RunAIAgentResolver extends ResolverBase {
724
740
  message: message,
725
741
  resourceConfiguration: {
726
742
  type: 'conversation',
727
- conversationId: detail.ConversationID,
743
+ conversationId: conversationId,
728
744
  messageId: conversationDetailId,
729
745
  artifactId: artifactInfo.artifactId,
730
746
  versionId: artifactInfo.versionId,
@@ -755,7 +771,7 @@ export class RunAIAgentResolver extends ResolverBase {
755
771
  action: 'create',
756
772
  title: `${agentName} completed your request`,
757
773
  message: message,
758
- conversationId: detail.ConversationID
774
+ conversationId: conversationId
759
775
  })
760
776
  });
761
777
 
@@ -781,10 +797,11 @@ export class RunAIAgentResolver extends ResolverBase {
781
797
  lastRunId: string,
782
798
  newRunId: string,
783
799
  userMessage: string | undefined,
784
- contextUser: UserInfo
800
+ contextUser: UserInfo,
801
+ provider: IMetadataProvider
785
802
  ): Promise<void> {
786
803
  try {
787
- const rv = new RunView();
804
+ const rv = RunView.FromMetadataProvider(provider);
788
805
  const result = await rv.RunView<MJAIAgentRequestEntity>({
789
806
  EntityName: 'MJ: AI Agent Requests',
790
807
  ExtraFilter: `OriginatingAgentRunID='${lastRunId}' AND Status='Requested'`,
@@ -918,11 +935,26 @@ export class RunAIAgentResolver extends ResolverBase {
918
935
  }
919
936
 
920
937
  try {
938
+ // LATENCY OPTIMIZATION (Opt #2 + #3): Load ConversationDetail once here to extract
939
+ // conversationId, then pass it downstream. Previously this record was loaded multiple
940
+ // times: once in loadConversationHistoryWithAttachments (just to get conversationId),
941
+ // once in AgentRunner (same reason), and once in createCompletionNotification. Now we
942
+ // load it a single time and thread conversationId through the call chain.
943
+ const currentDetail = await p.GetEntityObject<MJConversationDetailEntity>(
944
+ 'MJ: Conversation Details',
945
+ currentUser
946
+ );
947
+ if (!await currentDetail.Load(conversationDetailId)) {
948
+ throw new Error(`Conversation detail ${conversationDetailId} not found`);
949
+ }
950
+ const conversationId = currentDetail.ConversationID;
951
+
921
952
  // Load conversation history with attachments from DB
922
953
  const messages = await this.loadConversationHistoryWithAttachments(
923
- conversationDetailId,
954
+ conversationId,
924
955
  currentUser,
925
- maxHistoryMessages || 20
956
+ maxHistoryMessages || 20,
957
+ p
926
958
  );
927
959
 
928
960
  // Convert to JSON string for the existing executeAIAgent method
@@ -935,7 +967,7 @@ export class RunAIAgentResolver extends ResolverBase {
935
967
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
936
968
  data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
937
969
  conversationDetailId, createArtifacts || false, createNotification || false,
938
- sourceArtifactId, sourceArtifactVersionId
970
+ sourceArtifactId, sourceArtifactVersionId, conversationId
939
971
  );
940
972
 
941
973
  LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
@@ -965,7 +997,8 @@ export class RunAIAgentResolver extends ResolverBase {
965
997
  createArtifacts || false,
966
998
  createNotification || false,
967
999
  sourceArtifactId,
968
- sourceArtifactVersionId
1000
+ sourceArtifactVersionId,
1001
+ conversationId // LATENCY OPT #2: pass pre-resolved conversationId
969
1002
  );
970
1003
  } catch (error) {
971
1004
  const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
@@ -999,8 +1032,8 @@ export class RunAIAgentResolver extends ResolverBase {
999
1032
  throw new Error('Unable to determine current user');
1000
1033
  }
1001
1034
 
1002
- const md = new Metadata();
1003
- const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1035
+ const p = GetReadWriteProvider(providers);
1036
+ const request = await p.GetEntityObject<MJAIAgentRequestEntity>(
1004
1037
  'MJ: AI Agent Requests',
1005
1038
  currentUser
1006
1039
  );
@@ -1064,7 +1097,7 @@ export class RunAIAgentResolver extends ResolverBase {
1064
1097
  async ReassignAgentRequest(
1065
1098
  @Arg('requestId') requestId: string,
1066
1099
  @Arg('newUserID') newUserID: string,
1067
- @Ctx() { userPayload }: AppContext,
1100
+ @Ctx() { userPayload, providers }: AppContext,
1068
1101
  @Arg('note', { nullable: true }) note?: string
1069
1102
  ): Promise<AIAgentRunResult> {
1070
1103
  const startTime = Date.now();
@@ -1074,8 +1107,8 @@ export class RunAIAgentResolver extends ResolverBase {
1074
1107
  throw new Error('Unable to determine current user');
1075
1108
  }
1076
1109
 
1077
- const md = new Metadata();
1078
- const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1110
+ const p = GetReadWriteProvider(providers);
1111
+ const request = await p.GetEntityObject<MJAIAgentRequestEntity>(
1079
1112
  'MJ: AI Agent Requests',
1080
1113
  currentUser
1081
1114
  );
@@ -1178,14 +1211,16 @@ export class RunAIAgentResolver extends ResolverBase {
1178
1211
  createArtifacts: boolean = false,
1179
1212
  createNotification: boolean = false,
1180
1213
  sourceArtifactId?: string,
1181
- sourceArtifactVersionId?: string
1214
+ sourceArtifactVersionId?: string,
1215
+ /** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
1216
+ conversationId?: string
1182
1217
  ): void {
1183
1218
  // Execute in background - errors are handled within, not propagated
1184
1219
  this.executeAIAgent(
1185
1220
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
1186
1221
  data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
1187
1222
  configurationId, conversationDetailId, createArtifacts, createNotification,
1188
- sourceArtifactId, sourceArtifactVersionId
1223
+ sourceArtifactId, sourceArtifactVersionId, conversationId
1189
1224
  ).catch((error: unknown) => {
1190
1225
  // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
1191
1226
  // so this would only fire for truly unexpected errors).
@@ -1210,34 +1245,40 @@ export class RunAIAgentResolver extends ResolverBase {
1210
1245
  /**
1211
1246
  * Load conversation history with attachments from database.
1212
1247
  * Builds ChatMessage[] with multimodal content blocks for attachments.
1248
+ *
1249
+ * LATENCY OPTIMIZATIONS (plans/agent-latency-optimization.md — Opts #3 and #8):
1250
+ *
1251
+ * Opt #3: This method now accepts conversationId directly instead of conversationDetailId.
1252
+ * Previously it loaded a ConversationDetail entity object just to extract its ConversationID
1253
+ * field — a redundant DB round-trip (~40ms) since the caller already has this information.
1254
+ * The caller (RunAIAgentFromConversationDetail) now loads the ConversationDetail once and
1255
+ * passes conversationId down.
1256
+ *
1257
+ * Opt #8: Switched from ResultType 'entity_object' to 'simple' with explicit Fields.
1258
+ * The history query only needs ID, Role, and Message from each ConversationDetail record.
1259
+ * Using 'entity_object' created full BaseEntity instances with getters/setters, dirty tracking,
1260
+ * and validation — none of which are needed for read-only history assembly. The 'simple' result
1261
+ * type returns plain JS objects, reducing per-record overhead (~30ms total savings).
1213
1262
  */
1214
1263
  private async loadConversationHistoryWithAttachments(
1215
- conversationDetailId: string,
1264
+ conversationId: string,
1216
1265
  contextUser: UserInfo,
1217
- maxMessages: number
1266
+ maxMessages: number,
1267
+ provider: IMetadataProvider
1218
1268
  ): Promise<ChatMessage[]> {
1219
- const md = new Metadata();
1220
- const rv = new RunView();
1221
- const attachmentService = getAttachmentService();
1222
-
1223
- // Load the current conversation detail to get the conversation ID
1224
- const currentDetail = await md.GetEntityObject<MJConversationDetailEntity>(
1225
- 'MJ: Conversation Details',
1226
- contextUser
1227
- );
1228
- if (!await currentDetail.Load(conversationDetailId)) {
1229
- throw new Error(`Conversation detail ${conversationDetailId} not found`);
1230
- }
1269
+ const rv = RunView.FromMetadataProvider(provider);
1270
+ const attachmentService = GetAttachmentService();
1231
1271
 
1232
- const conversationId = currentDetail.ConversationID;
1233
-
1234
- // Load recent conversation details (messages) for this conversation
1235
- const detailsResult = await rv.RunView<MJConversationDetailEntity>({
1272
+ // Load recent conversation details (messages) for this conversation.
1273
+ // Only fetch the three fields we actually use — ID for attachment lookups,
1274
+ // Role for message routing, Message for content.
1275
+ const detailsResult = await rv.RunView<{ ID: string; Role: string; Message: string }>({
1236
1276
  EntityName: 'MJ: Conversation Details',
1237
1277
  ExtraFilter: `ConversationID='${conversationId}'`,
1238
1278
  OrderBy: '__mj_CreatedAt DESC',
1239
1279
  MaxRows: maxMessages,
1240
- ResultType: 'entity_object'
1280
+ Fields: ['ID', 'Role', 'Message'],
1281
+ ResultType: 'simple'
1241
1282
  }, contextUser);
1242
1283
 
1243
1284
  if (!detailsResult.Success || !detailsResult.Results) {
@@ -1251,9 +1292,12 @@ export class RunAIAgentResolver extends ResolverBase {
1251
1292
  const messageIds = details.map(d => d.ID);
1252
1293
 
1253
1294
  // Batch load all attachments for these messages
1254
- const attachmentsByDetailId = await attachmentService.getAttachmentsBatch(messageIds, contextUser);
1295
+ const attachmentsByDetailId = await attachmentService.GetAttachmentsBatch(messageIds, contextUser, provider);
1255
1296
 
1256
- // Build ChatMessage array with attachments
1297
+ // Batch load input artifacts for these messages
1298
+ const inputArtifactsByDetailId = await this.loadInputArtifactsBatch(messageIds, contextUser, provider);
1299
+
1300
+ // Build ChatMessage array with attachments and input artifacts
1257
1301
  const messages: ChatMessage[] = [];
1258
1302
 
1259
1303
  for (const detail of details) {
@@ -1262,7 +1306,7 @@ export class RunAIAgentResolver extends ResolverBase {
1262
1306
 
1263
1307
  // Get attachment data with content URLs (handles both inline and FileID storage)
1264
1308
  const attachmentDataPromises = attachments.map(att =>
1265
- attachmentService.getAttachmentData(att, contextUser)
1309
+ attachmentService.GetAttachmentData(att, contextUser, provider)
1266
1310
  );
1267
1311
  const attachmentDataResults = await Promise.all(attachmentDataPromises);
1268
1312
 
@@ -1280,12 +1324,38 @@ export class RunAIAgentResolver extends ResolverBase {
1280
1324
  content: result.contentUrl
1281
1325
  }));
1282
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
+
1283
1353
  // Build message content (with or without attachments)
1284
- let content: string | ReturnType<typeof ConversationUtility.BuildChatMessageContent>;
1354
+ let content: ChatMessageContent;
1285
1355
 
1286
1356
  if (validAttachments.length > 0) {
1287
1357
  // Use ConversationUtility to build multimodal content blocks
1288
- content = ConversationUtility.BuildChatMessageContent(
1358
+ content = await ConversationUtility.BuildChatMessageContent(
1289
1359
  detail.Message || '',
1290
1360
  validAttachments
1291
1361
  );
@@ -1313,4 +1383,87 @@ export class RunAIAgentResolver extends ResolverBase {
1313
1383
  return 'user'; // Default to user
1314
1384
  }
1315
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
+
1316
1469
  }