arionxiv 1.0.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arionxiv/__init__.py +40 -0
- arionxiv/__main__.py +10 -0
- arionxiv/arxiv_operations/__init__.py +0 -0
- arionxiv/arxiv_operations/client.py +225 -0
- arionxiv/arxiv_operations/fetcher.py +173 -0
- arionxiv/arxiv_operations/searcher.py +122 -0
- arionxiv/arxiv_operations/utils.py +293 -0
- arionxiv/cli/__init__.py +4 -0
- arionxiv/cli/commands/__init__.py +1 -0
- arionxiv/cli/commands/analyze.py +587 -0
- arionxiv/cli/commands/auth.py +365 -0
- arionxiv/cli/commands/chat.py +714 -0
- arionxiv/cli/commands/daily.py +482 -0
- arionxiv/cli/commands/fetch.py +217 -0
- arionxiv/cli/commands/library.py +295 -0
- arionxiv/cli/commands/preferences.py +426 -0
- arionxiv/cli/commands/search.py +254 -0
- arionxiv/cli/commands/settings_unified.py +1407 -0
- arionxiv/cli/commands/trending.py +41 -0
- arionxiv/cli/commands/welcome.py +168 -0
- arionxiv/cli/main.py +407 -0
- arionxiv/cli/ui/__init__.py +1 -0
- arionxiv/cli/ui/global_theme_manager.py +173 -0
- arionxiv/cli/ui/logo.py +127 -0
- arionxiv/cli/ui/splash.py +89 -0
- arionxiv/cli/ui/theme.py +32 -0
- arionxiv/cli/ui/theme_system.py +391 -0
- arionxiv/cli/utils/__init__.py +54 -0
- arionxiv/cli/utils/animations.py +522 -0
- arionxiv/cli/utils/api_client.py +583 -0
- arionxiv/cli/utils/api_config.py +505 -0
- arionxiv/cli/utils/command_suggestions.py +147 -0
- arionxiv/cli/utils/db_config_manager.py +254 -0
- arionxiv/github_actions_runner.py +206 -0
- arionxiv/main.py +23 -0
- arionxiv/prompts/__init__.py +9 -0
- arionxiv/prompts/prompts.py +247 -0
- arionxiv/rag_techniques/__init__.py +8 -0
- arionxiv/rag_techniques/basic_rag.py +1531 -0
- arionxiv/scheduler_daemon.py +139 -0
- arionxiv/server.py +1000 -0
- arionxiv/server_main.py +24 -0
- arionxiv/services/__init__.py +73 -0
- arionxiv/services/llm_client.py +30 -0
- arionxiv/services/llm_inference/__init__.py +58 -0
- arionxiv/services/llm_inference/groq_client.py +469 -0
- arionxiv/services/llm_inference/llm_utils.py +250 -0
- arionxiv/services/llm_inference/openrouter_client.py +564 -0
- arionxiv/services/unified_analysis_service.py +872 -0
- arionxiv/services/unified_auth_service.py +457 -0
- arionxiv/services/unified_config_service.py +456 -0
- arionxiv/services/unified_daily_dose_service.py +823 -0
- arionxiv/services/unified_database_service.py +1633 -0
- arionxiv/services/unified_llm_service.py +366 -0
- arionxiv/services/unified_paper_service.py +604 -0
- arionxiv/services/unified_pdf_service.py +522 -0
- arionxiv/services/unified_prompt_service.py +344 -0
- arionxiv/services/unified_scheduler_service.py +589 -0
- arionxiv/services/unified_user_service.py +954 -0
- arionxiv/utils/__init__.py +51 -0
- arionxiv/utils/api_helpers.py +200 -0
- arionxiv/utils/file_cleanup.py +150 -0
- arionxiv/utils/ip_helper.py +96 -0
- arionxiv-1.0.32.dist-info/METADATA +336 -0
- arionxiv-1.0.32.dist-info/RECORD +69 -0
- arionxiv-1.0.32.dist-info/WHEEL +5 -0
- arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
- arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
- arionxiv-1.0.32.dist-info/top_level.txt +1 -0
arionxiv/server.py
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI server for ArionXiv package
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Depends, Query
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional, Dict, Any, List
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from .services.unified_database_service import unified_database_service
|
|
13
|
+
from .services.unified_auth_service import unified_auth_service, verify_token, security
|
|
14
|
+
from .services.unified_paper_service import unified_paper_service
|
|
15
|
+
from .services.unified_user_service import unified_user_service
|
|
16
|
+
from .services.unified_analysis_service import unified_analysis_service, rag_chat_system
|
|
17
|
+
from .arxiv_operations.client import ArxivClient
|
|
18
|
+
from .arxiv_operations.searcher import ArxivSearcher
|
|
19
|
+
from .arxiv_operations.fetcher import ArxivFetcher
|
|
20
|
+
from .utils.api_helpers import (
|
|
21
|
+
RegisterRequest,
|
|
22
|
+
LoginRequest,
|
|
23
|
+
RefreshTokenRequest,
|
|
24
|
+
ChatMessageRequest,
|
|
25
|
+
ChatSessionRequest,
|
|
26
|
+
LibraryAddRequest,
|
|
27
|
+
LibraryUpdateRequest,
|
|
28
|
+
PaperSearchRequest,
|
|
29
|
+
create_error_response,
|
|
30
|
+
handle_service_error,
|
|
31
|
+
sanitize_arxiv_id,
|
|
32
|
+
format_user_response
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
app = FastAPI(
|
|
38
|
+
title="ArionXiv API",
|
|
39
|
+
description="AI-powered research paper ingestion pipeline with user authentication and daily analysis",
|
|
40
|
+
version="1.0.0"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# CORS middleware for frontend integration
|
|
44
|
+
app.add_middleware(
|
|
45
|
+
CORSMiddleware,
|
|
46
|
+
allow_origins=["*"], # Configure for production
|
|
47
|
+
allow_credentials=True,
|
|
48
|
+
allow_methods=["*"],
|
|
49
|
+
allow_headers=["*"],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Initialize services
|
|
53
|
+
arxiv_client = ArxivClient()
|
|
54
|
+
arxiv_searcher = ArxivSearcher()
|
|
55
|
+
arxiv_fetcher = ArxivFetcher()
|
|
56
|
+
|
|
57
|
+
# Startup event to initialize database connections
|
|
58
|
+
@app.on_event("startup")
|
|
59
|
+
async def startup_event():
|
|
60
|
+
"""Initialize database connections on startup"""
|
|
61
|
+
try:
|
|
62
|
+
logger.info("Starting ArionXiv API server")
|
|
63
|
+
await asyncio.wait_for(unified_database_service.connect_mongodb(), timeout=10.0)
|
|
64
|
+
logger.info("ArionXiv API server started successfully")
|
|
65
|
+
except asyncio.TimeoutError:
|
|
66
|
+
logger.error("Database connection timeout during startup")
|
|
67
|
+
raise Exception("Database connection timeout")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Failed to start ArionXiv API server: {e}", exc_info=True)
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
@app.on_event("shutdown")
|
|
73
|
+
async def shutdown_event():
|
|
74
|
+
"""Clean up connections on shutdown"""
|
|
75
|
+
try:
|
|
76
|
+
await unified_database_service.disconnect()
|
|
77
|
+
logger.info("ArionXiv API server shut down gracefully")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error during shutdown: {e}", exc_info=True)
|
|
80
|
+
|
|
81
|
+
# Health check endpoint
|
|
82
|
+
@app.get("/health")
|
|
83
|
+
async def health_check():
|
|
84
|
+
"""Health check endpoint"""
|
|
85
|
+
return {
|
|
86
|
+
"status": "healthy",
|
|
87
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
88
|
+
"version": "1.0.0"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Root endpoint
|
|
92
|
+
@app.get("/")
|
|
93
|
+
async def root():
|
|
94
|
+
"""Root endpoint with API information"""
|
|
95
|
+
return {
|
|
96
|
+
"message": "Welcome to ArionXiv API",
|
|
97
|
+
"version": "1.0.0",
|
|
98
|
+
"documentation": "/docs",
|
|
99
|
+
"health": "/health"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# AUTHENTICATION ENDPOINTS
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
@app.post("/auth/register")
|
|
108
|
+
async def register_user(request: RegisterRequest):
|
|
109
|
+
"""Register a new user account"""
|
|
110
|
+
try:
|
|
111
|
+
logger.info(f"Registration attempt for: {request.email}")
|
|
112
|
+
result = await unified_auth_service.register_user(
|
|
113
|
+
email=request.email,
|
|
114
|
+
user_name=request.user_name,
|
|
115
|
+
password=request.password,
|
|
116
|
+
full_name=request.full_name or ""
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not result.get("success"):
|
|
120
|
+
handle_service_error(result, "Registration")
|
|
121
|
+
|
|
122
|
+
logger.info(f"User registered successfully: {request.user_name}")
|
|
123
|
+
return {
|
|
124
|
+
"success": True,
|
|
125
|
+
"message": "User registered successfully",
|
|
126
|
+
"user": result.get("user")
|
|
127
|
+
}
|
|
128
|
+
except HTTPException:
|
|
129
|
+
raise
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Registration failed: {e}", exc_info=True)
|
|
132
|
+
raise create_error_response(500, "Registration failed", "InternalError")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.post("/auth/login")
|
|
136
|
+
async def login_user(request: LoginRequest):
|
|
137
|
+
"""Authenticate user and return JWT token"""
|
|
138
|
+
try:
|
|
139
|
+
logger.info(f"Login attempt for: {request.identifier}")
|
|
140
|
+
result = await unified_auth_service.authenticate_user(
|
|
141
|
+
identifier=request.identifier,
|
|
142
|
+
password=request.password
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not result.get("success"):
|
|
146
|
+
handle_service_error(result, "Authentication")
|
|
147
|
+
|
|
148
|
+
logger.info(f"User logged in successfully: {request.identifier}")
|
|
149
|
+
return {
|
|
150
|
+
"success": True,
|
|
151
|
+
"message": "Login successful",
|
|
152
|
+
"user": result.get("user"),
|
|
153
|
+
"token": result.get("token")
|
|
154
|
+
}
|
|
155
|
+
except HTTPException:
|
|
156
|
+
raise
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Login failed: {e}", exc_info=True)
|
|
159
|
+
raise create_error_response(500, "Login failed", "InternalError")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.post("/auth/logout")
|
|
163
|
+
async def logout_user(current_user: Dict = Depends(verify_token)):
|
|
164
|
+
"""Logout user (client should discard token)"""
|
|
165
|
+
logger.info(f"User logged out: {current_user.get('email')}")
|
|
166
|
+
return {
|
|
167
|
+
"success": True,
|
|
168
|
+
"message": "Logged out successfully"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.post("/auth/refresh")
|
|
173
|
+
async def refresh_token(request: RefreshTokenRequest):
|
|
174
|
+
"""Refresh JWT token"""
|
|
175
|
+
try:
|
|
176
|
+
result = unified_auth_service.verify_token(request.token)
|
|
177
|
+
|
|
178
|
+
if not result.get("valid"):
|
|
179
|
+
raise create_error_response(401, result.get("error", "Invalid token"), "AuthenticationError")
|
|
180
|
+
|
|
181
|
+
payload = result.get("payload", {})
|
|
182
|
+
user_data = {
|
|
183
|
+
"_id": payload.get("user_id"),
|
|
184
|
+
"email": payload.get("email"),
|
|
185
|
+
"user_name": payload.get("user_name")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
new_token = unified_auth_service.create_access_token(user_data)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"success": True,
|
|
192
|
+
"message": "Token refreshed",
|
|
193
|
+
"token": new_token
|
|
194
|
+
}
|
|
195
|
+
except HTTPException:
|
|
196
|
+
raise
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Token refresh failed: {e}", exc_info=True)
|
|
199
|
+
raise create_error_response(500, "Token refresh failed", "InternalError")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# USER ENDPOINTS
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
@app.get("/user/profile")
|
|
207
|
+
async def get_user_profile(current_user: Dict = Depends(verify_token)):
|
|
208
|
+
"""Get current user profile"""
|
|
209
|
+
try:
|
|
210
|
+
logger.debug(f"Fetching profile for user: {current_user['email']}")
|
|
211
|
+
user = await unified_user_service.get_user_by_email(current_user["email"])
|
|
212
|
+
if not user:
|
|
213
|
+
logger.warning(f"User not found: {current_user['email']}")
|
|
214
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
215
|
+
|
|
216
|
+
logger.debug(f"Profile fetched for user: {current_user['email']}")
|
|
217
|
+
return {"success": True, "user": format_user_response(user)}
|
|
218
|
+
except HTTPException:
|
|
219
|
+
raise
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Failed to get user profile: {e}", exc_info=True)
|
|
222
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.put("/user/settings")
|
|
226
|
+
async def update_user_settings(
|
|
227
|
+
settings: Dict[str, Any],
|
|
228
|
+
current_user: Dict = Depends(verify_token)
|
|
229
|
+
):
|
|
230
|
+
"""Update user settings"""
|
|
231
|
+
try:
|
|
232
|
+
result = await unified_auth_service.update_user_settings(
|
|
233
|
+
user_id=current_user.get("id") or current_user.get("user_id"),
|
|
234
|
+
settings=settings
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if not result.get("success"):
|
|
238
|
+
handle_service_error(result, "Settings update")
|
|
239
|
+
|
|
240
|
+
return {"success": True, "message": "Settings updated"}
|
|
241
|
+
except HTTPException:
|
|
242
|
+
raise
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Failed to update settings: {e}", exc_info=True)
|
|
245
|
+
raise HTTPException(status_code=500, detail="Failed to update settings")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.get("/user/settings")
|
|
249
|
+
async def get_user_settings(current_user: Dict = Depends(verify_token)):
|
|
250
|
+
"""Get user settings"""
|
|
251
|
+
try:
|
|
252
|
+
result = await unified_auth_service.get_user_settings(
|
|
253
|
+
user_id=current_user.get("id") or current_user.get("user_id")
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if not result.get("success"):
|
|
257
|
+
handle_service_error(result, "Get settings")
|
|
258
|
+
|
|
259
|
+
return {"success": True, "settings": result.get("settings", {})}
|
|
260
|
+
except HTTPException:
|
|
261
|
+
raise
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"Failed to get settings: {e}", exc_info=True)
|
|
264
|
+
raise HTTPException(status_code=500, detail="Failed to get settings")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# =============================================================================
|
|
268
|
+
# PAPER MANAGEMENT ENDPOINTS
|
|
269
|
+
# =============================================================================
|
|
270
|
+
|
|
271
|
+
# Paper Management Endpoints
|
|
272
|
+
@app.get("/papers/search")
|
|
273
|
+
async def search_papers(
|
|
274
|
+
query: str,
|
|
275
|
+
max_results: int = 10,
|
|
276
|
+
category: Optional[str] = None,
|
|
277
|
+
current_user: Dict = Depends(verify_token)
|
|
278
|
+
):
|
|
279
|
+
"""Search for papers on arXiv"""
|
|
280
|
+
try:
|
|
281
|
+
logger.info(f"Searching papers: query='{query}', max_results={max_results}, category={category}")
|
|
282
|
+
papers = await arxiv_searcher.search_papers(
|
|
283
|
+
query=query,
|
|
284
|
+
max_results=max_results,
|
|
285
|
+
category=category
|
|
286
|
+
)
|
|
287
|
+
logger.info(f"Paper search completed: {len(papers)} results found")
|
|
288
|
+
return {
|
|
289
|
+
"papers": papers,
|
|
290
|
+
"count": len(papers),
|
|
291
|
+
"query": query
|
|
292
|
+
}
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.error(f"Paper search failed: {e}", exc_info=True)
|
|
295
|
+
raise HTTPException(status_code=500, detail="Search failed")
|
|
296
|
+
|
|
297
|
+
@app.post("/papers/{arxiv_id}/fetch")
|
|
298
|
+
async def fetch_paper(
|
|
299
|
+
arxiv_id: str,
|
|
300
|
+
current_user: Dict = Depends(verify_token)
|
|
301
|
+
):
|
|
302
|
+
"""Fetch and store a specific paper"""
|
|
303
|
+
try:
|
|
304
|
+
logger.info(f"Fetching paper: {arxiv_id}")
|
|
305
|
+
paper = await arxiv_fetcher.fetch_paper(arxiv_id)
|
|
306
|
+
|
|
307
|
+
if not paper:
|
|
308
|
+
logger.warning(f"Paper not found: {arxiv_id}")
|
|
309
|
+
raise HTTPException(status_code=404, detail="Paper not found")
|
|
310
|
+
|
|
311
|
+
stored_paper = await unified_paper_service.store_paper(paper)
|
|
312
|
+
logger.info(f"Paper stored successfully: {arxiv_id}")
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"message": "Paper fetched successfully",
|
|
316
|
+
"paper": stored_paper
|
|
317
|
+
}
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Paper fetch failed for {arxiv_id}: {e}", exc_info=True)
|
|
320
|
+
raise HTTPException(status_code=500, detail="Failed to fetch paper")
|
|
321
|
+
|
|
322
|
+
@app.post("/papers/{paper_id}/analyze")
|
|
323
|
+
async def analyze_paper(
|
|
324
|
+
paper_id: str,
|
|
325
|
+
current_user: Dict = Depends(verify_token)
|
|
326
|
+
):
|
|
327
|
+
"""Analyze a stored paper"""
|
|
328
|
+
try:
|
|
329
|
+
logger.info(f"Starting paper analysis: {paper_id}")
|
|
330
|
+
paper = await unified_paper_service.get_paper_by_id(paper_id)
|
|
331
|
+
|
|
332
|
+
if not paper:
|
|
333
|
+
logger.warning(f"Paper not found for analysis: {paper_id}")
|
|
334
|
+
raise HTTPException(status_code=404, detail="Paper not found")
|
|
335
|
+
|
|
336
|
+
analysis = await unified_analysis_service.analyze_paper(paper)
|
|
337
|
+
logger.info(f"Paper analysis completed: {paper_id}")
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
"success": True,
|
|
341
|
+
"message": "Paper analyzed successfully",
|
|
342
|
+
"analysis": analysis
|
|
343
|
+
}
|
|
344
|
+
except HTTPException:
|
|
345
|
+
raise
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Paper analysis failed for {paper_id}: {e}", exc_info=True)
|
|
348
|
+
raise HTTPException(status_code=500, detail="Analysis failed")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# =============================================================================
|
|
352
|
+
# LIBRARY MANAGEMENT ENDPOINTS
|
|
353
|
+
# =============================================================================
|
|
354
|
+
|
|
355
|
+
@app.get("/library")
|
|
356
|
+
async def get_library(
|
|
357
|
+
current_user: Dict = Depends(verify_token),
|
|
358
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
359
|
+
skip: int = Query(default=0, ge=0)
|
|
360
|
+
):
|
|
361
|
+
"""Get user's paper library"""
|
|
362
|
+
try:
|
|
363
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
364
|
+
logger.debug(f"Fetching library for user: {user_name}")
|
|
365
|
+
|
|
366
|
+
papers = await unified_database_service.get_user_papers(user_name)
|
|
367
|
+
|
|
368
|
+
total = len(papers)
|
|
369
|
+
paginated = papers[skip:skip + limit]
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"success": True,
|
|
373
|
+
"papers": paginated,
|
|
374
|
+
"count": len(paginated),
|
|
375
|
+
"total": total,
|
|
376
|
+
"has_more": skip + limit < total
|
|
377
|
+
}
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"Failed to get library: {e}", exc_info=True)
|
|
380
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve library")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@app.post("/library/add")
|
|
384
|
+
async def add_to_library(
|
|
385
|
+
request: LibraryAddRequest,
|
|
386
|
+
current_user: Dict = Depends(verify_token)
|
|
387
|
+
):
|
|
388
|
+
"""Add a paper to user's library"""
|
|
389
|
+
try:
|
|
390
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
391
|
+
arxiv_id = sanitize_arxiv_id(request.arxiv_id)
|
|
392
|
+
|
|
393
|
+
logger.info(f"Adding paper {arxiv_id} to library for user: {user_name}")
|
|
394
|
+
|
|
395
|
+
# Fetch paper metadata from arXiv
|
|
396
|
+
paper_data = await arxiv_fetcher.fetch_paper(arxiv_id)
|
|
397
|
+
if not paper_data:
|
|
398
|
+
raise HTTPException(status_code=404, detail="Paper not found on arXiv")
|
|
399
|
+
|
|
400
|
+
# Add to user's library
|
|
401
|
+
library_entry = {
|
|
402
|
+
"arxiv_id": arxiv_id,
|
|
403
|
+
"user_name": user_name,
|
|
404
|
+
"title": paper_data.get("title", ""),
|
|
405
|
+
"authors": paper_data.get("authors", []),
|
|
406
|
+
"abstract": paper_data.get("abstract", ""),
|
|
407
|
+
"categories": paper_data.get("categories", []),
|
|
408
|
+
"tags": request.tags or [],
|
|
409
|
+
"notes": request.notes or "",
|
|
410
|
+
"added_at": datetime.utcnow(),
|
|
411
|
+
"paper_data": paper_data
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
result = await unified_database_service.insert_one("user_library", library_entry)
|
|
415
|
+
|
|
416
|
+
if result:
|
|
417
|
+
logger.info(f"Paper added to library: {arxiv_id}")
|
|
418
|
+
return {
|
|
419
|
+
"success": True,
|
|
420
|
+
"message": "Paper added to library",
|
|
421
|
+
"paper": {
|
|
422
|
+
"arxiv_id": arxiv_id,
|
|
423
|
+
"title": paper_data.get("title", "")
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
raise HTTPException(status_code=500, detail="Failed to add paper to library")
|
|
428
|
+
|
|
429
|
+
except HTTPException:
|
|
430
|
+
raise
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.error(f"Failed to add to library: {e}", exc_info=True)
|
|
433
|
+
raise HTTPException(status_code=500, detail="Failed to add paper to library")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@app.delete("/library/{arxiv_id}")
|
|
437
|
+
async def remove_from_library(
|
|
438
|
+
arxiv_id: str,
|
|
439
|
+
current_user: Dict = Depends(verify_token)
|
|
440
|
+
):
|
|
441
|
+
"""Remove a paper from user's library"""
|
|
442
|
+
try:
|
|
443
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
444
|
+
arxiv_id = sanitize_arxiv_id(arxiv_id)
|
|
445
|
+
|
|
446
|
+
logger.info(f"Removing paper {arxiv_id} from library for user: {user_name}")
|
|
447
|
+
|
|
448
|
+
result = await unified_database_service.delete_one(
|
|
449
|
+
"user_library",
|
|
450
|
+
{"arxiv_id": arxiv_id, "user_name": user_name}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if result and getattr(result, 'deleted_count', 0) > 0:
|
|
454
|
+
return {"success": True, "message": "Paper removed from library"}
|
|
455
|
+
|
|
456
|
+
raise HTTPException(status_code=404, detail="Paper not found in library")
|
|
457
|
+
|
|
458
|
+
except HTTPException:
|
|
459
|
+
raise
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Failed to remove from library: {e}", exc_info=True)
|
|
462
|
+
raise HTTPException(status_code=500, detail="Failed to remove paper")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@app.put("/library/{arxiv_id}")
|
|
466
|
+
async def update_library_paper(
|
|
467
|
+
arxiv_id: str,
|
|
468
|
+
request: LibraryUpdateRequest,
|
|
469
|
+
current_user: Dict = Depends(verify_token)
|
|
470
|
+
):
|
|
471
|
+
"""Update paper metadata in library (tags, notes)"""
|
|
472
|
+
try:
|
|
473
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
474
|
+
arxiv_id = sanitize_arxiv_id(arxiv_id)
|
|
475
|
+
|
|
476
|
+
update_data = {}
|
|
477
|
+
if request.tags is not None:
|
|
478
|
+
update_data["tags"] = request.tags
|
|
479
|
+
if request.notes is not None:
|
|
480
|
+
update_data["notes"] = request.notes
|
|
481
|
+
|
|
482
|
+
if not update_data:
|
|
483
|
+
return {"success": True, "message": "No updates provided"}
|
|
484
|
+
|
|
485
|
+
update_data["updated_at"] = datetime.utcnow()
|
|
486
|
+
|
|
487
|
+
result = await unified_database_service.update_one(
|
|
488
|
+
"user_library",
|
|
489
|
+
{"arxiv_id": arxiv_id, "user_name": user_name},
|
|
490
|
+
{"$set": update_data}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
matched = getattr(result, 'matched_count', 0)
|
|
494
|
+
if matched > 0:
|
|
495
|
+
return {"success": True, "message": "Paper updated"}
|
|
496
|
+
|
|
497
|
+
raise HTTPException(status_code=404, detail="Paper not found in library")
|
|
498
|
+
|
|
499
|
+
except HTTPException:
|
|
500
|
+
raise
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.error(f"Failed to update library paper: {e}", exc_info=True)
|
|
503
|
+
raise HTTPException(status_code=500, detail="Failed to update paper")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@app.get("/library/search")
|
|
507
|
+
async def search_library(
|
|
508
|
+
query: str = Query(..., min_length=1),
|
|
509
|
+
current_user: Dict = Depends(verify_token)
|
|
510
|
+
):
|
|
511
|
+
"""Search user's library"""
|
|
512
|
+
try:
|
|
513
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
514
|
+
|
|
515
|
+
# Search in title, abstract, tags, notes
|
|
516
|
+
search_filter = {
|
|
517
|
+
"user_name": user_name,
|
|
518
|
+
"$or": [
|
|
519
|
+
{"title": {"$regex": query, "$options": "i"}},
|
|
520
|
+
{"abstract": {"$regex": query, "$options": "i"}},
|
|
521
|
+
{"tags": {"$regex": query, "$options": "i"}},
|
|
522
|
+
{"notes": {"$regex": query, "$options": "i"}}
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
papers = await unified_database_service.find_many("user_library", search_filter)
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
"success": True,
|
|
530
|
+
"papers": papers or [],
|
|
531
|
+
"count": len(papers) if papers else 0,
|
|
532
|
+
"query": query
|
|
533
|
+
}
|
|
534
|
+
except Exception as e:
|
|
535
|
+
logger.error(f"Library search failed: {e}", exc_info=True)
|
|
536
|
+
raise HTTPException(status_code=500, detail="Search failed")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# =============================================================================
|
|
540
|
+
# CHAT / RAG ENDPOINTS
|
|
541
|
+
# =============================================================================
|
|
542
|
+
|
|
543
|
+
@app.post("/chat/session")
|
|
544
|
+
async def create_chat_session(
|
|
545
|
+
request: ChatSessionRequest,
|
|
546
|
+
current_user: Dict = Depends(verify_token)
|
|
547
|
+
):
|
|
548
|
+
"""Create a new chat session for a paper"""
|
|
549
|
+
try:
|
|
550
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
551
|
+
arxiv_id = sanitize_arxiv_id(request.paper_id)
|
|
552
|
+
|
|
553
|
+
logger.info(f"Creating chat session for paper {arxiv_id}, user: {user_name}")
|
|
554
|
+
|
|
555
|
+
session_data = {
|
|
556
|
+
"user_name": user_name,
|
|
557
|
+
"arxiv_id": arxiv_id,
|
|
558
|
+
"title": request.title or f"Chat about {arxiv_id}",
|
|
559
|
+
"created_at": datetime.utcnow(),
|
|
560
|
+
"last_activity": datetime.utcnow(),
|
|
561
|
+
"messages": [],
|
|
562
|
+
"is_active": True
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
result = await unified_database_service.insert_one("chat_sessions", session_data)
|
|
566
|
+
|
|
567
|
+
if result and getattr(result, 'inserted_id', None):
|
|
568
|
+
return {
|
|
569
|
+
"success": True,
|
|
570
|
+
"message": "Chat session created",
|
|
571
|
+
"session_id": str(result.inserted_id),
|
|
572
|
+
"arxiv_id": arxiv_id
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
raise HTTPException(status_code=500, detail="Failed to create session")
|
|
576
|
+
|
|
577
|
+
except HTTPException:
|
|
578
|
+
raise
|
|
579
|
+
except Exception as e:
|
|
580
|
+
logger.error(f"Failed to create chat session: {e}", exc_info=True)
|
|
581
|
+
raise HTTPException(status_code=500, detail="Failed to create chat session")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@app.post("/chat/message")
|
|
585
|
+
async def send_chat_message(
|
|
586
|
+
request: ChatMessageRequest,
|
|
587
|
+
current_user: Dict = Depends(verify_token)
|
|
588
|
+
):
|
|
589
|
+
"""Send a message and get AI response using RAG"""
|
|
590
|
+
try:
|
|
591
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
592
|
+
arxiv_id = sanitize_arxiv_id(request.paper_id)
|
|
593
|
+
|
|
594
|
+
logger.info(f"Chat message for paper {arxiv_id} from user: {user_name}")
|
|
595
|
+
|
|
596
|
+
# Get or create session
|
|
597
|
+
session_id = request.session_id
|
|
598
|
+
if not session_id:
|
|
599
|
+
session_data = {
|
|
600
|
+
"user_name": user_name,
|
|
601
|
+
"arxiv_id": arxiv_id,
|
|
602
|
+
"title": f"Chat about {arxiv_id}",
|
|
603
|
+
"created_at": datetime.utcnow(),
|
|
604
|
+
"last_activity": datetime.utcnow(),
|
|
605
|
+
"messages": [],
|
|
606
|
+
"is_active": True
|
|
607
|
+
}
|
|
608
|
+
result = await unified_database_service.insert_one("chat_sessions", session_data)
|
|
609
|
+
session_id = str(result.inserted_id) if result and getattr(result, 'inserted_id', None) else None
|
|
610
|
+
if not session_id:
|
|
611
|
+
raise HTTPException(status_code=500, detail="Failed to create session")
|
|
612
|
+
|
|
613
|
+
# Get RAG response
|
|
614
|
+
response_data = await rag_chat_system.chat(
|
|
615
|
+
user_name=user_name,
|
|
616
|
+
paper_id=arxiv_id,
|
|
617
|
+
message=request.message,
|
|
618
|
+
session_id=session_id
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Store message in session
|
|
622
|
+
message_entry = {
|
|
623
|
+
"role": "user",
|
|
624
|
+
"content": request.message,
|
|
625
|
+
"timestamp": datetime.utcnow()
|
|
626
|
+
}
|
|
627
|
+
assistant_entry = {
|
|
628
|
+
"role": "assistant",
|
|
629
|
+
"content": response_data.get("response", ""),
|
|
630
|
+
"timestamp": datetime.utcnow(),
|
|
631
|
+
"sources": response_data.get("sources", [])
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
from bson import ObjectId
|
|
635
|
+
await unified_database_service.update_one(
|
|
636
|
+
"chat_sessions",
|
|
637
|
+
{"_id": ObjectId(session_id)},
|
|
638
|
+
{
|
|
639
|
+
"$push": {"messages": {"$each": [message_entry, assistant_entry]}},
|
|
640
|
+
"$set": {"last_activity": datetime.utcnow()}
|
|
641
|
+
}
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
"success": True,
|
|
646
|
+
"response": response_data.get("response", ""),
|
|
647
|
+
"session_id": session_id,
|
|
648
|
+
"sources": response_data.get("sources", [])
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
except HTTPException:
|
|
652
|
+
raise
|
|
653
|
+
except Exception as e:
|
|
654
|
+
logger.error(f"Chat message failed: {e}", exc_info=True)
|
|
655
|
+
raise HTTPException(status_code=500, detail="Chat failed")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@app.get("/chat/sessions")
|
|
659
|
+
async def get_chat_sessions(
|
|
660
|
+
current_user: Dict = Depends(verify_token),
|
|
661
|
+
active_only: bool = Query(default=True)
|
|
662
|
+
):
|
|
663
|
+
"""Get user's chat sessions"""
|
|
664
|
+
try:
|
|
665
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
666
|
+
|
|
667
|
+
filter_query = {"user_name": user_name}
|
|
668
|
+
if active_only:
|
|
669
|
+
filter_query["is_active"] = True
|
|
670
|
+
|
|
671
|
+
sessions = await unified_database_service.find_many("chat_sessions", filter_query)
|
|
672
|
+
|
|
673
|
+
# Format sessions for response
|
|
674
|
+
formatted = []
|
|
675
|
+
for s in (sessions or []):
|
|
676
|
+
formatted.append({
|
|
677
|
+
"session_id": str(s.get("_id", "")),
|
|
678
|
+
"arxiv_id": s.get("arxiv_id", ""),
|
|
679
|
+
"title": s.get("title", ""),
|
|
680
|
+
"created_at": s.get("created_at", ""),
|
|
681
|
+
"last_activity": s.get("last_activity", ""),
|
|
682
|
+
"message_count": len(s.get("messages", []))
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
"success": True,
|
|
687
|
+
"sessions": formatted,
|
|
688
|
+
"count": len(formatted)
|
|
689
|
+
}
|
|
690
|
+
except Exception as e:
|
|
691
|
+
logger.error(f"Failed to get chat sessions: {e}", exc_info=True)
|
|
692
|
+
raise HTTPException(status_code=500, detail="Failed to get sessions")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@app.get("/chat/session/{session_id}")
|
|
696
|
+
async def get_chat_session(
|
|
697
|
+
session_id: str,
|
|
698
|
+
current_user: Dict = Depends(verify_token)
|
|
699
|
+
):
|
|
700
|
+
"""Get a specific chat session with messages"""
|
|
701
|
+
try:
|
|
702
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
703
|
+
|
|
704
|
+
from bson import ObjectId
|
|
705
|
+
session = await unified_database_service.find_one(
|
|
706
|
+
"chat_sessions",
|
|
707
|
+
{"_id": ObjectId(session_id), "user_name": user_name}
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
if not session:
|
|
711
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
"success": True,
|
|
715
|
+
"session": {
|
|
716
|
+
"session_id": str(session.get("_id", "")),
|
|
717
|
+
"arxiv_id": session.get("arxiv_id", ""),
|
|
718
|
+
"title": session.get("title", ""),
|
|
719
|
+
"messages": session.get("messages", []),
|
|
720
|
+
"created_at": session.get("created_at", ""),
|
|
721
|
+
"last_activity": session.get("last_activity", "")
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
except HTTPException:
|
|
725
|
+
raise
|
|
726
|
+
except Exception as e:
|
|
727
|
+
logger.error(f"Failed to get chat session: {e}", exc_info=True)
|
|
728
|
+
raise HTTPException(status_code=500, detail="Failed to get session")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@app.put("/chat/session/{session_id}")
|
|
732
|
+
async def update_chat_session(
|
|
733
|
+
session_id: str,
|
|
734
|
+
messages: List[Dict[str, Any]],
|
|
735
|
+
current_user: Dict = Depends(verify_token)
|
|
736
|
+
):
|
|
737
|
+
"""Update chat session with new messages"""
|
|
738
|
+
try:
|
|
739
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
740
|
+
|
|
741
|
+
from bson import ObjectId
|
|
742
|
+
# Verify session belongs to user
|
|
743
|
+
session = await unified_database_service.find_one(
|
|
744
|
+
"chat_sessions",
|
|
745
|
+
{"_id": ObjectId(session_id), "user_name": user_name}
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if not session:
|
|
749
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
750
|
+
|
|
751
|
+
# Update messages and last_activity
|
|
752
|
+
result = await unified_database_service.update_one(
|
|
753
|
+
"chat_sessions",
|
|
754
|
+
{"_id": ObjectId(session_id)},
|
|
755
|
+
{
|
|
756
|
+
"$set": {
|
|
757
|
+
"messages": messages,
|
|
758
|
+
"last_activity": datetime.utcnow()
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
if result and getattr(result, 'modified_count', 0) > 0:
|
|
764
|
+
return {
|
|
765
|
+
"success": True,
|
|
766
|
+
"message": "Session updated",
|
|
767
|
+
"message_count": len(messages)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# Even if no modification (same data), return success
|
|
771
|
+
return {
|
|
772
|
+
"success": True,
|
|
773
|
+
"message": "Session update processed",
|
|
774
|
+
"message_count": len(messages)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
except HTTPException:
|
|
778
|
+
raise
|
|
779
|
+
except Exception as e:
|
|
780
|
+
logger.error(f"Failed to update chat session: {e}", exc_info=True)
|
|
781
|
+
raise HTTPException(status_code=500, detail="Failed to update session")
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@app.delete("/chat/session/{session_id}")
|
|
785
|
+
async def delete_chat_session(
|
|
786
|
+
session_id: str,
|
|
787
|
+
current_user: Dict = Depends(verify_token)
|
|
788
|
+
):
|
|
789
|
+
"""Delete a chat session"""
|
|
790
|
+
try:
|
|
791
|
+
user_name = current_user.get("user_name") or current_user.get("email", "").split("@")[0]
|
|
792
|
+
|
|
793
|
+
from bson import ObjectId
|
|
794
|
+
result = await unified_database_service.delete_one(
|
|
795
|
+
"chat_sessions",
|
|
796
|
+
{"_id": ObjectId(session_id), "user_name": user_name}
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
if result and getattr(result, 'deleted_count', 0) > 0:
|
|
800
|
+
return {"success": True, "message": "Session deleted"}
|
|
801
|
+
|
|
802
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
803
|
+
|
|
804
|
+
except HTTPException:
|
|
805
|
+
raise
|
|
806
|
+
except Exception as e:
|
|
807
|
+
logger.error(f"Failed to delete chat session: {e}", exc_info=True)
|
|
808
|
+
raise HTTPException(status_code=500, detail="Failed to delete session")
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# =============================================================================
|
|
812
|
+
# ANALYSIS ENDPOINTS
|
|
813
|
+
# =============================================================================
|
|
814
|
+
|
|
815
|
+
@app.get("/papers/user")
|
|
816
|
+
async def get_user_papers(
|
|
817
|
+
current_user: Dict = Depends(verify_token),
|
|
818
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
819
|
+
skip: int = Query(default=0, ge=0)
|
|
820
|
+
):
|
|
821
|
+
"""Get papers associated with current user"""
|
|
822
|
+
try:
|
|
823
|
+
logger.debug(f"Fetching papers for user: {current_user['email']} (limit={limit}, skip={skip})")
|
|
824
|
+
papers = await unified_paper_service.get_user_papers(
|
|
825
|
+
user_email=current_user["email"],
|
|
826
|
+
limit=limit,
|
|
827
|
+
skip=skip
|
|
828
|
+
)
|
|
829
|
+
logger.debug(f"Retrieved {len(papers)} papers for user: {current_user['email']}")
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
"success": True,
|
|
833
|
+
"papers": papers,
|
|
834
|
+
"count": len(papers)
|
|
835
|
+
}
|
|
836
|
+
except Exception as e:
|
|
837
|
+
logger.error(f"Failed to get user papers: {e}", exc_info=True)
|
|
838
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve papers")
|
|
839
|
+
|
|
840
|
+
# Daily Analysis Endpoints
|
|
841
|
+
@app.get("/analysis/daily")
|
|
842
|
+
async def get_daily_analysis(
|
|
843
|
+
date: Optional[str] = Query(default=None),
|
|
844
|
+
current_user: Dict = Depends(verify_token)
|
|
845
|
+
):
|
|
846
|
+
"""Get daily analysis for a specific date"""
|
|
847
|
+
try:
|
|
848
|
+
if date:
|
|
849
|
+
analysis_date = datetime.fromisoformat(date)
|
|
850
|
+
else:
|
|
851
|
+
analysis_date = datetime.utcnow()
|
|
852
|
+
|
|
853
|
+
logger.info(f"Fetching daily analysis for date: {analysis_date.isoformat()}")
|
|
854
|
+
analysis = await unified_analysis_service.get_daily_analysis(analysis_date)
|
|
855
|
+
logger.debug(f"Daily analysis retrieved for: {analysis_date.isoformat()}")
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
"success": True,
|
|
859
|
+
"analysis": analysis,
|
|
860
|
+
"date": analysis_date.isoformat()
|
|
861
|
+
}
|
|
862
|
+
except Exception as e:
|
|
863
|
+
logger.error(f"Failed to get daily analysis: {e}", exc_info=True)
|
|
864
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve analysis")
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
@app.get("/daily/dose")
|
|
868
|
+
async def get_daily_dose(current_user: Dict = Depends(verify_token)):
|
|
869
|
+
"""Get user's latest daily dose"""
|
|
870
|
+
try:
|
|
871
|
+
user_id = current_user.get("id") or current_user.get("user_id") or str(current_user.get("_id", ""))
|
|
872
|
+
|
|
873
|
+
# Get the latest daily dose from the database
|
|
874
|
+
daily_dose = await unified_database_service.find_one(
|
|
875
|
+
"daily_dose",
|
|
876
|
+
{"user_id": user_id},
|
|
877
|
+
sort=[("generated_at", -1)]
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
if not daily_dose:
|
|
881
|
+
return {
|
|
882
|
+
"success": False,
|
|
883
|
+
"message": "No daily dose found. Generate one first.",
|
|
884
|
+
"dose": None
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
# Convert ObjectId to string
|
|
888
|
+
if "_id" in daily_dose:
|
|
889
|
+
daily_dose["_id"] = str(daily_dose["_id"])
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
"success": True,
|
|
893
|
+
"dose": daily_dose
|
|
894
|
+
}
|
|
895
|
+
except Exception as e:
|
|
896
|
+
logger.error(f"Failed to get daily dose: {e}", exc_info=True)
|
|
897
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve daily dose")
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@app.post("/daily/run")
|
|
901
|
+
async def run_daily_dose(current_user: Dict = Depends(verify_token)):
|
|
902
|
+
"""Run daily dose analysis for the user"""
|
|
903
|
+
try:
|
|
904
|
+
from .services.unified_daily_dose_service import unified_daily_dose_service
|
|
905
|
+
|
|
906
|
+
user_id = current_user.get("id") or current_user.get("user_id") or str(current_user.get("_id", ""))
|
|
907
|
+
|
|
908
|
+
logger.info(f"Running daily dose for user {user_id}")
|
|
909
|
+
|
|
910
|
+
result = await unified_daily_dose_service.execute_daily_dose(user_id=user_id)
|
|
911
|
+
|
|
912
|
+
return result
|
|
913
|
+
except Exception as e:
|
|
914
|
+
logger.error(f"Failed to run daily dose: {e}", exc_info=True)
|
|
915
|
+
raise HTTPException(status_code=500, detail="Failed to run daily dose")
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@app.get("/daily/settings")
|
|
919
|
+
async def get_daily_dose_settings(current_user: Dict = Depends(verify_token)):
|
|
920
|
+
"""Get user's daily dose settings"""
|
|
921
|
+
try:
|
|
922
|
+
from .services.unified_daily_dose_service import unified_daily_dose_service
|
|
923
|
+
|
|
924
|
+
user_id = current_user.get("id") or current_user.get("user_id") or str(current_user.get("_id", ""))
|
|
925
|
+
|
|
926
|
+
result = await unified_daily_dose_service.get_user_daily_dose_settings(user_id)
|
|
927
|
+
|
|
928
|
+
return result
|
|
929
|
+
except Exception as e:
|
|
930
|
+
logger.error(f"Failed to get daily dose settings: {e}", exc_info=True)
|
|
931
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve settings")
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@app.put("/daily/settings")
|
|
935
|
+
async def update_daily_dose_settings(
|
|
936
|
+
settings: Dict[str, Any],
|
|
937
|
+
current_user: Dict = Depends(verify_token)
|
|
938
|
+
):
|
|
939
|
+
"""Update user's daily dose settings"""
|
|
940
|
+
try:
|
|
941
|
+
from .services.unified_daily_dose_service import unified_daily_dose_service
|
|
942
|
+
|
|
943
|
+
user_id = current_user.get("id") or current_user.get("user_id") or str(current_user.get("_id", ""))
|
|
944
|
+
|
|
945
|
+
result = await unified_daily_dose_service.update_user_daily_dose_settings(
|
|
946
|
+
user_id=user_id,
|
|
947
|
+
keywords=settings.get("keywords"),
|
|
948
|
+
max_papers=settings.get("max_papers"),
|
|
949
|
+
scheduled_time=settings.get("scheduled_time"),
|
|
950
|
+
enabled=settings.get("enabled")
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
return result
|
|
954
|
+
except Exception as e:
|
|
955
|
+
logger.error(f"Failed to update daily dose settings: {e}", exc_info=True)
|
|
956
|
+
raise HTTPException(status_code=500, detail="Failed to update settings")
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@app.post("/analysis/daily/trigger")
|
|
960
|
+
async def trigger_daily_analysis(current_user: Dict = Depends(verify_token)):
|
|
961
|
+
"""Manually trigger daily analysis"""
|
|
962
|
+
try:
|
|
963
|
+
logger.info("Manually triggering daily analysis")
|
|
964
|
+
analysis_task = asyncio.create_task(
|
|
965
|
+
unified_analysis_service.run_daily_analysis()
|
|
966
|
+
)
|
|
967
|
+
logger.info("Daily analysis task started")
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
"success": True,
|
|
971
|
+
"message": "Daily analysis triggered",
|
|
972
|
+
"status": "running"
|
|
973
|
+
}
|
|
974
|
+
except Exception as e:
|
|
975
|
+
logger.error(f"Failed to trigger daily analysis: {e}", exc_info=True)
|
|
976
|
+
raise HTTPException(status_code=500, detail="Failed to trigger analysis")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
async def main():
|
|
980
|
+
"""Main function to run the server"""
|
|
981
|
+
import uvicorn
|
|
982
|
+
|
|
983
|
+
config = uvicorn.Config(
|
|
984
|
+
app=app,
|
|
985
|
+
host="0.0.0.0",
|
|
986
|
+
port=8000,
|
|
987
|
+
reload=False,
|
|
988
|
+
log_level="info"
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
server = uvicorn.Server(config)
|
|
992
|
+
|
|
993
|
+
logger.info("Starting ArionXiv API server on http://0.0.0.0:8000")
|
|
994
|
+
logger.info("API Documentation: http://0.0.0.0:8000/docs")
|
|
995
|
+
logger.info("Health Check: http://0.0.0.0:8000/health")
|
|
996
|
+
|
|
997
|
+
await server.serve()
|
|
998
|
+
|
|
999
|
+
if __name__ == "__main__":
|
|
1000
|
+
asyncio.run(main())
|