Spaces:
Sleeping
Sleeping
File size: 11,873 Bytes
9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 60572ef ca2bd00 9d76e23 ca2bd00 60572ef ca2bd00 60572ef 9d76e23 60572ef 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 ca2bd00 9d76e23 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
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)
|