@memberjunction/server 5.22.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 +35 -0
- package/dist/generated/generated.d.ts +148 -4
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +818 -2
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +3 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/package.json +63 -63
- package/src/generated/generated.ts +567 -7
- package/src/index.ts +10 -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/RunAIAgentResolver.ts +3 -1
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';
|
|
@@ -95,7 +96,10 @@ export * from './resolvers/RunAIPromptResolver.js';
|
|
|
95
96
|
export * from './resolvers/RunAIAgentResolver.js';
|
|
96
97
|
export * from './resolvers/VectorizeEntityResolver.js';
|
|
97
98
|
export * from './resolvers/SearchKnowledgeResolver.js';
|
|
99
|
+
export * from './resolvers/FetchEntityVectorsResolver.js';
|
|
98
100
|
export * from './resolvers/PipelineProgressResolver.js';
|
|
101
|
+
export * from './resolvers/ClientToolRequestResolver.js';
|
|
102
|
+
export * from './resolvers/AutotagPipelineResolver.js';
|
|
99
103
|
export * from './resolvers/TaskResolver.js';
|
|
100
104
|
export * from './generic/KeyValuePairInput.js';
|
|
101
105
|
export * from './generic/KeyInputOutputTypes.js';
|
|
@@ -582,6 +586,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
582
586
|
}
|
|
583
587
|
PubSubManager.Instance.SetPubSubEngine(pubSub as unknown as PubSubEngine);
|
|
584
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
|
+
|
|
585
595
|
// Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
|
|
586
596
|
// ANY BaseEntity save/delete occurs on this server — regardless of whether it
|
|
587
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
|
+
}
|
|
@@ -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
|
|