@memberjunction/server 2.1.4 → 2.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.
Files changed (149) hide show
  1. package/dist/apolloServer/TransactionPlugin.d.ts +1 -1
  2. package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -1
  3. package/dist/apolloServer/TransactionPlugin.js.map +1 -1
  4. package/dist/apolloServer/index.d.ts +1 -1
  5. package/dist/apolloServer/index.d.ts.map +1 -1
  6. package/dist/apolloServer/index.js +2 -2
  7. package/dist/apolloServer/index.js.map +1 -1
  8. package/dist/auth/exampleNewUserSubClass.d.ts +1 -1
  9. package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
  10. package/dist/auth/exampleNewUserSubClass.js +7 -7
  11. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  12. package/dist/auth/index.d.ts +1 -1
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +18 -8
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/auth/newUsers.js +1 -1
  17. package/dist/auth/newUsers.js.map +1 -1
  18. package/dist/context.d.ts +1 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +4 -4
  21. package/dist/context.js.map +1 -1
  22. package/dist/directives/Public.d.ts +1 -1
  23. package/dist/directives/Public.d.ts.map +1 -1
  24. package/dist/directives/index.d.ts +1 -1
  25. package/dist/directives/index.d.ts.map +1 -1
  26. package/dist/directives/index.js +1 -1
  27. package/dist/directives/index.js.map +1 -1
  28. package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
  29. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
  30. package/dist/entitySubclasses/entityPermissions.server.js +5 -6
  31. package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
  32. package/dist/generated/generated.d.ts +5 -31
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +33 -189
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +3 -3
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +1 -1
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts +2 -2
  41. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  42. package/dist/generic/RunViewResolver.js +1 -1
  43. package/dist/generic/RunViewResolver.js.map +1 -1
  44. package/dist/index.d.ts +21 -21
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +36 -27
  47. package/dist/index.js.map +1 -1
  48. package/dist/orm.js +4 -4
  49. package/dist/orm.js.map +1 -1
  50. package/dist/resolvers/AskSkipResolver.d.ts +3 -3
  51. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  52. package/dist/resolvers/AskSkipResolver.js +57 -55
  53. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  54. package/dist/resolvers/ColorResolver.d.ts +1 -1
  55. package/dist/resolvers/ColorResolver.d.ts.map +1 -1
  56. package/dist/resolvers/ColorResolver.js +1 -1
  57. package/dist/resolvers/ColorResolver.js.map +1 -1
  58. package/dist/resolvers/DatasetResolver.d.ts +1 -1
  59. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  60. package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -2
  61. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  62. package/dist/resolvers/EntityCommunicationsResolver.js +9 -4
  63. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -2
  65. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  66. package/dist/resolvers/EntityRecordNameResolver.js +2 -2
  67. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  68. package/dist/resolvers/EntityResolver.d.ts +2 -2
  69. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  70. package/dist/resolvers/EntityResolver.js +1 -1
  71. package/dist/resolvers/EntityResolver.js.map +1 -1
  72. package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
  73. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  74. package/dist/resolvers/FileCategoryResolver.js +2 -2
  75. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  76. package/dist/resolvers/FileResolver.d.ts +2 -2
  77. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  78. package/dist/resolvers/FileResolver.js +3 -3
  79. package/dist/resolvers/FileResolver.js.map +1 -1
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -2
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +4 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -2
  85. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  86. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  87. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  88. package/dist/resolvers/QueryResolver.d.ts +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  90. package/dist/resolvers/ReportResolver.d.ts +1 -1
  91. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  92. package/dist/resolvers/ReportResolver.js +16 -14
  93. package/dist/resolvers/ReportResolver.js.map +1 -1
  94. package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
  95. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  96. package/dist/resolvers/UserFavoriteResolver.js +17 -16
  97. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  98. package/dist/resolvers/UserResolver.d.ts +1 -1
  99. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  100. package/dist/resolvers/UserResolver.js +1 -1
  101. package/dist/resolvers/UserResolver.js.map +1 -1
  102. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  103. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  104. package/dist/resolvers/UserViewResolver.js +2 -2
  105. package/dist/resolvers/UserViewResolver.js.map +1 -1
  106. package/dist/util.d.ts.map +1 -1
  107. package/dist/util.js +11 -5
  108. package/dist/util.js.map +1 -1
  109. package/package.json +27 -23
  110. package/src/apolloServer/TransactionPlugin.ts +53 -0
  111. package/src/apolloServer/index.ts +33 -0
  112. package/src/auth/exampleNewUserSubClass.ts +79 -0
  113. package/src/auth/index.ts +171 -0
  114. package/src/auth/newUsers.ts +58 -0
  115. package/src/auth/tokenExpiredError.ts +12 -0
  116. package/src/cache.ts +10 -0
  117. package/src/config.ts +89 -0
  118. package/src/context.ts +111 -0
  119. package/src/directives/Public.ts +42 -0
  120. package/src/directives/index.ts +1 -0
  121. package/src/entitySubclasses/DuplicateRunEntity.server.ts +29 -0
  122. package/src/entitySubclasses/entityPermissions.server.ts +104 -0
  123. package/src/entitySubclasses/userViewEntity.server.ts +187 -0
  124. package/src/generated/generated.ts +25265 -0
  125. package/src/generic/DeleteOptionsInput.ts +13 -0
  126. package/src/generic/KeyInputOutputTypes.ts +35 -0
  127. package/src/generic/KeyValuePairInput.ts +14 -0
  128. package/src/generic/PushStatusResolver.ts +40 -0
  129. package/src/generic/ResolverBase.ts +767 -0
  130. package/src/generic/RunViewResolver.ts +579 -0
  131. package/src/index.ts +171 -0
  132. package/src/orm.ts +36 -0
  133. package/src/resolvers/AskSkipResolver.ts +1109 -0
  134. package/src/resolvers/ColorResolver.ts +61 -0
  135. package/src/resolvers/DatasetResolver.ts +115 -0
  136. package/src/resolvers/EntityCommunicationsResolver.ts +221 -0
  137. package/src/resolvers/EntityRecordNameResolver.ts +75 -0
  138. package/src/resolvers/EntityResolver.ts +35 -0
  139. package/src/resolvers/FileCategoryResolver.ts +69 -0
  140. package/src/resolvers/FileResolver.ts +152 -0
  141. package/src/resolvers/MergeRecordsResolver.ts +175 -0
  142. package/src/resolvers/PotentialDuplicateRecordResolver.ts +91 -0
  143. package/src/resolvers/QueryResolver.ts +42 -0
  144. package/src/resolvers/ReportResolver.ts +144 -0
  145. package/src/resolvers/UserFavoriteResolver.ts +176 -0
  146. package/src/resolvers/UserResolver.ts +33 -0
  147. package/src/resolvers/UserViewResolver.ts +64 -0
  148. package/src/types.ts +40 -0
  149. package/src/util.ts +112 -0
