@memberjunction/server 1.1.3 → 1.2.0

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,12 @@
1
1
  import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
2
+ import { LogError, LogStatus, Metadata, PrimaryKeyValue, RunView, UserInfo } from '@memberjunction/core';
3
3
  import { AppContext, UserPayload } from '../types';
4
4
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
5
  import { DataContext } from '@memberjunction/data-context'
6
6
  import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
7
7
  LoadDataContextItemsServer(); // prevent tree shaking since the DataContextItemServer class is not directly referenced in this file or otherwise statically instantiated, so it could be removed by the build process
8
8
 
9
- import { SkipAPIRequest, SkipAPIResponse, SkipMessage, SkipAPIAnalysisCompleteResponse, SkipAPIDataRequestResponse, SkipAPIClarifyingQuestionResponse, SkipEntityInfo, SkipQueryInfo, SkipAPIRunScriptRequest, SkipAPIRequestAPIKey } from '@memberjunction/skip-types';
9
+ import { SkipAPIRequest, SkipAPIResponse, SkipMessage, SkipAPIAnalysisCompleteResponse, SkipAPIDataRequestResponse, SkipAPIClarifyingQuestionResponse, SkipEntityInfo, SkipQueryInfo, SkipAPIRunScriptRequest, SkipAPIRequestAPIKey, SkipRequestPhase } from '@memberjunction/skip-types';
10
10
 
11
11
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver';
12
12
  import { ConversationDetailEntity, ConversationEntity, DataContextEntity, DataContextItemEntity, UserNotificationEntity } from '@memberjunction/core-entities';
@@ -18,6 +18,7 @@ import { registerEnumType } from "type-graphql";
18
18
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
19
19
  import { sendPostRequest } from '../util';
20
20
  import { GetAIAPIKey } from '@memberjunction/ai';
21
+ import { PrimaryKeyValueInputType } from './MergeRecordsResolver';
21
22
 
22
23
 
