@memberjunction/server 1.1.3 → 1.2.1

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.
@@ -72,6 +72,13 @@ export class RunViewByIDInput {
72
72
  })
73
73
  IgnoreMaxRows?: boolean;
74
74
 
75
+ @Field(() => Int, {
76
+ nullable: true,
77
+ description:
78
+ 'if a value > 0 is provided, and IgnoreMaxRows is set to false, this value is used for the max rows to be returned by the view.',
79
+ })
80
+ MaxRows?: number
81
+
75
82
  @Field(() => Boolean, {
76
83
  nullable: true,
77
84
  description:
@@ -147,6 +154,13 @@ export class RunViewByNameInput {
147
154
  })
148
155
  IgnoreMaxRows?: boolean;
149
156
 
157
+ @Field(() => Int, {
158
+ nullable: true,
159
+ description:
160
+ 'if a value > 0 is provided, and IgnoreMaxRows is set to false, this value is used for the max rows to be returned by the view.',
161
+ })
162
+ MaxRows?: number
163
+
150
164
  @Field(() => Boolean, {
151
165
  nullable: true,
152
166
  description:
@@ -207,6 +221,13 @@ export class RunDynamicViewInput {
207
221
  })
208
222
  IgnoreMaxRows?: boolean;
209
223
 
224
+ @Field(() => Int, {
225
+ nullable: true,
226
+ description:
227
+ 'if a value > 0 is provided, and IgnoreMaxRows is set to false, this value is used for the max rows to be returned by the view.',
228
+ })
229
+ MaxRows?: number
230
+
210
231
  @Field(() => Boolean, {
211
232
  nullable: true,
212
233
  description:
@@ -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,78 @@ 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, 10, user); // load again because we added a new data context item
103
+ await dataContext.SaveItems(); // persist
104
+
105
+ // also, in the situation for a new convo, we need to update the Conversation ID to have a LinkedEntity and LinkedRecord
106
+ convoEntity.LinkedEntityID = dci.EntityID;
107
+ convoEntity.LinkedRecordID = PrimaryKeys.map((pk) => pk.Value).join(',');
108
+ convoEntity.DataContextID = dataContext.ID;
109
+ await convoEntity.Save();
110
+ }
111
+
112
+ const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false);
113
+ messages.push({
114
+ content: UserQuestion,
115
+ role: 'user'
116
+ });
117
+
118
+ return this.handleSimpleSkipPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
119
+ }
120
+
121
+ protected async handleSimpleSkipPostRequest(input: SkipAPIRequest, conversationID: number = 0, UserMessageConversationDetailId: number = 0, createAIMessageConversationDetail: boolean = false, user: UserInfo = null): Promise<AskSkipResultType> {
93
122
  LogStatus(` >>> Sending request to Skip API: ${___skipAPIurl}`)
94
123
 
95
124
  const response = await sendPostRequest(___skipAPIurl, input, true, null);
96
125
 
97
126
  if (response && response.length > 0) {
98
127
  // the last object in the response array is the final response from the Skip API
99
- const apiResponse = <SkipAPIResponse>response[response.length - 1];
128
+ const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
129
+ const AIMessageConversationDetailID = createAIMessageConversationDetail ? await this.CreateAIMessageConversationDetail(apiResponse, conversationID, user) : 0;
100
130
  // const apiResponse = <SkipAPIResponse>response.data;
101
131
  LogStatus(` Skip API response: ${apiResponse.responsePhase}`)
102
132
  return {
103
133
  Success: true,
104
134
  Status: 'OK',
105
135
  ResponsePhase: SkipResponsePhase.AnalysisComplete,
106
- ConversationId: 0,
107
- UserMessageConversationDetailId: 0,
108
- AIMessageConversationDetailId: 0,
136
+ ConversationId: conversationID,
137
+ UserMessageConversationDetailId: UserMessageConversationDetailId,
138
+ AIMessageConversationDetailId: AIMessageConversationDetailID,
109
139
  Result: JSON.stringify(apiResponse)
110
140
  };
111
141
  }
@@ -113,15 +143,67 @@ export class AskSkipResolver {
113
143
  return {
114
144
  Success: false,
115
145
  Status: 'Error',
116
- Result: `Report Refresh failed`,
146
+ Result: `Request failed`,
117
147
  ResponsePhase: SkipResponsePhase.AnalysisComplete,
118
- ConversationId: 0,
119
- UserMessageConversationDetailId: 0,
148
+ ConversationId: conversationID,
149
+ UserMessageConversationDetailId: UserMessageConversationDetailId,
120
150
  AIMessageConversationDetailId: 0,
121
151
  };
122
152
  }
123
153
  }
124
154
 
