@memberjunction/server 2.35.0 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/config.d.ts +69 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -1
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +15 -12
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +73 -58
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +60 -5
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +587 -31
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +29 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -0
- package/dist/rest/EntityCRUDHandler.js +197 -0
- package/dist/rest/EntityCRUDHandler.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +41 -0
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -0
- package/dist/rest/RESTEndpointHandler.js +537 -0
- package/dist/rest/RESTEndpointHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +21 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -0
- package/dist/rest/ViewOperationsHandler.js +144 -0
- package/dist/rest/ViewOperationsHandler.js.map +1 -0
- package/dist/rest/index.d.ts +5 -0
- package/dist/rest/index.d.ts.map +1 -0
- package/dist/rest/index.js +5 -0
- package/dist/rest/index.js.map +1 -0
- package/dist/rest/setupRESTEndpoints.d.ts +12 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -0
- package/dist/rest/setupRESTEndpoints.js +27 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -0
- package/dist/scheduler/LearningCycleScheduler.d.ts +44 -0
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -0
- package/dist/scheduler/LearningCycleScheduler.js +188 -0
- package/dist/scheduler/LearningCycleScheduler.js.map +1 -0
- package/package.json +24 -26
- package/src/config.ts +15 -1
- package/src/generated/generated.ts +53 -44
- package/src/index.ts +56 -1
- package/src/resolvers/AskSkipResolver.ts +787 -51
- package/src/rest/EntityCRUDHandler.ts +279 -0
- package/src/rest/RESTEndpointHandler.ts +834 -0
- package/src/rest/ViewOperationsHandler.ts +207 -0
- package/src/rest/index.ts +4 -0
- package/src/rest/setupRESTEndpoints.ts +89 -0
- package/src/scheduler/LearningCycleScheduler.ts +312 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
-
import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo } from '@memberjunction/core';
|
|
3
|
-
import { AppContext, UserPayload } from '../types.js';
|
|
1
|
+
import { Arg, Ctx, Field, Int, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, AllMetadataArrays } from '@memberjunction/core';
|
|
3
|
+
import { AppContext, UserPayload, MJ_SERVER_EVENT_CODE } from '../types.js';
|
|
4
4
|
import { BehaviorSubject } from 'rxjs';
|
|
5
5
|
import { take } from 'rxjs/operators';
|
|
6
6
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
7
7
|
import { DataContext } from '@memberjunction/data-context';
|
|
8
8
|
import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
|
|
9
|
+
import { LearningCycleScheduler } from '../scheduler/LearningCycleScheduler.js';
|
|
9
10
|
LoadDataContextItemsServer(); // prevent tree shaking since the DataContextItemServer class is not directly referenced in this file or otherwise statically instantiated, so it could be removed by the build process
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -25,10 +26,20 @@ import {
|
|
|
25
26
|
SkipEntityFieldInfo,
|
|
26
27
|
SkipEntityRelationshipInfo,
|
|
27
28
|
SkipEntityFieldValueInfo,
|
|
29
|
+
SkipAPILearningCycleRequest,
|
|
30
|
+
SkipAPILearningCycleResponse,
|
|
31
|
+
SkipLearningCycleNoteChange,
|
|
32
|
+
SkipConversation,
|
|
33
|
+
SkipAPIArtifact,
|
|
34
|
+
SkipAPIAgentRequest,
|
|
28
35
|
} from '@memberjunction/skip-types';
|
|
29
36
|
|
|
30
37
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
38
|
+
|
|
31
39
|
import {
|
|
40
|
+
AIAgentLearningCycleEntity,
|
|
41
|
+
AIAgentNoteEntity,
|
|
42
|
+
AIAgentRequestEntity,
|
|
32
43
|
ConversationDetailEntity,
|
|
33
44
|
ConversationEntity,
|
|
34
45
|
DataContextEntity,
|
|
@@ -36,7 +47,7 @@ import {
|
|
|
36
47
|
UserNotificationEntity,
|
|
37
48
|
} from '@memberjunction/core-entities';
|
|
38
49
|
import { DataSource } from 'typeorm';
|
|
39
|
-
import { ___skipAPIOrgId, ___skipAPIurl, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
|
|
50
|
+
import { ___skipAPIOrgId, ___skipAPIurl, ___skipLearningAPIurl, ___skipLearningCycleIntervalInMinutes, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
|
|
40
51
|
|
|
41
52
|
import { registerEnumType } from 'type-graphql';
|
|
42
53
|
import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
|
|
@@ -81,9 +92,111 @@ export class AskSkipResultType {
|
|
|
81
92
|
AIMessageConversationDetailId: string;
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
@ObjectType()
|
|
96
|
+
export class ManualLearningCycleResultType {
|
|
97
|
+
@Field(() => Boolean)
|
|
98
|
+
Success: boolean;
|
|
99
|
+
|
|
100
|
+
@Field(() => String)
|
|
101
|
+
Message: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@ObjectType()
|
|
105
|
+
export class CycleDetailsType {
|
|
106
|
+
@Field(() => String)
|
|
107
|
+
LearningCycleId: string;
|
|
108
|
+
|
|
109
|
+
@Field(() => String)
|
|
110
|
+
StartTime: string;
|
|
111
|
+
|
|
112
|
+
@Field(() => Number)
|
|
113
|
+
RunningForMinutes: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@ObjectType()
|
|
117
|
+
export class RunningOrganizationType {
|
|
118
|
+
@Field(() => String)
|
|
119
|
+
OrganizationId: string;
|
|
120
|
+
|
|
121
|
+
@Field(() => String)
|
|
122
|
+
LearningCycleId: string;
|
|
123
|
+
|
|
124
|
+
@Field(() => String)
|
|
125
|
+
StartTime: string;
|
|
126
|
+
|
|
127
|
+
@Field(() => Number)
|
|
128
|
+
RunningForMinutes: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@ObjectType()
|
|
132
|
+
export class LearningCycleStatusType {
|
|
133
|
+
@Field(() => Boolean)
|
|
134
|
+
IsSchedulerRunning: boolean;
|
|
135
|
+
|
|
136
|
+
@Field(() => String, { nullable: true })
|
|
137
|
+
LastRunTime: string;
|
|
138
|
+
|
|
139
|
+
@Field(() => [RunningOrganizationType], { nullable: true })
|
|
140
|
+
RunningOrganizations: RunningOrganizationType[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@ObjectType()
|
|
144
|
+
export class StopLearningCycleResultType {
|
|
145
|
+
@Field(() => Boolean)
|
|
146
|
+
Success: boolean;
|
|
147
|
+
|
|
148
|
+
@Field(() => String)
|
|
149
|
+
Message: string;
|
|
150
|
+
|
|
151
|
+
@Field(() => Boolean)
|
|
152
|
+
WasRunning: boolean;
|
|
153
|
+
|
|
154
|
+
@Field(() => CycleDetailsType, { nullable: true })
|
|
155
|
+
CycleDetails: CycleDetailsType;
|
|
156
|
+
}
|
|
157
|
+
|
|
84
158
|
@Resolver(AskSkipResultType)
|
|
85
159
|
export class AskSkipResolver {
|
|
86
160
|
private static _defaultNewChatName = 'New Chat';
|
|
161
|
+
|
|
162
|
+
// Static initializer that runs when the class is loaded - initializes the learning cycle scheduler
|
|
163
|
+
static {
|
|
164
|
+
try {
|
|
165
|
+
LogStatus('Initializing Skip AI Learning Cycle Scheduler');
|
|
166
|
+
|
|
167
|
+
// Set up event listener for server initialization
|
|
168
|
+
const eventListener = MJGlobal.Instance.GetEventListener(true);
|
|
169
|
+
eventListener.subscribe(event => {
|
|
170
|
+
// Filter for our server's setup complete event
|
|
171
|
+
if (event.eventCode === MJ_SERVER_EVENT_CODE && event.args?.type === 'setupComplete') {
|
|
172
|
+
try {
|
|
173
|
+
const dataSources = event.args.dataSources;
|
|
174
|
+
if (dataSources && dataSources.length > 0) {
|
|
175
|
+
// Initialize the scheduler
|
|
176
|
+
const scheduler = LearningCycleScheduler.Instance;
|
|
177
|
+
|
|
178
|
+
// Set the data sources for the scheduler
|
|
179
|
+
scheduler.setDataSources(dataSources);
|
|
180
|
+
|
|
181
|
+
// Default is 60 minutes, if the interval is not set in the config, use 60 minutes
|
|
182
|
+
const interval = ___skipLearningCycleIntervalInMinutes ?? 60;
|
|
183
|
+
scheduler.start(interval);
|
|
184
|
+
LogStatus(`📅 Skip AI Learning cycle scheduler started with ${interval} minute interval`);
|
|
185
|
+
} else {
|
|
186
|
+
LogError('Cannot initialize Skip learning cycle scheduler: No data sources available');
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
LogError(`Error initializing Skip learning cycle scheduler: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
LogStatus('Skip AI Learning Cycle Scheduler initialization listener registered');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// Handle any errors from the static initializer
|
|
197
|
+
LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
87
200
|
private static _maxHistoricalMessages = 30;
|
|
88
201
|
|
|
89
202
|
/**
|
|
@@ -118,7 +231,7 @@ export class AskSkipResolver {
|
|
|
118
231
|
}
|
|
119
232
|
|
|
120
233
|
const md = new Metadata();
|
|
121
|
-
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.
|
|
234
|
+
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
|
|
122
235
|
dataSource,
|
|
123
236
|
ConversationId,
|
|
124
237
|
UserQuestion,
|
|
@@ -155,24 +268,175 @@ export class AskSkipResolver {
|
|
|
155
268
|
}
|
|
156
269
|
}
|
|
157
270
|
|
|
158
|
-
const input = await this.
|
|
271
|
+
const input = await this.buildSkipChatAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false, false, false, user, dataSource, false, false);
|
|
159
272
|
messages.push({
|
|
160
273
|
content: UserQuestion,
|
|
161
274
|
role: 'user',
|
|
162
275
|
conversationDetailID: convoDetailEntity.ID,
|
|
163
276
|
});
|
|
164
277
|
|
|
165
|
-
return this.
|
|
278
|
+
return this.handleSimpleSkipChatPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
|
|
166
279
|
}
|
|
167
280
|
|
|
168
|
-
|
|
281
|
+
@Mutation(() => AskSkipResultType)
|
|
282
|
+
async ExecuteAskSkipLearningCycle(
|
|
283
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
284
|
+
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
|
|
285
|
+
) {
|
|
286
|
+
const startTime = new Date();
|
|
287
|
+
// First, get the user from the cache
|
|
288
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
289
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
290
|
+
|
|
291
|
+
// if already configured this does nothing, just makes sure we're configured
|
|
292
|
+
await AIEngine.Instance.Config(false, user);
|
|
293
|
+
|
|
294
|
+
// Check if this organization is already running a learning cycle using their organization ID
|
|
295
|
+
const organizationId = ___skipAPIOrgId;
|
|
296
|
+
const scheduler = LearningCycleScheduler.Instance;
|
|
297
|
+
const runningStatus = scheduler.isOrganizationRunningCycle(organizationId);
|
|
298
|
+
|
|
299
|
+
if (runningStatus.isRunning) {
|
|
300
|
+
LogStatus(`Learning cycle already in progress for organization ${organizationId}, started at ${runningStatus.startTime.toISOString()}`);
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: `Learning cycle already in progress for this organization (started ${Math.round(runningStatus.runningForMinutes)} minutes ago)`,
|
|
304
|
+
elapsedTime: 0,
|
|
305
|
+
noteChanges: [],
|
|
306
|
+
queryChanges: [],
|
|
307
|
+
requestChanges: []
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
LogStatus(`Starting learning cycle for AI agent Skip`);
|
|
312
|
+
|
|
313
|
+
// Get the Skip agent ID
|
|
314
|
+
const md = new Metadata();
|
|
315
|
+
const skipAgent = AIEngine.Instance.GetAgentByName('Skip');
|
|
316
|
+
if (!skipAgent) {
|
|
317
|
+
throw new Error("Skip agent not found in AIEngine");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const agentID = skipAgent.ID;
|
|
321
|
+
|
|
322
|
+
// Get last complete learning cycle start date for this agent
|
|
323
|
+
const lastCompleteLearningCycleDate = await this.GetLastCompleteLearningCycleDate(agentID, user);
|
|
324
|
+
|
|
325
|
+
// Create a new learning cycle record for this run
|
|
326
|
+
const learningCycleEntity = await md.GetEntityObject<AIAgentLearningCycleEntity>('AI Agent Learning Cycles', user);
|
|
327
|
+
learningCycleEntity.NewRecord();
|
|
328
|
+
learningCycleEntity.AgentID = skipAgent.ID;
|
|
329
|
+
learningCycleEntity.Status = 'In-Progress';
|
|
330
|
+
learningCycleEntity.StartedAt = startTime;
|
|
331
|
+
|
|
332
|
+
if (!(await learningCycleEntity.Save())) {
|
|
333
|
+
throw new Error(`Failed to create learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const learningCycleId = learningCycleEntity.ID;
|
|
337
|
+
LogStatus(`Created new learning cycle with ID: ${learningCycleId}`);
|
|
338
|
+
|
|
339
|
+
// Register this organization as running a learning cycle
|
|
340
|
+
scheduler.registerRunningCycle(organizationId, learningCycleId);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
// Build the request to Skip learning API
|
|
344
|
+
LogStatus(`Building Skip Learning API request`);
|
|
345
|
+
const input = await this.buildSkipLearningAPIRequest(learningCycleId, lastCompleteLearningCycleDate, true, true, true, true, dataSource, user, ForceEntityRefresh || false);
|
|
346
|
+
|
|
347
|
+
// Make the API request
|
|
348
|
+
const response = await this.handleSimpleSkipLearningPostRequest(input, user, learningCycleId, agentID);
|
|
349
|
+
|
|
350
|
+
// Update learning cycle to completed
|
|
351
|
+
const endTime = new Date();
|
|
352
|
+
const elapsedTimeMs = endTime.getTime() - startTime.getTime();
|
|
353
|
+
|
|
354
|
+
LogStatus(`Learning cycle finished with status: ${response.success ? 'Success' : 'Failed'} in ${elapsedTimeMs / 1000} seconds`);
|
|
355
|
+
|
|
356
|
+
learningCycleEntity.Status = response.success ? 'Complete' : 'Failed';
|
|
357
|
+
learningCycleEntity.EndedAt = endTime;
|
|
358
|
+
|
|
359
|
+
if (!(await learningCycleEntity.Save())) {
|
|
360
|
+
LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Unregister the organization after completion
|
|
364
|
+
scheduler.unregisterRunningCycle(organizationId);
|
|
365
|
+
|
|
366
|
+
return response;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// Make sure to update the learning cycle record as failed
|
|
369
|
+
learningCycleEntity.Status = 'Failed';
|
|
370
|
+
learningCycleEntity.EndedAt = new Date();
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
await learningCycleEntity.Save();
|
|
374
|
+
} catch (saveError) {
|
|
375
|
+
LogError(`Failed to update learning cycle record after error: ${saveError}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Unregister the organization on error
|
|
379
|
+
scheduler.unregisterRunningCycle(organizationId);
|
|
380
|
+
|
|
381
|
+
// Re-throw the original error
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
protected async handleSimpleSkipLearningPostRequest(
|
|
387
|
+
input: SkipAPILearningCycleRequest,
|
|
388
|
+
user: UserInfo,
|
|
389
|
+
learningCycleId: string,
|
|
390
|
+
agentID: string
|
|
391
|
+
): Promise<SkipAPILearningCycleResponse> {
|
|
392
|
+
LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${___skipLearningAPIurl}`);
|
|
393
|
+
|
|
394
|
+
const response = await sendPostRequest(___skipLearningAPIurl, input, true, null);
|
|
395
|
+
|
|
396
|
+
if (response && response.length > 0) {
|
|
397
|
+
// the last object in the response array is the final response from the Skip API
|
|
398
|
+
const apiResponse = <SkipAPILearningCycleResponse>response[response.length - 1].value;
|
|
399
|
+
LogStatus(` Skip API response: ${apiResponse.success}`);
|
|
400
|
+
|
|
401
|
+
// Process any note changes, if any
|
|
402
|
+
if (apiResponse.noteChanges && apiResponse.noteChanges.length > 0) {
|
|
403
|
+
await this.processLearningCycleNoteChanges(apiResponse.noteChanges, agentID, user);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Not yet implemented
|
|
407
|
+
|
|
408
|
+
// // Process any query changes, if any
|
|
409
|
+
// if (apiResponse.queryChanges && apiResponse.queryChanges.length > 0) {
|
|
410
|
+
// await this.processLearningCycleQueryChanges(apiResponse.queryChanges, user);
|
|
411
|
+
// }
|
|
412
|
+
|
|
413
|
+
// // Process any request changes, if any
|
|
414
|
+
// if (apiResponse.requestChanges && apiResponse.requestChanges.length > 0) {
|
|
415
|
+
// await this.processLearningCycleRequestChanges(apiResponse.requestChanges, user);
|
|
416
|
+
// }
|
|
417
|
+
|
|
418
|
+
return apiResponse;
|
|
419
|
+
} else {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
error: 'Error',
|
|
423
|
+
elapsedTime: 0,
|
|
424
|
+
noteChanges: [],
|
|
425
|
+
queryChanges: [],
|
|
426
|
+
requestChanges: [],
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
protected async handleSimpleSkipChatPostRequest(
|
|
169
433
|
input: SkipAPIRequest,
|
|
170
434
|
conversationID: string = '',
|
|
171
435
|
UserMessageConversationDetailId: string = '',
|
|
172
436
|
createAIMessageConversationDetail: boolean = false,
|
|
173
437
|
user: UserInfo = null
|
|
174
438
|
): Promise<AskSkipResultType> {
|
|
175
|
-
LogStatus(` >>>
|
|
439
|
+
LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${___skipAPIurl}`);
|
|
176
440
|
|
|
177
441
|
const response = await sendPostRequest(___skipAPIurl, input, true, null);
|
|
178
442
|
|
|
@@ -206,6 +470,117 @@ export class AskSkipResolver {
|
|
|
206
470
|
}
|
|
207
471
|
}
|
|
208
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Processes note changes received from the Skip API learning cycle.
|
|
475
|
+
* @param noteChanges Changes to agent notes
|
|
476
|
+
* @param user The user making the request
|
|
477
|
+
*/
|
|
478
|
+
protected async processLearningCycleNoteChanges(
|
|
479
|
+
noteChanges: SkipLearningCycleNoteChange[],
|
|
480
|
+
agentID: string,
|
|
481
|
+
user: UserInfo
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
const md = new Metadata();
|
|
484
|
+
|
|
485
|
+
// Filter out any operations on "Human" notes
|
|
486
|
+
const validNoteChanges = noteChanges.filter(change => {
|
|
487
|
+
// Check if the note is of type "Human"
|
|
488
|
+
if (change.note.agentNoteType === "Human") {
|
|
489
|
+
LogStatus(`WARNING: Ignoring ${change.changeType} operation on Human note with ID ${change.note.id}. Human notes cannot be modified by the
|
|
490
|
+
learning cycle.`);
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Process all valid note changes in parallel
|
|
497
|
+
await Promise.all(validNoteChanges.map(async (change) => {
|
|
498
|
+
try {
|
|
499
|
+
if (change.changeType === 'add' || change.changeType === 'update') {
|
|
500
|
+
await this.processAddOrUpdateSkipNote(change, agentID, user);
|
|
501
|
+
} else if (change.changeType === 'delete') {
|
|
502
|
+
await this.processDeleteSkipNote(change, user);
|
|
503
|
+
}
|
|
504
|
+
} catch (e) {
|
|
505
|
+
LogError(`Error processing note change: ${e}`);
|
|
506
|
+
}
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
protected async processAddOrUpdateSkipNote(change: SkipLearningCycleNoteChange, agentID: string, user: UserInfo): Promise<boolean> {
|
|
511
|
+
try {
|
|
512
|
+
// Get the note entity object
|
|
513
|
+
const md = new Metadata();
|
|
514
|
+
const noteEntity = await md.GetEntityObject<AIAgentNoteEntity>('AI Agent Notes', user);
|
|
515
|
+
|
|
516
|
+
if (change.changeType === 'update') {
|
|
517
|
+
// Load existing note
|
|
518
|
+
const loadResult = await noteEntity.Load(change.note.id);
|
|
519
|
+
if (!loadResult) {
|
|
520
|
+
LogError(`Could not load note with ID ${change.note.id}`);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
// For new notes, ensure the note type is not "Human"
|
|
525
|
+
if (change.note.agentNoteType === "Human") {
|
|
526
|
+
LogStatus(`WARNING: Cannot create a new Human note with the learning cycle. Operation ignored.`);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Create a new note
|
|
531
|
+
noteEntity.NewRecord();
|
|
532
|
+
noteEntity.AgentID = agentID;
|
|
533
|
+
}
|
|
534
|
+
noteEntity.AgentNoteTypeID = this.getAgentNoteTypeIDByName('AI');
|
|
535
|
+
noteEntity.Note = change.note.note;
|
|
536
|
+
noteEntity.Type = change.note.type;
|
|
537
|
+
|
|
538
|
+
if (change.note.type === 'User') {
|
|
539
|
+
noteEntity.UserID = change.note.userId;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Save the note
|
|
543
|
+
if (!(await noteEntity.Save())) {
|
|
544
|
+
LogError(`Error saving AI Agent Note: ${noteEntity.LatestResult.Error}`);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return true;
|
|
549
|
+
} catch (e) {
|
|
550
|
+
LogError(`Error processing note change: ${e}`);
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
protected async processDeleteSkipNote(change: SkipLearningCycleNoteChange, user: UserInfo): Promise<boolean> {
|
|
556
|
+
// Get the note entity object
|
|
557
|
+
const md = new Metadata();
|
|
558
|
+
const noteEntity = await md.GetEntityObject<AIAgentNoteEntity>('AI Agent Notes', user);
|
|
559
|
+
|
|
560
|
+
// Load the note first
|
|
561
|
+
const loadResult = await noteEntity.Load(change.note.id);
|
|
562
|
+
|
|
563
|
+
if (!loadResult) {
|
|
564
|
+
LogError(`Could not load note with ID ${change.note.id} for deletion`);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Double-check if the loaded note is of type "Human"
|
|
569
|
+
if (change.note.agentNoteType === "Human") {
|
|
570
|
+
LogStatus(`WARNING: Ignoring delete operation on Human note with ID ${change.note.id}. Human notes cannot be deleted by the learning
|
|
571
|
+
cycle.`);
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Proceed with deletion
|
|
576
|
+
if (!(await noteEntity.Delete())) {
|
|
577
|
+
LogError(`Error deleting AI Agent Note: ${noteEntity.LatestResult.Error}`);
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
209
584
|
protected async CreateAIMessageConversationDetail(apiResponse: SkipAPIResponse, conversationID: string, user: UserInfo): Promise<string> {
|
|
210
585
|
const md = new Metadata();
|
|
211
586
|
const convoDetailEntityAI = <ConversationDetailEntity>await md.GetEntityObject('Conversation Details', user);
|
|
@@ -228,56 +603,275 @@ export class AskSkipResolver {
|
|
|
228
603
|
}
|
|
229
604
|
}
|
|
230
605
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
606
|
+
/**
|
|
607
|
+
* Builds the base Skip API request with common fields and data
|
|
608
|
+
* @param contextUser The user making the request
|
|
609
|
+
* @param dataSource The data source to use
|
|
610
|
+
* @param includeEntities Whether to include entities in the request
|
|
611
|
+
* @param includeQueries Whether to include queries in the request
|
|
612
|
+
* @param includeNotes Whether to include agent notes in the request
|
|
613
|
+
* @param forceEntitiesRefresh Whether to force refresh of entities
|
|
614
|
+
* @param includeCallBackKeyAndAccessToken Whether to include a callback key and access token
|
|
615
|
+
* @param additionalTokenInfo Additional info to include in the access token
|
|
616
|
+
* @returns Base request data that can be used by specific request builders
|
|
617
|
+
*/
|
|
618
|
+
protected async buildBaseSkipRequest(
|
|
619
|
+
contextUser: UserInfo,
|
|
620
|
+
dataSource: DataSource,
|
|
236
621
|
includeEntities: boolean,
|
|
237
622
|
includeQueries: boolean,
|
|
238
623
|
includeNotes: boolean,
|
|
239
|
-
|
|
240
|
-
dataSource: DataSource,
|
|
624
|
+
includeRequests: boolean,
|
|
241
625
|
forceEntitiesRefresh: boolean = false,
|
|
242
|
-
includeCallBackKeyAndAccessToken: boolean = false
|
|
243
|
-
|
|
626
|
+
includeCallBackKeyAndAccessToken: boolean = false,
|
|
627
|
+
additionalTokenInfo: any = {}
|
|
628
|
+
) {
|
|
629
|
+
|
|
244
630
|
const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
|
|
245
631
|
const queries = includeQueries ? this.BuildSkipQueries() : [];
|
|
246
|
-
const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser) : {notes: [], noteTypes: []};
|
|
247
|
-
|
|
248
|
-
|
|
632
|
+
const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser) : {notes: [], noteTypes: []};
|
|
633
|
+
const requests = includeRequests ? await this.BuildSkipRequests(contextUser) : [];
|
|
634
|
+
|
|
635
|
+
// Setup access token if needed
|
|
249
636
|
let accessToken: GetDataAccessToken;
|
|
250
637
|
if (includeCallBackKeyAndAccessToken) {
|
|
638
|
+
const tokenInfo = {
|
|
639
|
+
type: 'skip_api_request',
|
|
640
|
+
userEmail: contextUser.Email,
|
|
641
|
+
userName: contextUser.Name,
|
|
642
|
+
userID: contextUser.ID,
|
|
643
|
+
...additionalTokenInfo
|
|
644
|
+
};
|
|
645
|
+
|
|
251
646
|
accessToken = registerAccessToken(
|
|
252
647
|
undefined,
|
|
253
648
|
1000 * 60 * 10 /*10 minutes*/,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
userEmail: contextUser.Email,
|
|
257
|
-
userName: contextUser.Name,
|
|
258
|
-
userID: contextUser.ID,
|
|
259
|
-
conversationId: conversationId,
|
|
260
|
-
requestPhase: requestPhase,
|
|
261
|
-
}
|
|
262
|
-
);
|
|
649
|
+
tokenInfo
|
|
650
|
+
);
|
|
263
651
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
entities,
|
|
655
|
+
queries,
|
|
656
|
+
notes,
|
|
657
|
+
noteTypes,
|
|
658
|
+
requests,
|
|
659
|
+
accessToken,
|
|
660
|
+
organizationId: ___skipAPIOrgId,
|
|
267
661
|
organizationInfo: configInfo?.askSkip?.organizationInfo,
|
|
268
|
-
|
|
269
|
-
conversationID: conversationId.toString(),
|
|
270
|
-
dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
|
|
271
|
-
organizationID: ___skipAPIOrgId,
|
|
272
|
-
requestPhase: requestPhase,
|
|
273
|
-
entities: entities,
|
|
274
|
-
queries: queries,
|
|
275
|
-
notes: notes,
|
|
276
|
-
noteTypes: noteTypes,
|
|
662
|
+
apiKeys: this.buildSkipAPIKeys(),
|
|
277
663
|
callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
|
|
278
664
|
callingServerAPIKey: accessToken ? apiKey : undefined,
|
|
279
665
|
callingServerAccessToken: accessToken ? accessToken.Token : undefined
|
|
280
666
|
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Builds the learning API request for Skip
|
|
671
|
+
*/
|
|
672
|
+
protected async buildSkipLearningAPIRequest(
|
|
673
|
+
learningCycleId: string,
|
|
674
|
+
lastLearningCycleDate: Date,
|
|
675
|
+
includeEntities: boolean,
|
|
676
|
+
includeQueries: boolean,
|
|
677
|
+
includeNotes: boolean,
|
|
678
|
+
includeRequests: boolean,
|
|
679
|
+
dataSource: DataSource,
|
|
680
|
+
contextUser: UserInfo,
|
|
681
|
+
forceEntitiesRefresh: boolean = false,
|
|
682
|
+
includeCallBackKeyAndAccessToken: boolean = false
|
|
683
|
+
) {
|
|
684
|
+
// Build base Skip request data
|
|
685
|
+
const baseRequest = await this.buildBaseSkipRequest(
|
|
686
|
+
contextUser,
|
|
687
|
+
dataSource,
|
|
688
|
+
includeEntities,
|
|
689
|
+
includeQueries,
|
|
690
|
+
includeNotes,
|
|
691
|
+
includeRequests,
|
|
692
|
+
forceEntitiesRefresh,
|
|
693
|
+
includeCallBackKeyAndAccessToken
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// Get data specific to learning cycle
|
|
697
|
+
const newConversations = await this.BuildSkipLearningCycleNewConversations(lastLearningCycleDate, dataSource, contextUser);
|
|
698
|
+
|
|
699
|
+
// Create the learning-specific request object
|
|
700
|
+
const input: SkipAPILearningCycleRequest = {
|
|
701
|
+
organizationId: baseRequest.organizationId,
|
|
702
|
+
organizationInfo: baseRequest.organizationInfo,
|
|
703
|
+
learningCycleId,
|
|
704
|
+
lastLearningCycleDate,
|
|
705
|
+
newConversations,
|
|
706
|
+
entities: baseRequest.entities,
|
|
707
|
+
queries: baseRequest.queries,
|
|
708
|
+
notes: baseRequest.notes,
|
|
709
|
+
noteTypes: baseRequest.noteTypes,
|
|
710
|
+
requests: baseRequest.requests,
|
|
711
|
+
apiKeys: baseRequest.apiKeys
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return input;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Loads the conversations that have have an updated or new conversation detail since the last learning cycle
|
|
719
|
+
* @param dataSource the data source to use
|
|
720
|
+
* @param lastLearningCycleDate the date of the last learning cycle
|
|
721
|
+
* @param contextUser the user context
|
|
722
|
+
*/
|
|
723
|
+
protected async BuildSkipLearningCycleNewConversations(
|
|
724
|
+
lastLearningCycleDate: Date,
|
|
725
|
+
dataSource: DataSource,
|
|
726
|
+
contextUser: UserInfo
|
|
727
|
+
): Promise<SkipConversation[]> {
|
|
728
|
+
try {
|
|
729
|
+
const rv = new RunView();
|
|
730
|
+
|
|
731
|
+
// Get all conversations with a conversation detail that has been updated (modified or added) since the last learning cycle
|
|
732
|
+
const conversationsSinceLastLearningCycle = await rv.RunView<ConversationEntity>({
|
|
733
|
+
EntityName: 'Conversations',
|
|
734
|
+
ExtraFilter: `ID IN (SELECT ConversationID FROM __mj.vwConversationDetails WHERE __mj_UpdatedAt >= '${lastLearningCycleDate.toISOString()}')`,
|
|
735
|
+
ResultType: 'entity_object',
|
|
736
|
+
}, contextUser);
|
|
737
|
+
|
|
738
|
+
if (!conversationsSinceLastLearningCycle.Success || conversationsSinceLastLearningCycle.Results.length === 0) {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Now we map the conversations to SkipConversations and return
|
|
743
|
+
return await Promise.all(conversationsSinceLastLearningCycle.Results.map(async (c) => {
|
|
744
|
+
return {
|
|
745
|
+
id: c.ID,
|
|
746
|
+
name: c.Name,
|
|
747
|
+
userId: c.UserID,
|
|
748
|
+
user: c.User,
|
|
749
|
+
description: c.Description,
|
|
750
|
+
messages: await this.LoadConversationDetailsIntoSkipMessages(dataSource, c.ID),
|
|
751
|
+
createdAt: c.__mj_CreatedAt,
|
|
752
|
+
updatedAt: c.__mj_UpdatedAt
|
|
753
|
+
};
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
catch (e) {
|
|
757
|
+
LogError(`Error loading conversations since last learning cycle: ${e}`);
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Builds an array of agent requests
|
|
764
|
+
* @param contextUser the user context to load the requests
|
|
765
|
+
* @returns Array of SkipAPIAgentRequest objects
|
|
766
|
+
*/
|
|
767
|
+
protected async BuildSkipRequests(
|
|
768
|
+
contextUser: UserInfo
|
|
769
|
+
): Promise<SkipAPIAgentRequest[]> {
|
|
770
|
+
try {
|
|
771
|
+
const md = new Metadata();
|
|
772
|
+
const requestEntity = await md.GetEntityObject<AIAgentRequestEntity>('AI Agent Requests', contextUser);
|
|
773
|
+
const allRequests = await requestEntity.GetAll();
|
|
774
|
+
|
|
775
|
+
const requests = allRequests.map((r) => {
|
|
776
|
+
return {
|
|
777
|
+
id: r.ID,
|
|
778
|
+
agentId: r.AIAgentID,
|
|
779
|
+
agnet: r.AIAgent,
|
|
780
|
+
requestedAt: r.RequestedAt,
|
|
781
|
+
requestForUserId: r.RequestedForUserID,
|
|
782
|
+
requestForUser: r.RequestedForUser,
|
|
783
|
+
status: r.Status,
|
|
784
|
+
request: r.Request,
|
|
785
|
+
response: r.Response,
|
|
786
|
+
responseByUserId: r.ResponseByUserID,
|
|
787
|
+
responseByUser: r.ResponseByUser,
|
|
788
|
+
respondedAt: r.RespondedAt,
|
|
789
|
+
comments: r.Comments,
|
|
790
|
+
createdAt: r.__mj_CreatedAt,
|
|
791
|
+
updatedAt: r.__mj_UpdatedAt,
|
|
792
|
+
};
|
|
793
|
+
});
|
|
794
|
+
return requests;
|
|
795
|
+
|
|
796
|
+
} catch (e) {
|
|
797
|
+
LogError(`Error loading requests: ${e}`);
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
protected async GetLastCompleteLearningCycleDate(agentID: string, user: UserInfo): Promise<Date> {
|
|
803
|
+
const md = new Metadata();
|
|
804
|
+
const rv = new RunView();
|
|
805
|
+
|
|
806
|
+
const lastLearningCycleRV = await rv.RunView<AIAgentLearningCycleEntity>({
|
|
807
|
+
EntityName: 'AI Agent Learning Cycles',
|
|
808
|
+
ExtraFilter: `AgentID = '${agentID}' AND Status = 'Complete'`,
|
|
809
|
+
ResultType: 'entity_object',
|
|
810
|
+
OrderBy: 'StartedAt DESC',
|
|
811
|
+
MaxRows: 1,
|
|
812
|
+
}, user);
|
|
813
|
+
|
|
814
|
+
const lastLearningCycle = lastLearningCycleRV.Results[0];
|
|
815
|
+
|
|
816
|
+
if (lastLearningCycle) {
|
|
817
|
+
return lastLearningCycle.StartedAt;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
// if no lerarning cycle found, return the epoch date
|
|
821
|
+
return new Date(0);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Builds the chat API request for Skip
|
|
827
|
+
*/
|
|
828
|
+
protected async buildSkipChatAPIRequest(
|
|
829
|
+
messages: SkipMessage[],
|
|
830
|
+
conversationId: string,
|
|
831
|
+
dataContext: DataContext,
|
|
832
|
+
requestPhase: SkipRequestPhase,
|
|
833
|
+
includeEntities: boolean,
|
|
834
|
+
includeQueries: boolean,
|
|
835
|
+
includeNotes: boolean,
|
|
836
|
+
includeRequests: boolean,
|
|
837
|
+
contextUser: UserInfo,
|
|
838
|
+
dataSource: DataSource,
|
|
839
|
+
forceEntitiesRefresh: boolean = false,
|
|
840
|
+
includeCallBackKeyAndAccessToken: boolean = false
|
|
841
|
+
): Promise<SkipAPIRequest> {
|
|
842
|
+
// Additional token info specific to chat requests
|
|
843
|
+
const additionalTokenInfo = {
|
|
844
|
+
conversationId,
|
|
845
|
+
requestPhase,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// Get base request data
|
|
849
|
+
const baseRequest = await this.buildBaseSkipRequest(
|
|
850
|
+
contextUser,
|
|
851
|
+
dataSource,
|
|
852
|
+
includeEntities,
|
|
853
|
+
includeQueries,
|
|
854
|
+
includeNotes,
|
|
855
|
+
includeRequests,
|
|
856
|
+
forceEntitiesRefresh,
|
|
857
|
+
includeCallBackKeyAndAccessToken,
|
|
858
|
+
additionalTokenInfo
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// Create the chat-specific request object
|
|
862
|
+
const input: SkipAPIRequest = {
|
|
863
|
+
messages,
|
|
864
|
+
conversationID: conversationId.toString(),
|
|
865
|
+
dataContext: <DataContext>CopyScalarsAndArrays(dataContext), // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
|
|
866
|
+
organizationID: baseRequest.organizationId,
|
|
867
|
+
requestPhase,
|
|
868
|
+
entities: baseRequest.entities,
|
|
869
|
+
queries: baseRequest.queries,
|
|
870
|
+
notes: baseRequest.notes,
|
|
871
|
+
noteTypes: baseRequest.noteTypes,
|
|
872
|
+
apiKeys: baseRequest.apiKeys,
|
|
873
|
+
};
|
|
874
|
+
|
|
281
875
|
return input;
|
|
282
876
|
}
|
|
283
877
|
|
|
@@ -298,9 +892,9 @@ export class AskSkipResolver {
|
|
|
298
892
|
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
299
893
|
const dataContext: DataContext = new DataContext();
|
|
300
894
|
await dataContext.Load(DataContextId, dataSource, true, false, 0, user);
|
|
301
|
-
const input = <SkipAPIRunScriptRequest>await this.
|
|
895
|
+
const input = <SkipAPIRunScriptRequest>await this.buildSkipChatAPIRequest([], '', dataContext, 'run_existing_script', false, false, false, false, user, dataSource, false, false);
|
|
302
896
|
input.scriptText = ScriptText;
|
|
303
|
-
return this.
|
|
897
|
+
return this.handleSimpleSkipChatPostRequest(input);
|
|
304
898
|
}
|
|
305
899
|
|
|
306
900
|
protected buildSkipAPIKeys(): SkipAPIRequestAPIKey[] {
|
|
@@ -341,7 +935,7 @@ export class AskSkipResolver {
|
|
|
341
935
|
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
342
936
|
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
343
937
|
|
|
344
|
-
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.
|
|
938
|
+
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
|
|
345
939
|
dataSource,
|
|
346
940
|
ConversationId,
|
|
347
941
|
UserQuestion,
|
|
@@ -359,9 +953,9 @@ export class AskSkipResolver {
|
|
|
359
953
|
);
|
|
360
954
|
|
|
361
955
|
const conversationDetailCount = 1
|
|
362
|
-
const input = await this.
|
|
956
|
+
const input = await this.buildSkipChatAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true, true, false, user, dataSource, ForceEntityRefresh === undefined ? false : ForceEntityRefresh, true);
|
|
363
957
|
|
|
364
|
-
return this.
|
|
958
|
+
return this.HandleSkipChatRequest(
|
|
365
959
|
input,
|
|
366
960
|
UserQuestion,
|
|
367
961
|
user,
|
|
@@ -440,7 +1034,7 @@ export class AskSkipResolver {
|
|
|
440
1034
|
return {
|
|
441
1035
|
id: r.ID,
|
|
442
1036
|
agentNoteTypeId: r.AgentNoteTypeID,
|
|
443
|
-
agentNoteType: r.AgentNoteType,
|
|
1037
|
+
agentNoteType: r.AgentNoteType,
|
|
444
1038
|
note: r.Note,
|
|
445
1039
|
type: r.Type,
|
|
446
1040
|
userId: r.UserID,
|
|
@@ -743,7 +1337,7 @@ export class AskSkipResolver {
|
|
|
743
1337
|
}
|
|
744
1338
|
}
|
|
745
1339
|
|
|
746
|
-
protected async
|
|
1340
|
+
protected async HandleSkipChatInitialObjectLoading(
|
|
747
1341
|
dataSource: DataSource,
|
|
748
1342
|
ConversationId: string,
|
|
749
1343
|
UserQuestion: string,
|
|
@@ -971,7 +1565,7 @@ export class AskSkipResolver {
|
|
|
971
1565
|
}
|
|
972
1566
|
}
|
|
973
1567
|
|
|
974
|
-
protected async
|
|
1568
|
+
protected async HandleSkipChatRequest(
|
|
975
1569
|
input: SkipAPIRequest,
|
|
976
1570
|
UserQuestion: string,
|
|
977
1571
|
user: UserInfo,
|
|
@@ -1379,7 +1973,7 @@ export class AskSkipResolver {
|
|
|
1379
1973
|
}
|
|
1380
1974
|
conversationDetailCount++;
|
|
1381
1975
|
// we have all of the data now, add it to the data context and then submit it back to the Skip API
|
|
1382
|
-
return this.
|
|
1976
|
+
return this.HandleSkipChatRequest(
|
|
1383
1977
|
apiRequest,
|
|
1384
1978
|
UserQuestion,
|
|
1385
1979
|
user,
|
|
@@ -1497,12 +2091,154 @@ export class AskSkipResolver {
|
|
|
1497
2091
|
};
|
|
1498
2092
|
}
|
|
1499
2093
|
|
|
2094
|
+
protected getAgentNoteTypeIDByName(name: string): string {
|
|
2095
|
+
const noteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === name.trim().toLowerCase())?.ID;
|
|
2096
|
+
if (noteTypeID) {
|
|
2097
|
+
return noteTypeID;
|
|
2098
|
+
}
|
|
2099
|
+
else{
|
|
2100
|
+
// default to AI note ID
|
|
2101
|
+
const AINoteTypeID = AIEngine.Instance.AgentNoteTypes.find(nt => nt.Name.trim().toLowerCase() === 'AI')?.ID;
|
|
2102
|
+
return AINoteTypeID
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1500
2106
|
protected async getViewData(ViewId: string, user: UserInfo): Promise<any> {
|
|
1501
2107
|
const rv = new RunView();
|
|
1502
2108
|
const result = await rv.RunView({ ViewID: ViewId, IgnoreMaxRows: true }, user);
|
|
1503
2109
|
if (result && result.Success) return result.Results;
|
|
1504
2110
|
else throw new Error(`Error running view ${ViewId}`);
|
|
1505
2111
|
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Manually executes the Skip AI learning cycle.
|
|
2115
|
+
* @param OrganizationId Optional organization ID to register for this run
|
|
2116
|
+
*/
|
|
2117
|
+
@Mutation(() => ManualLearningCycleResultType)
|
|
2118
|
+
async ManuallyExecuteSkipLearningCycle(
|
|
2119
|
+
@Arg('OrganizationId', () => String, { nullable: true }) OrganizationId?: string
|
|
2120
|
+
): Promise<ManualLearningCycleResultType> {
|
|
2121
|
+
try {
|
|
2122
|
+
LogStatus('Manual execution of Skip learning cycle requested via API');
|
|
2123
|
+
|
|
2124
|
+
// Use the organization ID from config if not provided
|
|
2125
|
+
const orgId = OrganizationId || ___skipAPIOrgId;
|
|
2126
|
+
|
|
2127
|
+
// Call the scheduler's manual execution method with org ID
|
|
2128
|
+
const result = await LearningCycleScheduler.Instance.manuallyExecuteLearningCycle(orgId);
|
|
2129
|
+
|
|
2130
|
+
return {
|
|
2131
|
+
Success: result,
|
|
2132
|
+
Message: result
|
|
2133
|
+
? `Learning cycle was successfully executed manually for organization ${orgId}`
|
|
2134
|
+
: `Learning cycle execution failed for organization ${orgId}. Check server logs for details.`
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
catch (e) {
|
|
2138
|
+
LogError(`Error in ManuallyExecuteSkipLearningCycle: ${e}`);
|
|
2139
|
+
return {
|
|
2140
|
+
Success: false,
|
|
2141
|
+
Message: `Error executing learning cycle: ${e}`
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* Gets the current status of the learning cycle scheduler
|
|
2148
|
+
*/
|
|
2149
|
+
@Query(() => LearningCycleStatusType)
|
|
2150
|
+
async GetLearningCycleStatus(): Promise<LearningCycleStatusType> {
|
|
2151
|
+
try {
|
|
2152
|
+
const status = LearningCycleScheduler.Instance.getStatus();
|
|
2153
|
+
|
|
2154
|
+
return {
|
|
2155
|
+
IsSchedulerRunning: status.isSchedulerRunning,
|
|
2156
|
+
LastRunTime: status.lastRunTime ? status.lastRunTime.toISOString() : null,
|
|
2157
|
+
RunningOrganizations: status.runningOrganizations ? status.runningOrganizations.map(org => ({
|
|
2158
|
+
OrganizationId: org.organizationId,
|
|
2159
|
+
LearningCycleId: org.learningCycleId,
|
|
2160
|
+
StartTime: org.startTime.toISOString(),
|
|
2161
|
+
RunningForMinutes: org.runningForMinutes
|
|
2162
|
+
})) : []
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
catch (e) {
|
|
2166
|
+
LogError(`Error in GetLearningCycleStatus: ${e}`);
|
|
2167
|
+
return {
|
|
2168
|
+
IsSchedulerRunning: false,
|
|
2169
|
+
LastRunTime: null,
|
|
2170
|
+
RunningOrganizations: []
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/**
|
|
2176
|
+
* Checks if a specific organization is running a learning cycle
|
|
2177
|
+
* @param OrganizationId The organization ID to check
|
|
2178
|
+
*/
|
|
2179
|
+
@Query(() => RunningOrganizationType, { nullable: true })
|
|
2180
|
+
async IsOrganizationRunningLearningCycle(
|
|
2181
|
+
@Arg('OrganizationId', () => String) OrganizationId: string
|
|
2182
|
+
): Promise<RunningOrganizationType | null> {
|
|
2183
|
+
try {
|
|
2184
|
+
// Use the organization ID from config if not provided
|
|
2185
|
+
const orgId = OrganizationId || ___skipAPIOrgId;
|
|
2186
|
+
|
|
2187
|
+
const status = LearningCycleScheduler.Instance.isOrganizationRunningCycle(orgId);
|
|
2188
|
+
|
|
2189
|
+
if (!status.isRunning) {
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
return {
|
|
2194
|
+
OrganizationId: orgId,
|
|
2195
|
+
LearningCycleId: status.learningCycleId,
|
|
2196
|
+
StartTime: status.startTime.toISOString(),
|
|
2197
|
+
RunningForMinutes: status.runningForMinutes
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
catch (e) {
|
|
2201
|
+
LogError(`Error in IsOrganizationRunningLearningCycle: ${e}`);
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Stops a running learning cycle for a specific organization
|
|
2208
|
+
* @param OrganizationId The organization ID to stop the cycle for
|
|
2209
|
+
*/
|
|
2210
|
+
@Mutation(() => StopLearningCycleResultType)
|
|
2211
|
+
async StopLearningCycleForOrganization(
|
|
2212
|
+
@Arg('OrganizationId', () => String) OrganizationId: string
|
|
2213
|
+
): Promise<StopLearningCycleResultType> {
|
|
2214
|
+
try {
|
|
2215
|
+
// Use the organization ID from config if not provided
|
|
2216
|
+
const orgId = OrganizationId || ___skipAPIOrgId;
|
|
2217
|
+
|
|
2218
|
+
const result = LearningCycleScheduler.Instance.stopLearningCycleForOrganization(orgId);
|
|
2219
|
+
|
|
2220
|
+
// Transform the result to match our GraphQL type
|
|
2221
|
+
return {
|
|
2222
|
+
Success: result.success,
|
|
2223
|
+
Message: result.message,
|
|
2224
|
+
WasRunning: result.wasRunning,
|
|
2225
|
+
CycleDetails: result.cycleDetails ? {
|
|
2226
|
+
LearningCycleId: result.cycleDetails.learningCycleId,
|
|
2227
|
+
StartTime: result.cycleDetails.startTime.toISOString(),
|
|
2228
|
+
RunningForMinutes: result.cycleDetails.runningForMinutes
|
|
2229
|
+
} : null
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
catch (e) {
|
|
2233
|
+
LogError(`Error in StopLearningCycleForOrganization: ${e}`);
|
|
2234
|
+
return {
|
|
2235
|
+
Success: false,
|
|
2236
|
+
Message: `Error stopping learning cycle: ${e}`,
|
|
2237
|
+
WasRunning: false,
|
|
2238
|
+
CycleDetails: null
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
1506
2242
|
}
|
|
1507
2243
|
|
|
1508
2244
|
export default AskSkipResolver;
|