from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from pydantic import BaseModel from typing import List, Optional from app.database.database import get_db from app.database.models import User from app.middleware.cache import cache # Import AI helper engines from app.ai.forecasting import predict_future_balance, forecast_savings_and_investments, simulate_future_scenarios from app.ai.simulation import simulate_purchase_impact, simulate_investment_impact, simulate_subscription_cancellation from app.ai.behavior import analyze_spending_behavior from app.ai.coaching import calculate_financial_health_score, generate_daily_briefing from app.ai.subscriptions import analyze_subscriptions from app.ai.fraud import evaluate_transaction_for_fraud, get_user_fraud_alerts from app.ai.chat import get_chat_response, chat_memory from app.ai.intelligence import generate_weekly_coaching, explain_fraud_alert, generate_spending_narrative router = APIRouter(prefix="/api/ai", tags=["AI Intelligence"]) # Fallback helper to retrieve a valid user ID for demonstration def get_user_id_fallback(db: Session, user_id: Optional[str] = None) -> str: if user_id: return user_id user = db.query(User).first() if not user: raise HTTPException(status_code=404, detail="No users found in database. Please seed the database first.") return user.id # Pydantic Schemas for input class PurchaseRequest(BaseModel): amount: float merchant: str category: str class InvestmentRequest(BaseModel): monthly_sip: float asset_type: str lump_sum: float = 0.0 class SubscriptionSimulationRequest(BaseModel): subscription_ids: List[str] class ChatMessageRequest(BaseModel): message: str session_id: Optional[str] = None language: str = "English" class ChatSessionCreateRequest(BaseModel): title: Optional[str] = "New chat" # ─── FINANCIAL TWIN FORECASTS ────────────────────────────────────────────────── @router.get("/twin/predict") def get_twin_predict(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:twin:predict:{uid}" cached = cache.get(cache_key) if cached: return cached result = predict_future_balance(db, uid) cache.set(cache_key, result, ttl=300) # cache for 5 minutes return result @router.get("/twin/future") def get_twin_future(user_id: Optional[str] = None, months: int = Query(default=12, ge=1, le=60), db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:twin:future:{uid}:{months}" cached = cache.get(cache_key) if cached: return cached result = forecast_savings_and_investments(db, uid, months) cache.set(cache_key, result, ttl=300) return result @router.get("/twin/scenarios") def get_twin_scenarios(user_id: Optional[str] = None, months: int = Query(default=6, ge=1, le=24), db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:twin:scenarios:{uid}:{months}" cached = cache.get(cache_key) if cached: return cached result = simulate_future_scenarios(db, uid, months) cache.set(cache_key, result, ttl=300) return result # ─── SIMULATION ENDPOINTS ────────────────────────────────────────────────────── @router.post("/simulate/purchase") def post_simulate_purchase(req: PurchaseRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) return simulate_purchase_impact(db, uid, req.amount, req.category, req.merchant) @router.post("/simulate/investment") def post_simulate_investment(req: InvestmentRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) return simulate_investment_impact(db, uid, req.monthly_sip, req.asset_type, req.lump_sum) @router.post("/simulate/subscription") def post_simulate_subscription(req: SubscriptionSimulationRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) return simulate_subscription_cancellation(db, uid, req.subscription_ids) # ─── BEHAVIORAL ANALYTICS ───────────────────────────────────────────────────── @router.get("/behavior/insights") def get_behavior_insights(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:behavior:insights:{uid}" cached = cache.get(cache_key) if cached: return cached result = analyze_spending_behavior(db, uid) cache.set(cache_key, result, ttl=600) # cache for 10 minutes return result # ─── COACHING & BRIEFINGS ───────────────────────────────────────────────────── @router.get("/coaching/briefing") def get_coaching_briefing(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) # Cache briefings for 1 hour to prevent excessive LLM costs cache_key = f"ai:coaching:briefing:{uid}" cached = cache.get(cache_key) if cached: return cached result = generate_daily_briefing(db, uid) cache.set(cache_key, result, ttl=3600) return result @router.get("/coaching/score") def get_coaching_score(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:coaching:score:{uid}" cached = cache.get(cache_key) if cached: return cached result = calculate_financial_health_score(db, uid) cache.set(cache_key, result, ttl=600) return result # ─── SUBSCRIPTION OPTIMIZATION ──────────────────────────────────────────────── @router.get("/subscriptions/optimize") def get_subscriptions_optimize(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) cache_key = f"ai:subs:optimize:{uid}" cached = cache.get(cache_key) if cached: return cached result = analyze_subscriptions(db, uid) cache.set(cache_key, result, ttl=600) return result # ─── FRAUD & SECURITY ───────────────────────────────────────────────────────── @router.get("/fraud/analysis") def get_fraud_analysis(user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) return get_user_fraud_alerts(db, uid) @router.post("/fraud/evaluate/{transaction_id}") def post_fraud_evaluate(transaction_id: str, db: Session = Depends(get_db)): return evaluate_transaction_for_fraud(db, transaction_id) # ─── CONTEXTUAL CHAT ENDPOINT ────────────────────────────────────────────────── @router.get("/chat/sessions") def list_chat_sessions( user_id: Optional[str] = None, limit: int = Query(30, ge=1, le=50), db: Session = Depends(get_db), ): uid = get_user_id_fallback(db, user_id) sessions = chat_memory.list_sessions(db, uid, limit=limit) return {"sessions": sessions, "count": len(sessions)} @router.post("/chat/sessions") def create_chat_session( body: ChatSessionCreateRequest, user_id: Optional[str] = None, db: Session = Depends(get_db), ): uid = get_user_id_fallback(db, user_id) session = chat_memory.create_session(db, uid, title=body.title or "New chat") return { "id": session.id, "title": session.title, "created_at": session.created_at.isoformat() if session.created_at else None, "updated_at": session.updated_at.isoformat() if session.updated_at else None, "message_count": 0, "preview": "", } @router.delete("/chat/sessions/{session_id}") def delete_chat_session( session_id: str, user_id: Optional[str] = None, db: Session = Depends(get_db), ): uid = get_user_id_fallback(db, user_id) if not chat_memory.delete_session(db, uid, session_id): raise HTTPException(status_code=404, detail="Chat session not found") return {"ok": True, "message": "Chat deleted"} @router.get("/chat/history") def get_chat_history( session_id: str = Query(..., description="Chat session ID"), user_id: Optional[str] = None, limit: int = Query(100, ge=1, le=200), db: Session = Depends(get_db), ): uid = get_user_id_fallback(db, user_id) if not chat_memory.get_session(db, uid, session_id): raise HTTPException(status_code=404, detail="Chat session not found") messages = chat_memory.list_messages(db, session_id, limit=limit) return {"session_id": session_id, "messages": messages, "count": len(messages)} @router.delete("/chat/history") def delete_chat_history( session_id: str = Query(..., description="Chat session ID"), user_id: Optional[str] = None, db: Session = Depends(get_db), ): uid = get_user_id_fallback(db, user_id) if not chat_memory.clear_session_messages(db, uid, session_id): raise HTTPException(status_code=404, detail="Chat session not found") return {"ok": True, "message": "Conversation cleared", "session_id": session_id} @router.post("/chat") def post_chat(req: ChatMessageRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): uid = get_user_id_fallback(db, user_id) response_msg, session_id = get_chat_response(db, uid, req.message, req.session_id, req.language) return {"response": response_msg, "session_id": session_id} # ─── FINANCIAL COACH MODE ────────────────────────────────────────────────────── @router.get("/coach/weekly") def get_weekly_coaching(user_id: Optional[str] = None, db: Session = Depends(get_db)): """ Proactive weekly coaching report: summary, budget coaching, savings nudge, anomaly explanations, and top 3 actions — all grounded in real account data. """ uid = get_user_id_fallback(db, user_id) cache_key = f"ai:coach:weekly:{uid}" cached = cache.get(cache_key) if cached: return cached result = generate_weekly_coaching(db, uid) cache.set(cache_key, result, ttl=3600) # cache 1 hour return result # ─── AI FRAUD EXPLANATION ────────────────────────────────────────────────────── @router.get("/fraud/explain/{fraud_log_id}") def get_fraud_explanation(fraud_log_id: str, db: Session = Depends(get_db)): """ Returns a human-readable AI explanation of exactly why a specific transaction was flagged, with context about the user's normal patterns. """ return explain_fraud_alert(db, fraud_log_id) # ─── SPENDING NARRATIVE ──────────────────────────────────────────────────────── @router.get("/narrative/monthly") def get_monthly_narrative(user_id: Optional[str] = None, db: Session = Depends(get_db)): """ Monthly spending narrative: what changed, what improved, what to watch, investment insights, and a one-paragraph human story of the month. """ uid = get_user_id_fallback(db, user_id) cache_key = f"ai:narrative:monthly:{uid}" cached = cache.get(cache_key) if cached: return cached result = generate_spending_narrative(db, uid) cache.set(cache_key, result, ttl=1800) # cache 30 min return result