@memberjunction/server 2.38.0 → 2.40.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';
@@ -65,9 +65,16 @@ import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists
65
65
  import e from 'express';
66
66
  import { Skip } from '@graphql-tools/utils';
67
67
 
68
+ /**
69
+ * Enumeration representing the different phases of a Skip response
70
+ * Corresponds to the lifecycle of a Skip AI interaction
71
+ */
68
72
  enum SkipResponsePhase {
73
+ /** Skip is asking for clarification before proceeding */
69
74
  ClarifyingQuestion = 'clarifying_question',
75
+ /** Skip is requesting data from the system to process the request */
70
76
  DataRequest = 'data_request',
77
+ /** Skip has completed its analysis and has returned a final response */
71
78
  AnalysisComplete = 'analysis_complete',
72
79
  }
73
80
 
@@ -76,165 +83,267 @@ registerEnumType(SkipResponsePhase, {
76
83
  description: 'The phase of the respons: clarifying_question, data_request, or analysis_complete',
77
84
  });
78
85
 
86
+ /**
87
+ * Result type for Skip AI interactions
88
+ * Contains the status of the request, the response phase, the result payload,
89
+ * and references to the conversation and message IDs
90
+ */
79
91
  @ObjectType()
80
92
  export class AskSkipResultType {
93
+ /** Whether the interaction was successful */
81
94
  @Field(() => Boolean)
82
95
  Success: boolean;
83
96
 
97
+ /** Status message of the interaction */
84
98
  @Field(() => String)
85
99
  Status: string; // required
86
100
 
101
+ /** The phase of the response from Skip */
87
102
  @Field(() => SkipResponsePhase)
88
103
  ResponsePhase: SkipResponsePhase;
89
104
 
105
+ /** The result payload, usually a JSON string of the full response */
90
106
  @Field(() => String)
91
107
  Result: string;
92
108
 
109
+ /** The ID of the conversation this interaction belongs to */
93
110
  @Field(() => String)
94
111
  ConversationId: string;
95
112
 
113
+ /** The ID of the user message in the conversation */
96
114
  @Field(() => String)
97
115
  UserMessageConversationDetailId: string;
98
116
 
117
+ /** The ID of the AI response message in the conversation */
99
118
  @Field(() => String)
100
119
  AIMessageConversationDetailId: string;
101
120
  }
102
121
 
122
+ /**
123
+ * Result type for manual learning cycle operations
124
+ * Contains success status and a message describing the result
125
+ */
103
126
  @ObjectType()
104
127
  export class ManualLearningCycleResultType {
128
+ /** Whether the learning cycle operation was successful */
105
129
  @Field(() => Boolean)
106
130
  Success: boolean;
107
131
 
132
+ /** Descriptive message about the learning cycle operation */
108
133
  @Field(() => String)
109
134
  Message: string;
110
135
  }
111
136
 
137
+ /**
138
+ * Contains details about a specific learning cycle
139
+ * Includes identifier, start time, and duration information
140
+ */
112
141
  @ObjectType()
113
142
  export class CycleDetailsType {
143
+ /** Unique identifier for the learning cycle */
114
144
  @Field(() => String)
115
145
  LearningCycleId: string;
116
146
 
147
+ /** ISO timestamp when the cycle started */
117
148
  @Field(() => String)
118
149
  StartTime: string;
119
150
 
151
+ /** Duration of the cycle in minutes */
120
152
  @Field(() => Number)
121
153
  RunningForMinutes: number;
122
154
  }
123
155
 
156
+ /**
157
+ * Information about an organization that is currently running a learning cycle
158
+ * Links organization to specific learning cycle and provides timing details
159
+ */
124
160
  @ObjectType()
125
161
  export class RunningOrganizationType {
162
+ /** Identifier of the organization running the cycle */
126
163
  @Field(() => String)
127
164
  OrganizationId: string;
128
165
 
166
+ /** Unique identifier for the learning cycle */
129
167
  @Field(() => String)
130
168
  LearningCycleId: string;
131
169
 
170
+ /** ISO timestamp when the cycle started */
132
171
  @Field(() => String)
133
172
  StartTime: string;
134
173
 
174
+ /** Duration the cycle has been running in minutes */
135
175
  @Field(() => Number)
136
176
  RunningForMinutes: number;
137
177
  }
138
178
 
179
+ /**
180
+ * Status information about the learning cycle scheduler and running cycles
181
+ * Provides overall scheduler status and details about active learning cycles
182
+ */
139
183
  @ObjectType()
140
184
  export class LearningCycleStatusType {
185
+ /** Whether the scheduler process is currently active */
141
186
  @Field(() => Boolean)
142
187
  IsSchedulerRunning: boolean;
143
188
 
189
+ /** ISO timestamp of the last time the scheduler ran a cycle */
144
190
  @Field(() => String, { nullable: true })
145
191
  LastRunTime: string;
146
192
 
193
+ /** List of organizations that are currently running learning cycles */
147
194
  @Field(() => [RunningOrganizationType], { nullable: true })
148
195
  RunningOrganizations: RunningOrganizationType[];
149
196
  }
150
197
 
198
+ /**
199
+ * Result of an attempt to stop a learning cycle
200
+ * Provides status information about the stop operation
201
+ */
151
202
  @ObjectType()
152
203
  export class StopLearningCycleResultType {
204
+ /** Whether the stop operation succeeded */
153
205
  @Field(() => Boolean)
154
206
  Success: boolean;
155
207
 
208
+ /** Descriptive message about the result of the stop operation */
156
209
  @Field(() => String)
157
210
  Message: string;
158
211
 
212
+ /** Whether the cycle was actually running when the stop was attempted */
159
213
  @Field(() => Boolean)
160
214
  WasRunning: boolean;
161
215
 
216
+ /** Details about the cycle that was stopped (if any) */
162
217
  @Field(() => CycleDetailsType, { nullable: true })
163
218
  CycleDetails: CycleDetailsType;
164
219
  }
165
220
 
