@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.
- package/README.md +44 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +32 -2
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +172 -4
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +931 -2
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +21 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -0
- package/dist/resolvers/AutotagPipelineResolver.js +147 -0
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -0
- package/dist/resolvers/ClientToolRequestResolver.d.ts +43 -0
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -0
- package/dist/resolvers/ClientToolRequestResolver.js +161 -0
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +29 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -0
- package/dist/resolvers/FetchEntityVectorsResolver.js +218 -0
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -0
- package/dist/resolvers/PipelineProgressResolver.d.ts +33 -0
- package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -0
- package/dist/resolvers/PipelineProgressResolver.js +138 -0
- package/dist/resolvers/PipelineProgressResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +7 -5
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +85 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.js +587 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +21 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.js +134 -0
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -0
- package/package.json +63 -62
- package/src/agents/skip-sdk.ts +31 -2
- package/src/generated/generated.ts +650 -7
- package/src/index.ts +13 -0
- package/src/resolvers/AutotagPipelineResolver.ts +146 -0
- package/src/resolvers/ClientToolRequestResolver.ts +128 -0
- package/src/resolvers/FetchEntityVectorsResolver.ts +234 -0
- package/src/resolvers/PipelineProgressResolver.ts +107 -0
- package/src/resolvers/RunAIAgentResolver.ts +7 -5
- package/src/resolvers/SearchKnowledgeResolver.ts +614 -0
- 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
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
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
|
|