appledog00 commited on
Commit
759a1c9
·
verified ·
1 Parent(s): a43f1f0

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +178 -177
main.py CHANGED
@@ -1,178 +1,179 @@
1
- from contextlib import asynccontextmanager
2
- from fastapi import FastAPI, HTTPException
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from typing import List, Dict, Any, Optional
6
- import os
7
- import logging
8
- from datetime import datetime
9
-
10
- # Import refactored engines
11
- from recommender_core import recommender
12
- from ingestion_service import IngestionService
13
-
14
- # Set up logging
15
- logging.basicConfig(level=logging.INFO)
16
- logger = logging.getLogger(__name__)
17
-
18
- # --- LIFESPAN MANAGER ---
19
- @asynccontextmanager
20
- async def lifespan(app: FastAPI):
21
- logger.info("⏳ Starting up... RecommenderCore is ready.")
22
- yield
23
- logger.info("🛑 Shutting down...")
24
-
25
- # --- APP CONFIGURATION ---
26
- app = FastAPI(
27
- title="PPD Risk & Recommendation Engine",
28
- version="1.5",
29
- description="Advanced system with hybrid scoring, multi-field TF-IDF, and offline-first PubMed integration.",
30
- lifespan=lifespan,
31
- docs_url="/docs", # Swagger UI
32
- redoc_url="/redoc" # ReDoc alternative
33
- )
34
-
35
- # --- CORS SETUP ---
36
- app.add_middleware(
37
- CORSMiddleware,
38
- allow_origins=["*"],
39
- allow_credentials=True,
40
- allow_methods=["*"],
41
- allow_headers=["*"],
42
- )
43
-
44
- # --- DATA MODELS ---
45
- class RecommendationRequest(BaseModel):
46
- risk_level: str
47
- symptoms_text: str
48
- top_n: Optional[int] = 5
49
-
50
- class APIResponse(BaseModel):
51
- status: str
52
- risk_assessment: str
53
- recommendations: List[Dict[str, Any]]
54
-
55
- # --- API ENDPOINTS ---
56
-
57
- @app.get("/")
58
- def health_check():
59
- is_ready = recommender is not None and recommender.df is not None and not recommender.df.empty
60
- return {"status": "online", "engine_ready": is_ready, "version": "1.5"}
61
-
62
- @app.get("/api/health")
63
- def api_health():
64
- """Detailed health check for container monitoring."""
65
- try:
66
- is_ready = recommender is not None and recommender.df is not None and not recommender.df.empty
67
- db_connected = recommender.engine is not None
68
- model_loaded = recommender.vectorizer is not None and recommender.tfidf_matrix is not None
69
-
70
- return {
71
- "status": "healthy" if is_ready else "degraded",
72
- "timestamp": datetime.now().isoformat(),
73
- "checks": {
74
- "database": "ok" if db_connected else "error",
75
- "model": "ok" if model_loaded else "error",
76
- "articles_loaded": len(recommender.df) if is_ready else 0
77
- }
78
- }
79
- except Exception as e:
80
- logger.error(f"Health check failed: {e}")
81
- return {"status": "unhealthy", "error": str(e)}
82
-
83
- @app.get("/api/stats")
84
- def get_stats():
85
- """System statistics for monitoring."""
86
- try:
87
- if recommender.df is None:
88
- return {"error": "System not initialized"}
89
-
90
- stats = {
91
- "total_articles": len(recommender.df),
92
- "articles_by_type": recommender.df['format_type'].value_counts().to_dict(),
93
- "articles_by_risk": recommender.df['risk_level'].value_counts().to_dict(),
94
- "model_vocabulary_size": len(recommender.vectorizer.vocabulary_) if recommender.vectorizer else 0,
95
- "last_updated": datetime.now().isoformat()
96
- }
97
- return stats
98
- except Exception as e:
99
- logger.error(f"Stats error: {e}")
100
- raise HTTPException(status_code=500, detail=str(e))
101
-
102
- @app.post("/api/recommend", response_model=APIResponse)
103
- def get_recommendations(request: RecommendationRequest):
104
- """
105
- Main recommendation endpoint.
106
- Uses hybrid scoring: Cosine Similarity + Exact Symptom Boost + Source Weighting + Recency Boost.
107
- """
108
- try:
109
- results = recommender.recommend_articles(
110
- symptoms_text=request.symptoms_text,
111
- crisis_level=request.risk_level,
112
- top_n=request.top_n
113
- )
114
-
115
- return {
116
- "status": "success",
117
- "risk_assessment": request.risk_level,
118
- "recommendations": results
119
- }
120
-
121
- except Exception as e:
122
- logger.error(f"Recommendation error: {e}")
123
- raise HTTPException(status_code=500, detail="Internal processing error.")
124
-
125
- @app.get("/api/article/{article_id}")
126
- def get_article_content(article_id: int):
127
- """
128
- Retrieves full article content.
129
- Handles both direct contributor text and curated PubMed abstracts.
130
- """
131
- article_data = recommender.get_article_by_id(article_id)
132
-
133
- if not article_data:
134
- raise HTTPException(status_code=404, detail="Article not found")
135
-
136
- return {
137
- "article_id": article_data['article_id'],
138
- "title": article_data['title'],
139
- "category": article_data['category'],
140
- "format_type": article_data.get('format_type', 'text'),
141
- "external_url": article_data.get('external_url'),
142
- "content": article_data.get('content_raw') or article_data.get('content_clean')
143
- }
144
-
145
- @app.post("/api/admin/rebuild-model")
146
- def rebuild_model():
147
- """Admin endpoint to trigger a weighted TF-IDF rebuild."""
148
- try:
149
- service = IngestionService()
150
- service.build_tfidf_model()
151
- recommender.load_model()
152
- return {"status": "success", "message": "Weighted TF-IDF model rebuilt and reloaded."}
153
- except Exception as e:
154
- logger.error(f"Rebuild error: {e}")
155
- raise HTTPException(status_code=500, detail=str(e))
156
-
157
- @app.post("/api/admin/trigger-ingestion")
158
- def trigger_ingestion():
159
- """Admin endpoint to manually trigger PubMed ingestion."""
160
- try:
161
- service = IngestionService()
162
- articles = service.fetch_from_pubmed("postpartum depression OR maternal mental health", limit=100)
163
- if articles:
164
- count = service.store_articles(articles)
165
- service.build_tfidf_model()
166
- recommender.load_model()
167
- return {
168
- "status": "success",
169
- "message": f"Ingested {count} new articles and rebuilt model."
170
- }
171
- return {"status": "success", "message": "No new articles found."}
172
- except Exception as e:
173
- logger.error(f"Ingestion error: {e}")
174
- raise HTTPException(status_code=500, detail=str(e))
175
-
176
- if __name__ == "__main__":
177
- import uvicorn
 