166
221
  /**
167
- * Internally used type
222
+ * 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.
223
+ */
224
+ function initializeSkipLearningCycleScheduler() {
225
+ try {
226
+ // Set up event listener for server initialization
227
+ const eventListener = MJGlobal.Instance.GetEventListener(true);
228
+ eventListener.subscribe(event => {
229
+ // Filter for our server's setup complete event
230
+ if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
231
+ try {
232
+ const skipConfigInfo = configInfo.askSkip;
233
+ if (!skipConfigInfo) {
234
+ LogStatus('Skip AI Learning Cycle Scheduler not started: Skip configuration not found');
235
+ return;
236
+ }
237
+ if (!skipConfigInfo.learningCycleEnabled) {
238
+ LogStatus('Skip AI Learning Cycles not enabled in configuration');
239
+ return;
240
+ }
241
+
242
+ // Check if we have a valid endpoint when cycles are enabled
243
+ if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
244
+ LogError('Skip AI Learning cycle scheduler not started: Learning cycles are enabled but no Learning Cycle API endpoint is configured');
245
+ return;
246
+ }
247
+
248
+ const dataSources = event.args.dataSources;
249
+ if (dataSources && dataSources.length > 0) {
250
+ // Initialize the scheduler
251
+ const scheduler = LearningCycleScheduler.Instance;
252
+
253
+ // Set the data sources for the scheduler
254
+ scheduler.setDataSources(dataSources);
255
+
256
+ // Default is 60 minutes, if the interval is not set in the config, use 60 minutes
257
+ const interval = skipConfigInfo.learningCycleIntervalInMinutes ?? 60;
258
+
259
+
260
+ if (skipConfigInfo.learningCycleRunUponStartup) {
261
+ // If configured to run immediately, run the learning cycle
262
+ LogStatus('Skip API Learning Cycle: Run Upon Startup is enabled, running learning cycle immediately');
263
+ // Start the scheduler
264
+ scheduler.start(interval);
265
+ }
266
+ else {
267
+ // not asked to start right away, just start the scheduler after the interval
268
+ 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.`);
269
+
270
+ // create a one time timer to start the scheduler
271
+ setTimeout(() => {
272
+ 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.`);
273
+ scheduler.start(interval);
274
+ }, interval * 60 * 1000); // convert minutes to milliseconds
275
+ }
276
+ } else {
277
+ LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
278
+ }
279
+ } catch (error) {
280
+ LogError(`Error initializing Skip learning cycle scheduler: ${error}`);
281
+ }
282
+ }
283
+ });
284
+ } catch (error) {
285
+ // Handle any errors from the static initializer
286
+ LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
287
+ }
288
+ }
289
+ // now call the function to initialize the scheduler
290
+ initializeSkipLearningCycleScheduler();
291
+
292
+ /**
293
+ * Base type for Skip API requests containing common fields
294
+ * Used as the foundation for both chat and learning cycle requests
168
295
  */
169
296
  type BaseSkipRequest = {
297
+ /** Entity metadata to send to Skip */
170
298
  entities: SkipEntityInfo[],
299
+ /** Query metadata to send to Skip */
171
300
  queries: SkipQueryInfo[],
301
+ /** Agent notes to send to Skip */
172
302
  notes: SkipAPIAgentNote[],
303
+ /** Note type definitions to send to Skip */
173
304
  noteTypes: SkipAPIAgentNoteType[],
305
+ /** Agent requests to send to Skip */
174
306
  requests: SkipAPIAgentRequest[],
307
+ /** Access token for authorizing Skip to call back to MemberJunction */
175
308
  accessToken: GetDataAccessToken,
309
+ /** Organization identifier */
176
310
  organizationID: string,
311
+ /** Additional organization-specific information */
177
312
  organizationInfo: any,
313
+ /** API keys for various AI services to be used by Skip */
178
314
  apiKeys: SkipAPIRequestAPIKey[],
315
+ /** URL of the calling server for callback purposes */
179
316
  callingServerURL: string,
317
+ /** API key for the calling server */
180
318
  callingServerAPIKey: string,
319
+ /** Access token for the calling server */
181
320
  callingServerAccessToken: string
182
321
  }
322
+ /**
323
+ * Resolver for Skip AI interactions
324
+ * Handles conversations with Skip, learning cycles, and related operations.
325
+ * Skip is an AI agent that can analyze data, answer questions, and learn from interactions.
326
+ */
183
327
  @Resolver(AskSkipResultType)
184
328
  export class AskSkipResolver {
329
+ /** Default name for new conversations */
185
330
  private static _defaultNewChatName = 'New Chat';
186
331
 
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
- }
332
+ /** Maximum number of historical messages to include in a conversation context */
231
333
  private static _maxHistoricalMessages = 30;
232
334
 
233
335
  /**
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
336
+ * Handles a chat interaction with Skip about a specific data record
337
+ * Allows users to ask questions about a particular entity record
338
+ *
339
+ * @param UserQuestion The question or message from the user
340
+ * @param ConversationId ID of an existing conversation, or empty for a new conversation
341
+ * @param EntityName The name of the entity the record belongs to
342
+ * @param compositeKey The primary key values that identify the specific record
343
+ * @param dataSource Database connection
344
+ * @param userPayload Information about the authenticated user
345
+ * @param pubSub Publisher/subscriber for events
346
+ * @returns Result of the Skip interaction
238
347
  */
239
348
  @Query(() => AskSkipResultType)
