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