@memberjunction/server 3.2.0 → 3.4.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 (127) hide show
  1. package/README.md +106 -1
  2. package/dist/auth/APIKeyScopeAuth.d.ts +51 -0
  3. package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -0
  4. package/dist/auth/APIKeyScopeAuth.js +163 -0
  5. package/dist/auth/APIKeyScopeAuth.js.map +1 -0
  6. package/dist/auth/BaseAuthProvider.d.ts +1 -0
  7. package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
  8. package/dist/auth/BaseAuthProvider.js +2 -0
  9. package/dist/auth/BaseAuthProvider.js.map +1 -1
  10. package/dist/auth/IAuthProvider.d.ts +1 -0
  11. package/dist/auth/IAuthProvider.d.ts.map +1 -1
  12. package/dist/auth/index.d.ts +1 -0
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +1 -0
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/config.js +2 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/context.d.ts +8 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +44 -7
  21. package/dist/context.js.map +1 -1
  22. package/dist/generated/generated.d.ts +681 -2
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +10627 -6409
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +3 -2
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +52 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +29 -1
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +143 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/resolvers/APIKeyResolver.d.ts +24 -0
  39. package/dist/resolvers/APIKeyResolver.d.ts.map +1 -0
  40. package/dist/resolvers/APIKeyResolver.js +194 -0
  41. package/dist/resolvers/APIKeyResolver.js.map +1 -0
  42. package/dist/resolvers/ActionResolver.d.ts +2 -1
  43. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  44. package/dist/resolvers/ActionResolver.js +4 -1
  45. package/dist/resolvers/ActionResolver.js.map +1 -1
  46. package/dist/resolvers/DatasetResolver.d.ts +5 -4
  47. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  48. package/dist/resolvers/DatasetResolver.js +7 -4
  49. package/dist/resolvers/DatasetResolver.js.map +1 -1
  50. package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -1
  51. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  52. package/dist/resolvers/EntityCommunicationsResolver.js +3 -1
  53. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  54. package/dist/resolvers/GetDataContextDataResolver.d.ts +2 -1
  55. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  56. package/dist/resolvers/GetDataContextDataResolver.js +10 -3
  57. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  58. package/dist/resolvers/MCPResolver.d.ts +37 -0
  59. package/dist/resolvers/MCPResolver.d.ts.map +1 -0
  60. package/dist/resolvers/MCPResolver.js +363 -0
  61. package/dist/resolvers/MCPResolver.js.map +1 -0
  62. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
  63. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  64. package/dist/resolvers/MergeRecordsResolver.js +3 -1
  65. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  66. package/dist/resolvers/QueryResolver.d.ts +2 -1
  67. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  68. package/dist/resolvers/QueryResolver.js +6 -1
  69. package/dist/resolvers/QueryResolver.js.map +1 -1
  70. package/dist/resolvers/ReportResolver.d.ts +2 -1
  71. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  72. package/dist/resolvers/ReportResolver.js +4 -1
  73. package/dist/resolvers/ReportResolver.js.map +1 -1
  74. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  75. package/dist/resolvers/RunAIAgentResolver.js +3 -1
  76. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  77. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  78. package/dist/resolvers/RunAIPromptResolver.js +3 -0
  79. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  80. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  81. package/dist/resolvers/RunTemplateResolver.js +1 -0
  82. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  83. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  84. package/dist/resolvers/TaskResolver.js +1 -0
  85. package/dist/resolvers/TaskResolver.js.map +1 -1
  86. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  87. package/dist/resolvers/UserResolver.js +35 -1
  88. package/dist/resolvers/UserResolver.js.map +1 -1
  89. package/dist/types.d.ts +4 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js.map +1 -1
  92. package/package.json +47 -45
  93. package/src/auth/APIKeyScopeAuth.ts +366 -0
  94. package/src/auth/BaseAuthProvider.ts +3 -0
  95. package/src/auth/IAuthProvider.ts +5 -0
  96. package/src/auth/index.ts +1 -0
  97. package/src/config.ts +2 -2
  98. package/src/context.ts +91 -9
  99. package/src/generated/generated.ts +6327 -3668
  100. package/src/generic/ResolverBase.ts +127 -8
  101. package/src/generic/RunViewResolver.ts +132 -5
  102. package/src/index.ts +12 -2
  103. package/src/resolvers/APIKeyResolver.ts +241 -0
  104. package/src/resolvers/ActionResolver.ts +8 -1
  105. package/src/resolvers/DatasetResolver.ts +11 -4
  106. package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
  107. package/src/resolvers/GetDataContextDataResolver.ts +14 -6
  108. package/src/resolvers/MCPResolver.ts +480 -0
  109. package/src/resolvers/MergeRecordsResolver.ts +5 -1
  110. package/src/resolvers/QueryResolver.ts +17 -3
  111. package/src/resolvers/ReportResolver.ts +8 -1
  112. package/src/resolvers/RunAIAgentResolver.ts +7 -1
  113. package/src/resolvers/RunAIPromptResolver.ts +10 -1
  114. package/src/resolvers/RunTemplateResolver.ts +4 -1
  115. package/src/resolvers/TaskResolver.ts +3 -0
  116. package/src/resolvers/UserResolver.ts +52 -4
  117. package/src/types.ts +7 -2
  118. package/dist/resolvers/AskSkipResolver.d.ts +0 -123
  119. package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
  120. package/dist/resolvers/AskSkipResolver.js +0 -1788
  121. package/dist/resolvers/AskSkipResolver.js.map +0 -1
  122. package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
  123. package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
  124. package/dist/scheduler/LearningCycleScheduler.js +0 -4
  125. package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
  126. package/src/resolvers/AskSkipResolver.ts +0 -3446
  127. package/src/scheduler/LearningCycleScheduler.ts +0 -320
