import { randomUUID } from 'crypto';
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { existsSync } from 'fs';
import { tools } from '@buger/probe';
export class ProbeChat {
constructor(options = {}) {
this.options = {
debug: process.env.DEBUG_CHAT === 'true' || process.env.DEBUG_CHAT === '1' || options.debug,
model: options.model || process.env.MODEL_NAME,
anthropicApiKey: options.anthropicApiKey || process.env.ANTHROPIC_API_KEY,
openaiApiKey: options.openaiApiKey || process.env.OPENAI_API_KEY,
anthropicApiUrl: options.anthropicApiUrl || process.env.ANTHROPIC_API_URL || 'https://api.anthropic.com/v1',
openaiApiUrl: options.openaiApiUrl || process.env.OPENAI_API_URL || 'https://api.openai.com/v1',
allowedFolders: options.allowedFolders || (process.env.ALLOWED_FOLDERS ?
process.env.ALLOWED_FOLDERS.split(',').map(folder => folder.trim()).filter(Boolean) : [])
};
this.requestTokens = 0;
this.responseTokens = 0;
this.sessionId = randomUUID();
if (this.options.debug) {
console.log(`[DEBUG] Generated session ID for chat: ${this.sessionId}`);
}
process.env.PROBE_SESSION_ID = this.sessionId;
this.initializeModel();
this.history = [];
this.MAX_HISTORY_MESSAGES = 20;
}
initializeModel() {
if (this.options.anthropicApiKey) {
this.provider = (modelName) => {
process.env.ANTHROPIC_API_KEY = this.options.anthropicApiKey;
if (this.options.anthropicApiUrl) {
process.env.ANTHROPIC_API_URL = this.options.anthropicApiUrl;
}
return anthropic(modelName);
};
this.model = this.options.model || 'claude-3-7-sonnet-latest';
this.apiType = 'anthropic';
if (this.options.debug) {
console.log(`[DEBUG] Using Anthropic API with model: ${this.model}`);
}
} else if (this.options.openaiApiKey) {
this.provider = (modelName) => {
process.env.OPENAI_API_KEY = this.options.openaiApiKey;
if (this.options.openaiApiUrl) {
process.env.OPENAI_API_URL = this.options.openaiApiUrl;
}
return openai(modelName);
};
this.model = this.options.model || 'gpt-4o-2024-05-13';
this.apiType = 'openai';
if (this.options.debug) {
console.log(`[DEBUG] Using OpenAI API with model: ${this.model}`);
}
} else {
throw new Error('No API key provided. Please set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.');
}
}
getSessionId() {
return this.sessionId;
}
getTokenUsage() {
return {
request: this.requestTokens,
response: this.responseTokens,
total: this.requestTokens + this.responseTokens
};
}
getSystemMessage() {
let systemMessage = tools.DEFAULT_SYSTEM_MESSAGE;
if (this.options.allowedFolders.length > 0) {
const folderList = this.options.allowedFolders.map(f => `"${f}"`).join(', ');
systemMessage += `\n\nThe following folders are configured for code search: ${folderList}. When using search, specify one of these folders in the path argument.`;
} else {
systemMessage += `\n\nNo specific folders are configured for code search, so the current directory will be used by default. You can omit the path parameter in your search calls, or use '.' to explicitly search in the current directory.`;
}
return systemMessage;
}
countTokens(text) {
return Math.ceil(text.length / 4);
}
async chat(message) {
try {
if (this.options.debug) {
console.log(`[DEBUG] Received user message: ${message}`);
}
const messageTokens = this.countTokens(message);
this.requestTokens += messageTokens;
if (this.history.length > this.MAX_HISTORY_MESSAGES) {
const historyStart = this.history.length - this.MAX_HISTORY_MESSAGES;
this.history = this.history.slice(historyStart);
if (this.options.debug) {
console.log(`[DEBUG] Trimmed history to ${this.history.length} messages`);
}
}
const messages = [
...this.history,
{ role: 'user', content: message }
];
if (this.options.debug) {
console.log(`[DEBUG] Sending ${messages.length} messages to model`);
}
const generateOptions = {
model: this.provider(this.model),
messages: messages,
system: this.getSystemMessage(),
tools: {
search: tools.searchTool,
query: tools.queryTool,
extract: tools.extractTool
},
maxSteps: 15,
temperature: 0.7
};
if (this.apiType === 'anthropic' && this.model.includes('3-7')) {
generateOptions.experimental_thinking = {
enabled: true,
budget: 8000
};
}
const result = await generateText(generateOptions);
this.history.push({ role: 'user', content: message });
this.history.push({ role: 'assistant', content: result.text });
const responseTokens = this.countTokens(result.text);
this.responseTokens += responseTokens;
if (result.toolCalls && result.toolCalls.length > 0 && this.options.debug) {
console.log(`[DEBUG] Tool was used: ${result.toolCalls.length} times`);
result.toolCalls.forEach((call, index) => {
console.log(`[DEBUG] Tool call ${index + 1}: ${call.name}`);
});
}
return result.text;
} catch (error) {
console.error('Error generating response:', error);
throw error;
}
}
clearHistory() {
this.history = [];
this.sessionId = randomUUID();
process.env.PROBE_SESSION_ID = this.sessionId;
if (this.options.debug) {
console.log(`[DEBUG] Chat history cleared, new session ID: ${this.sessionId}`);
}
return this.sessionId;
}
}
export default ProbeChat;
export { tools };