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.
Files changed (69) hide show
  1. arionxiv/__init__.py +40 -0
  2. arionxiv/__main__.py +10 -0
  3. arionxiv/arxiv_operations/__init__.py +0 -0
  4. arionxiv/arxiv_operations/client.py +225 -0
  5. arionxiv/arxiv_operations/fetcher.py +173 -0
  6. arionxiv/arxiv_operations/searcher.py +122 -0
  7. arionxiv/arxiv_operations/utils.py +293 -0
  8. arionxiv/cli/__init__.py +4 -0
  9. arionxiv/cli/commands/__init__.py +1 -0
  10. arionxiv/cli/commands/analyze.py +587 -0
  11. arionxiv/cli/commands/auth.py +365 -0
  12. arionxiv/cli/commands/chat.py +714 -0
  13. arionxiv/cli/commands/daily.py +482 -0
  14. arionxiv/cli/commands/fetch.py +217 -0
  15. arionxiv/cli/commands/library.py +295 -0
  16. arionxiv/cli/commands/preferences.py +426 -0
  17. arionxiv/cli/commands/search.py +254 -0
  18. arionxiv/cli/commands/settings_unified.py +1407 -0
  19. arionxiv/cli/commands/trending.py +41 -0
  20. arionxiv/cli/commands/welcome.py +168 -0
  21. arionxiv/cli/main.py +407 -0
  22. arionxiv/cli/ui/__init__.py +1 -0
  23. arionxiv/cli/ui/global_theme_manager.py +173 -0
  24. arionxiv/cli/ui/logo.py +127 -0
  25. arionxiv/cli/ui/splash.py +89 -0
  26. arionxiv/cli/ui/theme.py +32 -0
  27. arionxiv/cli/ui/theme_system.py +391 -0
  28. arionxiv/cli/utils/__init__.py +54 -0
  29. arionxiv/cli/utils/animations.py +522 -0
  30. arionxiv/cli/utils/api_client.py +583 -0
  31. arionxiv/cli/utils/api_config.py +505 -0
  32. arionxiv/cli/utils/command_suggestions.py +147 -0
  33. arionxiv/cli/utils/db_config_manager.py +254 -0
  34. arionxiv/github_actions_runner.py +206 -0
  35. arionxiv/main.py +23 -0
  36. arionxiv/prompts/__init__.py +9 -0
  37. arionxiv/prompts/prompts.py +247 -0
  38. arionxiv/rag_techniques/__init__.py +8 -0
  39. arionxiv/rag_techniques/basic_rag.py +1531 -0
  40. arionxiv/scheduler_daemon.py +139 -0
  41. arionxiv/server.py +1000 -0
  42. arionxiv/server_main.py +24 -0
  43. arionxiv/services/__init__.py +73 -0
  44. arionxiv/services/llm_client.py +30 -0
  45. arionxiv/services/llm_inference/__init__.py +58 -0
  46. arionxiv/services/llm_inference/groq_client.py +469 -0
  47. arionxiv/services/llm_inference/llm_utils.py +250 -0
  48. arionxiv/services/llm_inference/openrouter_client.py +564 -0
  49. arionxiv/services/unified_analysis_service.py +872 -0
  50. arionxiv/services/unified_auth_service.py +457 -0
  51. arionxiv/services/unified_config_service.py +456 -0
  52. arionxiv/services/unified_daily_dose_service.py +823 -0
  53. arionxiv/services/unified_database_service.py +1633 -0
  54. arionxiv/services/unified_llm_service.py +366 -0
  55. arionxiv/services/unified_paper_service.py +604 -0
  56. arionxiv/services/unified_pdf_service.py +522 -0
  57. arionxiv/services/unified_prompt_service.py +344 -0
  58. arionxiv/services/unified_scheduler_service.py +589 -0
  59. arionxiv/services/unified_user_service.py +954 -0
  60. arionxiv/utils/__init__.py +51 -0
  61. arionxiv/utils/api_helpers.py +200 -0
  62. arionxiv/utils/file_cleanup.py +150 -0
  63. arionxiv/utils/ip_helper.py +96 -0
  64. arionxiv-1.0.32.dist-info/METADATA +336 -0
  65. arionxiv-1.0.32.dist-info/RECORD +69 -0
  66. arionxiv-1.0.32.dist-info/WHEEL +5 -0
  67. arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
  68. arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
  69. 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())