""" Analytics and monitoring for Dual-Path RAG routing. """ from datetime import datetime, timedelta from typing import Dict, Any, List from django.db.models import Count, Avg, Q, F from django.utils import timezone from hue_portal.core.models import QueryRoutingLog, GoldenQuery def get_routing_stats(days: int = 7) -> Dict[str, Any]: """ Get routing statistics for the last N days. Args: days: Number of days to analyze (default: 7). Returns: Dictionary with routing statistics. """ cutoff_date = timezone.now() - timedelta(days=days) logs = QueryRoutingLog.objects.filter(created_at__gte=cutoff_date) total_count = logs.count() if total_count == 0: return { 'total_queries': 0, 'fast_path_count': 0, 'slow_path_count': 0, 'fast_path_percentage': 0.0, 'slow_path_percentage': 0.0, 'fast_path_avg_time_ms': 0.0, 'slow_path_avg_time_ms': 0.0, 'router_methods': {}, 'intent_breakdown': {}, 'cache_hit_rate': 0.0, 'top_golden_queries': [], } # Path statistics fast_path_count = logs.filter(route='fast_path').count() slow_path_count = logs.filter(route='slow_path').count() # Average response times fast_path_avg = logs.filter(route='fast_path').aggregate( avg_time=Avg('response_time_ms') )['avg_time'] or 0.0 slow_path_avg = logs.filter(route='slow_path').aggregate( avg_time=Avg('response_time_ms') )['avg_time'] or 0.0 # Router methods breakdown router_methods = dict( logs.values('router_method') .annotate(count=Count('id')) .values_list('router_method', 'count') ) # Intent breakdown intent_breakdown = dict( logs.values('intent') .annotate(count=Count('id')) .values_list('intent', 'count') ) # Cache hit rate (Fast Path usage) cache_hit_rate = (fast_path_count / total_count * 100) if total_count > 0 else 0.0 # Top golden queries by usage top_golden_queries = list( GoldenQuery.objects.filter(is_active=True) .order_by('-usage_count')[:10] .values('id', 'query', 'intent', 'usage_count', 'accuracy_score') ) return { 'total_queries': total_count, 'fast_path_count': fast_path_count, 'slow_path_count': slow_path_count, 'fast_path_percentage': (fast_path_count / total_count * 100) if total_count > 0 else 0.0, 'slow_path_percentage': (slow_path_count / total_count * 100) if total_count > 0 else 0.0, 'fast_path_avg_time_ms': round(fast_path_avg, 2), 'slow_path_avg_time_ms': round(slow_path_avg, 2), 'router_methods': router_methods, 'intent_breakdown': intent_breakdown, 'cache_hit_rate': round(cache_hit_rate, 2), 'top_golden_queries': top_golden_queries, 'period_days': days, } def get_golden_dataset_stats() -> Dict[str, Any]: """ Get statistics about the golden dataset. Returns: Dictionary with golden dataset statistics. """ total_queries = GoldenQuery.objects.count() active_queries = GoldenQuery.objects.filter(is_active=True).count() # Intent breakdown intent_breakdown = dict( GoldenQuery.objects.filter(is_active=True) .values('intent') .annotate(count=Count('id')) .values_list('intent', 'count') ) # Total usage total_usage = GoldenQuery.objects.aggregate( total_usage=Count('usage_count') )['total_usage'] or 0 # Average accuracy avg_accuracy = GoldenQuery.objects.filter(is_active=True).aggregate( avg_accuracy=Avg('accuracy_score') )['avg_accuracy'] or 1.0 # Queries with embeddings with_embeddings = GoldenQuery.objects.filter( is_active=True, query_embedding__isnull=False ).count() return { 'total_queries': total_queries, 'active_queries': active_queries, 'intent_breakdown': intent_breakdown, 'total_usage': total_usage, 'avg_accuracy': round(avg_accuracy, 3), 'with_embeddings': with_embeddings, 'embedding_coverage': (with_embeddings / active_queries * 100) if active_queries > 0 else 0.0, } def get_performance_metrics(days: int = 7) -> Dict[str, Any]: """ Get performance metrics for both paths. Args: days: Number of days to analyze. Returns: Dictionary with performance metrics. """ cutoff_date = timezone.now() - timedelta(days=days) logs = QueryRoutingLog.objects.filter(created_at__gte=cutoff_date) # P95, P99 response times fast_path_times = list( logs.filter(route='fast_path') .values_list('response_time_ms', flat=True) .order_by('response_time_ms') ) slow_path_times = list( logs.filter(route='slow_path') .values_list('response_time_ms', flat=True) .order_by('response_time_ms') ) def percentile(data: List[float], p: float) -> float: """Calculate percentile of sorted data.""" if not data: return 0.0 if len(data) == 1: return data[0] k = (len(data) - 1) * p f = int(k) c = k - f if f + 1 < len(data): return float(data[f] + c * (data[f + 1] - data[f])) return float(data[-1]) return { 'fast_path': { 'p50': percentile(fast_path_times, 0.5), 'p95': percentile(fast_path_times, 0.95), 'p99': percentile(fast_path_times, 0.99), 'min': min(fast_path_times) if fast_path_times else 0.0, 'max': max(fast_path_times) if fast_path_times else 0.0, }, 'slow_path': { 'p50': percentile(slow_path_times, 0.5), 'p95': percentile(slow_path_times, 0.95), 'p99': percentile(slow_path_times, 0.99), 'min': min(slow_path_times) if slow_path_times else 0.0, 'max': max(slow_path_times) if slow_path_times else 0.0, }, }