155
+ protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: number, user: UserInfo): Promise<number> {
156
+ const md = new Metadata();
157
+ const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
158
+ convoDetailEntityAI.NewRecord();
159
+ convoDetailEntityAI.ConversationID = conversationID;
160
+ const systemMessages = apiResponse.messages.filter((m) => m.role === 'system');
161
+ const lastSystemMessage = systemMessages[systemMessages.length - 1];
162
+ convoDetailEntityAI.Message = lastSystemMessage?.content;
163
+ convoDetailEntityAI.Role = 'AI';
164
+ if (await convoDetailEntityAI.Save()) {
165
+ return convoDetailEntityAI.ID;
166
+ }
167
+ else
168
+ return 0;
169
+ }
170
+
171
+ protected buildSkipAPIRequest(messages: SkipMessage[], conversationId: number, dataContext: DataContext, requestPhase: SkipRequestPhase, includeEntities: boolean, includeQueries: boolean): SkipAPIRequest {
172
+ const entities = includeEntities ? this.BuildSkipEntities() : [];
173
+ const queries = includeQueries ? this.BuildSkipQueries() : [];
174
+ const input: SkipAPIRequest = {
175
+ apiKeys: this.buildSkipAPIKeys(),
176
+ organizationInfo: configInfo?.askSkip?.organizationInfo,
177
+ messages: messages,
178
+ conversationID: conversationId.toString(),
179
+ 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
180
+ organizationID: !isNaN(parseInt(___skipAPIOrgId)) ? parseInt(___skipAPIOrgId) : 0,
181
+ requestPhase: requestPhase,
182
+ entities: entities,
183
+ queries: queries
184
+ };
185
+ return input;
186
+ }
187
+
188
+ /**
189
+ * Executes a script in the context of a data context and returns the results
190
+ * @param pubSub
191
+ * @param DataContextId
192
+ * @param ScriptText
193
+ */
194
+ @Query(() => AskSkipResultType)
195
+ async ExecuteAskSkipRunScript(@Ctx() { dataSource, userPayload }: AppContext,
196
+ @PubSub() pubSub: PubSubEngine,
197
+ @Arg('DataContextId', () => Int) DataContextId: number,
198
+ @Arg('ScriptText', () => String) ScriptText: string) {
199
+ const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
200
+ if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
201
+ const dataContext: DataContext = new DataContext();
202
+ await dataContext.Load(DataContextId, dataSource, true, false, 0, user);
203
+ const input = this.buildSkipAPIRequest([], 0, dataContext, 'run_existing_script', false, false);
204
+ return this.handleSimpleSkipPostRequest(input);
205
+ }
206
+
125
207
  protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
126
208
  return [
127
209
  {
@@ -160,17 +242,7 @@ export class AskSkipResolver {
160
242
  // 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
243
  const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(dataSource, convoEntity.ID, AskSkipResolver._maxHistoricalMessages);
162
244
 
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
- };
245
+ const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true);
174
246
 
175
247
  return this.HandleSkipRequest(input, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
176
248
  }
@@ -356,7 +428,7 @@ export class AskSkipResolver {
356
428
  await convoDetailEntity.Save();
357
429
 
358
430
  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);
431
+ await dataContext.Load(dataContextEntity.ID, dataSource, false, false, 0, user);
360
432
  return {dataContext, convoEntity, dataContextEntity, convoDetailEntity};
361
433
  }
362
434
 
@@ -393,8 +465,16 @@ export class AskSkipResolver {
393
465
  const skipRole = this.MapDBRoleToSkipRole(r.Role);
394
466
  let outputMessage; // will be populated below for system messages
395
467
  if (skipRole === 'system') {
396
- const detail = <SkipAPIResponse>JSON.parse(r.Message);
397
- if (detail.responsePhase === SkipResponsePhase.AnalysisComplete) {
468
+ let detail: SkipAPIResponse;
469
+ try {
470
+ detail = <SkipAPIResponse>JSON.parse(r.Message);
471
+ }
472
+ catch (e) {
473
+ // ignore, sometimes we dont have a JSON message, just use the raw message
474
+ detail = null;
475
+ outputMessage = r.Message;
476
+ }
477
+ if (detail?.responsePhase === SkipResponsePhase.AnalysisComplete) {
398
478
  const analysisDetail = <SkipAPIAnalysisCompleteResponse>detail;
399
479
  outputMessage = JSON.stringify({
400
480
  responsePhase: SkipResponsePhase.AnalysisComplete,
@@ -404,18 +484,18 @@ export class AskSkipResolver {
404
484
  tableDataColumns: analysisDetail.tableDataColumns
405
485
  });
406
486
  }
407
- else if (detail.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
487
+ else if (detail?.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
408
488
  const clarifyingQuestionDetail = <SkipAPIClarifyingQuestionResponse>detail;
409
489
  outputMessage = JSON.stringify({
410
490
  responsePhase: SkipResponsePhase.ClarifyingQuestion,
411
491
  clarifyingQuestion: clarifyingQuestionDetail.clarifyingQuestion
412
492
  });
413
493
  }
414
- else {
494
+ else if (detail) {
415
495
  // we should never get here, AI responses only fit the above
416
496
  // don't throw an exception, but log an error
417
497
  LogError(`Unknown response phase: ${detail.responsePhase}`);
418
- }
498
+ }
419
499
  }
420
500
  const m: SkipMessage = {
421
501
  content: skipRole === 'system' ? outputMessage : r.Message,
@@ -635,7 +715,7 @@ export class AskSkipResolver {
635
715
  item.Type = 'sql';
636
716
  item.SQL = dr.text;
637
717
  item.AdditionalDescription = dr.description;
638
- if (!await item.LoadData(dataSource, false, user))
718
+ if (!await item.LoadData(dataSource, false, false, 0, user))
639
719
  throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
640
720
  break;
641
721
  case "stored_query":
@@ -646,7 +726,7 @@ export class AskSkipResolver {
646
726
  item.QueryID = query.ID;
647
727
  item.RecordName = query.Name;
648
728
  item.AdditionalDescription = dr.description;
649
- if (!await item.LoadData(dataSource, false, user))
729
+ if (!await item.LoadData(dataSource, false, false, 0, user))
650
730
  throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
651
731
  }
652
732
  else