240
349
  async ExecuteAskSkipRecordChat(
@@ -309,16 +418,26 @@ export class AskSkipResolver {
309
418
  return this.handleSimpleSkipChatPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
310
419
  }
311
420
 
421
+ /**
422
+ * Executes a Skip learning cycle
423
+ * Learning cycles allow Skip to analyze conversations and improve its knowledge and capabilities
424
+ *
425
+ * @param dataSource Database connection
426
+ * @param userPayload Information about the authenticated user
427
+ * @param ForceEntityRefresh Whether to force a refresh of entity metadata
428
+ * @returns Result of the learning cycle execution
429
+ */
312
430
  @Mutation(() => AskSkipResultType)
313
431
  async ExecuteAskSkipLearningCycle(
314
432
  @Ctx() { dataSource, userPayload }: AppContext,
315
433
  @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
316
434
  ) {
435
+ const skipConfigInfo = configInfo.askSkip;
317
436
  // First check if learning cycles are enabled in configuration
318
- if (___skipRunLearningCycles !== 'Y') {
437
+ if (!skipConfigInfo.learningCycleEnabled) {
319
438
  return {
320
439
  success: false,
321
- error: 'Learning cycles are disabled in configuration',
440
+ error: 'Learning cycles are not enabled in configuration',
322
441
  elapsedTime: 0,
323
442
  noteChanges: [],
324
443
  queryChanges: [],
@@ -327,7 +446,7 @@ export class AskSkipResolver {
327
446
  }
328
447
 
329
448
  // Check if we have a valid endpoint when cycles are enabled
330
- if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
449
+ if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
331
450
  return {
332
451
  success: false,
333
452
  error: 'Learning cycle API endpoint is not configured',
@@ -347,7 +466,7 @@ export class AskSkipResolver {
347
466
  await AIEngine.Instance.Config(false, user);
348
467
 
349
468
  // Check if this organization is already running a learning cycle using their organization ID
350
- const organizationId = ___skipAPIOrgId;
469
+ const organizationId = skipConfigInfo.orgID;
351
470
  const scheduler = LearningCycleScheduler.Instance;
352
471
  const runningStatus = scheduler.isOrganizationRunningCycle(organizationId);
353
472
 
@@ -396,55 +515,91 @@ export class AskSkipResolver {
396
515
  // Build the request to Skip learning API
397
516
  LogStatus(`Building Skip Learning API request`);
398
517
  const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, false, dataSource, user, ForceEntityRefresh || false);
518
+ if (input.newConversations.length === 0) {
519
+ // no new conversations to process
520
+ LogStatus(` Skip Learning Cycles: No new conversations to process for learning cycle`);
521
+ learningCycleEntity.Status = 'Complete';
522
+ learningCycleEntity.AgentSummary = 'No new conversations to process, learning cycle skipped, but recorded for audit purposes.';
523
+ learningCycleEntity.EndedAt = new Date();
524
+ if (!(await learningCycleEntity.Save())) {
525
+ LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
526
+ }
527
+ const result: SkipAPILearningCycleResponse = {
528
+ success: true,
529
+ learningCycleSkipped: true,
530
+ elapsedTime: 0,
531
+ noteChanges: [],
532
+ queryChanges: [],
533
+ requestChanges: [],
534
+ }
535
+ return result;
536
+ }
537
+ else {
538
+ // Make the API request
539
+ const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID);
399
540
 
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();
541
+ // Update learning cycle to completed
542
+ const endTime = new Date();
543
+ const elapsedTimeMs = endTime.getTime() - startTime.getTime();
406
544
 
407
- LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
545
+ LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
408
546
 
409
- learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
410
- learningCycleEntity.EndedAt = endTime;
547
+ learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
548
+ learningCycleEntity.EndedAt = endTime;
411
549
 
412
- if (!(await learningCycleEntity.Save())) {
413
- LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
550
+ if (!(await learningCycleEntity.Save())) {
551
+ LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
552
+ }
553
+
554
+ return response;
414
555
  }
415
-
416
- // Unregister the organization after completion
417
- scheduler.unregisterRunningCycle(organizationId);
418
-
419
- return response;
420
- } catch (error) {
556
+ }
557
+ catch (error) {
421
558
  // Make sure to update the learning cycle record as failed
422
559
  learningCycleEntity.Status = 'Failed';
423
560
  learningCycleEntity.EndedAt = new Date();
424
561
 
425
562
  try {
426
563
  await learningCycleEntity.Save();
427
- } catch (saveError) {
564
+ }
565
+ catch (saveError) {
428
566
  LogError(`Failed to update learning cycle record after error: ${saveError}`);
429
567
  }
430
568
 
431
- // Unregister the organization on error
432
- scheduler.unregisterRunningCycle(organizationId);
433
-
434
569
  // Re-throw the original error
435
570
  throw error;
436
571
  }
572
+ finally {
573
+ // Unregister the cycle/organizationId safely
574
+ try {
575
+ scheduler.unregisterRunningCycle(organizationId);
576
+ }
577
+ catch (error) {
578
+ LogError(`Failed to unregister organization ${organizationId} from running cycles: ${error}`);
579
+ }
580
+ }
437
581
  }
438
582
 
583
+ /**
584
+ * Handles the HTTP POST request to the Skip learning cycle API
585
+ * Sends the learning cycle request and processes the response
586
+ *
587
+ * @param input The learning cycle request payload
588
+ * @param user User context for the request
589
+ * @param learningCycleId ID of the current learning cycle
590
+ * @param agentID ID of the Skip agent
591
+ * @returns Response from the Skip learning cycle API
592
+ */
439
593
  protected async handleSimpleSkipLearningPostRequest(
440
594
  input: SkipAPILearningCycleRequest,
441
595
  user: UserInfo,
442
596
  learningCycleId: string,
443
597
  agentID: string
444
598
  ): Promise<SkipAPILearningCycleResponse> {
445
- LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${___skipLearningAPIurl}`);
599
+ const skipConfigInfo = configInfo.askSkip;
600
+ LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${skipConfigInfo.learningCycleURL}`);
446
601
 
447
- const response = await sendPostRequest(___skipLearningAPIurl, input, true, null);
602
+ const response = await sendPostRequest(skipConfigInfo.learningCycleURL, input, true, null);
448
603
 
449
604
  if (response && response.length > 0) {
450
605
  // the last object in the response array is the final response from the Skip API
@@ -482,6 +637,17 @@ export class AskSkipResolver {
482
637
  }
483
638
  }
484
639
 
640
+ /**
641
+ * Handles the HTTP POST request to the Skip chat API
642
+ * Sends the chat request and processes the response
643
+ *
644
+ * @param input The chat request payload
645
+ * @param conversationID ID of the conversation, or empty for a new conversation
646
+ * @param UserMessageConversationDetailId ID of the user's message in the conversation
647
+ * @param createAIMessageConversationDetail Whether to create a conversation detail for the AI response
648
+ * @param user User context for the request
649
+ * @returns Result of the Skip interaction
650
+ */
485
651
  protected async handleSimpleSkipChatPostRequest(
486
652
  input: SkipAPIRequest,
487
653
  conversationID: string = '',
@@ -489,9 +655,10 @@ export class AskSkipResolver {
489
655
  createAIMessageConversationDetail: boolean = false,
490
656
  user: UserInfo = null
491
657
  ): Promise<AskSkipResultType> {
492
- LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${___skipAPIurl}`);
658
+ const skipConfigInfo = configInfo.askSkip;
659
+ LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${skipConfigInfo.chatURL}`);
493
660
 
494
- const response = await sendPostRequest(___skipAPIurl, input, true, null);
661
+ const response = await sendPostRequest(skipConfigInfo.chatURL, input, true, null);
495
662
 
496
663
  if (response && response.length > 0) {
497
664
  // the last object in the response array is the final response from the Skip API
@@ -524,9 +691,13 @@ export class AskSkipResolver {
524
691
  }
525
692
 
526
693
  /**
527
- * Processes note changes received from the Skip API learning cycle.
694
+ * Processes note changes received from the Skip API learning cycle
695
+ * Applies changes to agent notes based on the learning cycle response
696
+ *
528
697
  * @param noteChanges Changes to agent notes
529
- * @param user The user making the request
698
+ * @param agentID ID of the Skip agent
699
+ * @param user User context for the request
700
+ * @returns Promise that resolves when processing is complete
530
701
  */
531
702
  protected async processLearningCycleNoteChanges(
532
703
  noteChanges: SkipLearningCycleNoteChange[],
@@ -560,6 +731,15 @@ export class AskSkipResolver {
560
731
  }));
561
732
  }
562
733
 
734
+ /**
735
+ * Processes an add or update operation for a Skip agent note
736
+ * Creates a new note or updates an existing one based on the change type
737
+ *
738
+ * @param change The note change information
739
+ * @param agentID ID of the Skip agent
740
+ * @param user User context for the operation
741
+ * @returns Whether the operation was successful
742
+ */
563
743
  protected async processAddOrUpdateSkipNote(change: SkipLearningCycleNoteChange, agentID: string, user: UserInfo): Promise<boolean> {
564
744
  try {
565
745
  // Get the note entity object
@@ -574,17 +754,11 @@ export class AskSkipResolver {
574
754
  return false;
575
755
  }
576
756
  } 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
757
  // Create a new note
584
758
  noteEntity.NewRecord();
585
759
  noteEntity.AgentID = agentID;
586
760
  }
587
- noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI');
761
+ noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI'); // always set to AI
588
762
  noteEntity.Note = change.note.note;
589
763
  noteEntity.Type = change.note.type;
590
764
 
@@ -605,6 +779,14 @@ export class AskSkipResolver {
605
779
  }
606
780
  }
607
781
 
782
+ /**
783
+ * Processes a delete operation for a Skip agent note
784
+ * Removes the specified note from the database
785
+ *
786
+ * @param change The note change information
787
+ * @param user User context for the operation
788
+ * @returns Whether the deletion was successful
789
+ */
608
790
  protected async processDeleteSkipNote(change: SkipLearningCycleNoteChange, user: UserInfo): Promise<boolean> {
609
791
  // Get the note entity object
610
792
  const md = new Metadata();
@@ -634,6 +816,15 @@ cycle.`);
634
816
  return true;
635
817
  }
636
818
 
819
+ /**
820
+ * Creates a conversation detail entry for an AI message
821
+ * Stores the AI response in the conversation history
822
+ *
823
+ * @param apiResponse The response from the Skip API
824
+ * @param conversationID ID of the conversation
825
+ * @param user User context for the operation
826
+ * @returns ID of the created conversation detail, or empty string if creation failed
827
+ */
637
828
  protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: string, user: UserInfo): Promise<string> {
638
829
  const md = new Metadata();
639
830
  const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
@@ -658,11 +849,14 @@ cycle.`);
658
849
 
659
850
  /**
660
851
  * Builds the base Skip API request with common fields and data
852
+ * Creates the foundation for both chat and learning cycle requests
853
+ *
661
854
  * @param contextUser The user making the request
662
855
  * @param dataSource The data source to use
663
856
  * @param includeEntities Whether to include entities in the request
664
857
  * @param includeQueries Whether to include queries in the request
665
858
  * @param includeNotes Whether to include agent notes in the request
859
+ * @param includeRequests Whether to include agent requests in the request
666
860
  * @param forceEntitiesRefresh Whether to force refresh of entities
667
861
  * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
668
862
  * @param additionalTokenInfo Additional info to include in the access token
@@ -674,15 +868,16 @@ cycle.`);
674
868
  includeEntities: boolean,
675
869
  includeQueries: boolean,
676
870
  includeNotes: boolean,
871
+ filterUserNotesToContextUser: boolean,
677
872
  includeRequests: boolean,
678
873
  forceEntitiesRefresh: boolean = false,
679
874
  includeCallBackKeyAndAccessToken: boolean = false,
680
875
  additionalTokenInfo: any = {}
681
876
  ): Promise<BaseSkipRequest> {
682
-
877
+ const skipConfigInfo = configInfo.askSkip;
683
878
  const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
684
879
  const queries = includeQueries ? this.BuildSkipQueries() : [];
685
- const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser) : {notes: [], noteTypes: []};
880
+ const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser, filterUserNotesToContextUser) : {notes: [], noteTypes: []};
686
881
  const requests = includeRequests ? await this.BuildSkipRequests(contextUser) : [];
687
882
 
688
883
  // Setup access token if needed
@@ -710,7 +905,7 @@ cycle.`);
710
905
  noteTypes,
711
906
  requests,
712
907
  accessToken,
713
- organizationID: ___skipAPIOrgId,
908
+ organizationID: skipConfigInfo.orgID,
714
909
  organizationInfo: configInfo?.askSkip?.organizationInfo,
715
910
  apiKeys: this.buildSkipAPIKeys(),
716
911
  callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
@@ -721,6 +916,19 @@ cycle.`);
721
916
 
722
917
  /**
723
918
  * Builds the learning API request for Skip
919
+ * Creates a request specific to the learning cycle operation
920
+ *
921
+ * @param learningCycleId ID of the current learning cycle
922
+ * @param lastLearningCycleDate Date of the last completed learning cycle
923
+ * @param includeEntities Whether to include entities in the request
924
+ * @param includeQueries Whether to include queries in the request
925
+ * @param includeNotes Whether to include agent notes in the request
926
+ * @param includeRequests Whether to include agent requests in the request
927
+ * @param dataSource Database connection
928
+ * @param contextUser User context for the request
929
+ * @param forceEntitiesRefresh Whether to force refresh of entities
930
+ * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
931
+ * @returns Complete learning cycle request object
724
932
  */
725
933
  protected async buildSkipLearningAPIRequest(
726
934
  learningCycleId: string,
@@ -741,6 +949,7 @@ cycle.`);
741
949
  includeEntities,
742
950
  includeQueries,
743
951
  includeNotes,
952
+ false,
744
953
  includeRequests,
745
954
  forceEntitiesRefresh,
746
955
  includeCallBackKeyAndAccessToken
@@ -768,11 +977,14 @@ cycle.`);
768
977
  }
769
978
 
770
979
  /**
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
- */
980
+ * Loads the conversations that have been updated or added since the last learning cycle
981
+ * These are used to train Skip and improve its understanding
982
+ *
983
+ * @param lastLearningCycleDate The date of the last learning cycle
984
+ * @param dataSource Database connection
985
+ * @param contextUser User context for the request
986
+ * @returns Array of conversations that are new or have been updated since the last cycle
987
+ */
776
988
  protected async BuildSkipLearningCycleNewConversations(
777
989
  lastLearningCycleDate: Date,
778
990
  dataSource: DataSource,
@@ -813,9 +1025,11 @@ cycle.`);
813
1025
  }
814
1026
 
815
1027
  /**
816
- * Builds an array of agent requests
817
- * @param contextUser the user context to load the requests
818
- * @returns Array of SkipAPIAgentRequest objects
1028
+ * Builds an array of agent requests
1029
+ * These are requests that have been made to the AI agent
1030
+ *
1031
+ * @param contextUser User context for loading the requests
1032
+ * @returns Array of agent request objects
819
1033
  */
820
1034
  protected async BuildSkipRequests(
821
1035
  contextUser: UserInfo
@@ -852,6 +1066,14 @@ cycle.`);
852
1066
  }
853
1067
  }
854
1068
 
1069
+ /**
1070
+ * Gets the date of the last complete learning cycle for the Skip agent
1071
+ * Used to determine which data to include in the next learning cycle
1072
+ *
1073
+ * @param agentID ID of the Skip agent
1074
+ * @param user User context for the query
1075
+ * @returns Date of the last complete learning cycle, or epoch if none exists
1076
+ */
855
1077
  protected async GetLastCompleteLearningCycleDate(agentID: string, user: UserInfo): Promise<Date> {
856
1078
  const md = new Metadata();
857
1079
  const rv = new RunView();
@@ -877,6 +1099,21 @@ cycle.`);
877
1099
 
878
1100
  /**
879
1101
  * Builds the chat API request for Skip
1102
+ * Creates a request specific to a chat interaction
1103
+ *
1104
+ * @param messages Array of messages in the conversation
1105
+ * @param conversationId ID of the conversation
1106
+ * @param dataContext Data context associated with the conversation
1107
+ * @param requestPhase The phase of the request (initial, clarifying, etc.)
1108
+ * @param includeEntities Whether to include entities in the request
1109
+ * @param includeQueries Whether to include queries in the request
1110
+ * @param includeNotes Whether to include agent notes in the request
1111
+ * @param includeRequests Whether to include agent requests in the request
1112
+ * @param contextUser User context for the request
1113
+ * @param dataSource Database connection
1114
+ * @param forceEntitiesRefresh Whether to force refresh of entities
1115
+ * @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
1116
+ * @returns Complete chat request object
880
1117
  */
881
1118
  protected async buildSkipChatAPIRequest(
882
1119
  messages: SkipMessage[],
@@ -905,6 +1142,7 @@ cycle.`);
905
1142
  includeEntities,
906
1143
  includeQueries,
907
1144
  includeNotes,
1145
+ true,
908
1146
  includeRequests,
909
1147
  forceEntitiesRefresh,
910
1148
  includeCallBackKeyAndAccessToken,
@@ -927,12 +1165,13 @@ cycle.`);
927
1165
  }
928
1166
 
929
1167
  /**
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
1168
+ * Builds up an array of artifacts associated with a conversation
1169
+ * Artifacts are content or documents generated during conversations
1170
+ *
1171
+ * @param contextUser User context for the query
1172
+ * @param dataSource Database connection
1173
+ * @param conversationId ID of the conversation
1174
+ * @returns Array of artifacts associated with the conversation
936
1175
  */
937
1176
  protected async buildSkipAPIArtifacts(contextUser: UserInfo, dataSource: DataSource, conversationId: string): Promise<SkipAPIArtifact[]> {
938
1177
  const md = new Metadata();
@@ -1007,10 +1246,15 @@ cycle.`);
1007
1246
 
