|
|
import logging |
|
|
import os |
|
|
import time |
|
|
from typing import Dict, List |
|
|
|
|
|
from fastapi import FastAPI, HTTPException |
|
|
|
|
|
from .db import fetch_recent_transactions, log_api_hit |
|
|
from .schemas import InsightRequest, InsightResponse, Transaction |
|
|
from .services import generate_insights |
|
|
|
|
|
logging.basicConfig( |
|
|
level=os.environ.get("LOG_LEVEL", "INFO"), |
|
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s", |
|
|
) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
app = FastAPI( |
|
|
title="AI Financial Insights", |
|
|
description="AI-powered personalized spending insight generator for WalletSync.", |
|
|
version="1.0.0", |
|
|
) |
|
|
|
|
|
|
|
|
@app.get("/") |
|
|
def root() -> Dict[str, str]: |
|
|
"""Simple description endpoint.""" |
|
|
return { |
|
|
"message": "AI Financial Insights is ready. POST /insights with transactions to receive insights.", |
|
|
"docs": "/docs", |
|
|
} |
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
def health() -> Dict[str, str]: |
|
|
"""Health probe for monitoring.""" |
|
|
return {"status": "ok"} |
|
|
|
|
|
|
|
|
def _resolve_transactions(payload: InsightRequest) -> List[Transaction]: |
|
|
if payload.transactions: |
|
|
return payload.transactions |
|
|
if not payload.user_id: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail="Provide `transactions` or `user_id` to fetch them from MongoDB.", |
|
|
) |
|
|
try: |
|
|
return fetch_recent_transactions(payload.user_id) |
|
|
except ValueError as exc: |
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc |
|
|
except Exception as exc: |
|
|
logger.exception("Failed to fetch MongoDB data for user_id=%s", payload.user_id) |
|
|
raise HTTPException(status_code=502, detail="Unable to query transaction store.") from exc |
|
|
|
|
|
|
|
|
@app.post("/insights", response_model=InsightResponse) |
|
|
def create_insights(payload: InsightRequest) -> InsightResponse: |
|
|
"""Generate insights from a transaction payload.""" |
|
|
start_time = time.time() |
|
|
status = "success" |
|
|
|
|
|
user_id = payload.user_id |
|
|
|
|
|
try: |
|
|
logger.info("Received request for user_id=%s", user_id) |
|
|
transactions = _resolve_transactions(payload) |
|
|
insights, months = generate_insights(transactions) |
|
|
response = InsightResponse(user_id=user_id, insights=insights, evaluated_months=months) |
|
|
return response |
|
|
except HTTPException: |
|
|
status = "error" |
|
|
raise |
|
|
except Exception as exc: |
|
|
status = "error" |
|
|
logger.exception("Unexpected error processing insights request for user_id=%s", user_id) |
|
|
raise HTTPException(status_code=500, detail="Internal server error") from exc |
|
|
finally: |
|
|
response_time = time.time() - start_time |
|
|
try: |
|
|
|
|
|
log_api_hit(user_id, status, response_time) |
|
|
except Exception as log_exc: |
|
|
|
|
|
logger.warning("Failed to log API hit: %s", log_exc) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|