recommendation / app.py
sundaram22verma's picture
Bug fix
ca2bd00
import logging
import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from src.core.recommender import recommender
from src.database.mongodb import mongodb
from src.config.settings import API_TITLE, API_DESCRIPTION, API_VERSION
logger = logging.getLogger(__name__)
# Configure basic logging if not already set up elsewhere
if not logger.hasHandlers():
logging.basicConfig(level=logging.INFO)
# Create a new FastAPI app instance instead of importing from src.main
app = FastAPI(
title=API_TITLE,
description=API_DESCRIPTION,
version=API_VERSION
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize recommender on startup
@app.on_event("startup")
async def startup_event():
"""Initialize recommender system on startup."""
try:
logger.info("Initializing recommender system...")
recommender.load_components()
app.state.recommender = recommender
logger.info("Recommender system initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize recommender system: {e}", exc_info=True)
# Don't raise here to allow the app to start even if recommender fails
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup on shutdown."""
try:
mongodb.close()
logger.info("MongoDB connection closed")
except Exception as e:
logger.error(f"Error during shutdown: {e}", exc_info=True)
# Pydantic models for request/response bodies
class FeedbackPayload(BaseModel):
user_id: str
msid: str
clicked_msid: str # Comma-separated string of MSIDs
k: int = Field(default=5, ge=1, le=10)
class FeedbackResponse(BaseModel):
message: str
# API Endpoints
@app.get("/health")
async def health_check():
"""
Health check endpoint to diagnose system status.
"""
health_status = {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"components": {}
}
# Check recommender system
if hasattr(app.state, "recommender") and app.state.recommender is not None:
health_status["components"]["recommender"] = {
"status": "available",
"models_loaded": {
"embed_model": app.state.recommender.embed_model is not None,
"reranker": app.state.recommender.reranker is not None,
"generator": app.state.recommender.generator is not None
},
"data_available": app.state.recommender.df is not None and not app.state.recommender.df.empty,
"faiss_index_available": app.state.recommender.index is not None,
"faiss_vectors": app.state.recommender.index.ntotal if app.state.recommender.index else 0
}
else:
health_status["components"]["recommender"] = {"status": "not_available"}
health_status["status"] = "degraded"
# Check MongoDB connection
try:
if mongodb.db is not None:
# Try a simple operation to test connection
mongodb.db.command("ping")
health_status["components"]["mongodb"] = {"status": "connected"}
else:
health_status["components"]["mongodb"] = {"status": "not_connected"}
health_status["status"] = "degraded"
except Exception as e:
health_status["components"]["mongodb"] = {
"status": "error",
"error": str(e)
}
health_status["status"] = "degraded"
return health_status
@app.get("/recommendations/")
async def get_recommendations_api(query: str, k: int = 5):
"""
Get recommendations based on a textual query.
"""
try:
if not hasattr(app.state, "recommender") or app.state.recommender is None:
logger.error("Recommender is not available.")
raise HTTPException(status_code=503, detail="Recommender service not available")
response = app.state.recommender.get_recommendations(query, k)
return response
except Exception as e:
logger.error(f"API Error in get_recommendations_api for query '{query}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
@app.get("/recommendations/msid/")
async def get_recommendations_by_id_api(msid: str, k: int = 5):
"""
Get recommendations based on a given MSID.
"""
try:
# Validate input parameters
if not msid or not isinstance(msid, str):
raise HTTPException(status_code=400, detail="Invalid MSID provided")
if not isinstance(k, int) or k < 1 or k > 10:
raise HTTPException(status_code=400, detail="k must be an integer between 1 and 10")
# Check if recommender service is available
if not hasattr(app.state, "recommender") or app.state.recommender is None:
logger.error("Recommender is not available.")
raise HTTPException(status_code=503, detail="Recommender service not available")
# Check if recommender has the necessary data
if app.state.recommender.df is None or app.state.recommender.df.empty:
logger.error("Recommender data not available (MongoDB connection issue).")
raise HTTPException(
status_code=503,
detail="Recommender data not available. The service is currently unable to access the required data."
)
# Get recommendations with error handling
try:
response = app.state.recommender.get_recommendations_by_id(msid, k)
if not response:
raise HTTPException(status_code=404, detail=f"No recommendations found for MSID: {msid}")
return response
except ValueError as ve:
logger.error(f"Value error in get_recommendations_by_id for msid '{msid}': {ve}")
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(f"Error getting recommendations for msid '{msid}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error while getting recommendations")
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Unexpected error in get_recommendations_by_id_api for msid '{msid}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail="An unexpected error occurred")
@app.post("/recommendations/feedback/user/", response_model=FeedbackResponse)
async def submit_user_feedback_api(payload: FeedbackPayload):
"""
Submit user feedback (e.g., clicked articles) and save it.
Optionally, this endpoint can also trigger re-computation of recommendations based on feedback,
though the primary response here is the status of feedback submission.
"""
try:
if not hasattr(app.state, "recommender") or app.state.recommender is None:
logger.error("Recommender is not available.")
raise HTTPException(status_code=503, detail="Recommender service not available")
# (Optional) Compute recommendations based on feedback, similar to Gradio function.
# The result of this call is not the primary output of this API endpoint.
try:
_ = app.state.recommender.get_recommendations_user_feedback(
payload.user_id, payload.msid, payload.clicked_msid, payload.k
)
logger.info(f"API: (Computed recommendations for user '{payload.user_id}' based on click, not part of this response)")
except Exception as e:
logger.warning(f"Could not compute recommendations based on feedback: {e}")
# Save feedback to MongoDB (optional - only if MongoDB is available)
try:
actual_clicked_msids = [s.strip() for s in payload.clicked_msid.split(',') if s.strip()]
if not actual_clicked_msids:
logger.warning(f"API: Invalid clicked_msid: '{payload.clicked_msid}' for user '{payload.user_id}'")
raise HTTPException(status_code=400, detail="clicked_msid parameter is invalid or does not contain valid MSIDs.")
logger.info(
f"API: Saving feedback for user '{payload.user_id}', context msid: '{payload.msid}', clicked msids: {actual_clicked_msids}"
)
feedback_collection_name = "user_feedback_tracking"
# Check if MongoDB is available
if mongodb.db is None:
logger.warning("MongoDB database connection is not available. Skipping feedback storage.")
return FeedbackResponse(message="Response processed successfully (feedback storage unavailable)")
feedback_collection = mongodb.db[feedback_collection_name]
user_doc = feedback_collection.find_one({"user_id": payload.user_id})
if user_doc:
feedback_collection.update_one(
{"user_id": payload.user_id},
{"$addToSet": {"Articles": {"msid": payload.msid, "Read": actual_clicked_msids}}}
)
else:
feedback_collection.insert_one({
"user_id": payload.user_id,
"Articles": [{"msid": payload.msid, "Read": actual_clicked_msids}]
})
logger.info(f"API: Successfully saved feedback for user '{payload.user_id}'")
return FeedbackResponse(message="Response saved successfully")
except Exception as e:
logger.error(f"Error saving feedback to MongoDB: {e}", exc_info=True)
# Don't fail the entire request if MongoDB is unavailable
return FeedbackResponse(message="Response processed successfully (feedback storage failed)")
except HTTPException:
raise # Re-raise HTTPException directly
except Exception as e:
logger.error(f"API Error in submit_user_feedback_api for user '{payload.user_id}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
@app.get("/recommendations/summary/")
async def get_recommendations_summary_api(msid: str, k: int = 5, summary: bool = True, smart_tip: bool = True):
"""
Get recommendations with optional summary and smart tip for a given MSID.
"""
try:
if not hasattr(app.state, "recommender") or app.state.recommender is None:
logger.error("Recommender is not available.")
raise HTTPException(status_code=503, detail="Recommender service not available")
try:
response = app.state.recommender.get_recommendations_summary(msid, k, summary, smart_tip)
except RuntimeError as e:
# Catch the meta tensor error and return a fallback
if "meta tensor" in str(e):
logger.error("Summary model error: %s", e)
response = {
"msid": msid,
"recommendations": [],
"summary": [],
"smart_tip": [],
"error": "Summary model is not available on this server."
}
else:
raise
return response
except Exception as e:
logger.error(f"API Error in get_recommendations_summary_api for msid '{msid}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)