@memberjunction/server 0.9.142 → 0.9.153

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.
@@ -1,12 +1,13 @@
1
1
  import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { SkipAnalyzeData, SkipExplainQuery } from '@memberjunction/aiengine';
3
- import { Metadata } from '@memberjunction/core';
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, SkipMessage } 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,158 @@ 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 ExecuteAskSkipQuery(
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
- try {
43
- const md = new Metadata();
44
- const user = UserCache.Instance.Users.find((u) => u.Email === userPayload.email);
45
- if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
46
-
47
- const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
48
- if (!ConversationId || ConversationId <= 0) {
49
- // create a new conversation id
50
- convoEntity.NewRecord();
51
- if (user) {
52
- convoEntity.UserID = user.ID;
53
- convoEntity.Name = AskSkipResolver._defaultNewChatName;
54
- if (await convoEntity.Save()) ConversationId = convoEntity.ID;
55
- else throw new Error(`Creating a new conversation failed`);
56
- } else throw new Error(`User ${userPayload.email} not found in UserCache`);
57
- } else {
58
- await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
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 messages: SkipMessage[] = [
91
+ {
92
+ content: UserQuestion,
93
+ role: 'user'
59
94
  }
95
+ ];
96
+
97
+ const input: SkipAPIRequest = {
98
+ messages: messages,
99
+ conversationID: ConversationId.toString(),
100
+ dataContext: dataContext,
101
+ organizationID: !isNaN(parseInt(OrganizationId)) ? parseInt(OrganizationId) : 0,
102
+ requestPhase: 'initial_request'
103
+ };
60
104
 
61
- // now, create a conversation detail record for the user message
62
- const convoDetailEntity = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
63
- convoDetailEntity.NewRecord();
64
- convoDetailEntity.ConversationID = ConversationId;
65
- convoDetailEntity.Message = UserQuestion;
66
- convoDetailEntity.Role = 'User';
67
- convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
68
- await convoDetailEntity.Save();
105
+ const url = 'http://localhost:8000'
106
+ // const url = process.env.BOT_EXTERNAL_API_URL;
107
+ // TEMP - call the separate server, we'll move this to real skip server soon!!!!!
69
108
 
70
- //const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
71
- const OrganizationId = process.env.BOT_SCHEMA_ORGANIZATION_ID;
72
109
 
73
- const input = { userInput: UserQuestion, conversationID: ConversationId, organizationID: OrganizationId };
74
- //const url = 'https://tasioskipapi.azurewebsites.net/report';
75
- const url = process.env.BOT_EXTERNAL_API_URL;
110
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
111
+ message: JSON.stringify({
112
+ type: 'AskSkip',
113
+ status: 'OK',
114
+ message: 'Sure, I can help with that, I will start by analyzing your request...',
115
+ }),
116
+ sessionId: userPayload.sessionId,
117
+ });
76
118
 
119
+ const response = await axios({
120
+ method: 'post',
121
+ url: url,
122
+ data: input,
123
+ });
124
+
125
+ if (response.status === 200) {
126
+ const apiResponse = <SkipAPIResponse>response.data;
127
+ let sUserMessage: string = '';
128
+ switch (apiResponse.responsePhase) {
129
+ case 'data_request':
130
+ sUserMessage = 'We need to gather some more data, I will do that next and update you soon.';
131
+ break;
132
+ case 'analysis_complete':
133
+ sUserMessage = 'I have completed the analysis, the results will be available momentarily.';
134
+ break;
135
+ case 'clarifying_question':
136
+ sUserMessage = 'I have a clarifying question for you, please see review our chat so you can provide me a little more info.';
137
+ break;
138
+ }
139
+
140
+ // update the UI
77
141
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
78
142
  message: JSON.stringify({
79
143
  type: 'AskSkip',
80
144
  status: 'OK',
81
- message: 'Sure, I can help with that, just give me a second and I will think about the best way to complete your request.',
145
+ message: sUserMessage,
82
146
  }),
83
147
  sessionId: userPayload.sessionId,
84
148
  });
85
149
 
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);
150
+ // now, based on the result type, we will either wait for the next phase or we will process the results
151
+ let nextAPIResponse: SkipAPIResponse | null = null;
152
+ if (apiResponse.responsePhase === 'data_request') {
153
+ nextAPIResponse = await this.HandleDataRequestPhase(apiResponse, user, dataSource, ConversationId, userPayload, pubSub);
197
154
  }