@@ -0,0 +1,1109 @@
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(() => Int)
66
+ ConversationId: string;
67
+
68
+ @Field(() => Int)
69
+ UserMessageConversationDetailId: string;
70
+
71
+ @Field(() => Int)
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({ dataRequest: dr, errorMessage: e && typeof e === 'object' && 'message' in e && e.message ? e.message : e.toString() });
957
+ }
958
+ }
959
+
960
+ if (executionErrors.length > 0) {
961
+ const dataGatheringFailedAttemptCount =
962
+ apiRequest.messages.filter((m) => m.content.includes(_dataGatheringFailureHeaderMessage)).length + 1;
963
+ if (dataGatheringFailedAttemptCount > _maxDataGatheringRetries) {
964
+ // 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
965
+ LogStatus(
966
+ `Execution errors for Skip data request occured, and we have exceeded the max retries${_maxDataGatheringRetries}, sending errors back to the user.`
967
+ );
968
+ return {
969
+ Success: false,
970
+ Status:
971
+ 'Error gathering data and we have exceedded the max retries. Try again later and Skip might be able to handle this request.',
972
+ ResponsePhase: SkipResponsePhase.DataRequest,
973
+ ConversationId: ConversationId,
974
+ UserMessageConversationDetailId: convoDetailEntity.ID,
975
+ AIMessageConversationDetailId: '',
976
+ Result: JSON.stringify(apiResponse),
977
+ };
978
+ } else {
979
+ LogStatus(`Execution errors for Skip data request occured, sending those errors back to the Skip API to get new instructions.`);
980
+ apiRequest.requestPhase = 'data_gathering_failure';
981
+ apiRequest.messages.push({
982
+ content: `${_dataGatheringFailureHeaderMessage} #${dataGatheringFailedAttemptCount} of ${_maxDataGatheringRetries} attempts to gather data failed. Errors:
983
+ ${JSON.stringify(executionErrors)}
984
+ `,
985
+ role: 'user', // use user role becuase to the Skip API what we send it is "user"
986
+ });
987
+ }
988
+ } else {
989
+ await dataContext.SaveItems(user, false); // save the data context items
990
+ // replace the data context copy that is in the apiRequest.
991
+ 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
992
+ apiRequest.requestPhase = 'data_gathering_response';
993
+ }
994
+ // we have all of the data now, add it to the data context and then submit it back to the Skip API
995
+ return this.HandleSkipRequest(
996
+ apiRequest,
997
+ UserQuestion,
998
+ user,
999
+ dataSource,
1000
+ ConversationId,
1001
+ userPayload,
1002
+ pubSub,
1003
+ md,
1004
+ convoEntity,
1005
+ convoDetailEntity,
1006
+ dataContext,
1007
+ dataContextEntity
1008
+ );
1009
+ } catch (e) {
1010
+ LogError(e);
1011
+ throw e;
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * 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.
1017
+ * @param apiResponse
1018
+ * @param md
1019
+ * @param user
1020
+ * @param convoEntity
1021
+ * @param pubSub
1022
+ * @param userPayload
1023
+ * @returns
1024
+ */
1025
+ protected async FinishConversationAndNotifyUser(
1026
+ apiResponse: SkipAPIAnalysisCompleteResponse,
1027
+ dataContext: DataContext,
1028
+ dataContextEntity: DataContextEntity,
1029
+ md: Metadata,
1030
+ user: UserInfo,
1031
+ convoEntity: ConversationEntity,
1032
+ pubSub: PubSubEngine,
1033
+ userPayload: UserPayload
1034
+ ): Promise<{ AIMessageConversationDetailID: string }> {
1035
+ const sTitle = apiResponse.reportTitle;
1036
+ const sResult = JSON.stringify(apiResponse);
1037
+
1038
+ // Create a conversation detail record for the Skip response
1039
+ const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
1040
+ convoDetailEntityAI.NewRecord();
1041
+ convoDetailEntityAI.ConversationID = convoEntity.ID;
1042
+ convoDetailEntityAI.Message = sResult;
1043
+ convoDetailEntityAI.Role = 'AI';
1044
+ convoDetailEntityAI.HiddenToUser = false;
1045
+ convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
1046
+ const convoDetailSaveResult: boolean = await convoDetailEntityAI.Save();
1047
+ if (!convoDetailSaveResult) {
1048
+ LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
1049
+ }
1050
+
1051
+ // finally update the convo name if it is still the default
1052
+ if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
1053
+ convoEntity.Name = sTitle; // use the title from the response
1054
+ const convoEntitySaveResult: boolean = await convoEntity.Save();
1055
+ if (!convoEntitySaveResult) {
1056
+ LogError(`Error saving conversation entity for AI message: ${sResult}`, undefined, convoEntity.LatestResult);
1057
+ }
1058
+ }
1059
+
1060
+ // now create a notification for the user
1061
+ const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
1062
+ userNotification.NewRecord();
1063
+ userNotification.UserID = user.ID;
1064
+ userNotification.Title = 'Report Created: ' + sTitle;
1065
+ userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
1066
+ userNotification.Unread = true;
1067
+ userNotification.ResourceConfiguration = JSON.stringify({
1068
+ type: 'askskip',
1069
+ conversationId: convoEntity.ID,
1070
+ });
1071
+
1072
+ const userNotificationSaveResult: boolean = await userNotification.Save();
1073
+ if (!userNotificationSaveResult) {
1074
+ LogError(`Error saving user notification entity for AI message: ${sResult}`, undefined, userNotification.LatestResult);
1075
+ }
1076
+
1077
+ // Save the data context items...
1078
+ // 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
1079
+ // 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
1080
+ dataContext.SaveItems(user, false);
1081
+
1082
+ // send a UI update trhough pub-sub
1083
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1084
+ message: JSON.stringify({
1085
+ type: 'UserNotifications',
1086
+ status: 'OK',
1087
+ conversationID: convoEntity.ID,
1088
+ details: {
1089
+ action: 'create',
1090
+ recordId: userNotification.ID,
1091
+ },
1092
+ }),
1093
+ sessionId: userPayload.sessionId,
1094
+ });
1095
+
1096
+ return {
1097
+ AIMessageConversationDetailID: convoDetailEntityAI.ID,
1098
+ };
1099
+ }
1100
+
1101
+ protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
1102
+ const rv = new RunView();
1103
+ const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
1104
+ if (result && result.Success) return result.Results;
1105
+ else throw new Error(`Error running view ${ViewId}`);
1106
+ }
1107
+ }
1108
+
1109
+ export default AskSkipResolver;