1008
1247
 
1009
1248
  /**
1010
- * Executes a script in the context of a data context and returns the results
1011
- * @param pubSub
1012
- * @param DataContextId
1013
- * @param ScriptText
1249
+ * Executes a script in the context of a data context
1250
+ * Allows running code against data context objects
1251
+ *
1252
+ * @param dataSource Database connection
1253
+ * @param userPayload Information about the authenticated user
1254
+ * @param pubSub Publisher/subscriber for events
1255
+ * @param DataContextId ID of the data context to run the script against
1256
+ * @param ScriptText The script to execute
1257
+ * @returns Result of the script execution
1014
1258
  */
1015
1259
  @Query(() => AskSkipResultType)
1016
1260
  async ExecuteAskSkipRunScript(
@@ -1028,6 +1272,12 @@ cycle.`);
1028
1272
  return this.handleSimpleSkipChatPostRequest(input);
1029
1273
  }
1030
1274
 
1275
+ /**
1276
+ * Builds the array of API keys for various AI services
1277
+ * These are used by Skip to call external AI services
1278
+ *
1279
+ * @returns Array of API keys for different vendor services
1280
+ */
1031
1281
  protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
1032
1282
  return [
1033
1283
  {
@@ -1053,6 +1303,19 @@ cycle.`);
1053
1303
  ];