23
24
  enum SkipResponsePhase {
@@ -55,7 +56,7 @@ export class AskSkipResultType {
55
56
  @Field(() => Int)
56
57
  AIMessageConversationDetailId: number;
57
58
  }
58
-
59
+
59
60
 
60
61
  @Resolver(AskSkipResultType)
61
62
  export class AskSkipResolver {
@@ -63,49 +64,76 @@ export class AskSkipResolver {
63
64
  private static _maxHistoricalMessages = 20;
64
65
 
65
66
  /**
66
- * Executes a script in the context of a data context and returns the results
67
- * @param pubSub
68
- * @param DataContextId
69
- * @param ScriptText
67
+ * Handles a simple chat request from a user to Skip, using a particular data record
68
+ * @param UserQuestion the user's question
69
+ * @param EntityName the name of the entity for the record the user is discussing
70
+ * @param PrimaryKeys the primary keys of the record the user is discussing
70
71
  */
71
72
  @Query(() => AskSkipResultType)
72
- async ExecuteAskSkipRunScript(@Ctx() { dataSource, userPayload }: AppContext,
73
- @PubSub() pubSub: PubSubEngine,
74
- @Arg('DataContextId', () => Int) DataContextId: number,
75
- @Arg('ScriptText', () => String) ScriptText: string) {
76
- const md = new Metadata();
73
+ async ExecuteAskSkipRecordChat(@Arg('UserQuestion', () => String) UserQuestion: string,
74
+ @Arg('ConversationId', () => Int) ConversationId: number,
75
+ @Arg('EntityName', () => String) EntityName: string,
76
+ @Arg('PrimaryKeys', () => [PrimaryKeyValueInputType]) PrimaryKeys: PrimaryKeyValueInputType[],
77
+ @Ctx() { dataSource, userPayload }: AppContext,
78
+ @PubSub() pubSub: PubSubEngine) {
79
+ // In this function we're simply going to call the Skip API and pass along the message from the user
80
+
81
+ // first, get the user from the cache
77
82
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
78
- if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
79
- const dataContext: DataContext = new DataContext();
80
- await dataContext.Load(DataContextId, dataSource, true, user);
81
- const input: SkipAPIRunScriptRequest = {
82
- apiKeys: this.buildSkipAPIKeys(),
83
- scriptText: ScriptText,
84
- messages: [], // not needed for this request
85
- conversationID: '', // not needed for this request
86
- dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
87
- organizationID: !isNaN(parseInt(___skipAPIOrgId)) ? parseInt(___skipAPIOrgId) : 0,
88
- requestPhase: 'run_existing_script',
89
- entities: [], // not needed for this request
90
- queries: [], // not needed for this request
91
- };
83
+ if (!user)
84
+ throw new Error(`User ${userPayload.email} not found in UserCache`);
85
+
86
+ // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
87
+ const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(dataSource, ConversationId, AskSkipResolver._maxHistoricalMessages);
88
+
89
+ const md = new Metadata();
90
+ const {convoEntity, dataContextEntity, convoDetailEntity, dataContext} =
91
+ await this.HandleSkipInitialObjectLoading(dataSource, ConversationId, UserQuestion, user, userPayload, md, null);
92
92
 
93
+ // if we have a new conversation, update the data context to have an item for this record
94
+ if (!ConversationId || ConversationId <= 0) {
95
+ const dci = await md.GetEntityObject<DataContextItemEntity>('Data Context Items', user);
96
+ dci.DataContextID = dataContext.ID;
97
+ dci.Type = 'single_record';
98
+ dci.EntityID = md.Entities.find((e) => e.Name === EntityName)?.ID;
99
+ dci.RecordID = PrimaryKeys.map((pk) => pk.Value).join(',');
100
+ await dci.Save();
101
+
102
+ await dataContext.Load(dataContext.ID, dataSource, false, true, user); // load again because we added a new data context item
103
+
104
+ // also, in the situation for a new convo, we need to update the Conversation ID to have a LinkedEntity and LinkedRecord
105
+ convoEntity.LinkedEntityID = dci.EntityID;
106
+ convoEntity.LinkedRecordID = PrimaryKeys.map((pk) => pk.Value).join(',');
107
+ await convoEntity.Save();
108
+ }
109
+
110
+ const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false);
111
+ messages.push({
112
+ content: UserQuestion,
113
+ role: 'user'
114
+ });
115
+
116
+ return this.handleSimpleSkipPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
117
+ }
118
+
119
+ protected async handleSimpleSkipPostRequest(input: SkipAPIRequest, conversationID: number = 0, UserMessageConversationDetailId: number = 0, createAIMessageConversationDetail: boolean = false, user: UserInfo = null): Promise<AskSkipResultType> {
93
120
  LogStatus(` >>> Sending request to Skip API: ${___skipAPIurl}`)
94
121
 
95
122
  const response = await sendPostRequest(___skipAPIurl, input, true, null);
96
123
 
97
124
  if (response && response.length > 0) {
98
125
  // the last object in the response array is the final response from the Skip API
99
- const apiResponse = <SkipAPIResponse>response[response.length - 1];
126
+ const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
127
+ const AIMessageConversationDetailID = createAIMessageConversationDetail ? await this.CreateAIMessageConversationDetail(apiResponse, conversationID, user) : 0;
100
128
  // const apiResponse = <SkipAPIResponse>response.data;
101
129
  LogStatus(` Skip API response: ${apiResponse.responsePhase}`)
102
130
  return {
103
131
  Success: true,
104
132
  Status: 'OK',
105
133
  ResponsePhase: SkipResponsePhase.AnalysisComplete,
106
- ConversationId: 0,
107
- UserMessageConversationDetailId: 0,
108
- AIMessageConversationDetailId: 0,
134
+ ConversationId: conversationID,
135
+ UserMessageConversationDetailId: UserMessageConversationDetailId,
136
+ AIMessageConversationDetailId: AIMessageConversationDetailID,
109
137
  Result: JSON.stringify(apiResponse)
110
138
  };
111
139
  }
@@ -113,15 +141,67 @@ export class AskSkipResolver {
113
141
  return {
114
142
  Success: false,
115
143
  Status: 'Error',
116
- Result: `Report Refresh failed`,
144
+ Result: `Request failed`,
117
145
  ResponsePhase: SkipResponsePhase.AnalysisComplete,
118
- ConversationId: 0,
119
- UserMessageConversationDetailId: 0,
146
+ ConversationId: conversationID,
147
+ UserMessageConversationDetailId: UserMessageConversationDetailId,
120
148
  AIMessageConversationDetailId: 0,
121
149
  };
122
150
  }
123
151
  }
124
152
 
153
+ protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: number, user: UserInfo): Promise<number> {
154
+ const md = new Metadata();
155
+ const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
156
+ convoDetailEntityAI.NewRecord();
157
+ convoDetailEntityAI.ConversationID = conversationID;
158
+ const systemMessages = apiResponse.messages.filter((m) => m.role === 'system');
159
+ const lastSystemMessage = systemMessages[systemMessages.length - 1];
160
+ convoDetailEntityAI.Message = lastSystemMessage?.content;
161
+ convoDetailEntityAI.Role = 'AI';
162
+ if (await convoDetailEntityAI.Save()) {
163
+ return convoDetailEntityAI.ID;
164
+ }
165
+ else
166
+ return 0;
167
+ }
168
+
169
+ protected buildSkipAPIRequest(messages: SkipMessage[], conversationId: number, dataContext: DataContext, requestPhase: SkipRequestPhase, includeEntities: boolean, includeQueries: boolean): SkipAPIRequest {
170
+ const entities = includeEntities ? this.BuildSkipEntities() : [];
171
+ const queries = includeQueries ? this.BuildSkipQueries() : [];
172
+ const input: SkipAPIRequest = {
173
+ apiKeys: this.buildSkipAPIKeys(),
174
+ organizationInfo: configInfo?.askSkip?.organizationInfo,
175
+ messages: messages,
176
+ conversationID: conversationId.toString(),
177
+ dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
178
+ organizationID: !isNaN(parseInt(___skipAPIOrgId)) ? parseInt(___skipAPIOrgId) : 0,
179
+ requestPhase: requestPhase,
180
+ entities: entities,
181
+ queries: queries
182
+ };
183
+ return input;
184
+ }
185
+
186
+ /**
187
+ * Executes a script in the context of a data context and returns the results
188
+ * @param pubSub
189
+ * @param DataContextId
190
+ * @param ScriptText
191
+ */
192
+ @Query(() => AskSkipResultType)
193
+ async ExecuteAskSkipRunScript(@Ctx() { dataSource, userPayload }: AppContext,
194
+ @PubSub() pubSub: PubSubEngine,
195
+ @Arg('DataContextId', () => Int) DataContextId: number,
196
+ @Arg('ScriptText', () => String) ScriptText: string) {
197
+ const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
198
+ if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
199
+ const dataContext: DataContext = new DataContext();
200
+ await dataContext.Load(DataContextId, dataSource, true, true, user);
201
+ const input = this.buildSkipAPIRequest([], 0, dataContext, 'run_existing_script', false, false);
202
+ return this.handleSimpleSkipPostRequest(input);
203
+ }
204
+
125
205
  protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
126
206
  return [
127
207
  {
@@ -160,17 +240,7 @@ export class AskSkipResolver {
160
240
  // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
161
241
  const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(dataSource, convoEntity.ID, AskSkipResolver._maxHistoricalMessages);
162
242
 
163
- const input: SkipAPIRequest = {
164
- apiKeys: this.buildSkipAPIKeys(),
165
- organizationInfo: configInfo?.askSkip?.organizationInfo,
166
- messages: messages,
167
- conversationID: ConversationId.toString(),
168
- dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
169
- organizationID: !isNaN(parseInt(___skipAPIOrgId)) ? parseInt(___skipAPIOrgId) : 0,
170
- requestPhase: 'initial_request',
171
- entities: this.BuildSkipEntities(),
172
- queries: this.BuildSkipQueries(),
173
- };
243
+ const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true);
174
244
 
175
245
  return this.HandleSkipRequest(input, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
176
246
  }
@@ -356,7 +426,7 @@ export class AskSkipResolver {
356
426
  await convoDetailEntity.Save();
357
427
 
358
428
  const dataContext = MJGlobal.Instance.ClassFactory.CreateInstance<DataContext>(DataContext); // await this.LoadDataContext(md, dataSource, dataContextEntity, user, false);
359
- await dataContext.Load(dataContextEntity.ID, dataSource, false, user);
429
+ await dataContext.Load(dataContextEntity.ID, dataSource, false, true, user);
360
430
  return {dataContext, convoEntity, dataContextEntity, convoDetailEntity};
361
431
  }
362
432
 
@@ -393,8 +463,16 @@ export class AskSkipResolver {
393
463
  const skipRole = this.MapDBRoleToSkipRole(r.Role);
394
464
  let outputMessage; // will be populated below for system messages
395
465
  if (skipRole === 'system') {
396
- const detail = <SkipAPIResponse>JSON.parse(r.Message);
397
- if (detail.responsePhase === SkipResponsePhase.AnalysisComplete) {
466
+ let detail: SkipAPIResponse;
467
+ try {
468
+ detail = <SkipAPIResponse>JSON.parse(r.Message);
469
+ }
470
+ catch (e) {
471
+ // ignore, sometimes we dont have a JSON message, just use the raw message
472
+ detail = null;
473
+ outputMessage = r.Message;
474
+ }
475
+ if (detail?.responsePhase === SkipResponsePhase.AnalysisComplete) {
398
476
  const analysisDetail = <SkipAPIAnalysisCompleteResponse>detail;
399
477
  outputMessage = JSON.stringify({
400
478
  responsePhase: SkipResponsePhase.AnalysisComplete,
@@ -404,18 +482,18 @@ export class AskSkipResolver {
404
482
  tableDataColumns: analysisDetail.tableDataColumns
405
483
  });
406
484
  }
407
- else if (detail.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
485
+ else if (detail?.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
408
486
  const clarifyingQuestionDetail = <SkipAPIClarifyingQuestionResponse>detail;
409
487
  outputMessage = JSON.stringify({
410
488
  responsePhase: SkipResponsePhase.ClarifyingQuestion,
411
489
  clarifyingQuestion: clarifyingQuestionDetail.clarifyingQuestion
412
490
  });
413
491
  }
414
- else {
492
+ else if (detail) {
415
493
  // we should never get here, AI responses only fit the above
416
494
  // don't throw an exception, but log an error
417
495
  LogError(`Unknown response phase: ${detail.responsePhase}`);
418
- }
496
+ }
419
497
  }
420
498
  const m: SkipMessage = {
421
499
  content: skipRole === 'system' ? outputMessage : r.Message,
@@ -635,7 +713,7 @@ export class AskSkipResolver {
635
713
  item.Type = 'sql';
636
714
  item.SQL = dr.text;
637
715
  item.AdditionalDescription = dr.description;
638
- if (!await item.LoadData(dataSource, false, user))
716
+ if (!await item.LoadData(dataSource, false, true, user))
639
717
  throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
640
718
  break;
641
719
  case "stored_query":
@@ -646,7 +724,7 @@ export class AskSkipResolver {
646
724
  item.QueryID = query.ID;
647
725
  item.RecordName = query.Name;
648
726
  item.AdditionalDescription = dr.description;
649
- if (!await item.LoadData(dataSource, false, user))
727
+ if (!await item.LoadData(dataSource, false, true, user))
650
728
  throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
651
729
  }
652
730
  else