@memberjunction/server 5.21.0 → 5.23.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 (50) hide show
  1. package/README.md +44 -0
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +32 -2
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/generated/generated.d.ts +172 -4
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +931 -2
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +10 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/resolvers/AutotagPipelineResolver.d.ts +21 -0
  14. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -0
  15. package/dist/resolvers/AutotagPipelineResolver.js +147 -0
  16. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -0
  17. package/dist/resolvers/ClientToolRequestResolver.d.ts +43 -0
  18. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -0
  19. package/dist/resolvers/ClientToolRequestResolver.js +161 -0
  20. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -0
  21. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +29 -0
  22. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -0
  23. package/dist/resolvers/FetchEntityVectorsResolver.js +218 -0
  24. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -0
  25. package/dist/resolvers/PipelineProgressResolver.d.ts +33 -0
  26. package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -0
  27. package/dist/resolvers/PipelineProgressResolver.js +138 -0
  28. package/dist/resolvers/PipelineProgressResolver.js.map +1 -0
  29. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  30. package/dist/resolvers/RunAIAgentResolver.js +7 -5
  31. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  32. package/dist/resolvers/SearchKnowledgeResolver.d.ts +85 -0
  33. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -0
  34. package/dist/resolvers/SearchKnowledgeResolver.js +587 -0
  35. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -0
  36. package/dist/resolvers/VectorizeEntityResolver.d.ts +21 -0
  37. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -0
  38. package/dist/resolvers/VectorizeEntityResolver.js +134 -0
  39. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -0
  40. package/package.json +63 -62
  41. package/src/agents/skip-sdk.ts +31 -2
  42. package/src/generated/generated.ts +650 -7
  43. package/src/index.ts +13 -0
  44. package/src/resolvers/AutotagPipelineResolver.ts +146 -0
  45. package/src/resolvers/ClientToolRequestResolver.ts +128 -0
  46. package/src/resolvers/FetchEntityVectorsResolver.ts +234 -0
  47. package/src/resolvers/PipelineProgressResolver.ts +107 -0
  48. package/src/resolvers/RunAIAgentResolver.ts +7 -5
  49. package/src/resolvers/SearchKnowledgeResolver.ts +614 -0
  50. package/src/resolvers/VectorizeEntityResolver.ts +123 -0
package/src/index.ts CHANGED
@@ -42,6 +42,7 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
42
42
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
43
43
  import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
44
44
  import { PubSubManager } from './generic/PubSubManager.js';
45
+ import { ClientToolRequestManager } from '@memberjunction/ai-agents';
45
46
  import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
46
47
  import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
47
48
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
@@ -93,6 +94,12 @@ export * from './generic/RunViewResolver.js';
93
94
  export * from './resolvers/RunTemplateResolver.js';
94
95
  export * from './resolvers/RunAIPromptResolver.js';
95
96
  export * from './resolvers/RunAIAgentResolver.js';
97
+ export * from './resolvers/VectorizeEntityResolver.js';
98
+ export * from './resolvers/SearchKnowledgeResolver.js';
99
+ export * from './resolvers/FetchEntityVectorsResolver.js';
100
+ export * from './resolvers/PipelineProgressResolver.js';
101
+ export * from './resolvers/ClientToolRequestResolver.js';
102
+ export * from './resolvers/AutotagPipelineResolver.js';
96
103
  export * from './resolvers/TaskResolver.js';
97
104
  export * from './generic/KeyValuePairInput.js';
98
105
  export * from './generic/KeyInputOutputTypes.js';
@@ -579,6 +586,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
579
586
  }
580
587
  PubSubManager.Instance.SetPubSubEngine(pubSub as unknown as PubSubEngine);
581
588
 
589
+ // Wire the ClientToolRequestManager so BaseAgent can publish client tool requests
590
+ // via the same PubSub infrastructure used for pipeline progress and cache invalidation.
591
+ ClientToolRequestManager.Instance.SetPublishFunction(
592
+ (topic: string, payload: Record<string, unknown>) => PubSubManager.Instance.Publish(topic, payload)
593
+ );
594
+
582
595
  // Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
