Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, Header, HTTPException, Query | |
| from typing import Optional | |
| from datetime import datetime, timedelta | |
| import logging | |
| import os | |
| from ..storage.analytics_store import AnalyticsStore | |
| from ..utils.access_control import require_api_permission | |
| router = APIRouter() | |
| logger = logging.getLogger(__name__) | |
| # Initialize analytics store, but don't crash the app if Supabase is not available. | |
| # In environments like Hugging Face Spaces where Supabase isn't configured, | |
| # we disable analytics gracefully instead of raising at import time. | |
| try: | |
| # Only attempt to initialize Supabase analytics when credentials are present | |
| if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"): | |
| analytics_store: Optional[AnalyticsStore] = AnalyticsStore() | |
| else: | |
| analytics_store = None | |
| logger.debug( | |
| "AnalyticsStore: Supabase credentials not configured. " | |
| "Analytics endpoints will return 503." | |
| ) | |
| except RuntimeError as exc: | |
| analytics_store = None | |
| # Only log at warning level if credentials are configured (actual error) | |
| # Otherwise log at debug level (expected when Supabase is not configured) | |
| if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"): | |
| logger.warning( | |
| "AnalyticsStore initialization failed (%s). Analytics endpoints will return 503.", | |
| str(exc).split('\n')[0], # Only first line | |
| ) | |
| else: | |
| logger.debug( | |
| "AnalyticsStore not configured (%s). Analytics endpoints will return 503.", | |
| str(exc).split('\n')[0], | |
| ) | |
| async def analytics_overview( | |
| x_tenant_id: str = Header(None), | |
| days: int = Query(30, description="Number of days to look back"), | |
| x_user_role: str = Header("viewer") | |
| ): | |
| """ | |
| Returns an overview of analytics for the dashboard. | |
| Includes total queries, tool usage, red-flag count, and active users. | |
| """ | |
| if not x_tenant_id: | |
| raise HTTPException(status_code=400, detail="Missing tenant ID") | |
| require_api_permission(x_user_role, "view_analytics") | |
| # Return empty data if analytics is not configured (instead of 503) | |
| if analytics_store is None: | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "overview": { | |
| "total_queries": 0, | |
| "tool_usage": {}, | |
| "redflag_count": 0, | |
| "active_users": 0, | |
| "last_query": None, | |
| "rag_quality": { | |
| "total_searches": 0, | |
| "avg_hits_per_search": 0, | |
| "avg_score": 0.0, | |
| "avg_top_score": 0.0, | |
| "avg_latency_ms": 0.0 | |
| } | |
| } | |
| } | |
| since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None | |
| tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp) | |
| activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp) | |
| rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp) | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "overview": { | |
| "total_queries": activity["total_queries"], | |
| "tool_usage": tool_usage, | |
| "redflag_count": activity["redflag_count"], | |
| "active_users": activity["active_users"], | |
| "last_query": activity["last_query"], | |
| "rag_quality": rag_quality | |
| } | |
| } | |
| async def analytics_tool_usage( | |
| x_tenant_id: str = Header(None), | |
| days: int = Query(30, description="Number of days to look back"), | |
| x_user_role: str = Header("viewer") | |
| ): | |
| """ | |
| Returns how often each tool (RAG, Web, Admin, LLM) was used with detailed stats. | |
| Includes counts, latency, tokens, and success/error rates. | |
| """ | |
| if not x_tenant_id: | |
| raise HTTPException(status_code=400, detail="Missing tenant ID") | |
| require_api_permission(x_user_role, "view_analytics") | |
| # Return empty data if analytics is not configured (instead of 503) | |
| if analytics_store is None: | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "tool_usage": {}, | |
| "period_days": days | |
| } | |
| since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None | |
| tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp) | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "tool_usage": tool_usage, | |
| "period_days": days | |
| } | |
| async def analytics_redflags( | |
| x_tenant_id: str = Header(None), | |
| limit: int = Query(50, description="Maximum number of violations to return"), | |
| days: int = Query(30, description="Number of days to look back"), | |
| x_user_role: str = Header("viewer") | |
| ): | |
| """ | |
| Returns red-flag violations for this tenant. | |
| Includes rule details, severity, confidence, and timestamps. | |
| """ | |
| if not x_tenant_id: | |
| raise HTTPException(status_code=400, detail="Missing tenant ID") | |
| require_api_permission(x_user_role, "view_analytics") | |
| # Return empty data if analytics is not configured (instead of 503) | |
| if analytics_store is None: | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "redflags": [], | |
| "count": 0 | |
| } | |
| since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None | |
| redflags = analytics_store.get_redflag_violations(x_tenant_id, limit, since_timestamp) | |
| # Convert timestamps to ISO format | |
| for violation in redflags: | |
| if "timestamp" in violation: | |
| violation["timestamp_iso"] = datetime.fromtimestamp(violation["timestamp"]).isoformat() | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "redflags": redflags, | |
| "count": len(redflags) | |
| } | |
| async def analytics_activity( | |
| x_tenant_id: str = Header(None), | |
| days: int = Query(30, description="Number of days to look back"), | |
| x_user_role: str = Header("viewer") | |
| ): | |
| """ | |
| Returns general tenant activity statistics. | |
| Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization. | |
| """ | |
| if not x_tenant_id: | |
| raise HTTPException(status_code=400, detail="Missing tenant ID") | |
| require_api_permission(x_user_role, "view_analytics") | |
| # Return empty data if analytics is not configured (instead of 503) | |
| if analytics_store is None: | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "activity": { | |
| "total_queries": 0, | |
| "active_users": 0, | |
| "redflag_count": 0, | |
| "last_query": None | |
| }, | |
| "activities": [], | |
| "period_days": days | |
| } | |
| since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None | |
| activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp) | |
| # Also fetch individual activity records for heatmap visualization | |
| activities = analytics_store.get_activity_records(x_tenant_id, since_timestamp) | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "activity": activity, | |
| "activities": activities, # Individual records with timestamps for heatmap | |
| "period_days": days | |
| } | |
| async def analytics_rag_quality( | |
| x_tenant_id: str = Header(None), | |
| days: int = Query(30, description="Number of days to look back"), | |
| x_user_role: str = Header("viewer") | |
| ): | |
| """ | |
| Returns RAG quality metrics including recall/precision indicators. | |
| Includes average hits, scores, and latency. | |
| """ | |
| if not x_tenant_id: | |
| raise HTTPException(status_code=400, detail="Missing tenant ID") | |
| require_api_permission(x_user_role, "view_analytics") | |
| # Return empty data if analytics is not configured (instead of 503) | |
| if analytics_store is None: | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "rag_quality": { | |
| "total_searches": 0, | |
| "avg_hits_per_search": 0, | |
| "avg_score": 0.0, | |
| "avg_top_score": 0.0, | |
| "avg_latency_ms": 0.0 | |
| }, | |
| "period_days": days | |
| } | |
| since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None | |
| rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp) | |
| return { | |
| "tenant_id": x_tenant_id, | |
| "rag_quality": rag_quality, | |
| "period_days": days | |
| } | |