178
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI, HTTPException
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from pydantic import BaseModel
5
+ from typing import List, Dict, Any, Optional
6
+ import os
7
+ import logging
8
+ from datetime import datetime
9
+
10
+ # Import refactored engines
11
+ from recommender_core import recommender
12
+ from ingestion_service import IngestionService
13
+
14
+ # Set up logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # --- LIFESPAN MANAGER ---
19
+ @asynccontextmanager
20
+ async def lifespan(app: FastAPI):
21
+ logger.info("⏳ Starting up... RecommenderCore is ready.")
22
+ yield
23
+ logger.info("🛑 Shutting down...")
24
+
25
+ # --- APP CONFIGURATION ---
26
+ app = FastAPI(
27
+ title="PPD Risk & Recommendation Engine",
28
+ version="1.5",
29
+ description="Advanced system with hybrid scoring, multi-field TF-IDF, and offline-first PubMed integration.",
30
+ lifespan=lifespan,
31
+ docs_url="/docs", # Swagger UI
32
+ redoc_url="/redoc" # ReDoc alternative
33
+ )
34
+
35
+ # --- CORS SETUP ---
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["*"],
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+ # --- DATA MODELS ---
45
+ class RecommendationRequest(BaseModel):
46
+ risk_level: str
47
+ symptoms_text: str
48
+ top_n: Optional[int] = 5
49
+
50
+ class APIResponse(BaseModel):
51
+ status: str
52
+ risk_assessment: str
53
+ recommendations: List[Dict[str, Any]]
54
+
55
+ # --- API ENDPOINTS ---
56
+
57
+ @app.get("/")
58
+ def health_check():
59
+ is_ready = recommender is not None and recommender.df is not None and not recommender.df.empty
60
+ return {"status": "online", "engine_ready": is_ready, "version": "1.5"}
61
+
62
+ @app.get("/api/health")
63
+ def api_health():
64
+ """Detailed health check for container monitoring."""
65
+ try:
66
+ is_ready = recommender is not None and recommender.df is not None and not recommender.df.empty
67
+ db_connected = recommender.engine is not None
68
+ model_loaded = recommender.vectorizer is not None and recommender.tfidf_matrix is not None
69
+
70
+ return {
71
+ "status": "healthy" if is_ready else "degraded",
72
+ "timestamp": datetime.now().isoformat(),
73
+ "checks": {
74
+ "database": "ok" if db_connected else "error",
75
+ "model": "ok" if model_loaded else "error",
76
+ "articles_loaded": len(recommender.df) if is_ready else 0
77
+ }
78
+ }
79
+ except Exception as e:
80
+ logger.error(f"Health check failed: {e}")
81
+ return {"status": "unhealthy", "error": str(e)}
82
+
83
+ @app.get("/api/stats")
84
+ def get_stats():
85
+ """System statistics for monitoring."""
86
+ try:
87
+ if recommender.df is None:
88
+ return {"error": "System not initialized"}
89
+
90
+ stats = {
91
+ "total_articles": len(recommender.df),
92
+ "articles_by_type": recommender.df['format_type'].value_counts().to_dict(),
93
+ "articles_by_risk": recommender.df['risk_level'].value_counts().to_dict(),
94
+ "model_vocabulary_size": len(recommender.vectorizer.vocabulary_) if recommender.vectorizer else 0,
95
+ "last_updated": datetime.now().isoformat()
96
+ }
97
+ return stats
98
+ except Exception as e:
99
+ logger.error(f"Stats error: {e}")
100
+ raise HTTPException(status_code=500, detail=str(e))
101
+
102
+ @app.post("/api/recommend", response_model=APIResponse)
103
+ def get_recommendations(request: RecommendationRequest):
104
+ """
105
+ Main recommendation endpoint.
106
+ Uses hybrid scoring: Cosine Similarity + Exact Symptom Boost + Source Weighting + Recency Boost.
107
+ """
108
+ try:
109
+ results = recommender.recommend_articles(
110
+ symptoms_text=request.symptoms_text,
111
+ crisis_level=request.risk_level,
112
+ top_n=request.top_n
113
+ )
114
+
115
+ return {
116
+ "status": "success",
117
+ "risk_assessment": request.risk_level,
118
+ "recommendations": results
119
+ }
120
+
121
+ except Exception as e:
122
+ logger.error(f"Recommendation error: {e}")
123
+ # Expose error for debugging during migration phase
124
+ raise HTTPException(status_code=500, detail=str(e))
125
+
126
+ @app.get("/api/article/{article_id}")
127
+ def get_article_content(article_id: int):
128
+ """
129
+ Retrieves full article content.
130
+ Handles both direct contributor text and curated PubMed abstracts.
131
+ """
132
+ article_data = recommender.get_article_by_id(article_id)
133
+
134
+ if not article_data:
135
+ raise HTTPException(status_code=404, detail="Article not found")
136
+
137
+ return {
138
+ "article_id": int(article_data['article_id']),
139
+ "title": str(article_data['title']),
140
+ "category": str(article_data['category']),
141
+ "format_type": str(article_data.get('format_type', 'text')),
142
+ "external_url": str(article_data.get('external_url')) if article_data.get('external_url') else None,
143
+ "content": str(article_data.get('content_raw') or article_data.get('content_clean'))
144
+ }
145
+
146
+ @app.post("/api/admin/rebuild-model")
147
+ def rebuild_model():
148
+ """Admin endpoint to trigger a weighted TF-IDF rebuild."""
149
+ try:
150
+ service = IngestionService()
151
+ service.build_tfidf_model()
152
+ recommender.load_model()
153
+ return {"status": "success", "message": "Weighted TF-IDF model rebuilt and reloaded."}
154
+ except Exception as e:
155
+ logger.error(f"Rebuild error: {e}")
156
+ raise HTTPException(status_code=500, detail=str(e))
157
+
158
+ @app.post("/api/admin/trigger-ingestion")
159
+ def trigger_ingestion():
160
+ """Admin endpoint to manually trigger PubMed ingestion."""
161
+ try:
162
+ service = IngestionService()
163
+ articles = service.fetch_from_pubmed("postpartum depression OR maternal mental health", limit=100)
164
+ if articles:
165
+ count = service.store_articles(articles)
166
+ service.build_tfidf_model()
167
+ recommender.load_model()
168
+ return {
169
+ "status": "success",
170
+ "message": f"Ingested {count} new articles and rebuilt model."
171
+ }
172
+ return {"status": "success", "message": "No new articles found."}
173
+ except Exception as e:
174
+ logger.error(f"Ingestion error: {e}")
175
+ raise HTTPException(status_code=500, detail=str(e))
176
+
177
+ if __name__ == "__main__":
178
+ import uvicorn
179
  uvicorn.run(app, host="0.0.0.0", port=8000)