/** * LLM Analyzer Service * Integrates with LLM services for advanced work pattern analysis and insight generation */ import { LLMAnalysisRequest, LLMAnalysisResponse, AnalyticsInsight } from '@/lib/types/analytics'; import { TimeEntry } from '@/lib/types/database'; export class LLMAnalyzer { private apiKey: string; private baseUrl: string; private model: string; private cache: Map; private cacheTimeout: number = 30 * 60 * 1000; // 30 minutes constructor() { this.apiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || ''; this.baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; this.model = process.env.LLM_MODEL || 'gpt-3.5-turbo'; this.cache = new Map(); } /** * Analyze time entries using LLM for work patterns and insights */ async analyzeTimeEntries(request: LLMAnalysisRequest): Promise { // Check cache first const cacheKey = this.generateCacheKey(request); const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { console.log('LLM Analyzer: Returning cached result'); return cached.data; } const startTime = Date.now(); try { const response = await this.callLLM(request); const processingTime = Date.now() - startTime; // Cache the result this.cache.set(cacheKey, { data: response, timestamp: Date.now(), }); // Add processing metadata response.processingTime = processingTime; console.log(`LLM Analyzer: Processed ${request.timeEntries.length} entries in ${processingTime}ms`); return response; } catch (error) { console.error('LLM Analyzer: Analysis failed', error); throw new Error(`LLM analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generate comprehensive insights from time entries */ async generateInsights(timeEntries: TimeEntry[]): Promise { if (timeEntries.length === 0) { return []; } const request: LLMAnalysisRequest = { timeEntries: timeEntries.map(entry => ({ id: entry.id, notes: entry.notes || undefined, title: entry.title || undefined, hours_worked: entry.hours_worked, entry_date: entry.entry_date.toISOString(), resource_name: entry.resource_id ? `Resource ${entry.resource_id}` : undefined, ticket_title: entry.ticket_id ? `Ticket ${entry.ticket_id}` : undefined, })), analysisType: 'comprehensive', }; const response = await this.analyzeTimeEntries(request); // Convert LLM insights to AnalyticsInsight format const insights: AnalyticsInsight[] = []; // Add insights from LLM response response.insights.forEach((insight, index) => { insights.push({ type: this.determineInsightType(insight), category: 'overall', title: `AI Insight ${index + 1}`, description: insight, recommendation: this.extractRecommendation(insight), severity: 'medium', actionable: true, }); }); // Add pattern-based insights response.patterns.forEach(pattern => { insights.push({ type: pattern.impact === 'high' ? 'warning' : 'info', category: 'performance', title: `Pattern: ${pattern.type}`, description: pattern.description, recommendation: `Address this ${pattern.frequency > 5 ? 'frequent' : 'occasional'} pattern`, severity: pattern.impact === 'high' ? 'high' : pattern.impact === 'medium' ? 'medium' : 'low', actionable: true, }); }); // Add recommendations response.recommendations.forEach(rec => { insights.push({ type: rec.priority === 'high' ? 'warning' : 'info', category: 'overall', title: `Recommendation: ${rec.category}`, description: rec.action, recommendation: rec.expectedImpact, severity: rec.priority === 'high' ? 'high' : rec.priority === 'medium' ? 'medium' : 'low', actionable: true, }); }); return insights; } /** * Analyze productivity patterns */ async analyzeProductivity(timeEntries: TimeEntry[]): Promise { const request: LLMAnalysisRequest = { timeEntries: timeEntries.map(entry => ({ id: entry.id, notes: entry.notes || undefined, title: entry.title || undefined, hours_worked: entry.hours_worked, entry_date: entry.entry_date.toISOString(), resource_name: entry.resource_id ? `Resource ${entry.resource_id}` : undefined, })), analysisType: 'productivity', }; return this.analyzeTimeEntries(request); } /** * Analyze work quality patterns */ async analyzeQuality(timeEntries: TimeEntry[]): Promise { const request: LLMAnalysisRequest = { timeEntries: timeEntries.map(entry => ({ id: entry.id, notes: entry.notes || undefined, title: entry.title || undefined, hours_worked: entry.hours_worked, entry_date: entry.entry_date.toISOString(), ticket_title: entry.ticket_id ? `Ticket ${entry.ticket_id}` : undefined, })), analysisType: 'quality', }; return this.analyzeTimeEntries(request); } /** * Detect anomalies in time entry patterns */ async detectAnomalies(timeEntries: TimeEntry[]): Promise { const request: LLMAnalysisRequest = { timeEntries: timeEntries.map(entry => ({ id: entry.id, notes: entry.notes || undefined, title: entry.title || undefined, hours_worked: entry.hours_worked, entry_date: entry.entry_date.toISOString(), resource_name: entry.resource_id ? `Resource ${entry.resource_id}` : undefined, })), analysisType: 'anomalies', }; return this.analyzeTimeEntries(request); } /** * Call the LLM API */ private async callLLM(request: LLMAnalysisRequest): Promise { const prompt = this.buildPrompt(request); const payload = { model: this.model, messages: [ { role: 'system', content: this.getSystemPrompt(request.analysisType), }, { role: 'user', content: prompt, }, ], temperature: 0.3, max_tokens: 1500, }; const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify(payload), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LLM API error: ${response.status} - ${errorText}`); } const data = await response.json(); const content = data.choices[0]?.message?.content; if (!content) { throw new Error('No content received from LLM'); } return this.parseResponse(content, data.usage?.total_tokens || 0); } /** * Build the prompt for LLM analysis */ private buildPrompt(request: LLMAnalysisRequest): string { const timeEntriesText = request.timeEntries.map(entry => `ID: ${entry.id}, Date: ${entry.entry_date}, Hours: ${entry.hours_worked}, Title: "${entry.title || 'No title'}", Notes: "${entry.notes || 'No notes'}", Resource: ${entry.resource_name || 'Unknown'}` ).join('\n'); let prompt = `Analyze the following time entries:\n\n${timeEntriesText}\n\n`; switch (request.analysisType) { case 'productivity': prompt += `Focus on productivity patterns, work efficiency, time utilization, and identify any productivity bottlenecks or high-performing patterns.`; break; case 'quality': prompt += `Focus on work quality, documentation detail, technical accuracy, and identify areas where documentation quality could be improved.`; break; case 'patterns': prompt += `Focus on recurring work patterns, routine tasks, common issues, and identify any patterns that could be optimized or automated.`; break; case 'anomalies': prompt += `Focus on unusual patterns, outliers, suspicious entries, and identify any anomalies that may require investigation.`; break; case 'comprehensive': default: prompt += `Provide a comprehensive analysis covering productivity, quality, patterns, and any notable insights or recommendations.`; break; } if (request.context) { prompt += `\n\nContext: Time range ${request.context.timeRange}`; if (request.context.resourceIds) { prompt += `, Resources: ${request.context.resourceIds.join(', ')}`; } if (request.context.projectIds) { prompt += `, Projects: ${request.context.projectIds.join(', ')}`; } } prompt += `\n\nPlease provide your analysis in the following JSON format: { "insights": ["insight 1", "insight 2", "insight 3"], "patterns": [ {"type": "pattern name", "description": "description", "frequency": number, "impact": "low|medium|high"} ], "recommendations": [ {"category": "category", "priority": "low|medium|high", "action": "action description", "expectedImpact": "impact description"} ], "summary": { "overallQuality": 0.8, "productivityLevel": 0.7, "keyFindings": ["finding 1", "finding 2"] } }`; return prompt; } /** * Get system prompt based on analysis type */ private getSystemPrompt(analysisType: string): string { return `You are an expert analyst specializing in time tracking and work pattern analysis. Your task is to analyze time entries and provide actionable insights. Key considerations: - Focus on practical, actionable recommendations - Consider both individual and team patterns - Highlight both strengths and areas for improvement - Provide specific, evidence-based insights - Consider the context of professional services work Analysis guidelines: - Be objective and data-driven - Provide constructive feedback - Suggest realistic improvements - Consider industry best practices for time tracking - Account for variations in work types and complexity Respond with valid JSON only, no additional text.`; } /** * Parse LLM response into structured format */ private parseResponse(content: string, tokensUsed: number): LLMAnalysisResponse { try { // Extract JSON from response const jsonMatch = content.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error('No JSON found in LLM response'); } const parsed = JSON.parse(jsonMatch[0]); // Validate and set defaults return { insights: Array.isArray(parsed.insights) ? parsed.insights : [], patterns: Array.isArray(parsed.patterns) ? parsed.patterns : [], recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [], summary: { overallQuality: parsed.summary?.overallQuality || 0.5, productivityLevel: parsed.summary?.productivityLevel || 0.5, keyFindings: Array.isArray(parsed.summary?.keyFindings) ? parsed.summary.keyFindings : [], }, processingTime: 0, // Will be set by caller tokensUsed, }; } catch (error) { console.error('Failed to parse LLM response:', error); console.error('Response content:', content); // Return fallback response return { insights: ['Unable to process AI analysis due to parsing error'], patterns: [], recommendations: [], summary: { overallQuality: 0.5, productivityLevel: 0.5, keyFindings: ['Analysis processing failed'], }, processingTime: 0, tokensUsed, }; } } /** * Determine insight type from content */ private determineInsightType(insight: string): 'success' | 'warning' | 'error' | 'info' { const lowerInsight = insight.toLowerCase(); if (lowerInsight.includes('excellent') || lowerInsight.includes('great') || lowerInsight.includes('good')) { return 'success'; } if (lowerInsight.includes('concern') || lowerInsight.includes('issue') || lowerInsight.includes('problem')) { return 'warning'; } if (lowerInsight.includes('error') || lowerInsight.includes('failed') || lowerInsight.includes('critical')) { return 'error'; } return 'info'; } /** * Extract recommendation from insight */ private extractRecommendation(insight: string): string { // Simple extraction - in a real implementation, this could be more sophisticated if (insight.includes('recommend') || insight.includes('should') || insight.includes('consider')) { return insight; } return 'Review this insight and consider appropriate action'; } /** * Generate cache key for request */ private generateCacheKey(request: LLMAnalysisRequest): string { const keyData = { analysisType: request.analysisType, entryCount: request.timeEntries.length, dateRange: { start: request.timeEntries[0]?.entry_date, end: request.timeEntries[request.timeEntries.length - 1]?.entry_date, }, context: request.context, }; return Buffer.from(JSON.stringify(keyData)).toString('base64'); } /** * Clear cache */ clearCache(): void { this.cache.clear(); } /** * Get cache statistics */ getCacheStats(): { size: number; hitRate: number } { return { size: this.cache.size, hitRate: 0, // Would need to track hits/misses for real implementation }; } } // Create singleton instance export const llmAnalyzer = new LLMAnalyzer();