""" 🧠 AUTONOMOUS BRAIN API - DataVision Auto-Analysis Endpoint ============================================================ Drop ANY file → Get complete analysis in seconds. Endpoints: - POST /auto-analyze - Analyze uploaded file - GET /brain/status - Get brain status - POST /brain/insights - Generate AI insights """ import logging from typing import Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Header from pydantic import BaseModel, Field import pandas as pd import io logger = logging.getLogger(__name__) router = APIRouter() # ============================================================================= # SECURITY HELPER - JWT Authentication # ============================================================================= def get_secure_user_id(body_user_id: str, x_user_id: Optional[str], authorization: Optional[str]) -> str: """ Get verified user_id from JWT token or headers. Priority: JWT token > X-User-ID header > Body data """ # 1. Try JWT token first (most secure) if authorization: try: token = authorization.replace("Bearer ", "") from core.auth import decode_jwt_token payload = decode_jwt_token(token) if payload and payload.get("sub"): return payload["sub"] except Exception as e: logger.debug(f"JWT decode failed: {e}") # 2. Try X-User-ID header (from authenticated frontend) if x_user_id and x_user_id != "default": return x_user_id # 3. Fallback to body data (least secure) if body_user_id and body_user_id != "default": logger.warning(f"Using body user_id: {body_user_id} - consider using JWT") return body_user_id # 4. Generate guest fingerprint import hashlib import time return f"guest_{hashlib.md5(str(time.time()).encode()).hexdigest()[:8]}" # ============================================================================= # REQUEST/RESPONSE MODELS # ============================================================================= class AnalysisRequest(BaseModel): """Request for analysis""" user_id: str file_name: Optional[str] = "data" generate_insights: bool = True class ColumnInfo(BaseModel): """Column information""" name: str type: str null_percentage: float unique_count: int issues: list = [] fix_suggestions: list = [] stats: Optional[dict] = None top_values: Optional[dict] = None class RelationshipInfo(BaseModel): """Relationship between columns""" column1: str column2: str type: str strength: float description: str class InsightInfo(BaseModel): """AI-generated insight""" title: str description: str importance: str category: str visualization: Optional[str] = None class AnalysisResponse(BaseModel): """Response from brain analysis""" success: bool file_name: str analysis_time_ms: int # Overview total_rows: int total_columns: int memory_mb: float # Quality quality_score: float quality_level: str quality_issues: list # Details columns: list relationships: list insights: list suggested_charts: list # Summary summary: str class QuickInsightRequest(BaseModel): """Request for quick insights""" user_id: str question: str context: Optional[str] = None # ============================================================================= # ENDPOINTS # ============================================================================= @router.post("/auto-analyze", response_model=AnalysisResponse) async def auto_analyze_file( file: UploadFile = File(...), user_id: str = "default", x_user_id: Optional[str] = Header(None, alias="X-User-ID"), authorization: Optional[str] = Header(None, alias="Authorization") ): """ 🚀 Drop ANY file and get complete autonomous analysis - SECURED Supports: CSV, Excel (.xlsx, .xls), JSON Returns: - Complete data profile - Quality score with fix suggestions - Column relationships - AI-generated insights - Visualization recommendations """ try: # SECURITY: Get verified user_id from JWT secure_user_id = get_secure_user_id(user_id, x_user_id, authorization) # Read file content content = await file.read() file_name = file.filename or "uploaded_data" # Detect file type and load DataFrame df = await _load_dataframe(content, file_name) if df is None or len(df) == 0: raise HTTPException(status_code=400, detail="Could not parse file or file is empty") # Import brain and analyze from core.autonomous_brain import get_brain brain = get_brain() analysis = await brain.analyze(df, file_name, generate_insights=True) result = brain.to_dict(analysis) return AnalysisResponse( success=True, file_name=result["file_name"], analysis_time_ms=result["analysis_duration_ms"], total_rows=result["total_rows"], total_columns=result["total_columns"], memory_mb=result["memory_usage_mb"], quality_score=result["quality_score"], quality_level=result["quality_level"], quality_issues=result["quality_issues"], columns=result["columns"], relationships=result["relationships"], insights=result["insights"], suggested_charts=result["suggested_charts"], summary=result["summary"] ) except HTTPException: raise except Exception as e: logger.error(f"Auto-analyze error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/analyze-dataframe") async def analyze_existing_data( request: AnalysisRequest, x_user_id: Optional[str] = Header(None, alias="X-User-ID"), authorization: Optional[str] = Header(None, alias="Authorization") ): """ Analyze data already uploaded by user - SECURED """ try: # SECURITY: Get verified user_id from JWT secure_user_id = get_secure_user_id(request.user_id, x_user_id, authorization) from utils.paths import get_user_paths import os paths = get_user_paths(secure_user_id) # Find user's data file data_file = None for ext in ['.csv', '.xlsx', '.json']: potential_file = os.path.join(paths["uploads"], f"data{ext}") if os.path.exists(potential_file): data_file = potential_file break if not data_file: # Look for any file if os.path.exists(paths["uploads"]): files = os.listdir(paths["uploads"]) if files: data_file = os.path.join(paths["uploads"], files[0]) if not data_file: raise HTTPException(status_code=404, detail="No data found. Please upload a file first.") # Load DataFrame df = await _load_dataframe_from_path(data_file) if df is None: raise HTTPException(status_code=400, detail="Could not load data file") # Analyze from core.autonomous_brain import get_brain brain = get_brain() analysis = await brain.analyze(df, request.file_name, request.generate_insights) result = brain.to_dict(analysis) return { "success": True, **result } except HTTPException: raise except Exception as e: logger.error(f"Analyze dataframe error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/quick-insight") async def get_quick_insight(request: QuickInsightRequest): """ Get quick AI insight about a specific question """ try: from core.reasoning_engine import reason, ReasoningMode result = await reason( query=request.question, context=request.context or "", mode=ReasoningMode.CHAIN_OF_THOUGHT ) return { "success": True, "question": request.question, "answer": result.final_answer, "confidence": result.confidence, "reasoning_steps": [ {"type": step.step_type, "content": step.content} for step in result.steps ], "reasoning_time_ms": result.reasoning_time_ms } except Exception as e: logger.error(f"Quick insight error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/root-cause") async def analyze_root_cause( user_id: str, target_column: str, question: str, time_column: Optional[str] = None ): """ Perform root cause analysis on data """ try: # Load user's data df = await _load_user_data(user_id) if df is None: raise HTTPException(status_code=404, detail="No data found") if target_column not in df.columns: raise HTTPException(status_code=400, detail=f"Column '{target_column}' not found") from mcp.advanced_mcps import analyze_root_cause result = await analyze_root_cause(df, target_column, question, time_column) return {"success": True, **result} except HTTPException: raise except Exception as e: logger.error(f"Root cause error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/segment") async def segment_data( user_id: str, n_segments: Optional[int] = None, features: Optional[str] = None # Comma-separated ): """ Segment data using AI clustering """ try: df = await _load_user_data(user_id) if df is None: raise HTTPException(status_code=404, detail="No data found") feature_list = features.split(",") if features else None from mcp.advanced_mcps import segment_data result = await segment_data(df, feature_list, n_segments) return {"success": True, **result} except HTTPException: raise except Exception as e: logger.error(f"Segmentation error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/trends") async def detect_trends( user_id: str, time_column: Optional[str] = None ): """ Detect trends and anomalies in data """ try: df = await _load_user_data(user_id) if df is None: raise HTTPException(status_code=404, detail="No data found") from mcp.advanced_mcps import detect_trends result = await detect_trends(df, time_column) return {"success": True, **result} except HTTPException: raise except Exception as e: logger.error(f"Trend detection error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/brain/status") async def get_brain_status(): """ Get the status of the Autonomous Brain """ try: from core.autonomous_brain import get_brain brain = get_brain() return { "status": "active", "cached_analyses": len(brain.analysis_cache), "version": "1.0.0", "capabilities": [ "auto_profiling", "quality_scoring", "relationship_discovery", "ai_insights", "visualization_suggestions", "root_cause_analysis", "segmentation", "trend_detection" ] } except Exception as e: logger.error(f"Brain status error: {e}") return {"status": "error", "error": str(e)} # ============================================================================= # HELPER FUNCTIONS # ============================================================================= async def _load_dataframe(content: bytes, filename: str) -> Optional[pd.DataFrame]: """Load DataFrame from file content""" try: filename_lower = filename.lower() if filename_lower.endswith('.csv'): return pd.read_csv(io.BytesIO(content)) elif filename_lower.endswith(('.xlsx', '.xls')): return pd.read_excel(io.BytesIO(content)) elif filename_lower.endswith('.json'): return pd.read_json(io.BytesIO(content)) else: # Try CSV as default return pd.read_csv(io.BytesIO(content)) except Exception as e: logger.error(f"Error loading DataFrame: {e}") return None async def _load_dataframe_from_path(path: str) -> Optional[pd.DataFrame]: """Load DataFrame from file path""" try: path_lower = path.lower() if path_lower.endswith('.csv'): return pd.read_csv(path) elif path_lower.endswith(('.xlsx', '.xls')): return pd.read_excel(path) elif path_lower.endswith('.json'): return pd.read_json(path) else: return pd.read_csv(path) except Exception as e: logger.error(f"Error loading DataFrame from path: {e}") return None async def _load_user_data(user_id: str) -> Optional[pd.DataFrame]: """Load user's uploaded data""" try: from utils.paths import get_user_paths import os paths = get_user_paths(user_id) uploads_dir = paths.get("uploads", "") if not os.path.exists(uploads_dir): return None # Find first data file for filename in os.listdir(uploads_dir): filepath = os.path.join(uploads_dir, filename) if os.path.isfile(filepath): return await _load_dataframe_from_path(filepath) return None except Exception as e: logger.error(f"Error loading user data: {e}") return None