@probelabs/probe 0.6.0-rc101 → 0.6.0-rc103
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/build/agent/ProbeAgent.js +254 -4
- package/build/agent/index.js +257 -41
- package/build/mcp/index.js +24 -145
- package/build/mcp/index.ts +23 -162
- package/build/tools/system-message.js +7 -1
- package/cjs/agent/ProbeAgent.cjs +202 -11
- package/cjs/index.cjs +214 -23
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +254 -4
- package/src/agent/index.js +50 -18
- package/src/mcp/index.ts +23 -162
- package/src/tools/system-message.js +7 -1
|
@@ -5,6 +5,9 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
|
5
5
|
import { streamText } from 'ai';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { readFile, stat } from 'fs/promises';
|
|
10
|
+
import { resolve, isAbsolute } from 'path';
|
|
8
11
|
import { TokenCounter } from './tokenCounter.js';
|
|
9
12
|
import {
|
|
10
13
|
createTools,
|
|
@@ -46,6 +49,12 @@ import {
|
|
|
46
49
|
const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
|
|
47
50
|
const MAX_HISTORY_MESSAGES = 100;
|
|
48
51
|
|
|
52
|
+
// Supported image file extensions
|
|
53
|
+
const SUPPORTED_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
|
|
54
|
+
|
|
55
|
+
// Maximum image file size (20MB) to prevent OOM attacks
|
|
56
|
+
const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
|
|
57
|
+
|
|
49
58
|
/**
|
|
50
59
|
* ProbeAgent class to handle AI interactions with code search capabilities
|
|
51
60
|
*/
|
|
@@ -105,6 +114,10 @@ export class ProbeAgent {
|
|
|
105
114
|
// Initialize chat history
|
|
106
115
|
this.history = [];
|
|
107
116
|
|
|
117
|
+
// Initialize image tracking for agentic loop
|
|
118
|
+
this.pendingImages = new Map(); // Map<imagePath, base64Data> to avoid reloading
|
|
119
|
+
this.currentImages = []; // Currently active images for AI calls
|
|
120
|
+
|
|
108
121
|
// Initialize event emitter for tool execution updates
|
|
109
122
|
this.events = new EventEmitter();
|
|
110
123
|
|
|
@@ -268,6 +281,226 @@ export class ProbeAgent {
|
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Process assistant response content and detect/load image references
|
|
286
|
+
* @param {string} content - The assistant's response content
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async processImageReferences(content) {
|
|
290
|
+
if (!content) return;
|
|
291
|
+
|
|
292
|
+
// Enhanced pattern to detect image file mentions in various contexts
|
|
293
|
+
// Looks for: "image", "file", "screenshot", etc. followed by path-like strings with image extensions
|
|
294
|
+
const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join('|')})`;
|
|
295
|
+
const imagePatterns = [
|
|
296
|
+
// Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
|
|
297
|
+
new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\\.${extensionsPattern}(?!\\w)`, 'gi'),
|
|
298
|
+
// Contextual mentions: "look at image.png", "the file screenshot.jpg shows"
|
|
299
|
+
new RegExp(`(?:image|file|screenshot|diagram|photo|picture|graphic)\\s*:?\\s*([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi'),
|
|
300
|
+
// Tool result mentions: often contain file paths
|
|
301
|
+
new RegExp(`(?:found|saved|created|generated).*?([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi')
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const foundPaths = new Set();
|
|
305
|
+
|
|
306
|
+
// Extract potential image paths using all patterns
|
|
307
|
+
for (const pattern of imagePatterns) {
|
|
308
|
+
let match;
|
|
309
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
310
|
+
// For patterns with capture groups, use the captured path; otherwise use the full match
|
|
311
|
+
const imagePath = match[1] || match[0];
|
|
312
|
+
if (imagePath && imagePath.length > 0) {
|
|
313
|
+
foundPaths.add(imagePath.trim());
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (foundPaths.size === 0) return;
|
|
319
|
+
|
|
320
|
+
if (this.debug) {
|
|
321
|
+
console.log(`[DEBUG] Found ${foundPaths.size} potential image references:`, Array.from(foundPaths));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Process each found path
|
|
325
|
+
for (const imagePath of foundPaths) {
|
|
326
|
+
await this.loadImageIfValid(imagePath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load and cache an image if it's valid and accessible
|
|
332
|
+
* @param {string} imagePath - Path to the image file
|
|
333
|
+
* @returns {Promise<boolean>} - True if image was loaded successfully
|
|
334
|
+
*/
|
|
335
|
+
async loadImageIfValid(imagePath) {
|
|
336
|
+
try {
|
|
337
|
+
// Skip if already loaded
|
|
338
|
+
if (this.pendingImages.has(imagePath)) {
|
|
339
|
+
if (this.debug) {
|
|
340
|
+
console.log(`[DEBUG] Image already loaded: ${imagePath}`);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Security validation: check if path is within any allowed directory
|
|
346
|
+
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
347
|
+
|
|
348
|
+
let absolutePath;
|
|
349
|
+
let isPathAllowed = false;
|
|
350
|
+
|
|
351
|
+
// If absolute path, check if it's within any allowed directory
|
|
352
|
+
if (isAbsolute(imagePath)) {
|
|
353
|
+
absolutePath = imagePath;
|
|
354
|
+
isPathAllowed = allowedDirs.some(dir => absolutePath.startsWith(resolve(dir)));
|
|
355
|
+
} else {
|
|
356
|
+
// For relative paths, try resolving against each allowed directory
|
|
357
|
+
for (const dir of allowedDirs) {
|
|
358
|
+
const resolvedPath = resolve(dir, imagePath);
|
|
359
|
+
if (resolvedPath.startsWith(resolve(dir))) {
|
|
360
|
+
absolutePath = resolvedPath;
|
|
361
|
+
isPathAllowed = true;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Security check: ensure path is within at least one allowed directory
|
|
368
|
+
if (!isPathAllowed) {
|
|
369
|
+
if (this.debug) {
|
|
370
|
+
console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check if file exists and get file stats
|
|
376
|
+
let fileStats;
|
|
377
|
+
try {
|
|
378
|
+
fileStats = await stat(absolutePath);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (this.debug) {
|
|
381
|
+
console.log(`[DEBUG] Image file not found: ${absolutePath}`);
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate file size to prevent OOM attacks
|
|
387
|
+
if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
|
|
388
|
+
if (this.debug) {
|
|
389
|
+
console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Validate file extension
|
|
395
|
+
const extension = absolutePath.toLowerCase().split('.').pop();
|
|
396
|
+
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
|
397
|
+
if (this.debug) {
|
|
398
|
+
console.log(`[DEBUG] Unsupported image format: ${extension}`);
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Determine MIME type
|
|
404
|
+
const mimeTypes = {
|
|
405
|
+
'png': 'image/png',
|
|
406
|
+
'jpg': 'image/jpeg',
|
|
407
|
+
'jpeg': 'image/jpeg',
|
|
408
|
+
'webp': 'image/webp',
|
|
409
|
+
'gif': 'image/gif',
|
|
410
|
+
'bmp': 'image/bmp',
|
|
411
|
+
'svg': 'image/svg+xml'
|
|
412
|
+
};
|
|
413
|
+
const mimeType = mimeTypes[extension];
|
|
414
|
+
|
|
415
|
+
// Read and encode file asynchronously
|
|
416
|
+
const fileBuffer = await readFile(absolutePath);
|
|
417
|
+
const base64Data = fileBuffer.toString('base64');
|
|
418
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
419
|
+
|
|
420
|
+
// Cache the loaded image
|
|
421
|
+
this.pendingImages.set(imagePath, dataUrl);
|
|
422
|
+
|
|
423
|
+
if (this.debug) {
|
|
424
|
+
console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return true;
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (this.debug) {
|
|
430
|
+
console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get all currently loaded images as an array for AI model consumption
|
|
438
|
+
* @returns {Array<string>} - Array of base64 data URLs
|
|
439
|
+
*/
|
|
440
|
+
getCurrentImages() {
|
|
441
|
+
return Array.from(this.pendingImages.values());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Clear loaded images (useful for new conversations)
|
|
446
|
+
*/
|
|
447
|
+
clearLoadedImages() {
|
|
448
|
+
this.pendingImages.clear();
|
|
449
|
+
this.currentImages = [];
|
|
450
|
+
if (this.debug) {
|
|
451
|
+
console.log('[DEBUG] Cleared all loaded images');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Prepare messages for AI consumption, adding images to the latest user message if available
|
|
457
|
+
* @param {Array} messages - Current conversation messages
|
|
458
|
+
* @returns {Array} - Messages formatted for AI SDK with potential image content
|
|
459
|
+
*/
|
|
460
|
+
prepareMessagesWithImages(messages) {
|
|
461
|
+
const loadedImages = this.getCurrentImages();
|
|
462
|
+
|
|
463
|
+
// If no images loaded, return messages as-is
|
|
464
|
+
if (loadedImages.length === 0) {
|
|
465
|
+
return messages;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Clone messages to avoid mutating the original
|
|
469
|
+
const messagesWithImages = [...messages];
|
|
470
|
+
|
|
471
|
+
// Find the last user message to attach images to
|
|
472
|
+
const lastUserMessageIndex = messagesWithImages.map(m => m.role).lastIndexOf('user');
|
|
473
|
+
|
|
474
|
+
if (lastUserMessageIndex === -1) {
|
|
475
|
+
if (this.debug) {
|
|
476
|
+
console.log('[DEBUG] No user messages found to attach images to');
|
|
477
|
+
}
|
|
478
|
+
return messages;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const lastUserMessage = messagesWithImages[lastUserMessageIndex];
|
|
482
|
+
|
|
483
|
+
// Convert to multimodal format if we have images
|
|
484
|
+
if (typeof lastUserMessage.content === 'string') {
|
|
485
|
+
messagesWithImages[lastUserMessageIndex] = {
|
|
486
|
+
...lastUserMessage,
|
|
487
|
+
content: [
|
|
488
|
+
{ type: 'text', text: lastUserMessage.content },
|
|
489
|
+
...loadedImages.map(imageData => ({
|
|
490
|
+
type: 'image',
|
|
491
|
+
image: imageData
|
|
492
|
+
}))
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (this.debug) {
|
|
497
|
+
console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return messagesWithImages;
|
|
502
|
+
}
|
|
503
|
+
|
|
271
504
|
/**
|
|
272
505
|
* Initialize mock model for testing
|
|
273
506
|
*/
|
|
@@ -384,7 +617,7 @@ Examples:
|
|
|
384
617
|
</extract>
|
|
385
618
|
|
|
386
619
|
<attempt_completion>
|
|
387
|
-
|
|
620
|
+
The configuration is loaded from src/config.js lines 15-25 which contains the database settings.
|
|
388
621
|
</attempt_completion>
|
|
389
622
|
|
|
390
623
|
# Special Case: Quick Completion
|
|
@@ -411,7 +644,7 @@ I need to find code related to error handling in the search module. The most app
|
|
|
411
644
|
6. Wait for the tool execution result, which will be provided in the next message (within a <tool_result> block).
|
|
412
645
|
7. Analyze the tool result and decide the next step. If more tool calls are needed, repeat steps 2-6.
|
|
413
646
|
8. If the task is fully complete and all previous steps were successful, use the \`<attempt_completion>\` tool to provide the final answer. This is the ONLY way to finish the task.
|
|
414
|
-
9. If you cannot proceed (e.g., missing information, invalid request), explain the issue clearly
|
|
647
|
+
9. If you cannot proceed (e.g., missing information, invalid request), use \`<attempt_completion>\` to explain the issue clearly with an appropriate message directly inside the tags.
|
|
415
648
|
10. If your previous response was already correct and complete, you may use \`<attempt_complete>\` as a shorthand.
|
|
416
649
|
|
|
417
650
|
Available Tools:
|
|
@@ -695,9 +928,12 @@ When troubleshooting:
|
|
|
695
928
|
try {
|
|
696
929
|
// Wrap AI request with tracing if available
|
|
697
930
|
const executeAIRequest = async () => {
|
|
931
|
+
// Prepare messages with potential image content
|
|
932
|
+
const messagesForAI = this.prepareMessagesWithImages(currentMessages);
|
|
933
|
+
|
|
698
934
|
const result = await streamText({
|
|
699
935
|
model: this.provider(this.model),
|
|
700
|
-
messages:
|
|
936
|
+
messages: messagesForAI,
|
|
701
937
|
maxTokens: maxResponseTokens,
|
|
702
938
|
temperature: 0.3,
|
|
703
939
|
});
|
|
@@ -741,6 +977,11 @@ When troubleshooting:
|
|
|
741
977
|
console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
|
|
742
978
|
}
|
|
743
979
|
|
|
980
|
+
// Process image references in assistant response for next iteration
|
|
981
|
+
if (assistantResponseContent) {
|
|
982
|
+
await this.processImageReferences(assistantResponseContent);
|
|
983
|
+
}
|
|
984
|
+
|
|
744
985
|
// Parse tool call from response with valid tools list
|
|
745
986
|
const validTools = [
|
|
746
987
|
'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
|
|
@@ -898,11 +1139,20 @@ When troubleshooting:
|
|
|
898
1139
|
|
|
899
1140
|
// Add assistant response and tool result to conversation
|
|
900
1141
|
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
1142
|
+
|
|
1143
|
+
const toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
|
|
1144
|
+
const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
|
|
1145
|
+
|
|
901
1146
|
currentMessages.push({
|
|
902
1147
|
role: 'user',
|
|
903
|
-
content:
|
|
1148
|
+
content: toolResultMessage
|
|
904
1149
|
});
|
|
905
1150
|
|
|
1151
|
+
// Process tool result for image references
|
|
1152
|
+
if (toolResultContent) {
|
|
1153
|
+
await this.processImageReferences(toolResultContent);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
906
1156
|
if (this.debug) {
|
|
907
1157
|
console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
|
|
908
1158
|
}
|