@@ -1,3446 +0,0 @@
1
- import { Arg, Ctx, Field, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions, IMetadataProvider } from '@memberjunction/core';
3
- import { AppContext, UserPayload } from '../types.js';
4
- import { BehaviorSubject } from 'rxjs';
5
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
6
- import { DataContext } from '@memberjunction/data-context';
7
- import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
8
- 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
9
-
10
- import {
11
- SkipAPIRequest,
12
- SkipAPIResponse,
13
- SkipMessage,
14
- SkipAPIAnalysisCompleteResponse,
15
- SkipAPIDataRequestResponse,
16
- SkipAPIClarifyingQuestionResponse,
17
- SkipEntityInfo,
18
- SkipQueryInfo,
19
- SkipAPIRunScriptRequest,
20
- SkipAPIRequestAPIKey,
21
- SkipRequestPhase,
22
- SkipAPIAgentNote,
23
- SkipAPIAgentNoteType,
24
- SkipEntityFieldInfo,
25
- SkipEntityRelationshipInfo,
26
- SkipEntityFieldValueInfo,
27
- SkipAPILearningCycleRequest,
28
- SkipConversation,
29
- SkipAPIArtifact,
30
- SkipAPIAgentRequest,
31
- SkipAPIArtifactType,
32
- SkipAPIArtifactVersion,
33
- } from '@memberjunction/skip-types';
34
-
35
- import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
36
-
37
- import {
38
- AIAgentLearningCycleEntity,
39
- AIAgentNoteEntity,
40
- AIAgentRequestEntity,
41
- ArtifactTypeEntity,
42
- ConversationArtifactEntity,
43
- ConversationArtifactVersionEntity,
44
- ConversationDetailEntity,
45
- ConversationEntity,
46
- DataContextEntity,
47
- DataContextItemEntity,
48
- UserNotificationEntity
49
- } from '@memberjunction/core-entities';
50
- import { apiKey as callbackAPIKey, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath } from '../config.js';
51
- import mssql from 'mssql';
52
-
53
- import { registerEnumType } from 'type-graphql';
54
- import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
55
- import { GetReadWriteProvider, sendPostRequest } from '../util.js';
56
- import { GetAIAPIKey } from '@memberjunction/ai';
57
- import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
58
- import { AIEngine } from '@memberjunction/aiengine';
59
- import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
60
- import e from 'express';
61
-
62
- /**
63
- * Skip API Endpoints Configuration
64
- * Defines all available endpoints for the Skip API
65
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
66
- */
67
- const SKIP_API_ENDPOINTS = {
68
- CHAT: '/chat',
69
- LEARNING: '/learning',
70
- FEEDBACK_COMPONENT: '/feedback/component',
71
- REGISTRY: '/registry',
72
- // Add more endpoints as needed
73
- } as const;
74
-
75
- /**
76
- * Store for active conversation streams
77
- * Maps conversationID to the last status message received
78
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
79
- */
80
- class ActiveConversationStreams {
81
- private static instance: ActiveConversationStreams;
82
- private streams: Map<string, {
83
- lastStatus: string,
84
- lastUpdate: Date,
85
- startTime: Date, // When processing actually started
86
- sessionIds: Set<string> // Track which sessions are listening
87
- }> = new Map();
88
-
89
- private constructor() {}
90
-
91
- static getInstance(): ActiveConversationStreams {
92
- if (!ActiveConversationStreams.instance) {
93
- ActiveConversationStreams.instance = new ActiveConversationStreams();
94
- }
95
- return ActiveConversationStreams.instance;
96
- }
97
-
98
- updateStatus(conversationId: string, status: string, sessionId?: string) {
99
- const existing = this.streams.get(conversationId);
100
- if (existing) {
101
- existing.lastStatus = status;
102
- existing.lastUpdate = new Date();
103
- if (sessionId) {
104
- existing.sessionIds.add(sessionId);
105
- }
106
- } else {
107
- const now = new Date();
108
- this.streams.set(conversationId, {
109
- lastStatus: status,
110
- lastUpdate: now,
111
- startTime: now, // Track when processing started
112
- sessionIds: sessionId ? new Set([sessionId]) : new Set()
113
- });
114
- }
115
- }
116
-
117
- getStatus(conversationId: string): string | null {
118
- const stream = this.streams.get(conversationId);
119
- return stream ? stream.lastStatus : null;
120
- }
121
-
122
- getStartTime(conversationId: string): Date | null {
123
- const stream = this.streams.get(conversationId);
124
- return stream ? stream.startTime : null;
125
- }
126
-
127
- addSession(conversationId: string, sessionId: string) {
128
- const stream = this.streams.get(conversationId);
129
- if (stream) {
130
- stream.sessionIds.add(sessionId);
131
- } else {
132
- // If no stream exists yet, create one with default status
133
- const now = new Date();
134
- this.streams.set(conversationId, {
135
- lastStatus: 'Processing...',
136
- lastUpdate: now,
137
- startTime: now, // Track when processing started
138
- sessionIds: new Set([sessionId])
139
- });
140
- }
141
- }
142
-
143
- removeConversation(conversationId: string) {
144
- this.streams.delete(conversationId);
145
- }
146
-
147
- isActive(conversationId: string): boolean {
148
- const stream = this.streams.get(conversationId);
149
- if (!stream) return false;
150
-
151
- // Consider a stream inactive if no update in last 5 minutes
152
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
153
- return stream.lastUpdate > fiveMinutesAgo;
154
- }
155
-
156
- getSessionIds(conversationId: string): string[] {
157
- const stream = this.streams.get(conversationId);
158
- return stream ? Array.from(stream.sessionIds) : [];
159
- }
160
-
161
- /**
162
- * Clean up stale streams that haven't been updated in a while
163
- * This prevents memory leaks from abandoned conversations
164
- */
165
- cleanupStaleStreams() {
166
- const now = new Date();
167
- const staleThreshold = new Date(now.getTime() - 30 * 60 * 1000); // 30 minutes
168
-
169
- const staleConversations: string[] = [];
170
- this.streams.forEach((stream, conversationId) => {
171
- if (stream.lastUpdate < staleThreshold) {
172
- staleConversations.push(conversationId);
173
- }
174
- });
175
-
176
- staleConversations.forEach(conversationId => {
177
- this.streams.delete(conversationId);
178
- LogStatus(`Cleaned up stale stream for conversation ${conversationId}`);
179
- });
180
-
181
- if (staleConversations.length > 0) {
182
- LogStatus(`Cleaned up ${staleConversations.length} stale conversation streams`);
183
- }
184
- }
185
- }
186
-
187
- const activeStreams = ActiveConversationStreams.getInstance();
188
-
189
- // Set up periodic cleanup of stale streams (every 10 minutes)
190
- setInterval(() => {
191
- activeStreams.cleanupStaleStreams();
192
- }, 10 * 60 * 1000);
193
-
194
- @ObjectType()
195
- class ReattachConversationResponse {
196
- @Field(() => String, { nullable: true })
197
- lastStatusMessage?: string;
198
-
199
- @Field(() => Date, { nullable: true })
200
- startTime?: Date;
201
- }
202
-
203
- /**
204
- * Enumeration representing the different phases of a Skip response
205
- * Corresponds to the lifecycle of a Skip AI interaction
206
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
207
- */
208
- enum SkipResponsePhase {
209
- /** Skip is asking for clarification before proceeding */
210
- ClarifyingQuestion = 'clarifying_question',
211
- /** Skip is requesting data from the system to process the request */
212
- DataRequest = 'data_request',
213
- /** Skip has completed its analysis and has returned a final response */
214
- AnalysisComplete = 'analysis_complete',
215
- }
216
-
217
- registerEnumType(SkipResponsePhase, {
218
- name: 'SkipResponsePhase',
219
- description: 'The phase of the respons: clarifying_question, data_request, or analysis_complete',
220
- });
221
-
222
- /**
223
- * Result type for Skip AI interactions
224
- * Contains the status of the request, the response phase, the result payload,
225
- * and references to the conversation and message IDs
226
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
227
- */
228
- @ObjectType()
229
- export class AskSkipResultType {
230
- /** Whether the interaction was successful */
231
- @Field(() => Boolean)
232
- Success: boolean;
233
-
234
- /** Status message of the interaction */
235
- @Field(() => String)
236
- Status: string; // required
237
-
238
- /** The phase of the response from Skip */
239
- @Field(() => SkipResponsePhase)
240
- ResponsePhase: SkipResponsePhase;
241
-
242
- /** The result payload, usually a JSON string of the full response */
243
- @Field(() => String)
244
- Result: string;
245
-
246
- /** The ID of the conversation this interaction belongs to */
247
- @Field(() => String)
248
- ConversationId: string;
249
-
250
- /** The ID of the user message in the conversation */
251
- @Field(() => String)
252
- UserMessageConversationDetailId: string;
253
-
254
- /** The ID of the AI response message in the conversation */
255
- @Field(() => String)
256
- AIMessageConversationDetailId: string;
257
- }
258
-
259
- /**
260
- * Result type for manual learning cycle operations
261
- * Contains success status and a message describing the result
262
- */
263
- @ObjectType()
264
- export class ManualLearningCycleResultType {
265
- /** Whether the learning cycle operation was successful */
266
- @Field(() => Boolean)
267
- Success: boolean;
268
-
269
- /** Descriptive message about the learning cycle operation */
270
- @Field(() => String)
271
- Message: string;
272
- }
273
-
274
- /**
275
- * Contains details about a specific learning cycle
276
- * Includes identifier, start time, and duration information
277
- */
278
- @ObjectType()
279
- export class CycleDetailsType {
280
- /** Unique identifier for the learning cycle */
281
- @Field(() => String)
282
- LearningCycleId: string;
283
-
284
- /** ISO timestamp when the cycle started */
285
- @Field(() => String)
286
- StartTime: string;
287
-
288
- /** Duration of the cycle in minutes */
289
- @Field(() => Number)
290
- RunningForMinutes: number;
291
- }
292
-
293
- /**
294
- * Information about an organization that is currently running a learning cycle
295
- * Links organization to specific learning cycle and provides timing details
296
- */
297
- @ObjectType()
298
- export class RunningOrganizationType {
299
- /** Identifier of the organization running the cycle */
300
- @Field(() => String)
301
- OrganizationId: string;
302
-
303
- /** Unique identifier for the learning cycle */
304
- @Field(() => String)
305
- LearningCycleId: string;
306
-
307
- /** ISO timestamp when the cycle started */
308
- @Field(() => String)
309
- StartTime: string;
310
-
311
- /** Duration the cycle has been running in minutes */
312
- @Field(() => Number)
313
- RunningForMinutes: number;
314
- }
315
-
316
- /**
317
- * Status information about the learning cycle scheduler and running cycles
318
- * Provides overall scheduler status and details about active learning cycles
319
- */
320
- @ObjectType()
321
- export class LearningCycleStatusType {
322
- /** Whether the scheduler process is currently active */
323
- @Field(() => Boolean)
324
- IsSchedulerRunning: boolean;
325
-
326
- /** ISO timestamp of the last time the scheduler ran a cycle */
327
- @Field(() => String, { nullable: true })
328
- LastRunTime: string;
329
-
330
- /** List of organizations that are currently running learning cycles */
331
- @Field(() => [RunningOrganizationType], { nullable: true })
332
- RunningOrganizations: RunningOrganizationType[];
333
- }
334
-
335
- /**
336
- * Result of an attempt to stop a learning cycle
337
- * Provides status information about the stop operation
338
- */
339
- @ObjectType()
340
- export class StopLearningCycleResultType {
341
- /** Whether the stop operation succeeded */
342
- @Field(() => Boolean)
343
- Success: boolean;
344
-
345
- /** Descriptive message about the result of the stop operation */
346
- @Field(() => String)
347
- Message: string;
348
-
349
- /** Whether the cycle was actually running when the stop was attempted */
350
- @Field(() => Boolean)
351
- WasRunning: boolean;
352
-
353
- /** Details about the cycle that was stopped (if any) */
354
- @Field(() => CycleDetailsType, { nullable: true })
355
- CycleDetails: CycleDetailsType;
356
- }
357
-
358
- // /**
359
- // * This function initializes the Skip learning cycle scheduler. It sets up an event listener for the server's setup complete event and starts the scheduler if learning cycles are enabled and a valid API endpoint is configured.
360
- // */
361
- // function initializeSkipLearningCycleScheduler() {
362
- // try {
363
- // // Set up event listener for server initialization
364
- // const eventListener = MJGlobal.Instance.GetEventListener(true);
365
- // eventListener.subscribe(event => {
366
- // // Filter for our server's setup complete event
367
- // if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
368
- // try {
369
- // const skipConfigInfo = configInfo.askSkip;
370
- // if (!skipConfigInfo) {
371
- // LogStatus('Skip AI Learning Cycle Scheduler not started: Skip configuration not found');
372
- // return;
373
- // }
374
- // if (!skipConfigInfo.learningCycleEnabled) {
375
- // // Skip AI Learning Cycles not enabled - disabled logging to reduce startup noise
376
- // // LogStatus('Skip AI Learning Cycles not enabled in configuration');
377
- // return;
378
- // }
379
-
380
- // // Check if we have a valid endpoint when cycles are enabled
381
- // const hasLearningEndpoint = (skipConfigInfo.url && skipConfigInfo.url.trim().length > 0) ||
382
- // (skipConfigInfo.learningCycleURL && skipConfigInfo.learningCycleURL.trim().length > 0);
383
- // if (!hasLearningEndpoint) {
384
- // LogError('Skip AI Learning cycle scheduler not started: Learning cycles are enabled but no Learning Cycle API endpoint is configured');
385
- // return;
386
- // }
387
-
388
- // const dataSources = event.args.dataSources;
389
- // if (dataSources && dataSources.length > 0) {
390
- // // Initialize the scheduler
391
- // const scheduler = LearningCycleScheduler.Instance;
392
-
393
- // // Set the data sources for the scheduler
394
- // scheduler.setDataSources(dataSources);
395
-
396
- // // Default is 60 minutes, if the interval is not set in the config, use 60 minutes
397
- // const interval = skipConfigInfo.learningCycleIntervalInMinutes ?? 60;
398
-
399
-
400
- // if (skipConfigInfo.learningCycleRunUponStartup) {
401
- // // If configured to run immediately, run the learning cycle
402
- // LogStatus('Skip API Learning Cycle: Run Upon Startup is enabled, running learning cycle immediately');
403
- // // Start the scheduler
404
- // scheduler.start(interval);
405
- // }
406
- // else {
407
- // // not asked to start right away, just start the scheduler after the interval
408
- // LogStatus(`Skip API Learning Cycle: Scheduler first run will start after interval of ${interval} minutes. If you want a learing cycle to run immediately, set the learningCycleRunUponStartup property in the config file to true.`);
409
-
410
- // // create a one time timer to start the scheduler
411
- // setTimeout(() => {
412
- // LogStatus(`Skip API Learning Cycle: Starting scheduler after ${interval} minutes. If you want a learing cycle to run immediately, set the learningCycleRunUponStartup property in the config file to true.`);
413
- // scheduler.start(interval);
414
- // }, interval * 60 * 1000); // convert minutes to milliseconds
415
- // }
416
- // } else {
417
- // LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
418
- // }
419
- // } catch (error) {
420
- // LogError(`Error initializing Skip learning cycle scheduler: ${error}`);
421
- // }
422
- // }
423
- // });
424
- // } catch (error) {
425
- // // Handle any errors from the static initializer
426
- // LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
427
- // }
428
- // }
429
- // Disabled: Skip AI Learning Cycles no longer used - commented out to prevent startup initialization
430
- // If needed in the future, uncomment the line below:
431
- // initializeSkipLearningCycleScheduler();
432
-
433
- /**
434
- * Base type for Skip API requests containing common fields
435
- * Used as the foundation for both chat and learning cycle requests
436
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
437
- */
438
- type BaseSkipRequest = {
439
- /** Entity metadata to send to Skip */
440
- entities: SkipEntityInfo[],
441
- /** Query metadata to send to Skip */
442
- queries: SkipQueryInfo[],
443
- /** Agent notes to send to Skip */
444
- notes: SkipAPIAgentNote[],
445
- /** Note type definitions to send to Skip */
446
- noteTypes: SkipAPIAgentNoteType[],
447
- /** Agent requests to send to Skip */
448
- requests: SkipAPIAgentRequest[],
449
- /** Access token for authorizing Skip to call back to MemberJunction */
450
- accessToken: GetDataAccessToken,
451
- /** Organization identifier */
452
- organizationID: string,
453
- /** Additional organization-specific information */
454
- organizationInfo: any,
455
- /** API keys for various AI services to be used by Skip */
456
- apiKeys: SkipAPIRequestAPIKey[],
457
- /** URL of the calling server for callback purposes */
458
- callingServerURL: string,
459
- /** API key for the calling server */
460
- callingServerAPIKey: string,
461
- /** Access token for the calling server */
462
- callingServerAccessToken: string,
463
- /** Email of the user making the request */
464
- userEmail: string
465
- }
466
- /**
467
- * Resolver for Skip AI interactions
468
- * Handles conversations with Skip, learning cycles, and related operations.
469
- * Skip is an AI agent that can analyze data, answer questions, and learn from interactions.
470
- * @deprecated AskSkipResolver and related are not in use anymore, the @see SkipProxyAgent is used instead
471
- */
472
- @Resolver(AskSkipResultType)
473
- export class AskSkipResolver {
474
- /** Default name for new conversations */
475
- private static _defaultNewChatName = 'New Chat';
476
-
477
- /** Maximum number of historical messages to include in a conversation context */
478
- private static _maxHistoricalMessages = 30;
479
-
480
- /**
481
- * Handles a chat interaction with Skip about a specific data record
482
- * Allows users to ask questions about a particular entity record
483
- *
484
- * @param UserQuestion The question or message from the user
485
- * @param ConversationId ID of an existing conversation, or empty for a new conversation
486
- * @param EntityName The name of the entity the record belongs to
487
- * @param compositeKey The primary key values that identify the specific record
488
- * @param dataSource Database connection
489
- * @param userPayload Information about the authenticated user
490
- * @param pubSub Publisher/subscriber for events
491
- * @returns Result of the Skip interaction
492
- */
493
- @Query(() => AskSkipResultType)
494
- async ExecuteAskSkipRecordChat(
495
- @Arg('UserQuestion', () => String) UserQuestion: string,
496
- @Arg('ConversationId', () => String) ConversationId: string,
497
- @Arg('EntityName', () => String) EntityName: string,
498
- @Arg('CompositeKey', () => CompositeKeyInputType) compositeKey: CompositeKeyInputType,
499
- @Ctx() { dataSource, userPayload, providers }: AppContext,
500
- @PubSub() pubSub: PubSubEngine
501
- ) {
502
- // In this function we're simply going to call the Skip API and pass along the message from the user
503
-
504
- // first, get the user from the cache
505
- const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
506
- if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
507
-
508
- // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
509
- let messages: SkipMessage[] = [];
510
- if (ConversationId && ConversationId.length > 0) {
511
- messages = await this.LoadConversationDetailsIntoSkipMessages(
512
- dataSource,
513
- ConversationId,
514
- AskSkipResolver._maxHistoricalMessages
515
- );
516
- }
517
-
518
- const md = GetReadWriteProvider(providers);
519
- const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
520
- dataSource,
521
- ConversationId,
522
- UserQuestion,
523
- user,
524
- userPayload,
525
- md as unknown as Metadata,
526
- null
527
- );
528
-
529
- // if we have a new conversation, update the data context to have an item for this record
530
- if (!ConversationId || ConversationId.length === 0) {
531
- const dci = await md.GetEntityObject<DataContextItemEntity>('Data Context Items', user);
532
- dci.DataContextID = dataContext.ID;
533
- dci.Type = 'single_record';
534
- dci.EntityID = md.Entities.find((e) => e.Name === EntityName)?.ID;
535
- const ck = new CompositeKey();
536
- ck.KeyValuePairs = compositeKey.KeyValuePairs;
537
- dci.RecordID = ck.Values();
538
-
539
- let dciSaveResult: boolean = await dci.Save();
540
- if (!dciSaveResult) {
541
- LogError(`Error saving DataContextItemEntity for record chat: ${EntityName} ${ck.Values()}`, undefined, dci.LatestResult);
542
- }
543
-
544
- await dataContext.Load(dataContext.ID, dataSource, false, true, 10, user); // load again because we added a new data context item
545
- await dataContext.SaveItems(user, true); // persist the data becuase the deep loading above with related data is expensive
546
-
547
- // also, in the situation for a new convo, we need to update the Conversation ID to have a LinkedEntity and LinkedRecord
548
- convoEntity.LinkedEntityID = dci.EntityID;
549
- convoEntity.LinkedRecordID = ck.Values();
550
- convoEntity.DataContextID = dataContext.ID;
551
- const convoEntitySaveResult: boolean = await convoEntity.Save();
552
- if (!convoEntitySaveResult) {
553
- LogError(`Error saving ConversationEntity for record chat: ${EntityName} ${ck.Values()}`, undefined, convoEntity.LatestResult);
554
- }
555
- }
556
-
557
- const input = await this.buildSkipChatAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false, false, false, user, dataSource, false, false);
558
- messages.push({
559
- content: UserQuestion,
560
- role: 'user',
561
- conversationDetailID: convoDetailEntity.ID,
562
- });
563
-
564
- return this.handleSimpleSkipChatPostRequest(input, convoEntity, convoDetailEntity, true, user, userPayload);
565
- }
566
-
567
- // /**
568
- // * Executes a Skip learning cycle
569
- // * Learning cycles allow Skip to analyze conversations and improve its knowledge and capabilities
570
- // *
571
- // * @param dataSource Database connection
572
- // * @param userPayload Information about the authenticated user
573
- // * @param ForceEntityRefresh Whether to force a refresh of entity metadata
574
- // * @returns Result of the learning cycle execution
575
- // */
576
- // @Mutation(() => AskSkipResultType)
577
- // async ExecuteAskSkipLearningCycle(
578
- // @Ctx() { dataSource, userPayload, providers }: AppContext,
579
- // @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
580
- // ) {
581
- // const skipConfigInfo = configInfo.askSkip;
582
- // // First check if learning cycles are enabled in configuration
583
- // if (!skipConfigInfo.learningCycleEnabled) {
584
- // return {
585
- // success: false,
586
- // error: 'Learning cycles are not enabled in configuration',
587
- // elapsedTime: 0,
588
- // noteChanges: [],
589
- // queryChanges: [],
590
- // requestChanges: []
591
- // };
592
- // }
593
-
594
- // // Check if we have a valid endpoint when cycles are enabled
595
- // const hasLearningEndpoint = (skipConfigInfo.url && skipConfigInfo.url.trim().length > 0) ||
596
- // (skipConfigInfo.learningCycleURL && skipConfigInfo.learningCycleURL.trim().length > 0);
597
- // if (!hasLearningEndpoint) {
598
- // return {
599
- // success: false,
600
- // error: 'Learning cycle API endpoint is not configured',
601
- // elapsedTime: 0,
602
- // noteChanges: [],
603
- // queryChanges: [],
604
- // requestChanges: []
605
- // };
606
- // }
607
-
608
- // const startTime = new Date();
609
- // // First, get the user from the cache
610
- // const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
611
- // if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
612
-
613
- // // if already configured this does nothing, just makes sure we're configured
614
- // await AIEngine.Instance.Config(false, user);
615
-
616
- // // Check if this organization is already running a learning cycle using their organization ID
617
- // const organizationId = skipConfigInfo.orgID;
618
- // const scheduler = LearningCycleScheduler.Instance;
619
- // const runningStatus = scheduler.isOrganizationRunningCycle(organizationId);
620
-
621
- // if (runningStatus.isRunning) {
622
- // LogStatus(`Learning cycle already in progress for organization ${organizationId}, started at ${runningStatus.startTime.toISOString()}`);
623
- // return {
624
- // success: false,
625
- // error: `Learning cycle already in progress for this organization (started ${Math.round(runningStatus.runningForMinutes)} minutes ago)`,
626
- // elapsedTime: 0,
627
- // noteChanges: [],
628
- // queryChanges: [],
629
- // requestChanges: []
630
- // };
631
- // }
632
-
633
- // // Get the Skip agent ID
634
- // const md = GetReadWriteProvider(providers);
635
- // const skipAgent = AIEngine.Instance.GetAgentByName('Skip');
636
- // if (!skipAgent) {
637
- // throw new Error("Skip agent not found in AIEngine");
638
- // }
639
-
640
- // const agentID = skipAgent.ID;
641
-
642
- // // Get last complete learning cycle start date for this agent
643
- // const lastCompleteLearningCycleDate = await this.GetLastCompleteLearningCycleDate(agentID, user);
644
-
645
- // // Create a new learning cycle record for this run
646
- // const learningCycleEntity = await md.GetEntityObject<AIAgentLearningCycleEntity>('AI Agent Learning Cycles', user);
647
- // learningCycleEntity.NewRecord();
648
- // learningCycleEntity.AgentID = skipAgent.ID;
649
- // learningCycleEntity.Status = 'In-Progress';
650
- // learningCycleEntity.StartedAt = startTime;
651
-
652
- // if (!(await learningCycleEntity.Save())) {
653
- // throw new Error(`Failed to create learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
654
- // }
655
-
656
- // const learningCycleId = learningCycleEntity.ID;
657
- // LogStatus(`Created new learning cycle with ID: ${learningCycleId}`);
658
-
659
- // // Register this organization as running a learning cycle
660
- // scheduler.registerRunningCycle(organizationId, learningCycleId);
661
-
662
- // try {
663
- // // Build the request to Skip learning API
664
- // LogStatus(`Building Skip Learning API request`);
665
- // const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, false, dataSource, user, ForceEntityRefresh || false);
666
- // if (input.newConversations.length === 0) {
667
- // // no new conversations to process
668
- // LogStatus(` Skip Learning Cycles: No new conversations to process for learning cycle`);
669
- // learningCycleEntity.Status = 'Complete';
670
- // learningCycleEntity.AgentSummary = 'No new conversations to process, learning cycle skipped, but recorded for audit purposes.';
671
- // learningCycleEntity.EndedAt = new Date();
672
- // if (!(await learningCycleEntity.Save())) {
673
- // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
674
- // }
675
- // const result: SkipAPILearningCycleResponse = {
676
- // success: true,
677
- // learningCycleSkipped: true,
678
- // elapsedTime: 0,
679
- // noteChanges: [],
680
- // queryChanges: [],
681
- // requestChanges: [],
682
- // }
683
- // return result;
684
- // }
685
- // else {
686
- // // Make the API request
687
- // const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID, userPayload);
688
-
689
- // // Update learning cycle to completed
690
- // const endTime = new Date();
691
- // const elapsedTimeMs = endTime.getTime() - startTime.getTime();
692
-
693
- // LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
694
-
695
- // learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
696
- // learningCycleEntity.EndedAt = endTime;
697
-
698
- // if (!(await learningCycleEntity.Save())) {
699
- // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
700
- // }
701
-
702
- // return response;
703
- // }
704
- // }
705
- // catch (error) {
706
- // // Make sure to update the learning cycle record as failed
707
- // learningCycleEntity.Status = 'Failed';
708
- // learningCycleEntity.EndedAt = new Date();
709
-
710
- // try {
711
- // await learningCycleEntity.Save();
712
- // }
713
- // catch (saveError) {
714
- // LogError(`Failed to update learning cycle record after error: ${saveError}`);
715
- // }
716
-
717
- // // Re-throw the original error
718
- // throw error;
719
- // }
720
- // finally {
721
- // // Unregister the cycle/organizationId safely
722
- // try {
723
- // scheduler.unregisterRunningCycle(organizationId);
724
- // }
725
- // catch (error) {
726
- // LogError(`Failed to unregister organization ${organizationId} from running cycles: ${error}`);
727
- // }
728
- // }
729
- // }
730
-
731
- // /**
732
- // * Handles the HTTP POST request to the Skip learning cycle API
733
- // * Sends the learning cycle request and processes the response
734
- // *
735
- // * @param input The learning cycle request payload
736
- // * @param user User context for the request
737
- // * @param learningCycleId ID of the current learning cycle
738
- // * @param agentID ID of the Skip agent
739
- // * @returns Response from the Skip learning cycle API
740
- // */
741
- // protected async handleSimpleSkipLearningPostRequest(
742
- // input: SkipAPILearningCycleRequest,
743
- // user: UserInfo,
744
- // learningCycleId: string,
745
- // agentID: string,
746
- // userPayload: UserPayload
747
- // ): Promise<SkipAPILearningCycleResponse> {
748
- // const skipConfigInfo = configInfo.askSkip;
749
- // const learningURL = skipConfigInfo.url ? `${skipConfigInfo.url}${SKIP_API_ENDPOINTS.LEARNING}` : skipConfigInfo.learningCycleURL;
750
- // LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${learningURL}`);
751
-
752
- // const response = await sendPostRequest(learningURL, input, true, this.buildSkipPostHeaders());
753
-
754
- // if (response && response.length > 0) {
755
- // // the last object in the response array is the final response from the Skip API
756
- // const apiResponse = <SkipAPILearningCycleResponse>response[response.length - 1].value;
757
- // LogStatus(` Skip API response: ${apiResponse.success}`);
758
-
759
- // // Process any note changes, if any
760
- // if (apiResponse.noteChanges && apiResponse.noteChanges.length > 0) {
761
- // await this.processLearningCycleNoteChanges(apiResponse.noteChanges, agentID, user, userPayload);
762
- // }
763
-
764
- // // Not yet implemented
765
-
766
- // // // Process any query changes, if any
767
- // // if (apiResponse.queryChanges && apiResponse.queryChanges.length > 0) {
768
- // // await this.processLearningCycleQueryChanges(apiResponse.queryChanges, user);
769
- // // }
770
-
771
- // // // Process any request changes, if any
772
- // // if (apiResponse.requestChanges && apiResponse.requestChanges.length > 0) {
773
- // // await this.processLearningCycleRequestChanges(apiResponse.requestChanges, user);
774
- // // }
775
-
776
- // return apiResponse;
777
- // } else {
778
- // return {
779
- // success: false,
780
- // error: 'Error',
781
- // elapsedTime: 0,
782
- // noteChanges: [],
783
- // queryChanges: [],
784
- // requestChanges: [],
785
- // };
786
-
787
- // }
788
- // }
789
-
790
- /**
791
- * Handles the HTTP POST request to the Skip chat API
792
- * Sends the chat request and processes the response
793
- *
794
- * @param input The chat request payload
795
- * @param convoEntity The conversation entity object
796
- * @param convoDetailEntity The conversation detail entity object
797
- * @param createAIMessageConversationDetail Whether to create a conversation detail for the AI response
798
- * @param user User context for the request
799
- * @returns Result of the Skip interaction
800
- */
801
- protected async handleSimpleSkipChatPostRequest(
802
- input: SkipAPIRequest,
803
- convoEntity: ConversationEntity = null,
804
- convoDetailEntity: ConversationDetailEntity = null,
805
- createAIMessageConversationDetail: boolean = false,
806
- user: UserInfo = null,
807
- userPayload: UserPayload = null
808
- ): Promise<AskSkipResultType> {
809
- const skipConfigInfo = configInfo.askSkip;
810
- const chatURL = skipConfigInfo.url ? `${skipConfigInfo.url}${SKIP_API_ENDPOINTS.CHAT}` : skipConfigInfo.chatURL;
811
- LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${chatURL}`);
812
-
813
- try {
814
- const response = await sendPostRequest(chatURL, input, true, this.buildSkipPostHeaders());
815
-
816
- if (response && response.length > 0) {
817
- // the last object in the response array is the final response from the Skip API
818
- const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
819
- const AIMessageConversationDetailID = createAIMessageConversationDetail && convoEntity
820
- ? await this.CreateAIMessageConversationDetail(apiResponse, convoEntity.ID, user, userPayload)
821
- : '';
822
- // const apiResponse = <SkipAPIResponse>response.data;
823
- LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
824
- return {
825
- Success: true,
826
- Status: 'OK',
827
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
828
- ConversationId: convoEntity ? convoEntity.ID : '',
829
- UserMessageConversationDetailId: convoDetailEntity ? convoDetailEntity.ID : '',
830
- AIMessageConversationDetailId: AIMessageConversationDetailID,
831
- Result: JSON.stringify(apiResponse),
832
- };
833
- } else {
834
- // Set conversation status to Available on failure so user can try again (if conversation exists)
835
- if (convoEntity) {
836
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
837
- }
838
-
839
- return {
840
- Success: false,
841
- Status: 'Error',
842
- Result: `Request failed`,
843
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
844
- ConversationId: convoEntity ? convoEntity.ID : '',
845
- UserMessageConversationDetailId: convoDetailEntity ? convoDetailEntity.ID : '',
846
- AIMessageConversationDetailId: '',
847
- };
848
- }
849
- } catch (error) {
850
- // Set conversation status to Available on error so user can try again (if conversation exists)
851
- if (convoEntity) {
852
- await this.setConversationStatus(convoEntity, 'Available', userPayload);
853
- }
854
-
855
- // Log the error for debugging
856
- LogError(`Error in handleSimpleSkipChatPostRequest: ${error}`);
857
-
858
- // Re-throw the error to propagate it up the stack
859
- throw error;
860
- }
861
- }
862
-
863
- // /**
864
- // * Processes note changes received from the Skip API learning cycle
865
- // * Applies changes to agent notes based on the learning cycle response
866
- // *
867
- // * @param noteChanges Changes to agent notes
868
- // * @param agentID ID of the Skip agent
869
- // * @param user User context for the request
870
- // * @returns Promise that resolves when processing is complete
871
- // */
872
- // protected async processLearningCycleNoteChanges(
873
- // noteChanges: SkipLearningCycleNoteChange[],
874
- // agentID: string,
875
- // user: UserInfo,
876
- // userPayload: UserPayload
877
- // ): Promise<void> {
878
- // const md = new Metadata();
879
-
880
- // // Filter out any operations on "Human" notes
881
- // const validNoteChanges = noteChanges.filter(change => {
882
- // // Check if the note is of type "Human"
883
- // if (change.note.agentNoteType === "Human") {
884
- // LogStatus(`WARNING: Ignoring ${change.changeType} operation on Human note with ID ${change.note.id}. Human notes cannot be modified by the
885
- // learning cycle.`);
886
- // return false;
887
- // }
888
- // return true;
889
- // });
890
-
891
- // // Process all valid note changes in parallel
892
- // await Promise.all(validNoteChanges.map(async (change) => {
893
- // try {
894
- // if (change.changeType === 'add' || change.changeType === 'update') {
895
- // await this.processAddOrUpdateSkipNote(change, agentID, user, userPayload);
896
- // } else if (change.changeType === 'delete') {
897
- // await this.processDeleteSkipNote(change, user, userPayload);
898
- // }
899
- // } catch (e) {
900
- // LogError(`Error processing note change: ${e}`);
901
- // }
902
- // }));
903
- // }
904
-
905
- // /**
906
- // * Processes an add or update operation for a Skip agent note
907
- // * Creates a new note or updates an existing one based on the change type
908
- // *
909
- // * @param change The note change information
910
- // * @param agentID ID of the Skip agent
911
- // * @param user User context for the operation
912
- // * @returns Whether the operation was successful
913
- // */
914
- // protected async processAddOrUpdateSkipNote(change: SkipLearningCycleNoteChange, agentID: string, user: UserInfo, userPayload: UserPayload): Promise<boolean> {
915
- // try {
916
- // // Get the note entity object
917
- // const md = new Metadata();
918
- // const noteEntity = await md.GetEntityObject<AIAgentNoteEntity>('AI Agent Notes', user);
919
-
920
- // if (change.changeType === 'update') {
921
- // // Load existing note
922
- // const loadResult = await noteEntity.Load(change.note.id);
923
- // if (!loadResult) {
924
- // LogError(`Could not load note with ID ${change.note.id}`);
925
- // return false;
926
- // }
927
- // } else {
928
- // // Create a new note
929
- // noteEntity.NewRecord();
930
- // noteEntity.AgentID = agentID;
931
- // }
932
- // noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI'); // always set to AI
933
- // noteEntity.Note = change.note.note;
934
- // noteEntity.Type = change.note.type;
935
-
936
- // if (change.note.type === 'User') {
937
- // noteEntity.UserID = change.note.userId;
938
- // }
939
-
940
- // // Save the note
941
- // if (!(await noteEntity.Save())) {
942
- // LogError(`Error saving AI Agent Note: ${noteEntity.LatestResult.CompleteMessage}`);
943
- // return false;
944
- // }
945
-
946
- // return true;
947
- // } catch (e) {
948
- // LogError(`Error processing note change: ${e}`);
949
- // return false;
950
- // }
951
- // }
952
-
953
- // /**
954
- // * Processes a delete operation for a Skip agent note
955
- // * Removes the specified note from the database
956
- // *
957
- // * @param change The note change information
958
- // * @param user User context for the operation
959
- // * @returns Whether the deletion was successful
960
- // */
961
- // protected async processDeleteSkipNote(change: SkipLearningCycleNoteChange, user: UserInfo, userPayload: UserPayload): Promise<boolean> {
962
- // // Get the note entity object
963
- // const md = new Metadata();
964
- // const noteEntity = await md.GetEntityObject<AIAgentNoteEntity>('AI Agent Notes', user);
965
-
966
- // // Load the note first
967
- // const loadResult = await noteEntity.Load(change.note.id);
968
-
969
- // if (!loadResult) {
970
- // LogError(`Could not load note with ID ${change.note.id} for deletion`);
971
- // return false;
972
- // }
973
-
974
- // // Double-check if the loaded note is of type "Human"
975
- // if (change.note.agentNoteType === "Human") {
976
- // LogStatus(`WARNING: Ignoring delete operation on Human note with ID ${change.note.id}. Human notes cannot be deleted by the learning
977
- // cycle.`);
978
- // return false;
979
- // }
980
-
981
- // // Proceed with deletion
982
- // if (!(await noteEntity.Delete())) {
983
- // LogError(`Error deleting AI Agent Note: ${noteEntity.LatestResult.CompleteMessage}`);
984
- // return false;
985
- // }
986
-
987
- // return true;
988
- // }
989
-
990
- /**
991
- * Creates a conversation detail entry for an AI message
992
- * Stores the AI response in the conversation history
993
- *
994
- * @param apiResponse The response from the Skip API
995
- * @param conversationID ID of the conversation
996
- * @param user User context for the operation
997
- * @returns ID of the created conversation detail, or empty string if creation failed
998
- */
999
- protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: string, user: UserInfo, userPayload: UserPayload): Promise<string> {
1000
- const md = new Metadata();
1001
- const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
1002
- convoDetailEntityAI.NewRecord();
1003
- convoDetailEntityAI.HiddenToUser = false;
1004
- convoDetailEntityAI.ConversationID = conversationID;
1005
- const systemMessages = apiResponse.messages.filter((m) => m.role === 'system');
1006
- const lastSystemMessage = systemMessages[systemMessages.length - 1];
1007
- convoDetailEntityAI.Message = lastSystemMessage?.content;
1008
- convoDetailEntityAI.Role = 'AI';
1009
-
1010
- if (await convoDetailEntityAI.Save()) {
1011
- return convoDetailEntityAI.ID;
1012
- } else {
1013
- LogError(
1014
- `Error saving conversation detail entity for AI message: ${lastSystemMessage?.content}`,
1015
- undefined,
1016
- convoDetailEntityAI.LatestResult
1017
- );
1018
- return '';
1019
- }
1020
- }
1021
-
1022
- /**
1023
- * Builds the base Skip API request with common fields and data
1024
- * Creates the foundation for both chat and learning cycle requests
1025
- *
1026
- * @param contextUser The user making the request
1027
- * @param dataSource The data source to use
1028
- * @param includeEntities Whether to include entities in the request
1029
- * @param includeQueries Whether to include queries in the request
1030
- * @param includeNotes Whether to include agent notes in the request
1031
- * @param includeRequests Whether to include agent requests in the request
1032
- * @param forceEntitiesRefresh Whether to force refresh of entities
1033
- * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
1034
- * @param additionalTokenInfo Additional info to include in the access token
1035
- * @returns Base request data that can be used by specific request builders
1036
- */
1037
- protected async buildBaseSkipRequest(
1038
- contextUser: UserInfo,
1039
- dataSource: mssql.ConnectionPool,
1040
- includeEntities: boolean,
1041
- includeQueries: boolean,
1042
- includeNotes: boolean,
1043
- filterUserNotesToContextUser: boolean,
1044
- includeRequests: boolean,
1045
- forceEntitiesRefresh: boolean = false,
1046
- includeCallBackKeyAndAccessToken: boolean = false,
1047
- additionalTokenInfo: any = {}
1048
- ): Promise<BaseSkipRequest> {
1049
- const skipConfigInfo = configInfo.askSkip;
1050
- const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
1051
- const queries = includeQueries ? this.BuildSkipQueries() : [];
1052
- //const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser, filterUserNotesToContextUser) : {notes: [], noteTypes: []};
1053
- const requests = includeRequests ? await this.BuildSkipRequests(contextUser) : [];
1054
-
1055
- // Setup access token if needed
1056
- let accessToken: GetDataAccessToken;
1057
- if (includeCallBackKeyAndAccessToken) {
1058
- const tokenInfo = {
1059
- type: 'skip_api_request',
1060
- userEmail: contextUser.Email,
1061
- userName: contextUser.Name,
1062
- userID: contextUser.ID,
1063
- ...additionalTokenInfo
1064
- };
1065
-
1066
- accessToken = registerAccessToken(
1067
- undefined,
1068
- 1000 * 60 * 10 /*10 minutes*/,
1069
- tokenInfo
1070
- );
1071
- }
1072
-
1073
- return {
1074
- entities,
1075
- queries,
1076
- notes: undefined,
1077
- noteTypes: undefined,
1078
- userEmail: contextUser.Email,
1079
- requests,
1080
- accessToken,
1081
- organizationID: skipConfigInfo.orgID,
1082
- organizationInfo: configInfo?.askSkip?.organizationInfo,
1083
- apiKeys: this.buildSkipAPIKeys(),
1084
- // Favors public URL for conciseness or when behind a proxy for local development
1085
- // otherwise uses base URL and GraphQL port/path from configuration
1086
- callingServerURL: accessToken ? (publicUrl || `${baseUrl}:${graphqlPort}${graphqlRootPath}`) : undefined,
1087
- callingServerAPIKey: accessToken ? callbackAPIKey : undefined,
1088
- callingServerAccessToken: accessToken ? accessToken.Token : undefined
1089
- };
1090
- }
1091
-
1092
- /**
1093
- * Builds the learning API request for Skip
1094
- * Creates a request specific to the learning cycle operation
1095
- *
1096
- * @param learningCycleId ID of the current learning cycle
1097
- * @param lastLearningCycleDate Date of the last completed learning cycle
1098
- * @param includeEntities Whether to include entities in the request
1099
- * @param includeQueries Whether to include queries in the request
1100
- * @param includeNotes Whether to include agent notes in the request
1101
- * @param includeRequests Whether to include agent requests in the request
1102
- * @param dataSource Database connection
1103
- * @param contextUser User context for the request
1104
- * @param forceEntitiesRefresh Whether to force refresh of entities
1105
- * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
1106
- * @returns Complete learning cycle request object
1107
- */
1108
- protected async buildSkipLearningAPIRequest(
1109
- learningCycleId: string,
1110
- lastLearningCycleDate: Date,
1111
- includeEntities: boolean,
1112
- includeQueries: boolean,
1113
- includeNotes: boolean,
1114
- includeRequests: boolean,
1115
- dataSource: mssql.ConnectionPool,
1116
- contextUser: UserInfo,
1117
- forceEntitiesRefresh: boolean = false,
1118
- includeCallBackKeyAndAccessToken: boolean = false
1119
- ) {
1120
- // Build base Skip request data
1121
- const baseRequest = await this.buildBaseSkipRequest(
1122
- contextUser,
1123
- dataSource,
1124
- includeEntities,
1125
- includeQueries,
1126
- includeNotes,
1127
- false,
1128
- includeRequests,
1129
- forceEntitiesRefresh,
1130
- includeCallBackKeyAndAccessToken
1131
- );
1132
-
1133
- // Get data specific to learning cycle
1134
- const newConversations = await this.BuildSkipLearningCycleNewConversations(lastLearningCycleDate, dataSource, contextUser);
1135
-
1136
- // Create the learning-specific request object
1137
- const input: SkipAPILearningCycleRequest = {
1138
- organizationId: baseRequest.organizationID,
1139
- organizationInfo: baseRequest.organizationInfo,
1140
- learningCycleId,
1141
- lastLearningCycleDate,
1142
- newConversations,
1143
- entities: baseRequest.entities,
1144
- queries: baseRequest.queries,
1145
- notes: baseRequest.notes,
1146
- noteTypes: baseRequest.noteTypes,
1147
- requests: baseRequest.requests,
1148
- apiKeys: baseRequest.apiKeys
1149
- };
1150
-
1151
- return input;
1152
- }
1153
-
1154
- /**
1155
- * Loads the conversations that have been updated or added since the last learning cycle
1156
- * These are used to train Skip and improve its understanding
1157
- *
1158
- * @param lastLearningCycleDate The date of the last learning cycle
1159
- * @param dataSource Database connection
1160
- * @param contextUser User context for the request
1161
- * @returns Array of conversations that are new or have been updated since the last cycle
1162
- */
1163
- protected async BuildSkipLearningCycleNewConversations(
1164
- lastLearningCycleDate: Date,
1165
- dataSource: mssql.ConnectionPool,
1166
- contextUser: UserInfo
1167
- ): Promise<SkipConversation[]> {
1168
- try {
1169
- const rv = new RunView();
1170
-
1171
- // Get all conversations with a conversation detail that has been updated (modified or added) since the last learning cycle
1172
- const conversationsSinceLastLearningCycle = await rv.RunView<ConversationEntity>({
1173
- EntityName: 'Conversations',
1174
- ExtraFilter: `ID IN (SELECT ConversationID FROM __mj.vwConversationDetails WHERE __mj_UpdatedAt >= '${lastLearningCycleDate.toISOString()}')`,
1175
- ResultType: 'entity_object',
1176
- }, contextUser);
1177
-
1178
- if (!conversationsSinceLastLearningCycle.Success || conversationsSinceLastLearningCycle.Results.length === 0) {
1179
- return [];
1180
- }
1181
-
1182
- // Now we map the conversations to SkipConversations and return
1183
- return await Promise.all(conversationsSinceLastLearningCycle.Results.map(async (c) => {
1184
- return {
1185
- id: c.ID,
1186
- name: c.Name,
1187
- userId: c.UserID,
1188
- user: c.User,
1189
- description: c.Description,
1190
- messages: await this.LoadConversationDetailsIntoSkipMessages(dataSource, c.ID),
1191
- createdAt: c.__mj_CreatedAt,
1192
- updatedAt: c.__mj_UpdatedAt
1193
- };
1194
- }));
1195
- }
1196
- catch (e) {
1197
- LogError(`Error loading conversations since last learning cycle: ${e}`);
1198
- return [];
1199
- }
1200
- }
1201
-
1202
- /**
1203
- * Builds an array of agent requests
1204
- * These are requests that have been made to the AI agent
1205
- *
1206
- * @param contextUser User context for loading the requests
1207
- * @returns Array of agent request objects
1208
- */
1209
- protected async BuildSkipRequests(
1210
- contextUser: UserInfo
1211
- ): Promise<SkipAPIAgentRequest[]> {
1212
- try {
1213
- const md = new Metadata();
1214
- const requestEntity = await md.GetEntityObject<AIAgentRequestEntity>('AI Agent Requests', contextUser);
1215
- const allRequests = await requestEntity.GetAll();
1216
-
1217
- const requests = allRequests.map((r) => {
1218
- return {
1219
- id: r.ID,
1220
- agentId: r.AIAgentID,
1221
- agnet: r.AIAgent,
1222
- requestedAt: r.RequestedAt,
1223
- requestForUserId: r.RequestedForUserID,
1224
- requestForUser: r.RequestedForUser,
1225
- status: r.Status,
1226
- request: r.Request,
1227
- response: r.Response,
1228
- responseByUserId: r.ResponseByUserID,
1229
- responseByUser: r.ResponseByUser,
1230
- respondedAt: r.RespondedAt,
1231
- comments: r.Comments,
1232
- createdAt: r.__mj_CreatedAt,
1233
- updatedAt: r.__mj_UpdatedAt,
1234
- };
1235
- });
1236
- return requests;
1237
-
1238
- } catch (e) {
1239
- LogError(`Error loading requests: ${e}`);
1240
- return [];
1241
- }
1242
- }
1243
-
1244
- /**
1245
- * Gets the date of the last complete learning cycle for the Skip agent
1246
- * Used to determine which data to include in the next learning cycle
1247
- *
1248
- * @param agentID ID of the Skip agent
1249
- * @param user User context for the query
1250
- * @returns Date of the last complete learning cycle, or epoch if none exists
1251
- */
1252
- protected async GetLastCompleteLearningCycleDate(agentID: string, user: UserInfo): Promise<Date> {
1253
- const md = new Metadata();
1254
- const rv = new RunView();
1255
-
1256
- const lastLearningCycleRV = await rv.RunView<AIAgentLearningCycleEntity>({
1257
- EntityName: 'AI Agent Learning Cycles',
1258
- ExtraFilter: `AgentID = '${agentID}' AND Status = 'Complete'`,
1259
- ResultType: 'entity_object',
1260
- OrderBy: 'StartedAt DESC',
1261
- MaxRows: 1,
1262
- }, user);
1263
-
1264
- const lastLearningCycle = lastLearningCycleRV.Results[0];
1265
-
1266
- if (lastLearningCycle) {
1267
- return lastLearningCycle.StartedAt;
1268
- }
1269
- else {
1270
- // if no lerarning cycle found, return the epoch date
1271
- return new Date(0);
1272
- }
1273
- }
1274
-
1275
- /**
1276
- * Builds the chat API request for Skip
1277
- * Creates a request specific to a chat interaction
1278
- *
1279
- * @param messages Array of messages in the conversation
1280
- * @param conversationId ID of the conversation
1281
- * @param dataContext Data context associated with the conversation
1282
- * @param requestPhase The phase of the request (initial, clarifying, etc.)
1283
- * @param includeEntities Whether to include entities in the request
1284
- * @param includeQueries Whether to include queries in the request
1285
- * @param includeNotes Whether to include agent notes in the request
1286
- * @param includeRequests Whether to include agent requests in the request
1287
- * @param contextUser User context for the request
1288
- * @param dataSource Database connection
1289
- * @param forceEntitiesRefresh Whether to force refresh of entities
1290
- * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
1291
- * @returns Complete chat request object
1292
- */
1293
- protected async buildSkipChatAPIRequest(
1294
- messages: SkipMessage[],
1295
- conversationId: string,
1296
- dataContext: DataContext,
1297
- requestPhase: SkipRequestPhase,
1298
- includeEntities: boolean,
1299
- includeQueries: boolean,
1300
- includeNotes: boolean,
1301
- includeRequests: boolean,
1302
- contextUser: UserInfo,
1303
- dataSource: mssql.ConnectionPool,
1304
- forceEntitiesRefresh: boolean = false,
1305
- includeCallBackKeyAndAccessToken: boolean = false
1306
- ): Promise<SkipAPIRequest> {
1307
- // Additional token info specific to chat requests
1308
- const additionalTokenInfo = {
1309
- conversationId,
1310
- requestPhase,
1311
- };
1312
-
1313
- // Get base request data
1314
- const baseRequest = await this.buildBaseSkipRequest(
1315
- contextUser,
1316
- dataSource,
1317
- includeEntities,
1318
- includeQueries,
1319
- includeNotes,
1320
- true,
1321
- includeRequests,
1322
- forceEntitiesRefresh,
1323
- includeCallBackKeyAndAccessToken,
1324
- additionalTokenInfo
1325
- );
1326
-
1327
- const artifacts: SkipAPIArtifact[] = await this.buildSkipAPIArtifacts(contextUser, dataSource, conversationId);
1328
-
1329
- // Create the chat-specific request object
1330
- const input: SkipAPIRequest = {
1331
- ...baseRequest,
1332
- messages,
1333
- conversationID: conversationId.toString(),
1334
- 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
1335
- requestPhase,
1336
- artifacts: artifacts
1337
- };
1338
-
1339
- return input;
1340
- }
1341
-
1342
- /**
1343
- * Builds up an array of artifacts associated with a conversation
1344
- * Artifacts are content or documents generated during conversations
1345
- *
1346
- * @param contextUser User context for the query
1347
- * @param dataSource Database connection
1348
- * @param conversationId ID of the conversation
1349
- * @returns Array of artifacts associated with the conversation
1350
- */
1351
- protected async buildSkipAPIArtifacts(contextUser: UserInfo, dataSource: mssql.ConnectionPool, conversationId: string): Promise<SkipAPIArtifact[]> {
1352
- const md = new Metadata();
1353
- const ei = md.EntityByName('MJ: Conversation Artifacts');
1354
- const rv = new RunView();
1355
- const results = await rv.RunViews([
1356
- {
1357
- EntityName: "MJ: Conversation Artifacts",
1358
- ExtraFilter: `ConversationID='${conversationId}'`, // get artifacts linked to this convo
1359
- OrderBy: "__mj_CreatedAt"
1360
- },
1361
- {
1362
- EntityName: "MJ: Artifact Types", // get all artifact types
1363
- OrderBy: "Name"
1364
- },
1365
- {
1366
- EntityName: "MJ: Conversation Artifact Versions",
1367
- ExtraFilter: `ConversationArtifactID IN (SELECT ID FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ConversationID='${conversationId}')`,
1368
- OrderBy: 'ConversationArtifactID, __mj_CreatedAt'
1369
- }
1370
- ], contextUser);
1371
- if (results && results.length > 0 && results.every((r) => r.Success)) {
1372
- const types: SkipAPIArtifactType[] = results[1].Results.map((a: ArtifactTypeEntity) => {
1373
- const retVal: SkipAPIArtifactType = {
1374
- id: a.ID,
1375
- name: a.Name,
1376
- description: a.Description,
1377
- contentType: a.ContentType,
1378
- enabled: a.IsEnabled,
1379
- createdAt: a.__mj_CreatedAt,
1380
- updatedAt: a.__mj_UpdatedAt
1381
- }
1382
- return retVal;
1383
- });
1384
- const allConvoArtifacts = results[0].Results.map((a: ConversationArtifactEntity) => {
1385
- const rawVersions: ConversationArtifactVersionEntity[] = results[2].Results as ConversationArtifactVersionEntity[];
1386
- const thisArtifactsVersions = rawVersions.filter(rv => rv.ConversationArtifactID === a.ID);
1387
- const versionsForThisArtifact: SkipAPIArtifactVersion[] = thisArtifactsVersions.map((v: ConversationArtifactVersionEntity) => {
1388
- const versionRetVal: SkipAPIArtifactVersion = {
1389
- id: v.ID,
1390
- artifactId: v.ConversationArtifactID,
1391
- version: v.Version,
1392
- configuration: v.Configuration,
1393
- content: v.Content,
1394
- comments: v.Comments,
1395
- createdAt: v.__mj_CreatedAt,
1396
- updatedAt: v.__mj_UpdatedAt
1397
- };
1398
- return versionRetVal;
1399
- });
1400
- const artifactRetVal: SkipAPIArtifact = {
1401
- id: a.ID,
1402
- name: a.Name,
1403
- description: a.Description,
1404
- comments: a.Comments,
1405
- sharingScope: a.SharingScope as 'None' |'SpecificUsers' |'Everyone' |'Public',
1406
- versions: versionsForThisArtifact,
1407
- conversationId: a.ConversationID,
1408
- artifactType: types.find((t => t.id === a.ArtifactTypeID)),
1409
- createdAt: a.__mj_CreatedAt,
1410
- updatedAt: a.__mj_UpdatedAt
1411
- };
1412
- return artifactRetVal;
1413
- });
1414
-
1415
- return allConvoArtifacts;
1416
- }
1417
- else {
1418
- return [];
1419
- }
1420
- }
1421
-
1422
-
1423
- /**
1424
- * Executes a script in the context of a data context
1425
- * Allows running code against data context objects
1426
- *
1427
- * @param dataSource Database connection
1428
- * @param userPayload Information about the authenticated user
1429
- * @param pubSub Publisher/subscriber for events
1430
- * @param DataContextId ID of the data context to run the script against
1431
- * @param ScriptText The script to execute
1432
- * @returns Result of the script execution
1433
- */
1434
- @Query(() => AskSkipResultType)
1435
- async ExecuteAskSkipRunScript(
1436
- @Ctx() { dataSource, userPayload }: AppContext,
1437
- @PubSub() pubSub: PubSubEngine,
1438
- @Arg('DataContextId', () => String) DataContextId: string,
1439
- @Arg('ScriptText', () => String) ScriptText: string
1440
- ) {
1441
- const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1442
- if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
1443
- const dataContext: DataContext = new DataContext();
1444
- await dataContext.Load(DataContextId, dataSource, true, false, 0, user);
1445
- const input = <SkipAPIRunScriptRequest>await this.buildSkipChatAPIRequest([], '', dataContext, 'run_existing_script', false, false, false, false, user, dataSource, false, false);
1446
- input.scriptText = ScriptText;
1447
- return this.handleSimpleSkipChatPostRequest(input, undefined, undefined, undefined, userPayload.userRecord, userPayload);
1448
- }
1449
-
1450
- /**
1451
- * Builds the array of API keys for various AI services
1452
- * These are used by Skip to call external AI services
1453
- *
1454
- * @returns Array of API keys for different vendor services
1455
- */
1456
- protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
1457
- return [
1458
- {
1459
- vendorDriverName: 'OpenAILLM',
1460
- apiKey: GetAIAPIKey('OpenAILLM'),
1461
- },
1462
- {
1463
- vendorDriverName: 'AnthropicLLM',
1464
- apiKey: GetAIAPIKey('AnthropicLLM'),
1465
- },
1466
- {
1467
- vendorDriverName: 'GeminiLLM',
1468
- apiKey: GetAIAPIKey('GeminiLLM'),
1469
- },
1470
- {
1471
- vendorDriverName: 'GroqLLM',
1472
- apiKey: GetAIAPIKey('GroqLLM'),
1473
- },
1474
- {
1475
- vendorDriverName: 'MistralLLM',
1476
- apiKey: GetAIAPIKey('MistralLLM'),
1477
- },
1478
- {
1479
- vendorDriverName: 'CerebrasLLM',
1480
- apiKey: GetAIAPIKey('CerebrasLLM'),
1481
- },
1482
- ];
1483
- }
1484
-
1485
- /**
1486
- * Re-attaches the current session to receive status updates for a processing conversation
1487
- * This is needed after page reloads to resume receiving push notifications
1488
- */
1489
- @Query(() => ReattachConversationResponse)
1490
- async ReattachToProcessingConversation(
1491
- @Arg('ConversationId', () => String) ConversationId: string,
1492
- @Ctx() { userPayload, providers }: AppContext,
1493
- @PubSub() pubSub: PubSubEngine
1494
- ): Promise<ReattachConversationResponse | null> {
1495
- try {
1496
- const md = GetReadWriteProvider(providers);
1497
- const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1498
- if (!user) {
1499
- LogError(`User ${userPayload.email} not found in UserCache`);
1500
- return null;
1501
- }
1502
-
1503
- // Load the conversation
1504
- const convoEntity = await md.GetEntityObject<ConversationEntity>('Conversations', user);
1505
- const loadResult = await convoEntity.Load(ConversationId);
1506
-
1507
- if (!loadResult) {
1508
- LogError(`Could not load conversation ${ConversationId} for re-attachment`);
1509
- return null;
1510
- }
1511
-
1512
- // Check if the conversation belongs to this user
1513
- if (convoEntity.UserID !== user.ID) {
1514
- LogError(`Conversation ${ConversationId} does not belong to user ${user.Email}`);
1515
- return null;
1516
- }
1517
-
1518
- // If the conversation is processing, reattach the session to receive updates
1519
- if (convoEntity.Status === 'Processing') {
1520
- // Add this session to the active streams for this conversation
1521
- activeStreams.addSession(ConversationId, userPayload.sessionId);
1522
-
1523
- // Get the last known status message and start time from our cache
1524
- const lastStatusMessage = activeStreams.getStatus(ConversationId) || 'Processing...';
1525
- const startTime = activeStreams.getStartTime(ConversationId);
1526
-
1527
- // Check if the stream is still active
1528
- const isStreamActive = activeStreams.isActive(ConversationId);
1529
-
1530
- if (isStreamActive) {
1531
- // Send the last known status to the frontend
1532
- const statusMessage = {
1533
- type: 'AskSkip',
1534
- status: 'OK',
1535
- ResponsePhase: 'Processing',
1536
- conversationID: convoEntity.ID,
1537
- message: lastStatusMessage,
1538
- };
1539
-
1540
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1541
- pushStatusUpdates: {
1542
- message: JSON.stringify(statusMessage),
1543
- sessionId: userPayload.sessionId
1544
- }
1545
- });
1546
-
1547
- LogStatus(`Re-attached session ${userPayload.sessionId} to active stream for conversation ${ConversationId}, last status: ${lastStatusMessage}`);
1548
-
1549
- // Return the status and start time
1550
- return {
1551
- lastStatusMessage,
1552
- startTime: startTime || convoEntity.__mj_UpdatedAt
1553
- };
1554
- } else {
1555
- // Stream is inactive or doesn't exist, just send default status
1556
- const statusMessage = {
1557
- type: 'AskSkip',
1558
- status: 'OK',
1559
- ResponsePhase: 'Processing',
1560
- conversationID: convoEntity.ID,
1561
- message: 'Processing...',
1562
- };
1563
-
1564
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1565
- pushStatusUpdates: {
1566
- message: JSON.stringify(statusMessage),
1567
- sessionId: userPayload.sessionId
1568
- }
1569
- });
1570
-
1571
- LogStatus(`Re-attached session ${userPayload.sessionId} to conversation ${ConversationId}, but stream is inactive`);
1572
-
1573
- // Return default start time since stream is inactive
1574
- return {
1575
- lastStatusMessage: 'Processing...',
1576
- startTime: convoEntity.__mj_UpdatedAt
1577
- };
1578
- }
1579
- } else {
1580
- LogStatus(`Conversation ${ConversationId} is not processing (Status: ${convoEntity.Status})`);
1581
- return null;
1582
- }
1583
- } catch (error) {
1584
- LogError(`Error re-attaching to conversation: ${error}`);
1585
- return null;
1586
- }
1587
- }
1588
-
1589
- /**
1590
- * Executes an analysis query with Skip
1591
- * This is the primary entry point for general Skip conversations
1592
- *
1593
- * @param UserQuestion The question or message from the user
1594
- * @param ConversationId ID of an existing conversation, or empty for a new conversation
1595
- * @param dataSource Database connection
1596
- * @param userPayload Information about the authenticated user
1597
- * @param pubSub Publisher/subscriber for events
1598
- * @param DataContextId Optional ID of a data context to use
1599
- * @param ForceEntityRefresh Whether to force a refresh of entity metadata
1600
- * @returns Result of the Skip interaction
1601
- */
1602
- @Query(() => AskSkipResultType)
1603
- async ExecuteAskSkipAnalysisQuery(
1604
- @Arg('UserQuestion', () => String) UserQuestion: string,
1605
- @Arg('ConversationId', () => String) ConversationId: string,
1606
- @Ctx() { dataSource, userPayload, providers }: AppContext,
1607
- @PubSub() pubSub: PubSubEngine,
1608
- @Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
1609
- @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean,
1610
- @Arg('StartTime', () => Date, { nullable: true }) StartTime?: Date
1611
- ) {
1612
- const md = GetReadWriteProvider(providers);
1613
- const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1614
- if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
1615
-
1616
- // Record the start time if not provided
1617
- const requestStartTime = StartTime || new Date();
1618
-
1619
- const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
1620
- dataSource,
1621
- ConversationId,
1622
- UserQuestion,
1623
- user,
1624
- userPayload,
1625
- md as unknown as Metadata,
1626
- DataContextId
1627
- );
1628
-
1629
- // Set the conversation status to 'Processing' when a request is initiated
1630
- await this.setConversationStatus(convoEntity, 'Processing', userPayload, pubSub);
1631
-
1632
- // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
1633
- const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
1634
- dataSource,
1635
- convoEntity.ID,
1636
- AskSkipResolver._maxHistoricalMessages
1637
- );
1638
-
1639
- const conversationDetailCount = 1
1640
- const input = await this.buildSkipChatAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true, true, false, user, dataSource, ForceEntityRefresh === undefined ? false : ForceEntityRefresh, true);
1641
-
1642
- return this.HandleSkipChatRequest(
1643
- input,
1644
- UserQuestion,
1645
- user,
1646
- dataSource,
1647
- ConversationId,
1648
- userPayload,
1649
- pubSub,
1650
- md as unknown as Metadata,
1651
- convoEntity,
1652
- convoDetailEntity,
1653
- dataContext,
1654
- dataContextEntity,
1655
- conversationDetailCount,
1656
- requestStartTime
1657
- );
1658
- }
1659
-
1660
-
1661
- /**
1662
- * Recursively builds the category path for a query
1663
- * @param md
1664
- * @param categoryID
1665
- */
1666
- protected buildQueryCategoryPath(md: Metadata, categoryID: string): string {
1667
- const cat = md.QueryCategories.find((c) => c.ID === categoryID);
1668
- if (!cat) return '';
1669
- if (!cat.ParentID) return cat.Name; // base case, no parent, just return the name
1670
- const parentPath = this.buildQueryCategoryPath(md, cat.ParentID); // build the path recursively
1671
- return parentPath ? `${parentPath}/${cat.Name}` : cat.Name;
1672
- }
1673
-
1674
- /**
1675
- * Packages up queries from the metadata based on their status
1676
- * Used to provide Skip with information about available queries
1677
- *
1678
- * @param status The status of queries to include
1679
- * @returns Array of query information objects
1680
- */
1681
- protected BuildSkipQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
1682
- const md = new Metadata();
1683
- const approvedQueries = md.Queries.filter((q) => q.Status === status);
1684
- return approvedQueries.map((q) => ({
1685
- ID: q.ID,
1686
- Name: q.Name,
1687
- Description: q.Description,
1688
- Category: q.Category,
1689
- CategoryPath: this.buildQueryCategoryPath(md, q.CategoryID),
1690
- CategoryID: q.CategoryID,
1691
- SQL: q.SQL,
1692
- Status: q.Status,
1693
- QualityRank: q.QualityRank,
1694
- EmbeddingVector: q.EmbeddingVector,
1695
- EmbeddingModelID: q.EmbeddingModelID,
1696
- EmbeddingModelName: q.EmbeddingModel,
1697
- Fields: q.Fields.map((f) => ({
1698
- ID: f.ID,
1699
- QueryID: f.QueryID,
1700
- Name: f.Name,
1701
- Description: f.Description,
1702
- Sequence: f.Sequence,
1703
- SQLBaseType: f.SQLBaseType,
1704
- SQLFullType: f.SQLFullType,
1705
- SourceEntityID: f.SourceEntityID,
1706
- SourceEntity: f.SourceEntity,
1707
- SourceFieldName: f.SourceFieldName,
1708
- IsComputed: f.IsComputed,
1709
- ComputationDescription: f.ComputationDescription,
1710
- IsSummary: f.IsSummary,
1711
- SummaryDescription: f.SummaryDescription
1712
- })),
1713
- Parameters: q.Parameters.map((p) => ({
1714
- ID: p.ID,
1715
- QueryID: p.QueryID,
1716
- Name: p.Name,
1717
- Description: p.Description,
1718
- Type: p.Type,
1719
- IsRequired: p.IsRequired,
1720
- DefaultValue: p.DefaultValue,
1721
- SampleValue: p.SampleValue,
1722
- ValidationFilters: p.ValidationFilters
1723
- })),
1724
- Entities: q.Entities.map((e) => ({
1725
- ID: e.ID,
1726
- QueryID: e.QueryID,
1727
- EntityID: e.EntityID,
1728
- Entity: e.Entity
1729
- })),
1730
- CacheEnabled: q.CacheEnabled,
1731
- CacheMaxSize: q.CacheMaxSize,
1732
- CacheTTLMinutes: q.CacheMaxSize,
1733
- CacheValidationSQL: q.CacheValidationSQL
1734
- }));
1735
- }
1736
-
1737
- // /**
1738
- // * Builds up the array of notes and note types for Skip
1739
- // * These notes are used to provide Skip with domain knowledge and context
1740
- // *
1741
- // * @param contextUser User context for the request
1742
- // * @returns Object containing arrays of notes and note types
1743
- // */
1744
- // protected async BuildSkipAgentNotes(contextUser: UserInfo, filterUserNotesToContextUser: boolean): Promise<{notes: SkipAPIAgentNote[], noteTypes: SkipAPIAgentNoteType[]}> {
1745
- // try {
1746
- // // if already configured this does nothing, just makes sure we're configured
1747
- // await AIEngine.Instance.Config(false, contextUser);
1748
-
1749
- // const agent: AIAgentEntityExtended = AIEngine.Instance.GetAgentByName('Skip');
1750
- // if (agent) {
1751
- // let notes: SkipAPIAgentNote[] = [];
1752
- // let noteTypes: SkipAPIAgentNoteType[] = [];
1753
-
1754
- // notes = agent.Notes.map((r) => {
1755
- // return {
1756
- // id: r.ID,
1757
- // agentNoteTypeId: r.AgentNoteTypeID,
1758
- // agentNoteType: r.AgentNoteType,
1759
- // note: r.Note,
1760
- // type: r.Type,
1761
- // userId: r.UserID,
1762
- // user: r.User,
1763
- // createdAt: r.__mj_CreatedAt,
1764
- // updatedAt: r.__mj_UpdatedAt,
1765
- // }
1766
- // });
1767
-
1768
- // if (filterUserNotesToContextUser){
1769
- // // filter out any notes that are not for this user
1770
- // notes = notes.filter((n) => n.type === 'Global' ||
1771
- // (n.type === 'User' && n.userId === contextUser.ID));
1772
- // }
1773
-
1774
- // noteTypes = AIEngine.Instance.AgentNoteTypes.map((r) => {
1775
- // return {
1776
- // id: r.ID,
1777
- // name: r.Name,
1778
- // description: r.Description
1779
- // }
1780
- // });
1781
-
1782
- // // now return the notes and note types
1783
- // return {notes, noteTypes};
1784
- // }
1785
- // else {
1786
- // console.warn(`No AI Agent found with the name 'Skip' in the AI Engine, so no notes will be sent to Skip`);
1787
- // return {notes: [], noteTypes: []}; // no agent found, so nothing to do
1788
- // }
1789
- // }
1790
- // catch (e) {
1791
- // LogError(`AskSkipResolver::BuildSkipAgentNotes: ${e}`);
1792
- // return {notes: [], noteTypes: []}; // non- fatal error just return empty arrays
1793
- // }
1794
- // }
1795
-
1796
- /**
1797
- * Packs entity rows for inclusion in Skip requests
1798
- * Provides sample data based on entity configuration
1799
- *
1800
- * @param e Entity information
1801
- * @param dataSource Database connection
1802
- * @returns Array of entity rows based on packing configuration
1803
- */
1804
- protected async PackEntityRows(e: EntityInfo, dataSource: mssql.ConnectionPool): Promise<any[]> {
1805
- try {
1806
- if (e.RowsToPackWithSchema === 'None')
1807
- return [];
1808
-
1809
- // only include columns that have a scopes including either All and/or AI or have Null for ScopeDefault
1810
- const fields = e.Fields.filter((f) => {
1811
- const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
1812
- return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
1813
- }).map(f => `[${f.Name}]`).join(',');
1814
-
1815
- // now run the query based on the row packing method
1816
- let sql: string = '';
1817
- switch (e.RowsToPackWithSchema) {
1818
- case 'All':
1819
- sql = `SELECT ${fields} FROM ${e.SchemaName}.${e.BaseView}`;
1820
- break;
1821
- case 'Sample':
1822
- switch (e.RowsToPackSampleMethod) {
1823
- case 'random':
1824
- sql = `SELECT TOP ${e.RowsToPackSampleCount} ${fields} FROM [${e.SchemaName}].[${e.BaseView}] ORDER BY newid()`; // SQL Server newid() function returns a new uniqueidentifier value for each row and when sorted it will be random
1825
- break;
1826
- case 'top n':
1827
- const orderBy = e.RowsToPackSampleOrder ? ` ORDER BY [${e.RowsToPackSampleOrder}]` : '';
1828
- sql = `SELECT TOP ${e.RowsToPackSampleCount} ${fields} FROM [${e.SchemaName}].[${e.BaseView}]${orderBy}`;
1829
- break;
1830
- case 'bottom n':
1831
- const firstPrimaryKey = e.FirstPrimaryKey.Name;
1832
- const innerOrderBy = e.RowsToPackSampleOrder ? `[${e.RowsToPackSampleOrder}]` : `[${firstPrimaryKey}] DESC`;
1833
- sql = `SELECT * FROM (
1834
- SELECT TOP ${e.RowsToPackSampleCount} ${fields}
1835
- FROM [${e.SchemaName}].[${e.BaseView}]
1836
- ORDER BY ${innerOrderBy}
1837
- ) sub
1838
- ORDER BY [${firstPrimaryKey}] ASC;`;
1839
- break;
1840
- }
1841
- }
1842
- const request = new mssql.Request(dataSource);
1843
- const result = await request.query(sql);
1844
- if (!result || !result.recordset) {
1845
- return [];
1846
- }
1847
- else {
1848
- return result.recordset;
1849
- }
1850
- }
1851
- catch (e) {
1852
- LogError(`AskSkipResolver::PackEntityRows: ${e}`);
1853
- return [];
1854
- }
1855
- }
1856
-
1857
- /**
1858
- * Packs possible values for an entity field
1859
- * These values help Skip understand the domain and valid values for fields
1860
- *
1861
- * @param f Field information
1862
- * @param dataSource Database connection
1863
- * @returns Array of possible values for the field
1864
- */
1865
- protected async PackFieldPossibleValues(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldValueInfo[]> {
1866
- try {
1867
- if (f.ValuesToPackWithSchema === 'None') {
1868
- return []; // don't pack anything
1869
- }
1870
- else if (f.ValuesToPackWithSchema === 'All') {
1871
- // wants ALL of the distinct values
1872
- return await this.GetFieldDistinctValues(f, dataSource);
1873
- }
1874
- else if (f.ValuesToPackWithSchema === 'Auto') {
1875
- // default setting - pack based on the ValueListType
1876
- if (f.ValueListTypeEnum === 'List') {
1877
- // simple list of values in the Entity Field Values table
1878
- return f.EntityFieldValues.map((v) => {
1879
- return {value: v.Value, displayValue: v.Value};
1880
- });
1881
- }
1882
- else if (f.ValueListTypeEnum === 'ListOrUserEntry') {
1883
- // could be a user provided value, OR the values in the list of possible values.
1884
- // get the distinct list of values from the DB and concat that with the f.EntityFieldValues array - deduped and return
1885
- const values = await this.GetFieldDistinctValues(f, dataSource);
1886
- if (!values || values.length === 0) {
1887
- // no result, just return the EntityFieldValues
1888
- return f.EntityFieldValues.map((v) => {
1889
- return {value: v.Value, displayValue: v.Value};
1890
- });
1891
- }
1892
- else {
1893
- return [...new Set([...f.EntityFieldValues.map((v) => {
1894
- return {value: v.Value, displayValue: v.Value};
1895
- }), ...values])];
1896
- }
1897
- }
1898
- }
1899
- return []; // if we get here, nothing to pack
1900
- }
1901
- catch (e) {
1902
- LogError(`AskSkipResolver::PackFieldPossibleValues: ${e}`);
1903
- return [];
1904
- }
1905
- }
1906
-
1907
- /**
1908
- * Gets distinct values for a field from the database
1909
- * Used to provide Skip with information about the possible values
1910
- *
1911
- * @param f Field information
1912
- * @param dataSource Database connection
1913
- * @returns Array of distinct values for the field
1914
- */
1915
- protected async GetFieldDistinctValues(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldValueInfo[]> {
1916
- try {
1917
- const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
1918
- const request = new mssql.Request(dataSource);
1919
- const result = await request.query(sql);
1920
- if (!result || !result.recordset) {
1921
- return [];
1922
- }
1923
- else {
1924
- return result.recordset.map((r) => {
1925
- return {
1926
- value: r[f.Name],
1927
- displayValue: r[f.Name]
1928
- };
1929
- });
1930
- }
1931
- }
1932
- catch (e) {
1933
- LogError(`AskSkipResolver::GetFieldDistinctValues: ${e}`);
1934
- return [];
1935
- }
1936
- }
1937
-
1938
-
1939
- // SKIP ENTITIES CACHING
1940
- // Static variables shared across all instances
1941
- private static __skipEntitiesCache$: BehaviorSubject<SkipEntityInfo[] | null> = new BehaviorSubject<SkipEntityInfo[] | null>(null);
1942
- private static __lastRefreshTime: number = 0;
1943
- private static __refreshInProgress: Promise<SkipEntityInfo[]> | null = null;
1944
-
1945
- /**
1946
- * Refreshes the Skip entities cache
1947
- * Rebuilds the entity information that is provided to Skip
1948
- *
1949
- * @param dataSource Database connection
1950
- * @returns Updated array of entity information
1951
- */
1952
- private async refreshSkipEntities(dataSource: mssql.ConnectionPool): Promise<SkipEntityInfo[]> {
1953
- try {
1954
- const md = new Metadata();
1955
-
1956
- // Diagnostic logging
1957
- console.log(`[refreshSkipEntities] Total entities in metadata: ${md.Entities.length}`);
1958
- console.log(`[refreshSkipEntities] Config excludeSchemas: ${JSON.stringify(configInfo.askSkip?.entitiesToSend?.excludeSchemas)}`);
1959
- console.log(`[refreshSkipEntities] Config includeEntitiesFromExcludedSchemas: ${JSON.stringify(configInfo.askSkip?.entitiesToSend?.includeEntitiesFromExcludedSchemas)}`);
1960
-
1961
- const skipSpecialIncludeEntities = (configInfo.askSkip?.entitiesToSend?.includeEntitiesFromExcludedSchemas ?? [])
1962
- .map((e) => e.trim().toLowerCase());
1963
-
1964
- // get the list of entities
1965
- const entities = md.Entities.filter((e) => {
1966
- if (!configInfo.askSkip.entitiesToSend.excludeSchemas.includes(e.SchemaName) ||
1967
- skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase())) {
1968
- const sd = e.ScopeDefault?.trim();
1969
- if (sd && sd.length > 0) {
1970
- const scopes = sd.split(',').map((s) => s.trim().toLowerCase()) ?? ['all'];
1971
- return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai') || skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase());
1972
- }
1973
- else {
1974
- return true; // no scope, so include it
1975
- }
1976
- }
1977
- return false;
1978
- });
1979
-
1980
- console.log(`[refreshSkipEntities] Filtered entities count: ${entities.length}`);
1981
- if (entities.length === 0) {
1982
- console.warn(`[refreshSkipEntities] WARNING: No entities passed filtering! This will result in empty Skip entities list.`);
1983
- }
1984
-
1985
- // now we have our list of entities, pack em up
1986
- const result = await Promise.all(entities.map((e) => this.PackSingleSkipEntityInfo(e, dataSource)));
1987
-
1988
- console.log(`[refreshSkipEntities] Successfully packed ${result.length} entities for Skip`);
1989
-
1990
- AskSkipResolver.__lastRefreshTime = Date.now(); // Update last refresh time
1991
- return result;
1992
- }
1993
- catch (e) {
1994
- LogError(`AskSkipResolver::refreshSkipEntities: ${e}`);
1995
- return [];
1996
- }
1997
- }
1998
-
1999
- /**
2000
- * Builds or retrieves Skip entities from cache
2001
- * Uses caching with request deduplication to avoid expensive rebuilding of entity information
2002
- * Multiple concurrent calls will share the same refresh operation
2003
- *
2004
- * @param dataSource Database connection
2005
- * @param forceRefresh Whether to force a refresh regardless of cache state
2006
- * @param refreshIntervalMinutes Minutes before cache expires
2007
- * @returns Array of entity information
2008
- */
2009
- public async BuildSkipEntities(dataSource: mssql.ConnectionPool, forceRefresh: boolean = false, refreshIntervalMinutes: number = 15): Promise<SkipEntityInfo[]> {
2010
- try {
2011
- const now = Date.now();
2012
- const cacheExpired = (now - AskSkipResolver.__lastRefreshTime) > (refreshIntervalMinutes * 60 * 1000);
2013
- const cacheIsEmpty = AskSkipResolver.__skipEntitiesCache$.value === null;
2014
-
2015
- console.log(`[BuildSkipEntities] forceRefresh: ${forceRefresh}, cacheExpired: ${cacheExpired}, cacheIsEmpty: ${cacheIsEmpty}`);
2016
-
2017
- // If force refresh is requested OR cache expired OR cache is empty, refresh
2018
- if (forceRefresh || cacheExpired || cacheIsEmpty) {
2019
- // Check if a refresh is already in progress - deduplicate concurrent requests
2020
- if (AskSkipResolver.__refreshInProgress) {
2021
- console.log(`[BuildSkipEntities] Refresh already in progress, waiting for it to complete...`);
2022
- return await AskSkipResolver.__refreshInProgress;
2023
- }
2024
-
2025
- console.log(`[BuildSkipEntities] Starting new refresh operation...`);
2026
-
2027
- // Start the refresh and store the Promise for request deduplication
2028
- AskSkipResolver.__refreshInProgress = this.refreshSkipEntities(dataSource);
2029
-
2030
- try {
2031
- const newData = await AskSkipResolver.__refreshInProgress;
2032
- AskSkipResolver.__skipEntitiesCache$.next(newData);
2033
- console.log(`[BuildSkipEntities] Refresh complete, cached ${newData.length} entities`);
2034
- return newData;
2035
- } finally {
2036
- // Clear the in-progress marker so future requests can trigger new refreshes
2037
- AskSkipResolver.__refreshInProgress = null;
2038
- }
2039
- }
2040
-
2041
- const result = AskSkipResolver.__skipEntitiesCache$.value || [];
2042
- console.log(`[BuildSkipEntities] Returning ${result.length} entities from cache`);
2043
- return result;
2044
- }
2045
- catch (e) {
2046
- LogError(`AskSkipResolver::BuildSkipEntities: ${e}`);
2047
- return [];
2048
- }
2049
- }
2050
-
2051
- /**
2052
- * Packs information about a single entity for Skip
2053
- * Includes fields, relationships, and sample data
2054
- *
2055
- * @param e Entity information
2056
- * @param dataSource Database connection
2057
- * @returns Packaged entity information
2058
- */
2059
- protected async PackSingleSkipEntityInfo(e: EntityInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityInfo> {
2060
- try {
2061
- const ret: SkipEntityInfo = {
2062
- id: e.ID,
2063
- name: e.Name,
2064
- schemaName: e.SchemaName,
2065
- baseView: e.BaseView,
2066
- description: e.Description,
2067
-
2068
- fields: await Promise.all(e.Fields.filter(f => {
2069
- // we want to check the scopes for the field level and make sure it is either All or AI or has both
2070
- const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
2071
- return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
2072
- }).map(f => {
2073
- return this.PackSingleSkipEntityField(f, dataSource);
2074
- })),
2075
-
2076
- relatedEntities: e.RelatedEntities.map((r) => {
2077
- return this.PackSingleSkipEntityRelationship(r);
2078
- }),
2079
-
2080
- rowsPacked: e.RowsToPackWithSchema,
2081
- rowsSampleMethod: e.RowsToPackSampleMethod,
2082
- rows: await this.PackEntityRows(e, dataSource)
2083
- };
2084
- return ret;
2085
- }
2086
- catch (e) {
2087
- LogError(`AskSkipResolver::PackSingleSkipEntityInfo: ${e}`);
2088
- return null;
2089
- }
2090
- }
2091
-
2092
- /**
2093
- * Packs information about a single entity relationship
2094
- * These relationships help Skip understand the data model
2095
- *
2096
- * @param r Relationship information
2097
- * @returns Packaged relationship information
2098
- */
2099
- protected PackSingleSkipEntityRelationship(r: EntityRelationshipInfo): SkipEntityRelationshipInfo {
2100
- try {
2101
- return {
2102
- entityID: r.EntityID,
2103
- relatedEntityID: r.RelatedEntityID,
2104
- type: r.Type,
2105
- entityKeyField: r.EntityKeyField,
2106
- relatedEntityJoinField: r.RelatedEntityJoinField,
2107
- joinView: r.JoinView,
2108
- joinEntityJoinField: r.JoinEntityJoinField,
2109
- joinEntityInverseJoinField: r.JoinEntityInverseJoinField,
2110
- entity: r.Entity,
2111
- entityBaseView: r.EntityBaseView,
2112
- relatedEntity: r.RelatedEntity,
2113
- relatedEntityBaseView: r.RelatedEntityBaseView,
2114
- };
2115
- }
2116
- catch (e) {
2117
- LogError(`AskSkipResolver::PackSingleSkipEntityRelationship: ${e}`);
2118
- return null;
2119
- }
2120
- }
2121
-
2122
- /**
2123
- * Packs information about a single entity field
2124
- * Includes metadata and possible values
2125
- *
2126
- * @param f Field information
2127
- * @param dataSource Database connection
2128
- * @returns Packaged field information
2129
- */
2130
- protected async PackSingleSkipEntityField(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldInfo> {
2131
- try {
2132
- return {
2133
- //id: f.ID,
2134
- entityID: f.EntityID,
2135
- sequence: f.Sequence,
2136
- name: f.Name,
2137
- displayName: f.DisplayName,
2138
- category: f.Category,
2139
- type: f.Type,
2140
- description: f.Description,
2141
- isPrimaryKey: f.IsPrimaryKey,
2142
- allowsNull: f.AllowsNull,
2143
- isUnique: f.IsUnique,
2144
- length: f.Length,
2145
- precision: f.Precision,
2146
- scale: f.Scale,
2147
- sqlFullType: f.SQLFullType,
2148
- defaultValue: f.DefaultValue,
2149
- autoIncrement: f.AutoIncrement,
2150
- valueListType: f.ValueListType,
2151
- extendedType: f.ExtendedType,
2152
- defaultInView: f.DefaultInView,
2153
- defaultColumnWidth: f.DefaultColumnWidth,
2154
- isVirtual: f.IsVirtual,
2155
- isNameField: f.IsNameField,
2156
- relatedEntityID: f.RelatedEntityID,
2157
- relatedEntityFieldName: f.RelatedEntityFieldName,
2158
- relatedEntity: f.RelatedEntity,
2159
- relatedEntitySchemaName: f.RelatedEntitySchemaName,
2160
- relatedEntityBaseView: f.RelatedEntityBaseView,
2161
- possibleValues: await this.PackFieldPossibleValues(f, dataSource),
2162
- };
2163
- }
2164
- catch (e) {
2165
- LogError(`AskSkipResolver::PackSingleSkipEntityField: ${e}`);
2166
- return null;
2167
- }
2168
- }
2169
-
2170
- /**
2171
- * Handles initial object loading for Skip chat interactions
2172
- * Creates or loads conversation objects, data contexts, and other required entities
2173
- *
2174
- * @param dataSource Database connection
2175
- * @param ConversationId ID of an existing conversation, or empty for a new one
2176
- * @param UserQuestion The user's question or message
2177
- * @param user User information
2178
- * @param userPayload User payload from context
2179
- * @param md Metadata instance
2180
- * @param DataContextId Optional ID of a data context to use
2181
- * @returns Object containing loaded entities and contexts
2182
- */
2183
- protected async HandleSkipChatInitialObjectLoading(
2184
- dataSource: mssql.ConnectionPool,
2185
- ConversationId: string,
2186
- UserQuestion: string,
2187
- user: UserInfo,
2188
- userPayload: UserPayload,
2189
- md: Metadata,
2190
- DataContextId: string
2191
- ): Promise<{
2192
- convoEntity: ConversationEntity;
2193
- dataContextEntity: DataContextEntity;
2194
- convoDetailEntity: ConversationDetailEntity;
2195
- dataContext: DataContext;
2196
- }> {
2197
- const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
2198
- let dataContextEntity: DataContextEntity;
2199
-
2200
- if (!ConversationId || ConversationId.length === 0) {
2201
- // create a new conversation id
2202
- convoEntity.NewRecord();
2203
- if (user) {
2204
- convoEntity.UserID = user.ID;
2205
- convoEntity.Name = AskSkipResolver._defaultNewChatName;
2206
- // Set initial status to Available since no processing has started yet
2207
- convoEntity.Status = 'Available';
2208
-
2209
- dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
2210
- if (!DataContextId || DataContextId.length === 0) {
2211
- dataContextEntity.NewRecord();
2212
- dataContextEntity.UserID = user.ID;
2213
- dataContextEntity.Name = 'Data Context for Skip Conversation ';
2214
- if (!(await dataContextEntity.Save())) {
2215
- LogError(`Creating a new data context failed`, undefined, dataContextEntity.LatestResult);
2216
- throw new Error(`Creating a new data context failed`);
2217
- }
2218
- }
2219
- else {
2220
- const dcLoadResult = await dataContextEntity.Load(DataContextId);
2221
- if (!dcLoadResult) {
2222
- throw new Error(`Loading DataContextEntity for DataContextId ${DataContextId} failed`);
2223
- }
2224
- }
2225
- convoEntity.DataContextID = dataContextEntity.ID;
2226
- if (await convoEntity.Save()) {
2227
- ConversationId = convoEntity.ID;
2228
- if (!DataContextId || dataContextEntity.ID.length === 0) {
2229
- // only do this if we created a new data context for this conversation
2230
- dataContextEntity.Name += ` ${ConversationId}`;
2231
- const dciSaveResult: boolean = await dataContextEntity.Save();
2232
- if (!dciSaveResult) {
2233
- LogError(`Error saving DataContextEntity for conversation: ${ConversationId}`, undefined, dataContextEntity.LatestResult);
2234
- }
2235
- }
2236
- }
2237
- else {
2238
- LogError(`Creating a new conversation failed`, undefined, convoEntity.LatestResult);
2239
- throw new Error(`Creating a new conversation failed`);
2240
- }
2241
- }
2242
- else {
2243
- throw new Error(`User ${userPayload.email} not found in UserCache`);
2244
- }
2245
- }
2246
- else {
2247
- await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
2248
- dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
2249
-
2250
- // check to see if the DataContextId is passed in and if it is different than the DataContextID in the conversation
2251
- if (DataContextId && DataContextId.length > 0 && DataContextId !== convoEntity.DataContextID) {
2252
- if (convoEntity.DataContextID === null) {
2253
- // use the DataContextId passed in if the conversation doesn't have a DataContextID
2254
- convoEntity.DataContextID = DataContextId;
2255
- const convoEntitySaveResult: boolean = await convoEntity.Save();
2256
- if (!convoEntitySaveResult) {
2257
- LogError(`Error saving conversation entity for conversation: ${ConversationId}`, undefined, convoEntity.LatestResult);
2258
- }
2259
- }
2260
- else {
2261
- // note - we ignore the parameter DataContextId if it is passed in, we will use the data context from the conversation that is saved.
2262
- // If a user wants to change the data context for a convo, they can do that elsewhere
2263
- console.warn(
2264
- `AskSkipResolver: DataContextId ${DataContextId} was passed in but it was ignored because it was different than the DataContextID in the conversation ${convoEntity.DataContextID}`
2265
- );
2266
- }
2267
- // only load if we have a data context here, otherwise we have a new record in the dataContext entity
2268
- if (convoEntity.DataContextID)
2269
- await dataContextEntity.Load(convoEntity.DataContextID);
2270
- }
2271
- else if ((!DataContextId || DataContextId.length === 0) && (!convoEntity.DataContextID || convoEntity.DataContextID.length === 0)) {
2272
- // in this branch of the logic we don't have a passed in DataContextId and we don't have a DataContextID in the conversation, so we need to save the data context, get the ID,
2273
- // update the conversation and save it as well
2274
- dataContextEntity.NewRecord();
2275
- dataContextEntity.UserID = user.ID;
2276
- dataContextEntity.Name = 'Data Context for Skip Conversation ' + ConversationId;
2277
- if (await dataContextEntity.Save()) {
2278
- DataContextId = convoEntity.DataContextID;
2279
- convoEntity.DataContextID = dataContextEntity.ID;
2280
- if (!await convoEntity.Save()) {
2281
- LogError(`Error saving conversation entity for conversation: ${ConversationId}`, undefined, convoEntity.LatestResult);
2282
- }
2283
- }
2284
- else
2285
- LogError(`Error saving DataContextEntity for conversation: ${ConversationId}`, undefined, dataContextEntity.LatestResult);
2286
- }
2287
- else {
2288
- // finally in this branch we get here if we have a DataContextId passed in and it is the same as the DataContextID in the conversation, in this case simply load the data context
2289
- await dataContextEntity.Load(convoEntity.DataContextID);
2290
- }
2291
- }
2292
-
2293
- // now, create a conversation detail record for the user message
2294
- const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
2295
- convoDetailEntity.NewRecord();
2296
- convoDetailEntity.ConversationID = ConversationId;
2297
- convoDetailEntity.UserID = user.ID;
2298
- convoDetailEntity.Message = UserQuestion;
2299
- convoDetailEntity.Role = 'User';
2300
- convoDetailEntity.HiddenToUser = false;
2301
-
2302
- let convoDetailSaveResult: boolean = await convoDetailEntity.Save();
2303
- if (!convoDetailSaveResult) {
2304
- LogError(`Error saving conversation detail entity for user message: ${UserQuestion}`, undefined, convoDetailEntity.LatestResult);
2305
- }
2306
-
2307
- const dataContext = MJGlobal.Instance.ClassFactory.CreateInstance<DataContext>(DataContext);
2308
- await dataContext.Load(dataContextEntity.ID, dataSource, false, false, 0, user);
2309
- return { dataContext, convoEntity, dataContextEntity, convoDetailEntity };
2310
- }
2311
-
2312
- /**
2313
- * Loads conversation details from the database and transforms them into Skip message format
2314
- * Used to provide Skip with conversation history for context
2315
- *
2316
- * @param dataSource Database connection
2317
- * @param ConversationId ID of the conversation to load details for
2318
- * @param maxHistoricalMessages Maximum number of historical messages to include
2319
- * @returns Array of messages in Skip format
2320
- */
2321
- protected async LoadConversationDetailsIntoSkipMessages(
2322
- dataSource: mssql.ConnectionPool,
2323
- ConversationId: string,
2324
- maxHistoricalMessages?: number,
2325
- roleFilter?: string
2326
- ): Promise<SkipMessage[]> {
2327
- try {
2328
- if (!ConversationId || ConversationId.length === 0) {
2329
- throw new Error(`ConversationId is required`);
2330
- }
2331
-
2332
- // load up all the conversation details from the database server
2333
- const md = new Metadata();
2334
- const e = md.Entities.find((e) => e.Name === 'Conversation Details');
2335
-
2336
- // Add role filter if specified
2337
- const roleFilterClause = roleFilter ? ` AND Role = '${roleFilter}'` : '';
2338
-
2339
- const sql = `SELECT
2340
- ${maxHistoricalMessages ? 'TOP ' + maxHistoricalMessages : ''} *
2341
- FROM
2342
- ${e.SchemaName}.${e.BaseView}
2343
- WHERE
2344
- ConversationID = '${ConversationId}'${roleFilterClause}
2345
- ORDER
2346
- BY __mj_CreatedAt DESC`;
2347
- const request = new mssql.Request(dataSource);
2348
- const result = await request.query(sql);
2349
- if (!result || !result.recordset)
2350
- throw new Error(`Error running SQL: ${sql}`);
2351
- else {
2352
- // 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
2353
- // 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
2354
- // 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.
2355
- const returnData = result.recordset.sort((a: any, b: any) => {
2356
- const aDate = new Date(a.__mj_CreatedAt);
2357
- const bDate = new Date(b.__mj_CreatedAt);
2358
- return aDate.getTime() - bDate.getTime();
2359
- });
2360
-
2361
- // now, we will map the returnData into an array of SkipMessages
2362
- return returnData.map((r: ConversationDetailEntity) => {
2363
- // we want to limit the # of characters in the message to 5000, rough approximation for 1000 words/tokens
2364
- // but we only do that for system messages
2365
- const skipRole = this.MapDBRoleToSkipRole(r.Role);
2366
- let outputMessage; // will be populated below for system messages
2367
- if (skipRole === 'system') {
2368
- let detail: SkipAPIResponse;
2369
- try {
2370
- detail = <SkipAPIResponse>JSON.parse(r.Message);
2371
- } catch (e) {
2372
- // ignore, sometimes we dont have a JSON message, just use the raw message
2373
- detail = null;
2374
- outputMessage = r.Message;
2375
- }
2376
- if (detail?.responsePhase === SkipResponsePhase.AnalysisComplete) {
2377
- const analysisDetail = <SkipAPIAnalysisCompleteResponse>detail;
2378
- outputMessage = JSON.stringify({
2379
- responsePhase: SkipResponsePhase.AnalysisComplete,
2380
- techExplanation: analysisDetail.techExplanation,
2381
- userExplanation: analysisDetail.userExplanation,
2382
- executionResults: analysisDetail.executionResults,
2383
- tableDataColumns: analysisDetail.tableDataColumns,
2384
- componentOptions: analysisDetail.componentOptions,
2385
- artifactRequest: analysisDetail.artifactRequest
2386
- });
2387
- } else if (detail?.responsePhase === SkipResponsePhase.ClarifyingQuestion) {
2388
- const clarifyingQuestionDetail = <SkipAPIClarifyingQuestionResponse>detail;
2389
- outputMessage = JSON.stringify({
2390
- responsePhase: SkipResponsePhase.ClarifyingQuestion,
2391
- clarifyingQuestion: clarifyingQuestionDetail.clarifyingQuestion,
2392
- });
2393
- } else if (detail) {
2394
- // we should never get here, AI responses only fit the above
2395
- // don't throw an exception, but log an error
2396
- LogError(`Unknown response phase: ${detail.responsePhase}`);
2397
- }
2398
- }
2399
- const m: SkipMessage = {
2400
- content: skipRole === 'system' ? outputMessage : r.Message,
2401
- role: skipRole,
2402
- conversationDetailID: r.ID,
2403
- hiddenToUser: r.HiddenToUser,
2404
- userRating: r.UserRating,
2405
- userFeedback: r.UserFeedback,
2406
- reflectionInsights: r.ReflectionInsights,
2407
- summaryOfEarlierConveration: r.SummaryOfEarlierConversation,
2408
- createdAt: r.__mj_CreatedAt,
2409
- updatedAt: r.__mj_UpdatedAt,
2410
- };
2411
- return m;
2412
- });
2413
- }
2414
- } catch (e) {
2415
- LogError(e);
2416
- throw e;
2417
- }
2418
- }
2419
-
2420
- /**
2421
- * Maps database role values to Skip API role format
2422
- * Converts role names from database format to the format expected by Skip API
2423
- *
2424
- * @param role Database role value
2425
- * @returns Skip API role value ('user' or 'system')
2426
- */
2427
- protected MapDBRoleToSkipRole(role: string): 'user' | 'system' {
2428
- switch (role.trim().toLowerCase()) {
2429
- case 'ai':
2430
- case 'system':
2431
- case 'assistant':
2432
- return 'system';
2433
- default:
2434
- return 'user';
2435
- }
2436
- }
2437
-
2438
- /**
2439
- * Handles the main Skip chat request processing flow
2440
- * Routes the request through the different phases based on the Skip API response
2441
- *
2442
- * @param input Skip API request to send
2443
- * @param UserQuestion The question or message from the user
2444
- * @param user User information
2445
- * @param dataSource Database connection
2446
- * @param ConversationId ID of the conversation
2447
- * @param userPayload User payload from context
2448
- * @param pubSub Publisher/subscriber for events
2449
- * @param md Metadata instance
2450
- * @param convoEntity Conversation entity
2451
- * @param convoDetailEntity Conversation detail entity for the user message
2452
- * @param dataContext Data context associated with the conversation
2453
- * @param dataContextEntity Data context entity
2454
- * @param conversationDetailCount Tracking count to prevent infinite loops
2455
- * @returns Result of the Skip interaction
2456
- */
2457
- protected async HandleSkipChatRequest(
2458
- input: SkipAPIRequest,
2459
- UserQuestion: string,
2460
- user: UserInfo,
2461
- dataSource: mssql.ConnectionPool,
2462
- ConversationId: string,
2463
- userPayload: UserPayload,
2464
- pubSub: PubSubEngine,
2465
- md: Metadata,
2466
- convoEntity: ConversationEntity,
2467
- convoDetailEntity: ConversationDetailEntity,
2468
- dataContext: DataContext,
2469
- dataContextEntity: DataContextEntity,
2470
- conversationDetailCount: number,
2471
- startTime: Date
2472
- ): Promise<AskSkipResultType> {
2473
- const skipConfigInfo = configInfo.askSkip;
2474
- const chatURL = skipConfigInfo.url ? `${skipConfigInfo.url}${SKIP_API_ENDPOINTS.CHAT}` : skipConfigInfo.chatURL;
2475
- LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${chatURL}`);
2476
-
2477
- if (conversationDetailCount > 10) {
2478
- // Set status of conversation to Available since we still want to allow the user to ask questions
2479
- await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
2480
-
2481
- // At this point it is likely that we are stuck in a loop, so we stop here
2482
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2483
- message: JSON.stringify({
2484
- type: 'AskSkip',
2485
- status: 'Error',
2486
- conversationID: ConversationId,
2487
- message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
2488
- }),
2489
- sessionId: userPayload.sessionId,
2490
- });
2491
-
2492
- return {
2493
- Success: false,
2494
- Status: 'Error',
2495
- Result: `Exceeded maximum attempts to answer the question ${UserQuestion}`,
2496
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
2497
- ConversationId: ConversationId,
2498
- UserMessageConversationDetailId: '',
2499
- AIMessageConversationDetailId: '',
2500
- };
2501
- }
2502
-
2503
- let response;
2504
- try {
2505
- response = await sendPostRequest(
2506
- chatURL,
2507
- input,
2508
- true,
2509
- this.buildSkipPostHeaders(),
2510
- (message: {
2511
- type: string;
2512
- value: {
2513
- success: boolean;
2514
- error: string;
2515
- responsePhase: string;
2516
- messages: {
2517
- role: string;
2518
- content: string;
2519
- }[];
2520
- };
2521
- }) => {
2522
- LogStatus(JSON.stringify(message, null, 4));
2523
- if (message.type === 'status_update') {
2524
- const statusContent = message.value.messages[0].content;
2525
-
2526
- // Store the status in our active streams cache
2527
- activeStreams.updateStatus(ConversationId, statusContent, userPayload.sessionId);
2528
-
2529
- // Publish to all sessions listening to this conversation
2530
- const sessionIds = activeStreams.getSessionIds(ConversationId);
2531
- for (const sessionId of sessionIds) {
2532
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2533
- message: JSON.stringify({
2534
- type: 'AskSkip',
2535
- status: 'OK',
2536
- conversationID: ConversationId,
2537
- ResponsePhase: message.value.responsePhase,
2538
- message: statusContent,
2539
- }),
2540
- sessionId: sessionId,
2541
- });
2542
- }
2543
- }
2544
- }
2545
- );
2546
- } catch (error) {
2547
- // Set conversation status to Available on error so user can try again
2548
- await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
2549
-
2550
- // Log the error for debugging
2551
- LogError(`Error in HandleSkipChatRequest sendPostRequest: ${error}`);
2552
-
2553
- // Publish error status update to user
2554
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2555
- message: JSON.stringify({
2556
- type: 'AskSkip',
2557
- status: 'Error',
2558
- conversationID: ConversationId,
2559
- message: 'Request failed. Please try again later and if this continues, contact your support desk.',
2560
- }),
2561
- sessionId: userPayload.sessionId,
2562
- });
2563
-
2564
- // Re-throw the error to propagate it up the stack
2565
- throw error;
2566
- }
2567
-
2568
- if (response && response.length > 0) {
2569
- // response.status === 200) {
2570
- // the last object in the response array is the final response from the Skip API
2571
- const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
2572
- //const apiResponse = <SkipAPIResponse>response.data;
2573
- LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
2574
- this.PublishApiResponseUserUpdateMessage(apiResponse, userPayload, ConversationId, pubSub);
2575
-
2576
- // now, based on the result type, we will either wait for the next phase or we will process the results
2577
- if (apiResponse.responsePhase === 'data_request') {
2578
- return await this.HandleDataRequestPhase(
2579
- input,
2580
- <SkipAPIDataRequestResponse>apiResponse,
2581
- UserQuestion,
2582
- user,
2583
- dataSource,
2584
- ConversationId,
2585
- userPayload,
2586
- pubSub,
2587
- convoEntity,
2588
- convoDetailEntity,
2589
- dataContext,
2590
- dataContextEntity,
2591
- conversationDetailCount,
2592
- startTime
2593
- );
2594
- } else if (apiResponse.responsePhase === 'clarifying_question') {
2595
- // need to send the request back to the user for a clarifying question
2596
- return await this.HandleClarifyingQuestionPhase(
2597
- input,
2598
- <SkipAPIClarifyingQuestionResponse>apiResponse,
2599
- UserQuestion,
2600
- user,
2601
- dataSource,
2602
- ConversationId,
2603
- userPayload,
2604
- pubSub,
2605
- convoEntity,
2606
- convoDetailEntity,
2607
- startTime,
2608
- );
2609
- } else if (apiResponse.responsePhase === 'analysis_complete') {
2610
- return await this.HandleAnalysisComplete(
2611
- input,
2612
- <SkipAPIAnalysisCompleteResponse>apiResponse,
2613
- UserQuestion,
2614
- user,
2615
- dataSource,
2616
- ConversationId,
2617
- userPayload,
2618
- pubSub,
2619
- convoEntity,
2620
- convoDetailEntity,
2621
- dataContext,
2622
- dataContextEntity,
2623
- startTime
2624
- );
2625
- } else {
2626
- // unknown response phase
2627
- throw new Error(`Unknown Skip API response phase: ${apiResponse.responsePhase}`);
2628
- }
2629
- } else {
2630
- // Set status of conversation to Available since we still want to allow the user to ask questions
2631
- await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
2632
-
2633
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2634
- message: JSON.stringify({
2635
- type: 'AskSkip',
2636
- status: 'Error',
2637
- conversationID: ConversationId,
2638
- message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
2639
- }),
2640
- sessionId: userPayload.sessionId,
2641
- });
2642
-
2643
- return {
2644
- Success: false,
2645
- Status: 'Error',
2646
- Result: `User Question ${UserQuestion} didn't work!`,
2647
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
2648
- ConversationId: ConversationId,
2649
- UserMessageConversationDetailId: '',
2650
- AIMessageConversationDetailId: '',
2651
- };
2652
- }
2653
- }
2654
-
2655
- protected buildSkipPostHeaders(): { [key: string]: string } {
2656
- return {
2657
- 'x-api-key': configInfo.askSkip?.apiKey ?? '',
2658
- };
2659
- }
2660
-
2661
- /**
2662
- * Publishes a status update message to the user based on the Skip API response
2663
- * Provides feedback about what phase of processing is happening
2664
- *
2665
- * @param apiResponse The response from the Skip API
2666
- * @param userPayload User payload from context
2667
- * @param conversationID ID of the conversation
2668
- * @param pubSub Publisher/subscriber for events
2669
- */
2670
- protected async PublishApiResponseUserUpdateMessage(
2671
- apiResponse: SkipAPIResponse,
2672
- userPayload: UserPayload,
2673
- conversationID: string,
2674
- pubSub: PubSubEngine
2675
- ) {
2676
- let sUserMessage: string = '';
2677
- switch (apiResponse.responsePhase) {
2678
- case 'data_request':
2679
- sUserMessage = 'We need to gather some more data, I will do that next and update you soon.';
2680
- break;
2681
- case 'analysis_complete':
2682
- sUserMessage = 'I have completed the analysis, the results will be available momentarily.';
2683
- break;
2684
- case 'clarifying_question':
2685
- // don't send an update because the actual message will happen and show up in the UI, so this is redundant
2686
- //sUserMessage = 'I have a clarifying question for you, please see review our chat so you can provide me a little more info.';
2687
- break;
2688
- }
2689
-
2690
- // update the UI
2691
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2692
- message: JSON.stringify({
2693
- type: 'AskSkip',
2694
- status: 'OK',
2695
- conversationID,
2696
- message: sUserMessage,
2697
- }),
2698
- sessionId: userPayload.sessionId,
2699
- });
2700
- }
2701
-
2702
- /**
2703
- * Handles the analysis complete phase of the Skip chat process
2704
- * Finalizes the conversation and creates necessary artifacts
2705
- *
2706
- * @param apiRequest The original request sent to Skip
2707
- * @param apiResponse The analysis complete response from Skip
2708
- * @param UserQuestion The original user question
2709
- * @param user User information
2710
- * @param dataSource Database connection
2711
- * @param ConversationId ID of the conversation
2712
- * @param userPayload User payload from context
2713
- * @param pubSub Publisher/subscriber for events
2714
- * @param convoEntity Conversation entity
2715
- * @param convoDetailEntity Conversation detail entity for the user message
2716
- * @param dataContext Data context associated with the conversation
2717
- * @param dataContextEntity Data context entity
2718
- * @returns Result of the Skip interaction
2719
- */
2720
- protected async HandleAnalysisComplete(
2721
- apiRequest: SkipAPIRequest,
2722
- apiResponse: SkipAPIAnalysisCompleteResponse,
2723
- UserQuestion: string,
2724
- user: UserInfo,
2725
- dataSource: mssql.ConnectionPool,
2726
- ConversationId: string,
2727
- userPayload: UserPayload,
2728
- pubSub: PubSubEngine,
2729
- convoEntity: ConversationEntity,
2730
- convoDetailEntity: ConversationDetailEntity,
2731
- dataContext: DataContext,
2732
- dataContextEntity: DataContextEntity,
2733
- startTime: Date
2734
- ): Promise<AskSkipResultType> {
2735
- // analysis is complete
2736
- // all done, wrap things up
2737
- const md = new Metadata();
2738
-
2739
- // if we created an access token, it will expire soon anyway but let's remove it for extra safety now
2740
- if (apiRequest.callingServerAccessToken && tokenExists(apiRequest.callingServerAccessToken)) {
2741
- deleteAccessToken(apiRequest.callingServerAccessToken);
2742
- }
2743
-
2744
- const { AIMessageConversationDetailID } = await this.FinishConversationAndNotifyUser(
2745
- apiResponse,
2746
- dataContext,
2747
- dataContextEntity,
2748
- md,
2749
- user,
2750
- convoEntity,
2751
- pubSub,
2752
- userPayload,
2753
- dataSource,
2754
- startTime
2755
- );
2756
- const response: AskSkipResultType = {
2757
- Success: true,
2758
- Status: 'OK',
2759
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
2760
- ConversationId: ConversationId,
2761
- UserMessageConversationDetailId: convoDetailEntity.ID,
2762
- AIMessageConversationDetailId: AIMessageConversationDetailID,
2763
- Result: JSON.stringify(apiResponse),
2764
- };
2765
- return response;
2766
- }
2767
-
2768
- /**
2769
- * Handles the clarifying question phase of the Skip chat process
2770
- * Creates a conversation detail for the clarifying question from Skip
2771
- *
2772
- * @param apiRequest The original request sent to Skip
2773
- * @param apiResponse The clarifying question response from Skip
2774
- * @param UserQuestion The original user question
2775
- * @param user User information
2776
- * @param dataSource Database connection
2777
- * @param ConversationId ID of the conversation
2778
- * @param userPayload User payload from context
2779
- * @param pubSub Publisher/subscriber for events
2780
- * @param convoEntity Conversation entity
2781
- * @param convoDetailEntity Conversation detail entity for the user message
2782
- * @returns Result of the Skip interaction
2783
- */
2784
- protected async HandleClarifyingQuestionPhase(
2785
- apiRequest: SkipAPIRequest,
2786
- apiResponse: SkipAPIClarifyingQuestionResponse,
2787
- UserQuestion: string,
2788
- user: UserInfo,
2789
- dataSource: mssql.ConnectionPool,
2790
- ConversationId: string,
2791
- userPayload: UserPayload,
2792
- pubSub: PubSubEngine,
2793
- convoEntity: ConversationEntity,
2794
- convoDetailEntity: ConversationDetailEntity,
2795
- startTime: Date
2796
- ): Promise<AskSkipResultType> {
2797
- // need to create a message here in the COnversation and then pass that id below
2798
- const endTime = new Date();
2799
- const md = new Metadata();
2800
- const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
2801
- convoDetailEntityAI.NewRecord();
2802
- convoDetailEntityAI.ConversationID = ConversationId;
2803
- convoDetailEntityAI.Message = JSON.stringify(apiResponse); //.clarifyingQuestion;
2804
- convoDetailEntityAI.Role = 'AI';
2805
- convoDetailEntityAI.HiddenToUser = false;
2806
- convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
2807
-
2808
- // Set conversation status back to Available since we need user input for the clarifying question
2809
- await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
2810
-
2811
- if (await convoDetailEntityAI.Save()) {
2812
- return {
2813
- Success: true,
2814
- Status: 'OK',
2815
- ResponsePhase: SkipResponsePhase.ClarifyingQuestion,
2816
- ConversationId: ConversationId,
2817
- UserMessageConversationDetailId: convoDetailEntity.ID,
2818
- AIMessageConversationDetailId: convoDetailEntityAI.ID,
2819
- Result: JSON.stringify(apiResponse),
2820
- };
2821
- } else {
2822
- LogError(
2823
- `Error saving conversation detail entity for AI message: ${apiResponse.clarifyingQuestion}`,
2824
- undefined,
2825
- convoDetailEntityAI.LatestResult
2826
- );
2827
- return {
2828
- Success: false,
2829
- Status: 'Error',
2830
- ResponsePhase: SkipResponsePhase.ClarifyingQuestion,
2831
- ConversationId: ConversationId,
2832
- UserMessageConversationDetailId: convoDetailEntity.ID,
2833
- AIMessageConversationDetailId: convoDetailEntityAI.ID,
2834
- Result: JSON.stringify(apiResponse),
2835
- };
2836
- }
2837
- }
2838
-
2839
- /**
2840
- * Handles the data request phase of the Skip chat process
2841
- * Processes data requests from Skip and loads requested data
2842
- *
2843
- * @param apiRequest The original request sent to Skip
2844
- * @param apiResponse The data request response from Skip
2845
- * @param UserQuestion The original user question
2846
- * @param user User information
2847
- * @param dataSource Database connection
2848
- * @param ConversationId ID of the conversation
2849
- * @param userPayload User payload from context
2850
- * @param pubSub Publisher/subscriber for events
2851
- * @param convoEntity Conversation entity
2852
- * @param convoDetailEntity Conversation detail entity for the user message
2853
- * @param dataContext Data context associated with the conversation
2854
- * @param dataContextEntity Data context entity
2855
- * @param conversationDetailCount Tracking count to prevent infinite loops
2856
- * @returns Result of the Skip interaction
2857
- */
2858
- protected async HandleDataRequestPhase(
2859
- apiRequest: SkipAPIRequest,
2860
- apiResponse: SkipAPIDataRequestResponse,
2861
- UserQuestion: string,
2862
- user: UserInfo,
2863
- dataSource: mssql.ConnectionPool,
2864
- ConversationId: string,
2865
- userPayload: UserPayload,
2866
- pubSub: PubSubEngine,
2867
- convoEntity: ConversationEntity,
2868
- convoDetailEntity: ConversationDetailEntity,
2869
- dataContext: DataContext,
2870
- dataContextEntity: DataContextEntity,
2871
- conversationDetailCount: number,
2872
- startTime: Date
2873
- ): Promise<AskSkipResultType> {
2874
- // 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
2875
- try {
2876
- if (!apiResponse.success) {
2877
- LogError(`Data request/gathering from Skip API failed: ${apiResponse.error}`);
2878
- return {
2879
- Success: false,
2880
- 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}`,
2881
- ResponsePhase: SkipResponsePhase.DataRequest,
2882
- ConversationId: ConversationId,
2883
- UserMessageConversationDetailId: convoDetailEntity.ID,
2884
- AIMessageConversationDetailId: '',
2885
- Result: JSON.stringify(apiResponse),
2886
- };
2887
- }
2888
-
2889
- const _maxDataGatheringRetries = 5;
2890
- const _dataGatheringFailureHeaderMessage = '***DATA GATHERING FAILURE***';
2891
- const md = new Metadata();
2892
- const executionErrors = [];
2893
- let dataRequest = apiResponse.dataRequest;
2894
-
2895
- // 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
2896
- apiRequest.messages.push({
2897
- content: `Skip API Requested Data as shown below
2898
- ${JSON.stringify(apiResponse.dataRequest)}`,
2899
- 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
2900
- conversationDetailID: convoDetailEntity.ID,
2901
- });
2902
-
2903
- // 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
2904
- if (!Array.isArray(dataRequest)) {
2905
- if (dataRequest) {
2906
- dataRequest = [dataRequest];
2907
- } else {
2908
- const errorMessage = `Data request from Skip API is not an array and not a single item.`;
2909
- LogError(errorMessage);
2910
- executionErrors.push({ dataRequest: apiResponse.dataRequest, errorMessage: errorMessage });
2911
- dataRequest = []; // make a blank array so we can continue
2912
- }
2913
- }
2914
-
2915
- for (const dr of dataRequest) {
2916
- try {
2917
- const item = dataContext.AddDataContextItem();
2918
- switch (dr.type) {
2919
- case 'sql':
2920
- item.Type = 'sql';
2921
- item.SQL = dr.text;
2922
- item.AdditionalDescription = dr.description;
2923
- item.CodeName = dr.codeName;
2924
- if (!(await item.LoadData(dataSource, false, false, 0, user)))
2925
- throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
2926
- break;
2927
- case 'stored_query':
2928
- const queryName = dr.text;
2929
- const query = md.Queries.find((q) => q.Name === queryName);
2930
- if (query) {
2931
- item.Type = 'query';
2932
- item.QueryID = query.ID;
2933
- item.RecordName = query.Name;
2934
- item.AdditionalDescription = dr.description;
2935
- if (!(await item.LoadData(dataSource, false, false, 0, user)))
2936
- throw new Error(`SQL data request failed: ${item.DataLoadingError}`);
2937
- } else throw new Error(`Query ${queryName} not found.`);
2938
- break;
2939
- default:
2940
- throw new Error(`Unknown data request type: ${dr.type}`);
2941
- break;
2942
- }
2943
- } catch (e) {
2944
- LogError(e);
2945
- executionErrors.push({
2946
- dataRequest: dr,
2947
- errorMessage: e && typeof e === 'object' && 'message' in e && e.message ? e.message : e.toString(),
2948
- });
2949
- }
2950
- }
2951
-
2952
- if (executionErrors.length > 0) {
2953
- const dataGatheringFailedAttemptCount =
2954
- apiRequest.messages.filter((m) => m.content.includes(_dataGatheringFailureHeaderMessage)).length + 1;
2955
- if (dataGatheringFailedAttemptCount > _maxDataGatheringRetries) {
2956
- // 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
2957
- LogStatus(
2958
- `Execution errors for Skip data request occured, and we have exceeded the max retries${_maxDataGatheringRetries}, sending errors back to the user.`
2959
- );
2960
- return {
2961
- Success: false,
2962
- Status:
2963
- 'Error gathering data and we have exceedded the max retries. Try again later and Skip might be able to handle this request.',
2964
- ResponsePhase: SkipResponsePhase.DataRequest,
2965
- ConversationId: ConversationId,
2966
- UserMessageConversationDetailId: convoDetailEntity.ID,
2967
- AIMessageConversationDetailId: '',
2968
- Result: JSON.stringify(apiResponse),
2969
- };
2970
- } else {
2971
- LogStatus(`Execution errors for Skip data request occured, sending those errors back to the Skip API to get new instructions.`);
2972
- apiRequest.requestPhase = 'data_gathering_failure';
2973
- apiRequest.messages.push({
2974
- content: `${_dataGatheringFailureHeaderMessage} #${dataGatheringFailedAttemptCount} of ${_maxDataGatheringRetries} attempts to gather data failed. Errors:
2975
- ${JSON.stringify(executionErrors)}
2976
- `,
2977
- role: 'user', // use user role becuase to the Skip API what we send it is "user"
2978
- conversationDetailID: convoDetailEntity.ID,
2979
- });
2980
- }
2981
- } else {
2982
- await dataContext.SaveItems(user, false); // save the data context items
2983
- // replace the data context copy that is in the apiRequest.
2984
- 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
2985
- apiRequest.requestPhase = 'data_gathering_response';
2986
- }
2987
- conversationDetailCount++;
2988
- // we have all of the data now, add it to the data context and then submit it back to the Skip API
2989
- return this.HandleSkipChatRequest(
2990
- apiRequest,
2991
- UserQuestion,
2992
- user,
2993
- dataSource,
2994
- ConversationId,
2995
- userPayload,
2996
- pubSub,
2997
- md,
2998
- convoEntity,
2999
- convoDetailEntity,
3000
- dataContext,
3001
- dataContextEntity,
3002
- conversationDetailCount,
3003
- startTime
3004
- );
3005
- } catch (e) {
3006
- LogError(e);
3007
- throw e;
3008
- }
3009
- }
3010
-
3011
- /**
3012
- * Finishes a successful conversation and notifies the user
3013
- * Creates necessary records, artifacts, and notifications
3014
- *
3015
- * @param apiResponse The analysis complete response from Skip
3016
- * @param dataContext Data context associated with the conversation
3017
- * @param dataContextEntity Data context entity
3018
- * @param md Metadata instance
3019
- * @param user User information
3020
- * @param convoEntity Conversation entity
3021
- * @param pubSub Publisher/subscriber for events
3022
- * @param userPayload User payload from context
3023
- * @param dataSource Database connection
3024
- * @returns The ID of the AI message conversation detail
3025
- */
3026
- protected async FinishConversationAndNotifyUser(
3027
- apiResponse: SkipAPIAnalysisCompleteResponse,
3028
- dataContext: DataContext,
3029
- dataContextEntity: DataContextEntity,
3030
- md: Metadata,
3031
- user: UserInfo,
3032
- convoEntity: ConversationEntity,
3033
- pubSub: PubSubEngine,
3034
- userPayload: UserPayload,
3035
- dataSource: mssql.ConnectionPool,
3036
- startTime: Date
3037
- ): Promise<{ AIMessageConversationDetailID: string }> {
3038
- const sTitle = apiResponse.title || apiResponse.reportTitle;
3039
- const sResult = JSON.stringify(apiResponse);
3040
-
3041
- // first up, let's see if Skip asked us to create an artifact or add a new version to an existing artifact, or NOT
3042
- // use artifacts at all...
3043
- let artifactId: string = null;
3044
- let artifactVersionId: string = null;
3045
-
3046
- if (apiResponse.artifactRequest?.action === 'new_artifact' || apiResponse.artifactRequest?.action === 'new_artifact_version') {
3047
- // Skip has requested that we create a new artifact or add a new version to an existing artifact
3048
- artifactId = apiResponse.artifactRequest.artifactId; // will only be populated if action == new_artifact_version
3049
- let newVersion: number = 0;
3050
- if (apiResponse.artifactRequest?.action === 'new_artifact') {
3051
- const artifactEntity = await md.GetEntityObject<ConversationArtifactEntity>('MJ: Conversation Artifacts', user);
3052
- // create the new artifact here
3053
- artifactEntity.NewRecord();
3054
- artifactEntity.ConversationID = convoEntity.ID;
3055
- artifactEntity.Name = apiResponse.artifactRequest.name;
3056
- artifactEntity.Description = apiResponse.artifactRequest.description;
3057
- // make sure AI Engine is configured.
3058
- await AIEngine.Instance.Config(false, user)
3059
- artifactEntity.ArtifactTypeID = AIEngine.Instance.ArtifactTypes.find((t) => t.Name === 'Report')?.ID;
3060
- artifactEntity.SharingScope = 'None';
3061
-
3062
- if (await artifactEntity.Save()) {
3063
- // saved, grab the new ID
3064
- artifactId = artifactEntity.ID;
3065
- }
3066
- else {
3067
- LogError(`Error saving artifact entity for conversation: ${convoEntity.ID}`, undefined, artifactEntity.LatestResult);
3068
- }
3069
- newVersion = 1;
3070
- }
3071
- else {
3072
- // we are updating an existing artifact with a new vesrion so we need to get the old max version and increment it
3073
- const ei = md.EntityByName("MJ: Conversation Artifact Versions");
3074
- const sSQL = `SELECT ISNULL(MAX(Version),0) AS MaxVersion FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ConversationArtifactID = '${artifactId}'`;
3075
- try {
3076
- const request = new mssql.Request(dataSource);
3077
- const result = await request.query(sSQL);
3078
- if (result && result.recordset && result.recordset.length > 0) {
3079
- newVersion = result.recordset[0].MaxVersion + 1;
3080
- }
3081
- else {
3082
- LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, result);
3083
- }
3084
- }
3085
- catch (e) {
3086
- LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, e);
3087
- }
3088
- }
3089
- if (artifactId && newVersion > 0) {
3090
- // only do this if we were provided an artifact ID or we saved a new one above successfully
3091
- const artifactVersionEntity = await md.GetEntityObject<ConversationArtifactVersionEntity>('MJ: Conversation Artifact Versions', user);
3092
- // create the new artifact version here
3093
- artifactVersionEntity.NewRecord();
3094
- artifactVersionEntity.ConversationArtifactID = artifactId;
3095
- artifactVersionEntity.Version = newVersion;
3096
- artifactVersionEntity.Configuration = sResult; // store the full response here
3097
-
3098
- if (await artifactVersionEntity.Save()) {
3099
- // success saving the new version, set the artifactVersionId
3100
- artifactVersionId = artifactVersionEntity.ID;
3101
- }
3102
- else {
3103
- LogError(`Error saving Artifact Version record`)
3104
- }
3105
- }
3106
- }
3107
-
3108
- // Create a conversation detail record for the Skip response
3109
- const endTime = new Date();
3110
- const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
3111
- convoDetailEntityAI.NewRecord();
3112
- convoDetailEntityAI.ConversationID = convoEntity.ID;
3113
- convoDetailEntityAI.Message = sResult;
3114
- convoDetailEntityAI.Role = 'AI';
3115
- convoDetailEntityAI.HiddenToUser = false;
3116
- convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
3117
-
3118
- if (artifactId && artifactId.length > 0) {
3119
- // bind the new convo detail record to the artifact + version for this response
3120
- convoDetailEntityAI.ArtifactID = artifactId;
3121
- if (artifactVersionId && artifactVersionId.length > 0) {
3122
- convoDetailEntityAI.ArtifactVersionID = artifactVersionId;
3123
- }
3124
- }
3125
-
3126
- const convoDetailSaveResult: boolean = await convoDetailEntityAI.Save();
3127
- if (!convoDetailSaveResult) {
3128
- LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
3129
- }
3130
-
3131
- // Update the conversation properties: name if it's the default, and set status back to 'Available'
3132
- let needToSaveConvo = false;
3133
-
3134
- // Update name if still default
3135
- if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
3136
- convoEntity.Name = sTitle; // use the title from the response
3137
- needToSaveConvo = true;
3138
- }
3139
-
3140
- // Set status back to 'Available' since processing is complete
3141
- if (convoEntity.Status === 'Processing') {
3142
- convoEntity.Status = 'Available';
3143
- needToSaveConvo = true;
3144
- }
3145
-
3146
- // Save if any changes were made
3147
- if (needToSaveConvo) {
3148
- const convoEntitySaveResult: boolean = await convoEntity.Save();
3149
- if (!convoEntitySaveResult) {
3150
- LogError(`Error saving conversation entity for AI message: ${sResult}`, undefined, convoEntity.LatestResult);
3151
- }
3152
- }
3153
-
3154
- // now create a notification for the user
3155
- const userNotification = <UserNotificationEntity>await md.GetEntityObject('User Notifications', user);
3156
- userNotification.NewRecord();
3157
- userNotification.UserID = user.ID;
3158
- userNotification.Title = 'Report Created: ' + sTitle;
3159
- userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
3160
- userNotification.Unread = true;
3161
- userNotification.ResourceConfiguration = JSON.stringify({
3162
- type: 'askskip',
3163
- conversationId: convoEntity.ID,
3164
- });
3165
-
3166
- const userNotificationSaveResult: boolean = await userNotification.Save();
3167
- if (!userNotificationSaveResult) {
3168
- LogError(`Error saving user notification entity for AI message: ${sResult}`, undefined, userNotification.LatestResult);
3169
- }
3170
-
3171
- // check to see if Skip retrieved additional data on his own outside of the DATA_REQUEST phase/process. It is possible for Skip to call back
3172
- // to the MJAPI in the instance using the GetData() query in the MJAPI. If Skip did this, we need to save the data context items here.
3173
- if (apiResponse.newDataItems) {
3174
- apiResponse.newDataItems.forEach((skipItem) => {
3175
- const newItem = dataContext.AddDataContextItem();
3176
- newItem.Type = 'sql';
3177
- newItem.SQL = skipItem.text;
3178
- newItem.AdditionalDescription = skipItem.description;
3179
- });
3180
- }
3181
-
3182
- // Save the data context items...
3183
- // 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
3184
- // 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
3185
- await dataContext.SaveItems(user, false);
3186
-
3187
- // send a UI update trhough pub-sub
3188
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
3189
- message: JSON.stringify({
3190
- type: 'UserNotifications',
3191
- status: 'OK',
3192
- conversationID: convoEntity.ID,
3193
- details: {
3194
- action: 'create',
3195
- recordId: userNotification.ID,
3196
- },
3197
- }),
3198
- sessionId: userPayload.sessionId,
3199
- });
3200
-
3201
- return {
3202
- AIMessageConversationDetailID: convoDetailEntityAI.ID,
3203
- };
3204
- }
3205
-
3206
- private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available', userPayload: UserPayload, pubSub?: PubSubEngine): Promise<boolean> {
3207
- if (convoEntity.Status !== status) {
3208
- convoEntity.Status = status;
3209
-
3210
- const convoSaveResult = await convoEntity.Save();
3211
- if (!convoSaveResult) {
3212
- LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
3213
- } else {
3214
- // If conversation is now Available (completed), remove it from active streams
3215
- if (status === 'Available') {
3216
- activeStreams.removeConversation(convoEntity.ID);
3217
- LogStatus(`Removed conversation ${convoEntity.ID} from active streams (status changed to Available)`);
3218
- } else if (status === 'Processing') {
3219
- // If conversation is starting to process, add the session to active streams
3220
- activeStreams.addSession(convoEntity.ID, userPayload.sessionId);
3221
- LogStatus(`Added session ${userPayload.sessionId} to active streams for conversation ${convoEntity.ID}`);
3222
- }
3223
-
3224
- if (pubSub) {
3225
- // Publish status update to notify frontend of conversation status change
3226
- const statusMessage = {
3227
- type: 'ConversationStatusUpdate',
3228
- conversationID: convoEntity.ID,
3229
- status: status,
3230
- timestamp: new Date().toISOString()
3231
- };
3232
-
3233
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
3234
- pushStatusUpdates: {
3235
- message: JSON.stringify(statusMessage),
3236
- sessionId: userPayload.sessionId
3237
- }
3238
- });
3239
-
3240
- LogStatus(`Published conversation status update for ${convoEntity.ID}: ${status}`);
3241
- }
3242
- }
3243
- return convoSaveResult;
3244
- }
3245
- return true;
3246
- }
3247
-
3248
- /**
3249
- * Gets the ID of an agent note type by its name
3250
- * Falls back to a default note type if the specified one is not found
3251
- *
3252
- * @param name Name of the agent note type
3253
- * @param defaultNoteType Default note type to use if the specified one is not found
3254
- * @returns ID of the agent note type
3255
- */
3256
- protected getAgentNoteTypeIDByName(name: string, defaultNoteType: string = 'AI'): string {
3257
- const noteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === name.trim().toLowerCase())?.ID;
3258
- if (noteTypeID) {
3259
- return noteTypeID;
3260
- }
3261
- else{
3262
- // default
3263
- const defaultNoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === defaultNoteType.trim().toLowerCase())?.ID;
3264
- return defaultNoteTypeID;
3265
- }
3266
- }
3267
-
3268
- /**
3269
- * Gets data from a view
3270
- * Helper method to run a view and retrieve its data
3271
- *
3272
- * @param ViewId ID of the view to run
3273
- * @param user User context for the query
3274
- * @returns Results of the view query
3275
- */
3276
- protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
3277
- const rv = new RunView();
3278
- const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
3279
- if (result && result.Success) return result.Results;
3280
- else throw new Error(`Error running view ${ViewId}`);
3281
- }
3282
-
3283
- // /**
3284
- // * Manually executes the Skip AI learning cycle
3285
- // * Allows triggering a learning cycle on demand rather than waiting for scheduled execution
3286
- // *
3287
- // * @param OrganizationId Optional organization ID to register for this run
3288
- // * @returns Result of the manual learning cycle execution
3289
- // */
3290
- // @Mutation(() => ManualLearningCycleResultType)
3291
- // async ManuallyExecuteSkipLearningCycle(
3292
- // @Arg('OrganizationId', () => String, { nullable: true }) OrganizationId?: string
3293
- // ): Promise<ManualLearningCycleResultType> {
3294
- // try {
3295
- // LogStatus('Manual execution of Skip learning cycle requested via API');
3296
- // const skipConfigInfo = configInfo.askSkip;
3297
- // // First check if learning cycles are enabled in configuration
3298
- // if (!skipConfigInfo.learningCycleEnabled) {
3299
- // return {
3300
- // Success: false,
3301
- // Message: 'Learning cycles are not enabled in configuration'
3302
- // };
3303
- // }
3304
-
3305
- // // Check if we have a valid endpoint when cycles are enabled
3306
- // const hasLearningEndpoint = (skipConfigInfo.url && skipConfigInfo.url.trim().length > 0) ||
3307
- // (skipConfigInfo.learningCycleURL && skipConfigInfo.learningCycleURL.trim().length > 0);
3308
- // if (!hasLearningEndpoint) {
3309
- // return {
3310
- // Success: false,
3311
- // Message: 'Learning cycle API endpoint is not configured'
3312
- // };
3313
- // }
3314
-
3315
- // // Use the organization ID from config if not provided
3316
- // const orgId = OrganizationId || skipConfigInfo.orgID;
3317
-
3318
- // // Call the scheduler's manual execution method with org ID
3319
- // const result = await LearningCycleScheduler.Instance.manuallyExecuteLearningCycle(orgId);
3320
-
3321
- // return {
3322
- // Success: result,
3323
- // Message: result
3324
- // ? `Learning cycle was successfully executed manually for organization ${orgId}`
3325
- // : `Learning cycle execution failed for organization ${orgId}. Check server logs for details.`
3326
- // };
3327
- // }
3328
- // catch (e) {
3329
- // LogError(`Error in ManuallyExecuteSkipLearningCycle: ${e}`);
3330
- // return {
3331
- // Success: false,
3332
- // Message: `Error executing learning cycle: ${e}`
3333
- // };
3334
- // }
3335
- // }
3336
-
3337
- // /**
3338
- // * Gets the current status of the learning cycle scheduler
3339
- // * Provides information about the scheduler state and any running cycles
3340
- // *
3341
- // * @returns Status information about the learning cycle scheduler
3342
- // */
3343
- // @Query(() => LearningCycleStatusType)
3344
- // async GetLearningCycleStatus(): Promise<LearningCycleStatusType> {
3345
- // try {
3346
- // const status = LearningCycleScheduler.Instance.getStatus();
3347
-
3348
- // return {
3349
- // IsSchedulerRunning: status.isSchedulerRunning,
3350
- // LastRunTime: status.lastRunTime ? status.lastRunTime.toISOString() : null,
3351
- // RunningOrganizations: status.runningOrganizations ? status.runningOrganizations.map(org => ({
3352
- // OrganizationId: org.organizationId,
3353
- // LearningCycleId: org.learningCycleId,
3354
- // StartTime: org.startTime.toISOString(),
3355
- // RunningForMinutes: org.runningForMinutes
3356
- // })) : []
3357
- // };
3358
- // }
3359
- // catch (e) {
3360
- // LogError(`Error in GetLearningCycleStatus: ${e}`);
3361
- // return {
3362
- // IsSchedulerRunning: false,
3363
- // LastRunTime: null,
3364
- // RunningOrganizations: []
3365
- // };
3366
- // }
3367
- // }
3368
-
3369
- // /**
3370
- // * Checks if a specific organization is running a learning cycle
3371
- // * Used to determine if a new learning cycle can be started for an organization
3372
- // *
3373
- // * @param OrganizationId The organization ID to check
3374
- // * @returns Information about the running cycle, or null if no cycle is running
3375
- // */
3376
- // @Query(() => RunningOrganizationType, { nullable: true })
3377
- // async IsOrganizationRunningLearningCycle(
3378
- // @Arg('OrganizationId', () => String) OrganizationId: string
3379
- // ): Promise<RunningOrganizationType | null> {
3380
- // try {
3381
- // const skipConfigInfo = configInfo.askSkip;
3382
- // // Use the organization ID from config if not provided
3383
- // const orgId = OrganizationId || skipConfigInfo.orgID;
3384
-
3385
- // const status = LearningCycleScheduler.Instance.isOrganizationRunningCycle(orgId);
3386
-
3387
- // if (!status.isRunning) {
3388
- // return null;
3389
- // }
3390
-
3391
- // return {
3392
- // OrganizationId: orgId,
3393
- // LearningCycleId: status.learningCycleId,
3394
- // StartTime: status.startTime.toISOString(),
3395
- // RunningForMinutes: status.runningForMinutes
3396
- // };
3397
- // }
3398
- // catch (e) {
3399
- // LogError(`Error in IsOrganizationRunningLearningCycle: ${e}`);
3400
- // return null;
3401
- // }
3402
- // }
3403
-
3404
- // /**
3405
- // * Stops a running learning cycle for a specific organization
3406
- // * Allows manual intervention to stop a learning cycle that is taking too long or causing issues
3407
- // *
3408
- // * @param OrganizationId The organization ID to stop the cycle for
3409
- // * @returns Result of the stop operation, including details about the stopped cycle
3410
- // */
3411
- // @Mutation(() => StopLearningCycleResultType)
3412
- // async StopLearningCycleForOrganization(
3413
- // @Arg('OrganizationId', () => String) OrganizationId: string
3414
- // ): Promise<StopLearningCycleResultType> {
3415
- // try {
3416
- // // Use the organization ID from config if not provided
3417
- // const orgId = OrganizationId || configInfo.askSkip.orgID;
3418
-
3419
- // const result = LearningCycleScheduler.Instance.stopLearningCycleForOrganization(orgId);
3420
-
3421
- // // Transform the result to match our GraphQL type
3422
- // return {
3423
- // Success: result.success,
3424
- // Message: result.message,
3425
- // WasRunning: result.wasRunning,
3426
- // CycleDetails: result.cycleDetails ? {
3427
- // LearningCycleId: result.cycleDetails.learningCycleId,
3428
- // StartTime: result.cycleDetails.startTime.toISOString(),
3429
- // RunningForMinutes: result.cycleDetails.runningForMinutes
3430
- // } : null
3431
- // };
3432
- // }
3433
- // catch (e) {
3434
- // LogError(`Error in StopLearningCycleForOrganization: ${e}`);
3435
- // return {
3436
- // Success: false,
3437
- // Message: `Error stopping learning cycle: ${e}`,
3438
- // WasRunning: false,
3439
- // CycleDetails: null
3440
- // };
3441
- // }
3442
- // }
3443
-
3444
- }
3445
-
3446
- export default AskSkipResolver;