UNPKG

optivise

Version:

Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support

665 lines 33 kB
/** * Context Analysis Engine * Core engine for analyzing prompts and curating Optimizely context */ import { DEFAULT_RELEVANCE_THRESHOLD } from '../types/index.js'; import { PromptAnalyzer } from './prompt-analyzer.js'; import { ProductDetectionService } from '../services/product-detection-service.js'; import { RuleIntelligenceService } from '../services/rule-intelligence-service.js'; import { DocumentationService } from '../services/documentation-service.js'; import { PromptAwareSearchService } from '../services/prompt-aware-search.js'; import { SessionMemoryService } from '../services/session-memory.js'; import { PromptCache } from '../services/prompt-cache.js'; import { chromaDBService } from '../integrations/chromadb-client.js'; import { openAIClient } from '../integrations/openai-client.js'; export class ContextAnalysisEngine { promptAnalyzer; productDetectionService; ruleIntelligenceService; documentationService; promptSearch; sessionMemory; promptCache = new PromptCache(60 * 1000); logger; isInitialized = false; aiEnabled = false; constructor(logger) { this.logger = logger; this.promptAnalyzer = new PromptAnalyzer(logger); this.productDetectionService = new ProductDetectionService(logger); this.ruleIntelligenceService = new RuleIntelligenceService(logger); this.documentationService = new DocumentationService(logger); this.promptSearch = new PromptAwareSearchService(logger); this.sessionMemory = new SessionMemoryService(logger); } async initialize() { if (this.isInitialized) { return; } try { this.logger.debug('Initializing Context Analysis Engine'); await Promise.all([ this.promptAnalyzer.initialize(), this.productDetectionService.initialize(), this.ruleIntelligenceService.initialize(), this.documentationService.initialize() ]); this.isInitialized = true; this.logger.info('Context Analysis Engine initialized successfully', { aiEnabled: this.aiEnabled }); // Initialize AI services asynchronously (non-blocking) this.initializeAIServicesAsync(); } catch (error) { this.logger.error('Failed to initialize Context Analysis Engine', error); throw error; } } async shutdown() { try { // Clean up AI clients and caches openAIClient.cleanup?.(); await chromaDBService.cleanup?.(); // Clear documentation cache this.documentationService.destroy?.(); this.logger.info('Context Analysis Engine shutdown completed'); } catch (error) { this.logger.warn('Context Analysis Engine shutdown encountered errors', { error: error }); } } /** * Initialize AI services asynchronously without blocking the main initialization */ initializeAIServicesAsync() { this.initializeAIServices().catch((error) => { this.logger.warn('AI services not available - running in basic mode', { openAI: false, chromaDB: false }); }); } /** * Initialize AI services (OpenAI and ChromaDB) if available */ async initializeAIServices() { try { // Initialize OpenAI client const openAIInitialized = await openAIClient.initialize(); // Initialize ChromaDB const chromaInitialized = await chromaDBService.initialize(); this.aiEnabled = openAIInitialized && chromaInitialized; if (this.aiEnabled) { this.logger.info('AI services initialized successfully', { openAI: openAIInitialized, chromaDB: chromaInitialized }); } else { this.logger.warn('AI services not available - running in basic mode', { openAI: openAIInitialized, chromaDB: chromaInitialized }); } } catch (error) { this.logger.warn('Failed to initialize AI services, continuing without AI features', { error: error }); this.aiEnabled = false; } } /** * Check if AI features are available */ isAIEnabled() { return this.aiEnabled; } async analyze(request) { const startTime = Date.now(); const timings = {}; try { // Cache: prompt hash dedupe const cacheKey = request.prompt?.length > 0 ? PromptCache.hashPrompt(request.prompt) : ''; if (cacheKey) { const cached = this.promptCache.get(cacheKey); if (cached) { return { ...cached, processingTime: Date.now() - startTime, diagnostics: { ...(cached.diagnostics || {}), cacheHit: true } }; } } // Step 1: Analyze prompt for Optimizely relevance const t1 = Date.now(); const promptAnalysis = await this.promptAnalyzer.analyze(request.prompt); const normalized = this.normalizePromptAnalysis(promptAnalysis); timings['promptAnalyzer'] = Date.now() - t1; // Step 2: Check relevance threshold - only proceed if relevant if (promptAnalysis.relevance < DEFAULT_RELEVANCE_THRESHOLD) { const low = this.createLowRelevanceResponse(promptAnalysis, startTime); low.diagnostics = { timings, cacheHit: false }; return low; } // Step 3: Detect Optimizely product context const t2 = Date.now(); const productDetection = await this.detectProductContext(request, normalized); timings['productDetection'] = Date.now() - t2; // Step 4: Analyze IDE rules (if project path available) const t3 = Date.now(); const ruleAnalysis = request.projectPath ? await this.analyzeProjectRules(request.projectPath) : null; if (request.projectPath) timings['ruleIntelligence'] = Date.now() - t3; // Step 4.1: Prompt-aware workspace search for mentioned artifacts const t4 = Date.now(); const promptSearchResults = request.projectPath && normalized.entities ? await this.promptSearch.findMentionedArtifacts(request.projectPath, { files: normalized.entities.files, classes: normalized.entities.classes }) : []; if (request.projectPath && normalized.entities) timings['promptAwareSearch'] = Date.now() - t4; // Step 5: Fetch relevant documentation const t5 = Date.now(); const documentation = await this.fetchRelevantDocumentation(productDetection, promptAnalysis.intent); timings['documentation'] = Date.now() - t5; // Step 6: Curate context based on analysis and detection const t6 = Date.now(); const curatedContext = await this.curateContext(normalized, productDetection, ruleAnalysis, documentation); timings['curation'] = Date.now() - t6; // Deterministic relevance: prompt + evidence + rules (bounded) const promptComponent = Math.max(0, Math.min(1, promptAnalysis.relevance)); const evidenceSignals = (promptAnalysis.entities?.files?.length || 0) + (promptAnalysis.entities?.classes?.length || 0); const rulesSignals = ruleAnalysis?.foundFiles?.length || 0; const evidenceComponent = Math.min(0.6, evidenceSignals * 0.05); const rulesComponent = Math.min(0.4, rulesSignals * 0.05); const finalRelevance = Math.min(1, (promptComponent * 0.6) + evidenceComponent + rulesComponent); // Step 6.1: Build promptContext for formatter stage const promptContext = { userIntent: normalized.intent, targetProducts: productDetection, artifacts: [ ...((normalized.entities?.files || []).map(v => ({ kind: 'file', value: v }))), ...((normalized.entities?.urls || []).map(v => ({ kind: 'url', value: v }))), ...((normalized.entities?.classes || []).map(v => ({ kind: 'class', value: v }))), ...((promptSearchResults || []).map((p) => ({ kind: 'file', value: p?.path || p }))) ], constraints: [], acceptanceCriteria: [], sessionHints: {} }; // Step 7: Build final response const response = { relevance: finalRelevance, detectedProducts: productDetection, curatedContext, processingTime: Date.now() - startTime, timestamp: new Date(), promptContext, diagnostics: { timings, cacheHit: false, relevanceBreakdown: { prompt: promptComponent, evidence: evidenceComponent, rules: rulesComponent, final: finalRelevance } } }; // Record in session memory this.sessionMemory.recordInteraction({ products: productDetection, files: (promptAnalysis.entities?.files || []), toolName: 'optidev_context_analyzer' }); // Store in cache if (cacheKey) { this.promptCache.set(cacheKey, response); } if (process.env.OPTIDEV_DEBUG === 'true') { console.error('[observability] entities', normalized.entities); console.error('[observability] ruleFiles', ruleAnalysis?.foundFiles); console.error('[observability] products', productDetection); console.error('[observability] relevance', { promptComponent, evidenceComponent, rulesComponent, finalRelevance }); } this.logger.debug('Context analysis completed', { relevance: response.relevance, products: response.detectedProducts, processingTime: response.processingTime }); return response; } catch (error) { this.logger.error('Context analysis failed', error); throw error; } } createLowRelevanceResponse(promptAnalysis, startTime) { this.logger.debug('Prompt below relevance threshold', { relevance: promptAnalysis.relevance, threshold: DEFAULT_RELEVANCE_THRESHOLD }); const artifacts = [ ...((promptAnalysis.entities?.files || []).map(v => ({ kind: 'file', value: v }))), ...((promptAnalysis.entities?.urls || []).map(v => ({ kind: 'url', value: v }))), ...((promptAnalysis.entities?.classes || []).map(v => ({ kind: 'class', value: v }))) ]; return { relevance: promptAnalysis.relevance, detectedProducts: [], curatedContext: { relevance: promptAnalysis.relevance, productContext: [], summary: 'This query does not appear to be related to Optimizely development. Optivise specializes in providing context for Optimizely-specific questions.', actionableSteps: [], codeExamples: [], documentation: [], bestPractices: [] }, processingTime: Date.now() - startTime, timestamp: new Date(), promptContext: { userIntent: promptAnalysis.intent || 'unknown', targetProducts: [], artifacts, constraints: [], acceptanceCriteria: [], sessionHints: {} } }; } async detectProductContext(request, promptAnalysis) { try { // If project path available (IDE mode), prefer project detection if (request.projectPath) { const project = await this.productDetectionService.detectFromProject(request.projectPath); // Hybrid scoring: blend with prompt-based detection const prompt = await this.productDetectionService.detectFromPrompt(request.prompt, promptAnalysis.productHints); // Expose evidence in debug if (process.env.OPTIDEV_DEBUG === 'true') { console.error('[evidence] project', project.evidence?.slice(0, 5)); console.error('[evidence] prompt', prompt.evidence?.slice(0, 5)); } const merged = new Map(); project.products.forEach(p => merged.set(p, (merged.get(p) || 0) + project.confidence)); prompt.products.forEach(p => merged.set(p, (merged.get(p) || 0) + prompt.confidence * 0.8)); // Sort by score desc return Array.from(merged.entries()).sort((a, b) => b[1] - a[1]).map(([p]) => p).slice(0, 3); } // Prompt-only const detection = await this.productDetectionService.detectFromPrompt(request.prompt, promptAnalysis.productHints); return detection.products; } catch (error) { this.logger.warn('Product detection failed, using prompt hints'); return promptAnalysis.productHints; } } async analyzeProjectRules(projectPath) { try { return await this.ruleIntelligenceService.analyzeIDERules(projectPath); } catch (error) { this.logger.warn('Rule analysis failed, continuing without rules'); return null; } } async fetchRelevantDocumentation(products, intent) { try { if (products.length === 0) return []; // Use AI-powered vector search if available if (this.aiEnabled && intent) { return await this.fetchAIEnhancedDocumentation(products, intent); } // Fallback to basic documentation service const documentation = await this.documentationService.fetchDocumentation(products); this.logger.debug('Documentation fetched (basic mode)', { products, documentsRetrieved: documentation.length }); return documentation; } catch (error) { this.logger.warn('Documentation fetch failed, continuing without docs'); return []; } } /** * Fetch documentation using AI-powered vector search */ async fetchAIEnhancedDocumentation(products, query) { try { const results = []; // Search across all relevant product collections for (const product of products.slice(0, 2)) { // Limit to top 2 products const searchResults = await chromaDBService.searchDocuments(query, { product, limit: 5, threshold: 0.7 }); results.push(...searchResults.map(result => ({ title: result.metadata.title || 'Documentation', content: result.content, url: result.metadata.url || '#', product: result.metadata.product, relevance: result.similarity, contentType: result.metadata.contentType }))); } // Also search general platform documentation const platformResults = await chromaDBService.searchDocuments(query, { product: 'platform', limit: 3, threshold: 0.6 }); results.push(...platformResults.map(result => ({ title: result.metadata.title || 'Platform Documentation', content: result.content, url: result.metadata.url || '#', product: 'platform', relevance: result.similarity, contentType: result.metadata.contentType }))); // Sort by relevance and deduplicate const uniqueResults = results .sort((a, b) => b.relevance - a.relevance) .filter((result, index, array) => array.findIndex(r => r.url === result.url) === index) .slice(0, 10); // Limit to top 10 results this.logger.debug('AI-enhanced documentation fetched', { products, query, documentsRetrieved: uniqueResults.length, avgRelevance: uniqueResults.reduce((sum, r) => sum + r.relevance, 0) / uniqueResults.length }); return uniqueResults; } catch (error) { this.logger.error('AI-enhanced documentation fetch failed', error); // Fallback to basic documentation service return await this.documentationService.fetchDocumentation(products); } } normalizePromptAnalysis(promptAnalysis) { // Ensure entities and intent are present; clamp relevance return { ...promptAnalysis, relevance: Math.max(0, Math.min(1, promptAnalysis.relevance ?? 0)), intent: promptAnalysis.intent || 'unknown', entities: { files: promptAnalysis.entities?.files || [], urls: promptAnalysis.entities?.urls || [], classes: promptAnalysis.entities?.classes || [], versions: promptAnalysis.entities?.versions || [] }, productHints: promptAnalysis.productHints || [] }; } async curateContext(promptAnalysis, detectedProducts, ruleAnalysis, documentation) { // Enhanced Phase 3 implementation with AI-powered curation const context = { relevance: promptAnalysis.relevance, productContext: detectedProducts, summary: await this.generateEnhancedSummary(promptAnalysis, detectedProducts, ruleAnalysis, documentation), actionableSteps: await this.generateEnhancedActionableSteps(promptAnalysis, detectedProducts, ruleAnalysis, documentation), codeExamples: this.extractCodeExamples(documentation || []), documentation: this.formatDocumentationLinks(documentation || []), bestPractices: await this.generateEnhancedBestPractices(detectedProducts, ruleAnalysis, documentation) }; return context; } generateSummary(promptAnalysis, detectedProducts, ruleAnalysis) { const productNames = detectedProducts.map(p => this.getProductDisplayName(p)); const productContext = productNames.length > 0 ? `for ${productNames.join(', ')} development` : 'for Optimizely development'; // Add rule context if available const ruleContext = ruleAnalysis?.foundFiles?.length ? ` (${ruleAnalysis.foundFiles.length} IDE rule files detected with ${ruleAnalysis.optimizelyRelevance.toFixed(1)} relevance)` : ''; switch (promptAnalysis.intent) { case 'code-help': return `Code assistance ${productContext}${ruleContext} - analyzing development requirements and providing implementation guidance.`; case 'documentation': return `Documentation search ${productContext}${ruleContext} - providing relevant documentation and reference materials.`; case 'troubleshooting': return `Troubleshooting support ${productContext}${ruleContext} - helping diagnose and resolve development issues.`; case 'best-practices': return `Best practices guidance ${productContext}${ruleContext} - sharing recommended approaches and patterns.`; case 'configuration': return `Configuration help ${productContext}${ruleContext} - assisting with setup and configuration tasks.`; default: return `Development assistance ${productContext}${ruleContext} - providing contextual guidance and support.`; } } /** * Generate enhanced summary using AI when available */ async generateEnhancedSummary(promptAnalysis, detectedProducts, ruleAnalysis, documentation) { if (!this.aiEnabled || !documentation?.length) { return this.generateSummary(promptAnalysis, detectedProducts, ruleAnalysis); } try { // Use AI to create a more contextual summary based on retrieved documentation const relevantContent = documentation .filter(doc => doc.relevance > 0.7) .slice(0, 3) .map(doc => doc.content.substring(0, 200)) .join(' '); if (relevantContent.length < 50) { return this.generateSummary(promptAnalysis, detectedProducts, ruleAnalysis); } const productNames = detectedProducts.map(p => this.getProductDisplayName(p)); const productContext = productNames.length > 0 ? `for ${productNames.join(', ')} development` : 'for Optimizely development'; // Enhanced summary with AI-retrieved context return `AI-enhanced analysis ${productContext} - Found ${documentation.length} relevant documentation sources with average relevance of ${(documentation.reduce((sum, doc) => sum + (doc.relevance || 0), 0) / documentation.length).toFixed(2)}. Context includes: ${relevantContent.split(' ').slice(0, 20).join(' ')}...`; } catch (error) { this.logger.warn('Failed to generate AI-enhanced summary, falling back to basic', { error: error }); return this.generateSummary(promptAnalysis, detectedProducts, ruleAnalysis); } } /** * Generate enhanced actionable steps using AI-retrieved documentation */ async generateEnhancedActionableSteps(promptAnalysis, detectedProducts, ruleAnalysis, documentation) { const basicSteps = this.generateActionableSteps(promptAnalysis, detectedProducts, ruleAnalysis); if (!this.aiEnabled || !documentation?.length) { return basicSteps; } try { const enhancedSteps = [...basicSteps]; // Add AI-powered contextual steps based on documentation const highRelevanceDocs = documentation.filter(doc => doc.relevance > 0.75); if (highRelevanceDocs.length > 0) { enhancedSteps.unshift(`🤖 AI-Suggested: Review ${highRelevanceDocs.length} highly relevant documentation sources (avg relevance: ${(highRelevanceDocs.reduce((sum, doc) => sum + doc.relevance, 0) / highRelevanceDocs.length).toFixed(2)})`); } // Add specific documentation links as actionable steps const topDocs = documentation.slice(0, 2); topDocs.forEach(doc => { if (doc.url && doc.url !== '#') { enhancedSteps.push(`📖 Review: ${doc.title} (relevance: ${doc.relevance.toFixed(2)})`); } }); return enhancedSteps.slice(0, 8); // Limit to 8 steps } catch (error) { this.logger.warn('Failed to generate AI-enhanced steps, using basic', { error: error }); return basicSteps; } } /** * Generate enhanced best practices using AI-retrieved documentation */ async generateEnhancedBestPractices(detectedProducts, ruleAnalysis, documentation) { const basicPractices = this.generateBestPractices(detectedProducts, ruleAnalysis); if (!this.aiEnabled || !documentation?.length) { return basicPractices; } try { const enhancedPractices = [...basicPractices]; // Extract best practices from AI-retrieved documentation const practiceKeywords = ['best practice', 'recommended', 'should', 'avoid', 'pattern', 'guideline']; documentation.forEach(doc => { if (doc.relevance > 0.6 && doc.contentType === 'documentation') { const content = doc.content.toLowerCase(); const hasPracticeContent = practiceKeywords.some(keyword => content.includes(keyword)); if (hasPracticeContent) { // Extract sentences that contain best practice keywords const sentences = doc.content.split(/[.!?]/); const practiceSentences = sentences.filter((sentence) => practiceKeywords.some(keyword => sentence.toLowerCase().includes(keyword))); if (practiceSentences.length > 0) { const cleanSentence = practiceSentences[0].trim().replace(/^[^A-Z]*/, ''); if (cleanSentence.length > 20 && cleanSentence.length < 150) { enhancedPractices.push(`🤖 AI-Found: ${cleanSentence}`); } } } } }); return [...new Set(enhancedPractices)].slice(0, 10); // Deduplicate and limit } catch (error) { this.logger.warn('Failed to generate AI-enhanced best practices, using basic', { error: error }); return basicPractices; } } generateActionableSteps(promptAnalysis, detectedProducts, ruleAnalysis) { const steps = []; if (detectedProducts.length > 0) { steps.push(`Working with ${detectedProducts.map(p => this.getProductDisplayName(p)).join(', ')}`); } // Add rule enhancement suggestions if (ruleAnalysis?.suggestedEnhancements?.length) { steps.push(`Rule enhancement available: ${ruleAnalysis.suggestedEnhancements[0].suggestion}`); } switch (promptAnalysis.intent) { case 'code-help': steps.push('Review relevant code examples and implementation patterns'); steps.push('Check official documentation for API references'); steps.push('Consider best practices for your specific use case'); break; case 'troubleshooting': steps.push('Identify the specific error or issue'); steps.push('Check logs and error messages for additional context'); steps.push('Review recent changes that might have caused the issue'); break; case 'best-practices': steps.push('Review established patterns and conventions'); steps.push('Consider performance and maintainability implications'); steps.push('Validate approach against official recommendations'); break; default: steps.push('Gather more specific requirements'); steps.push('Review relevant documentation and examples'); } return steps; } generateBestPractices(detectedProducts, ruleAnalysis) { const practices = []; // Add rule-based practices first if (ruleAnalysis?.suggestedEnhancements?.length) { for (const enhancement of ruleAnalysis.suggestedEnhancements.slice(0, 2)) { practices.push(`${enhancement.suggestion}: ${enhancement.rationale}`); } } if (detectedProducts.includes('configured-commerce')) { practices.push('Follow handler chain patterns for extending commerce functionality'); practices.push('Use proper dependency injection in your extensions'); practices.push('Implement proper error handling and logging'); } if (detectedProducts.includes('cms-paas') || detectedProducts.includes('cms-saas')) { practices.push('Use content types and properties appropriately'); practices.push('Follow MVC patterns in your implementations'); practices.push('Implement proper caching strategies'); } if (detectedProducts.includes('web-experimentation') || detectedProducts.includes('feature-experimentation')) { practices.push('Implement proper event tracking and analytics'); practices.push('Use feature flags to control experiment rollouts'); practices.push('Ensure proper audience targeting and segmentation'); } // IDE-specific practices based on rule analysis if (ruleAnalysis?.optimizelyRelevance < 0.5) { practices.push('Consider adding Optimizely-specific IDE configurations for better development experience'); } // General best practices practices.push('Follow Optimizely naming conventions and coding standards'); practices.push('Implement comprehensive error handling and logging'); practices.push('Write maintainable and well-documented code'); return practices; } extractCodeExamples(documentation) { const codeExamples = []; for (const doc of documentation) { if (doc?.content) { // Extract code blocks from documentation content const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; let match; while ((match = codeBlockRegex.exec(doc.content)) !== null && match.index !== undefined) { const language = match[1] || 'text'; const code = match[2]?.trim() || ''; if (code.length > 10) { // Only include substantial code blocks codeExamples.push({ language, code, description: this.extractCodeDescription(doc.content, match.index), source: doc.source || 'Documentation', relevance: 0.8 }); } } } } // Sort by relevance and limit to top 5 return codeExamples .sort((a, b) => b.relevance - a.relevance) .slice(0, 5); } formatDocumentationLinks(documentation) { return documentation .filter(doc => (doc?.source || doc?.url) && doc?.title) .map(doc => ({ title: doc.title, url: doc.source || doc.url, description: this.extractDocDescription(doc), relevance: doc.relevanceScore || doc.relevance || 0.8, lastUpdated: doc.lastUpdated })) .sort((a, b) => b.relevance - a.relevance) .slice(0, 3); // Limit to top 3 most relevant docs } extractCodeDescription(content, codeIndex) { // Look for the heading or paragraph before the code block const beforeCode = content.substring(0, codeIndex); const lines = beforeCode.split('\n').reverse(); for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('#') || (trimmed.length > 10 && !trimmed.startsWith('```'))) { return trimmed.replace(/^#+\s*/, '').substring(0, 100); } } return 'Code example from documentation'; } extractDocDescription(doc) { if (doc.content) { // Extract first meaningful paragraph const paragraphs = doc.content.split('\n\n'); for (const paragraph of paragraphs) { const cleaned = paragraph.replace(/#+\s*/g, '').trim(); if (cleaned.length > 50 && !cleaned.startsWith('```')) { return cleaned.substring(0, 150) + '...'; } } } const productLabel = doc.product || (Array.isArray(doc.products) ? doc.products.join(', ') : 'Optimizely'); return `Documentation for ${productLabel} development`; } getProductDisplayName(product) { const names = { 'configured-commerce': 'Configured Commerce', 'commerce-connect': 'Commerce Connect', 'cms-paas': 'CMS (PaaS)', 'cms-saas': 'CMS (SaaS)', 'cmp': 'Content Marketing Platform', 'dxp': 'Digital Experience Platform', 'web-experimentation': 'Web Experimentation', 'feature-experimentation': 'Feature Experimentation', 'data-platform': 'Data Platform', 'connect-platform': 'Connect Platform', 'recommendations': 'Recommendations' }; return names[product] || product; } isProductDetectionEnabled() { return this.productDetectionService?.isEnabled() || false; } } //# sourceMappingURL=context-analysis-engine.js.map