@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.
@@ -53,7 +53,7 @@ import {
53
53
  UserNotificationEntity,
54
54
  } from '@memberjunction/core-entities';
55
55
  import { DataSource } from 'typeorm';
56
- import { ___skipAPIOrgId, ___skipAPIurl, ___skipLearningAPIurl, ___skipLearningCycleIntervalInMinutes, ___skipRunLearningCycles, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
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
- * Internally used type
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
- // Static initializer that runs when the class is loaded - initializes the learning cycle scheduler
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 simple chat request from a user to Skip, using a particular data record
235
- * @param UserQuestion the user's question
236
- * @param EntityName the name of the entity for the record the user is discussing
237
- * @param PrimaryKeys the primary keys of the record the user is discussing
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 (___skipRunLearningCycles !== 'Y') {
436
+ if (!skipConfigInfo.learningCycleEnabled) {
319
437
  return {
320
438
  success: false,
321
- error: 'Learning cycles are disabled in configuration',
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 (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
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 = ___skipAPIOrgId;
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
- // Make the API request
401
- const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID);
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
- LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
544
+ LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
408
545
 
409
- learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
410
- learningCycleEntity.EndedAt = endTime;
546
+ learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
547
+ learningCycleEntity.EndedAt = endTime;
411
548
 
412
- if (!(await learningCycleEntity.Save())) {
413
- LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
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
- // Unregister the organization after completion
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
- } catch (saveError) {
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
- LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${___skipLearningAPIurl}`);
598
+ const skipConfigInfo = configInfo.askSkip;
599
+ LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${skipConfigInfo.learningCycleURL}`);
446
600
 
447
- const response = await sendPostRequest(___skipLearningAPIurl, input, true, null);
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
- LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${___skipAPIurl}`);
657
+ const skipConfigInfo = configInfo.askSkip;
658
+ LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${skipConfigInfo.chatURL}`);
493
659
 
494
- const response = await sendPostRequest(___skipAPIurl, input, true, null);
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 user The user making the request
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: ___skipAPIOrgId,
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 have an updated or new conversation detail since the last learning cycle
772
- * @param dataSource the data source to use
773
- * @param lastLearningCycleDate the date of the last learning cycle
774
- * @param contextUser the user context
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
- * @param contextUser the user context to load the requests
818
- * @returns Array of SkipAPIAgentRequest objects
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 SkipAPIArtifact types to send across information about the artifacts associated with this particular
931
- * conversation.
932
- * @param contextUser
933
- * @param dataSource
934
- * @param conversationId
935
- * @returns
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 and returns the results
1011
- * @param pubSub
1012
- * @param DataContextId
1013
- * @param ScriptText
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 the Approved queries from the metadata
1108
- * @returns
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 that are applicable for Skip to receive from MJAPI
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?.entitiesToSendSkip?.includeEntitiesFromExcludedSchemas ?? [])
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
- LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${___skipAPIurl}`);
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
- ___skipAPIurl,
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
- * This method will handle the process for an end of successful request where a user is notified of an AI message. The AI message is either the finished report or a clarifying question.
2130
- * @param apiResponse
2131
- * @param md
2132
- * @param user
2133
- * @param convoEntity
2134
- * @param pubSub
2135
- * @param userPayload
2136
- * @returns
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
- // finally update the convo name if it is still the default
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
- protected getAgentNoteTypeIDByName(name: string): string {
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 to AI note ID
2304
- const AINoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === 'AI')?.ID;
2305
- return AINoteTypeID
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 (___skipRunLearningCycles !== 'Y') {
2867
+ if (!skipConfigInfo.learningCycleEnabled) {
2329
2868
  return {
2330
2869
  Success: false,
2331
- Message: 'Learning cycles are disabled in configuration'
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 (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
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 || ___skipAPIOrgId;
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 || ___skipAPIOrgId;
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 || ___skipAPIOrgId;
2984
+ const orgId = OrganizationId || configInfo.askSkip.orgID;
2436
2985
 
2437
2986
  const result = LearningCycleScheduler.Instance.stopLearningCycleForOrganization(orgId);
2438
2987