@memberjunction/server 2.1.5 → 2.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/dist/apolloServer/TransactionPlugin.d.ts +1 -1
- package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -1
- package/dist/apolloServer/TransactionPlugin.js.map +1 -1
- package/dist/apolloServer/index.d.ts +1 -1
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +2 -2
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +7 -7
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +18 -8
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -4
- package/dist/context.js.map +1 -1
- package/dist/directives/Public.d.ts +1 -1
- package/dist/directives/Public.d.ts.map +1 -1
- package/dist/directives/index.d.ts +1 -1
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/index.js +1 -1
- package/dist/directives/index.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +5 -6
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +14 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +79 -18
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +3 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +2 -2
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +21 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -27
- package/dist/index.js.map +1 -1
- package/dist/orm.js +4 -4
- package/dist/orm.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +3 -3
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +64 -59
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/ColorResolver.d.ts +1 -1
- package/dist/resolvers/ColorResolver.d.ts.map +1 -1
- package/dist/resolvers/ColorResolver.js +1 -1
- package/dist/resolvers/ColorResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +1 -1
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -2
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +9 -4
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -2
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +2 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/EntityResolver.d.ts +2 -2
- package/dist/resolvers/EntityResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityResolver.js +1 -1
- package/dist/resolvers/EntityResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +2 -2
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +2 -2
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +3 -3
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +2 -2
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +4 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -2
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +1 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +16 -14
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +17 -16
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +1 -1
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +2 -2
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +11 -5
- package/dist/util.js.map +1 -1
- package/package.json +27 -23
- package/src/apolloServer/TransactionPlugin.ts +53 -0
- package/src/apolloServer/index.ts +33 -0
- package/src/auth/exampleNewUserSubClass.ts +79 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/newUsers.ts +58 -0
- package/src/auth/tokenExpiredError.ts +12 -0
- package/src/cache.ts +10 -0
- package/src/config.ts +89 -0
- package/src/context.ts +111 -0
- package/src/directives/Public.ts +42 -0
- package/src/directives/index.ts +1 -0
- package/src/entitySubclasses/DuplicateRunEntity.server.ts +29 -0
- package/src/entitySubclasses/entityPermissions.server.ts +104 -0
- package/src/entitySubclasses/userViewEntity.server.ts +187 -0
- package/src/generated/generated.ts +25406 -0
- package/src/generic/DeleteOptionsInput.ts +13 -0
- package/src/generic/KeyInputOutputTypes.ts +35 -0
- package/src/generic/KeyValuePairInput.ts +14 -0
- package/src/generic/PushStatusResolver.ts +40 -0
- package/src/generic/ResolverBase.ts +767 -0
- package/src/generic/RunViewResolver.ts +579 -0
- package/src/index.ts +171 -0
- package/src/orm.ts +36 -0
- package/src/resolvers/AskSkipResolver.ts +1112 -0
- package/src/resolvers/ColorResolver.ts +61 -0
- package/src/resolvers/DatasetResolver.ts +115 -0
- package/src/resolvers/EntityCommunicationsResolver.ts +221 -0
- package/src/resolvers/EntityRecordNameResolver.ts +75 -0
- package/src/resolvers/EntityResolver.ts +35 -0
- package/src/resolvers/FileCategoryResolver.ts +69 -0
- package/src/resolvers/FileResolver.ts +152 -0
- package/src/resolvers/MergeRecordsResolver.ts +175 -0
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +91 -0
- package/src/resolvers/QueryResolver.ts +42 -0
- package/src/resolvers/ReportResolver.ts +144 -0
- package/src/resolvers/UserFavoriteResolver.ts +176 -0
- package/src/resolvers/UserResolver.ts +33 -0
- package/src/resolvers/UserViewResolver.ts +64 -0
- package/src/types.ts +40 -0
- package/src/util.ts +112 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey } from '@memberjunction/core';
|
|
3
|
+
import { AppContext, UserPayload } from '../types.js';
|
|
4
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
5
|
+
import { DataContext } from '@memberjunction/data-context';
|
|
6
|
+
import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
|
|
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
|
+
|
|
9
|
+
import {
|
|
10
|
+
SkipAPIRequest,
|
|
11
|
+
SkipAPIResponse,
|
|
12
|
+
SkipMessage,
|
|
13
|
+
SkipAPIAnalysisCompleteResponse,
|
|
14
|
+
SkipAPIDataRequestResponse,
|
|
15
|
+
SkipAPIClarifyingQuestionResponse,
|
|
16
|
+
SkipEntityInfo,
|
|
17
|
+
SkipQueryInfo,
|
|
18
|
+
SkipAPIRunScriptRequest,
|
|
19
|
+
SkipAPIRequestAPIKey,
|
|
20
|
+
SkipRequestPhase,
|
|
21
|
+
} from '@memberjunction/skip-types';
|
|
22
|
+
|
|
23
|
+
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
24
|
+
import {
|
|
25
|
+
ConversationDetailEntity,
|
|
26
|
+
ConversationEntity,
|
|
27
|
+
DataContextEntity,
|
|
28
|
+
DataContextItemEntity,
|
|
29
|
+
UserNotificationEntity,
|
|
30
|
+
} from '@memberjunction/core-entities';
|
|
31
|
+
import { DataSource } from 'typeorm';
|
|
32
|
+
import { ___skipAPIOrgId, ___skipAPIurl, configInfo, mj_core_schema } from '../config.js';
|
|
33
|
+
|
|
34
|
+
import { registerEnumType } from 'type-graphql';
|
|
35
|
+
import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
|
|
36
|
+
import { sendPostRequest } from '../util.js';
|
|
37
|
+
import { GetAIAPIKey } from '@memberjunction/ai';
|
|
38
|
+
import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
|
|
39
|
+
|
|
40
|
+
enum SkipResponsePhase {
|
|
41
|
+
ClarifyingQuestion = 'clarifying_question',
|
|
42
|
+
DataRequest = 'data_request',
|
|
43
|
+
AnalysisComplete = 'analysis_complete',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
registerEnumType(SkipResponsePhase, {
|
|
47
|
+
name: 'SkipResponsePhase',
|
|
48
|
+
description: 'The phase of the respons: clarifying_question, data_request, or analysis_complete',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
@ObjectType()
|
|
52
|
+
export class AskSkipResultType {
|
|
53
|
+
@Field(() => Boolean)
|
|
54
|
+
Success: boolean;
|
|
55
|
+
|
|
56
|
+
@Field(() => String)
|
|
57
|
+
Status: string; // required
|
|
58
|
+
|
|
59
|
+
@Field(() => SkipResponsePhase)
|
|
60
|
+
ResponsePhase: SkipResponsePhase;
|
|
61
|
+
|
|
62
|
+
@Field(() => String)
|
|
63
|
+
Result: string;
|
|
64
|
+
|
|
65
|
+
@Field(() => String)
|
|
66
|
+
ConversationId: string;
|
|
67
|
+
|
|
68
|
+
@Field(() => String)
|
|
69
|
+
UserMessageConversationDetailId: string;
|
|
70
|
+
|
|
71
|
+
@Field(() => String)
|
|
72
|
+
AIMessageConversationDetailId: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Resolver(AskSkipResultType)
|
|
76
|
+
export class AskSkipResolver {
|
|
77
|
+
private static _defaultNewChatName = 'New Chat';
|
|
78
|
+
private static _maxHistoricalMessages = 20;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handles a simple chat request from a user to Skip, using a particular data record
|
|
82
|
+
* @param UserQuestion the user's question
|
|
83
|
+
* @param EntityName the name of the entity for the record the user is discussing
|
|
84
|
+
* @param PrimaryKeys the primary keys of the record the user is discussing
|
|
85
|
+
*/
|
|
86
|
+
@Query(() => AskSkipResultType)
|
|
87
|
+
async ExecuteAskSkipRecordChat(
|
|
88
|
+
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
89
|
+
@Arg('ConversationId', () => String) ConversationId: string,
|
|
90
|
+
@Arg('EntityName', () => String) EntityName: string,
|
|
91
|
+
@Arg('CompositeKey', () => CompositeKeyInputType) compositeKey: CompositeKeyInputType,
|
|
92
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
93
|
+
@PubSub() pubSub: PubSubEngine
|
|
94
|
+
) {
|
|
95
|
+
// In this function we're simply going to call the Skip API and pass along the message from the user
|
|
96
|
+
|
|
97
|
+
// first, get the user from the cache
|
|
98
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
99
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
100
|
+
|
|
101
|
+
// now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
|
|
102
|
+
const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
|
|
103
|
+
dataSource,
|
|
104
|
+
ConversationId,
|
|
105
|
+
AskSkipResolver._maxHistoricalMessages
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const md = new Metadata();
|
|
109
|
+
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipInitialObjectLoading(
|
|
110
|
+
dataSource,
|
|
111
|
+
ConversationId,
|
|
112
|
+
UserQuestion,
|
|
113
|
+
user,
|
|
114
|
+
userPayload,
|
|
115
|
+
md,
|
|
116
|
+
null
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// if we have a new conversation, update the data context to have an item for this record
|
|
120
|
+
if (!ConversationId || ConversationId.length === 0) {
|
|
121
|
+
const dci = await md.GetEntityObject<DataContextItemEntity>('Data Context Items', user);
|
|
122
|
+
dci.DataContextID = dataContext.ID;
|
|
123
|
+
dci.Type = 'single_record';
|
|
124
|
+
dci.EntityID = md.Entities.find((e) => e.Name === EntityName)?.ID;
|
|
125
|
+
const ck = new CompositeKey();
|
|
126
|
+
ck.KeyValuePairs = compositeKey.KeyValuePairs;
|
|
127
|
+
dci.RecordID = ck.Values();
|
|
128
|
+
let dciSaveResult: boolean = await dci.Save();
|
|
129
|
+
if (!dciSaveResult) {
|
|
130
|
+
LogError(`Error saving DataContextItemEntity for record chat: ${EntityName} ${ck.Values()}`, undefined, dci.LatestResult);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await dataContext.Load(dataContext.ID, dataSource, false, true, 10, user); // load again because we added a new data context item
|
|
134
|
+
await dataContext.SaveItems(user, true); // persist the data becuase the deep loading above with related data is expensive
|
|
135
|
+
|
|
136
|
+
// also, in the situation for a new convo, we need to update the Conversation ID to have a LinkedEntity and LinkedRecord
|
|
137
|
+
convoEntity.LinkedEntityID = dci.EntityID;
|
|
138
|
+
convoEntity.LinkedRecordID = ck.Values();
|
|
139
|
+
convoEntity.DataContextID = dataContext.ID;
|
|
140
|
+
const convoEntitySaveResult: boolean = await convoEntity.Save();
|
|
141
|
+
if (!convoEntitySaveResult) {
|
|
142
|
+
LogError(`Error saving ConversationEntity for record chat: ${EntityName} ${ck.Values()}`, undefined, convoEntity.LatestResult);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false);
|
|
147
|
+
messages.push({
|
|
148
|
+
content: UserQuestion,
|
|
149
|
+
role: 'user',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return this.handleSimpleSkipPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
protected async handleSimpleSkipPostRequest(
|
|
156
|
+
input: SkipAPIRequest,
|
|
157
|
+
conversationID: string = '',
|
|
158
|
+
UserMessageConversationDetailId: string = '',
|
|
159
|
+
createAIMessageConversationDetail: boolean = false,
|
|
160
|
+
user: UserInfo = null
|
|
161
|
+
): Promise<AskSkipResultType> {
|
|
162
|
+
LogStatus(` >>> HandleSimpleSkipPostRequest Sending request to Skip API: ${___skipAPIurl}`);
|
|
163
|
+
|
|
164
|
+
const response = await sendPostRequest(___skipAPIurl, input, true, null);
|
|
165
|
+
|
|
166
|
+
if (response && response.length > 0) {
|
|
167
|
+
// the last object in the response array is the final response from the Skip API
|
|
168
|
+
const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
|
|
169
|
+
const AIMessageConversationDetailID = createAIMessageConversationDetail
|
|
170
|
+
? await this.CreateAIMessageConversationDetail(apiResponse, conversationID, user)
|
|
171
|
+
: '';
|
|
172
|
+
// const apiResponse = <SkipAPIResponse>response.data;
|
|
173
|
+
LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
|
|
174
|
+
return {
|
|
175
|
+
Success: true,
|
|
176
|
+
Status: 'OK',
|
|
177
|
+
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
178
|
+
ConversationId: conversationID,
|
|
179
|
+
UserMessageConversationDetailId: UserMessageConversationDetailId,
|
|
180
|
+
AIMessageConversationDetailId: AIMessageConversationDetailID,
|
|
181
|
+
Result: JSON.stringify(apiResponse),
|
|
182
|
+
};
|
|
183
|
+
} else {
|
|
184
|
+
return {
|
|
185
|
+
Success: false,
|
|
186
|
+
Status: 'Error',
|
|
187
|
+
Result: `Request failed`,
|
|
188
|
+
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
189
|
+
ConversationId: conversationID,
|
|
190
|
+
UserMessageConversationDetailId: UserMessageConversationDetailId,
|
|
191
|
+
AIMessageConversationDetailId: '',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: string, user: UserInfo): Promise<string> {
|
|
197
|
+
const md = new Metadata();
|
|
198
|
+
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
199
|
+
convoDetailEntityAI.NewRecord();
|
|
200
|
+
convoDetailEntityAI.HiddenToUser = false;
|
|
201
|
+
convoDetailEntityAI.ConversationID = conversationID;
|
|
202
|
+
const systemMessages = apiResponse.messages.filter((m) => m.role === 'system');
|
|
203
|
+
const lastSystemMessage = systemMessages[systemMessages.length - 1];
|
|
204
|
+
convoDetailEntityAI.Message = lastSystemMessage?.content;
|
|
205
|
+
convoDetailEntityAI.Role = 'AI';
|
|
206
|
+
if (await convoDetailEntityAI.Save()) {
|
|
207
|
+
return convoDetailEntityAI.ID;
|
|
208
|
+
} else {
|
|
209
|
+
LogError(
|
|
210
|
+
`Error saving conversation detail entity for AI message: ${lastSystemMessage?.content}`,
|
|
211
|
+
undefined,
|
|
212
|
+
convoDetailEntityAI.LatestResult
|
|
213
|
+
);
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
protected buildSkipAPIRequest(
|
|
219
|
+
messages: SkipMessage[],
|
|
220
|
+
conversationId: string,
|
|
221
|
+
dataContext: DataContext,
|
|
222
|
+
requestPhase: SkipRequestPhase,
|
|
223
|
+
includeEntities: boolean,
|
|
224
|
+
includeQueries: boolean
|
|
225
|
+
): SkipAPIRequest {
|
|
226
|
+
const entities = includeEntities ? this.BuildSkipEntities() : [];
|
|
227
|
+
const queries = includeQueries ? this.BuildSkipQueries() : [];
|
|
228
|
+
const input: SkipAPIRequest = {
|
|
229
|
+
apiKeys: this.buildSkipAPIKeys(),
|
|
230
|
+
organizationInfo: configInfo?.askSkip?.organizationInfo,
|
|
231
|
+
messages: messages,
|
|
232
|
+
conversationID: conversationId.toString(),
|
|
233
|
+
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
|
|
234
|
+
organizationID: ___skipAPIOrgId,
|
|
235
|
+
requestPhase: requestPhase,
|
|
236
|
+
entities: entities,
|
|
237
|
+
queries: queries,
|
|
238
|
+
};
|
|
239
|
+
return input;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Executes a script in the context of a data context and returns the results
|
|
244
|
+
* @param pubSub
|
|
245
|
+
* @param DataContextId
|
|
246
|
+
* @param ScriptText
|
|
247
|
+
*/
|
|
248
|
+
@Query(() => AskSkipResultType)
|
|
249
|
+
async ExecuteAskSkipRunScript(
|
|
250
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
251
|
+
@PubSub() pubSub: PubSubEngine,
|
|
252
|
+
@Arg('DataContextId', () => String) DataContextId: string,
|
|
253
|
+
@Arg('ScriptText', () => String) ScriptText: string
|
|
254
|
+
) {
|
|
255
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
256
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
257
|
+
const dataContext: DataContext = new DataContext();
|
|
258
|
+
await dataContext.Load(DataContextId, dataSource, true, false, 0, user);
|
|
259
|
+
const input = this.buildSkipAPIRequest([], '', dataContext, 'run_existing_script', false, false);
|
|
260
|
+
return this.handleSimpleSkipPostRequest(input);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
vendorDriverName: 'OpenAILLM',
|
|
267
|
+
apiKey: GetAIAPIKey('OpenAILLM'),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
vendorDriverName: 'AnthropicLLM',
|
|
271
|
+
apiKey: GetAIAPIKey('AnthropicLLM'),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
vendorDriverName: 'GeminiLLM',
|
|
275
|
+
apiKey: GetAIAPIKey('GeminiLLM'),
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
vendorDriverName: 'GroqLLM',
|
|
279
|
+
apiKey: GetAIAPIKey('GroqLLM'),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
vendorDriverName: 'MistralLLM',
|
|
283
|
+
apiKey: GetAIAPIKey('MistralLLM'),
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@Query(() => AskSkipResultType)
|
|
289
|
+
async ExecuteAskSkipAnalysisQuery(
|
|
290
|
+
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
291
|
+
@Arg('ConversationId', () => String) ConversationId: string,
|
|
292
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
293
|
+
@PubSub() pubSub: PubSubEngine,
|
|
294
|
+
@Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string
|
|
295
|
+
) {
|
|
296
|
+
const md = new Metadata();
|
|
297
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
298
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
299
|
+
|
|
300
|
+
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipInitialObjectLoading(
|
|
301
|
+
dataSource,
|
|
302
|
+
ConversationId,
|
|
303
|
+
UserQuestion,
|
|
304
|
+
user,
|
|
305
|
+
userPayload,
|
|
306
|
+
md,
|
|
307
|
+
DataContextId
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
|
|
311
|
+
const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
|
|
312
|
+
dataSource,
|
|
313
|
+
convoEntity.ID,
|
|
314
|
+
AskSkipResolver._maxHistoricalMessages
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true);
|
|
318
|
+
|
|
319
|
+
return this.HandleSkipRequest(
|
|
320
|
+
input,
|
|
321
|
+
UserQuestion,
|
|
322
|
+
user,
|
|
323
|
+
dataSource,
|
|
324
|
+
ConversationId,
|
|
325
|
+
userPayload,
|
|
326
|
+
pubSub,
|
|
327
|
+
md,
|
|
328
|
+
convoEntity,
|
|
329
|
+
convoDetailEntity,
|
|
330
|
+
dataContext,
|
|
331
|
+
dataContextEntity
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
protected BuildSkipQueries(): SkipQueryInfo[] {
|
|
336
|
+
const md = new Metadata();
|
|
337
|
+
return md.Queries.map((q) => {
|
|
338
|
+
return {
|
|
339
|
+
id: q.ID,
|
|
340
|
+
name: q.Name,
|
|
341
|
+
description: q.Description,
|
|
342
|
+
category: q.Category,
|
|
343
|
+
sql: q.SQL,
|
|
344
|
+
originalSQL: q.OriginalSQL,
|
|
345
|
+
feedback: q.Feedback,
|
|
346
|
+
status: q.Status,
|
|
347
|
+
qualityRank: q.QualityRank,
|
|
348
|
+
createdAt: q.__mj_CreatedAt,
|
|
349
|
+
updatedAt: q.__mj_UpdatedAt,
|
|
350
|
+
categoryID: q.CategoryID,
|
|
351
|
+
fields: q.Fields.map((f) => {
|
|
352
|
+
return {
|
|
353
|
+
id: f.ID,
|
|
354
|
+
queryID: f.QueryID,
|
|
355
|
+
sequence: f.Sequence,
|
|
356
|
+
name: f.Name,
|
|
357
|
+
description: f.Description,
|
|
358
|
+
sqlBaseType: f.SQLBaseType,
|
|
359
|
+
sqlFullType: f.SQLFullType,
|
|
360
|
+
sourceEntityID: f.SourceEntityID,
|
|
361
|
+
sourceEntity: f.SourceEntity,
|
|
362
|
+
sourceFieldName: f.SourceFieldName,
|
|
363
|
+
isComputed: f.IsComputed,
|
|
364
|
+
computationDescription: f.ComputationDescription,
|
|
365
|
+
isSummary: f.IsSummary,
|
|
366
|
+
summaryDescription: f.SummaryDescription,
|
|
367
|
+
createdAt: f.__mj_CreatedAt,
|
|
368
|
+
updatedAt: f.__mj_UpdatedAt,
|
|
369
|
+
};
|
|
370
|
+
}),
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
protected BuildSkipEntities(): SkipEntityInfo[] {
|
|
376
|
+
// build the entity info for skip in its format which is
|
|
377
|
+
// narrower in scope than our native MJ metadata
|
|
378
|
+
// don't pass the mj_core_schema entities
|
|
379
|
+
const md = new Metadata();
|
|
380
|
+
return md.Entities.filter((e) => e.SchemaName !== mj_core_schema).map((e) => {
|
|
381
|
+
const ret: SkipEntityInfo = {
|
|
382
|
+
id: e.ID,
|
|
383
|
+
name: e.Name,
|
|
384
|
+
schemaName: e.SchemaName,
|
|
385
|
+
baseView: e.BaseView,
|
|
386
|
+
description: e.Description,
|
|
387
|
+
fields: e.Fields.map((f) => {
|
|
388
|
+
return {
|
|
389
|
+
id: f.ID,
|
|
390
|
+
entityID: f.EntityID,
|
|
391
|
+
sequence: f.Sequence,
|
|
392
|
+
name: f.Name,
|
|
393
|
+
displayName: f.DisplayName,
|
|
394
|
+
category: f.Category,
|
|
395
|
+
type: f.Type,
|
|
396
|
+
description: f.Description,
|
|
397
|
+
isPrimaryKey: f.IsPrimaryKey,
|
|
398
|
+
allowsNull: f.AllowsNull,
|
|
399
|
+
isUnique: f.IsUnique,
|
|
400
|
+
length: f.Length,
|
|
401
|
+
precision: f.Precision,
|
|
402
|
+
scale: f.Scale,
|
|
403
|
+
sqlFullType: f.SQLFullType,
|
|
404
|
+
defaultValue: f.DefaultValue,
|
|
405
|
+
autoIncrement: f.AutoIncrement,
|
|
406
|
+
valueListType: f.ValueListType,
|
|
407
|
+
extendedType: f.ExtendedType,
|
|
408
|
+
defaultInView: f.DefaultInView,
|
|
409
|
+
defaultColumnWidth: f.DefaultColumnWidth,
|
|
410
|
+
isVirtual: f.IsVirtual,
|
|
411
|
+
isNameField: f.IsNameField,
|
|
412
|
+
relatedEntityID: f.RelatedEntityID,
|
|
413
|
+
relatedEntityFieldName: f.RelatedEntityFieldName,
|
|
414
|
+
relatedEntity: f.RelatedEntity,
|
|
415
|
+
relatedEntitySchemaName: f.RelatedEntitySchemaName,
|
|
416
|
+
relatedEntityBaseView: f.RelatedEntityBaseView,
|
|
417
|
+
};
|
|
418
|
+
}),
|
|
419
|
+
relatedEntities: e.RelatedEntities.map((r) => {
|
|
420
|
+
return {
|
|
421
|
+
entityID: r.EntityID,
|
|
422
|
+
relatedEntityID: r.RelatedEntityID,
|
|
423
|
+
type: r.Type,
|
|
424
|
+
entityKeyField: r.EntityKeyField,
|
|
425
|
+
relatedEntityJoinField: r.RelatedEntityJoinField,
|
|
426
|
+
joinView: r.JoinView,
|
|
427
|
+
joinEntityJoinField: r.JoinEntityJoinField,
|
|
428
|
+
joinEntityInverseJoinField: r.JoinEntityInverseJoinField,
|
|
429
|
+
entity: r.Entity,
|
|
430
|
+
entityBaseView: r.EntityBaseView,
|
|
431
|
+
relatedEntity: r.RelatedEntity,
|
|
432
|
+
relatedEntityBaseView: r.RelatedEntityBaseView,
|
|
433
|
+
};
|
|
434
|
+
}),
|
|
435
|
+
};
|
|
436
|
+
return ret;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
protected async HandleSkipInitialObjectLoading(
|
|
441
|
+
dataSource: DataSource,
|
|
442
|
+
ConversationId: string,
|
|
443
|
+
UserQuestion: string,
|
|
444
|
+
user: UserInfo,
|
|
445
|
+
userPayload: UserPayload,
|
|
446
|
+
md: Metadata,
|
|
447
|
+
DataContextId: string
|
|
448
|
+
): Promise<{
|
|
449
|
+
convoEntity: ConversationEntity;
|
|
450
|
+
dataContextEntity: DataContextEntity;
|
|
451
|
+
convoDetailEntity: ConversationDetailEntity;
|
|
452
|
+
dataContext: DataContext;
|
|
453
|
+
}> {
|
|
454
|
+
const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
|
|
455
|
+
let dataContextEntity: DataContextEntity;
|
|
456
|
+
|
|
457
|
+
if (!ConversationId || ConversationId.length === 0) {
|
|
458
|
+
// create a new conversation id
|
|
459
|
+
convoEntity.NewRecord();
|
|
460
|
+
if (user) {
|
|
461
|
+
convoEntity.UserID = user.ID;
|
|
462
|
+
convoEntity.Name = AskSkipResolver._defaultNewChatName;
|
|
463
|
+
|
|
464
|
+
dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
|
|
465
|
+
if (!DataContextId || DataContextId.length === 0) {
|
|
466
|
+
dataContextEntity.NewRecord();
|
|
467
|
+
dataContextEntity.UserID = user.ID;
|
|
468
|
+
dataContextEntity.Name = 'Data Context for Skip Conversation';
|
|
469
|
+
if (!(await dataContextEntity.Save())) {
|
|
470
|
+
LogError(`Creating a new data context failed`, undefined, dataContextEntity.LatestResult);
|
|
471
|
+
throw new Error(`Creating a new data context failed`);
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
const dcLoadResult = await dataContextEntity.Load(DataContextId);
|
|
475
|
+
if (!dcLoadResult) {
|
|
476
|
+
throw new Error(`Loading DataContextEntity for DataContextId ${DataContextId} failed`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
convoEntity.DataContextID = dataContextEntity.ID;
|
|
480
|
+
if (await convoEntity.Save()) {
|
|
481
|
+
ConversationId = convoEntity.ID;
|
|
482
|
+
if (!DataContextId || dataContextEntity.ID.length === 0) {
|
|
483
|
+
// only do this if we created a new data context for this conversation
|
|
484
|
+
dataContextEntity.Name += ` ${ConversationId}`;
|
|
485
|
+
const dciSaveResult: boolean = await dataContextEntity.Save();
|
|
486
|
+
if (!dciSaveResult) {
|
|
487
|
+
LogError(`Error saving DataContextEntity for conversation: ${ConversationId}`, undefined, dataContextEntity.LatestResult);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
LogError(`Creating a new conversation failed`, undefined, convoEntity.LatestResult);
|
|
492
|
+
throw new Error(`Creating a new conversation failed`);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
|
|
499
|
+
dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
|
|
500
|
+
|
|
501
|
+
// note - we ignore the parameter DataContextId if it is passed in, we will use the data context from the conversation that is saved. If a user wants to change the data context for a convo, they can do that elsewhere
|
|
502
|
+
if (DataContextId && DataContextId.length > 0 && DataContextId !== convoEntity.DataContextID) {
|
|
503
|
+
if (convoEntity.DataContextID === null) {
|
|
504
|
+
convoEntity.DataContextID = DataContextId;
|
|
505
|
+
const convoEntitySaveResult: boolean = await convoEntity.Save();
|
|
506
|
+
if (!convoEntitySaveResult) {
|
|
507
|
+
LogError(`Error saving conversation entity for conversation: ${ConversationId}`, undefined, convoEntity.LatestResult);
|
|
508
|
+
}
|
|
509
|
+
} else
|
|
510
|
+
console.warn(
|
|
511
|
+
`AskSkipResolver: DataContextId ${DataContextId} was passed in but it was ignored because it was different than the DataContextID in the conversation ${convoEntity.DataContextID}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
await dataContextEntity.Load(convoEntity.DataContextID);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// now, create a conversation detail record for the user message
|
|
519
|
+
const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
|
|
520
|
+
convoDetailEntity.NewRecord();
|
|
521
|
+
convoDetailEntity.ConversationID = ConversationId;
|
|
522
|
+
convoDetailEntity.Message = UserQuestion;
|
|
523
|
+
convoDetailEntity.Role = 'User';
|
|
524
|
+
convoDetailEntity.HiddenToUser = false;
|
|
525
|
+
convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
|
|
526
|
+
let convoDetailSaveResult: boolean = await convoDetailEntity.Save();
|
|
527
|
+
if (!convoDetailSaveResult) {
|
|
528
|
+
LogError(`Error saving conversation detail entity for user message: ${UserQuestion}`, undefined, convoDetailEntity.LatestResult);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const dataContext = MJGlobal.Instance.ClassFactory.CreateInstance<DataContext>(DataContext); // await this.LoadDataContext(md, dataSource, dataContextEntity, user, false);
|
|
532
|
+
await dataContext.Load(dataContextEntity.ID, dataSource, false, false, 0, user);
|
|
533
|
+
return { dataContext, convoEntity, dataContextEntity, convoDetailEntity };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
protected async LoadConversationDetailsIntoSkipMessages(
|
|
537
|
+
dataSource: DataSource,
|
|
538
|
+
ConversationId: string,
|
|
539
|
+
maxHistoricalMessages?: number
|
|
540
|
+
): Promise<SkipMessage[]> {
|
|
541
|
+
try {
|
|
542
|
+
// load up all the conversation details from the database server
|
|
543
|
+
const md = new Metadata();
|
|
544
|
+
const e = md.Entities.find((e) => e.Name === 'Conversation Details');
|
|
545
|
+
const sql = `SELECT
|
|
546
|
+
${maxHistoricalMessages ? 'TOP ' + maxHistoricalMessages : ''} ID, Message, Role, __mj_CreatedAt
|
|
547
|
+
FROM
|
|
548
|
+
${e.SchemaName}.${e.BaseView}
|
|
549
|
+
WHERE
|
|
550
|
+
ConversationID = '${ConversationId}'
|
|
551
|
+
ORDER
|
|
552
|
+
BY __mj_CreatedAt DESC`;
|
|
553
|
+
const result = await dataSource.query(sql);
|
|
554
|
+
if (!result) throw new Error(`Error running SQL: ${sql}`);
|
|
555
|
+
else {
|
|
556
|
+
// first, let's sort the result array into a local variable called returnData and in that we will sort by __mj_CreatedAt in ASCENDING order so we have the right chronological order
|
|
557
|
+
// the reason we're doing a LOCAL sort here is because in the SQL query above, we're sorting in DESCENDING order so we can use the TOP clause to limit the number of records and get the
|
|
558
|
+
// N most recent records. We want to sort in ASCENDING order because we want to send the messages to the Skip API in the order they were created.
|
|
559
|
+
const returnData = result.sort((a: any, b: any) => {
|
|
560
|
+
const aDate = new Date(a.__mj_CreatedAt);
|
|
561
|
+
const bDate = new Date(b.__mj_CreatedAt);
|
|
562
|
+
return aDate.getTime() - bDate.getTime();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// now, we will map the returnData into an array of SkipMessages
|
|
566
|
+
return returnData.map((r: ConversationDetailEntity) => {
|
|
567
|
+
// we want to limit the # of characters in the message to 5000, rough approximation for 1000 words/tokens
|
|
568
|
+
// but we only do that for system messages
|
|
569
|
+
const skipRole = this.MapDBRoleToSkipRole(r.Role);
|
|
570
|
+
let outputMessage; // will be populated below for system messages
|
|
571
|
+
if (skipRole === 'system') {
|
|
572
|
+
let detail: SkipAPIResponse;
|
|
573
|
+
try {
|
|
574
|
+
detail = <SkipAPIResponse>JSON.parse(r.Message);
|
|
575
|
+
} catch (e) {
|
|
576
|
+
// ignore, sometimes we dont have a JSON message, just use the raw message
|
|
577
|
+
detail = null;
|
|
578
|
+
outputMessage = r.Message;
|
|
579
|
+
}
|
|
580
|
+
if (detail?.responsePhase === SkipResponsePhase.AnalysisComplete) {
|
|
581
|
+
const analysisDetail = <SkipAPIAnalysisCompleteResponse>detail;
|
|
582
|
+
outputMessage = JSON.stringify({
|
|
583
|
+
responsePhase: SkipResponsePhase.AnalysisComplete,
|
|
584
|
+
techExplanation: analysisDetail.techExplanation,
|
|
585
|
+
userExplanation: analysisDetail.userExplanation,
|
|
586
|
+
scriptText: analysisDetail.scriptText,
|
|
587
|
+
tableDataColumns: analysisDetail.tableDataColumns,
|
|
588
|
+
});
|
|
589
|
+
} else if (detail?.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
|
|
590
|
+
const clarifyingQuestionDetail = <SkipAPIClarifyingQuestionResponse>detail;
|
|
591
|
+
outputMessage = JSON.stringify({
|
|
592
|
+
responsePhase: SkipResponsePhase.ClarifyingQuestion,
|
|
593
|
+
clarifyingQuestion: clarifyingQuestionDetail.clarifyingQuestion,
|
|
594
|
+
});
|
|
595
|
+
} else if (detail) {
|
|
596
|
+
// we should never get here, AI responses only fit the above
|
|
597
|
+
// don't throw an exception, but log an error
|
|
598
|
+
LogError(`Unknown response phase: ${detail.responsePhase}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const m: SkipMessage = {
|
|
602
|
+
content: skipRole === 'system' ? outputMessage : r.Message,
|
|
603
|
+
role: skipRole,
|
|
604
|
+
};
|
|
605
|
+
return m;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
} catch (e) {
|
|
609
|
+
LogError(e);
|
|
610
|
+
throw e;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
protected MapDBRoleToSkipRole(role: string): 'user' | 'system' {
|
|
615
|
+
switch (role.trim().toLowerCase()) {
|
|
616
|
+
case 'ai':
|
|
617
|
+
case 'system':
|
|
618
|
+
case 'assistant':
|
|
619
|
+
return 'system';
|
|
620
|
+
default:
|
|
621
|
+
return 'user';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
protected async HandleSkipRequest(
|
|
626
|
+
input: SkipAPIRequest,
|
|
627
|
+
UserQuestion: string,
|
|
628
|
+
user: UserInfo,
|
|
629
|
+
dataSource: DataSource,
|
|
630
|
+
ConversationId: string,
|
|
631
|
+
userPayload: UserPayload,
|
|
632
|
+
pubSub: PubSubEngine,
|
|
633
|
+
md: Metadata,
|
|
634
|
+
convoEntity: ConversationEntity,
|
|
635
|
+
convoDetailEntity: ConversationDetailEntity,
|
|
636
|
+
dataContext: DataContext,
|
|
637
|
+
dataContextEntity: DataContextEntity
|
|
638
|
+
): Promise<AskSkipResultType> {
|
|
639
|
+
LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${___skipAPIurl}`);
|
|
640
|
+
|
|
641
|
+
const response = await sendPostRequest(
|
|
642
|
+
___skipAPIurl,
|
|
643
|
+
input,
|
|
644
|
+
true,
|
|
645
|
+
null,
|
|
646
|
+
(message: {
|
|
647
|
+
type: string;
|
|
648
|
+
value: {
|
|
649
|
+
success: boolean;
|
|
650
|
+
error: string;
|
|
651
|
+
responsePhase: string;
|
|
652
|
+
messages: {
|
|
653
|
+
role: string;
|
|
654
|
+
content: string;
|
|
655
|
+
}[];
|
|
656
|
+
};
|
|
657
|
+
}) => {
|
|
658
|
+
LogStatus(JSON.stringify(message, null, 4));
|
|
659
|
+
if (message.type === 'status_update') {
|
|
660
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
661
|
+
message: JSON.stringify({
|
|
662
|
+
type: 'AskSkip',
|
|
663
|
+
status: 'OK',
|
|
664
|
+
conversationID: ConversationId,
|
|
665
|
+
ResponsePhase: message.value.responsePhase,
|
|
666
|
+
message: message.value.messages[0].content,
|
|
667
|
+
}),
|
|
668
|
+
sessionId: userPayload.sessionId,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
if (response && response.length > 0) {
|
|
675
|
+
// response.status === 200) {
|
|
676
|
+
// the last object in the response array is the final response from the Skip API
|
|
677
|
+
const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
|
|
678
|
+
//const apiResponse = <SkipAPIResponse>response.data;
|
|
679
|
+
LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
|
|
680
|
+
this.PublishApiResponseUserUpdateMessage(apiResponse, userPayload, ConversationId, pubSub);
|
|
681
|
+
|
|
682
|
+
// now, based on the result type, we will either wait for the next phase or we will process the results
|
|
683
|
+
if (apiResponse.responsePhase === 'data_request') {
|
|
684
|
+
return await this.HandleDataRequestPhase(
|
|
685
|
+
input,
|
|
686
|
+
<SkipAPIDataRequestResponse>apiResponse,
|
|
687
|
+
UserQuestion,
|
|
688
|
+
user,
|
|
689
|
+
dataSource,
|
|
690
|
+
ConversationId,
|
|
691
|
+
userPayload,
|
|
692
|
+
pubSub,
|
|
693
|
+
convoEntity,
|
|
694
|
+
convoDetailEntity,
|
|
695
|
+
dataContext,
|
|
696
|
+
dataContextEntity
|
|
697
|
+
);
|
|
698
|
+
} else if (apiResponse.responsePhase === 'clarifying_question') {
|
|
699
|
+
// need to send the request back to the user for a clarifying question
|
|
700
|
+
return await this.HandleClarifyingQuestionPhase(
|
|
701
|
+
input,
|
|
702
|
+
<SkipAPIClarifyingQuestionResponse>apiResponse,
|
|
703
|
+
UserQuestion,
|
|
704
|
+
user,
|
|
705
|
+
dataSource,
|
|
706
|
+
ConversationId,
|
|
707
|
+
userPayload,
|
|
708
|
+
pubSub,
|
|
709
|
+
convoEntity,
|
|
710
|
+
convoDetailEntity
|
|
711
|
+
);
|
|
712
|
+
} else if (apiResponse.responsePhase === 'analysis_complete') {
|
|
713
|
+
return await this.HandleAnalysisComplete(
|
|
714
|
+
input,
|
|
715
|
+
<SkipAPIAnalysisCompleteResponse>apiResponse,
|
|
716
|
+
UserQuestion,
|
|
717
|
+
user,
|
|
718
|
+
dataSource,
|
|
719
|
+
ConversationId,
|
|
720
|
+
userPayload,
|
|
721
|
+
pubSub,
|
|
722
|
+
convoEntity,
|
|
723
|
+
convoDetailEntity,
|
|
724
|
+
dataContext,
|
|
725
|
+
dataContextEntity
|
|
726
|
+
);
|
|
727
|
+
} else {
|
|
728
|
+
// unknown response phase
|
|
729
|
+
throw new Error(`Unknown Skip API response phase: ${apiResponse.responsePhase}`);
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
733
|
+
message: JSON.stringify({
|
|
734
|
+
type: 'AskSkip',
|
|
735
|
+
status: 'Error',
|
|
736
|
+
conversationID: ConversationId,
|
|
737
|
+
message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
|
|
738
|
+
}),
|
|
739
|
+
sessionId: userPayload.sessionId,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
Success: false,
|
|
744
|
+
Status: 'Error',
|
|
745
|
+
Result: `User Question ${UserQuestion} didn't work!`,
|
|
746
|
+
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
747
|
+
ConversationId: ConversationId,
|
|
748
|
+
UserMessageConversationDetailId: '',
|
|
749
|
+
AIMessageConversationDetailId: '',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
protected async PublishApiResponseUserUpdateMessage(
|
|
755
|
+
apiResponse: SkipAPIResponse,
|
|
756
|
+
userPayload: UserPayload,
|
|
757
|
+
conversationID: string,
|
|
758
|
+
pubSub: PubSubEngine
|
|
759
|
+
) {
|
|
760
|
+
let sUserMessage: string = '';
|
|
761
|
+
switch (apiResponse.responsePhase) {
|
|
762
|
+
case 'data_request':
|
|
763
|
+
sUserMessage = 'We need to gather some more data, I will do that next and update you soon.';
|
|
764
|
+
break;
|
|
765
|
+
case 'analysis_complete':
|
|
766
|
+
sUserMessage = 'I have completed the analysis, the results will be available momentarily.';
|
|
767
|
+
break;
|
|
768
|
+
case 'clarifying_question':
|
|
769
|
+
// don't send an update because the actual message will happen and show up in the UI, so this is redundant
|
|
770
|
+
//sUserMessage = 'I have a clarifying question for you, please see review our chat so you can provide me a little more info.';
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// update the UI
|
|
775
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
776
|
+
message: JSON.stringify({
|
|
777
|
+
type: 'AskSkip',
|
|
778
|
+
status: 'OK',
|
|
779
|
+
conversationID,
|
|
780
|
+
message: sUserMessage,
|
|
781
|
+
}),
|
|
782
|
+
sessionId: userPayload.sessionId,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
protected async HandleAnalysisComplete(
|
|
787
|
+
apiRequest: SkipAPIRequest,
|
|
788
|
+
apiResponse: SkipAPIAnalysisCompleteResponse,
|
|
789
|
+
UserQuestion: string,
|
|
790
|
+
user: UserInfo,
|
|
791
|
+
dataSource: DataSource,
|
|
792
|
+
ConversationId: string,
|
|
793
|
+
userPayload: UserPayload,
|
|
794
|
+
pubSub: PubSubEngine,
|
|
795
|
+
convoEntity: ConversationEntity,
|
|
796
|
+
convoDetailEntity: ConversationDetailEntity,
|
|
797
|
+
dataContext: DataContext,
|
|
798
|
+
dataContextEntity: DataContextEntity
|
|
799
|
+
): Promise<AskSkipResultType> {
|
|
800
|
+
// analysis is complete
|
|
801
|
+
// all done, wrap things up
|
|
802
|
+
const md = new Metadata();
|
|
803
|
+
const { AIMessageConversationDetailID } = await this.FinishConversationAndNotifyUser(
|
|
804
|
+
apiResponse,
|
|
805
|
+
dataContext,
|
|
806
|
+
dataContextEntity,
|
|
807
|
+
md,
|
|
808
|
+
user,
|
|
809
|
+
convoEntity,
|
|
810
|
+
pubSub,
|
|
811
|
+
userPayload
|
|
812
|
+
);
|
|
813
|
+
const response: AskSkipResultType = {
|
|
814
|
+
Success: true,
|
|
815
|
+
Status: 'OK',
|
|
816
|
+
ResponsePhase: SkipResponsePhase.AnalysisComplete,
|
|
817
|
+
ConversationId: ConversationId,
|
|
818
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
819
|
+
AIMessageConversationDetailId: AIMessageConversationDetailID,
|
|
820
|
+
Result: JSON.stringify(apiResponse),
|
|
821
|
+
};
|
|
822
|
+
return response;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
protected async HandleClarifyingQuestionPhase(
|
|
826
|
+
apiRequest: SkipAPIRequest,
|
|
827
|
+
apiResponse: SkipAPIClarifyingQuestionResponse,
|
|
828
|
+
UserQuestion: string,
|
|
829
|
+
user: UserInfo,
|
|
830
|
+
dataSource: DataSource,
|
|
831
|
+
ConversationId: string,
|
|
832
|
+
userPayload: UserPayload,
|
|
833
|
+
pubSub: PubSubEngine,
|
|
834
|
+
convoEntity: ConversationEntity,
|
|
835
|
+
convoDetailEntity: ConversationDetailEntity
|
|
836
|
+
): Promise<AskSkipResultType> {
|
|
837
|
+
// need to create a message here in the COnversation and then pass that id below
|
|
838
|
+
const md = new Metadata();
|
|
839
|
+
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
840
|
+
convoDetailEntityAI.NewRecord();
|
|
841
|
+
convoDetailEntityAI.ConversationID = ConversationId;
|
|
842
|
+
convoDetailEntityAI.Message = JSON.stringify(apiResponse); //.clarifyingQuestion;
|
|
843
|
+
convoDetailEntityAI.Role = 'AI';
|
|
844
|
+
convoDetailEntityAI.HiddenToUser = false;
|
|
845
|
+
if (await convoDetailEntityAI.Save()) {
|
|
846
|
+
return {
|
|
847
|
+
Success: true,
|
|
848
|
+
Status: 'OK',
|
|
849
|
+
ResponsePhase: SkipResponsePhase.ClarifyingQuestion,
|
|
850
|
+
ConversationId: ConversationId,
|
|
851
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
852
|
+
AIMessageConversationDetailId: convoDetailEntityAI.ID,
|
|
853
|
+
Result: JSON.stringify(apiResponse),
|
|
854
|
+
};
|
|
855
|
+
} else {
|
|
856
|
+
LogError(
|
|
857
|
+
`Error saving conversation detail entity for AI message: ${apiResponse.clarifyingQuestion}`,
|
|
858
|
+
undefined,
|
|
859
|
+
convoDetailEntityAI.LatestResult
|
|
860
|
+
);
|
|
861
|
+
return {
|
|
862
|
+
Success: false,
|
|
863
|
+
Status: 'Error',
|
|
864
|
+
ResponsePhase: SkipResponsePhase.ClarifyingQuestion,
|
|
865
|
+
ConversationId: ConversationId,
|
|
866
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
867
|
+
AIMessageConversationDetailId: convoDetailEntityAI.ID,
|
|
868
|
+
Result: JSON.stringify(apiResponse),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
protected async HandleDataRequestPhase(
|
|
874
|
+
apiRequest: SkipAPIRequest,
|
|
875
|
+
apiResponse: SkipAPIDataRequestResponse,
|
|
876
|
+
UserQuestion: string,
|
|
877
|
+
user: UserInfo,
|
|
878
|
+
dataSource: DataSource,
|
|
879
|
+
ConversationId: string,
|
|
880
|
+
userPayload: UserPayload,
|
|
881
|
+
pubSub: PubSubEngine,
|
|
882
|
+
convoEntity: ConversationEntity,
|
|
883
|
+
convoDetailEntity: ConversationDetailEntity,
|
|
884
|
+
dataContext: DataContext,
|
|
885
|
+
dataContextEntity: DataContextEntity
|
|
886
|
+
): Promise<AskSkipResultType> {
|
|
887
|
+
// our job in this method is to go through each of the data requests from the Skip API, get the data, and then go back to the Skip API again and to the next phase
|
|
888
|
+
try {
|
|
889
|
+
if (!apiResponse.success) {
|
|
890
|
+
LogError(`Data request/gathering from Skip API failed: ${apiResponse.error}`);
|
|
891
|
+
return {
|
|
892
|
+
Success: false,
|
|
893
|
+
Status: `The Skip API Server data gathering phase returned a non-recoverable error. Try again later and Skip might be able to handle this request.\n${apiResponse.error}`,
|
|
894
|
+
ResponsePhase: SkipResponsePhase.DataRequest,
|
|
895
|
+
ConversationId: ConversationId,
|
|
896
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
897
|
+
AIMessageConversationDetailId: '',
|
|
898
|
+
Result: JSON.stringify(apiResponse),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const _maxDataGatheringRetries = 5;
|
|
903
|
+
const _dataGatheringFailureHeaderMessage = '***DATA GATHERING FAILURE***';
|
|
904
|
+
const md = new Metadata();
|
|
905
|
+
const executionErrors = [];
|
|
906
|
+
let dataRequest = apiResponse.dataRequest;
|
|
907
|
+
|
|
908
|
+
// first, in this situation we want to add a message to our apiRequest so that it is part of the message history with the server
|
|
909
|
+
apiRequest.messages.push({
|
|
910
|
+
content: `Skip API Requested Data as shown below
|
|
911
|
+
${JSON.stringify(apiResponse.dataRequest)}`,
|
|
912
|
+
role: 'system', // user role of system because this came from Skip, we are simplifying the message for the next round if we need to send it back
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// check to see if apiResponse.dataRequest is an array, if not, see if it is a single item, and if not, then throw an error
|
|
916
|
+
if (!Array.isArray(dataRequest)) {
|
|
917
|
+
if (dataRequest) {
|
|
918
|
+
dataRequest = [dataRequest];
|
|
919
|
+
} else {
|
|
920
|
+
const errorMessage = `Data request from Skip API is not an array and not a single item.`;
|
|
921
|
+
LogError(errorMessage);
|
|
922
|
+
executionErrors.push({ dataRequest: apiResponse.dataRequest, errorMessage: errorMessage });
|
|
923
|
+
dataRequest = []; // make a blank array so we can continue
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for (const dr of dataRequest) {
|
|
928
|
+
try {
|
|
929
|
+
const item = dataContext.AddDataContextItem();
|
|
930
|
+
switch (dr.type) {
|
|
931
|
+
case 'sql':
|
|
932
|
+
item.Type = 'sql';
|
|
933
|
+
item.SQL = dr.text;
|
|
934
|
+
item.AdditionalDescription = dr.description;
|
|
935
|
+
if (!(await item.LoadData(dataSource, false, false, 0, user)))
|
|
936
|
+
throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
|
|
937
|
+
break;
|
|
938
|
+
case 'stored_query':
|
|
939
|
+
const queryName = dr.text;
|
|
940
|
+
const query = md.Queries.find((q) => q.Name === queryName);
|
|
941
|
+
if (query) {
|
|
942
|
+
item.Type = 'query';
|
|
943
|
+
item.QueryID = query.ID;
|
|
944
|
+
item.RecordName = query.Name;
|
|
945
|
+
item.AdditionalDescription = dr.description;
|
|
946
|
+
if (!(await item.LoadData(dataSource, false, false, 0, user)))
|
|
947
|
+
throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
|
|
948
|
+
} else throw new Error(`Query ${queryName} not found.`);
|
|
949
|
+
break;
|
|
950
|
+
default:
|
|
951
|
+
throw new Error(`Unknown data request type: ${dr.type}`);
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
} catch (e) {
|
|
955
|
+
LogError(e);
|
|
956
|
+
executionErrors.push({
|
|
957
|
+
dataRequest: dr,
|
|
958
|
+
errorMessage: e && typeof e === 'object' && 'message' in e && e.message ? e.message : e.toString(),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (executionErrors.length > 0) {
|
|
964
|
+
const dataGatheringFailedAttemptCount =
|
|
965
|
+
apiRequest.messages.filter((m) => m.content.includes(_dataGatheringFailureHeaderMessage)).length + 1;
|
|
966
|
+
if (dataGatheringFailedAttemptCount > _maxDataGatheringRetries) {
|
|
967
|
+
// we have exceeded the max retries, so in this case we do NOT go back to Skip, instead we just send the errors back to the user
|
|
968
|
+
LogStatus(
|
|
969
|
+
`Execution errors for Skip data request occured, and we have exceeded the max retries${_maxDataGatheringRetries}, sending errors back to the user.`
|
|
970
|
+
);
|
|
971
|
+
return {
|
|
972
|
+
Success: false,
|
|
973
|
+
Status:
|
|
974
|
+
'Error gathering data and we have exceedded the max retries. Try again later and Skip might be able to handle this request.',
|
|
975
|
+
ResponsePhase: SkipResponsePhase.DataRequest,
|
|
976
|
+
ConversationId: ConversationId,
|
|
977
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
978
|
+
AIMessageConversationDetailId: '',
|
|
979
|
+
Result: JSON.stringify(apiResponse),
|
|
980
|
+
};
|
|
981
|
+
} else {
|
|
982
|
+
LogStatus(`Execution errors for Skip data request occured, sending those errors back to the Skip API to get new instructions.`);
|
|
983
|
+
apiRequest.requestPhase = 'data_gathering_failure';
|
|
984
|
+
apiRequest.messages.push({
|
|
985
|
+
content: `${_dataGatheringFailureHeaderMessage} #${dataGatheringFailedAttemptCount} of ${_maxDataGatheringRetries} attempts to gather data failed. Errors:
|
|
986
|
+
${JSON.stringify(executionErrors)}
|
|
987
|
+
`,
|
|
988
|
+
role: 'user', // use user role becuase to the Skip API what we send it is "user"
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
await dataContext.SaveItems(user, false); // save the data context items
|
|
993
|
+
// replace the data context copy that is in the apiRequest.
|
|
994
|
+
apiRequest.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
|
|
995
|
+
apiRequest.requestPhase = 'data_gathering_response';
|
|
996
|
+
}
|
|
997
|
+
// we have all of the data now, add it to the data context and then submit it back to the Skip API
|
|
998
|
+
return this.HandleSkipRequest(
|
|
999
|
+
apiRequest,
|
|
1000
|
+
UserQuestion,
|
|
1001
|
+
user,
|
|
1002
|
+
dataSource,
|
|
1003
|
+
ConversationId,
|
|
1004
|
+
userPayload,
|
|
1005
|
+
pubSub,
|
|
1006
|
+
md,
|
|
1007
|
+
convoEntity,
|
|
1008
|
+
convoDetailEntity,
|
|
1009
|
+
dataContext,
|
|
1010
|
+
dataContextEntity
|
|
1011
|
+
);
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
LogError(e);
|
|
1014
|
+
throw e;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* This method will handle the process for an end of successful request where a user is notified of an AI message. The AI message is either the finished report or a clarifying question.
|
|
1020
|
+
* @param apiResponse
|
|
1021
|
+
* @param md
|
|
1022
|
+
* @param user
|
|
1023
|
+
* @param convoEntity
|
|
1024
|
+
* @param pubSub
|
|
1025
|
+
* @param userPayload
|
|
1026
|
+
* @returns
|
|
1027
|
+
*/
|
|
1028
|
+
protected async FinishConversationAndNotifyUser(
|
|
1029
|
+
apiResponse: SkipAPIAnalysisCompleteResponse,
|
|
1030
|
+
dataContext: DataContext,
|
|
1031
|
+
dataContextEntity: DataContextEntity,
|
|
1032
|
+
md: Metadata,
|
|
1033
|
+
user: UserInfo,
|
|
1034
|
+
convoEntity: ConversationEntity,
|
|
1035
|
+
pubSub: PubSubEngine,
|
|
1036
|
+
userPayload: UserPayload
|
|
1037
|
+
): Promise<{ AIMessageConversationDetailID: string }> {
|
|
1038
|
+
const sTitle = apiResponse.reportTitle;
|
|
1039
|
+
const sResult = JSON.stringify(apiResponse);
|
|
1040
|
+
|
|
1041
|
+
// Create a conversation detail record for the Skip response
|
|
1042
|
+
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
1043
|
+
convoDetailEntityAI.NewRecord();
|
|
1044
|
+
convoDetailEntityAI.ConversationID = convoEntity.ID;
|
|
1045
|
+
convoDetailEntityAI.Message = sResult;
|
|
1046
|
+
convoDetailEntityAI.Role = 'AI';
|
|
1047
|
+
convoDetailEntityAI.HiddenToUser = false;
|
|
1048
|
+
convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
|
|
1049
|
+
const convoDetailSaveResult: boolean = await convoDetailEntityAI.Save();
|
|
1050
|
+
if (!convoDetailSaveResult) {
|
|
1051
|
+
LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// finally update the convo name if it is still the default
|
|
1055
|
+
if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
|
|
1056
|
+
convoEntity.Name = sTitle; // use the title from the response
|
|
1057
|
+
const convoEntitySaveResult: boolean = await convoEntity.Save();
|
|
1058
|
+
if (!convoEntitySaveResult) {
|
|
1059
|
+
LogError(`Error saving conversation entity for AI message: ${sResult}`, undefined, convoEntity.LatestResult);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// now create a notification for the user
|
|
1064
|
+
const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
|
|
1065
|
+
userNotification.NewRecord();
|
|
1066
|
+
userNotification.UserID = user.ID;
|
|
1067
|
+
userNotification.Title = 'Report Created: ' + sTitle;
|
|
1068
|
+
userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
|
|
1069
|
+
userNotification.Unread = true;
|
|
1070
|
+
userNotification.ResourceConfiguration = JSON.stringify({
|
|
1071
|
+
type: 'askskip',
|
|
1072
|
+
conversationId: convoEntity.ID,
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const userNotificationSaveResult: boolean = await userNotification.Save();
|
|
1076
|
+
if (!userNotificationSaveResult) {
|
|
1077
|
+
LogError(`Error saving user notification entity for AI message: ${sResult}`, undefined, userNotification.LatestResult);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Save the data context items...
|
|
1081
|
+
// FOR NOW, we don't want to store the data in the database, we will just load it from the data context when we need it
|
|
1082
|
+
// we need a better strategy to persist because the cost of storage and retrieval/parsing is higher than just running the query again in many/most cases
|
|
1083
|
+
dataContext.SaveItems(user, false);
|
|
1084
|
+
|
|
1085
|
+
// send a UI update trhough pub-sub
|
|
1086
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
1087
|
+
message: JSON.stringify({
|
|
1088
|
+
type: 'UserNotifications',
|
|
1089
|
+
status: 'OK',
|
|
1090
|
+
conversationID: convoEntity.ID,
|
|
1091
|
+
details: {
|
|
1092
|
+
action: 'create',
|
|
1093
|
+
recordId: userNotification.ID,
|
|
1094
|
+
},
|
|
1095
|
+
}),
|
|
1096
|
+
sessionId: userPayload.sessionId,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
return {
|
|
1100
|
+
AIMessageConversationDetailID: convoDetailEntityAI.ID,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
|
|
1105
|
+
const rv = new RunView();
|
|
1106
|
+
const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
|
|
1107
|
+
if (result && result.Success) return result.Results;
|
|
1108
|
+
else throw new Error(`Error running view ${ViewId}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export default AskSkipResolver;
|