wulf-pulse/lib/services/llm-analyzer.ts
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

429 lines
14 KiB
TypeScript

/**
* 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<string, { data: LLMAnalysisResponse; timestamp: number }>;
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<LLMAnalysisResponse> {
// 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<AnalyticsInsight[]> {
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<LLMAnalysisResponse> {
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<LLMAnalysisResponse> {
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<LLMAnalysisResponse> {
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<LLMAnalysisResponse> {
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();