@memberjunction/server 0.9.164 → 0.9.166

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.
@@ -10,7 +10,7 @@ import { promisify } from 'util';
10
10
  const gzip = promisify(zlib.gzip);
11
11
 
12
12
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver';
13
- import { ConversationDetailEntity, ConversationEntity, UserNotificationEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
13
+ import { ConversationDetailEntity, ConversationEntity, DataContextEntity, DataContextItemEntity, UserNotificationEntity, UserViewEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
14
14
  import { DataSource } from 'typeorm';
15
15
  import { ___skipAPIOrgId, ___skipAPIurl } from '../config';
16
16
 
@@ -64,24 +64,83 @@ export class AskSkipResolver {
64
64
  @Query(() => AskSkipResultType)
65
65
  async ExecuteAskSkipAnalysisQuery(
66
66
  @Arg('UserQuestion', () => String) UserQuestion: string,
67
- @Arg('ViewId', () => Int) ViewId: number,
68
67
  @Arg('ConversationId', () => Int) ConversationId: number,
69
68
  @Ctx() { dataSource, userPayload }: AppContext,
70
- @PubSub() pubSub: PubSubEngine
69
+ @PubSub() pubSub: PubSubEngine,
70
+ @Arg('DataContextId', () => Int, { nullable: true }) DataContextId?: number
71
71
  ) {
72
72
  const md = new Metadata();
73
73
  const user = UserCache.Instance.Users.find((u) => u.Email === userPayload.email);
74
74
  if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
75
75
 
76
+ const {convoEntity, dataContextEntity, convoDetailEntity, dataContext} = await this.HandleSkipInitialObjectLoading(dataSource, ConversationId, UserQuestion, user, userPayload, md, DataContextId);
77
+
78
+ const OrganizationId = ___skipAPIOrgId;
79
+
80
+ // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
81
+ const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(dataSource, convoEntity.ID, AskSkipResolver._maxHistoricalMessages);
82
+
83
+ const input: SkipAPIRequest = {
84
+ messages: messages,
85
+ conversationID: ConversationId.toString(),
86
+ dataContext: dataContext,
87
+ organizationID: !isNaN(parseInt(OrganizationId)) ? parseInt(OrganizationId) : 0,
88
+ requestPhase: 'initial_request'
89
+ };
90
+
91
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
92
+ message: JSON.stringify({
93
+ type: 'AskSkip',
94
+ status: 'OK',
95
+ message: 'I will be happy to help and will start by analyzing your request...',
96
+ }),
97
+ sessionId: userPayload.sessionId,
98
+ });
99
+
100
+ return this.HandleSkipRequest(input, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
101
+ }
102
+
103
+
104
+ protected async HandleSkipInitialObjectLoading(dataSource: DataSource,
105
+ ConversationId: number,
106
+ UserQuestion: string,
107
+ user: UserInfo,
108
+ userPayload: UserPayload,
109
+ md: Metadata,
110
+ DataContextId: number): Promise<{convoEntity: ConversationEntity,
111
+ dataContextEntity: DataContextEntity,
112
+ convoDetailEntity: ConversationDetailEntity,
113
+ dataContext: SkipDataContext}> {
76
114
  const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
115
+ let dataContextEntity: DataContextEntity;
116
+
77
117
  if (!ConversationId || ConversationId <= 0) {
78
118
  // create a new conversation id
79
119
  convoEntity.NewRecord();
80
120
  if (user) {
81
121
  convoEntity.UserID = user.ID;
82
122
  convoEntity.Name = AskSkipResolver._defaultNewChatName;
83
- if (await convoEntity.Save())
123
+
124
+ dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
125
+ if (!DataContextId || DataContextId <= 0) {
126
+ dataContextEntity.NewRecord();
127
+ dataContextEntity.UserID = user.ID;
128
+ dataContextEntity.Name = 'Data Context for Skip Conversation';
129
+ if (!await dataContextEntity.Save())
130
+ throw new Error(`Creating a new data context failed`);
131
+ }
132
+ else {
133
+ await dataContextEntity.Load(DataContextId);
134
+ }
135
+ convoEntity.DataContextID = dataContextEntity.ID;
136
+ if (await convoEntity.Save()) {
84
137
  ConversationId = convoEntity.ID;
138
+ if (!DataContextId || dataContextEntity.ID <= 0) {
139
+ // only do this if we created a new data context for this conversation
140
+ dataContextEntity.Name += ` ${ConversationId}`;
141
+ await dataContextEntity.Save();
142
+ }
143
+ }
85
144
  else
86
145
  throw new Error(`Creating a new conversation failed`);
87
146
  }
@@ -90,8 +149,16 @@ export class AskSkipResolver {
90
149
  }
91
150
  } else {
92
151
  await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
152
+ dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
153
+
154
+ // note - we ignore the parameter DataContextId if it is passed in, we will use the data context from the conversation that is saved. If a user wants to change the data context for a convo, they can do that elsewhere
155
+ if (DataContextId && DataContextId > 0 && DataContextId !== convoEntity.DataContextID)
156
+ console.log(`AskSkipResolver: DataContextId ${DataContextId} was passed in but it was ignored because it was different than the DataContextID in the conversation ${convoEntity.DataContextID}`);
157
+
158
+ await dataContextEntity.Load(convoEntity.DataContextID);
93
159
  }
94
160
 
161
+
95
162
  // now, create a conversation detail record for the user message
96
163
  const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
97
164
  convoDetailEntity.NewRecord();
@@ -101,38 +168,65 @@ export class AskSkipResolver {
101
168
  convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
102
169
  await convoDetailEntity.Save();
103
170
 
104
- //const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
105
- const OrganizationId = ___skipAPIOrgId;
106
171
  const dataContext: SkipDataContext = new SkipDataContext();
107
- dataContext.Items.push(
108
- {
109
- Type: 'view',
110
- RecordID: ViewId,
111
- Data: await this.getViewData(ViewId, user),
112
- } as SkipDataContextItem
113
- );
172
+ const dciEntityInfo = md.Entities.find((e) => e.Name === 'Data Context Items');
173
+ if (!dciEntityInfo)
174
+ throw new Error(`Data Context Items entity not found`);
175
+
176
+ const sql = `SELECT * FROM ${dciEntityInfo.SchemaName}.${dciEntityInfo.BaseView} WHERE DataContextID = ${dataContextEntity.ID}`;
177
+ const result = await dataSource.query(sql);
178
+ if (!result)
179
+ throw new Error(`Error running SQL: ${sql}`);
180
+ else {
181
+ for (const r of result) {
182
+ const item = new SkipDataContextItem();
183
+ item.Type = r.Type;
184
+ switch (item.Type) {
185
+ case 'full_entity':
186
+ item.EntityID = r.EntityID;
187
+ break;
188
+ case 'single_record':
189
+ item.RecordID = r.RecordID;
190
+ item.EntityID = r.EntityID;
191
+ break;
192
+ case 'query':
193
+ item.QueryID = r.QueryID; // map the QueryID in our database to the RecordID field in the object model for runtime use
194
+ break;
195
+ case 'sql':
196
+ item.SQL = r.SQL;
197
+ break;
198
+ case 'view':
199
+ item.ViewID = r.ViewID;
200
+ item.EntityID = r.EntityID;
201
+ break;
202
+ }
203
+ if (item.EntityID) {
204
+ item.Entity = md.Entities.find((e) => e.ID === item.EntityID);
205
+ item.EntityName = item.Entity.Name;
206
+ if (item.Type === 'full_entity')
207
+ item.RecordName = item.EntityName;
208
+ }
209
+ if (item.Type === 'query' && item.QueryID) {
210
+ const q = md.Queries.find((q) => q.ID === item.QueryID);
211
+ item.RecordName = q?.Name;
212
+ }
213
+ if (item.Type === 'view' && item.ViewID) {
214
+ const v = await md.GetEntityObject<UserViewEntityExtended>('User Views', user);
215
+ await v.Load(item.ViewID);
216
+ item.RecordName = v.Name;
217
+ item.ViewEntity = v;
218
+ }
219
+ item.Data = r.Data && r.Data.length > 0 ? JSON.parse(r.Data) : item.Data; // parse the stored data if it was saved, otherwise leave it to whatever the object's default is
220
+ item.AdditionalDescription = r.AdditionalDescription;
221
+ item.DataContextItemID = r.ID;
222
+ dataContext.Items.push(item);
223
+ }
224
+ }
114
225
 
115
- // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
116
- const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(dataSource, convoEntity.ID, AskSkipResolver._maxHistoricalMessages);
117
226
 
118
- const input: SkipAPIRequest = {
119
- messages: messages,
120
- conversationID: ConversationId.toString(),
121
- dataContext: dataContext,
122
- organizationID: !isNaN(parseInt(OrganizationId)) ? parseInt(OrganizationId) : 0,
123
- requestPhase: 'initial_request'
124
- };
125
-
126
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
127
- message: JSON.stringify({
128
- type: 'AskSkip',
129
- status: 'OK',
130
- message: 'I will be happy to help and will start by analyzing your request...',
131
- }),
132
- sessionId: userPayload.sessionId,
133
- });
227
+ /// TODO next up we need to modify MJExplorer to handle the data context stuff and then we can finish this method
134
228
 
135
- return this.HandleSkipRequest(input, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity);
229
+ return {dataContext, convoEntity, dataContextEntity, convoDetailEntity};
136
230
  }
137
231
 
138
232
  protected async LoadConversationDetailsIntoSkipMessages(dataSource: DataSource, ConversationId: number, maxHistoricalMessages?: number): Promise<SkipMessage[]> {
@@ -191,7 +285,8 @@ export class AskSkipResolver {
191
285
 
192
286
  protected async HandleSkipRequest(input: SkipAPIRequest, UserQuestion: string, user: UserInfo, dataSource: DataSource,
193
287
  ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, md: Metadata,
194
- convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity): Promise<AskSkipResultType> {
288
+ convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
289
+ dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
195
290
  LogStatus(`Sending request to Skip API: ${___skipAPIurl}`)
196
291
 
197
292
  // Convert JSON payload to a Buffer and compress it
@@ -212,14 +307,14 @@ export class AskSkipResolver {
212
307
 
213
308
  // now, based on the result type, we will either wait for the next phase or we will process the results
214
309
  if (apiResponse.responsePhase === 'data_request') {
215
- return await this.HandleDataRequestPhase(input, <SkipAPIDataRequestResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
310
+ return await this.HandleDataRequestPhase(input, <SkipAPIDataRequestResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
216
311
  }
217
312
  else if (apiResponse.responsePhase === 'clarifying_question') {
218
313
  // need to send the request back to the user for a clarifying question
219
314
  return await this.HandleClarifyingQuestionPhase(input, <SkipAPIClarifyingQuestionResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
220
315
  }
221
316
  else if (apiResponse.responsePhase === 'analysis_complete') {
222
- return await this.HandleAnalysisComplete(input, <SkipAPIAnalysisCompleteResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
317
+ return await this.HandleAnalysisComplete(input, <SkipAPIAnalysisCompleteResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
223
318
  }
224
319
  else {
225
320
  // unknown response phase
@@ -274,11 +369,12 @@ export class AskSkipResolver {
274
369
  }
275
370
 
276
371
  protected async HandleAnalysisComplete(apiRequest: SkipAPIRequest, apiResponse: SkipAPIAnalysisCompleteResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
277
- ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity): Promise<AskSkipResultType> {
372
+ ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
373
+ dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
278
374
  // analysis is complete
279
375
  // all done, wrap things up
280
376
  const md = new Metadata();
281
- const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, md, user, convoEntity, pubSub, userPayload);
377
+ const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, dataContext, dataContextEntity, md, user, convoEntity, pubSub, userPayload);
282
378
 
283
379
  return {
284
380
  Success: true,
@@ -325,7 +421,8 @@ export class AskSkipResolver {
325
421
  }
326
422
 
327
423
  protected async HandleDataRequestPhase(apiRequest: SkipAPIRequest, apiResponse: SkipAPIDataRequestResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
328
- ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity): Promise<AskSkipResultType> {
424
+ ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
425
+ dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
329
426
  // 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
330
427
  try {
331
428
  const _maxDataGatheringRetries = 5;
@@ -352,9 +449,9 @@ export class AskSkipResolver {
352
449
  const item = new SkipDataContextItem();
353
450
  item.Type = 'sql';
354
451
  item.Data = result;
355
- item.RecordName = dr.text;
452
+ item.SQL = dr.text;
356
453
  item.AdditionalDescription = dr.description;
357
- apiRequest.dataContext.Items.push(item);
454
+ dataContext.Items.push(item);
358
455
  break;
359
456
  case "stored_query":
360
457
  const queryName = dr.text;
@@ -366,10 +463,10 @@ export class AskSkipResolver {
366
463
  const item = new SkipDataContextItem();
367
464
  item.Type = 'query';
368
465
  item.Data = result.Results;
369
- item.RecordID = query.ID;
466
+ item.QueryID = query.ID;
370
467
  item.RecordName = query.Name;
371
468
  item.AdditionalDescription = dr.description;
372
- apiRequest.dataContext.Items.push(item);
469
+ dataContext.Items.push(item);
373
470
  }
374
471
  else
375
472
  throw new Error(`Error running query ${queryName}`);
@@ -415,7 +512,7 @@ export class AskSkipResolver {
415
512
  apiRequest.requestPhase = 'data_gathering_response';
416
513
  }
417
514
  // we have all of the data now, add it to the data context and then submit it back to the Skip API
418
- return this.HandleSkipRequest(apiRequest, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity);
515
+ return this.HandleSkipRequest(apiRequest, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
419
516
  }
420
517
  catch (e) {
421
518
  LogError(e);
@@ -424,7 +521,7 @@ export class AskSkipResolver {
424
521
  }
425
522
 
426
523
  /**
427
- * This method will handle the process for an end of request where a user is notified of an AI message. The AI message is either the finished report or a clarifying question.
524
+ * This method will handle the process for an end of successful request where a user is notified of an AI message. The AI message is either the finished report or a clarifying question.
428
525
  * @param apiResponse
429
526
  * @param md
430
527
  * @param user
@@ -433,11 +530,11 @@ export class AskSkipResolver {
433
530
  * @param userPayload
434
531
  * @returns
435
532
  */
436
- protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
533
+ protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, dataContext: SkipDataContext, dataContextEntity: DataContextEntity, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
437
534
  const sTitle = apiResponse.reportTitle;
438
535
  const sResult = JSON.stringify(apiResponse);
439
536
 
440
- // now, create a conversation detail record for the Skip response
537
+ // Create a conversation detail record for the Skip response
441
538
  const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
442
539
  convoDetailEntityAI.NewRecord();
443
540
  convoDetailEntityAI.ConversationID = convoEntity.ID;
@@ -447,7 +544,7 @@ export class AskSkipResolver {
447
544
  await convoDetailEntityAI.Save();
448
545
 
449
546
  // finally update the convo name if it is still the default
450
- if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle) {
547
+ if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
451
548
  convoEntity.Name = sTitle; // use the title from the response
452
549
  await convoEntity.Save();
453
550
  }
@@ -464,6 +561,39 @@ export class AskSkipResolver {
464
561
  conversationId: convoEntity.ID,
465
562
  });
466
563
  await userNotification.Save();
564
+
565
+ // now, persist the data context items, first let's get
566
+ for (const item of dataContext.Items) {
567
+ const dciEntity = <DataContextItemEntity>await md.GetEntityObject('Data Context Items', user);
568
+ if (item.DataContextItemID > 0)
569
+ await dciEntity.Load(item.DataContextItemID);
570
+ else
571
+ dciEntity.NewRecord();
572
+ dciEntity.DataContextID = dataContextEntity.ID;
573
+ dciEntity.Type = item.Type;
574
+ switch (item.Type) {
575
+ case 'full_entity':
576
+ case 'single_record':
577
+ const e = item.Entity || md.Entities.find((e) => e.Name === item.EntityName);
578
+ dciEntity.EntityID = e.ID;
579
+ if (item.Type === 'single_record')
580
+ dciEntity.RecordID = item.RecordID;
581
+ break;
582
+ case 'view':
583
+ dciEntity.ViewID = item.ViewID;
584
+ break;
585
+ case 'query':
586
+ dciEntity.QueryID = item.QueryID;
587
+ break;
588
+ case 'sql':
589
+ dciEntity.SQL = item.SQL;
590
+ break;
591
+ }
592
+ dciEntity.DataJSON = JSON.stringify(item.Data);
593
+ await dciEntity.Save();
594
+ }
595
+
596
+ // send a UI update trhough pub-sub
467
597
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
468
598
  message: JSON.stringify({
469
599
  type: 'UserNotifications',
@@ -475,6 +605,7 @@ export class AskSkipResolver {
475
605
  }),
476
606
  sessionId: userPayload.sessionId,
477
607
  });
608
+
478
609
  return {
479
610
  AIMessageConversationDetailID: convoDetailEntityAI.ID
480
611
  };