@memberjunction/server 1.1.2 → 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.
- package/CHANGELOG.json +169 -1
- package/CHANGELOG.md +41 -2
- package/dist/generated/generated.d.ts +3 -3
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +8 -7
- package/dist/generated/generated.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 +100 -40
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +13 -13
- package/src/generated/generated.ts +9 -8
- package/src/resolvers/AskSkipResolver.ts +130 -52
|
@@ -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
|
-
*
|
|
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, 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:
|
|
107
|
-
UserMessageConversationDetailId:
|
|
108
|
-
AIMessageConversationDetailId:
|
|
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: `
|
|
144
|
+
Result: `Request failed`,
|
|
117
145
|
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
118
|
-
ConversationId:
|
|
119
|
-
UserMessageConversationDetailId:
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
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
|
|
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
|