@memberjunction/server 2.129.0 → 2.130.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.
@@ -1,15 +1,17 @@
1
- import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID } from 'type-graphql';
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, UserInfo } from '@memberjunction/core';
4
- import { ConversationDetailEntity, UserNotificationEntity } from '@memberjunction/core-entities';
3
+ import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
+ import { ConversationDetailEntity, ConversationDetailAttachmentEntity, UserNotificationEntity } from '@memberjunction/core-entities';
5
5
  import { AgentRunner } from '@memberjunction/ai-agents';
6
- import { AIAgentEntityExtended, AIAgentRunEntityExtended, ExecuteAgentResult } from '@memberjunction/ai-core-plus';
6
+ import { AIAgentEntityExtended, AIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
8
+ import { ChatMessage } from '@memberjunction/ai';
8
9
  import { ResolverBase } from '../generic/ResolverBase.js';
9
10
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
10
11
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
11
12
  import { GetReadWriteProvider } from '../util.js';
12
13
  import { SafeJSONParse } from '@memberjunction/global';
14
+ import { getAttachmentService } from '@memberjunction/aiengine';
13
15
 
14
16
  @ObjectType()
15
17
  export class AIAgentRunResult {
@@ -699,4 +701,190 @@ export class RunAIAgentResolver extends ResolverBase {
699
701
  }
700
702
  }
701
703
 
704
+ /**
705
+ * Optimized mutation that loads conversation history server-side.
706
+ * This avoids sending large attachment data from client to server.
707
+ *
708
+ * @param conversationDetailId - The conversation detail ID (user's message already saved)
709
+ * @param agentId - The agent to execute
710
+ * @param maxHistoryMessages - Maximum number of history messages to include (default: 20)
711
+ */
712
+ @Mutation(() => AIAgentRunResult)
713
+ async RunAIAgentFromConversationDetail(
714
+ @Arg('conversationDetailId') conversationDetailId: string,
715
+ @Arg('agentId') agentId: string,
716
+ @Ctx() { userPayload, providers, dataSource }: AppContext,
717
+ @Arg('sessionId') sessionId: string,
718
+ @PubSub() pubSub: PubSubEngine,
719
+ @Arg('maxHistoryMessages', () => Int, { nullable: true }) maxHistoryMessages?: number,
720
+ @Arg('data', { nullable: true }) data?: string,
721
+ @Arg('payload', { nullable: true }) payload?: string,
722
+ @Arg('lastRunId', { nullable: true }) lastRunId?: string,
723
+ @Arg('autoPopulateLastRunPayload', { nullable: true }) autoPopulateLastRunPayload?: boolean,
724
+ @Arg('configurationId', { nullable: true }) configurationId?: string,
725
+ @Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
726
+ @Arg('createNotification', { nullable: true }) createNotification?: boolean,
727
+ @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
728
+ @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
729
+ ): Promise<AIAgentRunResult> {
730
+ const p = GetReadWriteProvider(providers);
731
+ const currentUser = this.GetUserFromPayload(userPayload);
732
+
733
+ if (!currentUser) {
734
+ return {
735
+ success: false,
736
+ errorMessage: 'Unable to determine current user',
737
+ result: JSON.stringify({ success: false, errorMessage: 'Unable to determine current user' })
738
+ };
739
+ }
740
+
741
+ try {
742
+ // Load conversation history with attachments from DB
743
+ const messages = await this.loadConversationHistoryWithAttachments(
744
+ conversationDetailId,
745
+ currentUser,
746
+ maxHistoryMessages || 20
747
+ );
748
+
749
+ // Convert to JSON string for the existing executeAIAgent method
750
+ const messagesJson = JSON.stringify(messages);
751
+
752
+ // Delegate to existing implementation
753
+ return this.executeAIAgent(
754
+ p,
755
+ dataSource,
756
+ agentId,
757
+ userPayload,
758
+ messagesJson,
759
+ sessionId,
760
+ pubSub,
761
+ data,
762
+ payload,
763
+ undefined, // templateData
764
+ lastRunId,
765
+ autoPopulateLastRunPayload,
766
+ configurationId,
767
+ conversationDetailId,
768
+ createArtifacts || false,
769
+ createNotification || false,
770
+ sourceArtifactId,
771
+ sourceArtifactVersionId
772
+ );
773
+ } catch (error) {
774
+ const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
775
+ LogError(`RunAIAgentFromConversationDetail failed: ${errorMessage}`, undefined, error);
776
+ return {
777
+ success: false,
778
+ errorMessage,
779
+ result: JSON.stringify({ success: false, errorMessage })
780
+ };
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Load conversation history with attachments from database.
786
+ * Builds ChatMessage[] with multimodal content blocks for attachments.
787
+ */
788
+ private async loadConversationHistoryWithAttachments(
789
+ conversationDetailId: string,
790
+ contextUser: UserInfo,
791
+ maxMessages: number
792
+ ): Promise<ChatMessage[]> {
793
+ const md = new Metadata();
794
+ const rv = new RunView();
795
+ const attachmentService = getAttachmentService();
796
+
797
+ // Load the current conversation detail to get the conversation ID
798
+ const currentDetail = await md.GetEntityObject<ConversationDetailEntity>(
799
+ 'Conversation Details',
800
+ contextUser
801
+ );
802
+ if (!await currentDetail.Load(conversationDetailId)) {
803
+ throw new Error(`Conversation detail ${conversationDetailId} not found`);
804
+ }
805
+
806
+ const conversationId = currentDetail.ConversationID;
807
+
808
+ // Load recent conversation details (messages) for this conversation
809
+ const detailsResult = await rv.RunView<ConversationDetailEntity>({
810
+ EntityName: 'Conversation Details',
811
+ ExtraFilter: `ConversationID='${conversationId}'`,
812
+ OrderBy: '__mj_CreatedAt DESC',
813
+ MaxRows: maxMessages,
814
+ ResultType: 'entity_object'
815
+ }, contextUser);
816
+
817
+ if (!detailsResult.Success || !detailsResult.Results) {
818
+ throw new Error('Failed to load conversation history');
819
+ }
820
+
821
+ // Reverse to get chronological order (oldest first)
822
+ const details = detailsResult.Results.reverse();
823
+
824
+ // Get all message IDs for batch loading attachments
825
+ const messageIds = details.map(d => d.ID);
826
+
827
+ // Batch load all attachments for these messages
828
+ const attachmentsByDetailId = await attachmentService.getAttachmentsBatch(messageIds, contextUser);
829
+
830
+ // Build ChatMessage array with attachments
831
+ const messages: ChatMessage[] = [];
832
+
833
+ for (const detail of details) {
834
+ const role = this.mapDetailRoleToMessageRole(detail.Role);
835
+ const attachments = attachmentsByDetailId.get(detail.ID) || [];
836
+
837
+ // Get attachment data with content URLs (handles both inline and FileID storage)
838
+ const attachmentDataPromises = attachments.map(att =>
839
+ attachmentService.getAttachmentData(att, contextUser)
840
+ );
841
+ const attachmentDataResults = await Promise.all(attachmentDataPromises);
842
+
843
+ // Filter out nulls and convert to AttachmentData format
844
+ const validAttachments: AttachmentData[] = attachmentDataResults
845
+ .filter((result): result is NonNullable<typeof result> => result !== null)
846
+ .map(result => ({
847
+ type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
848
+ mimeType: result.attachment.MimeType,
849
+ fileName: result.attachment.FileName ?? undefined,
850
+ sizeBytes: result.attachment.FileSizeBytes ?? undefined,
851
+ width: result.attachment.Width ?? undefined,
852
+ height: result.attachment.Height ?? undefined,
853
+ durationSeconds: result.attachment.DurationSeconds ?? undefined,
854
+ content: result.contentUrl
855
+ }));
856
+
857
+ // Build message content (with or without attachments)
858
+ let content: string | ReturnType<typeof ConversationUtility.BuildChatMessageContent>;
859
+
860
+ if (validAttachments.length > 0) {
861
+ // Use ConversationUtility to build multimodal content blocks
862
+ content = ConversationUtility.BuildChatMessageContent(
863
+ detail.Message || '',
864
+ validAttachments
865
+ );
866
+ } else {
867
+ content = detail.Message || '';
868
+ }
869
+
870
+ messages.push({
871
+ role,
872
+ content
873
+ });
874
+ }
875
+
876
+ return messages;
877
+ }
878
+
879
+ /**
880
+ * Map ConversationDetail Role to ChatMessage role
881
+ */
882
+ private mapDetailRoleToMessageRole(role: string): 'user' | 'assistant' | 'system' {
883
+ const roleLower = (role || '').toLowerCase();
884
+ if (roleLower === 'user') return 'user';
885
+ if (roleLower === 'assistant' || roleLower === 'agent') return 'assistant';
886
+ if (roleLower === 'system') return 'system';
887
+ return 'user'; // Default to user
888
+ }
889
+
702
890
  }
@@ -104,6 +104,7 @@ export class RunTestResolver extends ResolverBase {
104
104
  @Arg('testId') testId: string,
105
105
  @Arg('verbose', { nullable: true }) verbose: boolean = true,
106
106
  @Arg('environment', { nullable: true }) environment?: string,
107
+ @Arg('tags', { nullable: true }) tags?: string,
107
108
  @PubSub() pubSub?: PubSubEngine,
108
109
  @Ctx() { userPayload }: AppContext = {} as AppContext
109
110
  ): Promise<TestRunResult> {
@@ -132,6 +133,7 @@ export class RunTestResolver extends ResolverBase {
132
133
  const options = {
133
134
  verbose,
134
135
  environment,
136
+ tags,
135
137
  progressCallback
136
138
  };
137
139
 
@@ -210,6 +212,10 @@ export class RunTestResolver extends ResolverBase {
210
212
  @Arg('verbose', { nullable: true }) verbose: boolean = true,
211
213
  @Arg('environment', { nullable: true }) environment?: string,
212
214
  @Arg('parallel', { nullable: true }) parallel: boolean = false,
215
+ @Arg('tags', { nullable: true }) tags?: string,
216
+ @Arg('selectedTestIds', { nullable: true }) selectedTestIds?: string,
217
+ @Arg('sequenceStart', () => Int, { nullable: true }) sequenceStart?: number,
218
+ @Arg('sequenceEnd', () => Int, { nullable: true }) sequenceEnd?: number,
213
219
  @PubSub() pubSub?: PubSubEngine,
214
220
  @Ctx() { userPayload }: AppContext = {} as AppContext
215
221
  ): Promise<TestSuiteRunResult> {
@@ -231,10 +237,24 @@ export class RunTestResolver extends ResolverBase {
231
237
  this.createProgressCallback(pubSub, userPayload, suiteId) :
232
238
  undefined;
233
239
 
240
+ // Parse selectedTestIds from JSON string if provided
241
+ let parsedSelectedTestIds: string[] | undefined;
242
+ if (selectedTestIds) {
243
+ try {
244
+ parsedSelectedTestIds = JSON.parse(selectedTestIds);
245
+ } catch (e) {
246
+ LogError(`[RunTestResolver] Failed to parse selectedTestIds: ${selectedTestIds}`);
247
+ }
248
+ }
249
+
234
250
  const options = {
235
251
  verbose,
236
252
  environment,
237
253
  parallel,
254
+ tags,
255
+ selectedTestIds: parsedSelectedTestIds,
256
+ sequenceStart,
257
+ sequenceEnd,
238
258
  progressCallback
239
259
  };
240
260