1054
1304
  }
1055
1305
 
1306
+ /**
1307
+ * Executes an analysis query with Skip
1308
+ * This is the primary entry point for general Skip conversations
1309
+ *
1310
+ * @param UserQuestion The question or message from the user
1311
+ * @param ConversationId ID of an existing conversation, or empty for a new conversation
1312
+ * @param dataSource Database connection
1313
+ * @param userPayload Information about the authenticated user
1314
+ * @param pubSub Publisher/subscriber for events
1315
+ * @param DataContextId Optional ID of a data context to use
1316
+ * @param ForceEntityRefresh Whether to force a refresh of entity metadata
1317
+ * @returns Result of the Skip interaction
1318
+ */
1056
1319
  @Query(() => AskSkipResultType)
1057
1320
  async ExecuteAskSkipAnalysisQuery(
1058
1321
  @Arg('UserQuestion', () => String) UserQuestion: string,
@@ -1104,8 +1367,11 @@ cycle.`);
1104
1367
  }
1105
1368
 
1106
1369
  /**
1107
- * Packages up the Approved queries from the metadata
1108
- * @returns
1370
+ * Packages up queries from the metadata based on their status
1371
+ * Used to provide Skip with information about available queries
1372
+ *
1373
+ * @param status The status of queries to include
1374
+ * @returns Array of query information objects
1109
1375
  */
1110
1376
  protected BuildSkipQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
1111
1377
  const md = new Metadata();
@@ -1149,9 +1415,13 @@ cycle.`);
1149
1415
  }
1150
1416
 
1151
1417
  /**
1152
- * Builds up the array of notes that are applicable for Skip to receive from MJAPI
1418
+ * Builds up the array of notes and note types for Skip
1419
+ * These notes are used to provide Skip with domain knowledge and context
1420
+ *
1421
+ * @param contextUser User context for the request
1422
+ * @returns Object containing arrays of notes and note types
1153
1423
  */
1154
- protected async BuildSkipAgentNotes(contextUser: UserInfo): Promise<{notes: SkipAPIAgentNote[], noteTypes: SkipAPIAgentNoteType[]}> {
1424
+ protected async BuildSkipAgentNotes(contextUser: UserInfo, filterUserNotesToContextUser: boolean): Promise<{notes: SkipAPIAgentNote[], noteTypes: SkipAPIAgentNoteType[]}> {
1155
1425
  try {
1156
1426
  // if already configured this does nothing, just makes sure we're configured
1157
1427
  await AIEngine.Instance.Config(false, contextUser);
@@ -1175,6 +1445,12 @@ cycle.`);
1175
1445
  }
1176
1446
  });
1177
1447
 
1448
+ if (filterUserNotesToContextUser){
1449
+ // filter out any notes that are not for this user
1450
+ notes = notes.filter((n) => n.type === 'Global' ||
1451
+ (n.type === 'User' && n.userId === contextUser.ID));
1452
+ }
1453
+
1178
1454
  noteTypes = AIEngine.Instance.AgentNoteTypes.map((r) => {
1179
1455
  return {
1180
1456
  id: r.ID,
@@ -1197,6 +1473,14 @@ cycle.`);
1197
1473
  }
1198
1474
  }
1199
1475
 
1476
+ /**
1477
+ * Packs entity rows for inclusion in Skip requests
1478
+ * Provides sample data based on entity configuration
1479
+ *
1480
+ * @param e Entity information
1481
+ * @param dataSource Database connection
1482
+ * @returns Array of entity rows based on packing configuration
1483
+ */
1200
1484
  protected async PackEntityRows(e: EntityInfo, dataSource: DataSource): Promise<any[]> {
1201
1485
  try {
1202
1486
  if (e.RowsToPackWithSchema === 'None')
@@ -1249,6 +1533,14 @@ cycle.`);
1249
1533
  }
1250
1534
  }
1251
1535
 
1536
+ /**
1537
+ * Packs possible values for an entity field
1538
+ * These values help Skip understand the domain and valid values for fields
1539
+ *
1540
+ * @param f Field information
1541
+ * @param dataSource Database connection
1542
+ * @returns Array of possible values for the field
1543
+ */
1252
1544
  protected async PackFieldPossibleValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
1253
1545
  try {
1254
1546
  if (f.ValuesToPackWithSchema === 'None') {
@@ -1291,6 +1583,14 @@ cycle.`);
1291
1583
  }
1292
1584
  }
1293
1585
 
1586
+ /**
1587
+ * Gets distinct values for a field from the database
1588
+ * Used to provide Skip with information about the possible values
1589
+ *
1590
+ * @param f Field information
1591
+ * @param dataSource Database connection
1592
+ * @returns Array of distinct values for the field
1593
+ */
1294
1594
  protected async GetFieldDistinctValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
1295
1595
  try {
1296
1596
  const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
@@ -1319,10 +1619,17 @@ cycle.`);
1319
1619
  private static __skipEntitiesCache$: BehaviorSubject<Promise<SkipEntityInfo[]> | null> = new BehaviorSubject<Promise<SkipEntityInfo[]> | null>(null);
1320
1620
  private static __lastRefreshTime: number = 0;
1321
1621
 
1622
+ /**
1623
+ * Refreshes the Skip entities cache
1624
+ * Rebuilds the entity information that is provided to Skip
1625
+ *
1626
+ * @param dataSource Database connection
1627
+ * @returns Updated array of entity information
1628
+ */
1322
1629
  private async refreshSkipEntities(dataSource: DataSource): Promise<SkipEntityInfo[]> {
1323
1630
  try {
1324
1631
  const md = new Metadata();
1325
- const skipSpecialIncludeEntities = (configInfo.askSkip?.entitiesToSendSkip?.includeEntitiesFromExcludedSchemas ?? [])
1632
+ const skipSpecialIncludeEntities = (configInfo.askSkip?.entitiesToSend?.includeEntitiesFromExcludedSchemas ?? [])
1326
1633
  .map((e) => e.trim().toLowerCase());
1327
1634
 
1328
1635
  // get the list of entities
@@ -1352,6 +1659,15 @@ cycle.`);
1352
1659
  }
