@memberjunction/server 5.10.1 → 5.12.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/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +1 -0
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +220 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1516 -285
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.js +3 -3
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +8 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +2 -0
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +11 -0
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +16 -2
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +20 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +24 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +264 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/package.json +59 -59
- package/src/agents/skip-sdk.ts +1 -0
- package/src/generated/generated.ts +1135 -286
- package/src/generic/ResolverBase.ts +3 -3
- package/src/index.ts +31 -2
- package/src/resolvers/AdhocQueryResolver.ts +8 -0
- package/src/resolvers/CreateQueryResolver.ts +9 -0
- package/src/resolvers/GetDataResolver.ts +18 -2
- package/src/resolvers/QueryResolver.ts +18 -0
- package/src/resolvers/RunAIAgentResolver.ts +301 -2
|
@@ -1061,7 +1061,7 @@ export class ResolverBase {
|
|
|
1061
1061
|
if (await entityObject.Save()) {
|
|
1062
1062
|
// save worked, fire the AfterCreate event and then return all the data
|
|
1063
1063
|
await this.AfterCreate(provider, input); // fire event
|
|
1064
|
-
|
|
1064
|
+
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1065
1065
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
1066
1066
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1067
1067
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
@@ -1145,7 +1145,7 @@ export class ResolverBase {
|
|
|
1145
1145
|
if (await entityObject.Save()) {
|
|
1146
1146
|
// save worked, fire afterevent and return all the data
|
|
1147
1147
|
await this.AfterUpdate(provider, input); // fire event
|
|
1148
|
-
|
|
1148
|
+
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1149
1149
|
|
|
1150
1150
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1151
1151
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
|
|
@@ -1372,7 +1372,7 @@ export class ResolverBase {
|
|
|
1372
1372
|
|
|
1373
1373
|
if (await entityObject.Delete(options)) {
|
|
1374
1374
|
await this.AfterDelete(provider, key); // fire event
|
|
1375
|
-
|
|
1375
|
+
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1376
1376
|
return returnValue;
|
|
1377
1377
|
} else {
|
|
1378
1378
|
throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ dotenv.config({ quiet: true });
|
|
|
4
4
|
|
|
5
5
|
import { expressMiddleware } from '@as-integrations/express5';
|
|
6
6
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
7
|
-
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport } from '@memberjunction/core';
|
|
8
|
-
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
7
|
+
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent } from '@memberjunction/core';
|
|
8
|
+
import { MJGlobal, MJEventType, UUIDsEqual } from '@memberjunction/global';
|
|
9
9
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
10
10
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
11
11
|
import { default as BodyParser } from 'body-parser';
|
|
@@ -426,6 +426,28 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
426
426
|
}
|
|
427
427
|
PubSubManager.Instance.SetPubSubEngine(pubSub as unknown as PubSubEngine);
|
|
428
428
|
|
|
429
|
+
// Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
|
|
430
|
+
// ANY BaseEntity save/delete occurs on this server — regardless of whether it
|
|
431
|
+
// originated from a GraphQL mutation or internal server-side code (agents, actions,
|
|
432
|
+
// task orchestrator, etc.). This closes the gap where server-internal saves were
|
|
433
|
+
// invisible to browser BaseEngine caches.
|
|
434
|
+
MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
|
|
435
|
+
if (event.event === MJEventType.ComponentEvent && event.eventCode === BaseEntity.BaseEventCode) {
|
|
436
|
+
const beEvent = event.args as BaseEntityEvent;
|
|
437
|
+
if (beEvent.type === 'save' || beEvent.type === 'delete') {
|
|
438
|
+
PubSubManager.Instance.Publish(CACHE_INVALIDATION_TOPIC, {
|
|
439
|
+
entityName: beEvent.baseEntity.EntityInfo.Name,
|
|
440
|
+
primaryKeyValues: JSON.stringify(beEvent.baseEntity.PrimaryKey.KeyValuePairs),
|
|
441
|
+
action: beEvent.type,
|
|
442
|
+
sourceServerId: MJGlobal.Instance.ProcessUUID,
|
|
443
|
+
timestamp: new Date(),
|
|
444
|
+
originSessionId: null,
|
|
445
|
+
recordData: beEvent.type === 'save' ? JSON.stringify(beEvent.baseEntity.GetAll()) : undefined,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
429
451
|
let schema = mergeSchemas({
|
|
430
452
|
schemas: [
|
|
431
453
|
buildSchemaSync({
|
|
@@ -505,6 +527,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
505
527
|
level: 6
|
|
506
528
|
}));
|
|
507
529
|
|
|
530
|
+
// Health check endpoint - registered before auth middleware so cloud
|
|
531
|
+
// platform probes (Azure App Service, AWS ALB, k8s, etc.) don't
|
|
532
|
+
// generate noisy auth errors in the logs.
|
|
533
|
+
app.get('/healthcheck', (_req, res) => {
|
|
534
|
+
res.status(200).json({ status: 'ok' });
|
|
535
|
+
});
|
|
536
|
+
|
|
508
537
|
// Apply user-provided Express middleware (after compression, before routes)
|
|
509
538
|
if (options?.ExpressMiddlewareBefore) {
|
|
510
539
|
for (const mw of options.ExpressMiddlewareBefore) {
|
|
@@ -76,6 +76,8 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
76
76
|
Results: JSON.stringify(result.recordset ?? []),
|
|
77
77
|
RowCount: result.recordset?.length ?? 0,
|
|
78
78
|
TotalRowCount: result.recordset?.length ?? 0,
|
|
79
|
+
PageNumber: undefined,
|
|
80
|
+
PageSize: undefined,
|
|
79
81
|
ExecutionTime: executionTimeMs,
|
|
80
82
|
ErrorMessage: ''
|
|
81
83
|
};
|
|
@@ -92,6 +94,8 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
92
94
|
Results: '[]',
|
|
93
95
|
RowCount: 0,
|
|
94
96
|
TotalRowCount: 0,
|
|
97
|
+
PageNumber: undefined,
|
|
98
|
+
PageSize: undefined,
|
|
95
99
|
ExecutionTime: executionTimeMs,
|
|
96
100
|
ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
|
|
97
101
|
};
|
|
@@ -105,6 +109,8 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
105
109
|
Results: '[]',
|
|
106
110
|
RowCount: 0,
|
|
107
111
|
TotalRowCount: 0,
|
|
112
|
+
PageNumber: undefined,
|
|
113
|
+
PageSize: undefined,
|
|
108
114
|
ExecutionTime: executionTimeMs,
|
|
109
115
|
ErrorMessage: `Query execution failed: ${errorMessage}`
|
|
110
116
|
};
|
|
@@ -119,6 +125,8 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
119
125
|
Results: '[]',
|
|
120
126
|
RowCount: 0,
|
|
121
127
|
TotalRowCount: 0,
|
|
128
|
+
PageNumber: undefined,
|
|
129
|
+
PageSize: undefined,
|
|
122
130
|
ExecutionTime: 0,
|
|
123
131
|
ErrorMessage: errorMessage
|
|
124
132
|
};
|
|
@@ -293,6 +293,9 @@ export class CreateQueryResultType {
|
|
|
293
293
|
@Field(() => String, { nullable: true })
|
|
294
294
|
EmbeddingModelName?: string;
|
|
295
295
|
|
|
296
|
+
@Field(() => String, { nullable: true })
|
|
297
|
+
TechnicalDescription?: string;
|
|
298
|
+
|
|
296
299
|
// Related collections
|
|
297
300
|
@Field(() => [QueryFieldType], { nullable: true })
|
|
298
301
|
Fields?: QueryFieldType[];
|
|
@@ -349,6 +352,9 @@ export class UpdateQueryResultType {
|
|
|
349
352
|
@Field(() => String, { nullable: true })
|
|
350
353
|
EmbeddingModelName?: string;
|
|
351
354
|
|
|
355
|
+
@Field(() => String, { nullable: true })
|
|
356
|
+
TechnicalDescription?: string;
|
|
357
|
+
|
|
352
358
|
// Related collections
|
|
353
359
|
@Field(() => [QueryFieldType], { nullable: true })
|
|
354
360
|
Fields?: QueryFieldType[];
|
|
@@ -477,6 +483,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
477
483
|
EmbeddingVector: record.EmbeddingVector,
|
|
478
484
|
EmbeddingModelID: record.EmbeddingModelID,
|
|
479
485
|
EmbeddingModelName: record.EmbeddingModel,
|
|
486
|
+
TechnicalDescription: record.TechnicalDescription,
|
|
480
487
|
Fields: record.QueryFields.map(f => ({
|
|
481
488
|
ID: f.ID,
|
|
482
489
|
QueryID: f.QueryID,
|
|
@@ -540,6 +547,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
540
547
|
EmbeddingVector: existingQuery.EmbeddingVector,
|
|
541
548
|
EmbeddingModelID: existingQuery.EmbeddingModelID,
|
|
542
549
|
EmbeddingModelName: existingQuery.EmbeddingModel,
|
|
550
|
+
TechnicalDescription: existingQuery.TechnicalDescription,
|
|
543
551
|
Fields: existingQuery.Fields?.map((f: any) => ({
|
|
544
552
|
ID: f.ID,
|
|
545
553
|
QueryID: f.QueryID,
|
|
@@ -736,6 +744,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
736
744
|
EmbeddingVector: queryEntity.EmbeddingVector,
|
|
737
745
|
EmbeddingModelID: queryEntity.EmbeddingModelID,
|
|
738
746
|
EmbeddingModelName: queryEntity.EmbeddingModel,
|
|
747
|
+
TechnicalDescription: queryEntity.TechnicalDescription,
|
|
739
748
|
Fields: queryEntity.QueryFields.map(f => ({
|
|
740
749
|
ID: f.ID,
|
|
741
750
|
QueryID: f.QueryID,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, ObjectType, Query } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
|
-
import { LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
3
|
+
import { LogError, LogStatus, Metadata, QueryCompositionEngine } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
|
|
7
|
+
import { getDbType } from '../index.js';
|
|
8
|
+
import { getSystemUser } from '../auth/index.js';
|
|
7
9
|
import sql from 'mssql';
|
|
8
10
|
|
|
9
11
|
@InputType()
|
|
@@ -128,13 +130,27 @@ export class GetDataResolver {
|
|
|
128
130
|
throw new Error('Read-only data source not found');
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
// Resolve composition tokens in queries before execution.
|
|
134
|
+
// Queries may contain {{query:"CategoryPath/QueryName(params)"}} tokens that
|
|
135
|
+
// reference reusable queries. These must be resolved to CTEs before raw SQL execution.
|
|
136
|
+
const compositionEngine = new QueryCompositionEngine();
|
|
137
|
+
const platform = getDbType();
|
|
138
|
+
const systemUser = await getSystemUser();
|
|
139
|
+
|
|
131
140
|
// Execute all queries in parallel, but execute each individual query in its own try catch block so that if one fails, the others can still be processed
|
|
132
141
|
// and also so that we can capture the error message for each query and return it
|
|
133
142
|
const results = await Promise.allSettled(
|
|
134
143
|
input.Queries.map(async (query) => {
|
|
135
144
|
try {
|
|
145
|
+
// Resolve composition tokens if present
|
|
146
|
+
let resolvedSQL = query;
|
|
147
|
+
if (compositionEngine.HasCompositionTokens(query)) {
|
|
148
|
+
const compositionResult = compositionEngine.ResolveComposition(query, platform, systemUser);
|
|
149
|
+
resolvedSQL = compositionResult.ResolvedSQL;
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
const request = new sql.Request(readOnlyDataSource);
|
|
137
|
-
const result = await request.query(
|
|
153
|
+
const result = await request.query(resolvedSQL);
|
|
138
154
|
return { result: result.recordset, error: null };
|
|
139
155
|
} catch (err) {
|
|
140
156
|
// Extract clean SQL error message
|
|
@@ -61,6 +61,12 @@ export class RunQueryResultType {
|
|
|
61
61
|
@Field()
|
|
62
62
|
TotalRowCount: number;
|
|
63
63
|
|
|
64
|
+
@Field(() => Int, { nullable: true })
|
|
65
|
+
PageNumber?: number;
|
|
66
|
+
|
|
67
|
+
@Field(() => Int, { nullable: true })
|
|
68
|
+
PageSize?: number;
|
|
69
|
+
|
|
64
70
|
@Field()
|
|
65
71
|
ExecutionTime: number;
|
|
66
72
|
|
|
@@ -233,6 +239,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
233
239
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
234
240
|
ErrorMessage: result.ErrorMessage || '',
|
|
235
241
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
242
|
+
PageNumber: result.PageNumber,
|
|
243
|
+
PageSize: result.PageSize,
|
|
236
244
|
CacheHit: (result as any).CacheHit,
|
|
237
245
|
CacheTTLRemaining: (result as any).CacheTTLRemaining
|
|
238
246
|
};
|
|
@@ -276,6 +284,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
276
284
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
277
285
|
ErrorMessage: result.ErrorMessage || '',
|
|
278
286
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
287
|
+
PageNumber: result.PageNumber,
|
|
288
|
+
PageSize: result.PageSize,
|
|
279
289
|
CacheHit: (result as any).CacheHit,
|
|
280
290
|
CacheTTLRemaining: (result as any).CacheTTLRemaining
|
|
281
291
|
};
|
|
@@ -332,6 +342,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
332
342
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
333
343
|
ErrorMessage: result.ErrorMessage || '',
|
|
334
344
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
345
|
+
PageNumber: result.PageNumber,
|
|
346
|
+
PageSize: result.PageSize,
|
|
335
347
|
CacheHit: (result as any).CacheHit,
|
|
336
348
|
CacheTTLRemaining: (result as any).CacheTTLRemaining
|
|
337
349
|
};
|
|
@@ -374,6 +386,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
374
386
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
375
387
|
ErrorMessage: result.ErrorMessage || '',
|
|
376
388
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
389
|
+
PageNumber: result.PageNumber,
|
|
390
|
+
PageSize: result.PageSize,
|
|
377
391
|
CacheHit: (result as any).CacheHit,
|
|
378
392
|
CacheTTLRemaining: (result as any).CacheTTLRemaining
|
|
379
393
|
};
|
|
@@ -424,6 +438,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
424
438
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
425
439
|
ErrorMessage: result.ErrorMessage || '',
|
|
426
440
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
441
|
+
PageNumber: result.PageNumber,
|
|
442
|
+
PageSize: result.PageSize,
|
|
427
443
|
CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
|
|
428
444
|
CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
|
|
429
445
|
};
|
|
@@ -471,6 +487,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
471
487
|
ExecutionTime: result.ExecutionTime ?? 0,
|
|
472
488
|
ErrorMessage: result.ErrorMessage || '',
|
|
473
489
|
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
490
|
+
PageNumber: result.PageNumber,
|
|
491
|
+
PageSize: result.PageSize,
|
|
474
492
|
CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
|
|
475
493
|
CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
|
|
476
494
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID, Int } from 'type-graphql';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
-
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
|
|
5
5
|
import { AgentRunner } from '@memberjunction/ai-agents';
|
|
6
6
|
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
|
|
7
7
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
@@ -154,7 +154,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
154
154
|
errorMessage: result.agentRun?.ErrorMessage,
|
|
155
155
|
finalStep: result.agentRun?.FinalStep,
|
|
156
156
|
cancelled: result.agentRun?.Status === 'Cancelled',
|
|
157
|
-
cancellationReason: result.agentRun?.CancellationReason
|
|
157
|
+
cancellationReason: result.agentRun?.CancellationReason,
|
|
158
|
+
feedbackRequestId: result.feedbackRequestId
|
|
158
159
|
};
|
|
159
160
|
|
|
160
161
|
// Safely extract agent run data using GetAll() for proper serialization
|
|
@@ -433,6 +434,21 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
433
434
|
|
|
434
435
|
const executionTime = Date.now() - startTime;
|
|
435
436
|
|
|
437
|
+
// Sync feedback request if this is a continuation run (user responded via conversation)
|
|
438
|
+
if (lastRunId && result.agentRun?.ID) {
|
|
439
|
+
await this.syncFeedbackRequestFromConversation(
|
|
440
|
+
lastRunId,
|
|
441
|
+
result.agentRun.ID,
|
|
442
|
+
userMessage,
|
|
443
|
+
currentUser
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Send notification if agent created a feedback request (Chat step)
|
|
448
|
+
if (result.feedbackRequestId) {
|
|
449
|
+
await this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload);
|
|
450
|
+
}
|
|
451
|
+
|
|
436
452
|
// Create notification if enabled and artifact was created successfully
|
|
437
453
|
if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
|
|
438
454
|
await this.createCompletionNotification(
|
|
@@ -752,6 +768,112 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
752
768
|
}
|
|
753
769
|
}
|
|
754
770
|
|
|
771
|
+
/**
|
|
772
|
+
* When a continuation run completes (lastRunId was provided), sync the corresponding
|
|
773
|
+
* AIAgentRequest by marking it as responded. This keeps the dashboard accurate when
|
|
774
|
+
* users respond to Chat steps via the conversation UI.
|
|
775
|
+
*
|
|
776
|
+
* Called server-side in the resolver so the conversation UI doesn't need any changes.
|
|
777
|
+
*/
|
|
778
|
+
private async syncFeedbackRequestFromConversation(
|
|
779
|
+
lastRunId: string,
|
|
780
|
+
newRunId: string,
|
|
781
|
+
userMessage: string | undefined,
|
|
782
|
+
contextUser: UserInfo
|
|
783
|
+
): Promise<void> {
|
|
784
|
+
try {
|
|
785
|
+
const rv = new RunView();
|
|
786
|
+
const result = await rv.RunView<MJAIAgentRequestEntity>({
|
|
787
|
+
EntityName: 'MJ: AI Agent Requests',
|
|
788
|
+
ExtraFilter: `OriginatingAgentRunID='${lastRunId}' AND Status='Requested'`,
|
|
789
|
+
MaxRows: 1,
|
|
790
|
+
ResultType: 'entity_object'
|
|
791
|
+
}, contextUser);
|
|
792
|
+
|
|
793
|
+
if (!result.Success || !result.Results || result.Results.length === 0) {
|
|
794
|
+
return; // No pending request for this run — normal for non-Chat continuations
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const request = result.Results[0];
|
|
798
|
+
request.Status = 'Responded';
|
|
799
|
+
request.RespondedAt = new Date();
|
|
800
|
+
request.ResponseByUserID = contextUser.ID;
|
|
801
|
+
request.ResumingAgentRunID = newRunId;
|
|
802
|
+
if (userMessage) {
|
|
803
|
+
request.Response = userMessage;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const saved = await request.Save();
|
|
807
|
+
if (saved) {
|
|
808
|
+
LogStatus(`📋 Synced feedback request ${request.ID} → Responded (via conversation)`);
|
|
809
|
+
} else {
|
|
810
|
+
LogError(`Failed to save feedback request sync for ${request.ID}`);
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
// Don't let sync failure break the agent execution
|
|
814
|
+
LogError(`Error syncing feedback request: ${(error as Error).message}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Sends a notification when an agent creates a feedback request (Chat step).
|
|
820
|
+
* Called after execution completes if the result contains a feedbackRequestId.
|
|
821
|
+
*/
|
|
822
|
+
private async sendFeedbackRequestNotification(
|
|
823
|
+
result: ExecuteAgentResult,
|
|
824
|
+
contextUser: UserInfo,
|
|
825
|
+
pubSub: PubSubEngine,
|
|
826
|
+
userPayload: UserPayload
|
|
827
|
+
): Promise<void> {
|
|
828
|
+
if (!result.feedbackRequestId) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
// Get agent name
|
|
834
|
+
await AIEngine.Instance.Config(false, contextUser);
|
|
835
|
+
const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, result.agentRun?.AgentID));
|
|
836
|
+
const agentName = agent?.Name || 'Agent';
|
|
837
|
+
|
|
838
|
+
// Truncate message for notification
|
|
839
|
+
const message = result.agentRun?.Message || 'Agent needs your input';
|
|
840
|
+
const truncatedMessage = message.length > 200 ? message.substring(0, 197) + '...' : message;
|
|
841
|
+
|
|
842
|
+
const notificationEngine = NotificationEngine.Instance;
|
|
843
|
+
await notificationEngine.Config(false, contextUser);
|
|
844
|
+
const notifResult = await notificationEngine.SendNotification({
|
|
845
|
+
userId: contextUser.ID,
|
|
846
|
+
typeNameOrId: 'Agent Feedback Request',
|
|
847
|
+
title: `${agentName} needs your input`,
|
|
848
|
+
message: truncatedMessage,
|
|
849
|
+
resourceConfiguration: {
|
|
850
|
+
type: 'agent-request',
|
|
851
|
+
requestId: result.feedbackRequestId
|
|
852
|
+
}
|
|
853
|
+
}, contextUser);
|
|
854
|
+
|
|
855
|
+
if (notifResult.success && notifResult.inAppNotificationId) {
|
|
856
|
+
LogStatus(`📬 Feedback request notification sent (ID: ${notifResult.inAppNotificationId})`);
|
|
857
|
+
|
|
858
|
+
// Publish real-time notification event
|
|
859
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
860
|
+
userPayload: JSON.stringify(userPayload),
|
|
861
|
+
message: JSON.stringify({
|
|
862
|
+
type: 'notification',
|
|
863
|
+
notificationId: notifResult.inAppNotificationId,
|
|
864
|
+
action: 'create',
|
|
865
|
+
title: `${agentName} needs your input`,
|
|
866
|
+
message: truncatedMessage
|
|
867
|
+
})
|
|
868
|
+
});
|
|
869
|
+
} else if (!notifResult.success) {
|
|
870
|
+
LogError(`Feedback request notification failed: ${notifResult.errors?.join(', ')}`);
|
|
871
|
+
}
|
|
872
|
+
} catch (error) {
|
|
873
|
+
LogError(`Error sending feedback request notification: ${(error as Error).message}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
755
877
|
/**
|
|
756
878
|
* Optimized mutation that loads conversation history server-side.
|
|
757
879
|
* This avoids sending large attachment data from client to server.
|
|
@@ -854,6 +976,183 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
854
976
|
}
|
|
855
977
|
}
|
|
856
978
|
|
|
979
|
+
/**
|
|
980
|
+
* Respond to a pending AIAgentRequest from the dashboard or API.
|
|
981
|
+
* Updates the request record with the response and optionally spawns a
|
|
982
|
+
* new agent run to resume execution with the human's input.
|
|
983
|
+
*/
|
|
984
|
+
@Mutation(() => AIAgentRunResult)
|
|
985
|
+
async RespondToAgentRequest(
|
|
986
|
+
@Arg('requestId') requestId: string,
|
|
987
|
+
@Arg('status') status: string,
|
|
988
|
+
@Ctx() { userPayload, providers, dataSource }: AppContext,
|
|
989
|
+
@Arg('response', { nullable: true }) response?: string,
|
|
990
|
+
@Arg('responseData', { nullable: true }) responseData?: string,
|
|
991
|
+
@Arg('resumeAgent', { nullable: true }) resumeAgent?: boolean
|
|
992
|
+
): Promise<AIAgentRunResult> {
|
|
993
|
+
const startTime = Date.now();
|
|
994
|
+
try {
|
|
995
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
996
|
+
if (!currentUser) {
|
|
997
|
+
throw new Error('Unable to determine current user');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const md = new Metadata();
|
|
1001
|
+
const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
|
|
1002
|
+
'MJ: AI Agent Requests',
|
|
1003
|
+
currentUser
|
|
1004
|
+
);
|
|
1005
|
+
if (!(await request.Load(requestId))) {
|
|
1006
|
+
throw new Error(`Agent request ${requestId} not found`);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (request.Status !== 'Requested') {
|
|
1010
|
+
throw new Error(`Request ${requestId} is already ${request.Status}, cannot respond`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Validate status
|
|
1014
|
+
const validStatuses = ['Approved', 'Rejected', 'Responded'];
|
|
1015
|
+
if (!validStatuses.includes(status)) {
|
|
1016
|
+
throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Update the request
|
|
1020
|
+
request.Status = status as 'Approved' | 'Rejected' | 'Responded';
|
|
1021
|
+
request.Response = response || null;
|
|
1022
|
+
request.ResponseData = responseData || null;
|
|
1023
|
+
request.RespondedAt = new Date();
|
|
1024
|
+
request.ResponseByUserID = currentUser.ID;
|
|
1025
|
+
|
|
1026
|
+
const saved = await request.Save();
|
|
1027
|
+
if (!saved) {
|
|
1028
|
+
throw new Error(`Failed to save response for request ${requestId}`);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
LogStatus(`📋 Agent request ${requestId} → ${status} by ${currentUser.Email || currentUser.ID}`);
|
|
1032
|
+
|
|
1033
|
+
const executionTime = Date.now() - startTime;
|
|
1034
|
+
return {
|
|
1035
|
+
success: true,
|
|
1036
|
+
executionTimeMs: executionTime,
|
|
1037
|
+
result: JSON.stringify({
|
|
1038
|
+
success: true,
|
|
1039
|
+
requestId: requestId,
|
|
1040
|
+
status: status,
|
|
1041
|
+
resumed: false
|
|
1042
|
+
})
|
|
1043
|
+
};
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
const executionTime = Date.now() - startTime;
|
|
1046
|
+
const errorMessage = (error as Error).message || 'Unknown error';
|
|
1047
|
+
LogError(`RespondToAgentRequest failed: ${errorMessage}`, undefined, error);
|
|
1048
|
+
return {
|
|
1049
|
+
success: false,
|
|
1050
|
+
errorMessage,
|
|
1051
|
+
executionTimeMs: executionTime,
|
|
1052
|
+
result: JSON.stringify({ success: false, errorMessage })
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Reassign an agent request to a different user.
|
|
1059
|
+
* Updates RequestForUserID and sends a notification to the new assignee.
|
|
1060
|
+
*/
|
|
1061
|
+
@Mutation(() => AIAgentRunResult)
|
|
1062
|
+
async ReassignAgentRequest(
|
|
1063
|
+
@Arg('requestId') requestId: string,
|
|
1064
|
+
@Arg('newUserID') newUserID: string,
|
|
1065
|
+
@Ctx() { userPayload }: AppContext,
|
|
1066
|
+
@Arg('note', { nullable: true }) note?: string
|
|
1067
|
+
): Promise<AIAgentRunResult> {
|
|
1068
|
+
const startTime = Date.now();
|
|
1069
|
+
try {
|
|
1070
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
1071
|
+
if (!currentUser) {
|
|
1072
|
+
throw new Error('Unable to determine current user');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const md = new Metadata();
|
|
1076
|
+
const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
|
|
1077
|
+
'MJ: AI Agent Requests',
|
|
1078
|
+
currentUser
|
|
1079
|
+
);
|
|
1080
|
+
if (!(await request.Load(requestId))) {
|
|
1081
|
+
throw new Error(`Agent request ${requestId} not found`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (request.Status !== 'Requested') {
|
|
1085
|
+
throw new Error(`Request ${requestId} is ${request.Status} and cannot be reassigned`);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const previousUserID = request.RequestForUserID;
|
|
1089
|
+
request.RequestForUserID = newUserID;
|
|
1090
|
+
|
|
1091
|
+
// Append reassignment note to Comments
|
|
1092
|
+
if (note || previousUserID) {
|
|
1093
|
+
const timestamp = new Date().toISOString();
|
|
1094
|
+
const reassignEntry = `[${timestamp} by ${currentUser.Email || currentUser.ID}] Reassigned from ${previousUserID || '(unassigned)'} to ${newUserID}${note ? ` — "${note}"` : ''}`;
|
|
1095
|
+
request.Comments = request.Comments
|
|
1096
|
+
? `${request.Comments}\n${reassignEntry}`
|
|
1097
|
+
: reassignEntry;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const saved = await request.Save();
|
|
1101
|
+
if (!saved) {
|
|
1102
|
+
throw new Error(`Failed to save reassignment for request ${requestId}`);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Send notification to new assignee
|
|
1106
|
+
try {
|
|
1107
|
+
await AIEngine.Instance.Config(false, currentUser);
|
|
1108
|
+
const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, request.AgentID));
|
|
1109
|
+
const agentName = agent?.Name || 'Agent';
|
|
1110
|
+
const truncatedRequest = request.Request.length > 200
|
|
1111
|
+
? request.Request.substring(0, 197) + '...'
|
|
1112
|
+
: request.Request;
|
|
1113
|
+
|
|
1114
|
+
const notificationEngine = NotificationEngine.Instance;
|
|
1115
|
+
await notificationEngine.Config(false, currentUser);
|
|
1116
|
+
await notificationEngine.SendNotification({
|
|
1117
|
+
userId: newUserID,
|
|
1118
|
+
typeNameOrId: 'Agent Feedback Request',
|
|
1119
|
+
title: `${agentName} request assigned to you`,
|
|
1120
|
+
message: truncatedRequest,
|
|
1121
|
+
resourceConfiguration: {
|
|
1122
|
+
type: 'agent-request',
|
|
1123
|
+
requestId: requestId
|
|
1124
|
+
}
|
|
1125
|
+
}, currentUser);
|
|
1126
|
+
} catch (notifError) {
|
|
1127
|
+
LogError(`Failed to send reassignment notification: ${(notifError as Error).message}`);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
LogStatus(`📋 Agent request ${requestId} reassigned to ${newUserID}`);
|
|
1131
|
+
|
|
1132
|
+
const executionTime = Date.now() - startTime;
|
|
1133
|
+
return {
|
|
1134
|
+
success: true,
|
|
1135
|
+
executionTimeMs: executionTime,
|
|
1136
|
+
result: JSON.stringify({
|
|
1137
|
+
success: true,
|
|
1138
|
+
requestId,
|
|
1139
|
+
newUserID,
|
|
1140
|
+
reassigned: true
|
|
1141
|
+
})
|
|
1142
|
+
};
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
const executionTime = Date.now() - startTime;
|
|
1145
|
+
const errorMessage = (error as Error).message || 'Unknown error';
|
|
1146
|
+
LogError(`ReassignAgentRequest failed: ${errorMessage}`, undefined, error);
|
|
1147
|
+
return {
|
|
1148
|
+
success: false,
|
|
1149
|
+
errorMessage,
|
|
1150
|
+
executionTimeMs: executionTime,
|
|
1151
|
+
result: JSON.stringify({ success: false, errorMessage })
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
857
1156
|
/**
|
|
858
1157
|
* Execute agent in background (fire-and-forget).
|
|
859
1158
|
* Handles errors by publishing error completion events via PubSub,
|