@memberjunction/server 0.9.163 → 0.9.165
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/build.log.json +6 -0
- package/dist/generated/generated.js +560 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +89 -37
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +6 -6
- package/src/generated/generated.ts +433 -2
- package/src/resolvers/AskSkipResolver.ts +125 -48
|
@@ -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, UserViewEntityExtended } from '@memberjunction/core-entities';
|
|
14
14
|
import { DataSource } from 'typeorm';
|
|
15
15
|
import { ___skipAPIOrgId, ___skipAPIurl } from '../config';
|
|
16
16
|
|
|
@@ -73,25 +73,80 @@ export class AskSkipResolver {
|
|
|
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.HandleCreationOfEntityObjects(dataSource, ConversationId, UserQuestion, user, userPayload, md, ViewId);
|
|
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
|
+
protected async HandleCreationOfEntityObjects(dataSource: DataSource,
|
|
104
|
+
ConversationId: number,
|
|
105
|
+
UserQuestion: string,
|
|
106
|
+
user: UserInfo,
|
|
107
|
+
userPayload: UserPayload,
|
|
108
|
+
md: Metadata,
|
|
109
|
+
ViewId: number): Promise<{convoEntity: ConversationEntity,
|
|
110
|
+
dataContextEntity: DataContextEntity,
|
|
111
|
+
convoDetailEntity: ConversationDetailEntity,
|
|
112
|
+
dataContext: SkipDataContext}> {
|
|
76
113
|
const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
|
|
114
|
+
let dataContextEntity: DataContextEntity;
|
|
115
|
+
|
|
77
116
|
if (!ConversationId || ConversationId <= 0) {
|
|
78
117
|
// create a new conversation id
|
|
79
118
|
convoEntity.NewRecord();
|
|
80
119
|
if (user) {
|
|
81
120
|
convoEntity.UserID = user.ID;
|
|
82
121
|
convoEntity.Name = AskSkipResolver._defaultNewChatName;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
|
|
123
|
+
dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
|
|
124
|
+
dataContextEntity.NewRecord();
|
|
125
|
+
dataContextEntity.UserID = user.ID;
|
|
126
|
+
dataContextEntity.Name = 'Data Context for Skip Conversation';
|
|
127
|
+
if (await dataContextEntity.Save()) {
|
|
128
|
+
convoEntity.DataContextID = dataContextEntity.ID;
|
|
129
|
+
if (await convoEntity.Save()) {
|
|
130
|
+
ConversationId = convoEntity.ID;
|
|
131
|
+
dataContextEntity.Name += ` ${ConversationId}`;
|
|
132
|
+
await dataContextEntity.Save();
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
throw new Error(`Creating a new conversation failed`);
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
throw new Error(`Creating a new data context failed`);
|
|
87
139
|
}
|
|
88
140
|
else {
|
|
89
141
|
throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
90
142
|
}
|
|
91
143
|
} else {
|
|
92
144
|
await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
|
|
145
|
+
dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
|
|
146
|
+
await dataContextEntity.Load(convoEntity.DataContextID);
|
|
93
147
|
}
|
|
94
148
|
|
|
149
|
+
|
|
95
150
|
// now, create a conversation detail record for the user message
|
|
96
151
|
const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
|
|
97
152
|
convoDetailEntity.NewRecord();
|
|
@@ -101,38 +156,38 @@ export class AskSkipResolver {
|
|
|
101
156
|
convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
|
|
102
157
|
await convoDetailEntity.Save();
|
|
103
158
|
|
|
104
|
-
//const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
|
|
105
|
-
const OrganizationId = ___skipAPIOrgId;
|
|
106
159
|
const dataContext: SkipDataContext = new SkipDataContext();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
});
|
|
160
|
+
const dciEntityInfo = md.Entities.find((e) => e.Name === 'Data Context Items');
|
|
161
|
+
if (!dciEntityInfo)
|
|
162
|
+
throw new Error(`Data Context Items entity not found`);
|
|
163
|
+
|
|
164
|
+
const sql = `SELECT * FROM ${dciEntityInfo.SchemaName}.${dciEntityInfo.BaseView} WHERE DataContextID = ${dataContextEntity.ID}`;
|
|
165
|
+
const result = await dataSource.query(sql);
|
|
166
|
+
if (!result)
|
|
167
|
+
throw new Error(`Error running SQL: ${sql}`);
|
|
168
|
+
else {
|
|
169
|
+
for (const r of result) {
|
|
170
|
+
const item = new SkipDataContextItem();
|
|
171
|
+
item.Type = r.Type;
|
|
172
|
+
item.RecordID = r.RecordID;
|
|
173
|
+
item.RecordName = r.RecordName;
|
|
174
|
+
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
|
|
175
|
+
item.AdditionalDescription = r.AdditionalDescription;
|
|
176
|
+
dataContext.Items.push(item);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
134
179
|
|
|
135
|
-
|
|
180
|
+
// now if we don't already have this view in our data context, we will add it
|
|
181
|
+
if (ViewId && !dataContext.Items.find(i => i.Type === 'view' && i.RecordID === ViewId))
|
|
182
|
+
dataContext.Items.push(
|
|
183
|
+
{
|
|
184
|
+
Type: 'view',
|
|
185
|
+
RecordID: ViewId,
|
|
186
|
+
Data: await this.getViewData(ViewId, user),
|
|
187
|
+
} as SkipDataContextItem
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {dataContext, convoEntity, dataContextEntity, convoDetailEntity};
|
|
136
191
|
}
|
|
137
192
|
|
|
138
193
|
protected async LoadConversationDetailsIntoSkipMessages(dataSource: DataSource, ConversationId: number, maxHistoricalMessages?: number): Promise<SkipMessage[]> {
|
|
@@ -191,7 +246,8 @@ export class AskSkipResolver {
|
|
|
191
246
|
|
|
192
247
|
protected async HandleSkipRequest(input: SkipAPIRequest, UserQuestion: string, user: UserInfo, dataSource: DataSource,
|
|
193
248
|
ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, md: Metadata,
|
|
194
|
-
convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity
|
|
249
|
+
convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
|
|
250
|
+
dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
|
|
195
251
|
LogStatus(`Sending request to Skip API: ${___skipAPIurl}`)
|
|
196
252
|
|
|
197
253
|
// Convert JSON payload to a Buffer and compress it
|
|
@@ -212,14 +268,14 @@ export class AskSkipResolver {
|
|
|
212
268
|
|
|
213
269
|
// now, based on the result type, we will either wait for the next phase or we will process the results
|
|
214
270
|
if (apiResponse.responsePhase === 'data_request') {
|
|
215
|
-
return await this.HandleDataRequestPhase(input, <SkipAPIDataRequestResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
|
|
271
|
+
return await this.HandleDataRequestPhase(input, <SkipAPIDataRequestResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
|
|
216
272
|
}
|
|
217
273
|
else if (apiResponse.responsePhase === 'clarifying_question') {
|
|
218
274
|
// need to send the request back to the user for a clarifying question
|
|
219
275
|
return await this.HandleClarifyingQuestionPhase(input, <SkipAPIClarifyingQuestionResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
|
|
220
276
|
}
|
|
221
277
|
else if (apiResponse.responsePhase === 'analysis_complete') {
|
|
222
|
-
return await this.HandleAnalysisComplete(input, <SkipAPIAnalysisCompleteResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity);
|
|
278
|
+
return await this.HandleAnalysisComplete(input, <SkipAPIAnalysisCompleteResponse>apiResponse, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
|
|
223
279
|
}
|
|
224
280
|
else {
|
|
225
281
|
// unknown response phase
|
|
@@ -274,11 +330,12 @@ export class AskSkipResolver {
|
|
|
274
330
|
}
|
|
275
331
|
|
|
276
332
|
protected async HandleAnalysisComplete(apiRequest: SkipAPIRequest, apiResponse: SkipAPIAnalysisCompleteResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
|
|
277
|
-
ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity
|
|
333
|
+
ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
|
|
334
|
+
dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
|
|
278
335
|
// analysis is complete
|
|
279
336
|
// all done, wrap things up
|
|
280
337
|
const md = new Metadata();
|
|
281
|
-
const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, md, user, convoEntity, pubSub, userPayload);
|
|
338
|
+
const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, dataContext, dataContextEntity, md, user, convoEntity, pubSub, userPayload);
|
|
282
339
|
|
|
283
340
|
return {
|
|
284
341
|
Success: true,
|
|
@@ -325,7 +382,8 @@ export class AskSkipResolver {
|
|
|
325
382
|
}
|
|
326
383
|
|
|
327
384
|
protected async HandleDataRequestPhase(apiRequest: SkipAPIRequest, apiResponse: SkipAPIDataRequestResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
|
|
328
|
-
ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity
|
|
385
|
+
ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
|
|
386
|
+
dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
|
|
329
387
|
// 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
388
|
try {
|
|
331
389
|
const _maxDataGatheringRetries = 5;
|
|
@@ -354,7 +412,7 @@ export class AskSkipResolver {
|
|
|
354
412
|
item.Data = result;
|
|
355
413
|
item.RecordName = dr.text;
|
|
356
414
|
item.AdditionalDescription = dr.description;
|
|
357
|
-
|
|
415
|
+
dataContext.Items.push(item);
|
|
358
416
|
break;
|
|
359
417
|
case "stored_query":
|
|
360
418
|
const queryName = dr.text;
|
|
@@ -369,7 +427,7 @@ export class AskSkipResolver {
|
|
|
369
427
|
item.RecordID = query.ID;
|
|
370
428
|
item.RecordName = query.Name;
|
|
371
429
|
item.AdditionalDescription = dr.description;
|
|
372
|
-
|
|
430
|
+
dataContext.Items.push(item);
|
|
373
431
|
}
|
|
374
432
|
else
|
|
375
433
|
throw new Error(`Error running query ${queryName}`);
|
|
@@ -415,7 +473,7 @@ export class AskSkipResolver {
|
|
|
415
473
|
apiRequest.requestPhase = 'data_gathering_response';
|
|
416
474
|
}
|
|
417
475
|
// 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);
|
|
476
|
+
return this.HandleSkipRequest(apiRequest, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
|
|
419
477
|
}
|
|
420
478
|
catch (e) {
|
|
421
479
|
LogError(e);
|
|
@@ -424,7 +482,7 @@ export class AskSkipResolver {
|
|
|
424
482
|
}
|
|
425
483
|
|
|
426
484
|
/**
|
|
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.
|
|
485
|
+
* 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
486
|
* @param apiResponse
|
|
429
487
|
* @param md
|
|
430
488
|
* @param user
|
|
@@ -433,11 +491,11 @@ export class AskSkipResolver {
|
|
|
433
491
|
* @param userPayload
|
|
434
492
|
* @returns
|
|
435
493
|
*/
|
|
436
|
-
protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
|
|
494
|
+
protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, dataContext: SkipDataContext, dataContextEntity: DataContextEntity, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
|
|
437
495
|
const sTitle = apiResponse.reportTitle;
|
|
438
496
|
const sResult = JSON.stringify(apiResponse);
|
|
439
497
|
|
|
440
|
-
//
|
|
498
|
+
// Create a conversation detail record for the Skip response
|
|
441
499
|
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
442
500
|
convoDetailEntityAI.NewRecord();
|
|
443
501
|
convoDetailEntityAI.ConversationID = convoEntity.ID;
|
|
@@ -447,7 +505,7 @@ export class AskSkipResolver {
|
|
|
447
505
|
await convoDetailEntityAI.Save();
|
|
448
506
|
|
|
449
507
|
// finally update the convo name if it is still the default
|
|
450
|
-
if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle) {
|
|
508
|
+
if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
|
|
451
509
|
convoEntity.Name = sTitle; // use the title from the response
|
|
452
510
|
await convoEntity.Save();
|
|
453
511
|
}
|
|
@@ -464,6 +522,24 @@ export class AskSkipResolver {
|
|
|
464
522
|
conversationId: convoEntity.ID,
|
|
465
523
|
});
|
|
466
524
|
await userNotification.Save();
|
|
525
|
+
|
|
526
|
+
// now, persist the data context items, first let's get
|
|
527
|
+
for (const item of dataContext.Items) {
|
|
528
|
+
const dciEntity = <DataContextItemEntity>await md.GetEntityObject('Data Context Items', user);
|
|
529
|
+
if (item.DataContextItemID > 0)
|
|
530
|
+
await dciEntity.Load(item.DataContextItemID);
|
|
531
|
+
else
|
|
532
|
+
dciEntity.NewRecord();
|
|
533
|
+
dciEntity.DataContextID = dataContextEntity.ID;
|
|
534
|
+
dciEntity.Type = item.Type;
|
|
535
|
+
dciEntity.RecordID = item.RecordID;
|
|
536
|
+
if (item.Type === 'sql')
|
|
537
|
+
dciEntity.SQL = item.RecordName; // the SQL field in the database is where we store the SQL, in the object model it ends up in the RecordName property, mapping it here
|
|
538
|
+
dciEntity.DataJSON = JSON.stringify(item.Data);
|
|
539
|
+
await dciEntity.Save();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// send a UI update trhough pub-sub
|
|
467
543
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
468
544
|
message: JSON.stringify({
|
|
469
545
|
type: 'UserNotifications',
|
|
@@ -475,6 +551,7 @@ export class AskSkipResolver {
|
|
|
475
551
|
}),
|
|
476
552
|
sessionId: userPayload.sessionId,
|
|
477
553
|
});
|
|
554
|
+
|
|
478
555
|
return {
|
|
479
556
|
AIMessageConversationDetailID: convoDetailEntityAI.ID
|
|
480
557
|
};
|