Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,248 +1,248 @@
|
|
1 |
-
import asyncio
|
2 |
-
import os
|
3 |
-
from datetime import datetime
|
4 |
-
from typing import Dict, List, Optional
|
5 |
-
|
6 |
-
from dotenv import load_dotenv
|
7 |
-
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, status
|
8 |
-
from fastapi.middleware.cors import CORSMiddleware
|
9 |
-
from loguru import logger
|
10 |
-
|
11 |
-
# --- UPDATED IMPORTS ---
|
12 |
-
from analysis_service import AnalysisService
|
13 |
-
# No longer need RssTwitterService
|
14 |
-
from models import (ConflictAnalysis, HealthCheck, SubredditSource, TensionLevel,
|
15 |
-
UpdateRequest)
|
16 |
-
|
17 |
-
# Load environment variables from .env file
|
18 |
-
load_dotenv()
|
19 |
-
|
20 |
-
# Configure logging
|
21 |
-
os.makedirs("logs", exist_ok=True)
|
22 |
-
logger.add("logs/app.log", rotation="500 MB", level=os.getenv("LOG_LEVEL", "INFO"))
|
23 |
-
|
24 |
-
# Global readiness flag
|
25 |
-
app_ready = False
|
26 |
-
|
27 |
-
# Create FastAPI application
|
28 |
-
app = FastAPI(
|
29 |
-
title="WesternFront API",
|
30 |
-
description="AI-powered conflict tracker for monitoring India-Pakistan tensions using Reddit data",
|
31 |
-
version="2.0.0" # Version bump for new data source
|
32 |
-
)
|
33 |
-
|
34 |
-
# Add CORS middleware
|
35 |
-
app.add_middleware(
|
36 |
-
CORSMiddleware,
|
37 |
-
allow_origins=["*"], # Adjust this for production
|
38 |
-
allow_credentials=True,
|
39 |
-
allow_methods=["*"],
|
40 |
-
allow_headers=["*"],
|
41 |
-
)
|
42 |
-
|
43 |
-
# --- UPDATED: Services ---
|
44 |
-
# The AnalysisService now manages the RedditService internally
|
45 |
-
analysis_service = AnalysisService()
|
46 |
-
|
47 |
-
# In-memory store for latest analysis
|
48 |
-
latest_analysis: Optional[ConflictAnalysis] = None
|
49 |
-
last_update_time: Optional[datetime] = None
|
50 |
-
|
51 |
-
|
52 |
-
async def get_analysis_service() -> AnalysisService:
|
53 |
-
"""Dependency to get the Analysis service."""
|
54 |
-
return analysis_service
|
55 |
-
|
56 |
-
|
57 |
-
@app.on_event("startup")
|
58 |
-
async def startup_event():
|
59 |
-
"""Initialize services on startup."""
|
60 |
-
global app_ready
|
61 |
-
|
62 |
-
logger.info("Starting up WesternFront API v2.0")
|
63 |
-
|
64 |
-
try:
|
65 |
-
# Initialize Gemini AI and the internal Reddit service
|
66 |
-
analysis_service.initialize_gemini()
|
67 |
-
analysis_service.reddit_service.initialize()
|
68 |
-
|
69 |
-
# Schedule first update in background
|
70 |
-
asyncio.create_task(update_analysis_task("startup"))
|
71 |
-
|
72 |
-
# Set up periodic update task
|
73 |
-
asyncio.create_task(periodic_update())
|
74 |
-
|
75 |
-
# Mark application as ready to accept requests
|
76 |
-
app_ready = True
|
77 |
-
logger.info("Application ready to accept requests")
|
78 |
-
|
79 |
-
except Exception as e:
|
80 |
-
logger.error(f"Error during startup: {e}")
|
81 |
-
app_ready = False
|
82 |
-
|
83 |
-
|
84 |
-
@app.on_event("shutdown")
|
85 |
-
async def shutdown_event():
|
86 |
-
"""Clean up resources on shutdown."""
|
87 |
-
logger.info("Shutting down WesternFront API")
|
88 |
-
if analysis_service and hasattr(analysis_service, 'close'):
|
89 |
-
await analysis_service.close()
|
90 |
-
|
91 |
-
|
92 |
-
async def update_analysis_task(trigger: str = "scheduled") -> None:
|
93 |
-
"""Task to update the conflict analysis using the AnalysisService."""
|
94 |
-
global latest_analysis, last_update_time
|
95 |
-
|
96 |
-
try:
|
97 |
-
logger.info(f"Starting analysis update (trigger: {trigger})")
|
98 |
-
|
99 |
-
# --- REFACTORED: The analysis_service now handles everything ---
|
100 |
-
analysis = await analysis_service.generate_conflict_analysis(trigger=trigger)
|
101 |
-
|
102 |
-
if analysis:
|
103 |
-
latest_analysis = analysis
|
104 |
-
last_update_time = datetime.now()
|
105 |
-
logger.info(f"Analysis updated successfully. Tension level: {analysis.tension_level}")
|
106 |
-
else:
|
107 |
-
logger.warning("Failed to generate new analysis. No relevant data might be available.")
|
108 |
-
|
109 |
-
except Exception as e:
|
110 |
-
logger.error(f"Error in update_analysis_task: {str(e)}")
|
111 |
-
|
112 |
-
|
113 |
-
async def periodic_update() -> None:
|
114 |
-
"""Periodically update the analysis."""
|
115 |
-
update_interval = int(os.getenv("UPDATE_INTERVAL_MINUTES", 60))
|
116 |
-
|
117 |
-
while True:
|
118 |
-
try:
|
119 |
-
await asyncio.sleep(update_interval * 60)
|
120 |
-
await update_analysis_task("scheduled")
|
121 |
-
except Exception as e:
|
122 |
-
logger.error(f"Error in periodic_update: {str(e)}")
|
123 |
-
await asyncio.sleep(300) # Wait 5 minutes if there was an error
|
124 |
-
|
125 |
-
|
126 |
-
@app.get("/", response_model=Dict)
|
127 |
-
async def root():
|
128 |
-
"""Root endpoint with basic information about the API."""
|
129 |
-
return {
|
130 |
-
"name": "WesternFront API",
|
131 |
-
"description": "AI-powered conflict tracker for India-Pakistan tensions using Reddit data",
|
132 |
-
"version": "2.0.0",
|
133 |
-
"status": "ready" if app_ready else "initializing"
|
134 |
-
}
|
135 |
-
|
136 |
-
|
137 |
-
@app.get("/ready")
|
138 |
-
async def readiness_check():
|
139 |
-
"""Readiness check endpoint."""
|
140 |
-
if not app_ready:
|
141 |
-
raise HTTPException(status_code=503, detail="Application is starting up")
|
142 |
-
return {"status": "ready", "timestamp": datetime.now().isoformat()}
|
143 |
-
|
144 |
-
|
145 |
-
@app.get("/health", response_model=HealthCheck)
|
146 |
-
async def health_check():
|
147 |
-
"""Health check endpoint."""
|
148 |
-
# --- UPDATED: Check Reddit service instead of Twitter ---
|
149 |
-
reddit_initialized = analysis_service.reddit_service.reddit is not None
|
150 |
-
gemini_initialized = analysis_service.model is not None
|
151 |
-
|
152 |
-
return HealthCheck(
|
153 |
-
status="healthy" if app_ready else "initializing",
|
154 |
-
version="2.0.0",
|
155 |
-
timestamp=datetime.now(),
|
156 |
-
last_update=last_update_time,
|
157 |
-
components_status={
|
158 |
-
"reddit_service": reddit_initialized,
|
159 |
-
"analysis_service": gemini_initialized
|
160 |
-
}
|
161 |
-
)
|
162 |
-
|
163 |
-
# The HEAD /health endpoint is a bit redundant with FastAPI, so it can be removed for simplicity
|
164 |
-
# unless you have a specific use case for it.
|
165 |
-
|
166 |
-
@app.get("/analysis", response_model=Optional[ConflictAnalysis])
|
167 |
-
async def get_latest_analysis():
|
168 |
-
"""Get the latest conflict analysis."""
|
169 |
-
if not latest_analysis:
|
170 |
-
raise HTTPException(
|
171 |
-
status_code=status.HTTP_404_NOT_FOUND,
|
172 |
-
detail="No analysis available yet. Try triggering an update."
|
173 |
-
)
|
174 |
-
return latest_analysis
|
175 |
-
|
176 |
-
|
177 |
-
@app.post("/analysis/update", response_model=Dict)
|
178 |
-
async def trigger_update(request: UpdateRequest):
|
179 |
-
"""Trigger an analysis update."""
|
180 |
-
if request.force:
|
181 |
-
# --- UPDATED: Clear Reddit service cache ---
|
182 |
-
analysis_service.reddit_service.in_memory_cache.clear()
|
183 |
-
logger.info("Cache cleared for forced refresh.")
|
184 |
-
|
185 |
-
# Start update in background
|
186 |
-
asyncio.create_task(update_analysis_task("manual"))
|
187 |
-
|
188 |
-
return {
|
189 |
-
"message": "Analysis update triggered",
|
190 |
-
"timestamp": datetime.now().isoformat(),
|
191 |
-
"force_refresh": request.force
|
192 |
-
}
|
193 |
-
|
194 |
-
|
195 |
-
# --- UPDATED: Now manages subreddit sources ---
|
196 |
-
@app.get("/sources", response_model=List[SubredditSource])
|
197 |
-
async def get_subreddit_sources(
|
198 |
-
analysis: AnalysisService = Depends(get_analysis_service)
|
199 |
-
):
|
200 |
-
"""Get the current list of subreddit sources."""
|
201 |
-
return analysis.get_sources()
|
202 |
-
|
203 |
-
|
204 |
-
# --- UPDATED: Now manages subreddit sources ---
|
205 |
-
@app.post("/sources", response_model=Dict)
|
206 |
-
async def update_subreddit_sources(
|
207 |
-
sources: List[SubredditSource],
|
208 |
-
analysis: AnalysisService = Depends(get_analysis_service)
|
209 |
-
):
|
210 |
-
"""Update the list of subreddit sources."""
|
211 |
-
analysis.update_sources(sources)
|
212 |
-
return {
|
213 |
-
"message": "Subreddit sources updated",
|
214 |
-
"count": len(sources)
|
215 |
-
}
|
216 |
-
|
217 |
-
|
218 |
-
@app.get("/keywords", response_model=List[str])
|
219 |
-
async def get_search_keywords(
|
220 |
-
analysis: AnalysisService = Depends(get_analysis_service)
|
221 |
-
):
|
222 |
-
"""Get the current search keywords."""
|
223 |
-
return analysis.get_search_keywords()
|
224 |
-
|
225 |
-
|
226 |
-
@app.post("/keywords", response_model=Dict)
|
227 |
-
async def update_search_keywords(
|
228 |
-
keywords: List[str],
|
229 |
-
analysis: AnalysisService = Depends(get_analysis_service)
|
230 |
-
):
|
231 |
-
"""Update the search keywords."""
|
232 |
-
analysis.update_search_keywords(keywords)
|
233 |
-
return {
|
234 |
-
"message": "Search keywords updated",
|
235 |
-
"count": len(keywords)
|
236 |
-
}
|
237 |
-
|
238 |
-
|
239 |
-
@app.get("/tension-levels", response_model=List[str])
|
240 |
-
async def get_tension_levels():
|
241 |
-
"""Get the available tension levels."""
|
242 |
-
return [level.value for level in TensionLevel]
|
243 |
-
|
244 |
-
# --- REMOVED: /rss-feeds endpoint is no longer applicable ---
|
245 |
-
|
246 |
-
if __name__ == "__main__":
|
247 |
-
import uvicorn
|
248 |
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
|
|
1 |
+
import asyncio
|
2 |
+
import os
|
3 |
+
from datetime import datetime
|
4 |
+
from typing import Dict, List, Optional
|
5 |
+
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, status
|
8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
9 |
+
from loguru import logger
|
10 |
+
|
11 |
+
# --- UPDATED IMPORTS ---
|
12 |
+
from analysis_service import AnalysisService
|
13 |
+
# No longer need RssTwitterService
|
14 |
+
from models import (ConflictAnalysis, HealthCheck, SubredditSource, TensionLevel,
|
15 |
+
UpdateRequest)
|
16 |
+
|
17 |
+
# Load environment variables from .env file
|
18 |
+
load_dotenv()
|
19 |
+
|
20 |
+
# Configure logging
|
21 |
+
os.makedirs("logs", exist_ok=True)
|
22 |
+
logger.add("logs/app.log", rotation="500 MB", level=os.getenv("LOG_LEVEL", "INFO"))
|
23 |
+
|
24 |
+
# Global readiness flag
|
25 |
+
app_ready = False
|
26 |
+
|
27 |
+
# Create FastAPI application
|
28 |
+
app = FastAPI(
|
29 |
+
title="WesternFront API",
|
30 |
+
description="AI-powered conflict tracker for monitoring India-Pakistan tensions using Reddit data",
|
31 |
+
version="2.0.0" # Version bump for new data source
|
32 |
+
)
|
33 |
+
|
34 |
+
# Add CORS middleware
|
35 |
+
app.add_middleware(
|
36 |
+
CORSMiddleware,
|
37 |
+
allow_origins=["*"], # Adjust this for production
|
38 |
+
allow_credentials=True,
|
39 |
+
allow_methods=["*"],
|
40 |
+
allow_headers=["*"],
|
41 |
+
)
|
42 |
+
|
43 |
+
# --- UPDATED: Services ---
|
44 |
+
# The AnalysisService now manages the RedditService internally
|
45 |
+
analysis_service = AnalysisService()
|
46 |
+
|
47 |
+
# In-memory store for latest analysis
|
48 |
+
latest_analysis: Optional[ConflictAnalysis] = None
|
49 |
+
last_update_time: Optional[datetime] = None
|
50 |
+
|
51 |
+
|
52 |
+
async def get_analysis_service() -> AnalysisService:
|
53 |
+
"""Dependency to get the Analysis service."""
|
54 |
+
return analysis_service
|
55 |
+
|
56 |
+
|
57 |
+
@app.on_event("startup")
|
58 |
+
async def startup_event():
|
59 |
+
"""Initialize services on startup."""
|
60 |
+
global app_ready
|
61 |
+
|
62 |
+
logger.info("Starting up WesternFront API v2.0")
|
63 |
+
|
64 |
+
try:
|
65 |
+
# Initialize Gemini AI and the internal Reddit service
|
66 |
+
analysis_service.initialize_gemini()
|
67 |
+
analysis_service.reddit_service.initialize()
|
68 |
+
|
69 |
+
# Schedule first update in background
|
70 |
+
asyncio.create_task(update_analysis_task("startup"))
|
71 |
+
|
72 |
+
# Set up periodic update task
|
73 |
+
asyncio.create_task(periodic_update())
|
74 |
+
|
75 |
+
# Mark application as ready to accept requests
|
76 |
+
app_ready = True
|
77 |
+
logger.info("Application ready to accept requests")
|
78 |
+
|
79 |
+
except Exception as e:
|
80 |
+
logger.error(f"Error during startup: {e}")
|
81 |
+
app_ready = False
|
82 |
+
|
83 |
+
|
84 |
+
@app.on_event("shutdown")
|
85 |
+
async def shutdown_event():
|
86 |
+
"""Clean up resources on shutdown."""
|
87 |
+
logger.info("Shutting down WesternFront API")
|
88 |
+
if analysis_service and hasattr(analysis_service, 'close'):
|
89 |
+
await analysis_service.close()
|
90 |
+
|
91 |
+
|
92 |
+
async def update_analysis_task(trigger: str = "scheduled") -> None:
|
93 |
+
"""Task to update the conflict analysis using the AnalysisService."""
|
94 |
+
global latest_analysis, last_update_time
|
95 |
+
|
96 |
+
try:
|
97 |
+
logger.info(f"Starting analysis update (trigger: {trigger})")
|
98 |
+
|
99 |
+
# --- REFACTORED: The analysis_service now handles everything ---
|
100 |
+
analysis = await analysis_service.generate_conflict_analysis(trigger=trigger)
|
101 |
+
|
102 |
+
if analysis:
|
103 |
+
latest_analysis = analysis
|
104 |
+
last_update_time = datetime.now()
|
105 |
+
logger.info(f"Analysis updated successfully. Tension level: {analysis.tension_level}")
|
106 |
+
else:
|
107 |
+
logger.warning("Failed to generate new analysis. No relevant data might be available.")
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
logger.error(f"Error in update_analysis_task: {str(e)}")
|
111 |
+
|
112 |
+
|
113 |
+
async def periodic_update() -> None:
|
114 |
+
"""Periodically update the analysis."""
|
115 |
+
update_interval = int(os.getenv("UPDATE_INTERVAL_MINUTES", 60))
|
116 |
+
|
117 |
+
while True:
|
118 |
+
try:
|
119 |
+
await asyncio.sleep(update_interval * 60)
|
120 |
+
await update_analysis_task("scheduled")
|
121 |
+
except Exception as e:
|
122 |
+
logger.error(f"Error in periodic_update: {str(e)}")
|
123 |
+
await asyncio.sleep(300) # Wait 5 minutes if there was an error
|
124 |
+
|
125 |
+
|
126 |
+
@app.get("/", response_model=Dict)
|
127 |
+
async def root():
|
128 |
+
"""Root endpoint with basic information about the API."""
|
129 |
+
return {
|
130 |
+
"name": "WesternFront API",
|
131 |
+
"description": "AI-powered conflict tracker for India-Pakistan tensions using Reddit data",
|
132 |
+
"version": "2.0.0",
|
133 |
+
"status": "ready" if app_ready else "initializing"
|
134 |
+
}
|
135 |
+
|
136 |
+
|
137 |
+
@app.get("/ready")
|
138 |
+
async def readiness_check():
|
139 |
+
"""Readiness check endpoint."""
|
140 |
+
if not app_ready:
|
141 |
+
raise HTTPException(status_code=503, detail="Application is starting up")
|
142 |
+
return {"status": "ready", "timestamp": datetime.now().isoformat()}
|
143 |
+
|
144 |
+
@app.head("/health", response_model=HealthCheck)
|
145 |
+
@app.get("/health", response_model=HealthCheck)
|
146 |
+
async def health_check():
|
147 |
+
"""Health check endpoint."""
|
148 |
+
# --- UPDATED: Check Reddit service instead of Twitter ---
|
149 |
+
reddit_initialized = analysis_service.reddit_service.reddit is not None
|
150 |
+
gemini_initialized = analysis_service.model is not None
|
151 |
+
|
152 |
+
return HealthCheck(
|
153 |
+
status="healthy" if app_ready else "initializing",
|
154 |
+
version="2.0.0",
|
155 |
+
timestamp=datetime.now(),
|
156 |
+
last_update=last_update_time,
|
157 |
+
components_status={
|
158 |
+
"reddit_service": reddit_initialized,
|
159 |
+
"analysis_service": gemini_initialized
|
160 |
+
}
|
161 |
+
)
|
162 |
+
|
163 |
+
# The HEAD /health endpoint is a bit redundant with FastAPI, so it can be removed for simplicity
|
164 |
+
# unless you have a specific use case for it.
|
165 |
+
|
166 |
+
@app.get("/analysis", response_model=Optional[ConflictAnalysis])
|
167 |
+
async def get_latest_analysis():
|
168 |
+
"""Get the latest conflict analysis."""
|
169 |
+
if not latest_analysis:
|
170 |
+
raise HTTPException(
|
171 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
172 |
+
detail="No analysis available yet. Try triggering an update."
|
173 |
+
)
|
174 |
+
return latest_analysis
|
175 |
+
|
176 |
+
|
177 |
+
@app.post("/analysis/update", response_model=Dict)
|
178 |
+
async def trigger_update(request: UpdateRequest):
|
179 |
+
"""Trigger an analysis update."""
|
180 |
+
if request.force:
|
181 |
+
# --- UPDATED: Clear Reddit service cache ---
|
182 |
+
analysis_service.reddit_service.in_memory_cache.clear()
|
183 |
+
logger.info("Cache cleared for forced refresh.")
|
184 |
+
|
185 |
+
# Start update in background
|
186 |
+
asyncio.create_task(update_analysis_task("manual"))
|
187 |
+
|
188 |
+
return {
|
189 |
+
"message": "Analysis update triggered",
|
190 |
+
"timestamp": datetime.now().isoformat(),
|
191 |
+
"force_refresh": request.force
|
192 |
+
}
|
193 |
+
|
194 |
+
|
195 |
+
# --- UPDATED: Now manages subreddit sources ---
|
196 |
+
@app.get("/sources", response_model=List[SubredditSource])
|
197 |
+
async def get_subreddit_sources(
|
198 |
+
analysis: AnalysisService = Depends(get_analysis_service)
|
199 |
+
):
|
200 |
+
"""Get the current list of subreddit sources."""
|
201 |
+
return analysis.get_sources()
|
202 |
+
|
203 |
+
|
204 |
+
# --- UPDATED: Now manages subreddit sources ---
|
205 |
+
@app.post("/sources", response_model=Dict)
|
206 |
+
async def update_subreddit_sources(
|
207 |
+
sources: List[SubredditSource],
|
208 |
+
analysis: AnalysisService = Depends(get_analysis_service)
|
209 |
+
):
|
210 |
+
"""Update the list of subreddit sources."""
|
211 |
+
analysis.update_sources(sources)
|
212 |
+
return {
|
213 |
+
"message": "Subreddit sources updated",
|
214 |
+
"count": len(sources)
|
215 |
+
}
|
216 |
+
|
217 |
+
|
218 |
+
@app.get("/keywords", response_model=List[str])
|
219 |
+
async def get_search_keywords(
|
220 |
+
analysis: AnalysisService = Depends(get_analysis_service)
|
221 |
+
):
|
222 |
+
"""Get the current search keywords."""
|
223 |
+
return analysis.get_search_keywords()
|
224 |
+
|
225 |
+
|
226 |
+
@app.post("/keywords", response_model=Dict)
|
227 |
+
async def update_search_keywords(
|
228 |
+
keywords: List[str],
|
229 |
+
analysis: AnalysisService = Depends(get_analysis_service)
|
230 |
+
):
|
231 |
+
"""Update the search keywords."""
|
232 |
+
analysis.update_search_keywords(keywords)
|
233 |
+
return {
|
234 |
+
"message": "Search keywords updated",
|
235 |
+
"count": len(keywords)
|
236 |
+
}
|
237 |
+
|
238 |
+
|
239 |
+
@app.get("/tension-levels", response_model=List[str])
|
240 |
+
async def get_tension_levels():
|
241 |
+
"""Get the available tension levels."""
|
242 |
+
return [level.value for level in TensionLevel]
|
243 |
+
|
244 |
+
# --- REMOVED: /rss-feeds endpoint is no longer applicable ---
|
245 |
+
|
246 |
+
if __name__ == "__main__":
|
247 |
+
import uvicorn
|
248 |
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|