1353
1660
  }
1354
1661
 
1662
+ /**
1663
+ * Builds or retrieves Skip entities from cache
1664
+ * Uses caching to avoid expensive rebuilding of entity information
1665
+ *
1666
+ * @param dataSource Database connection
1667
+ * @param forceRefresh Whether to force a refresh regardless of cache state
1668
+ * @param refreshIntervalMinutes Minutes before cache expires
1669
+ * @returns Array of entity information
1670
+ */
1355
1671
  public async BuildSkipEntities(dataSource: DataSource, forceRefresh: boolean = false, refreshIntervalMinutes: number = 15): Promise<SkipEntityInfo[]> {
1356
1672
  try {
1357
1673
  const now = Date.now();
@@ -1372,6 +1688,14 @@ cycle.`);
1372
1688
  }
1373
1689
  }
1374
1690
 
1691
+ /**
1692
+ * Packs information about a single entity for Skip
1693
+ * Includes fields, relationships, and sample data
1694
+ *
1695
+ * @param e Entity information
1696
+ * @param dataSource Database connection
1697
+ * @returns Packaged entity information
1698
+ */
1375
1699
  protected async PackSingleSkipEntityInfo(e: EntityInfo, dataSource: DataSource): Promise<SkipEntityInfo> {
1376
1700
  try {
1377
1701
  const ret: SkipEntityInfo = {
@@ -1405,6 +1729,13 @@ cycle.`);
1405
1729
  }
1406
1730
  }
1407
1731
 
1732
+ /**
1733
+ * Packs information about a single entity relationship
1734
+ * These relationships help Skip understand the data model
1735
+ *
1736
+ * @param r Relationship information
1737
+ * @returns Packaged relationship information
1738
+ */
1408
1739
  protected PackSingleSkipEntityRelationship(r: EntityRelationshipInfo): SkipEntityRelationshipInfo {
1409
1740
  try {
1410
1741
  return {
@@ -1428,6 +1759,14 @@ cycle.`);
1428
1759
  }
1429
1760
  }
1430
1761
 
1762
+ /**
1763
+ * Packs information about a single entity field
1764
+ * Includes metadata and possible values
1765
+ *
1766
+ * @param f Field information
1767
+ * @param dataSource Database connection
1768
+ * @returns Packaged field information
1769
+ */
1431
1770
  protected async PackSingleSkipEntityField(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldInfo> {
1432
1771
  try {
1433
1772
  return {
@@ -1468,6 +1807,19 @@ cycle.`);
1468
1807
  }
1469
1808
  }
1470
1809
 
1810
+ /**
1811
+ * Handles initial object loading for Skip chat interactions
1812
+ * Creates or loads conversation objects, data contexts, and other required entities
1813
+ *
1814
+ * @param dataSource Database connection
1815
+ * @param ConversationId ID of an existing conversation, or empty for a new one
1816
+ * @param UserQuestion The user's question or message
1817
+ * @param user User information
1818
+ * @param userPayload User payload from context
1819
+ * @param md Metadata instance
1820
+ * @param DataContextId Optional ID of a data context to use
1821
+ * @returns Object containing loaded entities and contexts
1822
+ */
1471
1823
  protected async HandleSkipChatInitialObjectLoading(
1472
1824
  dataSource: DataSource,
1473
1825
  ConversationId: string,
@@ -1593,6 +1945,15 @@ cycle.`);
1593
1945
  return { dataContext, convoEntity, dataContextEntity, convoDetailEntity };
1594
1946
  }
1595
1947
 
1948
+ /**
1949
+ * Loads conversation details from the database and transforms them into Skip message format
1950
+ * Used to provide Skip with conversation history for context
1951
+ *
1952
+ * @param dataSource Database connection
1953
+ * @param ConversationId ID of the conversation to load details for
1954
+ * @param maxHistoricalMessages Maximum number of historical messages to include
1955
+ * @returns Array of messages in Skip format
1956
+ */
1596
1957
  protected async LoadConversationDetailsIntoSkipMessages(
1597
1958
  dataSource: DataSource,
1598
1959
  ConversationId: string,
@@ -1684,6 +2045,13 @@ cycle.`);
1684
2045
  }
1685
2046
  }
1686
2047
 
2048
+ /**
2049
+ * Maps database role values to Skip API role format
2050
+ * Converts role names from database format to the format expected by Skip API
2051
+ *
2052
+ * @param role Database role value
2053
+ * @returns Skip API role value ('user' or 'system')
2054
+ */
1687
2055
  protected MapDBRoleToSkipRole(role: string): 'user' | 'system' {
1688
2056
  switch (role.trim().toLowerCase()) {
1689
2057
  case 'ai':
@@ -1695,6 +2063,25 @@ cycle.`);
1695
2063
  }
1696
2064
  }
1697
2065
 
2066
+ /**
2067
+ * Handles the main Skip chat request processing flow
2068
+ * Routes the request through the different phases based on the Skip API response
2069
+ *
2070
+ * @param input Skip API request to send
2071
+ * @param UserQuestion The question or message from the user
2072
+ * @param user User information
2073
+ * @param dataSource Database connection
2074
+ * @param ConversationId ID of the conversation
2075
+ * @param userPayload User payload from context
2076
+ * @param pubSub Publisher/subscriber for events
2077
+ * @param md Metadata instance
2078
+ * @param convoEntity Conversation entity
2079
+ * @param convoDetailEntity Conversation detail entity for the user message
2080
+ * @param dataContext Data context associated with the conversation
2081
+ * @param dataContextEntity Data context entity
2082
+ * @param conversationDetailCount Tracking count to prevent infinite loops
2083
+ * @returns Result of the Skip interaction
2084
+ */
1698
2085
  protected async HandleSkipChatRequest(
1699
2086
  input: SkipAPIRequest,
1700
2087
  UserQuestion: string,
@@ -1710,7 +2097,8 @@ cycle.`);
1710
2097
  dataContextEntity: DataContextEntity,
1711
2098
  conversationDetailCount: number
1712
2099
  ): Promise<AskSkipResultType> {
1713
- LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${___skipAPIurl}`);
2100
+ const skipConfigInfo = configInfo.askSkip;
2101
+ LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${skipConfigInfo.chatURL}`);
1714
2102
 
1715
2103
  if (conversationDetailCount > 10) {
1716
2104
  // At this point it is likely that we are stuck in a loop, so we stop here
@@ -1736,7 +2124,7 @@ cycle.`);
1736
2124
  }
1737
2125
 
1738
2126
  const response = await sendPostRequest(
1739
- ___skipAPIurl,
2127
+ skipConfigInfo.chatURL,
1740
2128
  input,
1741
2129
  true,
1742
2130
  null,
@@ -1849,6 +2237,15 @@ cycle.`);
1849
2237
  }
