miningniti-api / app /api /v1 /analytics.py
milan1's picture
Deploy 679d3a45 from GitHub Actions
e86dfae verified
Raw
History Blame Contribute Delete
12.4 kB
"""
Analytics API Endpoints
Dashboard statistics and mining intelligence metrics
"""
import logging
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, Query
from sqlalchemy import case, func
from sqlalchemy.orm import Session
from app.api.deps import get_current_user_id
from app.db.session import get_db
from app.models.chat import ChatMessage, ChatSession
from app.models.document import (
ComplianceStatus,
Document,
DocumentCategory,
DocumentStatus,
)
from app.schemas.analytics import (
CategoryCount,
DashboardStats,
DocumentAnalytics,
SafetyAnalytics,
SafetyDistribution,
StatusCount,
)
logger = logging.getLogger(__name__)
router = APIRouter()
import json
import redis
from app.config import settings
# Optional Redis client
try:
redis_client = redis.Redis.from_url(
settings.REDIS_URL, decode_responses=True, socket_timeout=1
)
except Exception:
redis_client = None
@router.get("/dashboard", response_model=DashboardStats)
async def get_dashboard_stats(
user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db)
):
"""
Get dashboard statistics for the current user.
Provides overview of documents, chats, and safety metrics.
"""
cache_key = f"dashboard_stats:{user_id}"
if redis_client:
try:
cached_data = redis_client.get(cache_key)
if cached_data:
return DashboardStats.model_validate_json(cached_data)
except Exception as e:
logger.warning(f"Redis cache read error: {e}")
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - timedelta(days=7)
# Document counts
doc_query = db.query(Document).filter(Document.user_id == user_id)
total_documents = doc_query.count()
processed_documents = doc_query.filter(
Document.status == DocumentStatus.COMPLETED
).count()
pending_documents = doc_query.filter(
Document.status.in_(
[
DocumentStatus.PENDING,
DocumentStatus.PROCESSING,
DocumentStatus.ANALYZING,
]
)
).count()
failed_documents = doc_query.filter(
Document.status == DocumentStatus.FAILED
).count()
# Today and week counts
docs_today = doc_query.filter(Document.created_at >= today_start).count()
docs_week = doc_query.filter(Document.created_at >= week_start).count()
# Chat counts
total_sessions = (
db.query(ChatSession).filter(ChatSession.user_id == user_id).count()
)
total_messages = (
db.query(ChatMessage)
.join(ChatSession)
.filter(ChatSession.user_id == user_id)
.count()
)
# Safety metrics
safety_stats = (
db.query(
func.avg(Document.safety_score).label("avg_score"),
func.count(case((Document.hazards_detected.isnot(None), 1))).label(
"with_hazards"
),
func.count(
case((Document.compliance_status == ComplianceStatus.VIOLATION, 1))
).label("violations"),
func.count(
case((Document.compliance_status == ComplianceStatus.WARNING, 1))
).label("warnings"),
)
.filter(
Document.user_id == user_id, Document.status == DocumentStatus.COMPLETED
)
.first()
)
# Category breakdown
category_counts = (
db.query(Document.category, func.count(Document.id).label("count"))
.filter(Document.user_id == user_id, Document.category.isnot(None))
.group_by(Document.category)
.all()
)
total_categorized = sum(c.count for c in category_counts)
categories = [
CategoryCount(
category=cat.value if cat else "other",
count=count,
percentage=(
round(count / total_categorized * 100, 1)
if total_categorized > 0
else 0
),
)
for cat, count in category_counts
]
# Last activity
last_doc = doc_query.order_by(Document.created_at.desc()).first()
last_chat = (
db.query(ChatSession)
.filter(ChatSession.user_id == user_id)
.order_by(ChatSession.updated_at.desc())
.first()
)
stats = DashboardStats(
total_documents=total_documents,
processed_documents=processed_documents,
pending_documents=pending_documents,
failed_documents=failed_documents,
total_chat_sessions=total_sessions,
total_messages=total_messages,
average_safety_score=(
round(safety_stats.avg_score, 1)
if safety_stats and safety_stats.avg_score
else None
),
documents_with_hazards=safety_stats.with_hazards if safety_stats else 0,
compliance_violations=safety_stats.violations if safety_stats else 0,
compliance_warnings=safety_stats.warnings if safety_stats else 0,
documents_processed_today=docs_today,
documents_processed_this_week=docs_week,
documents_by_category=categories,
last_upload_at=last_doc.created_at if last_doc else None,
last_chat_at=last_chat.updated_at if last_chat else None,
)
if redis_client:
try:
# Cache for 60 seconds
redis_client.setex(cache_key, 60, stats.model_dump_json())
except Exception as e:
logger.warning(f"Redis cache write error: {e}")
return stats
@router.get("/documents", response_model=DocumentAnalytics)
async def get_document_analytics(
days: int = Query(30, ge=1, le=365),
user_id: str = Depends(get_current_user_id),
db: Session = Depends(get_db),
):
"""
Get detailed document analytics over time.
"""
start_date = datetime.utcnow() - timedelta(days=days)
# Uploads by day
uploads = (
db.query(
func.date(Document.created_at).label("date"),
func.count(Document.id).label("count"),
)
.filter(Document.user_id == user_id, Document.created_at >= start_date)
.group_by(func.date(Document.created_at))
.order_by("date")
.all()
)
uploads_by_day = [{"date": str(d), "count": c} for d, c in uploads]
# Category distribution
categories = (
db.query(Document.category, func.count(Document.id).label("count"))
.filter(Document.user_id == user_id, Document.category.isnot(None))
.group_by(Document.category)
.all()
)
total = sum(c for _, c in categories)
by_category = [
CategoryCount(
category=cat.value if cat else "other",
count=count,
percentage=round(count / total * 100, 1) if total > 0 else 0,
)
for cat, count in categories
]
# Status distribution
statuses = (
db.query(Document.status, func.count(Document.id).label("count"))
.filter(Document.user_id == user_id)
.group_by(Document.status)
.all()
)
by_status = [
StatusCount(status=s.value if s else "unknown", count=c) for s, c in statuses
]
# File type distribution
file_types = (
db.query(Document.file_type, func.count(Document.id).label("count"))
.filter(Document.user_id == user_id)
.group_by(Document.file_type)
.all()
)
by_file_type = [{"type": ft, "count": c} for ft, c in file_types]
return DocumentAnalytics(
uploads_by_day=uploads_by_day,
by_category=by_category,
by_status=by_status,
by_file_type=by_file_type,
)
@router.get("/safety", response_model=SafetyAnalytics)
async def get_safety_analytics(
user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db)
):
"""
Get safety compliance analytics.
"""
# Base query for completed documents with safety scores
base = db.query(Document).filter(
Document.user_id == user_id,
Document.status == DocumentStatus.COMPLETED,
Document.safety_score.isnot(None),
)
# Score statistics
stats = (
db.query(
func.avg(Document.safety_score).label("avg"),
func.min(Document.safety_score).label("min"),
func.max(Document.safety_score).label("max"),
)
.filter(Document.user_id == user_id, Document.safety_score.isnot(None))
.first()
)
# Score distribution
ranges = [
("0-25", 0, 25),
("26-50", 26, 50),
("51-75", 51, 75),
("76-100", 76, 100),
]
total_with_scores = base.count()
distribution = []
for label, low, high in ranges:
count = base.filter(
Document.safety_score >= low, Document.safety_score <= high
).count()
distribution.append(
SafetyDistribution(
range=label,
count=count,
percentage=(
round(count / total_with_scores * 100, 1)
if total_with_scores > 0
else 0
),
)
)
# Compliance counts
compliant = (
db.query(Document)
.filter(
Document.user_id == user_id,
Document.compliance_status == ComplianceStatus.COMPLIANT,
)
.count()
)
warnings = (
db.query(Document)
.filter(
Document.user_id == user_id,
Document.compliance_status == ComplianceStatus.WARNING,
)
.count()
)
violations = (
db.query(Document)
.filter(
Document.user_id == user_id,
Document.compliance_status == ComplianceStatus.VIOLATION,
)
.count()
)
return SafetyAnalytics(
average_safety_score=round(stats.avg, 1) if stats.avg else 0,
min_safety_score=stats.min,
max_safety_score=stats.max,
score_distribution=distribution,
compliant_count=compliant,
warning_count=warnings,
violation_count=violations,
)
@router.get("/violations")
async def get_recent_violations(
limit: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id),
db: Session = Depends(get_db),
):
"""
Get recent violations and warnings from analyzed documents.
Returns documents with detected hazards, ordered by severity and recency.
"""
# Get documents with violations or warnings that have hazards
docs = (
db.query(Document)
.filter(
Document.user_id == user_id,
Document.status == DocumentStatus.COMPLETED,
Document.compliance_status.in_(
[ComplianceStatus.VIOLATION, ComplianceStatus.WARNING]
),
Document.hazards_detected.isnot(None),
)
.order_by(Document.created_at.desc())
.limit(limit)
.all()
)
violations = []
for doc in docs:
hazards = doc.hazards_detected or []
if isinstance(hazards, list):
for h in hazards[:3]: # Max 3 hazards per doc
if isinstance(h, dict):
violations.append(
{
"document_id": str(doc.id),
"document_title": doc.title,
"file_name": doc.file_name,
"hazard_type": h.get("type", "Unknown Hazard"),
"severity": h.get("severity", "medium"),
"description": h.get("description", ""),
"regulation": h.get("regulation", ""),
"detected_at": (
doc.processed_at.isoformat()
if doc.processed_at
else doc.created_at.isoformat()
),
"compliance_status": (
doc.compliance_status.value
if doc.compliance_status
else "warning"
),
}
)
return {
"violations": violations,
"total": len(violations),
}