@memberjunction/server 0.9.142 → 0.9.151
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 +21 -0
- package/dist/generated/generated.js +1616 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +145 -154
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +9 -8
- package/src/generated/generated.ts +1251 -2
- package/src/resolvers/AskSkipResolver.ts +184 -172
- package/tsconfig.json +2 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { AppContext } from '../types';
|
|
2
|
+
import { Metadata, UserInfo } from '@memberjunction/core';
|
|
3
|
+
import { AppContext, UserPayload } from '../types';
|
|
5
4
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
5
|
+
import { SkipDataContext, SkipDataContextItem, SkipAPIRequest, SkipAPIResponse } from '@memberjunction/skip-types';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
|
|
8
8
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver';
|
|
9
|
-
import { ConversationDetailEntity, ConversationEntity, UserNotificationEntity } from '@memberjunction/core-entities';
|
|
9
|
+
import { ConversationDetailEntity, ConversationEntity, UserNotificationEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
|
|
10
|
+
import { DataSource } from 'typeorm';
|
|
10
11
|
|
|
11
12
|
@ObjectType()
|
|
12
13
|
export class AskSkipResultType {
|
|
@@ -28,174 +29,151 @@ export class AskSkipResultType {
|
|
|
28
29
|
@Field(() => Int)
|
|
29
30
|
AIMessageConversationDetailId: number;
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
|
|
31
34
|
@Resolver(AskSkipResultType)
|
|
32
35
|
export class AskSkipResolver {
|
|
33
36
|
private static _defaultNewChatName = 'New Chat';
|
|
34
37
|
|
|
38
|
+
|
|
35
39
|
@Query(() => AskSkipResultType)
|
|
36
|
-
async
|
|
40
|
+
async ExecuteAskSkipAnalysisQuery(
|
|
37
41
|
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
42
|
+
@Arg('ViewId', () => Int) ViewId: number,
|
|
38
43
|
@Arg('ConversationId', () => Int) ConversationId: number,
|
|
39
44
|
@Ctx() { dataSource, userPayload }: AppContext,
|
|
40
45
|
@PubSub() pubSub: PubSubEngine
|
|
41
46
|
) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
const md = new Metadata();
|
|
48
|
+
const user = UserCache.Instance.Users.find((u) => u.Email === userPayload.email);
|
|
49
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
50
|
+
|
|
51
|
+
const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
|
|
52
|
+
if (!ConversationId || ConversationId <= 0) {
|
|
53
|
+
// create a new conversation id
|
|
54
|
+
convoEntity.NewRecord();
|
|
55
|
+
if (user) {
|
|
56
|
+
convoEntity.UserID = user.ID;
|
|
57
|
+
convoEntity.Name = AskSkipResolver._defaultNewChatName;
|
|
58
|
+
if (await convoEntity.Save()) ConversationId = convoEntity.ID;
|
|
59
|
+
else throw new Error(`Creating a new conversation failed`);
|
|
60
|
+
} else throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
61
|
+
} else {
|
|
62
|
+
await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// now, create a conversation detail record for the user message
|
|
66
|
+
const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
|
|
67
|
+
convoDetailEntity.NewRecord();
|
|
68
|
+
convoDetailEntity.ConversationID = ConversationId;
|
|
69
|
+
convoDetailEntity.Message = UserQuestion;
|
|
70
|
+
convoDetailEntity.Role = 'User';
|
|
71
|
+
convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
|
|
72
|
+
await convoDetailEntity.Save();
|
|
73
|
+
|
|
74
|
+
//const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
|
|
75
|
+
const OrganizationId = process.env.BOT_SCHEMA_ORGANIZATION_ID;
|
|
76
|
+
const dataContext: SkipDataContext = new SkipDataContext();
|
|
77
|
+
dataContext.Items.push(
|
|
78
|
+
{
|
|
79
|
+
Type: 'view',
|
|
80
|
+
RecordID: ViewId,
|
|
81
|
+
} as SkipDataContextItem
|
|
82
|
+
);
|
|
83
|
+
dataContext.Items.push(
|
|
84
|
+
{
|
|
85
|
+
Type: 'view',
|
|
86
|
+
RecordID: 123, //test adding an extra item to the data context
|
|
87
|
+
} as SkipDataContextItem
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const input: SkipAPIRequest = {
|
|
91
|
+
userInput: UserQuestion,
|
|
92
|
+
conversationID: ConversationId.toString(),
|
|
93
|
+
dataContext: dataContext,
|
|
94
|
+
organizationID: !isNaN(parseInt(OrganizationId)) ? parseInt(OrganizationId) : 0,
|
|
95
|
+
requestPhase: 'initial_request'
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const url = 'http://localhost:8000'
|
|
99
|
+
// const url = process.env.BOT_EXTERNAL_API_URL;
|
|
100
|
+
// TEMP - call the separate server, we'll move this to real skip server soon!!!!!
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
104
|
+
message: JSON.stringify({
|
|
105
|
+
type: 'AskSkip',
|
|
106
|
+
status: 'OK',
|
|
107
|
+
message: 'Sure, I can help with that, I will start by analyzing your request...',
|
|
108
|
+
}),
|
|
109
|
+
sessionId: userPayload.sessionId,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const response = await axios({
|
|
113
|
+
method: 'post',
|
|
114
|
+
url: url,
|
|
115
|
+
data: input,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (response.status === 200) {
|
|
119
|
+
const apiResponse = <SkipAPIResponse>response.data;
|
|
120
|
+
let sUserMessage: string = '';
|
|
121
|
+
switch (apiResponse.responsePhase) {
|
|
122
|
+
case 'data_request':
|
|
123
|
+
sUserMessage = 'We need to gather some more data, I will do that next and update you soon.';
|
|
124
|
+
break;
|
|
125
|
+
case 'analysis_complete':
|
|
126
|
+
sUserMessage = 'I have completed the analysis, the results will be available momentarily.';
|
|
127
|
+
break;
|
|
128
|
+
case 'clarifying_question':
|
|
129
|
+
sUserMessage = 'I have a clarifying question for you, please see review our chat so you can provide me a little more info.';
|
|
130
|
+
break;
|
|
59
131
|
}
|
|
60
132
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
// update the UI
|
|
134
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
135
|
+
message: JSON.stringify({
|
|
136
|
+
type: 'AskSkip',
|
|
137
|
+
status: 'OK',
|
|
138
|
+
message: sUserMessage,
|
|
139
|
+
}),
|
|
140
|
+
sessionId: userPayload.sessionId,
|
|
141
|
+
});
|
|
69
142
|
|
|
70
|
-
//
|
|
71
|
-
|
|
143
|
+
// now, based on the result type, we will either wait for the next phase or we will process the results
|
|
144
|
+
let nextAPIResponse: SkipAPIResponse | null = null;
|
|
145
|
+
if (apiResponse.responsePhase === 'data_request') {
|
|
146
|
+
nextAPIResponse = await this.HandleDataRequestPhase(apiResponse, user, dataSource, ConversationId, userPayload, pubSub);
|
|
147
|
+
}
|
|
148
|
+
else if (apiResponse.responsePhase === 'clarifying_question') {
|
|
149
|
+
// need to send the request back to the user for a clarifying question
|
|
150
|
+
// TO-DO implement this
|
|
151
|
+
}
|
|
152
|
+
else if (apiResponse.responsePhase === 'analysis_complete') {
|
|
153
|
+
nextAPIResponse = apiResponse;
|
|
154
|
+
}
|
|
72
155
|
|
|
73
|
-
const
|
|
74
|
-
//const url = 'https://tasioskipapi.azurewebsites.net/report';
|
|
75
|
-
const url = process.env.BOT_EXTERNAL_API_URL;
|
|
156
|
+
const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, md, user, convoEntity, pubSub, userPayload);
|
|
76
157
|
|
|
158
|
+
return {
|
|
159
|
+
Success: true,
|
|
160
|
+
Status: 'OK',
|
|
161
|
+
ConversationId: ConversationId,
|
|
162
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
163
|
+
AIMessageConversationDetailId: AIMessageConversationDetailID,
|
|
164
|
+
Result: JSON.stringify(response.data)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
77
168
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
78
169
|
message: JSON.stringify({
|
|
79
170
|
type: 'AskSkip',
|
|
80
|
-
status: '
|
|
81
|
-
message: '
|
|
171
|
+
status: 'Error',
|
|
172
|
+
message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
|
|
82
173
|
}),
|
|
83
174
|
sessionId: userPayload.sessionId,
|
|
84
175
|
});
|
|
85
176
|
|
|
86
|
-
const response = await axios({
|
|
87
|
-
method: 'post',
|
|
88
|
-
url: process.env.BOT_EXTERNAL_API_URL,
|
|
89
|
-
data: input,
|
|
90
|
-
});
|
|
91
|
-
if (response.status === 200) {
|
|
92
|
-
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
93
|
-
message: JSON.stringify({
|
|
94
|
-
type: 'AskSkip',
|
|
95
|
-
status: 'OK',
|
|
96
|
-
message: 'I created the report structure, now I will get the data for you and analyze the results... Back to ya soon!',
|
|
97
|
-
}),
|
|
98
|
-
sessionId: userPayload.sessionId,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// it worked, run the SQL and return the results
|
|
102
|
-
const sql = response.data.params.sql;
|
|
103
|
-
const [{ result, analysis }, explanation] = await Promise.all([
|
|
104
|
-
this.getSkipDataAndAnalysis(dataSource, UserQuestion, sql),
|
|
105
|
-
this.getReportExplanation(UserQuestion, sql),
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
const sTitle = response.data.params.reportTitle || response.data.params.chartTitle;
|
|
109
|
-
const sResult = JSON.stringify({
|
|
110
|
-
SQLResults: {
|
|
111
|
-
results: result,
|
|
112
|
-
sql: sql,
|
|
113
|
-
columns: response.data.params.columns,
|
|
114
|
-
},
|
|
115
|
-
UserMessage: '',
|
|
116
|
-
ReportExplanation: explanation,
|
|
117
|
-
Analysis: analysis,
|
|
118
|
-
ReportTitle: sTitle,
|
|
119
|
-
DisplayType: response.data.type,
|
|
120
|
-
DrillDownView: response.data.params.drillDownView,
|
|
121
|
-
DrillDownBaseViewField: response.data.params.drillDownBaseViewField,
|
|
122
|
-
DrillDownReportValueField: response.data.params.drillDownReportValueField,
|
|
123
|
-
ChartOptions: {
|
|
124
|
-
xAxis: response.data.params.xAxis,
|
|
125
|
-
xLabel: response.data.params.xLabel,
|
|
126
|
-
yAxis: response.data.params.yAxis,
|
|
127
|
-
yLabel: response.data.params.yLabel,
|
|
128
|
-
color: response.data.params.color,
|
|
129
|
-
yFormat: response.data.params.yFormat,
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// now, create a conversation detail record for the Skip response
|
|
134
|
-
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
135
|
-
convoDetailEntityAI.NewRecord();
|
|
136
|
-
convoDetailEntityAI.ConversationID = ConversationId;
|
|
137
|
-
convoDetailEntityAI.Message = sResult;
|
|
138
|
-
convoDetailEntityAI.Role = 'AI';
|
|
139
|
-
convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
|
|
140
|
-
await convoDetailEntityAI.Save();
|
|
141
|
-
|
|
142
|
-
// finally update the convo name if it is still the default
|
|
143
|
-
if (convoEntity.Name === AskSkipResolver._defaultNewChatName) {
|
|
144
|
-
convoEntity.Name = response.data.params.reportTitle || response.data.params.chartTitle || AskSkipResolver._defaultNewChatName;
|
|
145
|
-
await convoEntity.Save();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// now create a notification for the user
|
|
149
|
-
const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
|
|
150
|
-
userNotification.NewRecord();
|
|
151
|
-
userNotification.UserID = user.ID;
|
|
152
|
-
userNotification.Title = 'Report Created: ' + sTitle;
|
|
153
|
-
userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
|
|
154
|
-
userNotification.Unread = true;
|
|
155
|
-
userNotification.ResourceConfiguration = JSON.stringify({
|
|
156
|
-
type: 'askskip',
|
|
157
|
-
conversationId: ConversationId,
|
|
158
|
-
});
|
|
159
|
-
await userNotification.Save();
|
|
160
|
-
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
161
|
-
message: JSON.stringify({
|
|
162
|
-
type: 'UserNotifications',
|
|
163
|
-
status: 'OK',
|
|
164
|
-
details: {
|
|
165
|
-
action: 'create',
|
|
166
|
-
recordId: userNotification.ID,
|
|
167
|
-
},
|
|
168
|
-
}),
|
|
169
|
-
sessionId: userPayload.sessionId,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
Success: true,
|
|
174
|
-
Status: 'OK',
|
|
175
|
-
ConversationId: ConversationId,
|
|
176
|
-
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
177
|
-
AIMessageConversationDetailId: convoDetailEntityAI.ID,
|
|
178
|
-
Result: sResult,
|
|
179
|
-
};
|
|
180
|
-
} else return { Success: false, Status: 'Error', Result: `User Question ${UserQuestion} didn't work!` };
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(`Error occurred: ${error}`);
|
|
183
|
-
if (error.response) {
|
|
184
|
-
// The request was made and the server responded with a status code
|
|
185
|
-
// that falls out of the range of 2xx
|
|
186
|
-
console.log(error.response.data);
|
|
187
|
-
console.log(error.response.status);
|
|
188
|
-
console.log(error.response.headers);
|
|
189
|
-
} else if (error.request) {
|
|
190
|
-
// The request was made but no response was received
|
|
191
|
-
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
|
192
|
-
// http.ClientRequest in node.js
|
|
193
|
-
console.log(error.request);
|
|
194
|
-
} else {
|
|
195
|
-
// Something happened in setting up the request that triggered an Error
|
|
196
|
-
console.log('Error', error.message);
|
|
197
|
-
}
|
|
198
|
-
console.log(error.config);
|
|
199
177
|
return {
|
|
200
178
|
Success: false,
|
|
201
179
|
Status: 'Error',
|
|
@@ -207,31 +185,65 @@ export class AskSkipResolver {
|
|
|
207
185
|
}
|
|
208
186
|
}
|
|
209
187
|
|
|
210
|
-
protected async
|
|
211
|
-
|
|
212
|
-
const stringResult = JSON.stringify(result);
|
|
213
|
-
// next get average string length of each row in result
|
|
214
|
-
const maxStringLength = 2000;
|
|
215
|
-
const avgRowStringLength = stringResult.length / result.length;
|
|
216
|
-
// next get the subset we actually want to use, either entire result
|
|
217
|
-
// of if too long, then a subset of the rows from the result
|
|
218
|
-
let sampleDataJSON = stringResult;
|
|
219
|
-
if (stringResult.length > maxStringLength) {
|
|
220
|
-
const rowsToUse = result.length / (maxStringLength / avgRowStringLength);
|
|
221
|
-
const subsetResult = result.slice(0, rowsToUse);
|
|
222
|
-
sampleDataJSON = JSON.stringify(subsetResult);
|
|
223
|
-
}
|
|
224
|
-
// now get the analysis since we have the sample data
|
|
225
|
-
const analysis = await this.getAnalysis(userQuestion, sql, sampleDataJSON);
|
|
226
|
-
return { result, analysis };
|
|
188
|
+
protected async HandleDataRequestPhase(apiResponse: SkipAPIResponse, user: UserInfo, dataSource: DataSource, ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine): Promise<SkipAPIResponse> {
|
|
189
|
+
throw new Error('Method not implemented.');
|
|
227
190
|
}
|
|
228
191
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
192
|
+
/**
|
|
193
|
+
* 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.
|
|
194
|
+
* @param apiResponse
|
|
195
|
+
* @param md
|
|
196
|
+
* @param user
|
|
197
|
+
* @param convoEntity
|
|
198
|
+
* @param pubSub
|
|
199
|
+
* @param userPayload
|
|
200
|
+
* @returns
|
|
201
|
+
*/
|
|
202
|
+
protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIResponse, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
|
|
203
|
+
const sTitle = apiResponse.reportTitle;
|
|
204
|
+
const sResult = JSON.stringify(apiResponse);
|
|
205
|
+
|
|
206
|
+
// now, create a conversation detail record for the Skip response
|
|
207
|
+
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
208
|
+
convoDetailEntityAI.NewRecord();
|
|
209
|
+
convoDetailEntityAI.ConversationID = convoEntity.ID;
|
|
210
|
+
convoDetailEntityAI.Message = sResult;
|
|
211
|
+
convoDetailEntityAI.Role = 'AI';
|
|
212
|
+
convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
|
|
213
|
+
await convoDetailEntityAI.Save();
|
|
214
|
+
|
|
215
|
+
// finally update the convo name if it is still the default
|
|
216
|
+
if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle) {
|
|
217
|
+
convoEntity.Name = sTitle; // use the title from the response
|
|
218
|
+
await convoEntity.Save();
|
|
219
|
+
}
|
|
232
220
|
|
|
233
|
-
|
|
234
|
-
|
|
221
|
+
// now create a notification for the user
|
|
222
|
+
const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
|
|
223
|
+
userNotification.NewRecord();
|
|
224
|
+
userNotification.UserID = user.ID;
|
|
225
|
+
userNotification.Title = 'Report Created: ' + sTitle;
|
|
226
|
+
userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
|
|
227
|
+
userNotification.Unread = true;
|
|
228
|
+
userNotification.ResourceConfiguration = JSON.stringify({
|
|
229
|
+
type: 'askskip',
|
|
230
|
+
conversationId: convoEntity.ID,
|
|
231
|
+
});
|
|
232
|
+
await userNotification.Save();
|
|
233
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
234
|
+
message: JSON.stringify({
|
|
235
|
+
type: 'UserNotifications',
|
|
236
|
+
status: 'OK',
|
|
237
|
+
details: {
|
|
238
|
+
action: 'create',
|
|
239
|
+
recordId: userNotification.ID,
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
sessionId: userPayload.sessionId,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
AIMessageConversationDetailID: convoDetailEntityAI.ID
|
|
246
|
+
};
|
|
235
247
|
}
|
|
236
248
|
}
|
|
237
249
|
|
package/tsconfig.json
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"noImplicitReturns": true,
|
|
21
21
|
"noFallthroughCasesInSwitch": true,
|
|
22
22
|
"allowSyntheticDefaultImports": true,
|
|
23
|
-
"emitDecoratorMetadata": true
|
|
23
|
+
"emitDecoratorMetadata": true,
|
|
24
|
+
"skipLibCheck": true
|
|
24
25
|
},
|
|
25
26
|
"exclude": ["node_modules"],
|
|
26
27
|
"include": ["./src/**/*.ts"]
|