583
596
  // ANY BaseEntity save/delete occurs on this server — regardless of whether it
584
597
  // originated from a GraphQL mutation or internal server-side code (agents, actions,
@@ -0,0 +1,146 @@
1
+ import { Resolver, Mutation, Ctx, ObjectType, Field } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { LogError, LogStatus } from '@memberjunction/core';
4
+ import { ResolverBase } from '../generic/ResolverBase.js';
5
+ import { ActionEngineServer } from '@memberjunction/actions';
6
+ import { PubSubManager } from '../generic/PubSubManager.js';
7
+ import { PipelineProgressNotification } from './PipelineProgressResolver.js';
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ const PIPELINE_PROGRESS_TOPIC = 'PIPELINE_PROGRESS';
11
+
12
+ @ObjectType()
13
+ export class AutotagPipelineResult {
14
+ @Field()
15
+ Success: boolean;
16
+
17
+ @Field({ nullable: true })
18
+ Status?: string;
19
+
20
+ @Field({ nullable: true })
21
+ ErrorMessage?: string;
22
+
23
+ @Field({ nullable: true })
24
+ PipelineRunID?: string;
25
+ }
26
+
27
+ @Resolver()
28
+ export class AutotagPipelineResolver extends ResolverBase {
29
+ @Mutation(() => AutotagPipelineResult)
30
+ async RunAutotagPipeline(
31
+ @Ctx() { userPayload }: AppContext = {} as AppContext
32
+ ): Promise<AutotagPipelineResult> {
33
+ try {
34
+ const currentUser = this.GetUserFromPayload(userPayload);
35
+ if (!currentUser) {
36
+ return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
37
+ }
38
+
39
+ const pipelineRunID = uuidv4();
40
+ LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
41
+
42
+ // Fire-and-forget: start the pipeline in the background and return immediately
43
+ this.runPipelineInBackground(pipelineRunID, currentUser);
44
+
45
+ return {
46
+ Success: true,
47
+ Status: 'Started',
48
+ PipelineRunID: pipelineRunID,
49
+ };
50
+ } catch (error) {
51
+ const msg = error instanceof Error ? error.message : String(error);
52
+ LogError(`RunAutotagPipeline mutation failed: ${msg}`);
53
+ return {
54
+ Success: false,
55
+ Status: 'Error',
56
+ ErrorMessage: msg
57
+ };
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Runs the autotag + vectorize pipeline in the background, publishing progress
63
+ * updates via PubSub so the client can subscribe via PipelineProgress.
64
+ */
65
+ private async runPipelineInBackground(
66
+ pipelineRunID: string,
67
+ currentUser: import('@memberjunction/core').UserInfo
68
+ ): Promise<void> {
69
+ const startTime = Date.now();
70
+ try {
71
+ this.publishProgress(pipelineRunID, 'autotag', 0, 0, startTime, 'Initializing pipeline...');
72
+
73
+ await ActionEngineServer.Instance.Config(false, currentUser);
74
+ const action = ActionEngineServer.Instance.Actions.find(
75
+ a => a.Name === 'Autotag and Vectorize Content'
76
+ );
77
+
78
+ if (!action) {
79
+ LogError(`RunAutotagPipeline: Action 'Autotag and Vectorize Content' not found`);
80
+ this.publishProgress(pipelineRunID, 'error', 0, 0, startTime, 'Autotag action not found');
81
+ return;
82
+ }
83
+
84
+ // Stage: autotagging — provide a progress callback that publishes per-item updates
85
+ this.publishProgress(pipelineRunID, 'autotag', 0, 0, startTime, 'Running autotaggers...');
86
+
87
+ const progressCallback = (processed: number, total: number, currentItem?: string) => {
88
+ const pct = total > 0 ? Math.round((processed / total) * 80) : 0; // 0-80% for tagging
89
+ this.publishProgress(pipelineRunID, 'autotag', total, pct, startTime, currentItem || `${processed}/${total} items`);
90
+ };
91
+
92
+ // Run with both Autotag=1 and Vectorize=1: the action will tag and embed in parallel
93
+ const result = await ActionEngineServer.Instance.RunAction({
94
+ Action: action,
95
+ ContextUser: currentUser,
96
+ 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
+ ]
102
+ });
103
+
104
+ // Stage: vectorize complete
105
+ this.publishProgress(pipelineRunID, 'vectorize', 100, 90, startTime, 'Vectorizing content...');
106
+
107
+ if (result.Success) {
108
+ LogStatus(`RunAutotagPipeline: pipeline ${pipelineRunID} completed successfully`);
109
+ this.publishProgress(pipelineRunID, 'complete', 100, 100, startTime);
110
+ } else {
111
+ LogError(`RunAutotagPipeline: pipeline ${pipelineRunID} failed: ${result.Message}`);
112
+ this.publishProgress(pipelineRunID, 'error', 0, 0, startTime, String(result.Message));
113
+ }
114
+ } catch (error) {
115
+ const msg = error instanceof Error ? error.message : String(error);
116
+ LogError(`RunAutotagPipeline pipeline ${pipelineRunID} failed: ${msg}`);
117
+ this.publishProgress(pipelineRunID, 'error', 0, 0, startTime, msg);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Publish a progress update to the PipelineProgress subscription topic.
123
+ */
124
+ private publishProgress(
125
+ pipelineRunID: string,
126
+ stage: string,
127
+ totalItems: number,
128
+ processedItems: number,
129
+ startTime: number,
130
+ currentItem?: string
131
+ ): void {
132
+ const elapsedMs = Date.now() - startTime;
133
+ const percentComplete = totalItems > 0 ? Math.round((processedItems / totalItems) * 100) : 0;
134
+
135
+ const notification: PipelineProgressNotification = {
136
+ PipelineRunID: pipelineRunID,
137
+ Stage: stage,
138
+ TotalItems: totalItems,
139
+ ProcessedItems: processedItems,
140
+ CurrentItem: currentItem,
141
+ ElapsedMs: elapsedMs,
142
+ PercentComplete: percentComplete,
143
+ };
144
+ PubSubManager.Instance.Publish(PIPELINE_PROGRESS_TOPIC, { ...notification });
145
+ }
146
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @fileoverview GraphQL resolver for client tool request/response communication.
3
+ *
4
+ * Provides:
5
+ * - Subscription: Client subscribes to receive tool requests for a session
6
+ * - Mutation: Client sends tool execution responses back to the server
7
+ * - Mutation: Client sends enriched tool definitions after decoration
8
+ *
9
+ * @module @memberjunction/server
10
+ */
11
+
12
+ import { Resolver, Subscription, Root, ObjectType, Field, Float, Mutation, Arg, Ctx } from 'type-graphql';
13
+ import { AppContext } from '../types.js';
14
+ import { LogStatus, LogError } from '@memberjunction/core';
15
+ import { ResolverBase } from '../generic/ResolverBase.js';
16
+ import { ClientToolRequestManager, CLIENT_TOOL_REQUEST_TOPIC, ClientToolRequestNotificationPayload } from '@memberjunction/ai-agents';
17
+
18
+ @ObjectType()
19
+ export class ClientToolRequestNotification {
20
+ @Field()
21
+ AgentRunID: string;
22
+
23
+ @Field()
24
+ SessionID: string;
25
+
26
+ @Field()
27
+ RequestID: string;
28
+
29
+ @Field()
30
+ ToolName: string;
31
+
32
+ /** JSON-encoded parameters */
33
+ @Field()
34
+ Params: string;
35
+
36
+ @Field(() => Float)
37
+ TimeoutMs: number;
38
+
39
+ @Field({ nullable: true })
40
+ Description?: string;
41
+ }
42
+
43
+ @Resolver()
44
+ export class ClientToolRequestResolver extends ResolverBase {
45
+ /**
46
+ * Subscribe to client tool requests for a specific session.
47
+ * The client listens on this subscription to know when an agent
48
+ * wants to invoke a browser-side tool.
49
+ */
50
+ @Subscription(() => ClientToolRequestNotification, {
51
+ topics: CLIENT_TOOL_REQUEST_TOPIC,
52
+ filter: ({ payload, args }: { payload: ClientToolRequestNotificationPayload; args: { sessionID: string } }) => {
53
+ return payload.SessionID === args.sessionID;
54
+ },
55
+ })
56
+ ClientToolRequest(
57
+ @Root() notification: ClientToolRequestNotificationPayload,
58
+ @Arg('sessionID') _sessionID: string
59
+ ): ClientToolRequestNotification {
60
+ return {
61
+ AgentRunID: notification.AgentRunID,
62
+ SessionID: notification.SessionID,
63
+ RequestID: notification.RequestID,
64
+ ToolName: notification.ToolName,
65
+ Params: notification.Params,
66
+ TimeoutMs: notification.TimeoutMs,
67
+ Description: notification.Description
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Client sends the result of executing a client tool back to the server.
73
+ * This resolves the pending Promise in ClientToolRequestManager so the
74
+ * agent loop can continue.
75
+ */
76
+ @Mutation(() => Boolean)
77
+ async RespondToClientToolRequest(
78
+ @Arg('requestID') requestID: string,
79
+ @Arg('success') success: boolean,
80
+ @Arg('result', { nullable: true }) result: string | undefined,
81
+ @Arg('errorMessage', { nullable: true }) errorMessage: string | undefined,
82
+ @Ctx() _context: AppContext = {} as AppContext
83
+ ): Promise<boolean> {
84
+ try {
85
+ const found = ClientToolRequestManager.Instance.ReceiveResponse({
86
+ RequestID: requestID,
87
+ Success: success,
88
+ Result: result ? JSON.parse(result) : undefined,
89
+ ErrorMessage: errorMessage
90
+ });
91
+
92
+ if (!found) {
93
+ LogError(`RespondToClientToolRequest: no pending request for ${requestID} (may have timed out)`);
94
+ }
95
+ return found;
96
+ } catch (error) {
97
+ const msg = error instanceof Error ? error.message : String(error);
98
+ LogError(`RespondToClientToolRequest error: ${msg}`);
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Client sends enriched tool definitions after running decorators.
105
+ * The server stores them per session for LLM prompt injection.
106
+ */
107
+ @Mutation(() => Boolean)
108
+ async UpdateClientToolDefinitions(
109
+ @Arg('sessionID') sessionID: string,
110
+ @Arg('tools') toolsJson: string,
111
+ @Ctx() _context: AppContext = {} as AppContext
112
+ ): Promise<boolean> {
113
+ try {
114
+ const tools = JSON.parse(toolsJson);
115
+ if (!Array.isArray(tools)) {
116
+ LogError('UpdateClientToolDefinitions: tools must be a JSON array');
117
+ return false;
118
+ }
119
+ ClientToolRequestManager.Instance.SetSessionTools(sessionID, tools);
120
+ LogStatus(`UpdateClientToolDefinitions: stored ${tools.length} tools for session ${sessionID}`);
121
+ return true;
122
+ } catch (error) {
123
+ const msg = error instanceof Error ? error.message : String(error);
124
+ LogError(`UpdateClientToolDefinitions error: ${msg}`);
125
+ return false;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,234 @@
1
+ import { Resolver, Query, Arg, Ctx, ObjectType, Field, Float, Int } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
+ import { MJEntityDocumentEntity, MJVectorIndexEntity, MJVectorDatabaseEntity } from '@memberjunction/core-entities';
5
+ import { ResolverBase } from '../generic/ResolverBase.js';
6
+ import { GetAIAPIKey } from '@memberjunction/ai';
7
+ import { VectorDBBase } from '@memberjunction/ai-vectordb';
8
+ import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
9
+
10
+ /* ───── GraphQL types ───── */
11
+
12
+ @ObjectType()
13
+ export class EntityVectorItem {
14
+ @Field()
15
+ ID: string;
16
+
17
+ @Field(() => [Float])
18
+ Values: number[];
19
+
20
+ @Field(() => String)
21
+ Metadata: string;
22
+ }
23
+
24
+ @ObjectType()
25
+ export class FetchEntityVectorsResult {
26
+ @Field()
27
+ Success: boolean;
28
+
29
+ @Field(() => [EntityVectorItem])
30
+ Results: EntityVectorItem[];
31
+
32
+ @Field()
33
+ TotalCount: number;
34
+
35
+ @Field()
36
+ ElapsedMs: number;
37
+
38
+ @Field({ nullable: true })
39
+ ErrorMessage?: string;
40
+ }
41
+
42
+ /* ───── Resolver ───── */
43
+
44
+ @Resolver()
45
+ export class FetchEntityVectorsResolver extends ResolverBase {
46
+
47
+ @Query(() => FetchEntityVectorsResult)
48
+ async FetchEntityVectors(
49
+ @Arg('entityDocumentID') entityDocumentID: string,
50
+ @Arg('maxRecords', () => Int, { nullable: true }) maxRecords: number | undefined,
51
+ @Arg('filter', { nullable: true }) filter: string | undefined,
52
+ @Ctx() { userPayload }: AppContext = {} as AppContext
53
+ ): Promise<FetchEntityVectorsResult> {
54
+ const startTime = Date.now();
55
+ try {
56
+ const currentUser = this.GetUserFromPayload(userPayload);
57
+ if (!currentUser) {
58
+ return this.errorResult('Unable to determine current user', startTime);
59
+ }
60
+
61
+ const limit = maxRecords ?? 1000;
62
+
63
+ // Step 1: Load the EntityDocument
64
+ const entityDoc = await this.loadEntityDocument(entityDocumentID, currentUser);
65
+ if (!entityDoc) {
66
+ return this.errorResult(`EntityDocument not found: ${entityDocumentID}`, startTime);
67
+ }
68
+
69
+ // Step 2: Resolve the VectorIndex
70
+ const vectorIndex = await this.resolveVectorIndex(entityDoc, currentUser);
71
+ if (!vectorIndex) {
72
+ return this.errorResult(
73
+ `Could not resolve VectorIndex for EntityDocument "${entityDoc.Name}"`,
74
+ startTime
75
+ );
76
+ }
77
+
78
+ // Step 3: Create VectorDB provider instance
79
+ const vectorDBInstance = await this.createVectorDBInstance(vectorIndex, currentUser);
80
+ if (!vectorDBInstance) {
81
+ return this.errorResult(
82
+ `Could not create VectorDB provider for index "${vectorIndex.Name}"`,
83
+ startTime
84
+ );
85
+ }
86
+
87
+ // Step 4: Query with zero vector + Entity metadata filter.
88
+ // Pinecone's list API doesn't support metadata filtering, but query does.
89
+ // A zero vector returns results in arbitrary order (similarity is meaningless),
90
+ // but the metadata filter ensures we only get vectors for this entity.
91
+ const entityName = entityDoc.Entity;
92
+ const dimensions = vectorIndex.Dimensions || 1536; // fall back to common embedding size
93
+ const zeroVector = new Array(dimensions).fill(0);
94
+
95
+ const metadataFilter: Record<string, unknown> = { Entity: { $eq: entityName } };
96
+
97
+ const queryResponse = await vectorDBInstance.QueryIndex({
98
+ id: vectorIndex.Name, // index name (stripped before Pinecone query)
99
+ vector: zeroVector,
100
+ topK: limit,
101
+ includeMetadata: true,
102
+ includeValues: true,
103
+ filter: metadataFilter,
104
+ });
105
+
106
+ if (!queryResponse.success || !queryResponse.data) {
107
+ return this.errorResult(
108
+ `Vector query failed for entity "${entityName}" in index "${vectorIndex.Name}"`,
109
+ startTime
110
+ );
111
+ }
112
+
113
+ // Step 5: Convert query matches to result format
114
+ const matches = (queryResponse.data as { matches?: Array<{ id: string; values?: number[]; metadata?: Record<string, unknown>; score?: number }> }).matches ?? [];
115
+ const results: EntityVectorItem[] = matches.map(match => ({
116
+ ID: match.id,
117
+ Values: match.values ?? [],
118
+ Metadata: JSON.stringify(match.metadata ?? {}),
119
+ }));
120
+ LogStatus(`FetchEntityVectors: Queried ${results.length} vectors for entity "${entityName}" in ${Date.now() - startTime}ms`);
121
+
122
+ return {
123
+ Success: true,
124
+ Results: results,
125
+ TotalCount: results.length,
126
+ ElapsedMs: Date.now() - startTime,
127
+ };
128
+ } catch (error) {
129
+ const msg = error instanceof Error ? error.message : String(error);
130
+ LogError(`FetchEntityVectors query failed: ${msg}`);
131
+ return this.errorResult(msg, startTime);
132
+ }
133
+ }
134
+
135
+ /** Load EntityDocument by ID */
136
+ private async loadEntityDocument(
137
+ entityDocumentID: string,
138
+ contextUser: UserInfo
139
+ ): Promise<MJEntityDocumentEntity | null> {
140
+ const rv = new RunView();
141
+ const result = await rv.RunView<MJEntityDocumentEntity>({
142
+ EntityName: 'MJ: Entity Documents',
143
+ ExtraFilter: `ID='${entityDocumentID}'`,
144
+ ResultType: 'entity_object',
145
+ }, contextUser);
146
+
147
+ if (!result.Success || result.Results.length === 0) {
148
+ return null;
149
+ }
150
+ return result.Results[0];
151
+ }
152
+
153
+ /**
154
+ * Resolve the VectorIndex for an EntityDocument.
155
+ * Prefers the explicit VectorIndexID on the EntityDocument; falls back to
156
+ * finding a VectorIndex by matching VectorDatabaseID + EmbeddingModelID.
157
+ */
158
+ private async resolveVectorIndex(
159
+ entityDoc: MJEntityDocumentEntity,
160
+ contextUser: UserInfo
161
+ ): Promise<MJVectorIndexEntity | null> {
162
+ const rv = new RunView();
163
+
164
+ // If the EntityDocument has an explicit VectorIndexID, use it directly
165
+ if (entityDoc.VectorIndexID) {
166
+ const result = await rv.RunView<MJVectorIndexEntity>({
167
+ EntityName: 'MJ: Vector Indexes',
168
+ ExtraFilter: `ID='${entityDoc.VectorIndexID}'`,
169
+ ResultType: 'entity_object',
170
+ }, contextUser);
171
+
172
+ if (result.Success && result.Results.length > 0) {
173
+ return result.Results[0];
174
+ }
175
+ LogError(`FetchEntityVectors: VectorIndex ${entityDoc.VectorIndexID} referenced by EntityDocument not found`);
176
+ }
177
+
178
+ // Fallback: find a VectorIndex by VectorDatabaseID + matching AIModelID as EmbeddingModelID
179
+ const indexResult = await rv.RunView<MJVectorIndexEntity>({
180
+ EntityName: 'MJ: Vector Indexes',
181
+ ExtraFilter: `VectorDatabaseID='${entityDoc.VectorDatabaseID}'`,
182
+ ResultType: 'entity_object',
183
+ }, contextUser);
184
+
185
+ if (!indexResult.Success || indexResult.Results.length === 0) {
186
+ return null;
187
+ }
188
+
189
+ // Match on EmbeddingModelID = EntityDocument.AIModelID
190
+ const match = indexResult.Results.find(idx =>
191
+ UUIDsEqual(idx.EmbeddingModelID, entityDoc.AIModelID)
192
+ );
193
+ return match ?? indexResult.Results[0];
194
+ }
195
+
196
+ /** Create a VectorDBBase provider instance for a given VectorIndex */
197
+ private async createVectorDBInstance(
198
+ vectorIndex: MJVectorIndexEntity,
199
+ contextUser: UserInfo
200
+ ): Promise<VectorDBBase | null> {
201
+ const rv = new RunView();
202
+ const dbResult = await rv.RunView<MJVectorDatabaseEntity>({
203
+ EntityName: 'MJ: Vector Databases',
204
+ ExtraFilter: `ID='${vectorIndex.VectorDatabaseID}'`,
205
+ ResultType: 'entity_object',
206
+ }, contextUser);
207
+
208
+ if (!dbResult.Success || dbResult.Results.length === 0) {
209
+ LogError(`FetchEntityVectors: VectorDatabase not found for index "${vectorIndex.Name}"`);
210
+ return null;
211
+ }
212
+
213
+ const vectorDB = dbResult.Results[0];
214
+ const apiKey = GetAIAPIKey(vectorDB.ClassKey);
215
+ const instance = MJGlobal.Instance.ClassFactory.CreateInstance<VectorDBBase>(
216
+ VectorDBBase, vectorDB.ClassKey, apiKey
217
+ );
218
+
219
+ if (!instance) {
220
+ LogError(`FetchEntityVectors: Failed to create VectorDB instance for ClassKey "${vectorDB.ClassKey}"`);
221
+ }
222
+ return instance;
223
+ }
224
+
225
+ private errorResult(message: string, startTime: number): FetchEntityVectorsResult {
226
+ return {
227
+ Success: false,
228
+ Results: [],
229
+ TotalCount: 0,
230
+ ElapsedMs: Date.now() - startTime,
231
+ ErrorMessage: message,
232
+ };
233
+ }
234
+ }
@@ -0,0 +1,107 @@
1
+ import { Resolver, Subscription, Root, ObjectType, Field, Float, Mutation, Arg, Ctx, PubSub, PubSubEngine } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { LogStatus, LogError } from '@memberjunction/core';
4
+ import { ResolverBase } from '../generic/ResolverBase.js';
5
+
6
+ const PIPELINE_PROGRESS_TOPIC = 'PIPELINE_PROGRESS';
7
+
8
+ /**
9
+ * Stage of the knowledge pipeline.
10
+ */
11
+ export type PipelineStageType = 'extract' | 'autotag' | 'vectorize' | 'complete' | 'error';
12
+
13
+ @ObjectType()
14
+ export class PipelineProgressNotification {
15
+ @Field()
16
+ PipelineRunID: string;
17
+
18
+ @Field()
19
+ Stage: string;
20
+
21
+ @Field()
22
+ TotalItems: number;
23
+
24
+ @Field()
25
+ ProcessedItems: number;
26
+
27
+ @Field({ nullable: true })
28
+ CurrentItem?: string;
29
+
30
+ @Field(() => Float)
31
+ ElapsedMs: number;
32
+
33
+ @Field(() => Float, { nullable: true })
34
+ EstimatedRemainingMs?: number;
35
+
36
+ @Field(() => Float)
37
+ PercentComplete: number;
38
+ }
39
+
40
+ @ObjectType()
41
+ export class PipelineStartResult {
42
+ @Field()
43
+ Success: boolean;
44
+
45
+ @Field()
46
+ PipelineRunID: string;
47
+
48
+ @Field({ nullable: true })
49
+ ErrorMessage?: string;
50
+ }
51
+
52
+ @Resolver()
53
+ export class PipelineProgressResolver extends ResolverBase {
54
+ /**
55
+ * Subscribe to pipeline progress notifications for a specific pipeline run.
56
+ */
57
+ @Subscription(() => PipelineProgressNotification, {
58
+ topics: PIPELINE_PROGRESS_TOPIC,
59
+ filter: ({ payload, args }: { payload: PipelineProgressNotification; args: { pipelineRunID: string } }) => {
60
+ return payload.PipelineRunID === args.pipelineRunID;
61
+ },
62
+ })
63
+ PipelineProgress(
64
+ @Root() notification: PipelineProgressNotification,
65
+ @Arg('pipelineRunID') _pipelineRunID: string
66
+ ): PipelineProgressNotification {
67
+ return notification;
68
+ }
69
+
70
+ /**
71
+ * Publish a pipeline progress update. Called internally by the pipeline engine.
72
+ */
73
+ @Mutation(() => Boolean)
74
+ async PublishPipelineProgress(
75
+ @Arg('pipelineRunID') pipelineRunID: string,
76
+ @Arg('stage') stage: string,
77
+ @Arg('totalItems') totalItems: number,
78
+ @Arg('processedItems') processedItems: number,
79
+ @Arg('currentItem', { nullable: true }) currentItem: string | undefined,
80
+ @Arg('elapsedMs', () => Float) elapsedMs: number,
81
+ @Arg('estimatedRemainingMs', () => Float, { nullable: true }) estimatedRemainingMs: number | undefined,
82
+ @PubSub() pubSub: PubSubEngine,
83
+ @Ctx() { userPayload }: AppContext = {} as AppContext
84
+ ): Promise<boolean> {
85
+ try {
86
+ const percentComplete = totalItems > 0 ? Math.round((processedItems / totalItems) * 100) : 0;
87
+
88
+ const notification: PipelineProgressNotification = {
89
+ PipelineRunID: pipelineRunID,
90
+ Stage: stage,
91
+ TotalItems: totalItems,
92
+ ProcessedItems: processedItems,
93
+ CurrentItem: currentItem,
94
+ ElapsedMs: elapsedMs,
95
+ EstimatedRemainingMs: estimatedRemainingMs,
96
+ PercentComplete: percentComplete,
97
+ };
98
+
99
+ await pubSub.publish(PIPELINE_PROGRESS_TOPIC, notification);
100
+ LogStatus(`PipelineProgress: ${stage} ${processedItems}/${totalItems} (${percentComplete}%)`);
101
+ return true;
102
+ } catch (error) {
103
+ LogError(`PipelineProgressResolver.PublishPipelineProgress failed: ${error}`);
104
+ return false;
105
+ }
106
+ }
107
+ }
@@ -390,10 +390,10 @@ export class RunAIAgentResolver extends ResolverBase {
390
390
  // Validate agent
391
391
  const agentEntity = await this.validateAgent(agentId, currentUser);
392
392
 
393
- // @jordanfanapour IMPORTANT TO-DO for various engine classes (via base engine class) and here for AI Agent Runner and for AI Prompt Runner, need to be able to pass in a IMetadataProvider for it to use
394
- // for multi-user server environments like this one
395
- // Create AI agent runner
396
- const agentRunner = new AgentRunner();
393
+ // Create AI agent runner with the per-request isolated provider so all agent DB operations
394
+ // (AIAgentRun, AIAgentRunSteps, AIAgentRequests, AIPromptRuns) never share the global
395
+ // singleton's transaction state with concurrent requests (e.g. conversation deletes).
396
+ const agentRunner = new AgentRunner(p);
397
397
 
398
398
  // Track agent run for streaming (use ref to update later)
399
399
  const agentRunRef = { current: null as any };
@@ -406,6 +406,7 @@ export class RunAIAgentResolver extends ResolverBase {
406
406
  conversationMessages: parsedMessages,
407
407
  payload: payload ? SafeJSONParse(payload) : undefined,
408
408
  contextUser: currentUser,
409
+ sessionID: sessionId,
409
410
  onProgress: this.createProgressCallback(pubSub, sessionId, userPayload, agentRunRef),
410
411
  onStreaming: this.createStreamingCallback(pubSub, sessionId, userPayload, agentRunRef),
411
412
  lastRunId: lastRunId,
@@ -753,7 +754,8 @@ export class RunAIAgentResolver extends ResolverBase {
753
754
  notificationId: result.inAppNotificationId,
754
755
  action: 'create',
755
756
  title: `${agentName} completed your request`,
756
- message: message
757
+ message: message,
758
+ conversationId: detail.ConversationID
757
759
  })
758
760
  });
759
761