@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.
@@ -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
- <result>The configuration is loaded from src/config.js lines 15-25 which contains the database settings.</result>
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 before using \`<attempt_completion>\` with an appropriate message in the \`<result>\` tag.
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: currentMessages,
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: `<tool_result>\n${typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2)}\n</tool_result>`
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
  }