198
- console.log(error.config);
155
+ else if (apiResponse.responsePhase === 'clarifying_question') {
156
+ // need to send the request back to the user for a clarifying question
157
+ // TO-DO implement this
158
+ }
159
+ else if (apiResponse.responsePhase === 'analysis_complete') {
160
+ nextAPIResponse = apiResponse;
161
+ }
162
+
163
+ const {AIMessageConversationDetailID} = await this.FinishConversationAndNotifyUser(apiResponse, md, user, convoEntity, pubSub, userPayload);
164
+
165
+ return {
166
+ Success: true,
167
+ Status: 'OK',
168
+ ConversationId: ConversationId,
169
+ UserMessageConversationDetailId: convoDetailEntity.ID,
170
+ AIMessageConversationDetailId: AIMessageConversationDetailID,
171
+ Result: JSON.stringify(response.data)
172
+ };
173
+ }
174
+ else {
175
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
176
+ message: JSON.stringify({
177
+ type: 'AskSkip',
178
+ status: 'Error',
179
+ message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
180
+ }),
181
+ sessionId: userPayload.sessionId,
182
+ });
183
+
199
184
  return {
200
185
  Success: false,
201
186
  Status: 'Error',
@@ -207,31 +192,65 @@ export class AskSkipResolver {
207
192
  }
208
193
  }
209
194
 
210
- protected async getSkipDataAndAnalysis(dataSource: any, userQuestion: string, sql: string): Promise<{ result: any[]; analysis: string }> {
211
- const result = await dataSource.query(sql);
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 };
195
+ protected async HandleDataRequestPhase(apiResponse: SkipAPIResponse, user: UserInfo, dataSource: DataSource, ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine): Promise<SkipAPIResponse> {
196
+ throw new Error('Method not implemented.');
227
197
  }
228
198
 
229
- protected async getAnalysis(userQuestion: string, sql: string, sampleDataJSON: string): Promise<string> {
230
- return await SkipAnalyzeData(userQuestion, sql, sampleDataJSON);
231
- }
199
+ /**
200
+ * 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.
201
+ * @param apiResponse
202
+ * @param md
203
+ * @param user
204
+ * @param convoEntity
205
+ * @param pubSub
206
+ * @param userPayload
207
+ * @returns
208
+ */
209
+ protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIResponse, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
210
+ const sTitle = apiResponse.reportTitle;
211
+ const sResult = JSON.stringify(apiResponse);
212
+
213
+ // now, create a conversation detail record for the Skip response
214
+ const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
215
+ convoDetailEntityAI.NewRecord();
216
+ convoDetailEntityAI.ConversationID = convoEntity.ID;
217
+ convoDetailEntityAI.Message = sResult;
218
+ convoDetailEntityAI.Role = 'AI';
219
+ convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
220
+ await convoDetailEntityAI.Save();
221
+
222
+ // finally update the convo name if it is still the default
223
+ if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle) {
224
+ convoEntity.Name = sTitle; // use the title from the response
225
+ await convoEntity.Save();
226
+ }
232
227
 
233
- protected async getReportExplanation(userQuestion: string, sql: string): Promise<string> {
234
- return await SkipExplainQuery(userQuestion, sql);
228
+ // now create a notification for the user
229
+ const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
230
+ userNotification.NewRecord();
231
+ userNotification.UserID = user.ID;
232
+ userNotification.Title = 'Report Created: ' + sTitle;
233
+ userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
234
+ userNotification.Unread = true;
235
+ userNotification.ResourceConfiguration = JSON.stringify({
236
+ type: 'askskip',
237
+ conversationId: convoEntity.ID,
238
+ });
239
+ await userNotification.Save();
240
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
241
+ message: JSON.stringify({
242
+ type: 'UserNotifications',
243
+ status: 'OK',
244
+ details: {
245
+ action: 'create',
246
+ recordId: userNotification.ID,
247
+ },
248
+ }),
249
+ sessionId: userPayload.sessionId,
250
+ });
251
+ return {
252
+ AIMessageConversationDetailID: convoDetailEntityAI.ID
253
+ };
235
254
  }
236
255
  }
237
256
 
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"]