snow-flow 2.9.0 → 2.9.4
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/check-mcp-resources.sh +85 -0
- package/dist/config/snow-flow-config.js +1 -1
- package/dist/mcp/mcp-on-demand-proxy.d.ts +7 -0
- package/dist/mcp/mcp-on-demand-proxy.js +187 -0
- package/dist/mcp/servicenow-machine-learning-mcp.js +310 -60
- package/dist/mcp/servicenow-operations-mcp.d.ts +71 -1
- package/dist/mcp/servicenow-operations-mcp.js +10 -6
- package/dist/test-smart-limits.d.ts +13 -0
- package/dist/test-smart-limits.js +100 -0
- package/dist/utils/mcp-on-demand-manager.d.ts +69 -0
- package/dist/utils/mcp-on-demand-manager.js +309 -0
- package/dist/utils/mcp-process-manager.d.ts +58 -0
- package/dist/utils/mcp-process-manager.js +220 -0
- package/dist/utils/mcp-server-manager.js +50 -9
- package/dist/utils/mcp-singleton-lock.d.ts +4 -0
- package/dist/utils/mcp-singleton-lock.js +35 -8
- package/dist/utils/ml-data-fetcher.d.ts +66 -0
- package/dist/utils/ml-data-fetcher.js +288 -0
- package/dist/utils/servicenow-client.js +43 -17
- package/dist/utils/timeout-manager.d.ts +62 -0
- package/dist/utils/timeout-manager.js +352 -0
- package/package.json +1 -1
- package/test-ml-improvements.sh +76 -0
|
@@ -14,6 +14,7 @@ const events_1 = require("events");
|
|
|
14
14
|
const os_1 = __importDefault(require("os"));
|
|
15
15
|
const unified_auth_store_js_1 = require("./unified-auth-store.js");
|
|
16
16
|
const mcp_singleton_lock_js_1 = require("./mcp-singleton-lock.js");
|
|
17
|
+
const mcp_process_manager_js_1 = require("./mcp-process-manager.js");
|
|
17
18
|
class MCPServerManager extends events_1.EventEmitter {
|
|
18
19
|
constructor(configPath) {
|
|
19
20
|
super();
|
|
@@ -148,6 +149,16 @@ class MCPServerManager extends events_1.EventEmitter {
|
|
|
148
149
|
if (server.status === 'running') {
|
|
149
150
|
return true; // Already running
|
|
150
151
|
}
|
|
152
|
+
// Check if we can spawn a new server
|
|
153
|
+
const processManager = mcp_process_manager_js_1.MCPProcessManager.getInstance();
|
|
154
|
+
if (!processManager.canSpawnServer()) {
|
|
155
|
+
// Try cleanup first
|
|
156
|
+
processManager.cleanup();
|
|
157
|
+
// Check again after cleanup
|
|
158
|
+
if (!processManager.canSpawnServer()) {
|
|
159
|
+
throw new Error('Cannot spawn server: resource limits exceeded. Too many MCP processes running.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
151
162
|
server.status = 'starting';
|
|
152
163
|
this.emit('serverStarting', name);
|
|
153
164
|
try {
|
|
@@ -252,15 +263,20 @@ class MCPServerManager extends events_1.EventEmitter {
|
|
|
252
263
|
try {
|
|
253
264
|
// Try graceful shutdown first
|
|
254
265
|
server.process.kill('SIGTERM');
|
|
255
|
-
// Wait for graceful shutdown
|
|
266
|
+
// Wait for graceful shutdown with shorter timeout to prevent hanging
|
|
256
267
|
await new Promise((resolve) => {
|
|
257
268
|
const timeout = setTimeout(() => {
|
|
258
269
|
// Force kill if graceful shutdown fails
|
|
259
270
|
if (server.process) {
|
|
260
|
-
|
|
271
|
+
try {
|
|
272
|
+
server.process.kill('SIGKILL');
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
// Process might already be dead
|
|
276
|
+
}
|
|
261
277
|
}
|
|
262
278
|
resolve(undefined);
|
|
263
|
-
},
|
|
279
|
+
}, 2000); // Reduced from 5000ms to 2000ms to prevent hanging
|
|
264
280
|
server.process?.on('exit', () => {
|
|
265
281
|
clearTimeout(timeout);
|
|
266
282
|
resolve(undefined);
|
|
@@ -287,12 +303,31 @@ class MCPServerManager extends events_1.EventEmitter {
|
|
|
287
303
|
if (!singletonLock.acquire()) {
|
|
288
304
|
throw new Error('❌ MCP servers already running. Cannot start duplicate instances.');
|
|
289
305
|
}
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
306
|
+
// Clean up any existing duplicates first
|
|
307
|
+
const processManager = mcp_process_manager_js_1.MCPProcessManager.getInstance();
|
|
308
|
+
processManager.killDuplicates();
|
|
309
|
+
console.log('✅ Starting all MCP servers (singleton protected with resource limits)...');
|
|
310
|
+
console.log(processManager.getResourceSummary());
|
|
311
|
+
// Start servers sequentially with delay to avoid resource spikes
|
|
312
|
+
let started = 0;
|
|
313
|
+
for (const name of Array.from(this.servers.keys())) {
|
|
314
|
+
try {
|
|
315
|
+
// Check resource limits before each start
|
|
316
|
+
if (!processManager.canSpawnServer()) {
|
|
317
|
+
console.warn(`⚠️ Skipping ${name} - resource limits reached`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
await this.startServer(name);
|
|
321
|
+
started++;
|
|
322
|
+
// Small delay between starts to avoid CPU spike
|
|
323
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error(`Failed to start server '${name}':`, error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log(`✅ Started ${started}/${this.servers.size} MCP servers`);
|
|
330
|
+
console.log(processManager.getResourceSummary());
|
|
296
331
|
}
|
|
297
332
|
/**
|
|
298
333
|
* Stop all running MCP servers
|
|
@@ -303,6 +338,12 @@ class MCPServerManager extends events_1.EventEmitter {
|
|
|
303
338
|
return false;
|
|
304
339
|
}));
|
|
305
340
|
await Promise.all(promises);
|
|
341
|
+
// Release singleton lock after stopping all servers
|
|
342
|
+
const singletonLock = (0, mcp_singleton_lock_js_1.getMCPSingletonLock)();
|
|
343
|
+
if (singletonLock.isAcquired()) {
|
|
344
|
+
singletonLock.release();
|
|
345
|
+
console.log('✅ Released MCP singleton lock after stopping all servers');
|
|
346
|
+
}
|
|
306
347
|
}
|
|
307
348
|
/**
|
|
308
349
|
* Get status of a specific server
|
|
@@ -15,6 +15,10 @@ export declare class MCPSingletonLock {
|
|
|
15
15
|
* Release the singleton lock
|
|
16
16
|
*/
|
|
17
17
|
release(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Release the singleton lock without blocking (for graceful shutdown)
|
|
20
|
+
*/
|
|
21
|
+
releaseAsync(): Promise<void>;
|
|
18
22
|
/**
|
|
19
23
|
* Check if lock is currently held by this process
|
|
20
24
|
*/
|
|
@@ -71,6 +71,18 @@ class MCPSingletonLock {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Release the singleton lock without blocking (for graceful shutdown)
|
|
76
|
+
*/
|
|
77
|
+
releaseAsync() {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
// Use setImmediate to avoid blocking
|
|
80
|
+
setImmediate(() => {
|
|
81
|
+
this.release();
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
74
86
|
/**
|
|
75
87
|
* Check if lock is currently held by this process
|
|
76
88
|
*/
|
|
@@ -82,20 +94,35 @@ class MCPSingletonLock {
|
|
|
82
94
|
*/
|
|
83
95
|
setupCleanupHandlers() {
|
|
84
96
|
const cleanup = () => {
|
|
85
|
-
|
|
97
|
+
// Use non-blocking release to prevent hanging during shutdown
|
|
98
|
+
setImmediate(() => {
|
|
99
|
+
this.release();
|
|
100
|
+
});
|
|
86
101
|
};
|
|
87
|
-
|
|
88
|
-
process.
|
|
89
|
-
process.
|
|
90
|
-
process.
|
|
102
|
+
// Remove existing handlers to prevent duplicate registrations
|
|
103
|
+
process.removeAllListeners('exit');
|
|
104
|
+
process.removeAllListeners('SIGINT');
|
|
105
|
+
process.removeAllListeners('SIGTERM');
|
|
106
|
+
process.once('exit', cleanup);
|
|
107
|
+
process.once('SIGINT', () => {
|
|
108
|
+
cleanup();
|
|
109
|
+
// Allow graceful exit after cleanup
|
|
110
|
+
setTimeout(() => process.exit(0), 100);
|
|
111
|
+
});
|
|
112
|
+
process.once('SIGTERM', () => {
|
|
113
|
+
cleanup();
|
|
114
|
+
// Allow graceful exit after cleanup
|
|
115
|
+
setTimeout(() => process.exit(0), 100);
|
|
116
|
+
});
|
|
117
|
+
process.once('uncaughtException', (error) => {
|
|
91
118
|
logger.error('Uncaught exception, releasing MCP lock:', error);
|
|
92
119
|
cleanup();
|
|
93
|
-
process.exit(1);
|
|
120
|
+
setTimeout(() => process.exit(1), 100);
|
|
94
121
|
});
|
|
95
|
-
process.
|
|
122
|
+
process.once('unhandledRejection', (reason) => {
|
|
96
123
|
logger.error('Unhandled rejection, releasing MCP lock:', reason);
|
|
97
124
|
cleanup();
|
|
98
|
-
process.exit(1);
|
|
125
|
+
setTimeout(() => process.exit(1), 100);
|
|
99
126
|
});
|
|
100
127
|
}
|
|
101
128
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart ML Data Fetcher
|
|
3
|
+
* Handles intelligent data fetching for ML training with batching and field discovery
|
|
4
|
+
* Prevents token limit errors and optimizes data retrieval
|
|
5
|
+
*/
|
|
6
|
+
export interface MLDataFetchOptions {
|
|
7
|
+
table: string;
|
|
8
|
+
query?: string;
|
|
9
|
+
totalSamples?: number;
|
|
10
|
+
batchSize?: number;
|
|
11
|
+
fields?: string[];
|
|
12
|
+
discoverFields?: boolean;
|
|
13
|
+
includeContent?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FieldDiscoveryResult {
|
|
16
|
+
allFields: string[];
|
|
17
|
+
recommendedFields: string[];
|
|
18
|
+
sampleData: any[];
|
|
19
|
+
}
|
|
20
|
+
export interface BatchFetchResult {
|
|
21
|
+
data: any[];
|
|
22
|
+
totalFetched: number;
|
|
23
|
+
batchesProcessed: number;
|
|
24
|
+
fields: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class MLDataFetcher {
|
|
27
|
+
private operationsMCP;
|
|
28
|
+
constructor(operationsMCP: any);
|
|
29
|
+
/**
|
|
30
|
+
* Smart fetch with automatic field discovery and batching
|
|
31
|
+
*/
|
|
32
|
+
smartFetch(options: MLDataFetchOptions): Promise<BatchFetchResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Get total count of records matching query
|
|
35
|
+
*/
|
|
36
|
+
private getRecordCount;
|
|
37
|
+
/**
|
|
38
|
+
* Discover available fields by sampling a few records
|
|
39
|
+
*/
|
|
40
|
+
private discoverFields;
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a single batch of data
|
|
43
|
+
*/
|
|
44
|
+
private fetchBatch;
|
|
45
|
+
/**
|
|
46
|
+
* Extract data from MCP tool result
|
|
47
|
+
*/
|
|
48
|
+
private extractDataFromResult;
|
|
49
|
+
/**
|
|
50
|
+
* Calculate optimal batch size based on data characteristics
|
|
51
|
+
*/
|
|
52
|
+
private calculateOptimalBatchSize;
|
|
53
|
+
/**
|
|
54
|
+
* Select appropriate fields for ML training
|
|
55
|
+
*/
|
|
56
|
+
private selectMLFields;
|
|
57
|
+
/**
|
|
58
|
+
* Get default fields for a table type
|
|
59
|
+
*/
|
|
60
|
+
private getDefaultFields;
|
|
61
|
+
/**
|
|
62
|
+
* Get key fields that should always be included
|
|
63
|
+
*/
|
|
64
|
+
private getKeyFields;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=ml-data-fetcher.d.ts.map
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Smart ML Data Fetcher
|
|
4
|
+
* Handles intelligent data fetching for ML training with batching and field discovery
|
|
5
|
+
* Prevents token limit errors and optimizes data retrieval
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MLDataFetcher = void 0;
|
|
9
|
+
const logger_js_1 = require("./logger.js");
|
|
10
|
+
const logger = new logger_js_1.Logger('MLDataFetcher');
|
|
11
|
+
class MLDataFetcher {
|
|
12
|
+
constructor(operationsMCP) {
|
|
13
|
+
this.operationsMCP = operationsMCP;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Smart fetch with automatic field discovery and batching
|
|
17
|
+
*/
|
|
18
|
+
async smartFetch(options) {
|
|
19
|
+
const { table, query = '', totalSamples = 1000, batchSize = 100, fields, discoverFields = true, includeContent = true } = options;
|
|
20
|
+
logger.info(`🧠 Smart ML data fetch for ${table} - Target: ${totalSamples} samples`);
|
|
21
|
+
// Step 1: Get total count
|
|
22
|
+
const countResult = await this.getRecordCount(table, query);
|
|
23
|
+
logger.info(`📊 Total available records: ${countResult}`);
|
|
24
|
+
// Step 2: Discover fields if needed
|
|
25
|
+
let fieldsToFetch = fields;
|
|
26
|
+
if (!fieldsToFetch && discoverFields) {
|
|
27
|
+
const discovery = await this.discoverFields(table, query);
|
|
28
|
+
fieldsToFetch = discovery.recommendedFields;
|
|
29
|
+
logger.info(`🔍 Discovered ${discovery.allFields.length} fields, using ${fieldsToFetch.length} for ML`);
|
|
30
|
+
}
|
|
31
|
+
// Step 3: Calculate optimal batching strategy
|
|
32
|
+
const actualTotal = Math.min(totalSamples, countResult);
|
|
33
|
+
const optimalBatchSize = this.calculateOptimalBatchSize(actualTotal, batchSize, fieldsToFetch?.length || 10);
|
|
34
|
+
const numBatches = Math.ceil(actualTotal / optimalBatchSize);
|
|
35
|
+
logger.info(`📦 Fetching ${actualTotal} records in ${numBatches} batches of ${optimalBatchSize}`);
|
|
36
|
+
// Step 4: Fetch data in batches
|
|
37
|
+
const allData = [];
|
|
38
|
+
for (let batch = 0; batch < numBatches; batch++) {
|
|
39
|
+
const offset = batch * optimalBatchSize;
|
|
40
|
+
const limit = Math.min(optimalBatchSize, actualTotal - offset);
|
|
41
|
+
logger.info(` Batch ${batch + 1}/${numBatches}: Fetching ${limit} records (offset: ${offset})`);
|
|
42
|
+
try {
|
|
43
|
+
const batchData = await this.fetchBatch(table, query, limit, offset, fieldsToFetch, includeContent);
|
|
44
|
+
allData.push(...batchData);
|
|
45
|
+
// Small delay between batches to avoid overwhelming the API
|
|
46
|
+
if (batch < numBatches - 1) {
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Increased from 100ms to 200ms
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.message?.includes('exceeds maximum allowed tokens')) {
|
|
52
|
+
// Reduce batch size and retry
|
|
53
|
+
logger.warn(`⚠️ Token limit hit, reducing batch size and retrying...`);
|
|
54
|
+
const smallerBatchSize = Math.floor(optimalBatchSize / 2);
|
|
55
|
+
return this.smartFetch({
|
|
56
|
+
...options,
|
|
57
|
+
batchSize: smallerBatchSize
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
data: allData,
|
|
65
|
+
totalFetched: allData.length,
|
|
66
|
+
batchesProcessed: numBatches,
|
|
67
|
+
fields: fieldsToFetch || []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get total count of records matching query
|
|
72
|
+
*/
|
|
73
|
+
async getRecordCount(table, query) {
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.operationsMCP.handleTool('snow_query_table', {
|
|
76
|
+
table,
|
|
77
|
+
query,
|
|
78
|
+
limit: 1,
|
|
79
|
+
include_content: false // Count only, no data
|
|
80
|
+
});
|
|
81
|
+
// Extract count from result
|
|
82
|
+
if (result?.content?.[0]?.text) {
|
|
83
|
+
const text = result.content[0].text;
|
|
84
|
+
const match = text.match(/Found (\d+) .* records/);
|
|
85
|
+
if (match) {
|
|
86
|
+
return parseInt(match[1], 10);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error('Failed to get record count:', error);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Discover available fields by sampling a few records
|
|
98
|
+
*/
|
|
99
|
+
async discoverFields(table, query) {
|
|
100
|
+
logger.info('🔍 Discovering fields from sample records...');
|
|
101
|
+
try {
|
|
102
|
+
// Fetch just 3 records with all fields to discover schema
|
|
103
|
+
const result = await this.operationsMCP.handleTool('snow_query_table', {
|
|
104
|
+
table,
|
|
105
|
+
query,
|
|
106
|
+
limit: 3,
|
|
107
|
+
include_content: true
|
|
108
|
+
// No fields specified = get all fields
|
|
109
|
+
});
|
|
110
|
+
const sampleData = this.extractDataFromResult(result);
|
|
111
|
+
if (sampleData.length === 0) {
|
|
112
|
+
logger.warn('No sample data available for field discovery');
|
|
113
|
+
return {
|
|
114
|
+
allFields: [],
|
|
115
|
+
recommendedFields: this.getDefaultFields(table),
|
|
116
|
+
sampleData: []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Extract all field names from sample
|
|
120
|
+
const allFields = Object.keys(sampleData[0] || {});
|
|
121
|
+
// Recommend fields for ML (exclude system fields and large text fields)
|
|
122
|
+
const recommendedFields = this.selectMLFields(allFields, sampleData, table);
|
|
123
|
+
return {
|
|
124
|
+
allFields,
|
|
125
|
+
recommendedFields,
|
|
126
|
+
sampleData
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
logger.error('Field discovery failed:', error);
|
|
131
|
+
return {
|
|
132
|
+
allFields: [],
|
|
133
|
+
recommendedFields: this.getDefaultFields(table),
|
|
134
|
+
sampleData: []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Fetch a single batch of data
|
|
140
|
+
*/
|
|
141
|
+
async fetchBatch(table, query, limit, offset, fields, includeContent = true) {
|
|
142
|
+
try {
|
|
143
|
+
// Build query with offset
|
|
144
|
+
const offsetQuery = query ? `${query}^ORDERBY${offset}` : `ORDERBY${offset}`;
|
|
145
|
+
const result = await this.operationsMCP.handleTool('snow_query_table', {
|
|
146
|
+
table,
|
|
147
|
+
query: offsetQuery,
|
|
148
|
+
limit,
|
|
149
|
+
fields,
|
|
150
|
+
include_content: includeContent
|
|
151
|
+
});
|
|
152
|
+
return this.extractDataFromResult(result);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.error(`Failed to fetch batch (offset: ${offset}, limit: ${limit}):`, error);
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Extract data from MCP tool result
|
|
161
|
+
*/
|
|
162
|
+
extractDataFromResult(result) {
|
|
163
|
+
if (!result?.content?.[0]?.text) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const text = result.content[0].text;
|
|
168
|
+
// Try to parse as JSON first
|
|
169
|
+
if (text.includes('[') && text.includes(']')) {
|
|
170
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
171
|
+
if (jsonMatch) {
|
|
172
|
+
return JSON.parse(jsonMatch[0]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Try to extract from formatted output
|
|
176
|
+
const lines = text.split('\n');
|
|
177
|
+
const data = [];
|
|
178
|
+
let currentRecord = null;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (line.includes('number:') || line.includes('sys_id:')) {
|
|
181
|
+
if (currentRecord) {
|
|
182
|
+
data.push(currentRecord);
|
|
183
|
+
}
|
|
184
|
+
currentRecord = {};
|
|
185
|
+
}
|
|
186
|
+
if (currentRecord && line.includes(':')) {
|
|
187
|
+
const [key, ...valueParts] = line.split(':');
|
|
188
|
+
const cleanKey = key.trim().replace(/^[-\s]+/, '');
|
|
189
|
+
const value = valueParts.join(':').trim();
|
|
190
|
+
if (cleanKey && value) {
|
|
191
|
+
currentRecord[cleanKey] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (currentRecord && Object.keys(currentRecord).length > 0) {
|
|
196
|
+
data.push(currentRecord);
|
|
197
|
+
}
|
|
198
|
+
return data;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
logger.error('Failed to extract data from result:', error);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Calculate optimal batch size based on data characteristics
|
|
207
|
+
*/
|
|
208
|
+
calculateOptimalBatchSize(totalRecords, requestedBatchSize, numFields) {
|
|
209
|
+
// Estimate tokens per record (rough approximation)
|
|
210
|
+
const avgTokensPerField = 10; // Conservative estimate
|
|
211
|
+
const tokensPerRecord = numFields * avgTokensPerField;
|
|
212
|
+
const maxTokensPerBatch = 20000; // Leave buffer below 25000 limit
|
|
213
|
+
// Calculate max records per batch based on token limit
|
|
214
|
+
const maxRecordsPerBatch = Math.floor(maxTokensPerBatch / tokensPerRecord);
|
|
215
|
+
// Use the smaller of requested batch size and calculated max
|
|
216
|
+
const optimalSize = Math.min(requestedBatchSize, maxRecordsPerBatch);
|
|
217
|
+
// Ensure at least 10 records per batch but not more than total
|
|
218
|
+
return Math.max(10, Math.min(optimalSize, totalRecords));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Select appropriate fields for ML training
|
|
222
|
+
*/
|
|
223
|
+
selectMLFields(allFields, sampleData, table) {
|
|
224
|
+
const excluded = new Set([
|
|
225
|
+
'sys_id', 'sys_created_on', 'sys_created_by', 'sys_updated_on', 'sys_updated_by',
|
|
226
|
+
'sys_mod_count', 'sys_tags', 'sys_package', 'sys_policy', 'sys_scope',
|
|
227
|
+
'sys_domain', 'sys_domain_path', 'sys_class_name'
|
|
228
|
+
]);
|
|
229
|
+
const mlFields = allFields.filter(field => {
|
|
230
|
+
// Exclude system fields
|
|
231
|
+
if (excluded.has(field))
|
|
232
|
+
return false;
|
|
233
|
+
// Check if field has useful data in samples
|
|
234
|
+
const hasData = sampleData.some(record => {
|
|
235
|
+
const value = record[field];
|
|
236
|
+
return value && value !== 'null' && value !== '';
|
|
237
|
+
});
|
|
238
|
+
return hasData;
|
|
239
|
+
});
|
|
240
|
+
// Always include key fields for the table type
|
|
241
|
+
const keyFields = this.getKeyFields(table);
|
|
242
|
+
const combinedFields = [...new Set([...keyFields, ...mlFields])];
|
|
243
|
+
// Limit to 20 most relevant fields to avoid token issues
|
|
244
|
+
return combinedFields.slice(0, 20);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get default fields for a table type
|
|
248
|
+
*/
|
|
249
|
+
getDefaultFields(table) {
|
|
250
|
+
const fieldMap = {
|
|
251
|
+
incident: [
|
|
252
|
+
'number', 'short_description', 'description', 'category', 'subcategory',
|
|
253
|
+
'priority', 'urgency', 'impact', 'state', 'assignment_group',
|
|
254
|
+
'assigned_to', 'caller_id', 'opened_at', 'resolved_at'
|
|
255
|
+
],
|
|
256
|
+
change_request: [
|
|
257
|
+
'number', 'short_description', 'description', 'type', 'category',
|
|
258
|
+
'priority', 'risk', 'impact', 'state', 'assignment_group',
|
|
259
|
+
'assigned_to', 'requested_by', 'start_date', 'end_date'
|
|
260
|
+
],
|
|
261
|
+
problem: [
|
|
262
|
+
'number', 'short_description', 'description', 'category', 'subcategory',
|
|
263
|
+
'priority', 'urgency', 'impact', 'state', 'assignment_group',
|
|
264
|
+
'assigned_to', 'opened_at', 'known_error'
|
|
265
|
+
],
|
|
266
|
+
sc_request: [
|
|
267
|
+
'number', 'short_description', 'description', 'request_state', 'approval',
|
|
268
|
+
'requested_for', 'requested_by', 'assignment_group', 'assigned_to',
|
|
269
|
+
'opened_at', 'closed_at'
|
|
270
|
+
]
|
|
271
|
+
};
|
|
272
|
+
return fieldMap[table] || ['number', 'short_description', 'state', 'priority'];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get key fields that should always be included
|
|
276
|
+
*/
|
|
277
|
+
getKeyFields(table) {
|
|
278
|
+
const keyFieldMap = {
|
|
279
|
+
incident: ['number', 'short_description', 'category', 'priority', 'state'],
|
|
280
|
+
change_request: ['number', 'short_description', 'type', 'risk', 'state'],
|
|
281
|
+
problem: ['number', 'short_description', 'category', 'priority', 'state'],
|
|
282
|
+
sc_request: ['number', 'short_description', 'request_state', 'approval']
|
|
283
|
+
};
|
|
284
|
+
return keyFieldMap[table] || ['number', 'short_description', 'state'];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
exports.MLDataFetcher = MLDataFetcher;
|
|
288
|
+
//# sourceMappingURL=ml-data-fetcher.js.map
|
|
@@ -13,10 +13,10 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
13
13
|
const https_1 = __importDefault(require("https"));
|
|
14
14
|
const snow_oauth_1 = require("./snow-oauth");
|
|
15
15
|
const action_type_cache_1 = require("./action-type-cache");
|
|
16
|
-
const snow_flow_config_js_1 = require("../config/snow-flow-config.js");
|
|
17
16
|
const widget_template_generator_js_1 = require("./widget-template-generator.js");
|
|
18
17
|
const logger_1 = require("./logger");
|
|
19
18
|
const unified_auth_store_js_1 = require("./unified-auth-store.js");
|
|
19
|
+
const timeout_manager_js_1 = require("./timeout-manager.js");
|
|
20
20
|
class ServiceNowClient {
|
|
21
21
|
constructor() {
|
|
22
22
|
this.credentials = null;
|
|
@@ -68,8 +68,10 @@ class ServiceNowClient {
|
|
|
68
68
|
maxVersion: 'TLSv1.3',
|
|
69
69
|
minVersion: 'TLSv1.2'
|
|
70
70
|
});
|
|
71
|
+
// Use intelligent timeout based on operation type (default to TABLE_QUERY)
|
|
72
|
+
const defaultTimeout = (0, timeout_manager_js_1.getTimeoutConfig)(timeout_manager_js_1.OperationType.TABLE_QUERY).baseTimeout;
|
|
71
73
|
this.client = axios_1.default.create({
|
|
72
|
-
timeout:
|
|
74
|
+
timeout: parseInt(process.env.SNOW_API_TIMEOUT || String(defaultTimeout)),
|
|
73
75
|
headers: {
|
|
74
76
|
'Content-Type': 'application/json',
|
|
75
77
|
'Accept': 'application/json'
|
|
@@ -979,21 +981,33 @@ class ServiceNowClient {
|
|
|
979
981
|
async searchRecords(table, query, limit = 10) {
|
|
980
982
|
try {
|
|
981
983
|
await this.ensureAuthenticated();
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
984
|
+
// Detect operation type for intelligent timeout
|
|
985
|
+
const operationType = (0, timeout_manager_js_1.detectOperationType)({
|
|
986
|
+
action: 'query',
|
|
987
|
+
table,
|
|
988
|
+
limit
|
|
987
989
|
});
|
|
990
|
+
// Use retry wrapper with intelligent timeout
|
|
991
|
+
const result = await (0, timeout_manager_js_1.withRetry)(async () => {
|
|
992
|
+
const response = await this.client.get(`${this.getBaseUrl()}/api/now/table/${table}`, {
|
|
993
|
+
params: {
|
|
994
|
+
sysparm_query: query,
|
|
995
|
+
sysparm_limit: limit
|
|
996
|
+
},
|
|
997
|
+
// Override timeout for this specific request
|
|
998
|
+
timeout: (0, timeout_manager_js_1.getTimeoutConfig)(operationType).baseTimeout
|
|
999
|
+
});
|
|
1000
|
+
return response;
|
|
1001
|
+
}, operationType, `Search ${table} (${limit} records)`);
|
|
988
1002
|
return {
|
|
989
1003
|
success: true,
|
|
990
1004
|
data: {
|
|
991
|
-
result:
|
|
1005
|
+
result: result.data.result || []
|
|
992
1006
|
}
|
|
993
1007
|
};
|
|
994
1008
|
}
|
|
995
1009
|
catch (error) {
|
|
996
|
-
|
|
1010
|
+
this.logger.error(`Failed to search records in ${table}:`, error);
|
|
997
1011
|
return {
|
|
998
1012
|
success: false,
|
|
999
1013
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -1006,22 +1020,34 @@ class ServiceNowClient {
|
|
|
1006
1020
|
async searchRecordsWithOffset(table, query, limit = 10, offset = 0) {
|
|
1007
1021
|
try {
|
|
1008
1022
|
await this.ensureAuthenticated();
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1023
|
+
// Detect operation type for intelligent timeout
|
|
1024
|
+
const operationType = (0, timeout_manager_js_1.detectOperationType)({
|
|
1025
|
+
action: 'query',
|
|
1026
|
+
table,
|
|
1027
|
+
limit
|
|
1015
1028
|
});
|
|
1029
|
+
// Use retry wrapper with intelligent timeout
|
|
1030
|
+
const result = await (0, timeout_manager_js_1.withRetry)(async () => {
|
|
1031
|
+
const response = await this.client.get(`${this.getBaseUrl()}/api/now/table/${table}`, {
|
|
1032
|
+
params: {
|
|
1033
|
+
sysparm_query: query,
|
|
1034
|
+
sysparm_limit: limit,
|
|
1035
|
+
sysparm_offset: offset
|
|
1036
|
+
},
|
|
1037
|
+
// Override timeout for this specific request
|
|
1038
|
+
timeout: (0, timeout_manager_js_1.getTimeoutConfig)(operationType).baseTimeout
|
|
1039
|
+
});
|
|
1040
|
+
return response;
|
|
1041
|
+
}, operationType, `Search ${table} with offset ${offset}`);
|
|
1016
1042
|
return {
|
|
1017
1043
|
success: true,
|
|
1018
1044
|
data: {
|
|
1019
|
-
result:
|
|
1045
|
+
result: result.data.result || []
|
|
1020
1046
|
}
|
|
1021
1047
|
};
|
|
1022
1048
|
}
|
|
1023
1049
|
catch (error) {
|
|
1024
|
-
|
|
1050
|
+
this.logger.error(`Failed to search records in ${table} with offset ${offset}:`, error);
|
|
1025
1051
|
return {
|
|
1026
1052
|
success: false,
|
|
1027
1053
|
error: error instanceof Error ? error.message : String(error)
|