@probelabs/probe 0.6.0-rc102 → 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 +252 -2
- package/build/agent/index.js +228 -36
- package/build/mcp/index.js +23 -144
- package/build/mcp/index.ts +22 -161
- package/cjs/agent/ProbeAgent.cjs +193 -8
- package/cjs/index.cjs +205 -20
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +252 -2
- package/src/agent/index.js +26 -14
- package/src/mcp/index.ts +22 -161
package/cjs/index.cjs
CHANGED
|
@@ -1543,7 +1543,7 @@ async function delegate({ task, timeout = 300, debug = false, currentIteration =
|
|
|
1543
1543
|
console.error(`[DELEGATE] Using binary at: ${binaryPath}`);
|
|
1544
1544
|
console.error(`[DELEGATE] Command args: ${args.join(" ")}`);
|
|
1545
1545
|
}
|
|
1546
|
-
return new Promise((
|
|
1546
|
+
return new Promise((resolve2, reject) => {
|
|
1547
1547
|
const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
|
|
1548
1548
|
const process2 = (0, import_child_process5.spawn)(binaryPath, args, {
|
|
1549
1549
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1608,7 +1608,7 @@ async function delegate({ task, timeout = 300, debug = false, currentIteration =
|
|
|
1608
1608
|
delegationSpan.end();
|
|
1609
1609
|
}
|
|
1610
1610
|
}
|
|
1611
|
-
|
|
1611
|
+
resolve2(response);
|
|
1612
1612
|
} else {
|
|
1613
1613
|
const errorMessage = stderr.trim() || `Delegate process failed with exit code ${code}`;
|
|
1614
1614
|
if (debug) {
|
|
@@ -2868,7 +2868,7 @@ function createMockProvider() {
|
|
|
2868
2868
|
provider: "mock",
|
|
2869
2869
|
// Mock the doGenerate method used by Vercel AI SDK
|
|
2870
2870
|
doGenerate: async ({ messages, tools: tools2 }) => {
|
|
2871
|
-
await new Promise((
|
|
2871
|
+
await new Promise((resolve2) => setTimeout(resolve2, 10));
|
|
2872
2872
|
return {
|
|
2873
2873
|
text: "This is a mock response for testing",
|
|
2874
2874
|
toolCalls: [],
|
|
@@ -4693,7 +4693,7 @@ var ProbeAgent_exports = {};
|
|
|
4693
4693
|
__export(ProbeAgent_exports, {
|
|
4694
4694
|
ProbeAgent: () => ProbeAgent
|
|
4695
4695
|
});
|
|
4696
|
-
var import_anthropic, import_openai, import_google, import_ai2, import_crypto5, import_events2, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, ProbeAgent;
|
|
4696
|
+
var import_anthropic, import_openai, import_google, import_ai2, import_crypto5, import_events2, import_fs5, import_promises, import_path7, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, SUPPORTED_IMAGE_EXTENSIONS, MAX_IMAGE_FILE_SIZE, ProbeAgent;
|
|
4697
4697
|
var init_ProbeAgent = __esm({
|
|
4698
4698
|
"src/agent/ProbeAgent.js"() {
|
|
4699
4699
|
"use strict";
|
|
@@ -4703,6 +4703,9 @@ var init_ProbeAgent = __esm({
|
|
|
4703
4703
|
import_ai2 = require("ai");
|
|
4704
4704
|
import_crypto5 = require("crypto");
|
|
4705
4705
|
import_events2 = require("events");
|
|
4706
|
+
import_fs5 = require("fs");
|
|
4707
|
+
import_promises = require("fs/promises");
|
|
4708
|
+
import_path7 = require("path");
|
|
4706
4709
|
init_tokenCounter();
|
|
4707
4710
|
init_tools2();
|
|
4708
4711
|
init_common();
|
|
@@ -4713,6 +4716,8 @@ var init_ProbeAgent = __esm({
|
|
|
4713
4716
|
init_mcp();
|
|
4714
4717
|
MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || "30", 10);
|
|
4715
4718
|
MAX_HISTORY_MESSAGES = 100;
|
|
4719
|
+
SUPPORTED_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif", "bmp", "svg"];
|
|
4720
|
+
MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
|
|
4716
4721
|
ProbeAgent = class {
|
|
4717
4722
|
/**
|
|
4718
4723
|
* Create a new ProbeAgent instance
|
|
@@ -4756,6 +4761,8 @@ var init_ProbeAgent = __esm({
|
|
|
4756
4761
|
}
|
|
4757
4762
|
this.initializeTools();
|
|
4758
4763
|
this.history = [];
|
|
4764
|
+
this.pendingImages = /* @__PURE__ */ new Map();
|
|
4765
|
+
this.currentImages = [];
|
|
4759
4766
|
this.events = new import_events2.EventEmitter();
|
|
4760
4767
|
this.enableMcp = !!options.enableMcp || process.env.ENABLE_MCP === "1";
|
|
4761
4768
|
this.mcpConfigPath = options.mcpConfigPath || null;
|
|
@@ -4881,6 +4888,175 @@ var init_ProbeAgent = __esm({
|
|
|
4881
4888
|
console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
|
|
4882
4889
|
}
|
|
4883
4890
|
}
|
|
4891
|
+
/**
|
|
4892
|
+
* Process assistant response content and detect/load image references
|
|
4893
|
+
* @param {string} content - The assistant's response content
|
|
4894
|
+
* @returns {Promise<void>}
|
|
4895
|
+
*/
|
|
4896
|
+
async processImageReferences(content) {
|
|
4897
|
+
if (!content) return;
|
|
4898
|
+
const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join("|")})`;
|
|
4899
|
+
const imagePatterns = [
|
|
4900
|
+
// Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
|
|
4901
|
+
new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\.${extensionsPattern}(?!\\w)`, "gi"),
|
|
4902
|
+
// Contextual mentions: "look at image.png", "the file screenshot.jpg shows"
|
|
4903
|
+
new RegExp(`(?:image|file|screenshot|diagram|photo|picture|graphic)\\s*:?\\s*([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, "gi"),
|
|
4904
|
+
// Tool result mentions: often contain file paths
|
|
4905
|
+
new RegExp(`(?:found|saved|created|generated).*?([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, "gi")
|
|
4906
|
+
];
|
|
4907
|
+
const foundPaths = /* @__PURE__ */ new Set();
|
|
4908
|
+
for (const pattern of imagePatterns) {
|
|
4909
|
+
let match;
|
|
4910
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
4911
|
+
const imagePath = match[1] || match[0];
|
|
4912
|
+
if (imagePath && imagePath.length > 0) {
|
|
4913
|
+
foundPaths.add(imagePath.trim());
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
if (foundPaths.size === 0) return;
|
|
4918
|
+
if (this.debug) {
|
|
4919
|
+
console.log(`[DEBUG] Found ${foundPaths.size} potential image references:`, Array.from(foundPaths));
|
|
4920
|
+
}
|
|
4921
|
+
for (const imagePath of foundPaths) {
|
|
4922
|
+
await this.loadImageIfValid(imagePath);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Load and cache an image if it's valid and accessible
|
|
4927
|
+
* @param {string} imagePath - Path to the image file
|
|
4928
|
+
* @returns {Promise<boolean>} - True if image was loaded successfully
|
|
4929
|
+
*/
|
|
4930
|
+
async loadImageIfValid(imagePath) {
|
|
4931
|
+
try {
|
|
4932
|
+
if (this.pendingImages.has(imagePath)) {
|
|
4933
|
+
if (this.debug) {
|
|
4934
|
+
console.log(`[DEBUG] Image already loaded: ${imagePath}`);
|
|
4935
|
+
}
|
|
4936
|
+
return true;
|
|
4937
|
+
}
|
|
4938
|
+
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
4939
|
+
let absolutePath;
|
|
4940
|
+
let isPathAllowed = false;
|
|
4941
|
+
if ((0, import_path7.isAbsolute)(imagePath)) {
|
|
4942
|
+
absolutePath = imagePath;
|
|
4943
|
+
isPathAllowed = allowedDirs.some((dir) => absolutePath.startsWith((0, import_path7.resolve)(dir)));
|
|
4944
|
+
} else {
|
|
4945
|
+
for (const dir of allowedDirs) {
|
|
4946
|
+
const resolvedPath = (0, import_path7.resolve)(dir, imagePath);
|
|
4947
|
+
if (resolvedPath.startsWith((0, import_path7.resolve)(dir))) {
|
|
4948
|
+
absolutePath = resolvedPath;
|
|
4949
|
+
isPathAllowed = true;
|
|
4950
|
+
break;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
if (!isPathAllowed) {
|
|
4955
|
+
if (this.debug) {
|
|
4956
|
+
console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
|
|
4957
|
+
}
|
|
4958
|
+
return false;
|
|
4959
|
+
}
|
|
4960
|
+
let fileStats;
|
|
4961
|
+
try {
|
|
4962
|
+
fileStats = await (0, import_promises.stat)(absolutePath);
|
|
4963
|
+
} catch (error) {
|
|
4964
|
+
if (this.debug) {
|
|
4965
|
+
console.log(`[DEBUG] Image file not found: ${absolutePath}`);
|
|
4966
|
+
}
|
|
4967
|
+
return false;
|
|
4968
|
+
}
|
|
4969
|
+
if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
|
|
4970
|
+
if (this.debug) {
|
|
4971
|
+
console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
|
|
4972
|
+
}
|
|
4973
|
+
return false;
|
|
4974
|
+
}
|
|
4975
|
+
const extension = absolutePath.toLowerCase().split(".").pop();
|
|
4976
|
+
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
|
4977
|
+
if (this.debug) {
|
|
4978
|
+
console.log(`[DEBUG] Unsupported image format: ${extension}`);
|
|
4979
|
+
}
|
|
4980
|
+
return false;
|
|
4981
|
+
}
|
|
4982
|
+
const mimeTypes = {
|
|
4983
|
+
"png": "image/png",
|
|
4984
|
+
"jpg": "image/jpeg",
|
|
4985
|
+
"jpeg": "image/jpeg",
|
|
4986
|
+
"webp": "image/webp",
|
|
4987
|
+
"gif": "image/gif",
|
|
4988
|
+
"bmp": "image/bmp",
|
|
4989
|
+
"svg": "image/svg+xml"
|
|
4990
|
+
};
|
|
4991
|
+
const mimeType = mimeTypes[extension];
|
|
4992
|
+
const fileBuffer = await (0, import_promises.readFile)(absolutePath);
|
|
4993
|
+
const base64Data = fileBuffer.toString("base64");
|
|
4994
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
4995
|
+
this.pendingImages.set(imagePath, dataUrl);
|
|
4996
|
+
if (this.debug) {
|
|
4997
|
+
console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
|
|
4998
|
+
}
|
|
4999
|
+
return true;
|
|
5000
|
+
} catch (error) {
|
|
5001
|
+
if (this.debug) {
|
|
5002
|
+
console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
|
|
5003
|
+
}
|
|
5004
|
+
return false;
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
/**
|
|
5008
|
+
* Get all currently loaded images as an array for AI model consumption
|
|
5009
|
+
* @returns {Array<string>} - Array of base64 data URLs
|
|
5010
|
+
*/
|
|
5011
|
+
getCurrentImages() {
|
|
5012
|
+
return Array.from(this.pendingImages.values());
|
|
5013
|
+
}
|
|
5014
|
+
/**
|
|
5015
|
+
* Clear loaded images (useful for new conversations)
|
|
5016
|
+
*/
|
|
5017
|
+
clearLoadedImages() {
|
|
5018
|
+
this.pendingImages.clear();
|
|
5019
|
+
this.currentImages = [];
|
|
5020
|
+
if (this.debug) {
|
|
5021
|
+
console.log("[DEBUG] Cleared all loaded images");
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
/**
|
|
5025
|
+
* Prepare messages for AI consumption, adding images to the latest user message if available
|
|
5026
|
+
* @param {Array} messages - Current conversation messages
|
|
5027
|
+
* @returns {Array} - Messages formatted for AI SDK with potential image content
|
|
5028
|
+
*/
|
|
5029
|
+
prepareMessagesWithImages(messages) {
|
|
5030
|
+
const loadedImages = this.getCurrentImages();
|
|
5031
|
+
if (loadedImages.length === 0) {
|
|
5032
|
+
return messages;
|
|
5033
|
+
}
|
|
5034
|
+
const messagesWithImages = [...messages];
|
|
5035
|
+
const lastUserMessageIndex = messagesWithImages.map((m) => m.role).lastIndexOf("user");
|
|
5036
|
+
if (lastUserMessageIndex === -1) {
|
|
5037
|
+
if (this.debug) {
|
|
5038
|
+
console.log("[DEBUG] No user messages found to attach images to");
|
|
5039
|
+
}
|
|
5040
|
+
return messages;
|
|
5041
|
+
}
|
|
5042
|
+
const lastUserMessage = messagesWithImages[lastUserMessageIndex];
|
|
5043
|
+
if (typeof lastUserMessage.content === "string") {
|
|
5044
|
+
messagesWithImages[lastUserMessageIndex] = {
|
|
5045
|
+
...lastUserMessage,
|
|
5046
|
+
content: [
|
|
5047
|
+
{ type: "text", text: lastUserMessage.content },
|
|
5048
|
+
...loadedImages.map((imageData) => ({
|
|
5049
|
+
type: "image",
|
|
5050
|
+
image: imageData
|
|
5051
|
+
}))
|
|
5052
|
+
]
|
|
5053
|
+
};
|
|
5054
|
+
if (this.debug) {
|
|
5055
|
+
console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
return messagesWithImages;
|
|
5059
|
+
}
|
|
4884
5060
|
/**
|
|
4885
5061
|
* Initialize mock model for testing
|
|
4886
5062
|
*/
|
|
@@ -5262,9 +5438,10 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
5262
5438
|
let assistantResponseContent = "";
|
|
5263
5439
|
try {
|
|
5264
5440
|
const executeAIRequest = async () => {
|
|
5441
|
+
const messagesForAI = this.prepareMessagesWithImages(currentMessages);
|
|
5265
5442
|
const result = await (0, import_ai2.streamText)({
|
|
5266
5443
|
model: this.provider(this.model),
|
|
5267
|
-
messages:
|
|
5444
|
+
messages: messagesForAI,
|
|
5268
5445
|
maxTokens: maxResponseTokens,
|
|
5269
5446
|
temperature: 0.3
|
|
5270
5447
|
});
|
|
@@ -5298,6 +5475,9 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
5298
5475
|
const assistantPreview = createMessagePreview(assistantResponseContent);
|
|
5299
5476
|
console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
|
|
5300
5477
|
}
|
|
5478
|
+
if (assistantResponseContent) {
|
|
5479
|
+
await this.processImageReferences(assistantResponseContent);
|
|
5480
|
+
}
|
|
5301
5481
|
const validTools = [
|
|
5302
5482
|
"search",
|
|
5303
5483
|
"query",
|
|
@@ -5422,12 +5602,17 @@ ${toolResultContent}
|
|
|
5422
5602
|
throw toolError;
|
|
5423
5603
|
}
|
|
5424
5604
|
currentMessages.push({ role: "assistant", content: assistantResponseContent });
|
|
5605
|
+
const toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
|
|
5606
|
+
const toolResultMessage = `<tool_result>
|
|
5607
|
+
${toolResultContent}
|
|
5608
|
+
</tool_result>`;
|
|
5425
5609
|
currentMessages.push({
|
|
5426
5610
|
role: "user",
|
|
5427
|
-
content:
|
|
5428
|
-
${typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2)}
|
|
5429
|
-
</tool_result>`
|
|
5611
|
+
content: toolResultMessage
|
|
5430
5612
|
});
|
|
5613
|
+
if (toolResultContent) {
|
|
5614
|
+
await this.processImageReferences(toolResultContent);
|
|
5615
|
+
}
|
|
5431
5616
|
if (this.debug) {
|
|
5432
5617
|
console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === "string" ? toolResult.length : JSON.stringify(toolResult).length}`);
|
|
5433
5618
|
}
|
|
@@ -5925,12 +6110,12 @@ function initializeSimpleTelemetryFromOptions(options) {
|
|
|
5925
6110
|
});
|
|
5926
6111
|
return telemetry;
|
|
5927
6112
|
}
|
|
5928
|
-
var
|
|
6113
|
+
var import_fs6, import_path8, SimpleTelemetry, SimpleAppTracer;
|
|
5929
6114
|
var init_simpleTelemetry = __esm({
|
|
5930
6115
|
"src/agent/simpleTelemetry.js"() {
|
|
5931
6116
|
"use strict";
|
|
5932
|
-
|
|
5933
|
-
|
|
6117
|
+
import_fs6 = require("fs");
|
|
6118
|
+
import_path8 = require("path");
|
|
5934
6119
|
SimpleTelemetry = class {
|
|
5935
6120
|
constructor(options = {}) {
|
|
5936
6121
|
this.serviceName = options.serviceName || "probe-agent";
|
|
@@ -5944,11 +6129,11 @@ var init_simpleTelemetry = __esm({
|
|
|
5944
6129
|
}
|
|
5945
6130
|
initializeFileExporter() {
|
|
5946
6131
|
try {
|
|
5947
|
-
const dir = (0,
|
|
5948
|
-
if (!(0,
|
|
5949
|
-
(0,
|
|
6132
|
+
const dir = (0, import_path8.dirname)(this.filePath);
|
|
6133
|
+
if (!(0, import_fs6.existsSync)(dir)) {
|
|
6134
|
+
(0, import_fs6.mkdirSync)(dir, { recursive: true });
|
|
5950
6135
|
}
|
|
5951
|
-
this.stream = (0,
|
|
6136
|
+
this.stream = (0, import_fs6.createWriteStream)(this.filePath, { flags: "a" });
|
|
5952
6137
|
this.stream.on("error", (error) => {
|
|
5953
6138
|
console.error(`[SimpleTelemetry] Stream error: ${error.message}`);
|
|
5954
6139
|
});
|
|
@@ -6009,20 +6194,20 @@ var init_simpleTelemetry = __esm({
|
|
|
6009
6194
|
}
|
|
6010
6195
|
async flush() {
|
|
6011
6196
|
if (this.stream) {
|
|
6012
|
-
return new Promise((
|
|
6013
|
-
this.stream.once("drain",
|
|
6197
|
+
return new Promise((resolve2) => {
|
|
6198
|
+
this.stream.once("drain", resolve2);
|
|
6014
6199
|
if (!this.stream.writableNeedDrain) {
|
|
6015
|
-
|
|
6200
|
+
resolve2();
|
|
6016
6201
|
}
|
|
6017
6202
|
});
|
|
6018
6203
|
}
|
|
6019
6204
|
}
|
|
6020
6205
|
async shutdown() {
|
|
6021
6206
|
if (this.stream) {
|
|
6022
|
-
return new Promise((
|
|
6207
|
+
return new Promise((resolve2) => {
|
|
6023
6208
|
this.stream.end(() => {
|
|
6024
6209
|
console.log(`[SimpleTelemetry] File stream closed: ${this.filePath}`);
|
|
6025
|
-
|
|
6210
|
+
resolve2();
|
|
6026
6211
|
});
|
|
6027
6212
|
});
|
|
6028
6213
|
}
|
package/package.json
CHANGED
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -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
|
}
|