@probelabs/probe-chat 0.6.0-rc56
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/README.md +338 -0
- package/TRACING.md +226 -0
- package/appTracer.js +947 -0
- package/auth.js +76 -0
- package/bin/probe-chat.js +13 -0
- package/cancelRequest.js +84 -0
- package/fileSpanExporter.js +183 -0
- package/implement/README.md +228 -0
- package/implement/backends/AiderBackend.js +750 -0
- package/implement/backends/BaseBackend.js +276 -0
- package/implement/backends/ClaudeCodeBackend.js +767 -0
- package/implement/backends/MockBackend.js +237 -0
- package/implement/backends/registry.js +85 -0
- package/implement/core/BackendManager.js +567 -0
- package/implement/core/ImplementTool.js +354 -0
- package/implement/core/config.js +428 -0
- package/implement/core/timeouts.js +58 -0
- package/implement/core/utils.js +496 -0
- package/implement/types/BackendTypes.js +126 -0
- package/index.html +3686 -0
- package/index.js +575 -0
- package/logo.png +0 -0
- package/package.json +100 -0
- package/probeChat.js +1677 -0
- package/probeTool.js +714 -0
- package/storage/JsonChatStorage.js +476 -0
- package/telemetry.js +287 -0
- package/test/integration/chatFlows.test.js +320 -0
- package/test/integration/toolCalling.test.js +471 -0
- package/test/mocks/mockLLMProvider.js +269 -0
- package/test/test-backends.js +90 -0
- package/test/testUtils.js +530 -0
- package/test/unit/backendTimeout.test.js +161 -0
- package/test/verify-tests.js +118 -0
- package/tokenCounter.js +419 -0
- package/tokenUsageDisplay.js +70 -0
- package/tools.js +186 -0
- package/webServer.js +1110 -0
package/probeChat.js
ADDED
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
3
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
4
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
5
|
+
import { streamText } from 'ai'; // Removed 'tool' import as it's not used directly here
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { TokenCounter } from './tokenCounter.js';
|
|
8
|
+
import { TokenUsageDisplay } from './tokenUsageDisplay.js';
|
|
9
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { TelemetryConfig } from './telemetry.js';
|
|
12
|
+
import { trace } from '@opentelemetry/api';
|
|
13
|
+
import { appTracer } from './appTracer.js';
|
|
14
|
+
// Import the tools that emit events and the listFilesByLevel utility
|
|
15
|
+
import { listFilesByLevel } from '@probelabs/probe';
|
|
16
|
+
// Import schemas and parser from common (assuming tools.js)
|
|
17
|
+
import {
|
|
18
|
+
searchSchema, querySchema, extractSchema, attemptCompletionSchema,
|
|
19
|
+
searchToolDefinition, queryToolDefinition, extractToolDefinition, attemptCompletionToolDefinition, implementToolDefinition,
|
|
20
|
+
listFilesToolDefinition, searchFilesToolDefinition,
|
|
21
|
+
parseXmlToolCallWithThinking
|
|
22
|
+
} from './tools.js'; // Assuming common.js is moved to tools/
|
|
23
|
+
// Import tool *instances* for execution
|
|
24
|
+
import { searchToolInstance, queryToolInstance, extractToolInstance, implementToolInstance, listFilesToolInstance, searchFilesToolInstance } from './probeTool.js'; // Added new tool instances
|
|
25
|
+
|
|
26
|
+
// Maximum number of messages to keep in history
|
|
27
|
+
const MAX_HISTORY_MESSAGES = 100;
|
|
28
|
+
// Maximum iterations for the tool loop - configurable via MAX_TOOL_ITERATIONS env var
|
|
29
|
+
const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
|
|
30
|
+
|
|
31
|
+
// Parse and validate allowed folders from environment variable
|
|
32
|
+
const allowedFolders = process.env.ALLOWED_FOLDERS
|
|
33
|
+
? process.env.ALLOWED_FOLDERS.split(',').map(folder => folder.trim()).filter(Boolean)
|
|
34
|
+
: [];
|
|
35
|
+
|
|
36
|
+
// Validate folders exist on startup - will be handled by index.js in non-interactive mode
|
|
37
|
+
// This is kept for backward compatibility with direct ProbeChat usage
|
|
38
|
+
const validateFolders = () => {
|
|
39
|
+
if (allowedFolders.length > 0) {
|
|
40
|
+
for (const folder of allowedFolders) {
|
|
41
|
+
const exists = existsSync(folder);
|
|
42
|
+
// Only log if not in non-interactive mode or if in debug mode
|
|
43
|
+
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
|
|
44
|
+
console.log(`- ${folder} ${exists ? '✓' : '✗ (not found)'}`);
|
|
45
|
+
if (!exists) {
|
|
46
|
+
console.warn(`Warning: Folder "${folder}" does not exist or is not accessible`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Only log if not in non-interactive mode or if in debug mode
|
|
52
|
+
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
|
|
53
|
+
console.warn('No folders configured via ALLOWED_FOLDERS. Tools might default to current directory or require explicit paths.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Only validate folders on startup if not in non-interactive mode
|
|
59
|
+
if (typeof process !== 'undefined' && !process.env.PROBE_CHAT_SKIP_FOLDER_VALIDATION) {
|
|
60
|
+
validateFolders();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract image URLs from message text
|
|
66
|
+
* @param {string} message - The message text to analyze
|
|
67
|
+
* @param {boolean} debug - Whether to log debug information
|
|
68
|
+
* @returns {Array} Array of { url: string, cleanedMessage: string }
|
|
69
|
+
*/
|
|
70
|
+
function extractImageUrls(message, debug = false) {
|
|
71
|
+
// This function should be called within the session context, so it will inherit the trace ID
|
|
72
|
+
const tracer = trace.getTracer('probe-chat', '1.0.0');
|
|
73
|
+
return tracer.startActiveSpan('content.image.extract', (span) => {
|
|
74
|
+
try {
|
|
75
|
+
// Pattern to match image URLs and base64 data:
|
|
76
|
+
// 1. GitHub private-user-images URLs (always images, regardless of extension)
|
|
77
|
+
// 2. GitHub user-attachments/assets URLs (always images, regardless of extension)
|
|
78
|
+
// 3. URLs with common image extensions (PNG, JPG, JPEG, WebP, GIF)
|
|
79
|
+
// 4. Base64 data URLs (data:image/...)
|
|
80
|
+
// Updated to stop at quotes, spaces, or common HTML/XML delimiters
|
|
81
|
+
const imageUrlPattern = /(?:data:image\/[a-zA-Z]*;base64,[A-Za-z0-9+/=]+|https?:\/\/(?:(?:private-user-images\.githubusercontent\.com|github\.com\/user-attachments\/assets)\/[^\s"'<>]+|[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)(?:\?[^\s"'<>]*)?))/gi;
|
|
82
|
+
|
|
83
|
+
span.setAttributes({
|
|
84
|
+
'message.length': message.length,
|
|
85
|
+
'debug.enabled': debug
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (debug) {
|
|
89
|
+
console.log(`[DEBUG] Scanning message for image URLs. Message length: ${message.length}`);
|
|
90
|
+
console.log(`[DEBUG] Image URL pattern: ${imageUrlPattern.toString()}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const urls = [];
|
|
94
|
+
let match;
|
|
95
|
+
|
|
96
|
+
while ((match = imageUrlPattern.exec(message)) !== null) {
|
|
97
|
+
urls.push(match[0]);
|
|
98
|
+
if (debug) {
|
|
99
|
+
console.log(`[DEBUG] Found image URL: ${match[0]}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Remove image URLs from message text
|
|
104
|
+
const cleanedMessage = message.replace(imageUrlPattern, '').trim();
|
|
105
|
+
|
|
106
|
+
span.setAttributes({
|
|
107
|
+
'images.found': urls.length,
|
|
108
|
+
'message.cleaned_length': cleanedMessage.length
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (debug) {
|
|
112
|
+
console.log(`[DEBUG] Total image URLs found: ${urls.length}`);
|
|
113
|
+
if (urls.length > 0) {
|
|
114
|
+
console.log(`[DEBUG] Original message length: ${message.length}, cleaned message length: ${cleanedMessage.length}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = {
|
|
119
|
+
imageUrls: urls,
|
|
120
|
+
cleanedMessage: cleanedMessage
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
span.setStatus({ code: 1 }); // SUCCESS
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
span.recordException(error);
|
|
127
|
+
span.setStatus({ code: 2, message: error.message }); // ERROR
|
|
128
|
+
throw error;
|
|
129
|
+
} finally {
|
|
130
|
+
span.end();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate image URLs by checking if they're accessible, handling redirects
|
|
137
|
+
* @param {string[]} imageUrls - Array of image URLs to validate
|
|
138
|
+
* @param {boolean} debug - Whether to log debug messages
|
|
139
|
+
* @returns {Promise<string[]>} Array of valid final image URLs (after redirects)
|
|
140
|
+
*/
|
|
141
|
+
async function validateImageUrls(imageUrls, debug = false) {
|
|
142
|
+
const validUrls = [];
|
|
143
|
+
|
|
144
|
+
for (const url of imageUrls) {
|
|
145
|
+
try {
|
|
146
|
+
// Check if it's a base64 data URL
|
|
147
|
+
if (url.startsWith('data:image/')) {
|
|
148
|
+
// Validate base64 data URL format
|
|
149
|
+
const dataUrlMatch = url.match(/^data:image\/([a-zA-Z]*);base64,([A-Za-z0-9+/=]+)$/);
|
|
150
|
+
if (dataUrlMatch) {
|
|
151
|
+
const [, imageType, base64Data] = dataUrlMatch;
|
|
152
|
+
|
|
153
|
+
// Basic validation of base64 data
|
|
154
|
+
if (base64Data.length > 0 && imageType) {
|
|
155
|
+
// Estimate file size from base64 (rough approximation: base64 is ~1.33x original size)
|
|
156
|
+
const estimatedSize = (base64Data.length * 3) / 4;
|
|
157
|
+
|
|
158
|
+
// Check size limit (10MB)
|
|
159
|
+
if (estimatedSize <= 10 * 1024 * 1024) {
|
|
160
|
+
validUrls.push(url);
|
|
161
|
+
if (debug) {
|
|
162
|
+
console.log(`[DEBUG] Valid base64 image: ${imageType} (~${(estimatedSize / 1024).toFixed(1)}KB)`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (debug) {
|
|
166
|
+
console.log(`[DEBUG] Base64 image too large: ~${(estimatedSize / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
if (debug) {
|
|
171
|
+
console.log(`[DEBUG] Invalid base64 data URL format: ${url.substring(0, 50)}...`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (debug) {
|
|
176
|
+
console.log(`[DEBUG] Invalid data URL format: ${url.substring(0, 50)}...`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Handle regular HTTP/HTTPS URLs
|
|
181
|
+
// Always use GET request with Range header to validate and get content type
|
|
182
|
+
// This works better than HEAD for GitHub URLs and other services
|
|
183
|
+
const response = await fetch(url, {
|
|
184
|
+
method: 'GET',
|
|
185
|
+
headers: {
|
|
186
|
+
'Range': 'bytes=0-1023' // Only fetch first 1KB to check content type and minimize data transfer
|
|
187
|
+
},
|
|
188
|
+
timeout: 10000, // TIMEOUTS.HTTP_REQUEST - 10 second timeout for GitHub URLs which can be slower
|
|
189
|
+
redirect: 'follow'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (response.ok || response.status === 206) { // 206 = Partial Content (from Range header)
|
|
193
|
+
// Check if the response has image content type
|
|
194
|
+
const contentType = response.headers.get('content-type');
|
|
195
|
+
if (contentType && contentType.startsWith('image/')) {
|
|
196
|
+
// Use the final URL after following redirects
|
|
197
|
+
const finalUrl = response.url;
|
|
198
|
+
validUrls.push(finalUrl);
|
|
199
|
+
if (debug) {
|
|
200
|
+
if (finalUrl !== url) {
|
|
201
|
+
console.log(`[DEBUG] Valid image URL after redirect: ${url} -> ${finalUrl} (${contentType})`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(`[DEBUG] Valid image URL: ${finalUrl} (${contentType})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
if (debug) {
|
|
208
|
+
console.log(`[DEBUG] URL not an image: ${url} (${contentType || 'unknown type'})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
if (debug) {
|
|
213
|
+
console.log(`[DEBUG] URL not accessible: ${url} (status: ${response.status})`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (debug) {
|
|
219
|
+
console.log(`[DEBUG] Error validating image URL ${url}: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return validUrls;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* ProbeChat class to handle chat interactions with AI models
|
|
229
|
+
*/
|
|
230
|
+
export class ProbeChat {
|
|
231
|
+
/**
|
|
232
|
+
* Create a new ProbeChat instance
|
|
233
|
+
* @param {Object} options - Configuration options
|
|
234
|
+
* @param {string} [options.sessionId] - Optional session ID
|
|
235
|
+
* @param {boolean} [options.isNonInteractive=false] - Suppress internal logs if true
|
|
236
|
+
* @param {Function} [options.toolCallCallback] - Callback function for tool calls (sessionId, toolCallData) - *Note: Callback may need adjustment for XML flow*
|
|
237
|
+
* @param {string} [options.customPrompt] - Custom prompt to replace the default system message
|
|
238
|
+
* @param {string} [options.promptType] - Predefined prompt type (architect, code-review, support)
|
|
239
|
+
* @param {boolean} [options.allowEdit=false] - Allow the use of the 'implement' tool
|
|
240
|
+
*/
|
|
241
|
+
constructor(options = {}) {
|
|
242
|
+
// Suppress internal logs if in non-interactive mode
|
|
243
|
+
this.isNonInteractive = !!options.isNonInteractive;
|
|
244
|
+
// Flag to track if a request has been cancelled
|
|
245
|
+
this.cancelled = false;
|
|
246
|
+
|
|
247
|
+
// AbortController for cancelling fetch requests
|
|
248
|
+
this.abortController = null;
|
|
249
|
+
// Make allowedFolders accessible as a property of the class
|
|
250
|
+
this.allowedFolders = allowedFolders;
|
|
251
|
+
|
|
252
|
+
// Store custom prompt or prompt type if provided
|
|
253
|
+
this.customPrompt = options.customPrompt || process.env.CUSTOM_PROMPT || null;
|
|
254
|
+
this.promptType = options.promptType || process.env.PROMPT_TYPE || null;
|
|
255
|
+
|
|
256
|
+
// Store allowEdit flag - enable if allow_edit is set or if allow_suggestions is set via environment
|
|
257
|
+
// Note: ALLOW_SUGGESTIONS also enables allowEdit because the implement tool is needed to generate
|
|
258
|
+
// code changes that reviewdog can then convert into PR review suggestions
|
|
259
|
+
this.allowEdit = !!options.allowEdit || process.env.ALLOW_EDIT === '1' || process.env.ALLOW_SUGGESTIONS === '1';
|
|
260
|
+
|
|
261
|
+
// Store client-provided API credentials if available
|
|
262
|
+
this.clientApiProvider = options.apiProvider;
|
|
263
|
+
this.clientApiKey = options.apiKey;
|
|
264
|
+
this.clientApiUrl = options.apiUrl;
|
|
265
|
+
|
|
266
|
+
// Initialize token counter and display
|
|
267
|
+
this.tokenCounter = new TokenCounter();
|
|
268
|
+
this.tokenDisplay = new TokenUsageDisplay({
|
|
269
|
+
maxTokens: 8192 // Will be updated based on model
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Use provided session ID or generate a unique one
|
|
273
|
+
this.sessionId = options.sessionId || randomUUID();
|
|
274
|
+
|
|
275
|
+
// Get debug mode
|
|
276
|
+
this.debug = process.env.DEBUG_CHAT === '1';
|
|
277
|
+
|
|
278
|
+
if (this.debug) {
|
|
279
|
+
console.log(`[DEBUG] Generated session ID for chat: ${this.sessionId}`);
|
|
280
|
+
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
281
|
+
console.log(`[DEBUG] Allow Edit (implement tool): ${this.allowEdit}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Store tool instances for execution
|
|
285
|
+
// These are the actual functions/objects that perform the actions
|
|
286
|
+
this.toolImplementations = {
|
|
287
|
+
search: searchToolInstance,
|
|
288
|
+
query: queryToolInstance,
|
|
289
|
+
extract: extractToolInstance,
|
|
290
|
+
listFiles: listFilesToolInstance,
|
|
291
|
+
searchFiles: searchFilesToolInstance,
|
|
292
|
+
// attempt_completion is handled specially in the loop, no direct implementation needed here
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Conditionally add the implement tool if allowed
|
|
296
|
+
if (this.allowEdit) {
|
|
297
|
+
this.toolImplementations.implement = implementToolInstance;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initialize the chat model
|
|
301
|
+
this.initializeModel();
|
|
302
|
+
|
|
303
|
+
// Initialize telemetry
|
|
304
|
+
this.initializeTelemetry();
|
|
305
|
+
|
|
306
|
+
// Initialize chat history
|
|
307
|
+
this.history = [];
|
|
308
|
+
|
|
309
|
+
// Initialize display history - tracks what users actually see
|
|
310
|
+
this.displayHistory = [];
|
|
311
|
+
|
|
312
|
+
// Store persistent storage instance if provided
|
|
313
|
+
this.storage = options.storage || null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Initialize the AI model based on available API keys and forced provider setting
|
|
318
|
+
*/
|
|
319
|
+
initializeModel() {
|
|
320
|
+
// Get API keys from environment variables or client-provided values
|
|
321
|
+
const anthropicApiKey = this.clientApiKey && this.clientApiProvider === 'anthropic' ?
|
|
322
|
+
this.clientApiKey : process.env.ANTHROPIC_API_KEY;
|
|
323
|
+
const openaiApiKey = this.clientApiKey && this.clientApiProvider === 'openai' ?
|
|
324
|
+
this.clientApiKey : process.env.OPENAI_API_KEY;
|
|
325
|
+
const googleApiKey = this.clientApiKey && this.clientApiProvider === 'google' ?
|
|
326
|
+
this.clientApiKey : process.env.GOOGLE_API_KEY;
|
|
327
|
+
|
|
328
|
+
// Get custom API URLs if provided (client URL takes precedence over environment variables)
|
|
329
|
+
const llmBaseUrl = process.env.LLM_BASE_URL; // Generic base URL for all providers
|
|
330
|
+
|
|
331
|
+
// For each provider, use client URL if available and matches the provider
|
|
332
|
+
const anthropicApiUrl = (this.clientApiUrl && this.clientApiProvider === 'anthropic') ?
|
|
333
|
+
this.clientApiUrl : (process.env.ANTHROPIC_API_URL || llmBaseUrl);
|
|
334
|
+
|
|
335
|
+
const openaiApiUrl = (this.clientApiUrl && this.clientApiProvider === 'openai') ?
|
|
336
|
+
this.clientApiUrl : (process.env.OPENAI_API_URL || llmBaseUrl);
|
|
337
|
+
|
|
338
|
+
const googleApiUrl = (this.clientApiUrl && this.clientApiProvider === 'google') ?
|
|
339
|
+
this.clientApiUrl : (process.env.GOOGLE_API_URL || llmBaseUrl);
|
|
340
|
+
|
|
341
|
+
// Get model override if provided
|
|
342
|
+
const modelName = process.env.MODEL_NAME;
|
|
343
|
+
|
|
344
|
+
// Check if client has specified a provider that should be forced
|
|
345
|
+
const clientForceProvider = this.clientApiProvider && this.clientApiKey ? this.clientApiProvider : null;
|
|
346
|
+
|
|
347
|
+
// Use client-forced provider or environment variable
|
|
348
|
+
const forceProvider = clientForceProvider || (process.env.FORCE_PROVIDER ? process.env.FORCE_PROVIDER.toLowerCase() : null);
|
|
349
|
+
|
|
350
|
+
if (this.debug) {
|
|
351
|
+
console.log(`[DEBUG] Available API keys: Anthropic=${!!anthropicApiKey}, OpenAI=${!!openaiApiKey}, Google=${!!googleApiKey}`);
|
|
352
|
+
console.log(`[DEBUG] Force provider: ${forceProvider || '(not set)'}`);
|
|
353
|
+
if (llmBaseUrl) console.log(`[DEBUG] Generic LLM Base URL: ${llmBaseUrl}`);
|
|
354
|
+
if (process.env.ANTHROPIC_API_URL) console.log(`[DEBUG] Custom Anthropic URL: ${anthropicApiUrl}`);
|
|
355
|
+
if (process.env.OPENAI_API_URL) console.log(`[DEBUG] Custom OpenAI URL: ${openaiApiUrl}`);
|
|
356
|
+
if (process.env.GOOGLE_API_URL) console.log(`[DEBUG] Custom Google URL: ${googleApiUrl}`);
|
|
357
|
+
if (modelName) console.log(`[DEBUG] Model override: ${modelName}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check if a specific provider is forced
|
|
361
|
+
|
|
362
|
+
if (forceProvider) {
|
|
363
|
+
if (!this.isNonInteractive || this.debug) {
|
|
364
|
+
console.log(`Provider forced to: ${forceProvider}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (forceProvider === 'anthropic' && anthropicApiKey) {
|
|
368
|
+
this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
|
|
369
|
+
return;
|
|
370
|
+
} else if (forceProvider === 'openai' && openaiApiKey) {
|
|
371
|
+
this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
|
|
372
|
+
return;
|
|
373
|
+
} else if (forceProvider === 'google' && googleApiKey) {
|
|
374
|
+
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// If no provider is forced or forced provider failed, use the first available API key
|
|
382
|
+
if (anthropicApiKey) {
|
|
383
|
+
this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
|
|
384
|
+
} else if (openaiApiKey) {
|
|
385
|
+
this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
|
|
386
|
+
} else if (googleApiKey) {
|
|
387
|
+
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
388
|
+
} else {
|
|
389
|
+
console.error('FATAL: No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY environment variable.');
|
|
390
|
+
this.noApiKeysMode = true; // Use flag for potential UI handling
|
|
391
|
+
this.model = 'none';
|
|
392
|
+
this.apiType = 'none';
|
|
393
|
+
console.log('ProbeChat cannot function without an API key.');
|
|
394
|
+
// Consider throwing an error here in a real application to prevent execution
|
|
395
|
+
// throw new Error('No API key configured for AI provider.');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Initialize Anthropic model
|
|
401
|
+
* @param {string} apiKey - Anthropic API key
|
|
402
|
+
* @param {string} [apiUrl] - Optional Anthropic API URL override
|
|
403
|
+
* @param {string} [modelName] - Optional model name override
|
|
404
|
+
*/
|
|
405
|
+
initializeAnthropicModel(apiKey, apiUrl, modelName) {
|
|
406
|
+
this.provider = createAnthropic({
|
|
407
|
+
apiKey: apiKey,
|
|
408
|
+
...(apiUrl && { baseURL: apiUrl }), // Conditionally add baseURL
|
|
409
|
+
});
|
|
410
|
+
this.model = modelName || 'claude-3-7-sonnet-20250219';
|
|
411
|
+
this.apiType = 'anthropic';
|
|
412
|
+
if (!this.isNonInteractive || this.debug) {
|
|
413
|
+
const urlSource = process.env.ANTHROPIC_API_URL ? 'ANTHROPIC_API_URL' :
|
|
414
|
+
(process.env.LLM_BASE_URL ? 'LLM_BASE_URL' : 'default');
|
|
415
|
+
console.log(`Using Anthropic API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl}, from: ${urlSource})` : ''}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Initialize OpenAI model
|
|
421
|
+
* @param {string} apiKey - OpenAI API key
|
|
422
|
+
* @param {string} [apiUrl] - Optional OpenAI API URL override
|
|
423
|
+
* @param {string} [modelName] - Optional model name override
|
|
424
|
+
*/
|
|
425
|
+
initializeOpenAIModel(apiKey, apiUrl, modelName) {
|
|
426
|
+
this.provider = createOpenAI({
|
|
427
|
+
compatibility: 'strict',
|
|
428
|
+
apiKey: apiKey,
|
|
429
|
+
...(apiUrl && { baseURL: apiUrl }), // Conditionally add baseURL
|
|
430
|
+
});
|
|
431
|
+
this.model = modelName || 'gpt-4o';
|
|
432
|
+
this.apiType = 'openai';
|
|
433
|
+
if (!this.isNonInteractive || this.debug) {
|
|
434
|
+
const urlSource = process.env.OPENAI_API_URL ? 'OPENAI_API_URL' :
|
|
435
|
+
(process.env.LLM_BASE_URL ? 'LLM_BASE_URL' : 'default');
|
|
436
|
+
console.log(`Using OpenAI API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl}, from: ${urlSource})` : ''}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Initialize Google model
|
|
442
|
+
* @param {string} apiKey - Google API key
|
|
443
|
+
* @param {string} [apiUrl] - Optional Google API URL override
|
|
444
|
+
* @param {string} [modelName] - Optional model name override
|
|
445
|
+
*/
|
|
446
|
+
initializeGoogleModel(apiKey, apiUrl, modelName) {
|
|
447
|
+
this.provider = createGoogleGenerativeAI({
|
|
448
|
+
apiKey: apiKey,
|
|
449
|
+
...(apiUrl && { baseURL: apiUrl }), // Conditionally add baseURL
|
|
450
|
+
});
|
|
451
|
+
this.model = modelName || 'gemini-2.0-flash';
|
|
452
|
+
this.apiType = 'google';
|
|
453
|
+
if (!this.isNonInteractive || this.debug) {
|
|
454
|
+
const urlSource = process.env.GOOGLE_API_URL ? 'GOOGLE_API_URL' :
|
|
455
|
+
(process.env.LLM_BASE_URL ? 'LLM_BASE_URL' : 'default');
|
|
456
|
+
console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl}, from: ${urlSource})` : ''}`);
|
|
457
|
+
}
|
|
458
|
+
// Note: Google's tool support might differ. Ensure XML approach works reliably.
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Initialize telemetry configuration
|
|
463
|
+
*/
|
|
464
|
+
initializeTelemetry() {
|
|
465
|
+
try {
|
|
466
|
+
// Check if telemetry is enabled via environment variables
|
|
467
|
+
const fileEnabled = process.env.OTEL_ENABLE_FILE === 'true';
|
|
468
|
+
const remoteEnabled = process.env.OTEL_ENABLE_REMOTE === 'true';
|
|
469
|
+
const consoleEnabled = process.env.OTEL_ENABLE_CONSOLE === 'true';
|
|
470
|
+
|
|
471
|
+
if (fileEnabled || remoteEnabled || consoleEnabled) {
|
|
472
|
+
this.telemetryConfig = new TelemetryConfig({
|
|
473
|
+
enableFile: fileEnabled,
|
|
474
|
+
enableRemote: remoteEnabled,
|
|
475
|
+
enableConsole: consoleEnabled,
|
|
476
|
+
filePath: process.env.OTEL_FILE_PATH || './traces.jsonl',
|
|
477
|
+
remoteEndpoint: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || 'http://localhost:4318/v1/traces'
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
this.telemetryConfig.initialize();
|
|
481
|
+
|
|
482
|
+
if (this.debug) {
|
|
483
|
+
console.log('[DEBUG] Telemetry initialized successfully');
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
if (this.debug) {
|
|
487
|
+
console.log('[DEBUG] Telemetry disabled - no exporters configured');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error('Failed to initialize telemetry:', error.message);
|
|
492
|
+
this.telemetryConfig = null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get the system message with instructions for the AI (XML Tool Format)
|
|
498
|
+
* @returns {Promise<string>} - The system message
|
|
499
|
+
*/
|
|
500
|
+
async getSystemMessage() {
|
|
501
|
+
// --- Dynamically build Tool Definitions ---
|
|
502
|
+
let toolDefinitions = `
|
|
503
|
+
${searchToolDefinition}
|
|
504
|
+
${queryToolDefinition}
|
|
505
|
+
${extractToolDefinition}
|
|
506
|
+
${listFilesToolDefinition}
|
|
507
|
+
${searchFilesToolDefinition}
|
|
508
|
+
${attemptCompletionToolDefinition}
|
|
509
|
+
`;
|
|
510
|
+
if (this.allowEdit) {
|
|
511
|
+
toolDefinitions += `${implementToolDefinition}\n`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- Dynamically build Tool Guidelines ---
|
|
515
|
+
let xmlToolGuidelines = `
|
|
516
|
+
# Tool Use Formatting
|
|
517
|
+
|
|
518
|
+
Tool use MUST be formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. You MUST use exactly ONE tool call per message until you are ready to complete the task.
|
|
519
|
+
|
|
520
|
+
Structure:
|
|
521
|
+
<tool_name>
|
|
522
|
+
<parameter1_name>value1</parameter1_name>
|
|
523
|
+
<parameter2_name>value2</parameter2_name>
|
|
524
|
+
...
|
|
525
|
+
</tool_name>
|
|
526
|
+
|
|
527
|
+
Example:
|
|
528
|
+
<search>
|
|
529
|
+
<query>error handling</query>
|
|
530
|
+
<path>src/search</path>
|
|
531
|
+
</search>
|
|
532
|
+
|
|
533
|
+
# Thinking Process
|
|
534
|
+
|
|
535
|
+
Before using a tool, analyze the situation within <thinking></thinking> tags. This helps you organize your thoughts and make better decisions. Your thinking process should include:
|
|
536
|
+
|
|
537
|
+
1. Analyze what information you already have and what information you need to proceed with the task.
|
|
538
|
+
2. Determine which of the available tools would be most effective for gathering this information or accomplishing the current step.
|
|
539
|
+
3. Check if all required parameters for the tool are available or can be inferred from the context.
|
|
540
|
+
4. If all parameters are available, proceed with the tool use.
|
|
541
|
+
5. If parameters are missing, explain what's missing and why it's needed.
|
|
542
|
+
|
|
543
|
+
Example:
|
|
544
|
+
<thinking>
|
|
545
|
+
I need to find code related to error handling in the search module. The most appropriate tool for this is the search tool, which requires a query parameter and a path parameter. I have both the query ("error handling") and the path ("src/search"), so I can proceed with the search.
|
|
546
|
+
</thinking>
|
|
547
|
+
|
|
548
|
+
# Tool Use Guidelines
|
|
549
|
+
|
|
550
|
+
1. Think step-by-step about how to achieve the user's goal.
|
|
551
|
+
2. Use <thinking></thinking> tags to analyze the situation and determine the appropriate tool.
|
|
552
|
+
3. Choose **one** tool that helps achieve the current step.
|
|
553
|
+
4. Format the tool call using the specified XML format. Ensure all required parameters are included.
|
|
554
|
+
5. **You MUST respond with exactly one tool call in the specified XML format in each turn.**
|
|
555
|
+
6. Wait for the tool execution result, which will be provided in the next message (within a <tool_result> block).
|
|
556
|
+
7. Analyze the tool result and decide the next step. If more tool calls are needed, repeat steps 2-6.
|
|
557
|
+
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.
|
|
558
|
+
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.
|
|
559
|
+
10. Do not be lazy and dig to the topic as deep as possible, until you see full picture.
|
|
560
|
+
|
|
561
|
+
Available Tools:
|
|
562
|
+
- search: Search code using keyword queries.
|
|
563
|
+
- query: Search code using structural AST patterns.
|
|
564
|
+
- extract: Extract specific code blocks or lines from files.
|
|
565
|
+
- listFiles: List files and directories in a specified location.
|
|
566
|
+
- searchFiles: Find files matching a glob pattern with recursive search capability.
|
|
567
|
+
${this.allowEdit ? '- implement: Implement a feature or fix a bug using aider.\n' : ''}
|
|
568
|
+
- attempt_completion: Finalize the task and provide the result to the user.
|
|
569
|
+
`;
|
|
570
|
+
// Common instructions that will be added to all prompts
|
|
571
|
+
const commonInstructions = `<instructions>
|
|
572
|
+
Follow these instructions carefully:
|
|
573
|
+
1. Analyze the user's request.
|
|
574
|
+
2. Use <thinking></thinking> tags to analyze the situation and determine the appropriate tool for each step.
|
|
575
|
+
3. Use the available tools step-by-step to fulfill the request.
|
|
576
|
+
4. You should always prefer the \`search\` tool for code-related questions. Read full files only if really necessary.
|
|
577
|
+
4. Ensure to get really deep and understand the full picture before answering. Ensure to check dependencies where required.
|
|
578
|
+
5. You MUST respond with exactly ONE tool call per message, using the specified XML format, until the task is complete.
|
|
579
|
+
6. Wait for the tool execution result (provided in the next user message in a <tool_result> block) before proceeding to the next step.
|
|
580
|
+
7. Once the task is fully completed, and you have confirmed the success of all steps, use the '<attempt_completion>' tool to provide the final result. This is the ONLY way to signal completion.
|
|
581
|
+
8. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results. Avoid reading files in full, only when absolutely necessary.
|
|
582
|
+
9. Show mermaid diagrams to illustrate complex code structures or workflows. In diagrams, content inside ["..."] always should be in quotes.</instructions>
|
|
583
|
+
`;
|
|
584
|
+
|
|
585
|
+
// Define predefined prompts (without the common instructions)
|
|
586
|
+
const predefinedPrompts = {
|
|
587
|
+
'code-explorer': `You are ProbeChat Code Explorer, a specialized AI assistant focused on helping developers, product managers, and QAs understand and navigate codebases. Your primary function is to answer questions based on code, explain how systems work, and provide insights into code functionality using the provided code analysis tools.
|
|
588
|
+
|
|
589
|
+
When exploring code:
|
|
590
|
+
- Provide clear, concise explanations based on user request
|
|
591
|
+
- Find and highlight the most relevant code snippets, if required
|
|
592
|
+
- Trace function calls and data flow through the system
|
|
593
|
+
- Use diagrams to illustrate code structure and relationships when helpful
|
|
594
|
+
- Try to understand the user's intent and provide relevant information
|
|
595
|
+
- Understand high level picture
|
|
596
|
+
- Balance detail with clarity in your explanations`,
|
|
597
|
+
|
|
598
|
+
'architect': `You are ProbeChat Architect, a specialized AI assistant focused on software architecture and design. Your primary function is to help users understand, analyze, and design software systems using the provided code analysis tools. You excel at identifying architectural patterns, suggesting improvements, and creating high-level design documentation. You provide detailed and accurate responses to user queries about system architecture, component relationships, and code organization.
|
|
599
|
+
|
|
600
|
+
When analyzing code:
|
|
601
|
+
- Focus on high-level design patterns and system organization
|
|
602
|
+
- Identify architectural patterns and component relationships
|
|
603
|
+
- Evaluate system structure and suggest architectural improvements
|
|
604
|
+
- Create diagrams to illustrate system architecture and workflows
|
|
605
|
+
- Consider scalability, maintainability, and extensibility in your analysis`,
|
|
606
|
+
|
|
607
|
+
'code-review': `You are ProbeChat Code Reviewer, a specialized AI assistant focused on code quality and best practices. Your primary function is to help users identify issues, suggest improvements, and ensure code follows best practices using the provided code analysis tools. You excel at spotting bugs, performance issues, security vulnerabilities, and style inconsistencies. You provide detailed and constructive feedback on code quality.
|
|
608
|
+
|
|
609
|
+
When reviewing code:
|
|
610
|
+
- Look for bugs, edge cases, and potential issues
|
|
611
|
+
- Identify performance bottlenecks and optimization opportunities
|
|
612
|
+
- Check for security vulnerabilities and best practices
|
|
613
|
+
- Evaluate code style and consistency
|
|
614
|
+
- Is the backward compatibility can be broken?
|
|
615
|
+
- Organize feedback by severity (critical, major, minor) and type (bug, performance, security, style)
|
|
616
|
+
- Provide specific, actionable suggestions with code examples where appropriate
|
|
617
|
+
|
|
618
|
+
## Failure Detection
|
|
619
|
+
|
|
620
|
+
If you detect critical issues that should prevent the code from being merged, include <fail> in your response:
|
|
621
|
+
- Security vulnerabilities that could be exploited
|
|
622
|
+
- Breaking changes without proper documentation or migration path
|
|
623
|
+
- Critical bugs that would cause system failures
|
|
624
|
+
- Severe violations of project standards that must be addressed
|
|
625
|
+
|
|
626
|
+
The <fail> tag will cause the GitHub check to fail, drawing immediate attention to these critical issues.`,
|
|
627
|
+
|
|
628
|
+
'engineer': `You are senior engineer focused on software architecture and design.
|
|
629
|
+
Before jumping on the task you first, in details analyse user request, and try to provide elegant and concise solution.
|
|
630
|
+
If solution is clear, you can jump to implementation right away, if not, you can ask user a clarification question, by calling attempt_completion tool, with required details.
|
|
631
|
+
You are allowed to use search tool with allow_tests argument, in order to find the tests.
|
|
632
|
+
|
|
633
|
+
Before jumping to implementation:
|
|
634
|
+
- Focus on high-level design patterns and system organization
|
|
635
|
+
- Identify architectural patterns and component relationships
|
|
636
|
+
- Evaluate system structure and suggest architectural improvements
|
|
637
|
+
- Focus on backward compatibility.
|
|
638
|
+
- Respond with diagrams to illustrate system architecture and workflows, if required.
|
|
639
|
+
- Consider scalability, maintainability, and extensibility in your analysis
|
|
640
|
+
|
|
641
|
+
During the implementation:
|
|
642
|
+
- Avoid implementing special cases
|
|
643
|
+
- Do not forget to add the tests`,
|
|
644
|
+
|
|
645
|
+
'support': `You are ProbeChat Support, a specialized AI assistant focused on helping developers troubleshoot issues and solve problems. Your primary function is to help users diagnose errors, understand unexpected behaviors, and find solutions using the provided code analysis tools. You excel at debugging, explaining complex concepts, and providing step-by-step guidance. You provide detailed and patient support to help users overcome technical challenges.
|
|
646
|
+
|
|
647
|
+
When troubleshooting:
|
|
648
|
+
- Focus on finding root causes, not just symptoms
|
|
649
|
+
- Explain concepts clearly with appropriate context
|
|
650
|
+
- Provide step-by-step guidance to solve problems
|
|
651
|
+
- Suggest diagnostic steps to verify solutions
|
|
652
|
+
- Consider edge cases and potential complications
|
|
653
|
+
- Be empathetic and patient in your explanations`
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
let systemMessage = '';
|
|
657
|
+
|
|
658
|
+
// Use custom prompt if provided
|
|
659
|
+
if (this.customPrompt) {
|
|
660
|
+
// For custom prompts, use the entire content as is
|
|
661
|
+
systemMessage = "<role>" + this.customPrompt + "</role>";
|
|
662
|
+
if (this.debug) {
|
|
663
|
+
console.log(`[DEBUG] Using custom prompt`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Use predefined prompt if specified
|
|
667
|
+
else if (this.promptType && predefinedPrompts[this.promptType]) {
|
|
668
|
+
systemMessage = "<role>" + predefinedPrompts[this.promptType] + "</role>";
|
|
669
|
+
if (this.debug) {
|
|
670
|
+
console.log(`[DEBUG] Using predefined prompt: ${this.promptType}`);
|
|
671
|
+
}
|
|
672
|
+
// Add common instructions to predefined prompts
|
|
673
|
+
systemMessage += commonInstructions;
|
|
674
|
+
} else {
|
|
675
|
+
// Use the default prompt (code explorer) if no prompt type is specified
|
|
676
|
+
systemMessage = "<role>" + predefinedPrompts['code-explorer'] + "</role>";
|
|
677
|
+
if (this.debug) {
|
|
678
|
+
console.log(`[DEBUG] Using default prompt: code explorer`);
|
|
679
|
+
}
|
|
680
|
+
// Add common instructions to the default prompt
|
|
681
|
+
systemMessage += commonInstructions;
|
|
682
|
+
}
|
|
683
|
+
// Add XML Tool Guidelines
|
|
684
|
+
systemMessage += `\n${xmlToolGuidelines}\n`;
|
|
685
|
+
|
|
686
|
+
// Add Tool Definitions
|
|
687
|
+
systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
|
|
688
|
+
|
|
689
|
+
// Add special emphasis for image handling
|
|
690
|
+
systemMessage += `\n# CRITICAL: XML Tool Format Required\n\nEven when processing images or visual content, you MUST respond using the XML tool format. Do not provide direct answers about images - instead use the appropriate tool (usually <attempt_completion>) with your analysis inside the <result> tag.\n\nExample when analyzing an image:\n<attempt_completion>\n<result>\nI can see this is a promotional image from Tyk showing... [your analysis here]\n</result>\n</attempt_completion>\n`;
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
const searchDirectory = this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd();
|
|
694
|
+
if (this.debug) {
|
|
695
|
+
console.log(`[DEBUG] Generating file list for base directory: ${searchDirectory}...`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Add folder information
|
|
699
|
+
if (this.allowedFolders.length > 0) {
|
|
700
|
+
const folderList = this.allowedFolders.map(f => `"${f}"`).join(', ');
|
|
701
|
+
systemMessage += `\n\nYou are configured to primarily operate within these folders: ${folderList}. When using tools like 'search' or 'query', the 'path' parameter should generally refer to these folders or subpaths within them. The root for relative paths is considered the project base.`;
|
|
702
|
+
} else {
|
|
703
|
+
systemMessage += `\n\nCurrent path: ${searchDirectory}. When using tools, specify paths like '.' for the current directory, 'src/utils', etc., within the 'path' parameter. Dependencies are located in /dep folder: "/dep/go/github.com/user/repo", "/dep/js/<package>", "/dep/rust/crate_name".`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Add Rules/Capabilities section
|
|
707
|
+
systemMessage += `\n\n# Capabilities & Rules\n- Search given folder using keywords (\`search\`) or structural patterns (\`query\`).\n- Extract specific code blocks or full files using (\`extract\`).\n- File paths are relative to the project base unless using dependency syntax.\n- Always wait for tool results (\`<tool_result>...\`) before proceeding.\n- Use \`attempt_completion\` ONLY when the entire task is finished.\n- Be direct and technical. Use exactly ONE tool call per response in the specified XML format. Prefer using search tool.\n`;
|
|
708
|
+
|
|
709
|
+
if (this.debug) {
|
|
710
|
+
console.log(`[DEBUG] Base system message length (pre-file list): ${systemMessage.length}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Add file list information if available
|
|
714
|
+
try {
|
|
715
|
+
let files = await listFilesByLevel({
|
|
716
|
+
directory: searchDirectory, // Use the determined search directory
|
|
717
|
+
maxFiles: 100, // Keep it reasonable
|
|
718
|
+
respectGitignore: true
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Exclude debug file(s) and common large directories
|
|
722
|
+
files = files.filter((file) => {
|
|
723
|
+
const lower = file.toLowerCase();
|
|
724
|
+
return !lower.includes('probe-debug.txt') && !lower.includes('node_modules') && !lower.includes('/.git/');
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
if (files.length > 0) {
|
|
728
|
+
const fileListHeader = `\n\n# Project Files (Sample of up to ${files.length} files in ${searchDirectory}):\n`;
|
|
729
|
+
const fileListContent = files.map(file => `- ${file}`).join('\n');
|
|
730
|
+
systemMessage += fileListHeader + fileListContent;
|
|
731
|
+
if (this.debug) {
|
|
732
|
+
console.log(`[DEBUG] Added ${files.length} files to system message. Total length: ${systemMessage.length}`);
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
if (this.debug) {
|
|
736
|
+
console.log(`[DEBUG] No files found or listed for the project directory: ${searchDirectory}.`);
|
|
737
|
+
}
|
|
738
|
+
systemMessage += `\n\n# Project Files\nNo files listed for the primary directory (${searchDirectory}). You may need to use tools like 'search' or 'query' with broad paths initially if the user's request requires file exploration.`;
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.warn(`Warning: Could not generate file list for directory "${searchDirectory}": ${error.message}`);
|
|
742
|
+
systemMessage += `\n\n# Project Files\nCould not retrieve file listing. Proceed based on user instructions and tool capabilities.`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (this.debug) {
|
|
746
|
+
console.log(`[DEBUG] Final system message length: ${systemMessage.length}`);
|
|
747
|
+
// Log first/last parts for verification
|
|
748
|
+
const debugFilePath = join(process.cwd(), 'probe-debug-system-prompt.txt');
|
|
749
|
+
try {
|
|
750
|
+
writeFileSync(debugFilePath, systemMessage);
|
|
751
|
+
console.log(`[DEBUG] Full system prompt saved to ${debugFilePath}`);
|
|
752
|
+
} catch (e) {
|
|
753
|
+
console.error(`[DEBUG] Failed to write full system prompt: ${e.message}`);
|
|
754
|
+
console.log(`[DEBUG] System message START:\n${systemMessage.substring(0, 300)}...`);
|
|
755
|
+
console.log(`[DEBUG] System message END:\n...${systemMessage.substring(systemMessage.length - 300)}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return systemMessage;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Abort the current chat request
|
|
764
|
+
*/
|
|
765
|
+
abort() {
|
|
766
|
+
if (!this.isNonInteractive || this.debug) {
|
|
767
|
+
console.log(`Aborting chat for session: ${this.sessionId}`);
|
|
768
|
+
}
|
|
769
|
+
this.cancelled = true;
|
|
770
|
+
|
|
771
|
+
// Abort any fetch requests
|
|
772
|
+
if (this.abortController) {
|
|
773
|
+
try {
|
|
774
|
+
this.abortController.abort('User cancelled request'); // Pass reason
|
|
775
|
+
} catch (error) {
|
|
776
|
+
// Ignore errors if already aborted or controller is in an unexpected state
|
|
777
|
+
if (error.name !== 'AbortError') {
|
|
778
|
+
console.error('Error aborting fetch request:', error);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Process a user message and get a response
|
|
786
|
+
* @param {string} message - The user message
|
|
787
|
+
* @param {string} [sessionId] - Optional session ID to use for this chat (overrides the default)
|
|
788
|
+
* @param {Object} [apiCredentials] - Optional API credentials for this call
|
|
789
|
+
* @param {string[]} [images] - Optional array of base64 image URLs
|
|
790
|
+
* @returns {Promise<string>} - The AI response
|
|
791
|
+
*/
|
|
792
|
+
async chat(message, sessionId, apiCredentials = null, images = []) {
|
|
793
|
+
// Use our custom app tracer for granular tracing
|
|
794
|
+
const effectiveSessionId = sessionId || this.sessionId;
|
|
795
|
+
|
|
796
|
+
// Start the chat session span first, then execute the entire chat flow within the session context
|
|
797
|
+
const chatSessionSpan = appTracer.startChatSession(effectiveSessionId, message, this.apiType, this.model);
|
|
798
|
+
|
|
799
|
+
// Execute the entire chat flow within the session context
|
|
800
|
+
return await appTracer.withSessionContext(effectiveSessionId, async () => {
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
|
|
804
|
+
// Update client credentials if provided in this call
|
|
805
|
+
if (apiCredentials) {
|
|
806
|
+
this.clientApiProvider = apiCredentials.apiProvider || this.clientApiProvider;
|
|
807
|
+
this.clientApiKey = apiCredentials.apiKey || this.clientApiKey;
|
|
808
|
+
this.clientApiUrl = apiCredentials.apiUrl || this.clientApiUrl;
|
|
809
|
+
|
|
810
|
+
// Re-initialize the model with the new credentials
|
|
811
|
+
if (apiCredentials.apiKey && apiCredentials.apiProvider) {
|
|
812
|
+
this.initializeModel();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Handle no API keys mode gracefully
|
|
817
|
+
if (this.noApiKeysMode) {
|
|
818
|
+
console.error("Cannot process chat: No API keys configured.");
|
|
819
|
+
appTracer.endChatSession(effectiveSessionId, false, 0);
|
|
820
|
+
// Return structured response even for API key errors
|
|
821
|
+
return {
|
|
822
|
+
response: "Error: ProbeChat is not configured with an AI provider API key. Please set the appropriate environment variable (e.g., ANTHROPIC_API_KEY, OPENAI_API_KEY) or provide an API key in the browser.",
|
|
823
|
+
tokenUsage: { contextWindow: 0, current: {}, total: {} }
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Reset cancelled flag for the new request
|
|
828
|
+
this.cancelled = false;
|
|
829
|
+
|
|
830
|
+
// Create a new AbortController for this specific request
|
|
831
|
+
// This ensures previous cancellations don't affect new requests
|
|
832
|
+
this.abortController = new AbortController();
|
|
833
|
+
|
|
834
|
+
// If a session ID is provided and it's different from the current one, update it
|
|
835
|
+
if (sessionId && sessionId !== this.sessionId) {
|
|
836
|
+
if (this.debug) {
|
|
837
|
+
console.log(`[DEBUG] Switching session ID from ${this.sessionId} to ${sessionId}`);
|
|
838
|
+
}
|
|
839
|
+
// Update the session ID for this instance
|
|
840
|
+
this.sessionId = sessionId;
|
|
841
|
+
// NOTE: History is NOT cleared automatically when session ID changes this way.
|
|
842
|
+
// Call clearHistory() explicitly if a new session should start fresh.
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Process the message using the potentially updated session ID
|
|
846
|
+
const result = await this._processChat(message, effectiveSessionId, images);
|
|
847
|
+
|
|
848
|
+
appTracer.endChatSession(effectiveSessionId, true, result.tokenUsage?.total?.total || 0);
|
|
849
|
+
|
|
850
|
+
// CRITICAL FIX: Ensure all spans are properly exported before returning
|
|
851
|
+
if (this.telemetryConfig) {
|
|
852
|
+
try {
|
|
853
|
+
// First, ensure the session span is ended within its context
|
|
854
|
+
await appTracer.withSessionContext(effectiveSessionId, async () => {
|
|
855
|
+
// Small delay to ensure all child spans are ended
|
|
856
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Give BatchSpanProcessor time to process the ended spans
|
|
860
|
+
// BatchSpanProcessor has a scheduledDelayMillis of 500ms (reduced from default 5000ms)
|
|
861
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
862
|
+
|
|
863
|
+
// Force flush all pending spans
|
|
864
|
+
await this.telemetryConfig.forceFlush();
|
|
865
|
+
|
|
866
|
+
// Additional delay to ensure file writes complete
|
|
867
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
868
|
+
} catch (flushError) {
|
|
869
|
+
if (this.debug) console.log('[DEBUG] Telemetry flush warning:', flushError.message);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return result;
|
|
874
|
+
} catch (error) {
|
|
875
|
+
appTracer.endChatSession(effectiveSessionId, false, 0);
|
|
876
|
+
|
|
877
|
+
// CRITICAL FIX: Ensure all spans are properly exported even on error
|
|
878
|
+
if (this.telemetryConfig) {
|
|
879
|
+
try {
|
|
880
|
+
// First, ensure the session span is ended within its context
|
|
881
|
+
await appTracer.withSessionContext(effectiveSessionId, async () => {
|
|
882
|
+
// Small delay to ensure all child spans are ended
|
|
883
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// Give BatchSpanProcessor time to process the ended spans
|
|
887
|
+
// BatchSpanProcessor has a scheduledDelayMillis of 500ms (reduced from default 5000ms)
|
|
888
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
889
|
+
|
|
890
|
+
// Force flush all pending spans
|
|
891
|
+
await this.telemetryConfig.forceFlush();
|
|
892
|
+
|
|
893
|
+
// Additional delay to ensure file writes complete
|
|
894
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
895
|
+
} catch (flushError) {
|
|
896
|
+
if (this.debug) console.log('[DEBUG] Telemetry flush warning:', flushError.message);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
}); // End withSessionContext
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Internal method to process a chat message using the XML tool loop
|
|
907
|
+
* @param {string} message - The user message
|
|
908
|
+
* @param {string} sessionId - The session ID for tracing
|
|
909
|
+
* @param {string[]} images - Array of base64 image URLs
|
|
910
|
+
* @returns {Promise<string>} - The final AI response after loop completion
|
|
911
|
+
* @private
|
|
912
|
+
*/
|
|
913
|
+
async _processChat(message, sessionId, images = []) {
|
|
914
|
+
let currentIteration = 0;
|
|
915
|
+
let completionAttempted = false;
|
|
916
|
+
let finalResult = `Error: Max tool iterations (${MAX_TOOL_ITERATIONS}) reached without completion. You can increase this limit using the MAX_TOOL_ITERATIONS environment variable or --max-iterations flag.`; // Default error
|
|
917
|
+
|
|
918
|
+
this.abortController = new AbortController();
|
|
919
|
+
const debugFilePath = join(process.cwd(), 'probe-debug.txt');
|
|
920
|
+
|
|
921
|
+
try {
|
|
922
|
+
if (this.debug) {
|
|
923
|
+
console.log(`[DEBUG] ===== Starting XML Tool Chat Loop (Session: ${this.sessionId}) =====`);
|
|
924
|
+
console.log(`[DEBUG] Received user message: ${message}`);
|
|
925
|
+
console.log(`[DEBUG] Initial history length: ${this.history.length}`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
this.tokenCounter.startNewTurn();
|
|
929
|
+
this.tokenCounter.addRequestTokens(this.tokenCounter.countTokens(message));
|
|
930
|
+
|
|
931
|
+
if (this.history.length > MAX_HISTORY_MESSAGES) {
|
|
932
|
+
const removedCount = this.history.length - MAX_HISTORY_MESSAGES;
|
|
933
|
+
this.history = this.history.slice(removedCount);
|
|
934
|
+
if (this.debug) console.log(`[DEBUG] Trimmed history to ${this.history.length} messages (removed ${removedCount}).`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const isFirstMessage = this.history.length === 0;
|
|
938
|
+
|
|
939
|
+
// Start user message processing trace
|
|
940
|
+
const messageId = `msg_${Date.now()}`;
|
|
941
|
+
appTracer.startUserMessageProcessing(sessionId, messageId, message);
|
|
942
|
+
|
|
943
|
+
// Extract image URLs from the message within the processing context
|
|
944
|
+
const { imageUrls, cleanedMessage } = appTracer.withUserProcessingContext(sessionId, () =>
|
|
945
|
+
extractImageUrls(message, this.debug)
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
// Start image processing trace if images are found
|
|
949
|
+
if (imageUrls.length > 0) {
|
|
950
|
+
appTracer.startImageProcessing(sessionId, messageId, imageUrls, cleanedMessage.length);
|
|
951
|
+
if (this.debug) console.log(`[DEBUG] Found ${imageUrls.length} image URLs in message`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Log image detection only in interactive mode or debug mode
|
|
955
|
+
if (imageUrls.length > 0) {
|
|
956
|
+
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
|
|
957
|
+
console.log(`Detected ${imageUrls.length} image URL(s) in message.`);
|
|
958
|
+
}
|
|
959
|
+
if (this.debug) {
|
|
960
|
+
console.log(`[DEBUG] Extracted image URLs:`, imageUrls);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Validate image URLs and filter out broken ones
|
|
965
|
+
let validImageUrls = [];
|
|
966
|
+
let validationResults = null;
|
|
967
|
+
|
|
968
|
+
if (imageUrls.length > 0) {
|
|
969
|
+
const validationStartTime = Date.now();
|
|
970
|
+
validImageUrls = await validateImageUrls(imageUrls, this.debug);
|
|
971
|
+
const validationEndTime = Date.now();
|
|
972
|
+
|
|
973
|
+
// Record validation results in trace
|
|
974
|
+
validationResults = {
|
|
975
|
+
totalUrls: imageUrls.length,
|
|
976
|
+
validUrls: validImageUrls.length,
|
|
977
|
+
invalidUrls: imageUrls.length - validImageUrls.length,
|
|
978
|
+
redirectedUrls: 0, // TODO: capture from validateImageUrls if needed
|
|
979
|
+
timeoutUrls: 0, // TODO: capture from validateImageUrls if needed
|
|
980
|
+
networkErrors: 0, // TODO: capture from validateImageUrls if needed
|
|
981
|
+
durationMs: validationEndTime - validationStartTime
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
appTracer.recordImageValidation(sessionId, validationResults);
|
|
985
|
+
appTracer.endImageProcessing(sessionId, validImageUrls.length > 0, validImageUrls.length);
|
|
986
|
+
} else {
|
|
987
|
+
validImageUrls = await validateImageUrls(imageUrls, this.debug);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Start the agent loop trace within user processing context
|
|
991
|
+
appTracer.withUserProcessingContext(sessionId, () => {
|
|
992
|
+
appTracer.startAgentLoop(sessionId, MAX_TOOL_ITERATIONS);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Log validation results only in interactive mode or debug mode
|
|
996
|
+
if (imageUrls.length > 0) {
|
|
997
|
+
const invalidCount = imageUrls.length - validImageUrls.length;
|
|
998
|
+
if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
|
|
999
|
+
if (validImageUrls.length > 0) {
|
|
1000
|
+
console.log(`Image validation: ${validImageUrls.length} valid, ${invalidCount} invalid/inaccessible.`);
|
|
1001
|
+
} else {
|
|
1002
|
+
console.log(`Image validation: All ${imageUrls.length} image URLs failed validation.`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (this.debug && validImageUrls.length > 0) {
|
|
1007
|
+
console.log(`[DEBUG] Valid image URLs:`, validImageUrls);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const wrappedMessage = isFirstMessage ? `<task>\n${cleanedMessage}\n</task>` : cleanedMessage;
|
|
1012
|
+
|
|
1013
|
+
// Combine extracted URL images with uploaded base64 images
|
|
1014
|
+
const allImages = [...validImageUrls, ...images];
|
|
1015
|
+
|
|
1016
|
+
// Create the user message with potential image attachments
|
|
1017
|
+
const userMessage = { role: 'user', content: wrappedMessage };
|
|
1018
|
+
|
|
1019
|
+
// Store user message in display history (always visible to users)
|
|
1020
|
+
const displayUserMessage = {
|
|
1021
|
+
role: 'user',
|
|
1022
|
+
content: message, // Store original unwrapped message
|
|
1023
|
+
visible: true,
|
|
1024
|
+
displayType: 'user',
|
|
1025
|
+
timestamp: new Date().toISOString()
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Add image attachments if any images are present
|
|
1029
|
+
if (allImages.length > 0) {
|
|
1030
|
+
userMessage.content = [
|
|
1031
|
+
{ type: 'text', text: wrappedMessage },
|
|
1032
|
+
...allImages.map(imageUrl => ({
|
|
1033
|
+
type: 'image',
|
|
1034
|
+
image: imageUrl
|
|
1035
|
+
}))
|
|
1036
|
+
];
|
|
1037
|
+
|
|
1038
|
+
// Add images to display message as well
|
|
1039
|
+
displayUserMessage.images = allImages;
|
|
1040
|
+
|
|
1041
|
+
if (this.debug) {
|
|
1042
|
+
console.log(`[DEBUG] Created message with ${allImages.length} images (${validImageUrls.length} from URLs, ${images.length} uploaded)`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Add user message to display history
|
|
1047
|
+
if (!this.displayHistory) {
|
|
1048
|
+
this.displayHistory = [];
|
|
1049
|
+
}
|
|
1050
|
+
this.displayHistory.push(displayUserMessage);
|
|
1051
|
+
|
|
1052
|
+
// Save user message to persistent storage
|
|
1053
|
+
if (this.storage) {
|
|
1054
|
+
this.storage.saveMessage(this.sessionId, {
|
|
1055
|
+
role: 'user',
|
|
1056
|
+
content: message, // Original message
|
|
1057
|
+
timestamp: Date.now(),
|
|
1058
|
+
displayType: 'user',
|
|
1059
|
+
visible: 1,
|
|
1060
|
+
images: allImages,
|
|
1061
|
+
metadata: {}
|
|
1062
|
+
}).catch(err => {
|
|
1063
|
+
console.error('Failed to save user message to persistent storage:', err);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
let currentMessages = [
|
|
1068
|
+
...this.history,
|
|
1069
|
+
userMessage
|
|
1070
|
+
];
|
|
1071
|
+
|
|
1072
|
+
const promptGenerationStart = Date.now();
|
|
1073
|
+
const systemPrompt = await this.getSystemMessage();
|
|
1074
|
+
const promptGenerationEnd = Date.now();
|
|
1075
|
+
|
|
1076
|
+
if (this.debug) {
|
|
1077
|
+
const systemTokens = this.tokenCounter.countTokens(systemPrompt);
|
|
1078
|
+
this.tokenCounter.addRequestTokens(systemTokens);
|
|
1079
|
+
console.log(`[DEBUG] System prompt estimated tokens: ${systemTokens}`);
|
|
1080
|
+
|
|
1081
|
+
// Record system prompt generation metrics
|
|
1082
|
+
appTracer.recordSystemPromptGeneration(sessionId, {
|
|
1083
|
+
baseLength: 11747, // Approximate base system message length
|
|
1084
|
+
finalLength: systemPrompt.length,
|
|
1085
|
+
filesAdded: this.history.length > 0 ? 35 : 36, // Approximate from logs
|
|
1086
|
+
generationDurationMs: promptGenerationEnd - promptGenerationStart,
|
|
1087
|
+
promptType: this.promptType || 'default',
|
|
1088
|
+
estimatedTokens: systemTokens
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
while (currentIteration < MAX_TOOL_ITERATIONS && !completionAttempted) {
|
|
1093
|
+
currentIteration++;
|
|
1094
|
+
if (this.cancelled) throw new Error('Request was cancelled by the user');
|
|
1095
|
+
|
|
1096
|
+
// Start iteration trace within agent loop context
|
|
1097
|
+
appTracer.withAgentLoopContext(sessionId, () => {
|
|
1098
|
+
appTracer.startAgentIteration(sessionId, currentIteration, currentMessages.length, this.tokenCounter.contextSize || 0);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
if (this.debug) {
|
|
1102
|
+
console.log(`\n[DEBUG] --- Tool Loop Iteration ${currentIteration}/${MAX_TOOL_ITERATIONS} ---`);
|
|
1103
|
+
console.log(`[DEBUG] Current messages count for AI call: ${currentMessages.length}`);
|
|
1104
|
+
currentMessages.slice(-3).forEach((msg, idx) => {
|
|
1105
|
+
const contentPreview = (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)).substring(0, 80).replace(/\n/g, ' ');
|
|
1106
|
+
console.log(`[DEBUG] Msg[${currentMessages.length - 3 + idx}]: ${msg.role}: ${contentPreview}...`);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1111
|
+
if (this.debug) console.log(`[DEBUG] Estimated context tokens BEFORE LLM call (Iter ${currentIteration}): ${this.tokenCounter.contextSize}`);
|
|
1112
|
+
|
|
1113
|
+
let maxResponseTokens = 4000;
|
|
1114
|
+
if (this.model.includes('claude-3-opus') || this.model.startsWith('gpt-4-')) {
|
|
1115
|
+
maxResponseTokens = 4096;
|
|
1116
|
+
} else if (this.model.includes('claude-3-5-sonnet') || this.model.startsWith('gpt-4o')) {
|
|
1117
|
+
maxResponseTokens = 8000;
|
|
1118
|
+
} else if (this.model.includes('gemini-2.5')) {
|
|
1119
|
+
maxResponseTokens = 60000;
|
|
1120
|
+
} else if (this.model.startsWith('gemini')) {
|
|
1121
|
+
maxResponseTokens = 8000;
|
|
1122
|
+
}
|
|
1123
|
+
this.tokenDisplay = new TokenUsageDisplay({ maxTokens: maxResponseTokens });
|
|
1124
|
+
|
|
1125
|
+
const userMsgIndices = currentMessages.reduce(
|
|
1126
|
+
(acc, msg, index) => (msg.role === 'user' ? [...acc, index] : acc),
|
|
1127
|
+
[]
|
|
1128
|
+
);
|
|
1129
|
+
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1;
|
|
1130
|
+
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1;
|
|
1131
|
+
|
|
1132
|
+
let transformedMessages = currentMessages;
|
|
1133
|
+
if (this.apiType === 'anthropic') {
|
|
1134
|
+
transformedMessages = currentMessages.map((message, index) => {
|
|
1135
|
+
if (message.role === 'user' && (index === lastUserMsgIndex || index === secondLastUserMsgIndex)) {
|
|
1136
|
+
return {
|
|
1137
|
+
...message,
|
|
1138
|
+
content: typeof message.content === 'string'
|
|
1139
|
+
? [{ type: "text", text: message.content, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } } }]
|
|
1140
|
+
: message.content.map((content, contentIndex) => {
|
|
1141
|
+
// Only apply cache_control to the text part, not images
|
|
1142
|
+
if (content.type === 'text' && contentIndex === 0) {
|
|
1143
|
+
return {
|
|
1144
|
+
...content,
|
|
1145
|
+
providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
return content;
|
|
1149
|
+
})
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
return message;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let streamError;
|
|
1157
|
+
|
|
1158
|
+
const generateOptions = {
|
|
1159
|
+
model: this.provider(this.model),
|
|
1160
|
+
messages: transformedMessages,
|
|
1161
|
+
system: systemPrompt,
|
|
1162
|
+
temperature: 0.3,
|
|
1163
|
+
maxTokens: maxResponseTokens,
|
|
1164
|
+
signal: this.abortController.signal,
|
|
1165
|
+
onError({ error }) {
|
|
1166
|
+
streamError = error;
|
|
1167
|
+
console.error(error); // your error logging logic here
|
|
1168
|
+
},
|
|
1169
|
+
providerOptions: {
|
|
1170
|
+
openai: {
|
|
1171
|
+
streamOptions: {
|
|
1172
|
+
include_usage: true
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
experimental_telemetry: {
|
|
1177
|
+
isEnabled: false, // Disable built-in telemetry in favor of our custom tracing
|
|
1178
|
+
functionId: this.sessionId,
|
|
1179
|
+
metadata: {
|
|
1180
|
+
sessionId: this.sessionId,
|
|
1181
|
+
iteration: currentIteration,
|
|
1182
|
+
model: this.model,
|
|
1183
|
+
apiType: this.apiType,
|
|
1184
|
+
allowEdit: this.allowEdit,
|
|
1185
|
+
promptType: this.promptType || 'default'
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
// Start AI generation request trace within iteration context
|
|
1191
|
+
const aiRequestSpan = appTracer.withIterationContext(sessionId, currentIteration, () => {
|
|
1192
|
+
return appTracer.startAiGenerationRequest(sessionId, currentIteration, this.model, this.apiType, {
|
|
1193
|
+
temperature: 0.3,
|
|
1194
|
+
maxTokens: maxResponseTokens,
|
|
1195
|
+
maxRetries: 2
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// **Streaming Response Handling**
|
|
1200
|
+
let assistantResponseContent = '';
|
|
1201
|
+
let startTime = Date.now();
|
|
1202
|
+
let firstChunkTime = null;
|
|
1203
|
+
try {
|
|
1204
|
+
if (this.debug) console.log(`[DEBUG] Calling streamText with model ${this.model}...`);
|
|
1205
|
+
|
|
1206
|
+
if (streamError) {
|
|
1207
|
+
throw streamError
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const { textStream } = streamText(generateOptions);
|
|
1211
|
+
for await (const chunk of textStream) {
|
|
1212
|
+
if (this.cancelled) throw new Error('Request was cancelled by the user');
|
|
1213
|
+
if (firstChunkTime === null) {
|
|
1214
|
+
firstChunkTime = Date.now();
|
|
1215
|
+
}
|
|
1216
|
+
assistantResponseContent += chunk;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (this.debug) {
|
|
1220
|
+
console.log(`[DEBUG] Streamed AI response (Iter ${currentIteration}). Length: ${assistantResponseContent.length}`);
|
|
1221
|
+
}
|
|
1222
|
+
if (assistantResponseContent.length == 0) {
|
|
1223
|
+
console.warn(`[WARN] Empty response from AI model (Iter ${currentIteration}).`);
|
|
1224
|
+
throw new Error('Empty response from AI model');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
1228
|
+
|
|
1229
|
+
const responseTokenCount = this.tokenCounter.countTokens(assistantResponseContent);
|
|
1230
|
+
if (this.debug) console.log(`[DEBUG] Estimated response tokens (Iter ${currentIteration}): ${responseTokenCount}`);
|
|
1231
|
+
this.tokenCounter.addResponseTokens(responseTokenCount);
|
|
1232
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1233
|
+
if (this.debug) console.log(`[DEBUG] Context size AFTER LLM response (Iter ${currentIteration}): ${this.tokenCounter.contextSize}`);
|
|
1234
|
+
|
|
1235
|
+
// Record AI response in trace
|
|
1236
|
+
const endTime = Date.now();
|
|
1237
|
+
appTracer.recordAiResponse(sessionId, currentIteration, {
|
|
1238
|
+
response: assistantResponseContent, // Include actual response content
|
|
1239
|
+
responseLength: assistantResponseContent.length,
|
|
1240
|
+
completionTokens: responseTokenCount,
|
|
1241
|
+
promptTokens: this.tokenCounter.contextSize || 0,
|
|
1242
|
+
finishReason: 'stop',
|
|
1243
|
+
timeToFirstChunk: firstChunkTime ? (firstChunkTime - startTime) : 0,
|
|
1244
|
+
timeToFinish: endTime - startTime
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
appTracer.endAiRequest(sessionId, currentIteration, true);
|
|
1248
|
+
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
// Classify and record the AI model error
|
|
1251
|
+
let errorCategory = 'unknown';
|
|
1252
|
+
if (this.cancelled || error.name === 'AbortError' || (error.message && error.message.includes('cancelled'))) {
|
|
1253
|
+
errorCategory = 'cancellation';
|
|
1254
|
+
} else if (error.message?.includes('timeout')) {
|
|
1255
|
+
errorCategory = 'timeout';
|
|
1256
|
+
} else if (error.message?.includes('rate limit') || error.message?.includes('quota')) {
|
|
1257
|
+
errorCategory = 'api_limit';
|
|
1258
|
+
} else if (error.message?.includes('network') || error.message?.includes('fetch')) {
|
|
1259
|
+
errorCategory = 'network';
|
|
1260
|
+
} else if (error.status >= 400 && error.status < 500) {
|
|
1261
|
+
errorCategory = 'client_error';
|
|
1262
|
+
} else if (error.status >= 500) {
|
|
1263
|
+
errorCategory = 'server_error';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
appTracer.recordAiModelError(sessionId, currentIteration, {
|
|
1267
|
+
category: errorCategory,
|
|
1268
|
+
message: error.message,
|
|
1269
|
+
model: this.model,
|
|
1270
|
+
provider: this.apiType,
|
|
1271
|
+
statusCode: error.status || 0,
|
|
1272
|
+
retryAttempt: 0
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
appTracer.endAiRequest(sessionId, currentIteration, false);
|
|
1276
|
+
|
|
1277
|
+
if (this.cancelled || error.name === 'AbortError' || (error.message && error.message.includes('cancelled'))) {
|
|
1278
|
+
console.log(`Chat request cancelled during LLM call (Iter ${currentIteration})`);
|
|
1279
|
+
this.cancelled = true;
|
|
1280
|
+
appTracer.recordSessionCancellation(sessionId, 'ai_request_cancelled', {
|
|
1281
|
+
currentIteration,
|
|
1282
|
+
activeTool: 'ai_generation'
|
|
1283
|
+
});
|
|
1284
|
+
throw new Error('Request was cancelled by the user');
|
|
1285
|
+
}
|
|
1286
|
+
console.error(`Error during streamText (Iter ${currentIteration}):`, error);
|
|
1287
|
+
finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
|
|
1288
|
+
throw new Error(finalResult);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const parsedTool = parseXmlToolCallWithThinking(assistantResponseContent);
|
|
1292
|
+
if (parsedTool) {
|
|
1293
|
+
const { toolName, params } = parsedTool;
|
|
1294
|
+
if (this.debug) console.log(`[DEBUG] Parsed tool call: ${toolName} with params:`, params);
|
|
1295
|
+
|
|
1296
|
+
// Record tool call parsing in trace
|
|
1297
|
+
appTracer.recordToolCallParsed(sessionId, currentIteration, toolName, params);
|
|
1298
|
+
|
|
1299
|
+
if (toolName === 'attempt_completion') {
|
|
1300
|
+
completionAttempted = true;
|
|
1301
|
+
const validation = attemptCompletionSchema.safeParse(params);
|
|
1302
|
+
if (!validation.success) {
|
|
1303
|
+
finalResult = `Error: AI attempted completion with invalid parameters: ${JSON.stringify(validation.error.issues)}`;
|
|
1304
|
+
console.warn(`[WARN] Invalid attempt_completion parameters:`, validation.error.issues);
|
|
1305
|
+
appTracer.recordCompletionAttempt(sessionId, false);
|
|
1306
|
+
} else {
|
|
1307
|
+
finalResult = validation.data.result;
|
|
1308
|
+
|
|
1309
|
+
// Store final assistant response in display history
|
|
1310
|
+
const displayAssistantMessage = {
|
|
1311
|
+
role: 'assistant',
|
|
1312
|
+
content: finalResult,
|
|
1313
|
+
visible: true,
|
|
1314
|
+
displayType: 'final',
|
|
1315
|
+
timestamp: new Date().toISOString()
|
|
1316
|
+
};
|
|
1317
|
+
this.displayHistory.push(displayAssistantMessage);
|
|
1318
|
+
|
|
1319
|
+
// Save final response to persistent storage
|
|
1320
|
+
if (this.storage) {
|
|
1321
|
+
this.storage.saveMessage(this.sessionId, {
|
|
1322
|
+
role: 'assistant',
|
|
1323
|
+
content: finalResult,
|
|
1324
|
+
timestamp: Date.now(),
|
|
1325
|
+
displayType: 'final',
|
|
1326
|
+
visible: 1,
|
|
1327
|
+
images: [],
|
|
1328
|
+
metadata: {}
|
|
1329
|
+
}).catch(err => {
|
|
1330
|
+
console.error('Failed to save final response to persistent storage:', err);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
appTracer.recordCompletionAttempt(sessionId, true, finalResult);
|
|
1335
|
+
if (this.debug) {
|
|
1336
|
+
console.log(`[DEBUG] Completion attempted successfully. Final Result captured.`);
|
|
1337
|
+
|
|
1338
|
+
try {
|
|
1339
|
+
const systemPrompt = await this.getSystemMessage();
|
|
1340
|
+
let debugContent = `system: ${systemPrompt}\n\n`;
|
|
1341
|
+
for (const msg of currentMessages) {
|
|
1342
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
1343
|
+
debugContent += `${msg.role}: ${msg.content}\n\n`;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
debugContent += `assistant (final result): ${finalResult}\n\n`;
|
|
1347
|
+
writeFileSync(debugFilePath, debugContent, { flag: 'w' });
|
|
1348
|
+
if (this.debug) console.log(`[DEBUG] Wrote complete chat history to ${debugFilePath}`);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
console.error(`Error writing chat history to debug file: ${error.message}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
break;
|
|
1355
|
+
|
|
1356
|
+
} else if (this.toolImplementations[toolName]) {
|
|
1357
|
+
const toolInstance = this.toolImplementations[toolName];
|
|
1358
|
+
let toolResultContent = '';
|
|
1359
|
+
|
|
1360
|
+
// Start tool execution trace within iteration context
|
|
1361
|
+
appTracer.withIterationContext(sessionId, currentIteration, () => {
|
|
1362
|
+
appTracer.startToolExecution(sessionId, currentIteration, toolName, params);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
const enhancedParams = { ...params, sessionId: this.sessionId };
|
|
1367
|
+
if (this.debug) console.log(`[DEBUG] Executing tool '${toolName}' with params:`, enhancedParams);
|
|
1368
|
+
const executionResult = await toolInstance.execute(enhancedParams);
|
|
1369
|
+
toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
|
|
1370
|
+
if (this.debug) {
|
|
1371
|
+
const preview = toolResultContent.substring(0, 200).replace(/\n/g, ' ') + (toolResultContent.length > 200 ? '...' : '');
|
|
1372
|
+
console.log(`[DEBUG] Tool '${toolName}' executed successfully. Result preview: ${preview}`);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// End tool execution trace with success
|
|
1376
|
+
appTracer.endToolExecution(sessionId, currentIteration, true, toolResultContent.length, null, toolResultContent);
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
1379
|
+
toolResultContent = `Error executing tool ${toolName}: ${error.message}`;
|
|
1380
|
+
if (this.debug) console.log(`[DEBUG] Tool '${toolName}' execution FAILED.`);
|
|
1381
|
+
|
|
1382
|
+
// Classify and record tool execution error
|
|
1383
|
+
let errorCategory = 'execution';
|
|
1384
|
+
if (error.message?.includes('validation')) {
|
|
1385
|
+
errorCategory = 'validation';
|
|
1386
|
+
} else if (error.message?.includes('permission') || error.message?.includes('access')) {
|
|
1387
|
+
errorCategory = 'filesystem';
|
|
1388
|
+
} else if (error.message?.includes('network') || error.message?.includes('fetch')) {
|
|
1389
|
+
errorCategory = 'network';
|
|
1390
|
+
} else if (error.message?.includes('timeout')) {
|
|
1391
|
+
errorCategory = 'timeout';
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
appTracer.recordToolError(sessionId, currentIteration, toolName, {
|
|
1395
|
+
category: errorCategory,
|
|
1396
|
+
message: error.message,
|
|
1397
|
+
exitCode: error.code || 0,
|
|
1398
|
+
signal: error.signal || '',
|
|
1399
|
+
params: enhancedParams
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// End tool execution trace with failure
|
|
1403
|
+
appTracer.endToolExecution(sessionId, currentIteration, false, 0, error.message, toolResultContent);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
|
|
1407
|
+
currentMessages.push({ role: 'user', content: toolResultMessage });
|
|
1408
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1409
|
+
if (this.debug) console.log(`[DEBUG] Context size after adding tool result for '${toolName}': ${this.tokenCounter.contextSize}`);
|
|
1410
|
+
|
|
1411
|
+
} else {
|
|
1412
|
+
if (this.debug) console.log(`[DEBUG] Assistant used invalid tool name: ${toolName}`);
|
|
1413
|
+
const errorContent = `<tool_result>\nError: Invalid tool name specified: '${toolName}'. Please use one of: search, query, extract, attempt_completion.\n</tool_result>`;
|
|
1414
|
+
currentMessages.push({ role: 'user', content: errorContent });
|
|
1415
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
} else {
|
|
1419
|
+
if (this.debug) console.log(`[DEBUG] Assistant response did not contain a valid XML tool call.`);
|
|
1420
|
+
const forceToolContent = `Your response did not contain a valid tool call in the required XML format. You MUST respond with exactly one tool call (e.g., <search>...</search> or <attempt_completion>...</attempt_completion>) based on the previous steps and the user's goal. Analyze the situation and choose the appropriate next tool.`;
|
|
1421
|
+
currentMessages.push({ role: 'user', content: forceToolContent });
|
|
1422
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (currentMessages.length > MAX_HISTORY_MESSAGES + 3) {
|
|
1426
|
+
const messagesBefore = currentMessages.length;
|
|
1427
|
+
const removeCount = currentMessages.length - MAX_HISTORY_MESSAGES;
|
|
1428
|
+
currentMessages = currentMessages.slice(removeCount);
|
|
1429
|
+
|
|
1430
|
+
// Record in-loop history management
|
|
1431
|
+
appTracer.recordHistoryOperation(sessionId, 'trim', {
|
|
1432
|
+
messagesBefore,
|
|
1433
|
+
messagesAfter: currentMessages.length,
|
|
1434
|
+
messagesRemoved: removeCount,
|
|
1435
|
+
reason: 'loop_memory_limit'
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
if (this.debug) console.log(`[DEBUG] Trimmed 'currentMessages' within loop to ${currentMessages.length} (removed ${removeCount}).`);
|
|
1439
|
+
this.tokenCounter.calculateContextSize(currentMessages);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// End iteration trace
|
|
1443
|
+
appTracer.endIteration(sessionId, currentIteration, true, completionAttempted ? 'completion_attempted' : 'tool_executed');
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (currentIteration >= MAX_TOOL_ITERATIONS && !completionAttempted) {
|
|
1447
|
+
console.warn(`[WARN] Max tool iterations (${MAX_TOOL_ITERATIONS}) reached for session ${this.sessionId}. Returning current error state.`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// End agent loop trace
|
|
1451
|
+
appTracer.endAgentLoop(sessionId, currentIteration, completionAttempted, completionAttempted ? 'completion' : 'max_iterations');
|
|
1452
|
+
|
|
1453
|
+
this.history = currentMessages.map(msg => ({ ...msg }));
|
|
1454
|
+
if (this.history.length > MAX_HISTORY_MESSAGES) {
|
|
1455
|
+
const messagesBefore = this.history.length;
|
|
1456
|
+
const finalRemoveCount = this.history.length - MAX_HISTORY_MESSAGES;
|
|
1457
|
+
this.history = this.history.slice(finalRemoveCount);
|
|
1458
|
+
|
|
1459
|
+
// Record history management operation
|
|
1460
|
+
appTracer.recordHistoryOperation(sessionId, 'trim', {
|
|
1461
|
+
messagesBefore,
|
|
1462
|
+
messagesAfter: this.history.length,
|
|
1463
|
+
messagesRemoved: finalRemoveCount,
|
|
1464
|
+
reason: 'max_length'
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
if (this.debug) console.log(`[DEBUG] Final history trim applied. Length: ${this.history.length} (removed ${finalRemoveCount})`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
this.tokenCounter.updateHistory(this.history);
|
|
1471
|
+
|
|
1472
|
+
// Record token metrics
|
|
1473
|
+
const tokenUsage = this.tokenCounter.getTokenUsage();
|
|
1474
|
+
appTracer.recordTokenMetrics(sessionId, {
|
|
1475
|
+
contextWindow: tokenUsage.contextWindow || 0,
|
|
1476
|
+
currentTotal: tokenUsage.current?.total || 0,
|
|
1477
|
+
requestTokens: tokenUsage.current?.request || 0,
|
|
1478
|
+
responseTokens: tokenUsage.current?.response || 0,
|
|
1479
|
+
cacheRead: tokenUsage.current?.cacheRead || 0,
|
|
1480
|
+
cacheWrite: tokenUsage.current?.cacheWrite || 0
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
// End user message processing trace
|
|
1484
|
+
appTracer.endUserMessageProcessing(sessionId, completionAttempted);
|
|
1485
|
+
|
|
1486
|
+
if (this.debug) {
|
|
1487
|
+
console.log(`[DEBUG] Updated tokenCounter history with ${this.history.length} messages`);
|
|
1488
|
+
console.log(`[DEBUG] Context size after history update: ${this.tokenCounter.contextSize}`);
|
|
1489
|
+
console.log(`[DEBUG] ===== Ending XML Tool Chat Loop =====`);
|
|
1490
|
+
console.log(`[DEBUG] Loop finished after ${currentIteration} iterations.`);
|
|
1491
|
+
console.log(`[DEBUG] Completion attempted: ${completionAttempted}`);
|
|
1492
|
+
console.log(`[DEBUG] Final history length: ${this.history.length}`);
|
|
1493
|
+
const resultPreview = (typeof finalResult === 'string' ? finalResult : JSON.stringify(finalResult)).substring(0, 200).replace(/\n/g, ' ');
|
|
1494
|
+
console.log(`[DEBUG] Returning final result: "${resultPreview}..."`);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
this.tokenCounter.calculateContextSize(this.history);
|
|
1498
|
+
const updatedTokenUsage = this.tokenCounter.getTokenUsage();
|
|
1499
|
+
if (this.debug) {
|
|
1500
|
+
console.log(`[DEBUG] Final context window size: ${updatedTokenUsage.contextWindow}`);
|
|
1501
|
+
console.log(`[DEBUG] Cache metrics - Read: ${updatedTokenUsage.current.cacheRead}, Write: ${updatedTokenUsage.current.cacheWrite}`);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return {
|
|
1505
|
+
response: finalResult,
|
|
1506
|
+
tokenUsage: updatedTokenUsage
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
// Check if this is a critical API error that should cause process exit
|
|
1511
|
+
const isCriticalApiError = this._isCriticalApiError(error);
|
|
1512
|
+
|
|
1513
|
+
// Record the top-level processing error
|
|
1514
|
+
if (this.cancelled || (error.message && error.message.includes('cancelled'))) {
|
|
1515
|
+
appTracer.recordSessionCancellation(sessionId, 'processing_cancelled', {
|
|
1516
|
+
currentIteration,
|
|
1517
|
+
errorMessage: error.message
|
|
1518
|
+
});
|
|
1519
|
+
} else {
|
|
1520
|
+
// Record as a general processing error
|
|
1521
|
+
appTracer.recordAiModelError(sessionId, currentIteration || 0, {
|
|
1522
|
+
category: isCriticalApiError ? 'critical_api_error' : 'processing_error',
|
|
1523
|
+
message: error.message,
|
|
1524
|
+
model: this.model,
|
|
1525
|
+
provider: this.apiType,
|
|
1526
|
+
statusCode: error.statusCode || 0,
|
|
1527
|
+
retryAttempt: 0
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// End chat session before cleanup to ensure span is properly captured
|
|
1532
|
+
appTracer.endChatSession(sessionId, false, 0);
|
|
1533
|
+
|
|
1534
|
+
// Clean up any remaining spans for this session (but session span is already ended)
|
|
1535
|
+
appTracer.cleanup(sessionId);
|
|
1536
|
+
|
|
1537
|
+
console.error('Error in chat processing loop:', error);
|
|
1538
|
+
if (this.debug) console.error('Error in chat processing loop:', error);
|
|
1539
|
+
|
|
1540
|
+
this.tokenCounter.updateHistory(this.history);
|
|
1541
|
+
if (this.debug) console.log(`[DEBUG] Error case - Updated tokenCounter history with ${this.history.length} messages`);
|
|
1542
|
+
|
|
1543
|
+
this.tokenCounter.calculateContextSize(this.history);
|
|
1544
|
+
const updatedTokenUsage = this.tokenCounter.getTokenUsage();
|
|
1545
|
+
if (this.debug) {
|
|
1546
|
+
console.log(`[DEBUG] Error case - Final context window size: ${updatedTokenUsage.contextWindow}`);
|
|
1547
|
+
console.log(`[DEBUG] Error case - Cache metrics - Read: ${updatedTokenUsage.current.cacheRead}, Write: ${updatedTokenUsage.current.cacheWrite}`);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (this.cancelled || (error.message && error.message.includes('cancelled'))) {
|
|
1551
|
+
return { response: "Request cancelled.", tokenUsage: updatedTokenUsage };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Re-throw critical API errors so the process exits with code 1
|
|
1555
|
+
if (isCriticalApiError) {
|
|
1556
|
+
throw error;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return {
|
|
1560
|
+
response: `Error during chat processing: ${error.message || 'An unexpected error occurred.'}`,
|
|
1561
|
+
tokenUsage: updatedTokenUsage
|
|
1562
|
+
};
|
|
1563
|
+
} finally {
|
|
1564
|
+
this.abortController = null;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Check if an error is a critical API error that should cause process exit
|
|
1570
|
+
* @param {Error} error - The error to check
|
|
1571
|
+
* @returns {boolean} - True if this is a critical API error
|
|
1572
|
+
* @private
|
|
1573
|
+
*/
|
|
1574
|
+
_isCriticalApiError(error) {
|
|
1575
|
+
// Check for AI SDK API call errors
|
|
1576
|
+
if (error[Symbol.for('vercel.ai.error.AI_APICallError')]) {
|
|
1577
|
+
const statusCode = error.statusCode;
|
|
1578
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
1579
|
+
|
|
1580
|
+
// Critical HTTP status codes that indicate configuration issues
|
|
1581
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1582
|
+
return true; // Unauthorized/Forbidden - bad API key
|
|
1583
|
+
}
|
|
1584
|
+
if (statusCode === 404) {
|
|
1585
|
+
return true; // Not Found - wrong URL or model not found
|
|
1586
|
+
}
|
|
1587
|
+
if (statusCode >= 500 && statusCode < 600) {
|
|
1588
|
+
return false; // Server errors - could be temporary, don't exit
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Check for specific error messages that indicate configuration issues
|
|
1593
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
1594
|
+
if (errorMessage.includes('not found')) {
|
|
1595
|
+
return true; // API endpoint not found
|
|
1596
|
+
}
|
|
1597
|
+
if (errorMessage.includes('unauthorized') || errorMessage.includes('invalid api key')) {
|
|
1598
|
+
return true; // Authentication issues
|
|
1599
|
+
}
|
|
1600
|
+
if (errorMessage.includes('forbidden') || errorMessage.includes('access denied')) {
|
|
1601
|
+
return true; // Permission issues
|
|
1602
|
+
}
|
|
1603
|
+
if (errorMessage.includes('empty response from ai model')) {
|
|
1604
|
+
return true; // Indicates API connection/configuration issue
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return false; // Other errors are not critical
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Get the current token usage summary
|
|
1612
|
+
* @returns {Object} - Raw token usage data for UI display
|
|
1613
|
+
*/
|
|
1614
|
+
getTokenUsage() {
|
|
1615
|
+
// Get raw token usage from the counter
|
|
1616
|
+
const usage = this.tokenCounter.getTokenUsage();
|
|
1617
|
+
|
|
1618
|
+
// Return the raw usage data directly
|
|
1619
|
+
// This allows the web interface to format it as needed
|
|
1620
|
+
return usage;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Clear the entire history and reset session/token usage
|
|
1625
|
+
* @returns {string} - The new session ID
|
|
1626
|
+
*/
|
|
1627
|
+
clearHistory() {
|
|
1628
|
+
const oldHistoryLength = this.history.length;
|
|
1629
|
+
const oldSessionId = this.sessionId;
|
|
1630
|
+
|
|
1631
|
+
this.history = [];
|
|
1632
|
+
this.sessionId = randomUUID(); // Generate a new session ID
|
|
1633
|
+
|
|
1634
|
+
// Clear the tokenCounter - this resets all counters and the internal history
|
|
1635
|
+
this.tokenCounter.clear();
|
|
1636
|
+
|
|
1637
|
+
// Double-check that the tokenCounter's history is empty
|
|
1638
|
+
if (this.tokenCounter.history && this.tokenCounter.history.length > 0) {
|
|
1639
|
+
this.tokenCounter.history = [];
|
|
1640
|
+
if (this.debug) {
|
|
1641
|
+
console.log(`[DEBUG] Explicitly cleared tokenCounter history after clear() call`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
this.cancelled = false; // Reset cancellation flag
|
|
1646
|
+
if (this.abortController) {
|
|
1647
|
+
// Ensure any lingering abort signal is cleared (though should be handled by `chat`)
|
|
1648
|
+
try { this.abortController.abort('History cleared'); } catch (e) { /* ignore */ }
|
|
1649
|
+
this.abortController = null;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
if (this.debug) {
|
|
1654
|
+
console.log(`[DEBUG] ===== CLEARING CHAT HISTORY & STATE =====`);
|
|
1655
|
+
console.log(`[DEBUG] Cleared ${oldHistoryLength} messages from history`);
|
|
1656
|
+
console.log(`[DEBUG] Old session ID: ${oldSessionId}`);
|
|
1657
|
+
console.log(`[DEBUG] New session ID: ${this.sessionId}`);
|
|
1658
|
+
console.log(`[DEBUG] Token counter reset.`);
|
|
1659
|
+
console.log(`[DEBUG] Cancellation flag reset.`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Tool implementations are instance properties, they persist. Session ID is passed during execution.
|
|
1663
|
+
|
|
1664
|
+
return this.sessionId; // Return the newly generated session ID
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Get the session ID for this chat instance
|
|
1669
|
+
* @returns {string} - The session ID
|
|
1670
|
+
*/
|
|
1671
|
+
getSessionId() {
|
|
1672
|
+
return this.sessionId;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Export the extractImageUrls function for testing
|
|
1677
|
+
export { extractImageUrls };
|