1850
2238
  }
1851
2239
 
2240
+ /**
2241
+ * Publishes a status update message to the user based on the Skip API response
2242
+ * Provides feedback about what phase of processing is happening
2243
+ *
2244
+ * @param apiResponse The response from the Skip API
2245
+ * @param userPayload User payload from context
2246
+ * @param conversationID ID of the conversation
2247
+ * @param pubSub Publisher/subscriber for events
2248
+ */
1852
2249
  protected async PublishApiResponseUserUpdateMessage(
1853
2250
  apiResponse: SkipAPIResponse,
1854
2251
  userPayload: UserPayload,
@@ -1881,6 +2278,24 @@ cycle.`);
1881
2278
  });
1882
2279
  }
1883
2280
 
2281
+ /**
2282
+ * Handles the analysis complete phase of the Skip chat process
2283
+ * Finalizes the conversation and creates necessary artifacts
2284
+ *
2285
+ * @param apiRequest The original request sent to Skip
2286
+ * @param apiResponse The analysis complete response from Skip
2287
+ * @param UserQuestion The original user question
2288
+ * @param user User information
2289
+ * @param dataSource Database connection
2290
+ * @param ConversationId ID of the conversation
2291
+ * @param userPayload User payload from context
2292
+ * @param pubSub Publisher/subscriber for events
2293
+ * @param convoEntity Conversation entity
2294
+ * @param convoDetailEntity Conversation detail entity for the user message
2295
+ * @param dataContext Data context associated with the conversation
2296
+ * @param dataContextEntity Data context entity
2297
+ * @returns Result of the Skip interaction
2298
+ */
1884
2299
  protected async HandleAnalysisComplete(
1885
2300
  apiRequest: SkipAPIRequest,
1886
2301
  apiResponse: SkipAPIAnalysisCompleteResponse,
@@ -1927,6 +2342,22 @@ cycle.`);
1927
2342
  return response;
1928
2343
  }
1929
2344
 
2345
+ /**
2346
+ * Handles the clarifying question phase of the Skip chat process
2347
+ * Creates a conversation detail for the clarifying question from Skip
2348
+ *
2349
+ * @param apiRequest The original request sent to Skip
2350
+ * @param apiResponse The clarifying question response from Skip
2351
+ * @param UserQuestion The original user question
2352
+ * @param user User information
2353
+ * @param dataSource Database connection
2354
+ * @param ConversationId ID of the conversation
2355
+ * @param userPayload User payload from context
2356
+ * @param pubSub Publisher/subscriber for events
2357
+ * @param convoEntity Conversation entity
2358
+ * @param convoDetailEntity Conversation detail entity for the user message
2359
+ * @returns Result of the Skip interaction
2360
+ */
1930
2361
  protected async HandleClarifyingQuestionPhase(
1931
2362
  apiRequest: SkipAPIRequest,
1932
2363
  apiResponse: SkipAPIClarifyingQuestionResponse,
@@ -1975,6 +2406,25 @@ cycle.`);
1975
2406
  }
1976
2407
  }
1977
2408
 
2409
+ /**
2410
+ * Handles the data request phase of the Skip chat process
2411
+ * Processes data requests from Skip and loads requested data
2412
+ *
2413
+ * @param apiRequest The original request sent to Skip
2414
+ * @param apiResponse The data request response from Skip
2415
+ * @param UserQuestion The original user question
2416
+ * @param user User information
2417
+ * @param dataSource Database connection
2418
+ * @param ConversationId ID of the conversation
2419
+ * @param userPayload User payload from context
2420
+ * @param pubSub Publisher/subscriber for events
2421
+ * @param convoEntity Conversation entity
2422
+ * @param convoDetailEntity Conversation detail entity for the user message
2423
+ * @param dataContext Data context associated with the conversation
2424
+ * @param dataContextEntity Data context entity
2425
+ * @param conversationDetailCount Tracking count to prevent infinite loops
2426
+ * @returns Result of the Skip interaction
2427
+ */
1978
2428
  protected async HandleDataRequestPhase(
1979
2429
  apiRequest: SkipAPIRequest,
1980
2430
  apiResponse: SkipAPIDataRequestResponse,
@@ -2126,14 +2576,19 @@ cycle.`);
2126
2576
  }
2127
2577
 
2128
2578
  /**
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
2579
+ * Finishes a successful conversation and notifies the user
2580
+ * Creates necessary records, artifacts, and notifications
2581
+ *
2582
+ * @param apiResponse The analysis complete response from Skip
2583
+ * @param dataContext Data context associated with the conversation
2584
+ * @param dataContextEntity Data context entity
2585
+ * @param md Metadata instance
2586
+ * @param user User information
2587
+ * @param convoEntity Conversation entity
2588
+ * @param pubSub Publisher/subscriber for events
2589
+ * @param userPayload User payload from context
2590
+ * @param dataSource Database connection
2591
+ * @returns The ID of the AI message conversation detail
2137
2592
  */
2138
2593
  protected async FinishConversationAndNotifyUser(
2139
2594
  apiResponse: SkipAPIAnalysisCompleteResponse,
@@ -2159,12 +2614,17 @@ cycle.`);
2159
2614
  artifactId = apiResponse.artifactRequest.artifactId; // will only be populated if action == new_artifact_version
2160
2615
  let newVersion: number = 0;
2161
2616
  if (apiResponse.artifactRequest?.action === 'new_artifact') {
2162
- const artifactEntity = await md.GetEntityObject<ConversationArtifactEntity>('MJ: Convesration Artifacts', user);
2617
+ const artifactEntity = await md.GetEntityObject<ConversationArtifactEntity>('MJ: Conversation Artifacts', user);
2163
2618
  // create the new artifact here
2164
2619
  artifactEntity.NewRecord();
2165
2620
  artifactEntity.ConversationID = convoEntity.ID;
2166
2621
  artifactEntity.Name = apiResponse.artifactRequest.name;
2167
2622
  artifactEntity.Description = apiResponse.artifactRequest.description;
2623
+ // make sure AI Engine is configured.
2624
+ await AIEngine.Instance.Config(false, user)
2625
+ artifactEntity.ArtifactTypeID = AIEngine.Instance.ArtifactTypes.find((t) => t.Name === 'Report')?.ID;
2626
+ artifactEntity.SharingScope = 'None';
2627
+
2168
2628
  if (await artifactEntity.Save()) {
2169
2629
  // saved, grab the new ID
2170
2630
  artifactId = artifactEntity.ID;
@@ -2176,13 +2636,19 @@ cycle.`);
2176
2636
  }
2177
2637
  else {
2178
2638
  // we are updating an existing artifact with a new vesrion so we need to get the old max version and increment it
2179
- const ei = md.EntityByName("MJ: Convesration Artifacts");
2180
- const sSQL = `SELECT ISNULL(MAX(Version),0) AS MaxVersion FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ID = '${artifactId}'`;
2181
- const result = await dataSource.query(sSQL);
2182
- if (result && result.length > 0) {
2183
- newVersion = result[0].MaxVersion + 1;
2184
- } else {
2185
- LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, result);
2639
+ const ei = md.EntityByName("MJ: Conversation Artifact Versions");
2640
+ const sSQL = `SELECT ISNULL(MAX(Version),0) AS MaxVersion FROM [${ei.SchemaName}].[${ei.BaseView}] WHERE ConversationArtifactID = '${artifactId}'`;
2641
+ try {
2642
+ const result = await dataSource.query(sSQL);
2643
+ if (result && result.length > 0) {
2644
+ newVersion = result[0].MaxVersion + 1;
2645
+ }
2646
+ else {
2647
+ LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, result);
2648
+ }
2649
+ }
2650
+ catch (e) {
2651
+ LogError(`Error getting max version for artifact ID: ${artifactId}`, undefined, e);
2186
2652
  }
