@memberjunction/server 2.128.0 → 2.130.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 +5 -1
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -3
- package/dist/auth/index.js.map +1 -1
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +10020 -9166
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +52323 -46987
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +4 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +122 -10
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +22 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +171 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +5 -1
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +37 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +304 -2
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +114 -2
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts +2 -2
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +27 -8
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +5 -5
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +120 -0
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -0
- package/dist/resolvers/TelemetryResolver.js +731 -0
- package/dist/resolvers/TelemetryResolver.js.map +1 -0
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +43 -42
- package/src/agents/skip-sdk.ts +5 -1
- package/src/auth/index.ts +2 -3
- package/src/config.ts +9 -0
- package/src/generated/generated.ts +29861 -26490
- package/src/generic/ResolverBase.ts +201 -12
- package/src/generic/RunViewResolver.ts +157 -3
- package/src/index.ts +25 -5
- package/src/resolvers/AskSkipResolver.ts +18 -20
- package/src/resolvers/QueryResolver.ts +276 -2
- package/src/resolvers/RunAIAgentResolver.ts +192 -4
- package/src/resolvers/RunAIPromptResolver.ts +1 -1
- package/src/resolvers/RunTestResolver.ts +20 -0
- package/src/resolvers/SyncDataResolver.ts +5 -5
- package/src/resolvers/TelemetryResolver.ts +567 -0
- package/src/services/TaskOrchestrator.ts +2 -1
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int } from 'type-graphql';
|
|
2
|
-
import { RunQuery, QueryInfo, IRunQueryProvider, IMetadataProvider } from '@memberjunction/core';
|
|
1
|
+
import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
+
import { RunQuery, QueryInfo, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
6
6
|
import { Metadata } from '@memberjunction/core';
|
|
7
7
|
import { GetReadOnlyProvider } from '../util.js';
|
|
8
|
+
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Input type for batch query execution - allows running multiple queries in a single network call
|
|
12
|
+
*/
|
|
13
|
+
@InputType()
|
|
14
|
+
export class RunQueryInput {
|
|
15
|
+
@Field(() => String, { nullable: true, description: 'Query ID to run - either QueryID or QueryName must be provided' })
|
|
16
|
+
QueryID?: string;
|
|
17
|
+
|
|
18
|
+
@Field(() => String, { nullable: true, description: 'Query Name to run - either QueryID or QueryName must be provided' })
|
|
19
|
+
QueryName?: string;
|
|
20
|
+
|
|
21
|
+
@Field(() => String, { nullable: true, description: 'Category ID to help disambiguate queries with the same name' })
|
|
22
|
+
CategoryID?: string;
|
|
23
|
+
|
|
24
|
+
@Field(() => String, { nullable: true, description: 'Category path to help disambiguate queries with the same name' })
|
|
25
|
+
CategoryPath?: string;
|
|
26
|
+
|
|
27
|
+
@Field(() => GraphQLJSONObject, { nullable: true, description: 'Parameters to pass to the query' })
|
|
28
|
+
Parameters?: Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
@Field(() => Int, { nullable: true, description: 'Maximum number of rows to return' })
|
|
31
|
+
MaxRows?: number;
|
|
32
|
+
|
|
33
|
+
@Field(() => Int, { nullable: true, description: 'Starting row offset for pagination' })
|
|
34
|
+
StartRow?: number;
|
|
35
|
+
|
|
36
|
+
@Field(() => Boolean, { nullable: true, description: 'Force audit logging regardless of query settings' })
|
|
37
|
+
ForceAuditLog?: boolean;
|
|
38
|
+
|
|
39
|
+
@Field(() => String, { nullable: true, description: 'Description to use in audit log' })
|
|
40
|
+
AuditLogDescription?: string;
|
|
41
|
+
}
|
|
8
42
|
|
|
9
43
|
@ObjectType()
|
|
10
44
|
export class RunQueryResultType {
|
|
@@ -42,6 +76,70 @@ export class RunQueryResultType {
|
|
|
42
76
|
CacheTTLRemaining?: number;
|
|
43
77
|
}
|
|
44
78
|
|
|
79
|
+
//****************************************************************************
|
|
80
|
+
// INPUT/OUTPUT TYPES for RunQueriesWithCacheCheck
|
|
81
|
+
//****************************************************************************
|
|
82
|
+
|
|
83
|
+
@InputType()
|
|
84
|
+
export class RunQueryCacheStatusInput {
|
|
85
|
+
@Field(() => String, { description: 'The maximum __mj_UpdatedAt value from cached results' })
|
|
86
|
+
maxUpdatedAt: string;
|
|
87
|
+
|
|
88
|
+
@Field(() => Int, { description: 'The number of rows in cached results' })
|
|
89
|
+
rowCount: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@InputType()
|
|
93
|
+
export class RunQueryWithCacheCheckInput {
|
|
94
|
+
@Field(() => RunQueryInput, { description: 'The RunQuery parameters' })
|
|
95
|
+
params: RunQueryInput;
|
|
96
|
+
|
|
97
|
+
@Field(() => RunQueryCacheStatusInput, {
|
|
98
|
+
nullable: true,
|
|
99
|
+
description: 'Optional cache status - if provided, server will check if cache is current'
|
|
100
|
+
})
|
|
101
|
+
cacheStatus?: RunQueryCacheStatusInput;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@ObjectType()
|
|
105
|
+
export class RunQueryWithCacheCheckResultOutput {
|
|
106
|
+
@Field(() => Int, { description: 'The index of this query in the batch request' })
|
|
107
|
+
queryIndex: number;
|
|
108
|
+
|
|
109
|
+
@Field(() => String, { description: 'The query ID' })
|
|
110
|
+
queryId: string;
|
|
111
|
+
|
|
112
|
+
@Field(() => String, { description: "'current', 'stale', 'no_validation', or 'error'" })
|
|
113
|
+
status: string;
|
|
114
|
+
|
|
115
|
+
@Field(() => String, {
|
|
116
|
+
nullable: true,
|
|
117
|
+
description: 'JSON-stringified results - only populated when status is stale or no_validation'
|
|
118
|
+
})
|
|
119
|
+
Results?: string;
|
|
120
|
+
|
|
121
|
+
@Field(() => String, { nullable: true, description: 'Max __mj_UpdatedAt from results when stale' })
|
|
122
|
+
maxUpdatedAt?: string;
|
|
123
|
+
|
|
124
|
+
@Field(() => Int, { nullable: true, description: 'Row count of results when stale' })
|
|
125
|
+
rowCount?: number;
|
|
126
|
+
|
|
127
|
+
@Field(() => String, { nullable: true, description: 'Error message if status is error' })
|
|
128
|
+
errorMessage?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@ObjectType()
|
|
132
|
+
export class RunQueriesWithCacheCheckOutput {
|
|
133
|
+
@Field(() => Boolean, { description: 'Whether the overall operation succeeded' })
|
|
134
|
+
success: boolean;
|
|
135
|
+
|
|
136
|
+
@Field(() => [RunQueryWithCacheCheckResultOutput], { description: 'Results for each query in the batch' })
|
|
137
|
+
results: RunQueryWithCacheCheckResultOutput[];
|
|
138
|
+
|
|
139
|
+
@Field(() => String, { nullable: true, description: 'Overall error message if success is false' })
|
|
140
|
+
errorMessage?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
45
143
|
@Resolver()
|
|
46
144
|
export class RunQueryResolver {
|
|
47
145
|
private async findQuery(md: IMetadataProvider, QueryID: string, QueryName?: string, CategoryID?: string, CategoryPath?: string, refreshMetadataIfNotFound: boolean = false): Promise<QueryInfo | null> {
|
|
@@ -273,4 +371,180 @@ export class RunQueryResolver {
|
|
|
273
371
|
CacheTTLRemaining: (result as any).CacheTTLRemaining
|
|
274
372
|
};
|
|
275
373
|
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Batch query execution - runs multiple queries in a single network call
|
|
377
|
+
* This is more efficient than making multiple individual query calls
|
|
378
|
+
*/
|
|
379
|
+
@Query(() => [RunQueryResultType])
|
|
380
|
+
async RunQueries(
|
|
381
|
+
@Arg('input', () => [RunQueryInput]) input: RunQueryInput[],
|
|
382
|
+
@Ctx() context: AppContext
|
|
383
|
+
): Promise<RunQueryResultType[]> {
|
|
384
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
385
|
+
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
386
|
+
|
|
387
|
+
// Convert input to RunQueryParams array
|
|
388
|
+
const params: RunQueryParams[] = input.map(i => ({
|
|
389
|
+
QueryID: i.QueryID,
|
|
390
|
+
QueryName: i.QueryName,
|
|
391
|
+
CategoryID: i.CategoryID,
|
|
392
|
+
CategoryPath: i.CategoryPath,
|
|
393
|
+
Parameters: i.Parameters,
|
|
394
|
+
MaxRows: i.MaxRows,
|
|
395
|
+
StartRow: i.StartRow,
|
|
396
|
+
ForceAuditLog: i.ForceAuditLog,
|
|
397
|
+
AuditLogDescription: i.AuditLogDescription
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
// Execute all queries in parallel using the batch method
|
|
401
|
+
const results = await rq.RunQueries(params, context.userPayload.userRecord);
|
|
402
|
+
|
|
403
|
+
// Map results to output format
|
|
404
|
+
return results.map((result, index) => {
|
|
405
|
+
const inputItem = input[index];
|
|
406
|
+
return {
|
|
407
|
+
QueryID: result.QueryID || inputItem.QueryID || '',
|
|
408
|
+
QueryName: result.QueryName || inputItem.QueryName || 'Unknown Query',
|
|
409
|
+
Success: result.Success ?? false,
|
|
410
|
+
Results: JSON.stringify(result.Results ?? null),
|
|
411
|
+
RowCount: result.RowCount ?? 0,
|
|
412
|
+
TotalRowCount: result.TotalRowCount ?? 0,
|
|
413
|
+
ExecutionTime: result.ExecutionTime ?? 0,
|
|
414
|
+
ErrorMessage: result.ErrorMessage || '',
|
|
415
|
+
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
416
|
+
CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
|
|
417
|
+
CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Batch query execution with system user privileges
|
|
424
|
+
*/
|
|
425
|
+
@RequireSystemUser()
|
|
426
|
+
@Query(() => [RunQueryResultType])
|
|
427
|
+
async RunQueriesSystemUser(
|
|
428
|
+
@Arg('input', () => [RunQueryInput]) input: RunQueryInput[],
|
|
429
|
+
@Ctx() context: AppContext
|
|
430
|
+
): Promise<RunQueryResultType[]> {
|
|
431
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
432
|
+
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
433
|
+
|
|
434
|
+
// Convert input to RunQueryParams array
|
|
435
|
+
const params: RunQueryParams[] = input.map(i => ({
|
|
436
|
+
QueryID: i.QueryID,
|
|
437
|
+
QueryName: i.QueryName,
|
|
438
|
+
CategoryID: i.CategoryID,
|
|
439
|
+
CategoryPath: i.CategoryPath,
|
|
440
|
+
Parameters: i.Parameters,
|
|
441
|
+
MaxRows: i.MaxRows,
|
|
442
|
+
StartRow: i.StartRow,
|
|
443
|
+
ForceAuditLog: i.ForceAuditLog,
|
|
444
|
+
AuditLogDescription: i.AuditLogDescription
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
// Execute all queries in parallel using the batch method
|
|
448
|
+
const results = await rq.RunQueries(params, context.userPayload.userRecord);
|
|
449
|
+
|
|
450
|
+
// Map results to output format
|
|
451
|
+
return results.map((result, index) => {
|
|
452
|
+
const inputItem = input[index];
|
|
453
|
+
return {
|
|
454
|
+
QueryID: result.QueryID || inputItem.QueryID || '',
|
|
455
|
+
QueryName: result.QueryName || inputItem.QueryName || 'Unknown Query',
|
|
456
|
+
Success: result.Success ?? false,
|
|
457
|
+
Results: JSON.stringify(result.Results ?? null),
|
|
458
|
+
RowCount: result.RowCount ?? 0,
|
|
459
|
+
TotalRowCount: result.TotalRowCount ?? 0,
|
|
460
|
+
ExecutionTime: result.ExecutionTime ?? 0,
|
|
461
|
+
ErrorMessage: result.ErrorMessage || '',
|
|
462
|
+
AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
|
|
463
|
+
CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
|
|
464
|
+
CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* RunQueriesWithCacheCheck - Smart cache validation for batch RunQueries.
|
|
471
|
+
* For each query, if cacheStatus is provided, the server checks if the cache is current
|
|
472
|
+
* using the Query's CacheValidationSQL. If current, returns status='current' with no data.
|
|
473
|
+
* If stale, returns status='stale' with fresh data.
|
|
474
|
+
* If the Query doesn't have CacheValidationSQL configured, returns 'no_validation' with data.
|
|
475
|
+
*/
|
|
476
|
+
@Query(() => RunQueriesWithCacheCheckOutput)
|
|
477
|
+
async RunQueriesWithCacheCheck(
|
|
478
|
+
@Arg('input', () => [RunQueryWithCacheCheckInput]) input: RunQueryWithCacheCheckInput[],
|
|
479
|
+
@Ctx() context: AppContext
|
|
480
|
+
): Promise<RunQueriesWithCacheCheckOutput> {
|
|
481
|
+
try {
|
|
482
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
483
|
+
|
|
484
|
+
// Cast provider to SQLServerDataProvider to access RunQueriesWithCacheCheck method
|
|
485
|
+
const sqlProvider = provider as unknown as SQLServerDataProvider;
|
|
486
|
+
if (!sqlProvider.RunQueriesWithCacheCheck) {
|
|
487
|
+
throw new Error('Provider does not support RunQueriesWithCacheCheck');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Convert GraphQL input types to core types
|
|
491
|
+
const coreParams: RunQueryWithCacheCheckParams[] = input.map(item => ({
|
|
492
|
+
params: {
|
|
493
|
+
QueryID: item.params.QueryID,
|
|
494
|
+
QueryName: item.params.QueryName,
|
|
495
|
+
CategoryID: item.params.CategoryID,
|
|
496
|
+
CategoryPath: item.params.CategoryPath,
|
|
497
|
+
Parameters: item.params.Parameters,
|
|
498
|
+
MaxRows: item.params.MaxRows,
|
|
499
|
+
StartRow: item.params.StartRow,
|
|
500
|
+
ForceAuditLog: item.params.ForceAuditLog,
|
|
501
|
+
AuditLogDescription: item.params.AuditLogDescription,
|
|
502
|
+
},
|
|
503
|
+
cacheStatus: item.cacheStatus ? {
|
|
504
|
+
maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
|
|
505
|
+
rowCount: item.cacheStatus.rowCount,
|
|
506
|
+
} : undefined,
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
const response = await sqlProvider.RunQueriesWithCacheCheck(coreParams, context.userPayload.userRecord);
|
|
510
|
+
|
|
511
|
+
// Transform results to GraphQL output format
|
|
512
|
+
const transformedResults: RunQueryWithCacheCheckResultOutput[] = response.results.map(result => ({
|
|
513
|
+
queryIndex: result.queryIndex,
|
|
514
|
+
queryId: result.queryId,
|
|
515
|
+
status: result.status,
|
|
516
|
+
Results: result.results ? JSON.stringify(result.results) : undefined,
|
|
517
|
+
maxUpdatedAt: result.maxUpdatedAt,
|
|
518
|
+
rowCount: result.rowCount,
|
|
519
|
+
errorMessage: result.errorMessage,
|
|
520
|
+
}));
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
success: response.success,
|
|
524
|
+
results: transformedResults,
|
|
525
|
+
errorMessage: response.errorMessage,
|
|
526
|
+
};
|
|
527
|
+
} catch (err) {
|
|
528
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
529
|
+
LogError(err);
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
results: [],
|
|
533
|
+
errorMessage,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* RunQueriesWithCacheCheck with system user privileges
|
|
540
|
+
*/
|
|
541
|
+
@RequireSystemUser()
|
|
542
|
+
@Query(() => RunQueriesWithCacheCheckOutput)
|
|
543
|
+
async RunQueriesWithCacheCheckSystemUser(
|
|
544
|
+
@Arg('input', () => [RunQueryWithCacheCheckInput]) input: RunQueryWithCacheCheckInput[],
|
|
545
|
+
@Ctx() context: AppContext
|
|
546
|
+
): Promise<RunQueriesWithCacheCheckOutput> {
|
|
547
|
+
// Same implementation as regular version - RequireSystemUser handles auth
|
|
548
|
+
return this.RunQueriesWithCacheCheck(input, context);
|
|
549
|
+
}
|
|
276
550
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID } from 'type-graphql';
|
|
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
|
-
import { DatabaseProviderBase, LogError, LogStatus, Metadata, UserInfo } from '@memberjunction/core';
|
|
4
|
-
import {
|
|
3
|
+
import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
+
import { ConversationDetailEntity, ConversationDetailAttachmentEntity, UserNotificationEntity } from '@memberjunction/core-entities';
|
|
5
5
|
import { AgentRunner } from '@memberjunction/ai-agents';
|
|
6
|
-
import { ExecuteAgentResult } from '@memberjunction/ai-core-plus';
|
|
6
|
+
import { AIAgentEntityExtended, AIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
|
|
7
7
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
8
|
+
import { ChatMessage } from '@memberjunction/ai';
|
|
8
9
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
9
10
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
10
11
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
11
12
|
import { GetReadWriteProvider } from '../util.js';
|
|
12
13
|
import { SafeJSONParse } from '@memberjunction/global';
|
|
14
|
+
import { getAttachmentService } from '@memberjunction/aiengine';
|
|
13
15
|
|
|
14
16
|
@ObjectType()
|
|
15
17
|
export class AIAgentRunResult {
|
|
@@ -699,4 +701,190 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
699
701
|
}
|
|
700
702
|
}
|
|
701
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Optimized mutation that loads conversation history server-side.
|
|
706
|
+
* This avoids sending large attachment data from client to server.
|
|
707
|
+
*
|
|
708
|
+
* @param conversationDetailId - The conversation detail ID (user's message already saved)
|
|
709
|
+
* @param agentId - The agent to execute
|
|
710
|
+
* @param maxHistoryMessages - Maximum number of history messages to include (default: 20)
|
|
711
|
+
*/
|
|
712
|
+
@Mutation(() => AIAgentRunResult)
|
|
713
|
+
async RunAIAgentFromConversationDetail(
|
|
714
|
+
@Arg('conversationDetailId') conversationDetailId: string,
|
|
715
|
+
@Arg('agentId') agentId: string,
|
|
716
|
+
@Ctx() { userPayload, providers, dataSource }: AppContext,
|
|
717
|
+
@Arg('sessionId') sessionId: string,
|
|
718
|
+
@PubSub() pubSub: PubSubEngine,
|
|
719
|
+
@Arg('maxHistoryMessages', () => Int, { nullable: true }) maxHistoryMessages?: number,
|
|
720
|
+
@Arg('data', { nullable: true }) data?: string,
|
|
721
|
+
@Arg('payload', { nullable: true }) payload?: string,
|
|
722
|
+
@Arg('lastRunId', { nullable: true }) lastRunId?: string,
|
|
723
|
+
@Arg('autoPopulateLastRunPayload', { nullable: true }) autoPopulateLastRunPayload?: boolean,
|
|
724
|
+
@Arg('configurationId', { nullable: true }) configurationId?: string,
|
|
725
|
+
@Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
|
|
726
|
+
@Arg('createNotification', { nullable: true }) createNotification?: boolean,
|
|
727
|
+
@Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
|
|
728
|
+
@Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
|
|
729
|
+
): Promise<AIAgentRunResult> {
|
|
730
|
+
const p = GetReadWriteProvider(providers);
|
|
731
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
732
|
+
|
|
733
|
+
if (!currentUser) {
|
|
734
|
+
return {
|
|
735
|
+
success: false,
|
|
736
|
+
errorMessage: 'Unable to determine current user',
|
|
737
|
+
result: JSON.stringify({ success: false, errorMessage: 'Unable to determine current user' })
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
// Load conversation history with attachments from DB
|
|
743
|
+
const messages = await this.loadConversationHistoryWithAttachments(
|
|
744
|
+
conversationDetailId,
|
|
745
|
+
currentUser,
|
|
746
|
+
maxHistoryMessages || 20
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
// Convert to JSON string for the existing executeAIAgent method
|
|
750
|
+
const messagesJson = JSON.stringify(messages);
|
|
751
|
+
|
|
752
|
+
// Delegate to existing implementation
|
|
753
|
+
return this.executeAIAgent(
|
|
754
|
+
p,
|
|
755
|
+
dataSource,
|
|
756
|
+
agentId,
|
|
757
|
+
userPayload,
|
|
758
|
+
messagesJson,
|
|
759
|
+
sessionId,
|
|
760
|
+
pubSub,
|
|
761
|
+
data,
|
|
762
|
+
payload,
|
|
763
|
+
undefined, // templateData
|
|
764
|
+
lastRunId,
|
|
765
|
+
autoPopulateLastRunPayload,
|
|
766
|
+
configurationId,
|
|
767
|
+
conversationDetailId,
|
|
768
|
+
createArtifacts || false,
|
|
769
|
+
createNotification || false,
|
|
770
|
+
sourceArtifactId,
|
|
771
|
+
sourceArtifactVersionId
|
|
772
|
+
);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
|
|
775
|
+
LogError(`RunAIAgentFromConversationDetail failed: ${errorMessage}`, undefined, error);
|
|
776
|
+
return {
|
|
777
|
+
success: false,
|
|
778
|
+
errorMessage,
|
|
779
|
+
result: JSON.stringify({ success: false, errorMessage })
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Load conversation history with attachments from database.
|
|
786
|
+
* Builds ChatMessage[] with multimodal content blocks for attachments.
|
|
787
|
+
*/
|
|
788
|
+
private async loadConversationHistoryWithAttachments(
|
|
789
|
+
conversationDetailId: string,
|
|
790
|
+
contextUser: UserInfo,
|
|
791
|
+
maxMessages: number
|
|
792
|
+
): Promise<ChatMessage[]> {
|
|
793
|
+
const md = new Metadata();
|
|
794
|
+
const rv = new RunView();
|
|
795
|
+
const attachmentService = getAttachmentService();
|
|
796
|
+
|
|
797
|
+
// Load the current conversation detail to get the conversation ID
|
|
798
|
+
const currentDetail = await md.GetEntityObject<ConversationDetailEntity>(
|
|
799
|
+
'Conversation Details',
|
|
800
|
+
contextUser
|
|
801
|
+
);
|
|
802
|
+
if (!await currentDetail.Load(conversationDetailId)) {
|
|
803
|
+
throw new Error(`Conversation detail ${conversationDetailId} not found`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const conversationId = currentDetail.ConversationID;
|
|
807
|
+
|
|
808
|
+
// Load recent conversation details (messages) for this conversation
|
|
809
|
+
const detailsResult = await rv.RunView<ConversationDetailEntity>({
|
|
810
|
+
EntityName: 'Conversation Details',
|
|
811
|
+
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
812
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
813
|
+
MaxRows: maxMessages,
|
|
814
|
+
ResultType: 'entity_object'
|
|
815
|
+
}, contextUser);
|
|
816
|
+
|
|
817
|
+
if (!detailsResult.Success || !detailsResult.Results) {
|
|
818
|
+
throw new Error('Failed to load conversation history');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Reverse to get chronological order (oldest first)
|
|
822
|
+
const details = detailsResult.Results.reverse();
|
|
823
|
+
|
|
824
|
+
// Get all message IDs for batch loading attachments
|
|
825
|
+
const messageIds = details.map(d => d.ID);
|
|
826
|
+
|
|
827
|
+
// Batch load all attachments for these messages
|
|
828
|
+
const attachmentsByDetailId = await attachmentService.getAttachmentsBatch(messageIds, contextUser);
|
|
829
|
+
|
|
830
|
+
// Build ChatMessage array with attachments
|
|
831
|
+
const messages: ChatMessage[] = [];
|
|
832
|
+
|
|
833
|
+
for (const detail of details) {
|
|
834
|
+
const role = this.mapDetailRoleToMessageRole(detail.Role);
|
|
835
|
+
const attachments = attachmentsByDetailId.get(detail.ID) || [];
|
|
836
|
+
|
|
837
|
+
// Get attachment data with content URLs (handles both inline and FileID storage)
|
|
838
|
+
const attachmentDataPromises = attachments.map(att =>
|
|
839
|
+
attachmentService.getAttachmentData(att, contextUser)
|
|
840
|
+
);
|
|
841
|
+
const attachmentDataResults = await Promise.all(attachmentDataPromises);
|
|
842
|
+
|
|
843
|
+
// Filter out nulls and convert to AttachmentData format
|
|
844
|
+
const validAttachments: AttachmentData[] = attachmentDataResults
|
|
845
|
+
.filter((result): result is NonNullable<typeof result> => result !== null)
|
|
846
|
+
.map(result => ({
|
|
847
|
+
type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
|
|
848
|
+
mimeType: result.attachment.MimeType,
|
|
849
|
+
fileName: result.attachment.FileName ?? undefined,
|
|
850
|
+
sizeBytes: result.attachment.FileSizeBytes ?? undefined,
|
|
851
|
+
width: result.attachment.Width ?? undefined,
|
|
852
|
+
height: result.attachment.Height ?? undefined,
|
|
853
|
+
durationSeconds: result.attachment.DurationSeconds ?? undefined,
|
|
854
|
+
content: result.contentUrl
|
|
855
|
+
}));
|
|
856
|
+
|
|
857
|
+
// Build message content (with or without attachments)
|
|
858
|
+
let content: string | ReturnType<typeof ConversationUtility.BuildChatMessageContent>;
|
|
859
|
+
|
|
860
|
+
if (validAttachments.length > 0) {
|
|
861
|
+
// Use ConversationUtility to build multimodal content blocks
|
|
862
|
+
content = ConversationUtility.BuildChatMessageContent(
|
|
863
|
+
detail.Message || '',
|
|
864
|
+
validAttachments
|
|
865
|
+
);
|
|
866
|
+
} else {
|
|
867
|
+
content = detail.Message || '';
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
messages.push({
|
|
871
|
+
role,
|
|
872
|
+
content
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return messages;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Map ConversationDetail Role to ChatMessage role
|
|
881
|
+
*/
|
|
882
|
+
private mapDetailRoleToMessageRole(role: string): 'user' | 'assistant' | 'system' {
|
|
883
|
+
const roleLower = (role || '').toLowerCase();
|
|
884
|
+
if (roleLower === 'user') return 'user';
|
|
885
|
+
if (roleLower === 'assistant' || roleLower === 'agent') return 'assistant';
|
|
886
|
+
if (roleLower === 'system') return 'system';
|
|
887
|
+
return 'user'; // Default to user
|
|
888
|
+
}
|
|
889
|
+
|
|
702
890
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { DatabaseProviderBase, LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
4
|
-
import { AIPromptEntityExtended, AIModelEntityExtended } from '@memberjunction/core-
|
|
4
|
+
import { AIPromptEntityExtended, AIModelEntityExtended } from '@memberjunction/ai-core-plus';
|
|
5
5
|
import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
6
6
|
import { AIPromptParams } from '@memberjunction/ai-core-plus';
|
|
7
7
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -104,6 +104,7 @@ export class RunTestResolver extends ResolverBase {
|
|
|
104
104
|
@Arg('testId') testId: string,
|
|
105
105
|
@Arg('verbose', { nullable: true }) verbose: boolean = true,
|
|
106
106
|
@Arg('environment', { nullable: true }) environment?: string,
|
|
107
|
+
@Arg('tags', { nullable: true }) tags?: string,
|
|
107
108
|
@PubSub() pubSub?: PubSubEngine,
|
|
108
109
|
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
109
110
|
): Promise<TestRunResult> {
|
|
@@ -132,6 +133,7 @@ export class RunTestResolver extends ResolverBase {
|
|
|
132
133
|
const options = {
|
|
133
134
|
verbose,
|
|
134
135
|
environment,
|
|
136
|
+
tags,
|
|
135
137
|
progressCallback
|
|
136
138
|
};
|
|
137
139
|
|
|
@@ -210,6 +212,10 @@ export class RunTestResolver extends ResolverBase {
|
|
|
210
212
|
@Arg('verbose', { nullable: true }) verbose: boolean = true,
|
|
211
213
|
@Arg('environment', { nullable: true }) environment?: string,
|
|
212
214
|
@Arg('parallel', { nullable: true }) parallel: boolean = false,
|
|
215
|
+
@Arg('tags', { nullable: true }) tags?: string,
|
|
216
|
+
@Arg('selectedTestIds', { nullable: true }) selectedTestIds?: string,
|
|
217
|
+
@Arg('sequenceStart', () => Int, { nullable: true }) sequenceStart?: number,
|
|
218
|
+
@Arg('sequenceEnd', () => Int, { nullable: true }) sequenceEnd?: number,
|
|
213
219
|
@PubSub() pubSub?: PubSubEngine,
|
|
214
220
|
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
215
221
|
): Promise<TestSuiteRunResult> {
|
|
@@ -231,10 +237,24 @@ export class RunTestResolver extends ResolverBase {
|
|
|
231
237
|
this.createProgressCallback(pubSub, userPayload, suiteId) :
|
|
232
238
|
undefined;
|
|
233
239
|
|
|
240
|
+
// Parse selectedTestIds from JSON string if provided
|
|
241
|
+
let parsedSelectedTestIds: string[] | undefined;
|
|
242
|
+
if (selectedTestIds) {
|
|
243
|
+
try {
|
|
244
|
+
parsedSelectedTestIds = JSON.parse(selectedTestIds);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
LogError(`[RunTestResolver] Failed to parse selectedTestIds: ${selectedTestIds}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
const options = {
|
|
235
251
|
verbose,
|
|
236
252
|
environment,
|
|
237
253
|
parallel,
|
|
254
|
+
tags,
|
|
255
|
+
selectedTestIds: parsedSelectedTestIds,
|
|
256
|
+
sequenceStart,
|
|
257
|
+
sequenceEnd,
|
|
238
258
|
progressCallback
|
|
239
259
|
};
|
|
240
260
|
|
|
@@ -226,7 +226,7 @@ export class SyncDataResolver {
|
|
|
226
226
|
for (const entityObject of data.Results) {
|
|
227
227
|
if (!await entityObject.Delete()) {
|
|
228
228
|
overallSuccess = false;
|
|
229
|
-
combinedErrorMessage += 'Failed to delete the item :' + entityObject.LatestResult.
|
|
229
|
+
combinedErrorMessage += 'Failed to delete the item :' + entityObject.LatestResult.CompleteMessage + '\n';
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
result.Success = overallSuccess
|
|
@@ -316,7 +316,7 @@ export class SyncDataResolver {
|
|
|
316
316
|
result.Success = true;
|
|
317
317
|
}
|
|
318
318
|
else {
|
|
319
|
-
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.
|
|
319
|
+
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.CompleteMessage;
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
}
|
|
@@ -334,7 +334,7 @@ export class SyncDataResolver {
|
|
|
334
334
|
result.Success = true;
|
|
335
335
|
}
|
|
336
336
|
else {
|
|
337
|
-
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.
|
|
337
|
+
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.CompleteMessage;
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
else {
|
|
@@ -363,7 +363,7 @@ export class SyncDataResolver {
|
|
|
363
363
|
});
|
|
364
364
|
}
|
|
365
365
|
else {
|
|
366
|
-
result.ErrorMessage = 'Failed to create the item :' + entityObject.LatestResult.
|
|
366
|
+
result.ErrorMessage = 'Failed to create the item :' + entityObject.LatestResult.CompleteMessage;
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
@@ -406,7 +406,7 @@ export class SyncDataResolver {
|
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
408
|
else {
|
|
409
|
-
result.ErrorMessage = 'Failed to update the item :' + entityObject.LatestResult.
|
|
409
|
+
result.ErrorMessage = 'Failed to update the item :' + entityObject.LatestResult.CompleteMessage;
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
}
|