@memberjunction/server 2.40.0 → 2.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/server",
3
- "version": "2.40.0",
3
+ "version": "2.41.0",
4
4
  "description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./src/index.ts",
@@ -22,27 +22,27 @@
22
22
  "dependencies": {
23
23
  "@apollo/server": "^4.9.1",
24
24
  "@graphql-tools/utils": "^10.0.1",
25
- "@memberjunction/actions": "2.40.0",
26
- "@memberjunction/ai": "2.40.0",
27
- "@memberjunction/ai-mistral": "2.40.0",
28
- "@memberjunction/ai-openai": "2.40.0",
29
- "@memberjunction/ai-vectors-pinecone": "2.40.0",
30
- "@memberjunction/aiengine": "2.40.0",
31
- "@memberjunction/core": "2.40.0",
32
- "@memberjunction/core-actions": "2.40.0",
33
- "@memberjunction/core-entities": "2.40.0",
34
- "@memberjunction/data-context": "2.40.0",
35
- "@memberjunction/data-context-server": "2.40.0",
36
- "@memberjunction/doc-utils": "2.40.0",
37
- "@memberjunction/entity-communications-server": "2.40.0",
38
- "@memberjunction/external-change-detection": "2.40.0",
39
- "@memberjunction/global": "2.40.0",
40
- "@memberjunction/graphql-dataprovider": "2.40.0",
41
- "@memberjunction/queue": "2.40.0",
42
- "@memberjunction/skip-types": "2.40.0",
43
- "@memberjunction/sqlserver-dataprovider": "2.40.0",
44
- "@memberjunction/storage": "2.40.0",
45
- "@memberjunction/templates": "2.40.0",
25
+ "@memberjunction/actions": "2.41.0",
26
+ "@memberjunction/ai": "2.41.0",
27
+ "@memberjunction/ai-mistral": "2.41.0",
28
+ "@memberjunction/ai-openai": "2.41.0",
29
+ "@memberjunction/ai-vectors-pinecone": "2.41.0",
30
+ "@memberjunction/aiengine": "2.41.0",
31
+ "@memberjunction/core": "2.41.0",
32
+ "@memberjunction/core-actions": "2.41.0",
33
+ "@memberjunction/core-entities": "2.41.0",
34
+ "@memberjunction/data-context": "2.41.0",
35
+ "@memberjunction/data-context-server": "2.41.0",
36
+ "@memberjunction/doc-utils": "2.41.0",
37
+ "@memberjunction/entity-communications-server": "2.41.0",
38
+ "@memberjunction/external-change-detection": "2.41.0",
39
+ "@memberjunction/global": "2.41.0",
40
+ "@memberjunction/graphql-dataprovider": "2.41.0",
41
+ "@memberjunction/queue": "2.41.0",
42
+ "@memberjunction/skip-types": "2.41.0",
43
+ "@memberjunction/sqlserver-dataprovider": "2.41.0",
44
+ "@memberjunction/storage": "2.41.0",
45
+ "@memberjunction/templates": "2.41.0",
46
46
  "@types/compression": "^1.7.5",
47
47
  "@types/cors": "^2.8.13",
48
48
  "@types/jsonwebtoken": "9.0.6",
@@ -6271,15 +6271,15 @@ export class User_ {
6271
6271
  @Field(() => [UserFavorite_])
6272
6272
  UserFavorites_UserIDArray: UserFavorite_[]; // Link to UserFavorites
6273
6273
 
6274
- @Field(() => [ResourceLink_])
6275
- ResourceLinks_UserIDArray: ResourceLink_[]; // Link to ResourceLinks
6276
-
6277
6274
  @Field(() => [ListCategory_])
6278
6275
  ListCategories_UserIDArray: ListCategory_[]; // Link to ListCategories
6279
6276
 
6280
6277
  @Field(() => [ScheduledAction_])
6281
6278
  ScheduledActions_CreatedByUserIDArray: ScheduledAction_[]; // Link to ScheduledActions
6282
6279
 
6280
+ @Field(() => [ResourceLink_])
6281
+ ResourceLinks_UserIDArray: ResourceLink_[]; // Link to ResourceLinks
6282
+
6283
6283
  @Field(() => [AIAgentRequest_])
6284
6284
  AIAgentRequests_ResponseByUserIDArray: AIAgentRequest_[]; // Link to AIAgentRequests
6285
6285
 
@@ -6720,15 +6720,6 @@ export class UserResolverBase extends ResolverBase {
6720
6720
  return result;
6721
6721
  }
6722
6722
 
6723
- @FieldResolver(() => [ResourceLink_])
6724
- async ResourceLinks_UserIDArray(@Root() user_: User_, @Ctx() { dataSources, userPayload }: AppContext, @PubSub() pubSub: PubSubEngine) {
6725
- this.CheckUserReadPermissions('Resource Links', userPayload);
6726
- const dataSource = GetReadOnlyDataSource(dataSources, { allowFallbackToReadWrite: true });
6727
- const sSQL = `SELECT * FROM [${Metadata.Provider.ConfigData.MJCoreSchemaName}].[vwResourceLinks] WHERE [UserID]='${user_.ID}' ` + this.getRowLevelSecurityWhereClause('Resource Links', userPayload, EntityPermissionType.Read, 'AND');
6728
- const result = this.ArrayMapFieldNamesToCodeNames('Resource Links', await dataSource.query(sSQL));
6729
- return result;
6730
- }
6731
-
6732
6723
  @FieldResolver(() => [ListCategory_])
6733
6724
  async ListCategories_UserIDArray(@Root() user_: User_, @Ctx() { dataSources, userPayload }: AppContext, @PubSub() pubSub: PubSubEngine) {
6734
6725
  this.CheckUserReadPermissions('List Categories', userPayload);
@@ -6747,6 +6738,15 @@ export class UserResolverBase extends ResolverBase {
6747
6738
  return result;
6748
6739
  }
6749
6740
 
6741
+ @FieldResolver(() => [ResourceLink_])
6742
+ async ResourceLinks_UserIDArray(@Root() user_: User_, @Ctx() { dataSources, userPayload }: AppContext, @PubSub() pubSub: PubSubEngine) {
6743
+ this.CheckUserReadPermissions('Resource Links', userPayload);
6744
+ const dataSource = GetReadOnlyDataSource(dataSources, { allowFallbackToReadWrite: true });
6745
+ const sSQL = `SELECT * FROM [${Metadata.Provider.ConfigData.MJCoreSchemaName}].[vwResourceLinks] WHERE [UserID]='${user_.ID}' ` + this.getRowLevelSecurityWhereClause('Resource Links', userPayload, EntityPermissionType.Read, 'AND');
6746
+ const result = this.ArrayMapFieldNamesToCodeNames('Resource Links', await dataSource.query(sSQL));
6747
+ return result;
6748
+ }
6749
+
6750
6750
  @FieldResolver(() => [AIAgentRequest_])
6751
6751
  async AIAgentRequests_ResponseByUserIDArray(@Root() user_: User_, @Ctx() { dataSources, userPayload }: AppContext, @PubSub() pubSub: PubSubEngine) {
6752
6752
  this.CheckUserReadPermissions('AI Agent Requests', userPayload);
@@ -15717,6 +15717,9 @@ export class ConversationDetail_ {
15717
15717
  @MaxLength(16)
15718
15718
  ArtifactVersionID?: string;
15719
15719
 
15720
+ @Field(() => Int, {nullable: true, description: `Duration in milliseconds representing how long the AI response processing took to complete for this conversation detail.`})
15721
+ CompletionTime?: number;
15722
+
15720
15723
  @Field({nullable: true})
15721
15724
  @MaxLength(510)
15722
15725
  Conversation?: string;
@@ -15777,6 +15780,9 @@ export class CreateConversationDetailInput {
15777
15780
 
15778
15781
  @Field({ nullable: true })
15779
15782
  ArtifactVersionID: string | null;
15783
+
15784
+ @Field(() => Int, { nullable: true })
15785
+ CompletionTime: number | null;
15780
15786
  }
15781
15787
 
15782
15788
 
@@ -15827,6 +15833,9 @@ export class UpdateConversationDetailInput {
15827
15833
  @Field({ nullable: true })
15828
15834
  ArtifactVersionID?: string | null;
15829
15835
 
15836
+ @Field(() => Int, { nullable: true })
15837
+ CompletionTime?: number | null;
15838
+
15830
15839
  @Field(() => [KeyValuePairInput], { nullable: true })
15831
15840
  OldValues___?: KeyValuePairInput[];
15832
15841
  }
@@ -15976,6 +15985,10 @@ export class Conversation_ {
15976
15985
  @MaxLength(10)
15977
15986
  _mj__UpdatedAt: Date;
15978
15987
 
15988
+ @Field({description: `Tracks the processing status of the conversation: Available, Processing`})
15989
+ @MaxLength(40)
15990
+ Status: string;
15991
+
15979
15992
  @Field()
15980
15993
  @MaxLength(200)
15981
15994
  User: string;
@@ -16030,6 +16043,9 @@ export class CreateConversationInput {
16030
16043
 
16031
16044
  @Field({ nullable: true })
16032
16045
  DataContextID: string | null;
16046
+
16047
+ @Field({ nullable: true })
16048
+ Status?: string;
16033
16049
  }
16034
16050
 
16035
16051
 
@@ -16068,6 +16084,9 @@ export class UpdateConversationInput {
16068
16084
  @Field({ nullable: true })
16069
16085
  DataContextID?: string | null;
16070
16086
 
16087
+ @Field({ nullable: true })
16088
+ Status?: string;
16089
+
16071
16090
  @Field(() => [KeyValuePairInput], { nullable: true })
16072
16091
  OldValues___?: KeyValuePairInput[];
16073
16092
  }
@@ -63,7 +63,6 @@ import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
63
63
  import { AIAgentEntityExtended, AIEngine } from '@memberjunction/aiengine';
64
64
  import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
65
65
  import e from 'express';
66
- import { Skip } from '@graphql-tools/utils';
67
66
 
68
67
  /**
69
68
  * Enumeration representing the different phases of a Skip response
@@ -1323,12 +1322,16 @@ cycle.`);
1323
1322
  @Ctx() { dataSource, userPayload }: AppContext,
1324
1323
  @PubSub() pubSub: PubSubEngine,
1325
1324
  @Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
1326
- @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
1325
+ @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean,
1326
+ @Arg('StartTime', () => Date, { nullable: true }) StartTime?: Date
1327
1327
  ) {
1328
1328
  const md = new Metadata();
1329
1329
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1330
1330
  if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
1331
1331
 
1332
+ // Record the start time if not provided
1333
+ const requestStartTime = StartTime || new Date();
1334
+
1332
1335
  const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
1333
1336
  dataSource,
1334
1337
  ConversationId,
@@ -1339,6 +1342,9 @@ cycle.`);
1339
1342
  DataContextId
1340
1343
  );
1341
1344
 
1345
+ // Set the conversation status to 'Processing' when a request is initiated
1346
+ this.setConversationStatus(convoEntity, 'Processing');
1347
+
1342
1348
  // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
1343
1349
  const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
1344
1350
  dataSource,
@@ -1363,6 +1369,7 @@ cycle.`);
1363
1369
  dataContext,
1364
1370
  dataContextEntity,
1365
1371
  conversationDetailCount,
1372
+ requestStartTime
1366
1373
  );
1367
1374
  }
1368
1375
 
@@ -1843,6 +1850,8 @@ cycle.`);
1843
1850
  if (user) {
1844
1851
  convoEntity.UserID = user.ID;
1845
1852
  convoEntity.Name = AskSkipResolver._defaultNewChatName;
1853
+ // Set initial status to Available since no processing has started yet
1854
+ convoEntity.Status = 'Available';
1846
1855
 
1847
1856
  dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
1848
1857
  if (!DataContextId || DataContextId.length === 0) {
@@ -1957,7 +1966,8 @@ cycle.`);
1957
1966
  protected async LoadConversationDetailsIntoSkipMessages(
1958
1967
  dataSource: DataSource,
1959
1968
  ConversationId: string,
1960
- maxHistoricalMessages?: number
1969
+ maxHistoricalMessages?: number,
1970
+ roleFilter?: string
1961
1971
  ): Promise<SkipMessage[]> {
1962
1972
  try {
1963
1973
  if (!ConversationId || ConversationId.length === 0) {
@@ -1967,12 +1977,16 @@ cycle.`);
1967
1977
  // load up all the conversation details from the database server
1968
1978
  const md = new Metadata();
1969
1979
  const e = md.Entities.find((e) => e.Name === 'Conversation Details');
1980
+
1981
+ // Add role filter if specified
1982
+ const roleFilterClause = roleFilter ? ` AND Role = '${roleFilter}'` : '';
1983
+
1970
1984
  const sql = `SELECT
1971
1985
  ${maxHistoricalMessages ? 'TOP ' + maxHistoricalMessages : ''} *
1972
1986
  FROM
1973
1987
  ${e.SchemaName}.${e.BaseView}
1974
1988
  WHERE
1975
- ConversationID = '${ConversationId}'
1989
+ ConversationID = '${ConversationId}'${roleFilterClause}
1976
1990
  ORDER
1977
1991
  BY __mj_CreatedAt DESC`;
1978
1992
  const result = await dataSource.query(sql);
@@ -2095,12 +2109,16 @@ cycle.`);
2095
2109
  convoDetailEntity: ConversationDetailEntity,
2096
2110
  dataContext: DataContext,
2097
2111
  dataContextEntity: DataContextEntity,
2098
- conversationDetailCount: number
2112
+ conversationDetailCount: number,
2113
+ startTime: Date
2099
2114
  ): Promise<AskSkipResultType> {
2100
2115
  const skipConfigInfo = configInfo.askSkip;
2101
2116
  LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${skipConfigInfo.chatURL}`);
2102
2117
 
2103
2118
  if (conversationDetailCount > 10) {
2119
+ // Set status of conversation to Available since we still want to allow the user to ask questions
2120
+ await this.setConversationStatus(convoEntity, 'Available');
2121
+
2104
2122
  // At this point it is likely that we are stuck in a loop, so we stop here
2105
2123
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2106
2124
  message: JSON.stringify({
@@ -2179,7 +2197,8 @@ cycle.`);
2179
2197
  convoDetailEntity,
2180
2198
  dataContext,
2181
2199
  dataContextEntity,
2182
- conversationDetailCount
2200
+ conversationDetailCount,
2201
+ startTime
2183
2202
  );
2184
2203
  } else if (apiResponse.responsePhase === 'clarifying_question') {
2185
2204
  // need to send the request back to the user for a clarifying question
@@ -2193,7 +2212,8 @@ cycle.`);
2193
2212
  userPayload,
2194
2213
  pubSub,
2195
2214
  convoEntity,
2196
- convoDetailEntity
2215
+ convoDetailEntity,
2216
+ startTime,
2197
2217
  );
2198
2218
  } else if (apiResponse.responsePhase === 'analysis_complete') {
2199
2219
  return await this.HandleAnalysisComplete(
@@ -2208,13 +2228,17 @@ cycle.`);
2208
2228
  convoEntity,
2209
2229
  convoDetailEntity,
2210
2230
  dataContext,
2211
- dataContextEntity
2231
+ dataContextEntity,
2232
+ startTime
2212
2233
  );
2213
2234
  } else {
2214
2235
  // unknown response phase
2215
2236
  throw new Error(`Unknown Skip API response phase: ${apiResponse.responsePhase}`);
2216
2237
  }
2217
2238
  } else {
2239
+ // Set status of conversation to Available since we still want to allow the user to ask questions
2240
+ await this.setConversationStatus(convoEntity, 'Available');
2241
+
2218
2242
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2219
2243
  message: JSON.stringify({
2220
2244
  type: 'AskSkip',
@@ -2308,7 +2332,8 @@ cycle.`);
2308
2332
  convoEntity: ConversationEntity,
2309
2333
  convoDetailEntity: ConversationDetailEntity,
2310
2334
  dataContext: DataContext,
2311
- dataContextEntity: DataContextEntity
2335
+ dataContextEntity: DataContextEntity,
2336
+ startTime: Date
2312
2337
  ): Promise<AskSkipResultType> {
2313
2338
  // analysis is complete
2314
2339
  // all done, wrap things up
@@ -2328,7 +2353,8 @@ cycle.`);
2328
2353
  convoEntity,
2329
2354
  pubSub,
2330
2355
  userPayload,
2331
- dataSource
2356
+ dataSource,
2357
+ startTime
2332
2358
  );
2333
2359
  const response: AskSkipResultType = {
2334
2360
  Success: true,
@@ -2368,9 +2394,11 @@ cycle.`);
2368
2394
  userPayload: UserPayload,
2369
2395
  pubSub: PubSubEngine,
2370
2396
  convoEntity: ConversationEntity,
2371
- convoDetailEntity: ConversationDetailEntity
2397
+ convoDetailEntity: ConversationDetailEntity,
2398
+ startTime: Date
2372
2399
  ): Promise<AskSkipResultType> {
2373
2400
  // need to create a message here in the COnversation and then pass that id below
2401
+ const endTime = new Date();
2374
2402
  const md = new Metadata();
2375
2403
  const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
2376
2404
  convoDetailEntityAI.NewRecord();
@@ -2378,6 +2406,11 @@ cycle.`);
2378
2406
  convoDetailEntityAI.Message = JSON.stringify(apiResponse); //.clarifyingQuestion;
2379
2407
  convoDetailEntityAI.Role = 'AI';
2380
2408
  convoDetailEntityAI.HiddenToUser = false;
2409
+ convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
2410
+
2411
+ // Set conversation status back to Available since we need user input for the clarifying question
2412
+ this.setConversationStatus(convoEntity, 'Available');
2413
+
2381
2414
  if (await convoDetailEntityAI.Save()) {
2382
2415
  return {
2383
2416
  Success: true,
@@ -2438,7 +2471,8 @@ cycle.`);
2438
2471
  convoDetailEntity: ConversationDetailEntity,
2439
2472
  dataContext: DataContext,
2440
2473
  dataContextEntity: DataContextEntity,
2441
- conversationDetailCount: number
2474
+ conversationDetailCount: number,
2475
+ startTime: Date
2442
2476
  ): Promise<AskSkipResultType> {
2443
2477
  // our job in this method is to go through each of the data requests from the Skip API, get the data, and then go back to the Skip API again and to the next phase
2444
2478
  try {
@@ -2567,7 +2601,8 @@ cycle.`);
2567
2601
  convoDetailEntity,
2568
2602
  dataContext,
2569
2603
  dataContextEntity,
2570
- conversationDetailCount
2604
+ conversationDetailCount,
2605
+ startTime
2571
2606
  );
2572
2607
  } catch (e) {
2573
2608
  LogError(e);
@@ -2599,7 +2634,8 @@ cycle.`);
2599
2634
  convoEntity: ConversationEntity,
2600
2635
  pubSub: PubSubEngine,
2601
2636
  userPayload: UserPayload,
2602
- dataSource: DataSource
2637
+ dataSource: DataSource,
2638
+ startTime: Date
2603
2639
  ): Promise<{ AIMessageConversationDetailID: string }> {
2604
2640
  const sTitle = apiResponse.reportTitle;
2605
2641
  const sResult = JSON.stringify(apiResponse);
@@ -2670,12 +2706,15 @@ cycle.`);
2670
2706
  }
2671
2707
 
2672
2708
  // Create a conversation detail record for the Skip response
2709
+ const endTime = new Date();
2673
2710
  const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
2674
2711
  convoDetailEntityAI.NewRecord();
2675
2712
  convoDetailEntityAI.ConversationID = convoEntity.ID;
2676
2713
  convoDetailEntityAI.Message = sResult;
2677
2714
  convoDetailEntityAI.Role = 'AI';
2678
2715
  convoDetailEntityAI.HiddenToUser = false;
2716
+ convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
2717
+
2679
2718
  if (artifactId && artifactId.length > 0) {
2680
2719
  // bind the new convo detail record to the artifact + version for this response
2681
2720
  convoDetailEntityAI.ArtifactID = artifactId;
@@ -2688,9 +2727,23 @@ cycle.`);
2688
2727
  LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
2689
2728
  }
2690
2729
 
2691
- // finally update the convo name if it is still the default
2730
+ // Update the conversation properties: name if it's the default, and set status back to 'Available'
2731
+ let needToSaveConvo = false;
2732
+
2733
+ // Update name if still default
2692
2734
  if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
2693
2735
  convoEntity.Name = sTitle; // use the title from the response
2736
+ needToSaveConvo = true;
2737
+ }
2738
+
2739
+ // Set status back to 'Available' since processing is complete
2740
+ if (convoEntity.Status === 'Processing') {
2741
+ convoEntity.Status = 'Available';
2742
+ needToSaveConvo = true;
2743
+ }
2744
+
2745
+ // Save if any changes were made
2746
+ if (needToSaveConvo) {
2694
2747
  const convoEntitySaveResult: boolean = await convoEntity.Save();
2695
2748
  if (!convoEntitySaveResult) {
2696
2749
  LogError(`Error saving conversation entity for AI message: ${sResult}`, undefined, convoEntity.LatestResult);
@@ -2749,6 +2802,18 @@ cycle.`);
2749
2802
  };
2750
2803
  }
2751
2804
 
2805
+ private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available'): Promise<boolean> {
2806
+ if (convoEntity.Status !== status) {
2807
+ convoEntity.Status = status;
2808
+ const convoSaveResult = await convoEntity.Save();
2809
+ if (!convoSaveResult) {
2810
+ LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
2811
+ }
2812
+ return convoSaveResult;
2813
+ }
2814
+ return true;
2815
+ }
2816
+
2752
2817
  /**
2753
2818
  * Gets the ID of an agent note type by its name
2754
2819
  * Falls back to a default note type if the specified one is not found