nothingworry's picture
Add Docker support and remove Ollama
0452a50
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],
)
@router.get("/overview")
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
}
}
@router.get("/tool-usage")
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
}
@router.get("/redflags")
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)
}
@router.get("/activity")
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
}
@router.get("/rag-quality")
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
}