@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.
- package/CHANGELOG.json +187 -1
- package/CHANGELOG.md +44 -2
- package/dist/generated/generated.d.ts +292 -5
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1601 -16
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +3 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +21 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +7 -10
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +102 -40
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +13 -13
- package/src/generated/generated.ts +1223 -15
- package/src/generic/RunViewResolver.ts +21 -0
- package/src/resolvers/AskSkipResolver.ts +132 -52
|
@@ -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
|
-
*
|
|
67
|
-
* @param
|
|
68
|
-
* @param
|
|
69
|
-
* @param
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
107
|
-
UserMessageConversationDetailId:
|
|
108
|
-
AIMessageConversationDetailId:
|
|
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: `
|
|
146
|
+
Result: `Request failed`,
|
|
117
147
|
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
118
|
-
ConversationId:
|
|
119
|
-
UserMessageConversationDetailId:
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
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
|
|
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
|