@memberjunction/server 2.39.0 → 2.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +99 -30
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -13
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +8 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +49 -18
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +11 -10
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +167 -87
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.js +4 -1
- package/dist/scheduler/LearningCycleScheduler.js.map +1 -1
- package/package.json +22 -22
- package/src/config.ts +34 -17
- package/src/generated/generated.ts +31 -12
- package/src/resolvers/AskSkipResolver.ts +700 -151
- package/src/scheduler/LearningCycleScheduler.ts +7 -2
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
UserNotificationEntity,
|
|
54
54
|
} from '@memberjunction/core-entities';
|
|
55
55
|
import { DataSource } from 'typeorm';
|
|
56
|
-
import {
|
|
56
|
+
import { apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
|
|
57
57
|
|
|
58
58
|
import { registerEnumType } from 'type-graphql';
|
|
59
59
|
import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
|
|
@@ -63,11 +63,17 @@ import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
|
|
|
63
63
|
import { AIAgentEntityExtended, AIEngine } from '@memberjunction/aiengine';
|
|
64
64
|
import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
|
|
65
65
|
import e from 'express';
|
|
66
|
-
import { Skip } from '@graphql-tools/utils';
|
|
67
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Enumeration representing the different phases of a Skip response
|
|
69
|
+
* Corresponds to the lifecycle of a Skip AI interaction
|
|
70
|
+
*/
|
|
68
71
|
enum SkipResponsePhase {
|
|
72
|
+
/** Skip is asking for clarification before proceeding */
|
|
69
73
|
ClarifyingQuestion = 'clarifying_question',
|
|
74
|
+
/** Skip is requesting data from the system to process the request */
|
|
70
75
|
DataRequest = 'data_request',
|
|
76
|
+
/** Skip has completed its analysis and has returned a final response */
|
|
71
77
|
AnalysisComplete = 'analysis_complete',
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -76,165 +82,267 @@ registerEnumType(SkipResponsePhase, {
|
|
|
76
82
|
description: 'The phase of the respons: clarifying_question, data_request, or analysis_complete',
|
|
77
83
|
});
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Result type for Skip AI interactions
|
|
87
|
+
* Contains the status of the request, the response phase, the result payload,
|
|
88
|
+
* and references to the conversation and message IDs
|
|
89
|
+
*/
|
|
79
90
|
@ObjectType()
|
|
80
91
|
export class AskSkipResultType {
|
|
92
|
+
/** Whether the interaction was successful */
|
|
81
93
|
@Field(() => Boolean)
|
|
82
94
|
Success: boolean;
|
|
83
95
|
|
|
96
|
+
/** Status message of the interaction */
|
|
84
97
|
@Field(() => String)
|
|
85
98
|
Status: string; // required
|
|
86
99
|
|
|
100
|
+
/** The phase of the response from Skip */
|
|
87
101
|
@Field(() => SkipResponsePhase)
|
|
88
102
|
ResponsePhase: SkipResponsePhase;
|
|
89
103
|
|
|
104
|
+
/** The result payload, usually a JSON string of the full response */
|
|
90
105
|
@Field(() => String)
|
|
91
106
|
Result: string;
|
|
92
107
|
|
|
108
|
+
/** The ID of the conversation this interaction belongs to */
|
|
93
109
|
@Field(() => String)
|
|
94
110
|
ConversationId: string;
|
|
95
111
|
|
|
112
|
+
/** The ID of the user message in the conversation */
|
|
96
113
|
@Field(() => String)
|
|
97
114
|
UserMessageConversationDetailId: string;
|
|
98
115
|
|
|
116
|
+
/** The ID of the AI response message in the conversation */
|
|
99
117
|
@Field(() => String)
|
|
100
118
|
AIMessageConversationDetailId: string;
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Result type for manual learning cycle operations
|
|
123
|
+
* Contains success status and a message describing the result
|
|
124
|
+
*/
|
|
103
125
|
@ObjectType()
|
|
104
126
|
export class ManualLearningCycleResultType {
|
|
127
|
+
/** Whether the learning cycle operation was successful */
|
|
105
128
|
@Field(() => Boolean)
|
|
106
129
|
Success: boolean;
|
|
107
130
|
|
|
131
|
+
/** Descriptive message about the learning cycle operation */
|
|
108
132
|
@Field(() => String)
|
|
109
133
|
Message: string;
|
|
110
134
|
}
|
|
111
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Contains details about a specific learning cycle
|
|
138
|
+
* Includes identifier, start time, and duration information
|
|
139
|
+
*/
|
|
112
140
|
@ObjectType()
|
|
113
141
|
export class CycleDetailsType {
|
|
142
|
+
/** Unique identifier for the learning cycle */
|
|
114
143
|
@Field(() => String)
|
|
115
144
|
LearningCycleId: string;
|
|
116
145
|
|
|
146
|
+
/** ISO timestamp when the cycle started */
|
|
117
147
|
@Field(() => String)
|
|
118
148
|
StartTime: string;
|
|
119
149
|
|
|
150
|
+
/** Duration of the cycle in minutes */
|
|
120
151
|
@Field(() => Number)
|
|
121
152
|
RunningForMinutes: number;
|
|
122
153
|
}
|
|
123
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Information about an organization that is currently running a learning cycle
|
|
157
|
+
* Links organization to specific learning cycle and provides timing details
|
|
158
|
+
*/
|
|
124
159
|
@ObjectType()
|
|
125
160
|
export class RunningOrganizationType {
|
|
161
|
+
/** Identifier of the organization running the cycle */
|
|
126
162
|
@Field(() => String)
|
|
127
163
|
OrganizationId: string;
|
|
128
164
|
|
|
165
|
+
/** Unique identifier for the learning cycle */
|
|
129
166
|
@Field(() => String)
|
|
130
167
|
LearningCycleId: string;
|
|
131
168
|
|
|
169
|
+
/** ISO timestamp when the cycle started */
|
|
132
170
|
@Field(() => String)
|
|
133
171
|
StartTime: string;
|
|
134
172
|
|
|
173
|
+
/** Duration the cycle has been running in minutes */
|
|
135
174
|
@Field(() => Number)
|
|
136
175
|
RunningForMinutes: number;
|
|
137
176
|
}
|
|
138
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Status information about the learning cycle scheduler and running cycles
|
|
180
|
+
* Provides overall scheduler status and details about active learning cycles
|
|
181
|
+
*/
|
|
139
182
|
@ObjectType()
|
|
140
183
|
export class LearningCycleStatusType {
|
|
184
|
+
/** Whether the scheduler process is currently active */
|
|
141
185
|
@Field(() => Boolean)
|
|
142
186
|
IsSchedulerRunning: boolean;
|
|
143
187
|
|
|
188
|
+
/** ISO timestamp of the last time the scheduler ran a cycle */
|
|
144
189
|
@Field(() => String, { nullable: true })
|
|
145
190
|
LastRunTime: string;
|
|
146
191
|
|
|
192
|
+
/** List of organizations that are currently running learning cycles */
|
|
147
193
|
@Field(() => [RunningOrganizationType], { nullable: true })
|
|
148
194
|
RunningOrganizations: RunningOrganizationType[];
|
|
149
195
|
}
|
|
150
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Result of an attempt to stop a learning cycle
|
|
199
|
+
* Provides status information about the stop operation
|
|
200
|
+
*/
|
|
151
201
|
@ObjectType()
|
|
152
202
|
export class StopLearningCycleResultType {
|
|
203
|
+
/** Whether the stop operation succeeded */
|
|
153
204
|
@Field(() => Boolean)
|
|
154
205
|
Success: boolean;
|
|
155
206
|
|
|
207
|
+
/** Descriptive message about the result of the stop operation */
|
|
156
208
|
@Field(() => String)
|
|
157
209
|
Message: string;
|
|
158
210
|
|
|
211
|
+
/** Whether the cycle was actually running when the stop was attempted */
|
|
159
212
|
@Field(() => Boolean)
|
|
160
213
|
WasRunning: boolean;
|
|
161
214
|
|
|
215
|
+
/** Details about the cycle that was stopped (if any) */
|
|
162
216
|
@Field(() => CycleDetailsType, { nullable: true })
|
|
163
217
|
CycleDetails: CycleDetailsType;
|
|
164
218
|
}
|
|
165
219
|
|
|
166
220
|
/**
|
|
167
|
-
*
|
|
221
|
+
* 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.
|
|
222
|
+
*/
|
|
223
|
+
function initializeSkipLearningCycleScheduler() {
|
|
224
|
+
try {
|
|
225
|
+
// Set up event listener for server initialization
|
|
226
|
+
const eventListener = MJGlobal.Instance.GetEventListener(true);
|
|
227
|
+
eventListener.subscribe(event => {
|
|
228
|
+
// Filter for our server's setup complete event
|
|
229
|
+
if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
|
|
230
|
+
try {
|
|
231
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
232
|
+
if (!skipConfigInfo) {
|
|
233
|
+
LogStatus('Skip AI Learning Cycle Scheduler not started: Skip configuration not found');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!skipConfigInfo.learningCycleEnabled) {
|
|
237
|
+
LogStatus('Skip AI Learning Cycles not enabled in configuration');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if we have a valid endpoint when cycles are enabled
|
|
242
|
+
if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
|
|
243
|
+
LogError('Skip AI Learning cycle scheduler not started: Learning cycles are enabled but no Learning Cycle API endpoint is configured');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const dataSources = event.args.dataSources;
|
|
248
|
+
if (dataSources && dataSources.length > 0) {
|
|
249
|
+
// Initialize the scheduler
|
|
250
|
+
const scheduler = LearningCycleScheduler.Instance;
|
|
251
|
+
|
|
252
|
+
// Set the data sources for the scheduler
|
|
253
|
+
scheduler.setDataSources(dataSources);
|
|
254
|
+
|
|
255
|
+
// Default is 60 minutes, if the interval is not set in the config, use 60 minutes
|
|
256
|
+
const interval = skipConfigInfo.learningCycleIntervalInMinutes ?? 60;
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if (skipConfigInfo.learningCycleRunUponStartup) {
|
|
260
|
+
// If configured to run immediately, run the learning cycle
|
|
261
|
+
LogStatus('Skip API Learning Cycle: Run Upon Startup is enabled, running learning cycle immediately');
|
|
262
|
+
// Start the scheduler
|
|
263
|
+
scheduler.start(interval);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// not asked to start right away, just start the scheduler after the interval
|
|
267
|
+
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.`);
|
|
268
|
+
|
|
269
|
+
// create a one time timer to start the scheduler
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
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.`);
|
|
272
|
+
scheduler.start(interval);
|
|
273
|
+
}, interval * 60 * 1000); // convert minutes to milliseconds
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
LogError(`Error initializing Skip learning cycle scheduler: ${error}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// Handle any errors from the static initializer
|
|
285
|
+
LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// now call the function to initialize the scheduler
|
|
289
|
+
initializeSkipLearningCycleScheduler();
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Base type for Skip API requests containing common fields
|
|
293
|
+
* Used as the foundation for both chat and learning cycle requests
|
|
168
294
|
*/
|
|
169
295
|
type BaseSkipRequest = {
|
|
296
|
+
/** Entity metadata to send to Skip */
|
|
170
297
|
entities: SkipEntityInfo[],
|
|
298
|
+
/** Query metadata to send to Skip */
|
|
171
299
|
queries: SkipQueryInfo[],
|
|
300
|
+
/** Agent notes to send to Skip */
|
|
172
301
|
notes: SkipAPIAgentNote[],
|
|
302
|
+
/** Note type definitions to send to Skip */
|
|
173
303
|
noteTypes: SkipAPIAgentNoteType[],
|
|
304
|
+
/** Agent requests to send to Skip */
|
|
174
305
|
requests: SkipAPIAgentRequest[],
|
|
306
|
+
/** Access token for authorizing Skip to call back to MemberJunction */
|
|
175
307
|
accessToken: GetDataAccessToken,
|
|
308
|
+
/** Organization identifier */
|
|
176
309
|
organizationID: string,
|
|
310
|
+
/** Additional organization-specific information */
|
|
177
311
|
organizationInfo: any,
|
|
312
|
+
/** API keys for various AI services to be used by Skip */
|
|
178
313
|
apiKeys: SkipAPIRequestAPIKey[],
|
|
314
|
+
/** URL of the calling server for callback purposes */
|
|
179
315
|
callingServerURL: string,
|
|
316
|
+
/** API key for the calling server */
|
|
180
317
|
callingServerAPIKey: string,
|
|
318
|
+
/** Access token for the calling server */
|
|
181
319
|
callingServerAccessToken: string
|
|
182
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Resolver for Skip AI interactions
|
|
323
|
+
* Handles conversations with Skip, learning cycles, and related operations.
|
|
324
|
+
* Skip is an AI agent that can analyze data, answer questions, and learn from interactions.
|
|
325
|
+
*/
|
|
183
326
|
@Resolver(AskSkipResultType)
|
|
184
327
|
export class AskSkipResolver {
|
|
328
|
+
/** Default name for new conversations */
|
|
185
329
|
private static _defaultNewChatName = 'New Chat';
|
|
186
330
|
|
|
187
|
-
|
|
188
|
-
static {
|
|
189
|
-
try {
|
|
190
|
-
// Set up event listener for server initialization
|
|
191
|
-
const eventListener = MJGlobal.Instance.GetEventListener(true);
|
|
192
|
-
eventListener.subscribe(event => {
|
|
193
|
-
// Filter for our server's setup complete event
|
|
194
|
-
if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
|
|
195
|
-
try {
|
|
196
|
-
if (___skipRunLearningCycles !== 'Y') {
|
|
197
|
-
LogStatus('Skip AI Learning cycle scheduler not started: Disabled in configuration');
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check if we have a valid endpoint when cycles are enabled
|
|
202
|
-
if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
|
|
203
|
-
LogError('Skip AI Learning cycle scheduler not started: Learning cycles are enabled but no API endpoint is configured');
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const dataSources = event.args.dataSources;
|
|
208
|
-
if (dataSources && dataSources.length > 0) {
|
|
209
|
-
// Initialize the scheduler
|
|
210
|
-
const scheduler = LearningCycleScheduler.Instance;
|
|
211
|
-
|
|
212
|
-
// Set the data sources for the scheduler
|
|
213
|
-
scheduler.setDataSources(dataSources);
|
|
214
|
-
|
|
215
|
-
// Default is 60 minutes, if the interval is not set in the config, use 60 minutes
|
|
216
|
-
const interval = ___skipLearningCycleIntervalInMinutes ?? 60;
|
|
217
|
-
scheduler.start(interval);
|
|
218
|
-
} else {
|
|
219
|
-
LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
|
|
220
|
-
}
|
|
221
|
-
} catch (error) {
|
|
222
|
-
LogError(`Error initializing Skip learning cycle scheduler: ${error}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
} catch (error) {
|
|
227
|
-
// Handle any errors from the static initializer
|
|
228
|
-
LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
331
|
+
/** Maximum number of historical messages to include in a conversation context */
|
|
231
332
|
private static _maxHistoricalMessages = 30;
|
|
232
333
|
|
|
233
334
|
/**
|
|
234
|
-
* Handles a
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* @param
|
|
335
|
+
* Handles a chat interaction with Skip about a specific data record
|
|
336
|
+
* Allows users to ask questions about a particular entity record
|
|
337
|
+
*
|
|
338
|
+
* @param UserQuestion The question or message from the user
|
|
339
|
+
* @param ConversationId ID of an existing conversation, or empty for a new conversation
|
|
340
|
+
* @param EntityName The name of the entity the record belongs to
|
|
341
|
+
* @param compositeKey The primary key values that identify the specific record
|
|
342
|
+
* @param dataSource Database connection
|
|
343
|
+
* @param userPayload Information about the authenticated user
|
|
344
|
+
* @param pubSub Publisher/subscriber for events
|
|
345
|
+
* @returns Result of the Skip interaction
|
|
238
346
|
*/
|
|
239
347
|
@Query(() => AskSkipResultType)
|
|
240
348
|
async ExecuteAskSkipRecordChat(
|
|
@@ -309,16 +417,26 @@ export class AskSkipResolver {
|
|
|
309
417
|
return this.handleSimpleSkipChatPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
|
|
310
418
|
}
|
|
311
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Executes a Skip learning cycle
|
|
422
|
+
* Learning cycles allow Skip to analyze conversations and improve its knowledge and capabilities
|
|
423
|
+
*
|
|
424
|
+
* @param dataSource Database connection
|
|
425
|
+
* @param userPayload Information about the authenticated user
|
|
426
|
+
* @param ForceEntityRefresh Whether to force a refresh of entity metadata
|
|
427
|
+
* @returns Result of the learning cycle execution
|
|
428
|
+
*/
|
|
312
429
|
@Mutation(() => AskSkipResultType)
|
|
313
430
|
async ExecuteAskSkipLearningCycle(
|
|
314
431
|
@Ctx() { dataSource, userPayload }: AppContext,
|
|
315
432
|
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
|
|
316
433
|
) {
|
|
434
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
317
435
|
// First check if learning cycles are enabled in configuration
|
|
318
|
-
if (
|
|
436
|
+
if (!skipConfigInfo.learningCycleEnabled) {
|
|
319
437
|
return {
|
|
320
438
|
success: false,
|
|
321
|
-
error: 'Learning cycles are
|
|
439
|
+
error: 'Learning cycles are not enabled in configuration',
|
|
322
440
|
elapsedTime: 0,
|
|
323
441
|
noteChanges: [],
|
|
324
442
|
queryChanges: [],
|
|
@@ -327,7 +445,7 @@ export class AskSkipResolver {
|
|
|
327
445
|
}
|
|
328
446
|
|
|
329
447
|
// Check if we have a valid endpoint when cycles are enabled
|
|
330
|
-
if (!
|
|
448
|
+
if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
|
|
331
449
|
return {
|
|
332
450
|
success: false,
|
|
333
451
|
error: 'Learning cycle API endpoint is not configured',
|
|
@@ -347,7 +465,7 @@ export class AskSkipResolver {
|
|
|
347
465
|
await AIEngine.Instance.Config(false, user);
|
|
348
466
|
|
|
349
467
|
// Check if this organization is already running a learning cycle using their organization ID
|
|
350
|
-
const organizationId =
|
|
468
|
+
const organizationId = skipConfigInfo.orgID;
|
|
351
469
|
const scheduler = LearningCycleScheduler.Instance;
|
|
352
470
|
const runningStatus = scheduler.isOrganizationRunningCycle(organizationId);
|
|
353
471
|
|
|
@@ -396,55 +514,91 @@ export class AskSkipResolver {
|
|
|
396
514
|
// Build the request to Skip learning API
|
|
397
515
|
LogStatus(`Building Skip Learning API request`);
|
|
398
516
|
const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, false, dataSource, user, ForceEntityRefresh || false);
|
|
517
|
+
if (input.newConversations.length === 0) {
|
|
518
|
+
// no new conversations to process
|
|
519
|
+
LogStatus(` Skip Learning Cycles: No new conversations to process for learning cycle`);
|
|
520
|
+
learningCycleEntity.Status = 'Complete';
|
|
521
|
+
learningCycleEntity.AgentSummary = 'No new conversations to process, learning cycle skipped, but recorded for audit purposes.';
|
|
522
|
+
learningCycleEntity.EndedAt = new Date();
|
|
523
|
+
if (!(await learningCycleEntity.Save())) {
|
|
524
|
+
LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
|
|
525
|
+
}
|
|
526
|
+
const result: SkipAPILearningCycleResponse = {
|
|
527
|
+
success: true,
|
|
528
|
+
learningCycleSkipped: true,
|
|
529
|
+
elapsedTime: 0,
|
|
530
|
+
noteChanges: [],
|
|
531
|
+
queryChanges: [],
|
|
532
|
+
requestChanges: [],
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Make the API request
|
|
538
|
+
const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID);
|
|
399
539
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// Update learning cycle to completed
|
|
404
|
-
const endTime = new Date();
|
|
405
|
-
const elapsedTimeMs = endTime.getTime() - startTime.getTime();
|
|
540
|
+
// Update learning cycle to completed
|
|
541
|
+
const endTime = new Date();
|
|
542
|
+
const elapsedTimeMs = endTime.getTime() - startTime.getTime();
|
|
406
543
|
|
|
407
|
-
|
|
544
|
+
LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
|
|
408
545
|
|
|
409
|
-
|
|
410
|
-
|
|
546
|
+
learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
|
|
547
|
+
learningCycleEntity.EndedAt = endTime;
|
|
411
548
|
|
|
412
|
-
|
|
413
|
-
|
|
549
|
+
if (!(await learningCycleEntity.Save())) {
|
|
550
|
+
LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return response;
|
|
414
554
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
scheduler.unregisterRunningCycle(organizationId);
|
|
418
|
-
|
|
419
|
-
return response;
|
|
420
|
-
} catch (error) {
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
421
557
|
// Make sure to update the learning cycle record as failed
|
|
422
558
|
learningCycleEntity.Status = 'Failed';
|
|
423
559
|
learningCycleEntity.EndedAt = new Date();
|
|
424
560
|
|
|
425
561
|
try {
|
|
426
562
|
await learningCycleEntity.Save();
|
|
427
|
-
}
|
|
563
|
+
}
|
|
564
|
+
catch (saveError) {
|
|
428
565
|
LogError(`Failed to update learning cycle record after error: ${saveError}`);
|
|
429
566
|
}
|
|
430
567
|
|
|
431
|
-
// Unregister the organization on error
|
|
432
|
-
scheduler.unregisterRunningCycle(organizationId);
|
|
433
|
-
|
|
434
568
|
// Re-throw the original error
|
|
435
569
|
throw error;
|
|
436
570
|
}
|
|
571
|
+
finally {
|
|
572
|
+
// Unregister the cycle/organizationId safely
|
|
573
|
+
try {
|
|
574
|
+
scheduler.unregisterRunningCycle(organizationId);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
LogError(`Failed to unregister organization ${organizationId} from running cycles: ${error}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
437
580
|
}
|
|
438
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Handles the HTTP POST request to the Skip learning cycle API
|
|
584
|
+
* Sends the learning cycle request and processes the response
|
|
585
|
+
*
|
|
586
|
+
* @param input The learning cycle request payload
|
|
587
|
+
* @param user User context for the request
|
|
588
|
+
* @param learningCycleId ID of the current learning cycle
|
|
589
|
+
* @param agentID ID of the Skip agent
|
|
590
|
+
* @returns Response from the Skip learning cycle API
|
|
591
|
+
*/
|
|
439
592
|
protected async handleSimpleSkipLearningPostRequest(
|
|
440
593
|
input: SkipAPILearningCycleRequest,
|
|
441
594
|
user: UserInfo,
|
|
442
595
|
learningCycleId: string,
|
|
443
596
|
agentID: string
|
|
444
597
|
): Promise<SkipAPILearningCycleResponse> {
|
|
445
|
-
|
|
598
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
599
|
+
LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${skipConfigInfo.learningCycleURL}`);
|
|
446
600
|
|
|
447
|
-
const response = await sendPostRequest(
|
|
601
|
+
const response = await sendPostRequest(skipConfigInfo.learningCycleURL, input, true, null);
|
|
448
602
|
|
|
449
603
|
if (response && response.length > 0) {
|
|
450
604
|
// the last object in the response array is the final response from the Skip API
|
|
@@ -482,6 +636,17 @@ export class AskSkipResolver {
|
|
|
482
636
|
}
|
|
483
637
|
}
|
|
484
638
|
|
|
639
|
+
/**
|
|
640
|
+
* Handles the HTTP POST request to the Skip chat API
|
|
641
|
+
* Sends the chat request and processes the response
|
|
642
|
+
*
|
|
643
|
+
* @param input The chat request payload
|
|
644
|
+
* @param conversationID ID of the conversation, or empty for a new conversation
|
|
645
|
+
* @param UserMessageConversationDetailId ID of the user's message in the conversation
|
|
646
|
+
* @param createAIMessageConversationDetail Whether to create a conversation detail for the AI response
|
|
647
|
+
* @param user User context for the request
|
|
648
|
+
* @returns Result of the Skip interaction
|
|
649
|
+
*/
|
|
485
650
|
protected async handleSimpleSkipChatPostRequest(
|
|
486
651
|
input: SkipAPIRequest,
|
|
487
652
|
conversationID: string = '',
|
|
@@ -489,9 +654,10 @@ export class AskSkipResolver {
|
|
|
489
654
|
createAIMessageConversationDetail: boolean = false,
|
|
490
655
|
user: UserInfo = null
|
|
491
656
|
): Promise<AskSkipResultType> {
|
|
492
|
-
|
|
657
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
658
|
+
LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${skipConfigInfo.chatURL}`);
|
|
493
659
|
|
|
494
|
-
const response = await sendPostRequest(
|
|
660
|
+
const response = await sendPostRequest(skipConfigInfo.chatURL, input, true, null);
|
|
495
661
|
|
|
496
662
|
if (response && response.length > 0) {
|
|
497
663
|
// the last object in the response array is the final response from the Skip API
|
|
@@ -524,9 +690,13 @@ export class AskSkipResolver {
|
|
|
524
690
|
}
|
|
525
691
|
|
|
526
692
|
/**
|
|
527
|
-
* Processes note changes received from the Skip API learning cycle
|
|
693
|
+
* Processes note changes received from the Skip API learning cycle
|
|
694
|
+
* Applies changes to agent notes based on the learning cycle response
|
|
695
|
+
*
|
|
528
696
|
* @param noteChanges Changes to agent notes
|
|
529
|
-
* @param
|
|
697
|
+
* @param agentID ID of the Skip agent
|
|
698
|
+
* @param user User context for the request
|
|
699
|
+
* @returns Promise that resolves when processing is complete
|
|
530
700
|
*/
|
|
531
701
|
protected async processLearningCycleNoteChanges(
|
|
532
702
|
noteChanges: SkipLearningCycleNoteChange[],
|
|
@@ -560,6 +730,15 @@ export class AskSkipResolver {
|
|
|
560
730
|
}));
|
|
561
731
|
}
|
|
562
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Processes an add or update operation for a Skip agent note
|
|
735
|
+
* Creates a new note or updates an existing one based on the change type
|
|
736
|
+
*
|
|
737
|
+
* @param change The note change information
|
|
738
|
+
* @param agentID ID of the Skip agent
|
|
739
|
+
* @param user User context for the operation
|
|
740
|
+
* @returns Whether the operation was successful
|
|
741
|
+
*/
|
|
563
742
|
protected async processAddOrUpdateSkipNote(change: SkipLearningCycleNoteChange, agentID: string, user: UserInfo): Promise<boolean> {
|
|
564
743
|
try {
|
|
565
744
|
// Get the note entity object
|
|
@@ -574,17 +753,11 @@ export class AskSkipResolver {
|
|
|
574
753
|
return false;
|
|
575
754
|
}
|
|
576
755
|
} else {
|
|
577
|
-
// For new notes, ensure the note type is not "Human"
|
|
578
|
-
if (change.note.agentNoteType === "Human") {
|
|
579
|
-
LogStatus(`WARNING: Cannot create a new Human note with the learning cycle. Operation ignored.`);
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
756
|
// Create a new note
|
|
584
757
|
noteEntity.NewRecord();
|
|
585
758
|
noteEntity.AgentID = agentID;
|
|
586
759
|
}
|
|
587
|
-
noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI');
|
|
760
|
+
noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI'); // always set to AI
|
|
588
761
|
noteEntity.Note = change.note.note;
|
|
589
762
|
noteEntity.Type = change.note.type;
|
|
590
763
|
|
|
@@ -605,6 +778,14 @@ export class AskSkipResolver {
|
|
|
605
778
|
}
|
|
606
779
|
}
|
|
607
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Processes a delete operation for a Skip agent note
|
|
783
|
+
* Removes the specified note from the database
|
|
784
|
+
*
|
|
785
|
+
* @param change The note change information
|
|
786
|
+
* @param user User context for the operation
|
|
787
|
+
* @returns Whether the deletion was successful
|
|
788
|
+
*/
|
|
608
789
|
protected async processDeleteSkipNote(change: SkipLearningCycleNoteChange, user: UserInfo): Promise<boolean> {
|
|
609
790
|
// Get the note entity object
|
|
610
791
|
const md = new Metadata();
|
|
@@ -634,6 +815,15 @@ cycle.`);
|
|
|
634
815
|
return true;
|
|
635
816
|
}
|
|
636
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Creates a conversation detail entry for an AI message
|
|
820
|
+
* Stores the AI response in the conversation history
|
|
821
|
+
*
|
|
822
|
+
* @param apiResponse The response from the Skip API
|
|
823
|
+
* @param conversationID ID of the conversation
|
|
824
|
+
* @param user User context for the operation
|
|
825
|
+
* @returns ID of the created conversation detail, or empty string if creation failed
|
|
826
|
+
*/
|
|
637
827
|
protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: string, user: UserInfo): Promise<string> {
|
|
638
828
|
const md = new Metadata();
|
|
639
829
|
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
@@ -658,11 +848,14 @@ cycle.`);
|
|
|
658
848
|
|
|
659
849
|
/**
|
|
660
850
|
* Builds the base Skip API request with common fields and data
|
|
851
|
+
* Creates the foundation for both chat and learning cycle requests
|
|
852
|
+
*
|
|
661
853
|
* @param contextUser The user making the request
|
|
662
854
|
* @param dataSource The data source to use
|
|
663
855
|
* @param includeEntities Whether to include entities in the request
|
|
664
856
|
* @param includeQueries Whether to include queries in the request
|
|
665
857
|
* @param includeNotes Whether to include agent notes in the request
|
|
858
|
+
* @param includeRequests Whether to include agent requests in the request
|
|
666
859
|
* @param forceEntitiesRefresh Whether to force refresh of entities
|
|
667
860
|
* @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
|
|
668
861
|
* @param additionalTokenInfo Additional info to include in the access token
|
|
@@ -674,15 +867,16 @@ cycle.`);
|
|
|
674
867
|
includeEntities: boolean,
|
|
675
868
|
includeQueries: boolean,
|
|
676
869
|
includeNotes: boolean,
|
|
870
|
+
filterUserNotesToContextUser: boolean,
|
|
677
871
|
includeRequests: boolean,
|
|
678
872
|
forceEntitiesRefresh: boolean = false,
|
|
679
873
|
includeCallBackKeyAndAccessToken: boolean = false,
|
|
680
874
|
additionalTokenInfo: any = {}
|
|
681
875
|
): Promise<BaseSkipRequest> {
|
|
682
|
-
|
|
876
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
683
877
|
const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
|
|
684
878
|
const queries = includeQueries ? this.BuildSkipQueries() : [];
|
|
685
|
-
const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser) : {notes: [], noteTypes: []};
|
|
879
|
+
const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser, filterUserNotesToContextUser) : {notes: [], noteTypes: []};
|
|
686
880
|
const requests = includeRequests ? await this.BuildSkipRequests(contextUser) : [];
|
|
687
881
|
|
|
688
882
|
// Setup access token if needed
|
|
@@ -710,7 +904,7 @@ cycle.`);
|
|
|
710
904
|
noteTypes,
|
|
711
905
|
requests,
|
|
712
906
|
accessToken,
|
|
713
|
-
organizationID:
|
|
907
|
+
organizationID: skipConfigInfo.orgID,
|
|
714
908
|
organizationInfo: configInfo?.askSkip?.organizationInfo,
|
|
715
909
|
apiKeys: this.buildSkipAPIKeys(),
|
|
716
910
|
callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
|
|
@@ -721,6 +915,19 @@ cycle.`);
|
|
|
721
915
|
|
|
722
916
|
/**
|
|
723
917
|
* Builds the learning API request for Skip
|
|
918
|
+
* Creates a request specific to the learning cycle operation
|
|
919
|
+
*
|
|
920
|
+
* @param learningCycleId ID of the current learning cycle
|
|
921
|
+
* @param lastLearningCycleDate Date of the last completed learning cycle
|
|
922
|
+
* @param includeEntities Whether to include entities in the request
|
|
923
|
+
* @param includeQueries Whether to include queries in the request
|
|
924
|
+
* @param includeNotes Whether to include agent notes in the request
|
|
925
|
+
* @param includeRequests Whether to include agent requests in the request
|
|
926
|
+
* @param dataSource Database connection
|
|
927
|
+
* @param contextUser User context for the request
|
|
928
|
+
* @param forceEntitiesRefresh Whether to force refresh of entities
|
|
929
|
+
* @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
|
|
930
|
+
* @returns Complete learning cycle request object
|
|
724
931
|
*/
|
|
725
932
|
protected async buildSkipLearningAPIRequest(
|
|
726
933
|
learningCycleId: string,
|
|
@@ -741,6 +948,7 @@ cycle.`);
|
|
|
741
948
|
includeEntities,
|
|
742
949
|
includeQueries,
|
|
743
950
|
includeNotes,
|
|
951
|
+
false,
|
|
744
952
|
includeRequests,
|
|
745
953
|
forceEntitiesRefresh,
|
|
746
954
|
includeCallBackKeyAndAccessToken
|
|
@@ -768,11 +976,14 @@ cycle.`);
|
|
|
768
976
|
}
|
|
769
977
|
|
|
770
978
|
/**
|
|
771
|
-
* Loads the conversations that have
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* @param
|
|
775
|
-
|
|
979
|
+
* Loads the conversations that have been updated or added since the last learning cycle
|
|
980
|
+
* These are used to train Skip and improve its understanding
|
|
981
|
+
*
|
|
982
|
+
* @param lastLearningCycleDate The date of the last learning cycle
|
|
983
|
+
* @param dataSource Database connection
|
|
984
|
+
* @param contextUser User context for the request
|
|
985
|
+
* @returns Array of conversations that are new or have been updated since the last cycle
|
|
986
|
+
*/
|
|
776
987
|
protected async BuildSkipLearningCycleNewConversations(
|
|
777
988
|
lastLearningCycleDate: Date,
|
|
778
989
|
dataSource: DataSource,
|
|
@@ -813,9 +1024,11 @@ cycle.`);
|
|
|
813
1024
|
}
|
|
814
1025
|
|
|
815
1026
|
/**
|
|
816
|
-
* Builds an array of agent requests
|
|
817
|
-
*
|
|
818
|
-
*
|
|
1027
|
+
* Builds an array of agent requests
|
|
1028
|
+
* These are requests that have been made to the AI agent
|
|
1029
|
+
*
|
|
1030
|
+
* @param contextUser User context for loading the requests
|
|
1031
|
+
* @returns Array of agent request objects
|
|
819
1032
|
*/
|
|
820
1033
|
protected async BuildSkipRequests(
|
|
821
1034
|
contextUser: UserInfo
|
|
@@ -852,6 +1065,14 @@ cycle.`);
|
|
|
852
1065
|
}
|
|
853
1066
|
}
|
|
854
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Gets the date of the last complete learning cycle for the Skip agent
|
|
1070
|
+
* Used to determine which data to include in the next learning cycle
|
|
1071
|
+
*
|
|
1072
|
+
* @param agentID ID of the Skip agent
|
|
1073
|
+
* @param user User context for the query
|
|
1074
|
+
* @returns Date of the last complete learning cycle, or epoch if none exists
|
|
1075
|
+
*/
|
|
855
1076
|
protected async GetLastCompleteLearningCycleDate(agentID: string, user: UserInfo): Promise<Date> {
|
|
856
1077
|
const md = new Metadata();
|
|
857
1078
|
const rv = new RunView();
|
|
@@ -877,6 +1098,21 @@ cycle.`);
|
|
|
877
1098
|
|
|
878
1099
|
/**
|
|
879
1100
|
* Builds the chat API request for Skip
|
|
1101
|
+
* Creates a request specific to a chat interaction
|
|
1102
|
+
*
|
|
1103
|
+
* @param messages Array of messages in the conversation
|
|
1104
|
+
* @param conversationId ID of the conversation
|
|
1105
|
+
* @param dataContext Data context associated with the conversation
|
|
1106
|
+
* @param requestPhase The phase of the request (initial, clarifying, etc.)
|
|
1107
|
+
* @param includeEntities Whether to include entities in the request
|
|
1108
|
+
* @param includeQueries Whether to include queries in the request
|
|
1109
|
+
* @param includeNotes Whether to include agent notes in the request
|
|
1110
|
+
* @param includeRequests Whether to include agent requests in the request
|
|
1111
|
+
* @param contextUser User context for the request
|
|
1112
|
+
* @param dataSource Database connection
|
|
1113
|
+
* @param forceEntitiesRefresh Whether to force refresh of entities
|
|
1114
|
+
* @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
|
|
1115
|
+
* @returns Complete chat request object
|
|
880
1116
|
*/
|
|
881
1117
|
protected async buildSkipChatAPIRequest(
|
|
882
1118
|
messages: SkipMessage[],
|
|
@@ -905,6 +1141,7 @@ cycle.`);
|
|
|
905
1141
|
includeEntities,
|
|
906
1142
|
includeQueries,
|
|
907
1143
|
includeNotes,
|
|
1144
|
+
true,
|
|
908
1145
|
includeRequests,
|
|
909
1146
|
forceEntitiesRefresh,
|
|
910
1147
|
includeCallBackKeyAndAccessToken,
|
|
@@ -927,12 +1164,13 @@ cycle.`);
|
|
|
927
1164
|
}
|
|
928
1165
|
|
|
929
1166
|
/**
|
|
930
|
-
* Builds up an array of
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* @param
|
|
934
|
-
* @param
|
|
935
|
-
* @
|
|
1167
|
+
* Builds up an array of artifacts associated with a conversation
|
|
1168
|
+
* Artifacts are content or documents generated during conversations
|
|
1169
|
+
*
|
|
1170
|
+
* @param contextUser User context for the query
|
|
1171
|
+
* @param dataSource Database connection
|
|
1172
|
+
* @param conversationId ID of the conversation
|
|
1173
|
+
* @returns Array of artifacts associated with the conversation
|
|
936
1174
|
*/
|
|
937
1175
|
protected async buildSkipAPIArtifacts(contextUser: UserInfo, dataSource: DataSource, conversationId: string): Promise<SkipAPIArtifact[]> {
|
|
938
1176
|
const md = new Metadata();
|
|
@@ -1007,10 +1245,15 @@ cycle.`);
|
|
|
1007
1245
|
|
|
1008
1246
|
|
|
1009
1247
|
/**
|
|
1010
|
-
* Executes a script in the context of a data context
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
* @param
|
|
1248
|
+
* Executes a script in the context of a data context
|
|
1249
|
+
* Allows running code against data context objects
|
|
1250
|
+
*
|
|
1251
|
+
* @param dataSource Database connection
|
|
1252
|
+
* @param userPayload Information about the authenticated user
|
|
1253
|
+
* @param pubSub Publisher/subscriber for events
|
|
1254
|
+
* @param DataContextId ID of the data context to run the script against
|
|
1255
|
+
* @param ScriptText The script to execute
|
|
1256
|
+
* @returns Result of the script execution
|
|
1014
1257
|
*/
|
|
1015
1258
|
@Query(() => AskSkipResultType)
|
|
1016
1259
|
async ExecuteAskSkipRunScript(
|
|
@@ -1028,6 +1271,12 @@ cycle.`);
|
|
|
1028
1271
|
return this.handleSimpleSkipChatPostRequest(input);
|
|
1029
1272
|
}
|
|
1030
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Builds the array of API keys for various AI services
|
|
1276
|
+
* These are used by Skip to call external AI services
|
|
1277
|
+
*
|
|
1278
|
+
* @returns Array of API keys for different vendor services
|
|
1279
|
+
*/
|
|
1031
1280
|
protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
|
|
1032
1281
|
return [
|
|
1033
1282
|
{
|
|
@@ -1053,6 +1302,19 @@ cycle.`);
|
|
|
1053
1302
|
];
|
|
1054
1303
|
}
|
|
1055
1304
|
|
|
1305
|
+
/**
|
|
1306
|
+
* Executes an analysis query with Skip
|
|
1307
|
+
* This is the primary entry point for general Skip conversations
|
|
1308
|
+
*
|
|
1309
|
+
* @param UserQuestion The question or message from the user
|
|
1310
|
+
* @param ConversationId ID of an existing conversation, or empty for a new conversation
|
|
1311
|
+
* @param dataSource Database connection
|
|
1312
|
+
* @param userPayload Information about the authenticated user
|
|
1313
|
+
* @param pubSub Publisher/subscriber for events
|
|
1314
|
+
* @param DataContextId Optional ID of a data context to use
|
|
1315
|
+
* @param ForceEntityRefresh Whether to force a refresh of entity metadata
|
|
1316
|
+
* @returns Result of the Skip interaction
|
|
1317
|
+
*/
|
|
1056
1318
|
@Query(() => AskSkipResultType)
|
|
1057
1319
|
async ExecuteAskSkipAnalysisQuery(
|
|
1058
1320
|
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
@@ -1060,12 +1322,16 @@ cycle.`);
|
|
|
1060
1322
|
@Ctx() { dataSource, userPayload }: AppContext,
|
|
1061
1323
|
@PubSub() pubSub: PubSubEngine,
|
|
1062
1324
|
@Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
|
|
1063
|
-
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
|
|
1325
|
+
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean,
|
|
1326
|
+
@Arg('StartTime', () => Date, { nullable: true }) StartTime?: Date
|
|
1064
1327
|
) {
|
|
1065
1328
|
const md = new Metadata();
|
|
1066
1329
|
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
1067
1330
|
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
1068
1331
|
|
|
1332
|
+
// Record the start time if not provided
|
|
1333
|
+
const requestStartTime = StartTime || new Date();
|
|
1334
|
+
|
|
1069
1335
|
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
|
|
1070
1336
|
dataSource,
|
|
1071
1337
|
ConversationId,
|
|
@@ -1076,6 +1342,9 @@ cycle.`);
|
|
|
1076
1342
|
DataContextId
|
|
1077
1343
|
);
|
|
1078
1344
|
|
|
1345
|
+
// Set the conversation status to 'Processing' when a request is initiated
|
|
1346
|
+
this.setConversationStatus(convoEntity, 'Processing');
|
|
1347
|
+
|
|
1079
1348
|
// now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
|
|
1080
1349
|
const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
|
|
1081
1350
|
dataSource,
|
|
@@ -1100,12 +1369,16 @@ cycle.`);
|
|
|
1100
1369
|
dataContext,
|
|
1101
1370
|
dataContextEntity,
|
|
1102
1371
|
conversationDetailCount,
|
|
1372
|
+
requestStartTime
|
|
1103
1373
|
);
|
|
1104
1374
|
}
|
|
1105
1375
|
|
|
1106
1376
|
/**
|
|
1107
|
-
* Packages up
|
|
1108
|
-
*
|
|
1377
|
+
* Packages up queries from the metadata based on their status
|
|
1378
|
+
* Used to provide Skip with information about available queries
|
|
1379
|
+
*
|
|
1380
|
+
* @param status The status of queries to include
|
|
1381
|
+
* @returns Array of query information objects
|
|
1109
1382
|
*/
|
|
1110
1383
|
protected BuildSkipQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
|
|
1111
1384
|
const md = new Metadata();
|
|
@@ -1149,9 +1422,13 @@ cycle.`);
|
|
|
1149
1422
|
}
|
|
1150
1423
|
|
|
1151
1424
|
/**
|
|
1152
|
-
* Builds up the array of notes
|
|
1425
|
+
* Builds up the array of notes and note types for Skip
|
|
1426
|
+
* These notes are used to provide Skip with domain knowledge and context
|
|
1427
|
+
*
|
|
1428
|
+
* @param contextUser User context for the request
|
|
1429
|
+
* @returns Object containing arrays of notes and note types
|
|
1153
1430
|
*/
|
|
1154
|
-
protected async BuildSkipAgentNotes(contextUser: UserInfo): Promise<{notes: SkipAPIAgentNote[], noteTypes: SkipAPIAgentNoteType[]}> {
|
|
1431
|
+
protected async BuildSkipAgentNotes(contextUser: UserInfo, filterUserNotesToContextUser: boolean): Promise<{notes: SkipAPIAgentNote[], noteTypes: SkipAPIAgentNoteType[]}> {
|
|
1155
1432
|
try {
|
|
1156
1433
|
// if already configured this does nothing, just makes sure we're configured
|
|
1157
1434
|
await AIEngine.Instance.Config(false, contextUser);
|
|
@@ -1175,6 +1452,12 @@ cycle.`);
|
|
|
1175
1452
|
}
|
|
1176
1453
|
});
|
|
1177
1454
|
|
|
1455
|
+
if (filterUserNotesToContextUser){
|
|
1456
|
+
// filter out any notes that are not for this user
|
|
1457
|
+
notes = notes.filter((n) => n.type === 'Global' ||
|
|
1458
|
+
(n.type === 'User' && n.userId === contextUser.ID));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1178
1461
|
noteTypes = AIEngine.Instance.AgentNoteTypes.map((r) => {
|
|
1179
1462
|
return {
|
|
1180
1463
|
id: r.ID,
|
|
@@ -1197,6 +1480,14 @@ cycle.`);
|
|
|
1197
1480
|
}
|
|
1198
1481
|
}
|
|
1199
1482
|
|
|
1483
|
+
/**
|
|
1484
|
+
* Packs entity rows for inclusion in Skip requests
|
|
1485
|
+
* Provides sample data based on entity configuration
|
|
1486
|
+
*
|
|
1487
|
+
* @param e Entity information
|
|
1488
|
+
* @param dataSource Database connection
|
|
1489
|
+
* @returns Array of entity rows based on packing configuration
|
|
1490
|
+
*/
|
|
1200
1491
|
protected async PackEntityRows(e: EntityInfo, dataSource: DataSource): Promise<any[]> {
|
|
1201
1492
|
try {
|
|
1202
1493
|
if (e.RowsToPackWithSchema === 'None')
|
|
@@ -1249,6 +1540,14 @@ cycle.`);
|
|
|
1249
1540
|
}
|
|
1250
1541
|
}
|
|
1251
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* Packs possible values for an entity field
|
|
1545
|
+
* These values help Skip understand the domain and valid values for fields
|
|
1546
|
+
*
|
|
1547
|
+
* @param f Field information
|
|
1548
|
+
* @param dataSource Database connection
|
|
1549
|
+
* @returns Array of possible values for the field
|
|
1550
|
+
*/
|
|
1252
1551
|
protected async PackFieldPossibleValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
|
|
1253
1552
|
try {
|
|
1254
1553
|
if (f.ValuesToPackWithSchema === 'None') {
|
|
@@ -1291,6 +1590,14 @@ cycle.`);
|
|
|
1291
1590
|
}
|
|
1292
1591
|
}
|
|
1293
1592
|
|
|
1593
|
+
/**
|
|
1594
|
+
* Gets distinct values for a field from the database
|
|
1595
|
+
* Used to provide Skip with information about the possible values
|
|
1596
|
+
*
|
|
1597
|
+
* @param f Field information
|
|
1598
|
+
* @param dataSource Database connection
|
|
1599
|
+
* @returns Array of distinct values for the field
|
|
1600
|
+
*/
|
|
1294
1601
|
protected async GetFieldDistinctValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
|
|
1295
1602
|
try {
|
|
1296
1603
|
const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
|
|
@@ -1319,10 +1626,17 @@ cycle.`);
|
|
|
1319
1626
|
private static __skipEntitiesCache$: BehaviorSubject<Promise<SkipEntityInfo[]> | null> = new BehaviorSubject<Promise<SkipEntityInfo[]> | null>(null);
|
|
1320
1627
|
private static __lastRefreshTime: number = 0;
|
|
1321
1628
|
|
|
1629
|
+
/**
|
|
1630
|
+
* Refreshes the Skip entities cache
|
|
1631
|
+
* Rebuilds the entity information that is provided to Skip
|
|
1632
|
+
*
|
|
1633
|
+
* @param dataSource Database connection
|
|
1634
|
+
* @returns Updated array of entity information
|
|
1635
|
+
*/
|
|
1322
1636
|
private async refreshSkipEntities(dataSource: DataSource): Promise<SkipEntityInfo[]> {
|
|
1323
1637
|
try {
|
|
1324
1638
|
const md = new Metadata();
|
|
1325
|
-
const skipSpecialIncludeEntities = (configInfo.askSkip?.
|
|
1639
|
+
const skipSpecialIncludeEntities = (configInfo.askSkip?.entitiesToSend?.includeEntitiesFromExcludedSchemas ?? [])
|
|
1326
1640
|
.map((e) => e.trim().toLowerCase());
|
|
1327
1641
|
|
|
1328
1642
|
// get the list of entities
|
|
@@ -1352,6 +1666,15 @@ cycle.`);
|
|
|
1352
1666
|
}
|
|
1353
1667
|
}
|
|
1354
1668
|
|
|
1669
|
+
/**
|
|
1670
|
+
* Builds or retrieves Skip entities from cache
|
|
1671
|
+
* Uses caching to avoid expensive rebuilding of entity information
|
|
1672
|
+
*
|
|
1673
|
+
* @param dataSource Database connection
|
|
1674
|
+
* @param forceRefresh Whether to force a refresh regardless of cache state
|
|
1675
|
+
* @param refreshIntervalMinutes Minutes before cache expires
|
|
1676
|
+
* @returns Array of entity information
|
|
1677
|
+
*/
|
|
1355
1678
|
public async BuildSkipEntities(dataSource: DataSource, forceRefresh: boolean = false, refreshIntervalMinutes: number = 15): Promise<SkipEntityInfo[]> {
|
|
1356
1679
|
try {
|
|
1357
1680
|
const now = Date.now();
|
|
@@ -1372,6 +1695,14 @@ cycle.`);
|
|
|
1372
1695
|
}
|
|
1373
1696
|
}
|
|
1374
1697
|
|
|
1698
|
+
/**
|
|
1699
|
+
* Packs information about a single entity for Skip
|
|
1700
|
+
* Includes fields, relationships, and sample data
|
|
1701
|
+
*
|
|
1702
|
+
* @param e Entity information
|
|
1703
|
+
* @param dataSource Database connection
|
|
1704
|
+
* @returns Packaged entity information
|
|
1705
|
+
*/
|
|
1375
1706
|
protected async PackSingleSkipEntityInfo(e: EntityInfo, dataSource: DataSource): Promise<SkipEntityInfo> {
|
|
1376
1707
|
try {
|
|
1377
1708
|
const ret: SkipEntityInfo = {
|
|
@@ -1405,6 +1736,13 @@ cycle.`);
|
|
|
1405
1736
|
}
|
|
1406
1737
|
}
|
|
1407
1738
|
|
|
1739
|
+
/**
|
|
1740
|
+
* Packs information about a single entity relationship
|
|
1741
|
+
* These relationships help Skip understand the data model
|
|
1742
|
+
*
|
|
1743
|
+
* @param r Relationship information
|
|
1744
|
+
* @returns Packaged relationship information
|
|
1745
|
+
*/
|
|
1408
1746
|
protected PackSingleSkipEntityRelationship(r: EntityRelationshipInfo): SkipEntityRelationshipInfo {
|
|
1409
1747
|
try {
|
|
1410
1748
|
return {
|
|
@@ -1428,6 +1766,14 @@ cycle.`);
|
|
|
1428
1766
|
}
|
|
1429
1767
|
}
|
|
1430
1768
|
|
|
1769
|
+
/**
|
|
1770
|
+
* Packs information about a single entity field
|
|
1771
|
+
* Includes metadata and possible values
|
|
1772
|
+
*
|
|
1773
|
+
* @param f Field information
|
|
1774
|
+
* @param dataSource Database connection
|
|
1775
|
+
* @returns Packaged field information
|
|
1776
|
+
*/
|
|
1431
1777
|
protected async PackSingleSkipEntityField(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldInfo> {
|
|
1432
1778
|
try {
|
|
1433
1779
|
return {
|
|
@@ -1468,6 +1814,19 @@ cycle.`);
|
|
|
1468
1814
|
}
|
|
1469
1815
|
}
|
|
1470
1816
|
|
|
1817
|
+
/**
|
|
1818
|
+
* Handles initial object loading for Skip chat interactions
|
|
1819
|
+
* Creates or loads conversation objects, data contexts, and other required entities
|
|
1820
|
+
*
|
|
1821
|
+
* @param dataSource Database connection
|
|
1822
|
+
* @param ConversationId ID of an existing conversation, or empty for a new one
|
|
1823
|
+
* @param UserQuestion The user's question or message
|
|
1824
|
+
* @param user User information
|
|
1825
|
+
* @param userPayload User payload from context
|
|
1826
|
+
* @param md Metadata instance
|
|
1827
|
+
* @param DataContextId Optional ID of a data context to use
|
|
1828
|
+
* @returns Object containing loaded entities and contexts
|
|
1829
|
+
*/
|
|
1471
1830
|
protected async HandleSkipChatInitialObjectLoading(
|
|
1472
1831
|
dataSource: DataSource,
|
|
1473
1832
|
ConversationId: string,
|
|
@@ -1491,6 +1850,8 @@ cycle.`);
|
|
|
1491
1850
|
if (user) {
|
|
1492
1851
|
convoEntity.UserID = user.ID;
|
|
1493
1852
|
convoEntity.Name = AskSkipResolver._defaultNewChatName;
|
|
1853
|
+
// Set initial status to Available since no processing has started yet
|
|
1854
|
+
convoEntity.Status = 'Available';
|
|
1494
1855
|
|
|
1495
1856
|
dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
|
|
1496
1857
|
if (!DataContextId || DataContextId.length === 0) {
|
|
@@ -1593,10 +1954,20 @@ cycle.`);
|
|
|
1593
1954
|
return { dataContext, convoEntity, dataContextEntity, convoDetailEntity };
|
|
1594
1955
|
}
|
|
1595
1956
|
|
|
1957
|
+
/**
|
|
1958
|
+
* Loads conversation details from the database and transforms them into Skip message format
|
|
1959
|
+
* Used to provide Skip with conversation history for context
|
|
1960
|
+
*
|
|
1961
|
+
* @param dataSource Database connection
|
|
1962
|
+
* @param ConversationId ID of the conversation to load details for
|
|
1963
|
+
* @param maxHistoricalMessages Maximum number of historical messages to include
|
|
1964
|
+
* @returns Array of messages in Skip format
|
|
1965
|
+
*/
|
|
1596
1966
|
protected async LoadConversationDetailsIntoSkipMessages(
|
|
1597
1967
|
dataSource: DataSource,
|
|
1598
1968
|
ConversationId: string,
|
|
1599
|
-
maxHistoricalMessages?: number
|
|
1969
|
+
maxHistoricalMessages?: number,
|
|
1970
|
+
roleFilter?: string
|
|
1600
1971
|
): Promise<SkipMessage[]> {
|
|
1601
1972
|
try {
|
|
1602
1973
|
if (!ConversationId || ConversationId.length === 0) {
|
|
@@ -1606,12 +1977,16 @@ cycle.`);
|
|
|
1606
1977
|
// load up all the conversation details from the database server
|
|
1607
1978
|
const md = new Metadata();
|
|
1608
1979
|
const e = md.Entities.find((e) => e.Name === 'Conversation Details');
|
|
1980
|
+
|
|
1981
|
+
// Add role filter if specified
|
|
1982
|
+
const roleFilterClause = roleFilter ? ` AND Role = '${roleFilter}'` : '';
|
|
1983
|
+
|
|
1609
1984
|
const sql = `SELECT
|
|
1610
1985
|
${maxHistoricalMessages ? 'TOP ' + maxHistoricalMessages : ''} *
|
|
1611
1986
|
FROM
|
|
1612
1987
|
${e.SchemaName}.${e.BaseView}
|
|
1613
1988
|
WHERE
|
|
1614
|
-
ConversationID = '${ConversationId}'
|
|
1989
|
+
ConversationID = '${ConversationId}'${roleFilterClause}
|
|
1615
1990
|
ORDER
|
|
1616
1991
|
BY __mj_CreatedAt DESC`;
|
|
1617
1992
|
const result = await dataSource.query(sql);
|
|
@@ -1684,6 +2059,13 @@ cycle.`);
|
|
|
1684
2059
|
}
|
|
1685
2060
|
}
|
|
1686
2061
|
|
|
2062
|
+
/**
|
|
2063
|
+
* Maps database role values to Skip API role format
|
|
2064
|
+
* Converts role names from database format to the format expected by Skip API
|
|
2065
|
+
*
|
|
2066
|
+
* @param role Database role value
|
|
2067
|
+
* @returns Skip API role value ('user' or 'system')
|
|
2068
|
+
*/
|
|
1687
2069
|
protected MapDBRoleToSkipRole(role: string): 'user' | 'system' {
|
|
1688
2070
|
switch (role.trim().toLowerCase()) {
|
|
1689
2071
|
case 'ai':
|
|
@@ -1695,6 +2077,25 @@ cycle.`);
|
|
|
1695
2077
|
}
|
|
1696
2078
|
}
|
|
1697
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* Handles the main Skip chat request processing flow
|
|
2082
|
+
* Routes the request through the different phases based on the Skip API response
|
|
2083
|
+
*
|
|
2084
|
+
* @param input Skip API request to send
|
|
2085
|
+
* @param UserQuestion The question or message from the user
|
|
2086
|
+
* @param user User information
|
|
2087
|
+
* @param dataSource Database connection
|
|
2088
|
+
* @param ConversationId ID of the conversation
|
|
2089
|
+
* @param userPayload User payload from context
|
|
2090
|
+
* @param pubSub Publisher/subscriber for events
|
|
2091
|
+
* @param md Metadata instance
|
|
2092
|
+
* @param convoEntity Conversation entity
|
|
2093
|
+
* @param convoDetailEntity Conversation detail entity for the user message
|
|
2094
|
+
* @param dataContext Data context associated with the conversation
|
|
2095
|
+
* @param dataContextEntity Data context entity
|
|
2096
|
+
* @param conversationDetailCount Tracking count to prevent infinite loops
|
|
2097
|
+
* @returns Result of the Skip interaction
|
|
2098
|
+
*/
|
|
1698
2099
|
protected async HandleSkipChatRequest(
|
|
1699
2100
|
input: SkipAPIRequest,
|
|
1700
2101
|
UserQuestion: string,
|
|
@@ -1708,11 +2109,16 @@ cycle.`);
|
|
|
1708
2109
|
convoDetailEntity: ConversationDetailEntity,
|
|
1709
2110
|
dataContext: DataContext,
|
|
1710
2111
|
dataContextEntity: DataContextEntity,
|
|
1711
|
-
conversationDetailCount: number
|
|
2112
|
+
conversationDetailCount: number,
|
|
2113
|
+
startTime: Date
|
|
1712
2114
|
): Promise<AskSkipResultType> {
|
|
1713
|
-
|
|
2115
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
2116
|
+
LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${skipConfigInfo.chatURL}`);
|
|
1714
2117
|
|
|
1715
2118
|
if (conversationDetailCount > 10) {
|
|
2119
|
+
// Set status of conversation to Available since we still want to allow the user to ask questions
|
|
2120
|
+
await this.setConversationStatus(convoEntity, 'Available');
|
|
2121
|
+
|
|
1716
2122
|
// At this point it is likely that we are stuck in a loop, so we stop here
|
|
1717
2123
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
1718
2124
|
message: JSON.stringify({
|
|
@@ -1736,7 +2142,7 @@ cycle.`);
|
|
|
1736
2142
|
}
|
|
1737
2143
|
|
|
1738
2144
|
const response = await sendPostRequest(
|
|
1739
|
-
|
|
2145
|
+
skipConfigInfo.chatURL,
|
|
1740
2146
|
input,
|
|
1741
2147
|
true,
|
|
1742
2148
|
null,
|
|
@@ -1791,7 +2197,8 @@ cycle.`);
|
|
|
1791
2197
|
convoDetailEntity,
|
|
1792
2198
|
dataContext,
|
|
1793
2199
|
dataContextEntity,
|
|
1794
|
-
conversationDetailCount
|
|
2200
|
+
conversationDetailCount,
|
|
2201
|
+
startTime
|
|
1795
2202
|
);
|
|
1796
2203
|
} else if (apiResponse.responsePhase === 'clarifying_question') {
|
|
1797
2204
|
// need to send the request back to the user for a clarifying question
|
|
@@ -1805,7 +2212,8 @@ cycle.`);
|
|
|
1805
2212
|
userPayload,
|
|
1806
2213
|
pubSub,
|
|
1807
2214
|
convoEntity,
|
|
1808
|
-
convoDetailEntity
|
|
2215
|
+
convoDetailEntity,
|
|
2216
|
+
startTime,
|
|
1809
2217
|
);
|
|
1810
2218
|
} else if (apiResponse.responsePhase === 'analysis_complete') {
|
|
1811
2219
|
return await this.HandleAnalysisComplete(
|
|
@@ -1820,13 +2228,17 @@ cycle.`);
|
|
|
1820
2228
|
convoEntity,
|
|
1821
2229
|
convoDetailEntity,
|
|
1822
2230
|
dataContext,
|
|
1823
|
-
dataContextEntity
|
|
2231
|
+
dataContextEntity,
|
|
2232
|
+
startTime
|
|
1824
2233
|
);
|
|
1825
2234
|
} else {
|
|
1826
2235
|
// unknown response phase
|
|
1827
2236
|
throw new Error(`Unknown Skip API response phase: ${apiResponse.responsePhase}`);
|
|
1828
2237
|
}
|
|
1829
2238
|
} else {
|
|
2239
|
+
// Set status of conversation to Available since we still want to allow the user to ask questions
|
|
2240
|
+
await this.setConversationStatus(convoEntity, 'Available');
|
|
2241
|
+
|
|
1830
2242
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
1831
2243
|
message: JSON.stringify({
|
|
1832
2244
|
type: 'AskSkip',
|
|
@@ -1849,6 +2261,15 @@ cycle.`);
|
|
|
1849
2261
|
}
|
|
1850
2262
|
}
|
|
1851
2263
|
|
|
2264
|
+
/**
|
|
2265
|
+
* Publishes a status update message to the user based on the Skip API response
|
|
2266
|
+
* Provides feedback about what phase of processing is happening
|
|
2267
|
+
*
|
|
2268
|
+
* @param apiResponse The response from the Skip API
|
|
2269
|
+
* @param userPayload User payload from context
|
|
2270
|
+
* @param conversationID ID of the conversation
|
|
2271
|
+
* @param pubSub Publisher/subscriber for events
|
|
2272
|
+
*/
|
|
1852
2273
|
protected async PublishApiResponseUserUpdateMessage(
|
|
1853
2274
|
apiResponse: SkipAPIResponse,
|
|
1854
2275
|
userPayload: UserPayload,
|
|
@@ -1881,6 +2302,24 @@ cycle.`);
|
|
|
1881
2302
|
});
|
|
1882
2303
|
}
|
|
1883
2304
|
|
|
2305
|
+
/**
|
|
2306
|
+
* Handles the analysis complete phase of the Skip chat process
|
|
2307
|
+
* Finalizes the conversation and creates necessary artifacts
|
|
2308
|
+
*
|
|
2309
|
+
* @param apiRequest The original request sent to Skip
|
|
2310
|
+
* @param apiResponse The analysis complete response from Skip
|
|
2311
|
+
* @param UserQuestion The original user question
|
|
2312
|
+
* @param user User information
|
|
2313
|
+
* @param dataSource Database connection
|
|
2314
|
+
* @param ConversationId ID of the conversation
|
|
2315
|
+
* @param userPayload User payload from context
|
|
2316
|
+
* @param pubSub Publisher/subscriber for events
|
|
2317
|
+
* @param convoEntity Conversation entity
|
|
2318
|
+
* @param convoDetailEntity Conversation detail entity for the user message
|
|
2319
|
+
* @param dataContext Data context associated with the conversation
|
|
2320
|
+
* @param dataContextEntity Data context entity
|
|
2321
|
+
* @returns Result of the Skip interaction
|
|
2322
|
+
*/
|
|
1884
2323
|
protected async HandleAnalysisComplete(
|
|
1885
2324
|
apiRequest: SkipAPIRequest,
|
|
1886
2325
|
apiResponse: SkipAPIAnalysisCompleteResponse,
|
|
@@ -1893,7 +2332,8 @@ cycle.`);
|
|
|
1893
2332
|
convoEntity: ConversationEntity,
|
|
1894
2333
|
convoDetailEntity: ConversationDetailEntity,
|
|
1895
2334
|
dataContext: DataContext,
|
|
1896
|
-
dataContextEntity: DataContextEntity
|
|
2335
|
+
dataContextEntity: DataContextEntity,
|
|
2336
|
+
startTime: Date
|
|
1897
2337
|
): Promise<AskSkipResultType> {
|
|
1898
2338
|
// analysis is complete
|
|
1899
2339
|
// all done, wrap things up
|
|
@@ -1913,7 +2353,8 @@ cycle.`);
|
|
|
1913
2353
|
convoEntity,
|
|
1914
2354
|
pubSub,
|
|
1915
2355
|
userPayload,
|
|
1916
|
-
dataSource
|
|
2356
|
+
dataSource,
|
|
2357
|
+
startTime
|
|
1917
2358
|
);
|
|
1918
2359
|
const response: AskSkipResultType = {
|
|
1919
2360
|
Success: true,
|
|
@@ -1927,6 +2368,22 @@ cycle.`);
|
|
|
1927
2368
|
return response;
|
|
1928
2369
|
}
|
|
1929
2370
|
|
|
2371
|
+
/**
|
|
2372
|
+
* Handles the clarifying question phase of the Skip chat process
|
|
2373
|
+
* Creates a conversation detail for the clarifying question from Skip
|
|
2374
|
+
*
|
|
2375
|
+
* @param apiRequest The original request sent to Skip
|
|
2376
|
+
* @param apiResponse The clarifying question response from Skip
|
|
2377
|
+
* @param UserQuestion The original user question
|
|
2378
|
+
* @param user User information
|
|
2379
|
+
* @param dataSource Database connection
|
|
2380
|
+
* @param ConversationId ID of the conversation
|
|
2381
|
+
* @param userPayload User payload from context
|
|
2382
|
+
* @param pubSub Publisher/subscriber for events
|
|
2383
|
+
* @param convoEntity Conversation entity
|
|
2384
|
+
* @param convoDetailEntity Conversation detail entity for the user message
|
|
2385
|
+
* @returns Result of the Skip interaction
|
|
2386
|
+
*/
|
|
1930
2387
|
protected async HandleClarifyingQuestionPhase(
|
|
1931
2388
|
apiRequest: SkipAPIRequest,
|
|
1932
2389
|
apiResponse: SkipAPIClarifyingQuestionResponse,
|
|
@@ -1937,9 +2394,11 @@ cycle.`);
|
|
|
1937
2394
|
userPayload: UserPayload,
|
|
1938
2395
|
pubSub: PubSubEngine,
|
|
1939
2396
|
convoEntity: ConversationEntity,
|
|
1940
|
-
convoDetailEntity: ConversationDetailEntity
|
|
2397
|
+
convoDetailEntity: ConversationDetailEntity,
|
|
2398
|
+
startTime: Date
|
|
1941
2399
|
): Promise<AskSkipResultType> {
|
|
1942
2400
|
// need to create a message here in the COnversation and then pass that id below
|
|
2401
|
+
const endTime = new Date();
|
|
1943
2402
|
const md = new Metadata();
|
|
1944
2403
|
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
1945
2404
|
convoDetailEntityAI.NewRecord();
|
|
@@ -1947,6 +2406,11 @@ cycle.`);
|
|
|
1947
2406
|
convoDetailEntityAI.Message = JSON.stringify(apiResponse); //.clarifyingQuestion;
|
|
1948
2407
|
convoDetailEntityAI.Role = 'AI';
|
|
1949
2408
|
convoDetailEntityAI.HiddenToUser = false;
|
|
2409
|
+
convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
|
|
2410
|
+
|
|
2411
|
+
// Set conversation status back to Available since we need user input for the clarifying question
|
|
2412
|
+
this.setConversationStatus(convoEntity, 'Available');
|
|
2413
|
+
|
|
1950
2414
|
if (await convoDetailEntityAI.Save()) {
|
|
1951
2415
|
return {
|
|
1952
2416
|
Success: true,
|
|
@@ -1975,6 +2439,25 @@ cycle.`);
|
|
|
1975
2439
|
}
|
|
1976
2440
|
}
|
|
1977
2441
|
|
|
2442
|
+
/**
|
|
2443
|
+
* Handles the data request phase of the Skip chat process
|
|
2444
|
+
* Processes data requests from Skip and loads requested data
|
|
2445
|
+
*
|
|
2446
|
+
* @param apiRequest The original request sent to Skip
|
|
2447
|
+
* @param apiResponse The data request response from Skip
|
|
2448
|
+
* @param UserQuestion The original user question
|
|
2449
|
+
* @param user User information
|
|
2450
|
+
* @param dataSource Database connection
|
|
2451
|
+
* @param ConversationId ID of the conversation
|
|
2452
|
+
* @param userPayload User payload from context
|
|
2453
|
+
* @param pubSub Publisher/subscriber for events
|
|
2454
|
+
* @param convoEntity Conversation entity
|
|
2455
|
+
* @param convoDetailEntity Conversation detail entity for the user message
|
|
2456
|
+
* @param dataContext Data context associated with the conversation
|
|
2457
|
+
* @param dataContextEntity Data context entity
|
|
2458
|
+
* @param conversationDetailCount Tracking count to prevent infinite loops
|
|
2459
|
+
* @returns Result of the Skip interaction
|
|
2460
|
+
*/
|
|
1978
2461
|
protected async HandleDataRequestPhase(
|
|
1979
2462
|
apiRequest: SkipAPIRequest,
|
|
1980
2463
|
apiResponse: SkipAPIDataRequestResponse,
|
|
@@ -1988,7 +2471,8 @@ cycle.`);
|
|
|
1988
2471
|
convoDetailEntity: ConversationDetailEntity,
|
|
1989
2472
|
dataContext: DataContext,
|
|
1990
2473
|
dataContextEntity: DataContextEntity,
|
|
1991
|
-
conversationDetailCount: number
|
|
2474
|
+
conversationDetailCount: number,
|
|
2475
|
+
startTime: Date
|
|
1992
2476
|
): Promise<AskSkipResultType> {
|
|
1993
2477
|
// 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
|
|
1994
2478
|
try {
|
|
@@ -2117,7 +2601,8 @@ cycle.`);
|
|
|
2117
2601
|
convoDetailEntity,
|
|
2118
2602
|
dataContext,
|
|
2119
2603
|
dataContextEntity,
|
|
2120
|
-
conversationDetailCount
|
|
2604
|
+
conversationDetailCount,
|
|
2605
|
+
startTime
|
|
2121
2606
|
);
|
|
2122
2607
|
} catch (e) {
|
|
2123
2608
|
LogError(e);
|
|
@@ -2126,14 +2611,19 @@ cycle.`);
|
|
|
2126
2611
|
}
|
|
2127
2612
|
|
|
2128
2613
|
/**
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
* @param
|
|
2133
|
-
* @param
|
|
2134
|
-
* @param
|
|
2135
|
-
* @param
|
|
2136
|
-
* @
|
|
2614
|
+
* Finishes a successful conversation and notifies the user
|
|
2615
|
+
* Creates necessary records, artifacts, and notifications
|
|
2616
|
+
*
|
|
2617
|
+
* @param apiResponse The analysis complete response from Skip
|
|
2618
|
+
* @param dataContext Data context associated with the conversation
|
|
2619
|
+
* @param dataContextEntity Data context entity
|
|
2620
|
+
* @param md Metadata instance
|
|
2621
|
+
* @param user User information
|
|
2622
|
+
* @param convoEntity Conversation entity
|
|
2623
|
+
* @param pubSub Publisher/subscriber for events
|
|
2624
|
+
* @param userPayload User payload from context
|
|
2625
|
+
* @param dataSource Database connection
|
|
2626
|
+
* @returns The ID of the AI message conversation detail
|
|
2137
2627
|
*/
|
|
2138
2628
|
protected async FinishConversationAndNotifyUser(
|
|
2139
2629
|
apiResponse: SkipAPIAnalysisCompleteResponse,
|
|
@@ -2144,7 +2634,8 @@ cycle.`);
|
|
|
2144
2634
|
convoEntity: ConversationEntity,
|
|
2145
2635
|
pubSub: PubSubEngine,
|
|
2146
2636
|
userPayload: UserPayload,
|
|
2147
|
-
dataSource: DataSource
|
|
2637
|
+
dataSource: DataSource,
|
|
2638
|
+
startTime: Date
|
|
2148
2639
|
): Promise<{ AIMessageConversationDetailID: string }> {
|
|
2149
2640
|
const sTitle = apiResponse.reportTitle;
|
|
2150
2641
|
const sResult = JSON.stringify(apiResponse);
|
|
@@ -2215,12 +2706,15 @@ cycle.`);
|
|
|
2215
2706
|
}
|
|
2216
2707
|
|
|
2217
2708
|
// Create a conversation detail record for the Skip response
|
|
2709
|
+
const endTime = new Date();
|
|
2218
2710
|
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
2219
2711
|
convoDetailEntityAI.NewRecord();
|
|
2220
2712
|
convoDetailEntityAI.ConversationID = convoEntity.ID;
|
|
2221
2713
|
convoDetailEntityAI.Message = sResult;
|
|
2222
2714
|
convoDetailEntityAI.Role = 'AI';
|
|
2223
2715
|
convoDetailEntityAI.HiddenToUser = false;
|
|
2716
|
+
convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
|
|
2717
|
+
|
|
2224
2718
|
if (artifactId && artifactId.length > 0) {
|
|
2225
2719
|
// bind the new convo detail record to the artifact + version for this response
|
|
2226
2720
|
convoDetailEntityAI.ArtifactID = artifactId;
|
|
@@ -2233,9 +2727,23 @@ cycle.`);
|
|
|
2233
2727
|
LogError(`Error saving conversation detail entity for AI message: ${sResult}`, undefined, convoDetailEntityAI.LatestResult);
|
|
2234
2728
|
}
|
|
2235
2729
|
|
|
2236
|
-
//
|
|
2730
|
+
// Update the conversation properties: name if it's the default, and set status back to 'Available'
|
|
2731
|
+
let needToSaveConvo = false;
|
|
2732
|
+
|
|
2733
|
+
// Update name if still default
|
|
2237
2734
|
if (convoEntity.Name === AskSkipResolver._defaultNewChatName && sTitle && sTitle !== AskSkipResolver._defaultNewChatName) {
|
|
2238
2735
|
convoEntity.Name = sTitle; // use the title from the response
|
|
2736
|
+
needToSaveConvo = true;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// Set status back to 'Available' since processing is complete
|
|
2740
|
+
if (convoEntity.Status === 'Processing') {
|
|
2741
|
+
convoEntity.Status = 'Available';
|
|
2742
|
+
needToSaveConvo = true;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// Save if any changes were made
|
|
2746
|
+
if (needToSaveConvo) {
|
|
2239
2747
|
const convoEntitySaveResult: boolean = await convoEntity.Save();
|
|
2240
2748
|
if (!convoEntitySaveResult) {
|
|
2241
2749
|
LogError(`Error saving conversation entity for AI message: ${sResult}`, undefined, convoEntity.LatestResult);
|
|
@@ -2294,18 +2802,46 @@ cycle.`);
|
|
|
2294
2802
|
};
|
|
2295
2803
|
}
|
|
2296
2804
|
|
|
2297
|
-
|
|
2805
|
+
private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available'): Promise<boolean> {
|
|
2806
|
+
if (convoEntity.Status !== status) {
|
|
2807
|
+
convoEntity.Status = status;
|
|
2808
|
+
const convoSaveResult = await convoEntity.Save();
|
|
2809
|
+
if (!convoSaveResult) {
|
|
2810
|
+
LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
|
|
2811
|
+
}
|
|
2812
|
+
return convoSaveResult;
|
|
2813
|
+
}
|
|
2814
|
+
return true;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/**
|
|
2818
|
+
* Gets the ID of an agent note type by its name
|
|
2819
|
+
* Falls back to a default note type if the specified one is not found
|
|
2820
|
+
*
|
|
2821
|
+
* @param name Name of the agent note type
|
|
2822
|
+
* @param defaultNoteType Default note type to use if the specified one is not found
|
|
2823
|
+
* @returns ID of the agent note type
|
|
2824
|
+
*/
|
|
2825
|
+
protected getAgentNoteTypeIDByName(name: string, defaultNoteType: string = 'AI'): string {
|
|
2298
2826
|
const noteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === name.trim().toLowerCase())?.ID;
|
|
2299
2827
|
if (noteTypeID) {
|
|
2300
2828
|
return noteTypeID;
|
|
2301
2829
|
}
|
|
2302
2830
|
else{
|
|
2303
|
-
// default
|
|
2304
|
-
const
|
|
2305
|
-
return
|
|
2831
|
+
// default
|
|
2832
|
+
const defaultNoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === defaultNoteType.trim().toLowerCase())?.ID;
|
|
2833
|
+
return defaultNoteTypeID;
|
|
2306
2834
|
}
|
|
2307
2835
|
}
|
|
2308
2836
|
|
|
2837
|
+
/**
|
|
2838
|
+
* Gets data from a view
|
|
2839
|
+
* Helper method to run a view and retrieve its data
|
|
2840
|
+
*
|
|
2841
|
+
* @param ViewId ID of the view to run
|
|
2842
|
+
* @param user User context for the query
|
|
2843
|
+
* @returns Results of the view query
|
|
2844
|
+
*/
|
|
2309
2845
|
protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
|
|
2310
2846
|
const rv = new RunView();
|
|
2311
2847
|
const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
|
|
@@ -2314,8 +2850,11 @@ cycle.`);
|
|
|
2314
2850
|
}
|
|
2315
2851
|
|
|
2316
2852
|
/**
|
|
2317
|
-
* Manually executes the Skip AI learning cycle
|
|
2853
|
+
* Manually executes the Skip AI learning cycle
|
|
2854
|
+
* Allows triggering a learning cycle on demand rather than waiting for scheduled execution
|
|
2855
|
+
*
|
|
2318
2856
|
* @param OrganizationId Optional organization ID to register for this run
|
|
2857
|
+
* @returns Result of the manual learning cycle execution
|
|
2319
2858
|
*/
|
|
2320
2859
|
@Mutation(() => ManualLearningCycleResultType)
|
|
2321
2860
|
async ManuallyExecuteSkipLearningCycle(
|
|
@@ -2323,17 +2862,17 @@ cycle.`);
|
|
|
2323
2862
|
): Promise<ManualLearningCycleResultType> {
|
|
2324
2863
|
try {
|
|
2325
2864
|
LogStatus('Manual execution of Skip learning cycle requested via API');
|
|
2326
|
-
|
|
2865
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
2327
2866
|
// First check if learning cycles are enabled in configuration
|
|
2328
|
-
if (
|
|
2867
|
+
if (!skipConfigInfo.learningCycleEnabled) {
|
|
2329
2868
|
return {
|
|
2330
2869
|
Success: false,
|
|
2331
|
-
Message: 'Learning cycles are
|
|
2870
|
+
Message: 'Learning cycles are not enabled in configuration'
|
|
2332
2871
|
};
|
|
2333
2872
|
}
|
|
2334
2873
|
|
|
2335
2874
|
// Check if we have a valid endpoint when cycles are enabled
|
|
2336
|
-
if (!
|
|
2875
|
+
if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
|
|
2337
2876
|
return {
|
|
2338
2877
|
Success: false,
|
|
2339
2878
|
Message: 'Learning cycle API endpoint is not configured'
|
|
@@ -2341,7 +2880,7 @@ cycle.`);
|
|
|
2341
2880
|
}
|
|
2342
2881
|
|
|
2343
2882
|
// Use the organization ID from config if not provided
|
|
2344
|
-
const orgId = OrganizationId ||
|
|
2883
|
+
const orgId = OrganizationId || skipConfigInfo.orgID;
|
|
2345
2884
|
|
|
2346
2885
|
// Call the scheduler's manual execution method with org ID
|
|
2347
2886
|
const result = await LearningCycleScheduler.Instance.manuallyExecuteLearningCycle(orgId);
|
|
@@ -2364,6 +2903,9 @@ cycle.`);
|
|
|
2364
2903
|
|
|
2365
2904
|
/**
|
|
2366
2905
|
* Gets the current status of the learning cycle scheduler
|
|
2906
|
+
* Provides information about the scheduler state and any running cycles
|
|
2907
|
+
*
|
|
2908
|
+
* @returns Status information about the learning cycle scheduler
|
|
2367
2909
|
*/
|
|
2368
2910
|
@Query(() => LearningCycleStatusType)
|
|
2369
2911
|
async GetLearningCycleStatus(): Promise<LearningCycleStatusType> {
|
|
@@ -2393,15 +2935,19 @@ cycle.`);
|
|
|
2393
2935
|
|
|
2394
2936
|
/**
|
|
2395
2937
|
* Checks if a specific organization is running a learning cycle
|
|
2938
|
+
* Used to determine if a new learning cycle can be started for an organization
|
|
2939
|
+
*
|
|
2396
2940
|
* @param OrganizationId The organization ID to check
|
|
2941
|
+
* @returns Information about the running cycle, or null if no cycle is running
|
|
2397
2942
|
*/
|
|
2398
2943
|
@Query(() => RunningOrganizationType, { nullable: true })
|
|
2399
2944
|
async IsOrganizationRunningLearningCycle(
|
|
2400
2945
|
@Arg('OrganizationId', () => String) OrganizationId: string
|
|
2401
2946
|
): Promise<RunningOrganizationType | null> {
|
|
2402
2947
|
try {
|
|
2948
|
+
const skipConfigInfo = configInfo.askSkip;
|
|
2403
2949
|
// Use the organization ID from config if not provided
|
|
2404
|
-
const orgId = OrganizationId ||
|
|
2950
|
+
const orgId = OrganizationId || skipConfigInfo.orgID;
|
|
2405
2951
|
|
|
2406
2952
|
const status = LearningCycleScheduler.Instance.isOrganizationRunningCycle(orgId);
|
|
2407
2953
|
|
|
@@ -2424,7 +2970,10 @@ cycle.`);
|
|
|
2424
2970
|
|
|
2425
2971
|
/**
|
|
2426
2972
|
* Stops a running learning cycle for a specific organization
|
|
2973
|
+
* Allows manual intervention to stop a learning cycle that is taking too long or causing issues
|
|
2974
|
+
*
|
|
2427
2975
|
* @param OrganizationId The organization ID to stop the cycle for
|
|
2976
|
+
* @returns Result of the stop operation, including details about the stopped cycle
|
|
2428
2977
|
*/
|
|
2429
2978
|
@Mutation(() => StopLearningCycleResultType)
|
|
2430
2979
|
async StopLearningCycleForOrganization(
|
|
@@ -2432,7 +2981,7 @@ cycle.`);
|
|
|
2432
2981
|
): Promise<StopLearningCycleResultType> {
|
|
2433
2982
|
try {
|
|
2434
2983
|
// Use the organization ID from config if not provided
|
|
2435
|
-
const orgId = OrganizationId ||
|
|
2984
|
+
const orgId = OrganizationId || configInfo.askSkip.orgID;
|
|
2436
2985
|
|
|
2437
2986
|
const result = LearningCycleScheduler.Instance.stopLearningCycleForOrganization(orgId);
|
|
2438
2987
|
|