lynkr 3.0.0 → 3.2.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/LICENSE +201 -21
- package/README.md +626 -145
- package/docs/index.md +150 -18
- package/install.sh +63 -16
- package/package.json +2 -2
- package/scripts/setup.js +117 -43
- package/src/api/router.js +78 -0
- package/src/clients/openrouter-utils.js +51 -7
- package/src/config/index.js +51 -0
- package/src/context/budget.js +326 -0
- package/src/context/compression.js +397 -0
- package/src/memory/format.js +156 -0
- package/src/memory/retriever.js +55 -14
- package/src/memory/search.js +36 -12
- package/src/memory/store.js +61 -13
- package/src/memory/surprise.js +56 -15
- package/src/orchestrator/index.js +189 -2
- package/src/prompts/system.js +320 -0
- package/src/tools/index.js +9 -0
- package/src/tools/smart-selection.js +356 -0
- package/src/tools/truncate.js +105 -0
- package/src/utils/tokens.js +217 -0
- package/test/llamacpp-integration.test.js +198 -0
- package/test/memory/extractor.test.js +34 -6
- package/test/memory/retriever.test.js +45 -15
- package/test/memory/retriever.test.js.bak +585 -0
- package/test/memory/search.test.js +160 -12
- package/test/memory/search.test.js.bak +389 -0
- package/test/memory/store.test.js +57 -25
- package/test/memory/store.test.js.bak +312 -0
- package/test/memory/surprise.test.js +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic System Prompt Optimization
|
|
3
|
+
* * Provides utilities for optimizing system prompts and tool descriptions
|
|
4
|
+
* to reduce token usage while maintaining functionality.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const logger = require('../logger');
|
|
9
|
+
const config = require('../config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compress tool descriptions to minimal format
|
|
13
|
+
*
|
|
14
|
+
* Converts verbose tool schemas to minimal versions by:
|
|
15
|
+
* - Shortening descriptions
|
|
16
|
+
* - Removing optional fields when not critical
|
|
17
|
+
* - Using concise parameter descriptions
|
|
18
|
+
*
|
|
19
|
+
* @param {Array} tools - Array of Anthropic-format tool definitions
|
|
20
|
+
* @param {string} mode - 'minimal' or 'full' (default: from config)
|
|
21
|
+
* @returns {Array} Optimized tool definitions
|
|
22
|
+
*/
|
|
23
|
+
function compressToolDescriptions(tools, mode = null) {
|
|
24
|
+
if (!tools || tools.length === 0) return tools;
|
|
25
|
+
|
|
26
|
+
mode = mode || config.systemPrompt?.toolDescriptions || 'minimal';
|
|
27
|
+
|
|
28
|
+
if (mode !== 'minimal') {
|
|
29
|
+
return tools; // Return unmodified if not in minimal mode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return tools.map(tool => {
|
|
33
|
+
const compressed = {
|
|
34
|
+
name: tool.name,
|
|
35
|
+
input_schema: {
|
|
36
|
+
type: tool.input_schema.type,
|
|
37
|
+
properties: {},
|
|
38
|
+
required: tool.input_schema.required || [],
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Add minimal description only if it exists
|
|
43
|
+
if (tool.description) {
|
|
44
|
+
compressed.description = compressText(tool.description, 50);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Compress property descriptions
|
|
48
|
+
if (tool.input_schema.properties) {
|
|
49
|
+
for (const [key, value] of Object.entries(tool.input_schema.properties)) {
|
|
50
|
+
compressed.input_schema.properties[key] = {
|
|
51
|
+
type: value.type,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Only include description if it's critical
|
|
55
|
+
if (value.description && !isObviousFromName(key)) {
|
|
56
|
+
compressed.input_schema.properties[key].description = compressText(value.description, 30);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Preserve enum, format, and other critical constraints
|
|
60
|
+
if (value.enum) compressed.input_schema.properties[key].enum = value.enum;
|
|
61
|
+
if (value.format) compressed.input_schema.properties[key].format = value.format;
|
|
62
|
+
if (value.items) compressed.input_schema.properties[key].items = value.items;
|
|
63
|
+
if (value.additionalProperties !== undefined) {
|
|
64
|
+
compressed.input_schema.properties[key].additionalProperties = value.additionalProperties;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Preserve additionalProperties if set
|
|
70
|
+
if (tool.input_schema.additionalProperties !== undefined) {
|
|
71
|
+
compressed.input_schema.additionalProperties = tool.input_schema.additionalProperties;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return compressed;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compress text to maximum length while preserving meaning
|
|
80
|
+
* @param {string} text - Text to compress
|
|
81
|
+
* @param {number} maxLength - Maximum length
|
|
82
|
+
* @returns {string} Compressed text
|
|
83
|
+
*/
|
|
84
|
+
function compressText(text, maxLength) {
|
|
85
|
+
if (!text || text.length <= maxLength) return text;
|
|
86
|
+
|
|
87
|
+
// Try to cut at sentence/word boundary
|
|
88
|
+
let cut = text.substring(0, maxLength);
|
|
89
|
+
const lastPeriod = cut.lastIndexOf('.');
|
|
90
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
91
|
+
|
|
92
|
+
if (lastPeriod > maxLength * 0.7) {
|
|
93
|
+
return cut.substring(0, lastPeriod + 1);
|
|
94
|
+
} else if (lastSpace > maxLength * 0.8) {
|
|
95
|
+
return cut.substring(0, lastSpace);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return cut;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if property name is self-explanatory
|
|
103
|
+
* @param {string} name - Property name
|
|
104
|
+
* @returns {boolean} True if name is obvious
|
|
105
|
+
*/
|
|
106
|
+
function isObviousFromName(name) {
|
|
107
|
+
const obvious = [
|
|
108
|
+
'id', 'name', 'type', 'value', 'data', 'text', 'content',
|
|
109
|
+
'message', 'query', 'command', 'url', 'path', 'file', 'filename',
|
|
110
|
+
'email', 'username', 'password', 'token', 'key', 'timeout',
|
|
111
|
+
'limit', 'offset', 'page', 'size', 'count', 'total', 'status'
|
|
112
|
+
];
|
|
113
|
+
return obvious.includes(name.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Optimize system prompt based on context
|
|
118
|
+
*
|
|
119
|
+
* Analyzes the system prompt and removes or compresses sections
|
|
120
|
+
* that aren't relevant to the current context.
|
|
121
|
+
*
|
|
122
|
+
* @param {string|Array} system - System prompt (string or content blocks)
|
|
123
|
+
* @param {Object} context - Context information
|
|
124
|
+
* @param {Array} context.tools - Tools available in this request
|
|
125
|
+
* @param {Array} context.messages - Recent messages
|
|
126
|
+
* @param {string} mode - 'dynamic' or 'full' (default: from config)
|
|
127
|
+
* @returns {string|Array} Optimized system prompt
|
|
128
|
+
*/
|
|
129
|
+
function optimizeSystemPrompt(system, context = {}, mode = null) {
|
|
130
|
+
if (!system) return system;
|
|
131
|
+
|
|
132
|
+
mode = mode || config.systemPrompt?.mode || 'dynamic';
|
|
133
|
+
|
|
134
|
+
if (mode !== 'dynamic') {
|
|
135
|
+
return system; // Return unmodified if not in dynamic mode
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Convert to string if array of blocks
|
|
139
|
+
let text = typeof system === 'string' ? system : flattenBlocks(system);
|
|
140
|
+
|
|
141
|
+
const optimizations = [];
|
|
142
|
+
const originalLength = text.length;
|
|
143
|
+
|
|
144
|
+
// 1. Remove verbose tool usage examples if no tools present
|
|
145
|
+
if (!context.tools || context.tools.length === 0) {
|
|
146
|
+
text = removeSection(text, /# Tool Usage Examples?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'tool examples');
|
|
147
|
+
text = removeSection(text, /<tool_usage>[\s\S]*?<\/tool_usage>/gi, optimizations, 'tool usage blocks');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Remove file operation guidelines if no file tools
|
|
151
|
+
const hasFileTools = context.tools?.some(t =>
|
|
152
|
+
['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
|
|
153
|
+
);
|
|
154
|
+
if (!hasFileTools) {
|
|
155
|
+
text = removeSection(text, /# File Operations?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'file operations');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 3. Remove git guidelines if no git tools
|
|
159
|
+
const hasGitTools = context.tools?.some(t =>
|
|
160
|
+
t.name.toLowerCase().includes('git')
|
|
161
|
+
);
|
|
162
|
+
if (!hasGitTools) {
|
|
163
|
+
text = removeSection(text, /# Git.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git guidelines');
|
|
164
|
+
text = removeSection(text, /## Committing changes[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'git commit guidelines');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 4. Remove web search guidelines if no web tools
|
|
168
|
+
const hasWebTools = context.tools?.some(t =>
|
|
169
|
+
['WebSearch', 'WebFetch'].includes(t.name)
|
|
170
|
+
);
|
|
171
|
+
if (!hasWebTools) {
|
|
172
|
+
text = removeSection(text, /# Web.*?[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'web guidelines');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 5. Compress code review guidelines if no recent code edits
|
|
176
|
+
const hasRecentEdits = context.messages?.some(m =>
|
|
177
|
+
typeof m.content === 'string' && m.content.toLowerCase().includes('edit')
|
|
178
|
+
);
|
|
179
|
+
if (!hasRecentEdits) {
|
|
180
|
+
text = removeSection(text, /# Code Review[\s\S]*?(?=\n#|\n\n[A-Z]|$)/gi, optimizations, 'code review');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 6. Remove verbose examples and keep only essential instructions
|
|
184
|
+
text = text.replace(/(<example>[\s\S]*?<\/example>\s*){3,}/g, (match) => {
|
|
185
|
+
// Keep first two examples, remove rest
|
|
186
|
+
const examples = match.match(/<example>[\s\S]*?<\/example>/g) || [];
|
|
187
|
+
optimizations.push('excessive examples');
|
|
188
|
+
return examples.slice(0, 2).join('\n\n');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 7. Compress whitespace
|
|
192
|
+
text = text.replace(/\n{4,}/g, '\n\n\n'); // Max 2 blank lines
|
|
193
|
+
text = text.replace(/[ \t]+\n/g, '\n'); // Remove trailing spaces
|
|
194
|
+
|
|
195
|
+
const finalLength = text.length;
|
|
196
|
+
const saved = originalLength - finalLength;
|
|
197
|
+
|
|
198
|
+
if (saved > 100 && optimizations.length > 0) {
|
|
199
|
+
logger.debug({
|
|
200
|
+
originalLength,
|
|
201
|
+
finalLength,
|
|
202
|
+
saved,
|
|
203
|
+
percentage: ((saved / originalLength) * 100).toFixed(1),
|
|
204
|
+
optimizations: [...new Set(optimizations)]
|
|
205
|
+
}, 'System prompt optimization applied');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Return in original format (string or blocks)
|
|
209
|
+
return typeof system === 'string' ? text : text;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Remove a section from text using regex
|
|
214
|
+
* @param {string} text - Text to modify
|
|
215
|
+
* @param {RegExp} pattern - Pattern to match
|
|
216
|
+
* @param {Array} optimizations - Array to track optimizations
|
|
217
|
+
* @param {string} label - Label for this optimization
|
|
218
|
+
* @returns {string} Modified text
|
|
219
|
+
*/
|
|
220
|
+
function removeSection(text, pattern, optimizations, label) {
|
|
221
|
+
const matches = text.match(pattern);
|
|
222
|
+
if (matches && matches.length > 0) {
|
|
223
|
+
optimizations.push(label);
|
|
224
|
+
return text.replace(pattern, '');
|
|
225
|
+
}
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Flatten content blocks to text
|
|
231
|
+
* @param {Array} blocks - Content blocks
|
|
232
|
+
* @returns {string} Flattened text
|
|
233
|
+
*/
|
|
234
|
+
function flattenBlocks(blocks) {
|
|
235
|
+
if (!Array.isArray(blocks)) return String(blocks || '');
|
|
236
|
+
|
|
237
|
+
return blocks
|
|
238
|
+
.map(block => {
|
|
239
|
+
if (typeof block === 'string') return block;
|
|
240
|
+
if (block.type === 'text' && block.text) return block.text;
|
|
241
|
+
if (block.text) return block.text;
|
|
242
|
+
return '';
|
|
243
|
+
})
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
.join('\n\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Analyze context to determine what optimizations are safe
|
|
250
|
+
* @param {Object} context - Request context
|
|
251
|
+
* @returns {Object} Analysis results
|
|
252
|
+
*/
|
|
253
|
+
function analyzeContext(context) {
|
|
254
|
+
const analysis = {
|
|
255
|
+
hasTools: Boolean(context.tools && context.tools.length > 0),
|
|
256
|
+
toolCount: context.tools?.length || 0,
|
|
257
|
+
toolNames: context.tools?.map(t => t.name) || [],
|
|
258
|
+
messageCount: context.messages?.length || 0,
|
|
259
|
+
hasFileOps: false,
|
|
260
|
+
hasGitOps: false,
|
|
261
|
+
hasWebOps: false,
|
|
262
|
+
hasBashOps: false,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (context.tools) {
|
|
266
|
+
analysis.hasFileOps = context.tools.some(t =>
|
|
267
|
+
['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(t.name)
|
|
268
|
+
);
|
|
269
|
+
analysis.hasGitOps = context.tools.some(t =>
|
|
270
|
+
t.name.toLowerCase().includes('git')
|
|
271
|
+
);
|
|
272
|
+
analysis.hasWebOps = context.tools.some(t =>
|
|
273
|
+
['WebSearch', 'WebFetch'].includes(t.name)
|
|
274
|
+
);
|
|
275
|
+
analysis.hasBashOps = context.tools.some(t =>
|
|
276
|
+
['Bash', 'BashOutput', 'KillShell'].includes(t.name)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return analysis;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Calculate token savings from optimizations
|
|
285
|
+
* @param {string|Array} original - Original system prompt
|
|
286
|
+
* @param {string|Array} optimized - Optimized system prompt
|
|
287
|
+
* @returns {Object} Savings statistics
|
|
288
|
+
*/
|
|
289
|
+
function calculateSavings(original, optimized) {
|
|
290
|
+
const origText = typeof original === 'string' ? original : flattenBlocks(original);
|
|
291
|
+
const optText = typeof optimized === 'string' ? optimized : flattenBlocks(optimized);
|
|
292
|
+
|
|
293
|
+
const origLength = origText.length;
|
|
294
|
+
const optLength = optText.length;
|
|
295
|
+
const saved = origLength - optLength;
|
|
296
|
+
|
|
297
|
+
// Rough token estimate (4 chars ≈ 1 token)
|
|
298
|
+
const tokensOriginal = Math.ceil(origLength / 4);
|
|
299
|
+
const tokensOptimized = Math.ceil(optLength / 4);
|
|
300
|
+
const tokensSaved = tokensOriginal - tokensOptimized;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
originalChars: origLength,
|
|
304
|
+
optimizedChars: optLength,
|
|
305
|
+
charsSaved: saved,
|
|
306
|
+
tokensOriginal,
|
|
307
|
+
tokensOptimized,
|
|
308
|
+
tokensSaved,
|
|
309
|
+
percentage: origLength > 0 ? ((saved / origLength) * 100).toFixed(1) : '0.0'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
compressToolDescriptions,
|
|
315
|
+
optimizeSystemPrompt,
|
|
316
|
+
analyzeContext,
|
|
317
|
+
calculateSavings,
|
|
318
|
+
compressText,
|
|
319
|
+
flattenBlocks,
|
|
320
|
+
};
|
package/src/tools/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logger = require("../logger");
|
|
2
|
+
const { truncateToolOutput } = require("./truncate");
|
|
2
3
|
|
|
3
4
|
const registry = new Map();
|
|
4
5
|
const registryLowercase = new Map();
|
|
@@ -216,14 +217,22 @@ async function executeToolCall(call, context = {}) {
|
|
|
216
217
|
context,
|
|
217
218
|
);
|
|
218
219
|
const formatted = normalizeHandlerResult(result);
|
|
220
|
+
|
|
221
|
+
// Apply tool output truncation for token efficiency
|
|
222
|
+
const truncatedContent = truncateToolOutput(normalisedCall.name, formatted.content);
|
|
223
|
+
|
|
219
224
|
return {
|
|
220
225
|
id: normalisedCall.id,
|
|
221
226
|
name: normalisedCall.name,
|
|
222
227
|
arguments: normalisedCall.arguments,
|
|
223
228
|
...formatted,
|
|
229
|
+
content: truncatedContent,
|
|
224
230
|
metadata: {
|
|
225
231
|
...(formatted.metadata ?? {}),
|
|
226
232
|
registered: true,
|
|
233
|
+
truncated: truncatedContent !== formatted.content,
|
|
234
|
+
originalLength: formatted.content?.length,
|
|
235
|
+
truncatedLength: truncatedContent?.length
|
|
227
236
|
},
|
|
228
237
|
};
|
|
229
238
|
} catch (err) {
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Tool Selection Module
|
|
3
|
+
*
|
|
4
|
+
* Intelligently selects relevant tools based on request type classification.
|
|
5
|
+
* Reduces tool token overhead by 50-70% for non-coding queries.
|
|
6
|
+
*
|
|
7
|
+
* @module tools/smart-selection
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require('../logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tool selection map: request type → relevant tools
|
|
14
|
+
*/
|
|
15
|
+
const TOOL_SELECTION_MAP = {
|
|
16
|
+
conversational: [], // No tools needed for greetings
|
|
17
|
+
simple_qa: [], // No tools needed for simple questions
|
|
18
|
+
|
|
19
|
+
research: [
|
|
20
|
+
'Read', 'Grep', 'Glob', // File search
|
|
21
|
+
'WebSearch', 'WebFetch' // Web research
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
file_reading: [
|
|
25
|
+
'Read', 'Grep', 'Glob' // Read-only tools
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
file_modification: [
|
|
29
|
+
'Read', 'Write', 'Edit', // Full I/O
|
|
30
|
+
'Grep', 'Glob', 'Bash' // Support tools
|
|
31
|
+
],
|
|
32
|
+
|
|
33
|
+
code_execution: [
|
|
34
|
+
'Read', 'Write', 'Edit', // File operations
|
|
35
|
+
'Bash', 'Grep', 'Glob' // Execution + search
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
coding: [
|
|
39
|
+
'Read', 'Write', 'Edit', // Core file ops
|
|
40
|
+
'Bash', 'Grep', 'Glob' // Support tools
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
complex_task: [
|
|
44
|
+
'Read', 'Write', 'Edit', // Tier 1
|
|
45
|
+
'Bash', 'Grep', 'Glob', // Tier 1
|
|
46
|
+
'WebSearch', 'WebFetch', // Tier 2
|
|
47
|
+
'Task', 'TodoWrite', 'AskUserQuestion' // Tier 3+4
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract content from last user message
|
|
53
|
+
*/
|
|
54
|
+
function getLastUserMessage(payload) {
|
|
55
|
+
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Find last user message
|
|
60
|
+
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
61
|
+
const msg = payload.messages[i];
|
|
62
|
+
if (msg?.role === 'user') {
|
|
63
|
+
return msg;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract text content from message (handles string or array format)
|
|
72
|
+
*/
|
|
73
|
+
function extractContent(message) {
|
|
74
|
+
if (!message) return '';
|
|
75
|
+
|
|
76
|
+
if (typeof message.content === 'string') {
|
|
77
|
+
return message.content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(message.content)) {
|
|
81
|
+
return message.content
|
|
82
|
+
.filter(block => block?.type === 'text')
|
|
83
|
+
.map(block => block.text || '')
|
|
84
|
+
.join(' ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if content matches greeting patterns
|
|
92
|
+
*/
|
|
93
|
+
function isGreeting(content) {
|
|
94
|
+
const greetingPattern = /^(hi|hello|hey|good morning|good afternoon|good evening|howdy|greetings|sup|yo)[\s\.\!\?]*$/i;
|
|
95
|
+
return greetingPattern.test(content.trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if content is short and non-technical
|
|
100
|
+
*/
|
|
101
|
+
function isShortNonTechnical(content) {
|
|
102
|
+
const trimmed = content.trim();
|
|
103
|
+
|
|
104
|
+
// Very short messages (< 20 chars)
|
|
105
|
+
if (trimmed.length >= 20) return false;
|
|
106
|
+
|
|
107
|
+
// Check for technical keywords
|
|
108
|
+
const technicalKeywords = /code|file|function|error|bug|fix|write|read|create|implement|class|module|import|export|async|await/i;
|
|
109
|
+
return !technicalKeywords.test(trimmed);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if content is a simple question
|
|
114
|
+
*/
|
|
115
|
+
function isSimpleQuestion(content) {
|
|
116
|
+
const questionPattern = /^(what is|what's|how does|when|where|why|explain|define|tell me about|can you explain)/i;
|
|
117
|
+
return questionPattern.test(content.trim());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check for technical keywords
|
|
122
|
+
*/
|
|
123
|
+
function hasTechnicalKeywords(content) {
|
|
124
|
+
const technicalPattern = /code|function|class|file|module|import|export|async|await|promise|callback|api|database|server|client|component|method|variable|array|object|string|number/i;
|
|
125
|
+
return technicalPattern.test(content);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check for explanation/research keywords
|
|
130
|
+
*/
|
|
131
|
+
function hasExplanationKeywords(content) {
|
|
132
|
+
const explanationPattern = /explain|describe|summarize|what does|how does|tell me about|give me an overview|clarify|elaborate/i;
|
|
133
|
+
return explanationPattern.test(content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check for web/search keywords
|
|
138
|
+
*/
|
|
139
|
+
function hasWebKeywords(content) {
|
|
140
|
+
const webPattern = /search|lookup|find info|google|documentation|docs|website|url|link|online|internet|browse/i;
|
|
141
|
+
return webPattern.test(content);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check for file reading keywords
|
|
146
|
+
*/
|
|
147
|
+
function hasReadKeywords(content) {
|
|
148
|
+
const readPattern = /read|show|display|view|cat|check|inspect|look at|see|examine|review|print|output/i;
|
|
149
|
+
return readPattern.test(content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check for file writing/modification keywords
|
|
154
|
+
*/
|
|
155
|
+
function hasWriteKeywords(content) {
|
|
156
|
+
const writePattern = /write|create|add|update|modify|change|fix|delete|remove|insert|append|replace|save/i;
|
|
157
|
+
return writePattern.test(content);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check for edit/refactor keywords
|
|
162
|
+
*/
|
|
163
|
+
function hasEditKeywords(content) {
|
|
164
|
+
const editPattern = /edit|refactor|rename|move|reorganize|restructure|rewrite/i;
|
|
165
|
+
return editPattern.test(content);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check for execution/testing keywords
|
|
170
|
+
*/
|
|
171
|
+
function hasExecutionKeywords(content) {
|
|
172
|
+
const executionPattern = /run|execute|test|compile|build|deploy|start|install|launch|boot|fire up|npm|git|python|node|docker|bash|sh|cmd/i;
|
|
173
|
+
return executionPattern.test(content);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check for complex task keywords
|
|
178
|
+
*/
|
|
179
|
+
function hasComplexKeywords(content) {
|
|
180
|
+
const complexPattern = /implement|build|create|develop|design|architect|plan|strategy|approach|help with|work on|improve|optimize|enhance|refactor|migrate/i;
|
|
181
|
+
return complexPattern.test(content);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Classify request type based on content analysis
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} payload - Request payload with messages
|
|
188
|
+
* @returns {Object} Classification result { type, confidence, keywords }
|
|
189
|
+
*/
|
|
190
|
+
function classifyRequestType(payload) {
|
|
191
|
+
const lastMessage = getLastUserMessage(payload);
|
|
192
|
+
|
|
193
|
+
if (!lastMessage) {
|
|
194
|
+
logger.debug('No user message found for classification');
|
|
195
|
+
return { type: 'coding', confidence: 0.5, keywords: [] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const content = extractContent(lastMessage);
|
|
199
|
+
const contentLower = content.toLowerCase();
|
|
200
|
+
const messageCount = payload.messages?.length ?? 0;
|
|
201
|
+
|
|
202
|
+
logger.debug({
|
|
203
|
+
contentLength: content.length,
|
|
204
|
+
messageCount,
|
|
205
|
+
contentPreview: content.substring(0, 100)
|
|
206
|
+
}, 'Classifying request type');
|
|
207
|
+
|
|
208
|
+
// 1. Conversational (no tools)
|
|
209
|
+
if (isGreeting(contentLower)) {
|
|
210
|
+
return { type: 'conversational', confidence: 1.0, keywords: ['greeting'] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isShortNonTechnical(contentLower)) {
|
|
214
|
+
return { type: 'conversational', confidence: 0.8, keywords: ['short', 'non-technical'] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 2. Simple Q&A (no tools)
|
|
218
|
+
if (isSimpleQuestion(contentLower) && !hasTechnicalKeywords(contentLower)) {
|
|
219
|
+
return { type: 'simple_qa', confidence: 0.9, keywords: ['question', 'non-technical'] };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3. Research/Explanation (minimal tools)
|
|
223
|
+
if (hasExplanationKeywords(contentLower)) {
|
|
224
|
+
return { type: 'research', confidence: 0.85, keywords: ['explanation'] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (hasWebKeywords(contentLower)) {
|
|
228
|
+
return { type: 'research', confidence: 0.9, keywords: ['web', 'search'] };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 4. File reading (read-only tools)
|
|
232
|
+
if (hasReadKeywords(contentLower) && !hasWriteKeywords(contentLower)) {
|
|
233
|
+
return { type: 'file_reading', confidence: 0.8, keywords: ['read'] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 5. File modification (full I/O tools)
|
|
237
|
+
if (hasWriteKeywords(contentLower) || hasEditKeywords(contentLower)) {
|
|
238
|
+
return { type: 'file_modification', confidence: 0.85, keywords: ['write', 'edit'] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 6. Execution/Testing (execution tools)
|
|
242
|
+
if (hasExecutionKeywords(contentLower)) {
|
|
243
|
+
return { type: 'code_execution', confidence: 0.8, keywords: ['execution'] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 7. Complex task (all tools)
|
|
247
|
+
if (hasComplexKeywords(contentLower)) {
|
|
248
|
+
return { type: 'complex_task', confidence: 0.75, keywords: ['complex'] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Long conversations likely need more tools
|
|
252
|
+
if (messageCount > 10) {
|
|
253
|
+
return { type: 'complex_task', confidence: 0.7, keywords: ['long_conversation'] };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Default: coding (core tools)
|
|
257
|
+
return { type: 'coding', confidence: 0.6, keywords: ['default'] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Estimate token count for tools (rough approximation)
|
|
262
|
+
*/
|
|
263
|
+
function estimateToolTokens(tools) {
|
|
264
|
+
if (!Array.isArray(tools)) return 0;
|
|
265
|
+
|
|
266
|
+
// Average: ~175 tokens per tool (based on STANDARD_TOOLS analysis)
|
|
267
|
+
return tools.length * 175;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Select relevant tools based on classification
|
|
272
|
+
*
|
|
273
|
+
* @param {Array} tools - Available tools
|
|
274
|
+
* @param {Object} classification - Classification result from classifyRequestType
|
|
275
|
+
* @param {Object} options - Selection options (provider, tokenBudget, config)
|
|
276
|
+
* @returns {Array} Filtered list of relevant tools
|
|
277
|
+
*/
|
|
278
|
+
function selectToolsSmartly(tools, classification, options = {}) {
|
|
279
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
280
|
+
return tools;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { provider = 'databricks', tokenBudget = 2500, config = {} } = options;
|
|
284
|
+
const requestType = classification.type || 'coding';
|
|
285
|
+
|
|
286
|
+
// Get relevant tool names for this request type
|
|
287
|
+
const relevantToolNames = TOOL_SELECTION_MAP[requestType] || TOOL_SELECTION_MAP.coding;
|
|
288
|
+
|
|
289
|
+
logger.debug({
|
|
290
|
+
requestType,
|
|
291
|
+
confidence: classification.confidence,
|
|
292
|
+
relevantToolNames,
|
|
293
|
+
mode: config.mode
|
|
294
|
+
}, 'Selecting tools for request type');
|
|
295
|
+
|
|
296
|
+
// Filter to relevant tools only
|
|
297
|
+
let selectedTools = tools.filter(tool => relevantToolNames.includes(tool.name));
|
|
298
|
+
|
|
299
|
+
// Mode-specific adjustments
|
|
300
|
+
if (config.mode === 'aggressive') {
|
|
301
|
+
// Aggressive: Further reduce tools for ambiguous cases
|
|
302
|
+
if (classification.confidence < 0.7 && selectedTools.length > 4) {
|
|
303
|
+
selectedTools = selectedTools.slice(0, 4);
|
|
304
|
+
}
|
|
305
|
+
} else if (config.mode === 'conservative') {
|
|
306
|
+
// Conservative: Include one extra tier of tools for safety
|
|
307
|
+
if (requestType === 'file_reading' && !relevantToolNames.includes('Bash')) {
|
|
308
|
+
const bashTool = tools.find(t => t.name === 'Bash');
|
|
309
|
+
if (bashTool) selectedTools.push(bashTool);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Provider-specific limits
|
|
314
|
+
if (provider === 'ollama' && selectedTools.length > 8) {
|
|
315
|
+
logger.debug({
|
|
316
|
+
originalCount: selectedTools.length,
|
|
317
|
+
limit: 8
|
|
318
|
+
}, 'Limiting tools for Ollama provider');
|
|
319
|
+
selectedTools = selectedTools.slice(0, 8);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Token budget enforcement
|
|
323
|
+
const estimatedTokens = estimateToolTokens(selectedTools);
|
|
324
|
+
if (estimatedTokens > tokenBudget) {
|
|
325
|
+
const targetCount = Math.floor(tokenBudget / 175);
|
|
326
|
+
logger.debug({
|
|
327
|
+
estimatedTokens,
|
|
328
|
+
tokenBudget,
|
|
329
|
+
targetCount
|
|
330
|
+
}, 'Enforcing token budget on tools');
|
|
331
|
+
selectedTools = selectedTools.slice(0, Math.max(targetCount, 0));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Minimal mode override (if configured)
|
|
335
|
+
if (config.minimalMode) {
|
|
336
|
+
const minimalTools = ['Read', 'Write', 'Edit', 'Bash'];
|
|
337
|
+
selectedTools = selectedTools.filter(t => minimalTools.includes(t.name));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
logger.info({
|
|
341
|
+
requestType,
|
|
342
|
+
originalToolCount: tools.length,
|
|
343
|
+
selectedToolCount: selectedTools.length,
|
|
344
|
+
removedToolCount: tools.length - selectedTools.length,
|
|
345
|
+
estimatedTokenSavings: estimateToolTokens(tools) - estimateToolTokens(selectedTools)
|
|
346
|
+
}, 'Smart tool selection completed');
|
|
347
|
+
|
|
348
|
+
return selectedTools;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
classifyRequestType,
|
|
353
|
+
selectToolsSmartly,
|
|
354
|
+
estimateToolTokens,
|
|
355
|
+
TOOL_SELECTION_MAP
|
|
356
|
+
};
|