- 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
429 lines
14 KiB
TypeScript
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();
|