2187
2653
  }
2188
2654
  if (artifactId && newVersion > 0) {
@@ -2283,18 +2749,34 @@ cycle.`);
2283
2749
  };
2284
2750
  }
2285
2751
 
2286
- protected getAgentNoteTypeIDByName(name: string): string {
2752
+ /**
2753
+ * Gets the ID of an agent note type by its name
2754
+ * Falls back to a default note type if the specified one is not found
2755
+ *
2756
+ * @param name Name of the agent note type
2757
+ * @param defaultNoteType Default note type to use if the specified one is not found
2758
+ * @returns ID of the agent note type
2759
+ */
2760
+ protected getAgentNoteTypeIDByName(name: string, defaultNoteType: string = 'AI'): string {
2287
2761
  const noteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === name.trim().toLowerCase())?.ID;
2288
2762
  if (noteTypeID) {
2289
2763
  return noteTypeID;
2290
2764
  }
2291
2765
  else{
2292
- // default to AI note ID
2293
- const AINoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === 'AI')?.ID;
2294
- return AINoteTypeID
2766
+ // default
2767
+ const defaultNoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === defaultNoteType.trim().toLowerCase())?.ID;
2768
+ return defaultNoteTypeID;
2295
2769
  }
2296
2770
  }
2297
2771
 
2772
+ /**
2773
+ * Gets data from a view
2774
+ * Helper method to run a view and retrieve its data
2775
+ *
2776
+ * @param ViewId ID of the view to run
2777
+ * @param user User context for the query
2778
+ * @returns Results of the view query
2779
+ */
2298
2780
  protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
2299
2781
  const rv = new RunView();
2300
2782
  const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
@@ -2303,8 +2785,11 @@ cycle.`);
2303
2785
  }
2304
2786
 
2305
2787
  /**
2306
- * Manually executes the Skip AI learning cycle.
2788
+ * Manually executes the Skip AI learning cycle
2789
+ * Allows triggering a learning cycle on demand rather than waiting for scheduled execution
2790
+ *
2307
2791
  * @param OrganizationId Optional organization ID to register for this run
2792
+ * @returns Result of the manual learning cycle execution
2308
2793
  */
2309
2794
  @Mutation(() => ManualLearningCycleResultType)
2310
2795
  async ManuallyExecuteSkipLearningCycle(
@@ -2312,17 +2797,17 @@ cycle.`);
2312
2797
  ): Promise<ManualLearningCycleResultType> {
2313
2798
  try {
2314
2799
  LogStatus('Manual execution of Skip learning cycle requested via API');
2315
-
2800
+ const skipConfigInfo = configInfo.askSkip;
2316
2801
  // First check if learning cycles are enabled in configuration
2317
- if (___skipRunLearningCycles !== 'Y') {
2802
+ if (!skipConfigInfo.learningCycleEnabled) {
2318
2803
  return {
2319
2804
  Success: false,
2320
- Message: 'Learning cycles are disabled in configuration'
2805
+ Message: 'Learning cycles are not enabled in configuration'
2321
2806
  };
2322
2807
  }
2323
2808
 
2324
2809
  // Check if we have a valid endpoint when cycles are enabled
2325
- if (!___skipLearningAPIurl || ___skipLearningAPIurl.trim() === '') {
2810
+ if (!skipConfigInfo.learningCycleURL || skipConfigInfo.learningCycleURL.trim().length === 0) {
2326
2811
  return {
2327
2812
  Success: false,
2328
2813
  Message: 'Learning cycle API endpoint is not configured'
@@ -2330,7 +2815,7 @@ cycle.`);
2330
2815
  }
2331
2816
 
2332
2817
  // Use the organization ID from config if not provided
2333
- const orgId = OrganizationId || ___skipAPIOrgId;
2818
+ const orgId = OrganizationId || skipConfigInfo.orgID;
2334
2819
 
2335
2820
  // Call the scheduler's manual execution method with org ID
2336
2821
  const result = await LearningCycleScheduler.Instance.manuallyExecuteLearningCycle(orgId);
@@ -2353,6 +2838,9 @@ cycle.`);
2353
2838
 
2354
2839
  /**
2355
2840
  * Gets the current status of the learning cycle scheduler
2841
+ * Provides information about the scheduler state and any running cycles
2842
+ *
2843
+ * @returns Status information about the learning cycle scheduler
2356
2844
  */
2357
2845
  @Query(() => LearningCycleStatusType)
2358
2846
  async GetLearningCycleStatus(): Promise<LearningCycleStatusType> {
@@ -2382,15 +2870,19 @@ cycle.`);
2382
2870
 
2383
2871
  /**
2384
2872
  * Checks if a specific organization is running a learning cycle
2873
+ * Used to determine if a new learning cycle can be started for an organization
2874
+ *
2385
2875
  * @param OrganizationId The organization ID to check
2876
+ * @returns Information about the running cycle, or null if no cycle is running
2386
2877
  */
2387
2878
  @Query(() => RunningOrganizationType, { nullable: true })
2388
2879
  async IsOrganizationRunningLearningCycle(
2389
2880
  @Arg('OrganizationId', () => String) OrganizationId: string
2390
2881
  ): Promise<RunningOrganizationType | null> {
2391
2882
  try {
2883
+ const skipConfigInfo = configInfo.askSkip;
2392
2884
  // Use the organization ID from config if not provided
2393
- const orgId = OrganizationId || ___skipAPIOrgId;
2885
+ const orgId = OrganizationId || skipConfigInfo.orgID;
2394
2886
 
2395
2887
  const status = LearningCycleScheduler.Instance.isOrganizationRunningCycle(orgId);
2396
2888
 
@@ -2413,7 +2905,10 @@ cycle.`);
2413
2905
 
2414
2906
  /**
2415
2907
  * Stops a running learning cycle for a specific organization
2908
+ * Allows manual intervention to stop a learning cycle that is taking too long or causing issues
2909
+ *
2416
2910
  * @param OrganizationId The organization ID to stop the cycle for
2911
+ * @returns Result of the stop operation, including details about the stopped cycle
2417
2912
  */
2418
2913
  @Mutation(() => StopLearningCycleResultType)
2419
2914
  async StopLearningCycleForOrganization(
@@ -2421,7 +2916,7 @@ cycle.`);
2421
2916
  ): Promise<StopLearningCycleResultType> {
2422
2917
  try {
2423
2918
  // Use the organization ID from config if not provided
2424
- const orgId = OrganizationId || ___skipAPIOrgId;
2919
+ const orgId = OrganizationId || configInfo.askSkip.orgID;
2425
2920
 
2426
2921
  const result = LearningCycleScheduler.Instance.stopLearningCycleForOrganization(orgId);
2427
2922