@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.
@@ -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 } 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 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 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
- // 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();
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
- //const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
71
- const OrganizationId = process.env.BOT_SCHEMA_ORGANIZATION_ID;
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 input = { userInput: UserQuestion, conversationID: ConversationId, organizationID: OrganizationId };
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: '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.',
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 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 };
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
- protected async getAnalysis(userQuestion: string, sql: string, sampleDataJSON: string): Promise<string> {
230
- return await SkipAnalyzeData(userQuestion, sql, sampleDataJSON);
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
- protected async getReportExplanation(userQuestion: string, sql: string): Promise<string> {
234
- return await SkipExplainQuery(userQuestion, sql);
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"]