@memberjunction/server 5.23.0 → 5.24.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.
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +462 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +16584 -13956
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +10 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +97 -13
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +6 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +21 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +73 -33
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +42 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +239 -13
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/package.json +63 -63
- package/src/__tests__/search-knowledge-tags.test.ts +415 -0
- package/src/config.ts +11 -0
- package/src/generated/generated.ts +1807 -1
- package/src/generic/RunViewResolver.ts +1 -0
- package/src/resolvers/AutotagPipelineResolver.ts +99 -10
- package/src/resolvers/FetchEntityVectorsResolver.ts +6 -2
- package/src/resolvers/RunAIAgentResolver.ts +95 -56
- package/src/resolvers/SearchKnowledgeResolver.ts +270 -13
|
@@ -765,6 +765,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
765
765
|
for (const [index, data] of rawData.entries()) {
|
|
766
766
|
// EntityName is backfilled by RunViewsGeneric when ViewID/ViewName was used
|
|
767
767
|
const entity = input[index].EntityName ? provider.Entities.find((e) => e.Name === input[index].EntityName) : null;
|
|
768
|
+
|
|
768
769
|
const returnData: any[] = this.processRawData(data.Results, entity ? entity.ID : null, entity);
|
|
769
770
|
|
|
770
771
|
results.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Resolver, Mutation, Ctx, ObjectType, Field } from 'type-graphql';
|
|
1
|
+
import { Resolver, Mutation, Ctx, Arg, ObjectType, Field } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
|
-
import { LogError, LogStatus } from '@memberjunction/core';
|
|
3
|
+
import { LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
4
|
+
import { MJContentProcessRunEntity } from '@memberjunction/core-entities';
|
|
4
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
6
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
6
7
|
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
@@ -28,6 +29,8 @@ export class AutotagPipelineResult {
|
|
|
28
29
|
export class AutotagPipelineResolver extends ResolverBase {
|
|
29
30
|
@Mutation(() => AutotagPipelineResult)
|
|
30
31
|
async RunAutotagPipeline(
|
|
32
|
+
@Arg('contentSourceIDs', () => [String], { nullable: true }) contentSourceIDs: string[] | undefined,
|
|
33
|
+
@Arg('forceReprocess', { nullable: true }) forceReprocess: boolean | undefined,
|
|
31
34
|
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
32
35
|
): Promise<AutotagPipelineResult> {
|
|
33
36
|
try {
|
|
@@ -40,7 +43,7 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
40
43
|
LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
|
|
41
44
|
|
|
42
45
|
// Fire-and-forget: start the pipeline in the background and return immediately
|
|
43
|
-
this.runPipelineInBackground(pipelineRunID, currentUser);
|
|
46
|
+
this.runPipelineInBackground(pipelineRunID, currentUser, contentSourceIDs, forceReprocess);
|
|
44
47
|
|
|
45
48
|
return {
|
|
46
49
|
Success: true,
|
|
@@ -64,7 +67,9 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
64
67
|
*/
|
|
65
68
|
private async runPipelineInBackground(
|
|
66
69
|
pipelineRunID: string,
|
|
67
|
-
currentUser: import('@memberjunction/core').UserInfo
|
|
70
|
+
currentUser: import('@memberjunction/core').UserInfo,
|
|
71
|
+
contentSourceIDs?: string[],
|
|
72
|
+
forceReprocess?: boolean
|
|
68
73
|
): Promise<void> {
|
|
69
74
|
const startTime = Date.now();
|
|
70
75
|
try {
|
|
@@ -89,16 +94,24 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
89
94
|
this.publishProgress(pipelineRunID, 'autotag', total, pct, startTime, currentItem || `${processed}/${total} items`);
|
|
90
95
|
};
|
|
91
96
|
|
|
92
|
-
//
|
|
97
|
+
// Build action params
|
|
98
|
+
const actionParams: Array<{ Name: string; Value: unknown; Type: 'Input' | 'Output' | 'Both' }> = [
|
|
99
|
+
{ Name: 'Autotag', Value: 1, Type: 'Input' },
|
|
100
|
+
{ Name: 'Vectorize', Value: 1, Type: 'Input' },
|
|
101
|
+
{ Name: '__progressCallback', Value: progressCallback, Type: 'Input' }
|
|
102
|
+
];
|
|
103
|
+
if (contentSourceIDs && contentSourceIDs.length > 0) {
|
|
104
|
+
actionParams.push({ Name: 'ContentSourceIDs', Value: contentSourceIDs, Type: 'Input' });
|
|
105
|
+
}
|
|
106
|
+
if (forceReprocess) {
|
|
107
|
+
actionParams.push({ Name: 'ForceReprocess', Value: 1, Type: 'Input' });
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
const result = await ActionEngineServer.Instance.RunAction({
|
|
94
111
|
Action: action,
|
|
95
112
|
ContextUser: currentUser,
|
|
96
113
|
Filters: [],
|
|
97
|
-
Params:
|
|
98
|
-
{ Name: 'Autotag', Value: 1, Type: 'Input' },
|
|
99
|
-
{ Name: 'Vectorize', Value: 1, Type: 'Input' },
|
|
100
|
-
{ Name: '__progressCallback', Value: progressCallback, Type: 'Input' }
|
|
101
|
-
]
|
|
114
|
+
Params: actionParams
|
|
102
115
|
});
|
|
103
116
|
|
|
104
117
|
// Stage: vectorize complete
|
|
@@ -118,6 +131,82 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Pause a running classification pipeline by setting CancellationRequested on the process run.
|
|
136
|
+
* The engine checks this flag between batches and pauses gracefully.
|
|
137
|
+
*/
|
|
138
|
+
@Mutation(() => AutotagPipelineResult)
|
|
139
|
+
async PauseClassificationPipeline(
|
|
140
|
+
@Arg('processRunID') processRunID: string,
|
|
141
|
+
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
142
|
+
): Promise<AutotagPipelineResult> {
|
|
143
|
+
try {
|
|
144
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
145
|
+
if (!currentUser) {
|
|
146
|
+
return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const md = new Metadata();
|
|
150
|
+
const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
|
|
151
|
+
const loaded = await run.Load(processRunID);
|
|
152
|
+
if (!loaded) {
|
|
153
|
+
return { Success: false, Status: 'Error', ErrorMessage: `Process run ${processRunID} not found` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
run.CancellationRequested = true;
|
|
157
|
+
await run.Save();
|
|
158
|
+
|
|
159
|
+
LogStatus(`PauseClassificationPipeline: Pause requested for run ${processRunID}`);
|
|
160
|
+
return { Success: true, Status: 'PauseRequested' };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return { Success: false, Status: 'Error', ErrorMessage: msg };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resume a paused classification pipeline from its last completed offset.
|
|
169
|
+
*/
|
|
170
|
+
@Mutation(() => AutotagPipelineResult)
|
|
171
|
+
async ResumeClassificationPipeline(
|
|
172
|
+
@Arg('processRunID') processRunID: string,
|
|
173
|
+
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
174
|
+
): Promise<AutotagPipelineResult> {
|
|
175
|
+
try {
|
|
176
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
177
|
+
if (!currentUser) {
|
|
178
|
+
return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const md = new Metadata();
|
|
182
|
+
const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
|
|
183
|
+
const loaded = await run.Load(processRunID);
|
|
184
|
+
if (!loaded) {
|
|
185
|
+
return { Success: false, Status: 'Error', ErrorMessage: `Process run ${processRunID} not found` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (run.Status !== 'Paused') {
|
|
189
|
+
return { Success: false, Status: 'Error', ErrorMessage: `Run is not paused (Status: ${run.Status})` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Reset cancellation flag and set status back to Running
|
|
193
|
+
run.CancellationRequested = false;
|
|
194
|
+
run.Status = 'Running';
|
|
195
|
+
await run.Save();
|
|
196
|
+
|
|
197
|
+
// Fire-and-forget: resume pipeline in background from the last offset
|
|
198
|
+
const pipelineRunID = uuidv4();
|
|
199
|
+
LogStatus(`ResumeClassificationPipeline: Resuming run ${processRunID} from offset ${run.LastProcessedOffset}`);
|
|
200
|
+
|
|
201
|
+
this.runPipelineInBackground(pipelineRunID, currentUser, undefined, undefined);
|
|
202
|
+
|
|
203
|
+
return { Success: true, Status: 'Resumed', PipelineRunID: pipelineRunID };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
206
|
+
return { Success: false, Status: 'Error', ErrorMessage: msg };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
/**
|
|
122
211
|
* Publish a progress update to the PipelineProgress subscription topic.
|
|
123
212
|
*/
|
|
@@ -90,13 +90,17 @@ export class FetchEntityVectorsResolver extends ResolverBase {
|
|
|
90
90
|
// but the metadata filter ensures we only get vectors for this entity.
|
|
91
91
|
const entityName = entityDoc.Entity;
|
|
92
92
|
const dimensions = vectorIndex.Dimensions || 1536; // fall back to common embedding size
|
|
93
|
-
|
|
93
|
+
// Use a tiny uniform vector instead of zero — cosine similarity is undefined
|
|
94
|
+
// for a zero vector (division by zero), causing Pinecone to return 0 matches.
|
|
95
|
+
// A uniform vector has equal similarity to all vectors, giving us an unbiased
|
|
96
|
+
// listing that respects the metadata filter.
|
|
97
|
+
const uniformVector = new Array(dimensions).fill(1.0 / Math.sqrt(dimensions));
|
|
94
98
|
|
|
95
99
|
const metadataFilter: Record<string, unknown> = { Entity: { $eq: entityName } };
|
|
96
100
|
|
|
97
101
|
const queryResponse = await vectorDBInstance.QueryIndex({
|
|
98
102
|
id: vectorIndex.Name, // index name (stripped before Pinecone query)
|
|
99
|
-
vector:
|
|
103
|
+
vector: uniformVector,
|
|
100
104
|
topK: limit,
|
|
101
105
|
includeMetadata: true,
|
|
102
106
|
includeValues: true,
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
441
|
-
lastRunId,
|
|
442
|
-
result.agentRun.ID,
|
|
443
|
-
userMessage,
|
|
444
|
-
currentUser
|
|
455
|
+
postExecutionOps.push(
|
|
456
|
+
this.syncFeedbackRequestFromConversation(lastRunId, result.agentRun.ID, userMessage, currentUser)
|
|
445
457
|
);
|
|
446
458
|
}
|
|
447
459
|
|
|
448
|
-
// Send notification if agent created a feedback request (Chat step)
|
|
449
460
|
if (result.feedbackRequestId) {
|
|
450
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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/${
|
|
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:
|
|
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:
|
|
774
|
+
conversationId: conversationId
|
|
759
775
|
})
|
|
760
776
|
});
|
|
761
777
|
|
|
@@ -918,9 +934,24 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
918
934
|
}
|
|
919
935
|
|
|
920
936
|
try {
|
|
937
|
+
// LATENCY OPTIMIZATION (Opt #2 + #3): Load ConversationDetail once here to extract
|
|
938
|
+
// conversationId, then pass it downstream. Previously this record was loaded multiple
|
|
939
|
+
// times: once in loadConversationHistoryWithAttachments (just to get conversationId),
|
|
940
|
+
// once in AgentRunner (same reason), and once in createCompletionNotification. Now we
|
|
941
|
+
// load it a single time and thread conversationId through the call chain.
|
|
942
|
+
const md = new Metadata();
|
|
943
|
+
const currentDetail = await md.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
|
-
|
|
954
|
+
conversationId,
|
|
924
955
|
currentUser,
|
|
925
956
|
maxHistoryMessages || 20
|
|
926
957
|
);
|
|
@@ -935,7 +966,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
935
966
|
p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
|
|
936
967
|
data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
|
|
937
968
|
conversationDetailId, createArtifacts || false, createNotification || false,
|
|
938
|
-
sourceArtifactId, sourceArtifactVersionId
|
|
969
|
+
sourceArtifactId, sourceArtifactVersionId, conversationId
|
|
939
970
|
);
|
|
940
971
|
|
|
941
972
|
LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
|
|
@@ -965,7 +996,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
965
996
|
createArtifacts || false,
|
|
966
997
|
createNotification || false,
|
|
967
998
|
sourceArtifactId,
|
|
968
|
-
sourceArtifactVersionId
|
|
999
|
+
sourceArtifactVersionId,
|
|
1000
|
+
conversationId // LATENCY OPT #2: pass pre-resolved conversationId
|
|
969
1001
|
);
|
|
970
1002
|
} catch (error) {
|
|
971
1003
|
const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
|
|
@@ -1178,14 +1210,16 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1178
1210
|
createArtifacts: boolean = false,
|
|
1179
1211
|
createNotification: boolean = false,
|
|
1180
1212
|
sourceArtifactId?: string,
|
|
1181
|
-
sourceArtifactVersionId?: string
|
|
1213
|
+
sourceArtifactVersionId?: string,
|
|
1214
|
+
/** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
|
|
1215
|
+
conversationId?: string
|
|
1182
1216
|
): void {
|
|
1183
1217
|
// Execute in background - errors are handled within, not propagated
|
|
1184
1218
|
this.executeAIAgent(
|
|
1185
1219
|
p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
|
|
1186
1220
|
data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
|
|
1187
1221
|
configurationId, conversationDetailId, createArtifacts, createNotification,
|
|
1188
|
-
sourceArtifactId, sourceArtifactVersionId
|
|
1222
|
+
sourceArtifactId, sourceArtifactVersionId, conversationId
|
|
1189
1223
|
).catch((error: unknown) => {
|
|
1190
1224
|
// Background execution failed unexpectedly (executeAIAgent has its own try-catch,
|
|
1191
1225
|
// so this would only fire for truly unexpected errors).
|
|
@@ -1210,34 +1244,39 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1210
1244
|
/**
|
|
1211
1245
|
* Load conversation history with attachments from database.
|
|
1212
1246
|
* Builds ChatMessage[] with multimodal content blocks for attachments.
|
|
1247
|
+
*
|
|
1248
|
+
* LATENCY OPTIMIZATIONS (plans/agent-latency-optimization.md — Opts #3 and #8):
|
|
1249
|
+
*
|
|
1250
|
+
* Opt #3: This method now accepts conversationId directly instead of conversationDetailId.
|
|
1251
|
+
* Previously it loaded a ConversationDetail entity object just to extract its ConversationID
|
|
1252
|
+
* field — a redundant DB round-trip (~40ms) since the caller already has this information.
|
|
1253
|
+
* The caller (RunAIAgentFromConversationDetail) now loads the ConversationDetail once and
|
|
1254
|
+
* passes conversationId down.
|
|
1255
|
+
*
|
|
1256
|
+
* Opt #8: Switched from ResultType 'entity_object' to 'simple' with explicit Fields.
|
|
1257
|
+
* The history query only needs ID, Role, and Message from each ConversationDetail record.
|
|
1258
|
+
* Using 'entity_object' created full BaseEntity instances with getters/setters, dirty tracking,
|
|
1259
|
+
* and validation — none of which are needed for read-only history assembly. The 'simple' result
|
|
1260
|
+
* type returns plain JS objects, reducing per-record overhead (~30ms total savings).
|
|
1213
1261
|
*/
|
|
1214
1262
|
private async loadConversationHistoryWithAttachments(
|
|
1215
|
-
|
|
1263
|
+
conversationId: string,
|
|
1216
1264
|
contextUser: UserInfo,
|
|
1217
1265
|
maxMessages: number
|
|
1218
1266
|
): Promise<ChatMessage[]> {
|
|
1219
|
-
const md = new Metadata();
|
|
1220
1267
|
const rv = new RunView();
|
|
1221
1268
|
const attachmentService = getAttachmentService();
|
|
1222
1269
|
|
|
1223
|
-
// Load
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
);
|
|
1228
|
-
if (!await currentDetail.Load(conversationDetailId)) {
|
|
1229
|
-
throw new Error(`Conversation detail ${conversationDetailId} not found`);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
const conversationId = currentDetail.ConversationID;
|
|
1233
|
-
|
|
1234
|
-
// Load recent conversation details (messages) for this conversation
|
|
1235
|
-
const detailsResult = await rv.RunView<MJConversationDetailEntity>({
|
|
1270
|
+
// Load recent conversation details (messages) for this conversation.
|
|
1271
|
+
// Only fetch the three fields we actually use — ID for attachment lookups,
|
|
1272
|
+
// Role for message routing, Message for content.
|
|
1273
|
+
const detailsResult = await rv.RunView<{ ID: string; Role: string; Message: string }>({
|
|
1236
1274
|
EntityName: 'MJ: Conversation Details',
|
|
1237
1275
|
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
1238
1276
|
OrderBy: '__mj_CreatedAt DESC',
|
|
1239
1277
|
MaxRows: maxMessages,
|
|
1240
|
-
|
|
1278
|
+
Fields: ['ID', 'Role', 'Message'],
|
|
1279
|
+
ResultType: 'simple'
|
|
1241
1280
|
}, contextUser);
|
|
1242
1281
|
|
|
1243
1282
|
if (!detailsResult.Success || !detailsResult.Results) {
|