vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
267 lines (266 loc) • 10.6 kB
JavaScript
import { performFormatAwareLlmCall } from '../../../utils/llmHelper.js';
import { getPromptService } from '../services/prompt-service.js';
import { OpenRouterConfigManager } from '../../../utils/openrouter-config-manager.js';
import logger from '../../../logger.js';
export class LLMFallbackSystem {
static instance;
config;
openRouterConfig = null;
cache = new Map();
promptService = getPromptService();
constructor(config = {}) {
this.config = {
minPatternConfidence: config.minPatternConfidence ?? 0.7,
maxProcessingTime: config.maxProcessingTime ?? 5000,
temperature: config.temperature ?? 0.1,
enableCaching: config.enableCaching ?? true,
cacheTTL: config.cacheTTL ?? 300,
maxRetries: config.maxRetries ?? 2
};
this.initializeConfig();
}
static getInstance(config) {
if (!LLMFallbackSystem.instance) {
LLMFallbackSystem.instance = new LLMFallbackSystem(config);
}
return LLMFallbackSystem.instance;
}
async initializeConfig() {
try {
const configManager = OpenRouterConfigManager.getInstance();
await configManager.initialize();
this.openRouterConfig = await configManager.getOpenRouterConfig();
logger.info('LLM Fallback System initialized with OpenRouter config');
}
catch (error) {
logger.error({ err: error }, 'Failed to initialize LLM Fallback System config');
}
}
async recognizeIntent(text, patternConfidence = 0, context) {
const startTime = Date.now();
try {
if (!this.openRouterConfig) {
await this.initializeConfig();
if (!this.openRouterConfig) {
throw new Error('OpenRouter configuration not available');
}
}
if (patternConfidence >= this.config.minPatternConfidence) {
logger.debug({ patternConfidence }, 'Pattern confidence sufficient, skipping LLM fallback');
return null;
}
if (this.config.enableCaching) {
const cached = this.getCachedResult(text);
if (cached) {
logger.debug('Returning cached LLM intent recognition result');
return cached;
}
}
const systemPrompt = await this.promptService.getPrompt('intent_recognition');
const userPrompt = this.buildUserPrompt(text, context);
const llmResponse = await this.callLLMWithRetry(userPrompt, systemPrompt);
const parsedResponse = this.parseLLMResponse(llmResponse);
const recognizedIntent = this.convertToRecognizedIntent(text, parsedResponse, startTime);
if (this.config.enableCaching) {
this.cacheResult(text, recognizedIntent);
}
logger.info({
intent: recognizedIntent.intent,
confidence: recognizedIntent.confidence,
processingTime: recognizedIntent.metadata.processingTime
}, 'LLM intent recognition completed');
return recognizedIntent;
}
catch (error) {
logger.error({ err: error, text: text.substring(0, 100) }, 'LLM intent recognition failed');
return null;
}
}
buildUserPrompt(text, context) {
let prompt = `Please analyze the following user input and identify the intent:\n\n"${text}"\n\n`;
if (context) {
prompt += `Additional context:\n`;
for (const [key, value] of Object.entries(context)) {
prompt += `- ${key}: ${value}\n`;
}
prompt += '\n';
}
prompt += `Respond with valid JSON matching the specified format. Focus on accuracy and provide confidence scores based on how clear the intent is.`;
return prompt;
}
async callLLMWithRetry(userPrompt, systemPrompt) {
let lastError = null;
for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
try {
const response = await performFormatAwareLlmCall(userPrompt, systemPrompt, this.openRouterConfig, 'intent_recognition', 'json', undefined, this.config.temperature);
return response;
}
catch (error) {
lastError = error;
logger.warn({
attempt,
maxRetries: this.config.maxRetries,
error: error
}, 'LLM call failed, retrying');
if (attempt < this.config.maxRetries) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
throw lastError || new Error('LLM call failed after all retries');
}
parseLLMResponse(response) {
try {
const cleanedResponse = response
.replace(/```json\s*/g, '')
.replace(/```\s*/g, '')
.trim();
const parsed = JSON.parse(cleanedResponse);
if (!parsed.intent || typeof parsed.confidence !== 'number') {
throw new Error('Invalid LLM response structure');
}
parsed.confidence = Math.max(0, Math.min(1, parsed.confidence));
return parsed;
}
catch (error) {
logger.error({ err: error, response: response.substring(0, 200) }, 'Failed to parse LLM response');
throw new Error('Invalid JSON response from LLM');
}
}
convertToRecognizedIntent(originalInput, llmResponse, startTime) {
const processingTime = Date.now() - startTime;
let intent = llmResponse.intent;
let confidence = llmResponse.confidence;
if (intent === 'unrecognized_intent' || intent === 'clarification_needed') {
intent = 'unknown';
confidence = Math.min(confidence, 0.3);
logger.debug({
originalIntent: llmResponse.intent,
originalConfidence: llmResponse.confidence,
adjustedConfidence: confidence
}, 'Adjusted confidence for unrecognized intent');
}
else if (!this.isValidIntent(intent)) {
intent = 'unknown';
logger.debug({
originalIntent: llmResponse.intent,
originalConfidence: llmResponse.confidence,
adjustedIntent: intent
}, 'Converted invalid intent to unknown');
}
return {
intent: intent,
confidence,
confidenceLevel: this.getConfidenceLevel(confidence),
entities: this.convertParametersToEntities(llmResponse.parameters || {}),
originalInput,
processedInput: originalInput.toLowerCase().trim(),
alternatives: llmResponse.alternatives?.map(alt => ({
intent: this.isValidIntent(alt.intent) ? alt.intent : 'unknown',
confidence: alt.confidence
})) || [],
metadata: {
processingTime,
method: 'llm',
modelUsed: this.openRouterConfig?.llm_mapping?.intent_recognition || 'unknown',
timestamp: new Date()
}
};
}
convertParametersToEntities(parameters) {
const entityArray = [];
for (const [type, value] of Object.entries(parameters)) {
if (value !== undefined && value !== null) {
entityArray.push({
type,
value: String(value),
confidence: 0.8
});
}
}
return entityArray;
}
getConfidenceLevel(confidence) {
if (confidence >= 0.9)
return 'very_high';
if (confidence >= 0.7)
return 'high';
if (confidence >= 0.5)
return 'medium';
if (confidence >= 0.3)
return 'low';
return 'very_low';
}
isValidIntent(intent) {
const validIntents = [
'create_project', 'list_projects', 'open_project', 'update_project', 'archive_project',
'create_task', 'list_tasks', 'run_task', 'check_status',
'decompose_task', 'decompose_epic', 'decompose_project', 'search_files', 'search_content',
'refine_task', 'assign_task', 'get_help', 'parse_prd', 'parse_tasks',
'import_artifact', 'unrecognized_intent', 'clarification_needed', 'unknown'
];
return validIntents.includes(intent);
}
getCachedResult(text) {
const cacheKey = this.getCacheKey(text);
const entry = this.cache.get(cacheKey);
if (entry && entry.expiresAt > new Date()) {
return entry.response;
}
if (entry) {
this.cache.delete(cacheKey);
}
return null;
}
cacheResult(text, result) {
const cacheKey = this.getCacheKey(text);
const expiresAt = new Date(Date.now() + this.config.cacheTTL * 1000);
this.cache.set(cacheKey, {
response: result,
timestamp: new Date(),
expiresAt
});
if (this.cache.size > 100) {
this.cleanupExpiredCache();
}
}
getCacheKey(text) {
return text.toLowerCase().trim().replace(/\s+/g, ' ');
}
cleanupExpiredCache() {
const now = new Date();
let removedCount = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.expiresAt <= now) {
this.cache.delete(key);
removedCount++;
}
}
logger.debug({ removedCount, remainingCount: this.cache.size }, 'Cleaned up expired cache entries');
}
updateConfig(config) {
this.config = { ...this.config, ...config };
logger.info({ config: this.config }, 'LLM Fallback configuration updated');
}
getConfig() {
return { ...this.config };
}
clearCache() {
this.cache.clear();
logger.info('LLM Fallback cache cleared');
}
getCacheStats() {
const now = new Date();
let expiredCount = 0;
for (const entry of this.cache.values()) {
if (entry.expiresAt <= now) {
expiredCount++;
}
}
return {
size: this.cache.size,
hitRate: 0,
expiredEntries: expiredCount
};
}
}