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
@@ -0,0 +1,604 @@
1
+ """
2
+ Unified Paper Service for ArionXiv
3
+ Consolidates paper_service.py, paper_library_manager.py, and arxiv_service.py
4
+ Provides comprehensive paper management, ArXiv fetching, and library navigation
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import asyncio
10
+ from pathlib import Path
11
+ from typing import Dict, List, Any, Optional
12
+ from datetime import datetime, timedelta
13
+ import logging
14
+
15
+ from rich.console import Console
16
+ from rich.table import Table
17
+ from rich.panel import Panel
18
+ from rich.prompt import Prompt, Confirm
19
+ from rich.text import Text
20
+
21
+ from .unified_database_service import unified_database_service
22
+ from .unified_config_service import unified_config_service
23
+ from ..arxiv_operations.client import arxiv_client
24
+ from ..arxiv_operations.fetcher import arxiv_fetcher
25
+
26
+ try:
27
+ from ..cli.ui.theme_system import create_themed_console, style_text, get_theme_colors
28
+ except ImportError:
29
+ try:
30
+ from ..cli.ui.theme import create_themed_console, style_text, get_theme_colors
31
+ except ImportError:
32
+ # Fallback for when running without CLI context (e.g., server mode)
33
+ def create_themed_console():
34
+ from rich.console import Console
35
+ return Console()
36
+ def style_text(text, style=None):
37
+ return text
38
+ def get_theme_colors():
39
+ return {}
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class UnifiedPaperService:
45
+ """
46
+ Comprehensive paper service that handles:
47
+ 1. Paper storage and retrieval (paper_service.py functionality)
48
+ 2. ArXiv paper fetching based on preferences (arxiv_service.py functionality)
49
+ 3. Interactive paper library management (paper_library_manager.py functionality)
50
+ """
51
+
52
+ def __init__(self, database_client_instance=None):
53
+ # ArXiv configuration
54
+ self.config = unified_config_service.get_arxiv_config()
55
+ self.max_results_per_query = self.config["max_results_per_query"]
56
+ self.search_days_back = self.config["search_days_back"]
57
+ self.default_categories = self.config["default_categories"]
58
+ self._database_client = database_client_instance
59
+
60
+ # Console for interactive UI
61
+ self.console = create_themed_console()
62
+
63
+ logger.info("UnifiedPaperService initialized")
64
+
65
+ def _get_database_client(self):
66
+ """Get database client instance"""
67
+ if self._database_client is not None:
68
+ return self._database_client
69
+ return unified_database_service
70
+
71
+ # ====================
72
+ # PAPER STORAGE & RETRIEVAL (from paper_service.py)
73
+ # ====================
74
+
75
+ async def save_paper(self, paper_data: Dict[str, Any]) -> Dict[str, Any]:
76
+ """Save a research paper"""
77
+ try:
78
+ db_client = self._get_database_client()
79
+ return await db_client.save_paper(paper_data)
80
+ except Exception as e:
81
+ logger.error("Failed to save paper", error=str(e))
82
+ return {"success": False, "message": str(e)}
83
+
84
+ async def get_paper_by_id(self, paper_id: str) -> Dict[str, Any]:
85
+ """Get paper by ID"""
86
+ try:
87
+ db_client = self._get_database_client()
88
+ paper = await db_client.db.papers.find_one({"arxiv_id": paper_id})
89
+ if paper:
90
+ paper["_id"] = str(paper["_id"])
91
+ return {"success": True, "paper": paper}
92
+ return {"success": False, "message": "Paper not found"}
93
+ except Exception as e:
94
+ logger.error("Failed to get paper", paper_id=paper_id, error=str(e))
95
+ return {"success": False, "message": str(e)}
96
+
97
+ async def get_papers_by_user(self, user_id: str, limit: int = 50) -> Dict[str, Any]:
98
+ """Get papers associated with a user"""
99
+ try:
100
+ db_client = self._get_database_client()
101
+ papers = await db_client.db.papers.find(
102
+ {"user_id": user_id}
103
+ ).limit(limit).to_list(limit)
104
+
105
+ # Convert ObjectId to string
106
+ for paper in papers:
107
+ paper["_id"] = str(paper["_id"])
108
+
109
+ return {"success": True, "papers": papers, "count": len(papers)}
110
+ except Exception as e:
111
+ logger.error("Failed to get user papers", user_id=user_id, error=str(e))
112
+ return {"success": False, "message": str(e)}
113
+
114
+ async def search_papers(self, query: str, filters: Dict[str, Any] = None) -> Dict[str, Any]:
115
+ """Search papers with text query and filters"""
116
+ try:
117
+ db_client = self._get_database_client()
118
+ search_filter = {"$text": {"$search": query}}
119
+
120
+ if filters:
121
+ search_filter.update(filters)
122
+
123
+ papers = await db_client.db.papers.find(search_filter).to_list(100)
124
+
125
+ # Convert ObjectId to string
126
+ for paper in papers:
127
+ paper["_id"] = str(paper["_id"])
128
+
129
+ return {"success": True, "papers": papers, "count": len(papers)}
130
+ except Exception as e:
131
+ logger.error("Failed to search papers", query=query, error=str(e))
132
+ return {"success": False, "message": str(e)}
133
+
134
+ async def update_paper(self, paper_id: str, updates: Dict[str, Any]) -> Dict[str, Any]:
135
+ """Update paper data"""
136
+ try:
137
+ db_client = self._get_database_client()
138
+ result = await db_client.db.papers.update_one(
139
+ {"arxiv_id": paper_id},
140
+ {"$set": {**updates, "updated_at": datetime.utcnow()}}
141
+ )
142
+
143
+ if result.modified_count > 0:
144
+ return {"success": True, "message": "Paper updated successfully"}
145
+ else:
146
+ return {"success": False, "message": "Paper not found or no changes made"}
147
+ except Exception as e:
148
+ logger.error("Failed to update paper", paper_id=paper_id, error=str(e))
149
+ return {"success": False, "message": str(e)}
150
+
151
+ async def delete_paper(self, paper_id: str) -> Dict[str, Any]:
152
+ """Delete a paper"""
153
+ try:
154
+ db_client = self._get_database_client()
155
+ result = await db_client.db.papers.delete_one({"arxiv_id": paper_id})
156
+
157
+ if result.deleted_count > 0:
158
+ return {"success": True, "message": "Paper deleted successfully"}
159
+ else:
160
+ return {"success": False, "message": "Paper not found"}
161
+ except Exception as e:
162
+ logger.error("Failed to delete paper", paper_id=paper_id, error=str(e))
163
+ return {"success": False, "message": str(e)}
164
+
165
+ # ====================
166
+ # ARXIV FETCHING (from arxiv_service.py)
167
+ # ====================
168
+
169
+ async def fetch_papers_for_user(self, user_id: str) -> List[Dict[str, Any]]:
170
+ """Fetch papers based on user's preferences"""
171
+ try:
172
+ db_client = self._get_database_client()
173
+
174
+ # Get user preferences
175
+ user_prefs = await db_client.db.user_preferences.find_one({"user_id": user_id})
176
+ if not user_prefs:
177
+ logger.info(f"No preferences found for user {user_id}, using defaults")
178
+ user_prefs = {"categories": self.default_categories}
179
+
180
+ # Fetch papers for each category
181
+ all_papers = []
182
+ categories = user_prefs.get("categories", self.default_categories)
183
+ keywords = user_prefs.get("keywords", [])
184
+ authors = user_prefs.get("authors", [])
185
+
186
+ for category in categories:
187
+ try:
188
+ # Build search query
189
+ query_parts = [f"cat:{category}"]
190
+
191
+ if keywords:
192
+ keyword_query = " OR ".join([f'"{keyword}"' for keyword in keywords])
193
+ query_parts.append(f"({keyword_query})")
194
+
195
+ if authors:
196
+ author_query = " OR ".join([f"au:{author}" for author in authors])
197
+ query_parts.append(f"({author_query})")
198
+
199
+ search_query = " AND ".join(query_parts)
200
+
201
+ # Calculate date range
202
+ end_date = datetime.now()
203
+ start_date = end_date - timedelta(days=self.search_days_back)
204
+
205
+ # Fetch papers
206
+ papers = await arxiv_client.search_papers(
207
+ query=search_query,
208
+ max_results=self.max_results_per_query,
209
+ start_date=start_date,
210
+ end_date=end_date
211
+ )
212
+
213
+ # Add user context
214
+ for paper in papers:
215
+ paper["fetched_for_user"] = user_id
216
+ paper["fetched_at"] = datetime.utcnow()
217
+ paper["category_matched"] = category
218
+
219
+ all_papers.extend(papers)
220
+
221
+ except Exception as e:
222
+ logger.error(f"Failed to fetch papers for category {category}", error=str(e))
223
+ continue
224
+
225
+ # Remove duplicates based on arxiv_id
226
+ seen_ids = set()
227
+ unique_papers = []
228
+ for paper in all_papers:
229
+ if paper.get("id") not in seen_ids:
230
+ seen_ids.add(paper.get("id"))
231
+ unique_papers.append(paper)
232
+
233
+ logger.info(f"Fetched {len(unique_papers)} unique papers for user {user_id}")
234
+ return unique_papers
235
+
236
+ except Exception as e:
237
+ logger.error(f"Failed to fetch papers for user {user_id}", error=str(e))
238
+ return []
239
+
240
+ async def fetch_trending_papers(self, category: str = None, days: int = 7) -> List[Dict[str, Any]]:
241
+ """Fetch trending papers from ArXiv"""
242
+ try:
243
+ # Build query for trending papers
244
+ if category:
245
+ query = f"cat:{category}"
246
+ else:
247
+ query = "cat:cs.AI OR cat:cs.LG OR cat:cs.CL OR cat:stat.ML"
248
+
249
+ # Calculate date range
250
+ end_date = datetime.now()
251
+ start_date = end_date - timedelta(days=days)
252
+
253
+ # Fetch papers
254
+ papers = await arxiv_client.search_papers(
255
+ query=query,
256
+ max_results=self.max_results_per_query * 2, # Get more for trending
257
+ start_date=start_date,
258
+ end_date=end_date,
259
+ sort_by="submittedDate",
260
+ sort_order="descending"
261
+ )
262
+
263
+ # Add trending metadata
264
+ for paper in papers:
265
+ paper["is_trending"] = True
266
+ paper["fetched_at"] = datetime.utcnow()
267
+
268
+ logger.info(f"Fetched {len(papers)} trending papers")
269
+ return papers
270
+
271
+ except Exception as e:
272
+ logger.error("Failed to fetch trending papers", error=str(e))
273
+ return []
274
+
275
+ async def search_arxiv_papers(self, query: str, max_results: int = 20) -> List[Dict[str, Any]]:
276
+ """Search ArXiv directly with custom query"""
277
+ try:
278
+ papers = await arxiv_client.search_papers(
279
+ query=query,
280
+ max_results=max_results
281
+ )
282
+
283
+ for paper in papers:
284
+ paper["search_query"] = query
285
+ paper["fetched_at"] = datetime.utcnow()
286
+
287
+ logger.info(f"Found {len(papers)} papers for query: {query}")
288
+ return papers
289
+
290
+ except Exception as e:
291
+ logger.error(f"ArXiv search failed for query: {query}", error=str(e))
292
+ return []
293
+
294
+ # ====================
295
+ # LIBRARY MANAGEMENT (from paper_library_manager.py)
296
+ # ====================
297
+
298
+ async def show_paper_library(self, user_id: str = "default") -> Optional[str]:
299
+ """Show interactive paper library and return selected paper ID"""
300
+ try:
301
+ # Get user's papers from database
302
+ papers_result = await self.get_papers_by_user(user_id)
303
+
304
+ if not papers_result["success"] or not papers_result["papers"]:
305
+ self.console.print("[yellow]No papers found in your library.[/yellow]")
306
+
307
+ # Offer to fetch new papers
308
+ if Confirm.ask("Would you like to fetch some papers from ArXiv?"):
309
+ colors = get_theme_colors()
310
+ with self.console.status(f"[bold {colors['primary']}]Fetching papers..."):
311
+ new_papers = await self.fetch_papers_for_user(user_id)
312
+
313
+ if new_papers:
314
+ # Save fetched papers
315
+ for paper in new_papers:
316
+ await self.save_paper(paper)
317
+
318
+ self.console.print(f"[green]Fetched and saved {len(new_papers)} papers![/green]")
319
+ return await self.show_paper_library(user_id) # Recursive call
320
+ else:
321
+ self.console.print("[red]Failed to fetch papers.[/red]")
322
+ return None
323
+ else:
324
+ return None
325
+
326
+ papers = papers_result["papers"]
327
+ colors = get_theme_colors()
328
+
329
+ # Display papers in a table
330
+ table = Table(title="Your Paper Library", header_style=f"bold {colors['primary']}")
331
+ table.add_column("ID", style="bold white", no_wrap=True)
332
+ table.add_column("Title", style="white", max_width=50)
333
+ table.add_column("Authors", style="white", max_width=30)
334
+ table.add_column("Date", style="white")
335
+ table.add_column("Categories", style="white", max_width=20)
336
+
337
+ for i, paper in enumerate(papers):
338
+ # Truncate long titles and authors
339
+ title = paper.get("title", "Unknown")[:47] + "..." if len(paper.get("title", "")) > 50 else paper.get("title", "Unknown")
340
+
341
+ authors = paper.get("authors", [])
342
+ if isinstance(authors, list):
343
+ authors_str = ", ".join(authors[:2]) # Show first 2 authors
344
+ if len(authors) > 2:
345
+ authors_str += f" (+{len(authors)-2} more)"
346
+ else:
347
+ authors_str = str(authors)[:27] + "..." if len(str(authors)) > 30 else str(authors)
348
+
349
+ date_str = paper.get("published", paper.get("submitted", "Unknown"))[:10] # Just date part
350
+
351
+ categories = paper.get("categories", [])
352
+ if isinstance(categories, list):
353
+ cat_str = ", ".join(categories[:2])
354
+ if len(categories) > 2:
355
+ cat_str += "..."
356
+ else:
357
+ cat_str = str(categories)[:17] + "..." if len(str(categories)) > 20 else str(categories)
358
+
359
+ table.add_row(
360
+ str(i + 1),
361
+ title,
362
+ authors_str,
363
+ date_str,
364
+ cat_str
365
+ )
366
+
367
+ self.console.print(table)
368
+
369
+ # Show options
370
+ colors = get_theme_colors()
371
+ self.console.print(f"\n[bold {colors['primary']}]Options:[/bold {colors['primary']}]")
372
+ self.console.print("• Enter a number (1-{}) to select a paper".format(len(papers)))
373
+ self.console.print("• Type 'search' to search your library")
374
+ self.console.print("• Type 'fetch' to get new papers from ArXiv")
375
+ self.console.print("• Type 'refresh' to refresh the library")
376
+ self.console.print("• Type 'quit' to exit")
377
+
378
+ while True:
379
+ choice = Prompt.ask("\n[bold green]Your choice[/bold green]").strip().lower()
380
+
381
+ if choice == 'quit':
382
+ return None
383
+ elif choice == 'search':
384
+ return await self._handle_library_search(user_id)
385
+ elif choice == 'fetch':
386
+ return await self._handle_fetch_new_papers(user_id)
387
+ elif choice == 'refresh':
388
+ return await self.show_paper_library(user_id) # Recursive refresh
389
+ else:
390
+ try:
391
+ paper_index = int(choice) - 1
392
+ if 0 <= paper_index < len(papers):
393
+ selected_paper = papers[paper_index]
394
+ paper_id = selected_paper.get("arxiv_id", selected_paper.get("id"))
395
+
396
+ # Show paper details
397
+ self._show_paper_details(selected_paper)
398
+
399
+ if Confirm.ask("Use this paper?"):
400
+ return paper_id
401
+ else:
402
+ self.console.print(f"[red]Please enter a number between 1 and {len(papers)}[/red]")
403
+ except ValueError:
404
+ self.console.print("[red]Please enter a valid number or command[/red]")
405
+
406
+ except Exception as e:
407
+ logger.error("Paper library display failed", error=str(e))
408
+ self.console.print(f"[red]Error showing library: {str(e)}[/red]")
409
+ return None
410
+
411
+ async def _handle_library_search(self, user_id: str) -> Optional[str]:
412
+ """Handle library search functionality"""
413
+ query = Prompt.ask("Enter search terms")
414
+ colors = get_theme_colors()
415
+
416
+ with self.console.status(f"[bold {colors['primary']}]Searching for '{query}'..."):
417
+ search_result = await self.search_papers(query, {"user_id": user_id})
418
+
419
+ if not search_result["success"] or not search_result["papers"]:
420
+ self.console.print("[yellow]No papers found matching your search.[/yellow]")
421
+ return await self.show_paper_library(user_id)
422
+
423
+ # Show search results
424
+ papers = search_result["papers"]
425
+ colors = get_theme_colors()
426
+ self.console.print(f"\n[{colors['primary']}]Found {len(papers)} papers matching '{query}':[/{colors['primary']}]")
427
+
428
+ # Create simplified table for search results
429
+ table = Table(header_style=f"bold {colors['primary']}")
430
+ table.add_column("ID", style="bold white")
431
+ table.add_column("Title", style="white", max_width=60)
432
+ table.add_column("Relevance", style="white")
433
+
434
+ for i, paper in enumerate(papers):
435
+ title = paper.get("title", "Unknown")[:57] + "..." if len(paper.get("title", "")) > 60 else paper.get("title", "Unknown")
436
+ table.add_row(str(i + 1), title, "***") # Placeholder relevance
437
+
438
+ self.console.print(table)
439
+
440
+ # Let user select from search results
441
+ while True:
442
+ choice = Prompt.ask("Select a paper (number) or 'back' to return to library")
443
+
444
+ if choice.lower() == 'back':
445
+ return await self.show_paper_library(user_id)
446
+
447
+ try:
448
+ paper_index = int(choice) - 1
449
+ if 0 <= paper_index < len(papers):
450
+ selected_paper = papers[paper_index]
451
+ paper_id = selected_paper.get("arxiv_id", selected_paper.get("id"))
452
+
453
+ self._show_paper_details(selected_paper)
454
+
455
+ if Confirm.ask("Use this paper?"):
456
+ return paper_id
457
+ else:
458
+ self.console.print(f"[red]Please enter a number between 1 and {len(papers)}[/red]")
459
+ except ValueError:
460
+ self.console.print("[red]Please enter a valid number or 'back'[/red]")
461
+
462
+ async def _handle_fetch_new_papers(self, user_id: str) -> Optional[str]:
463
+ """Handle fetching new papers"""
464
+ colors = get_theme_colors()
465
+ self.console.print(f"[{colors['primary']}]Fetching new papers based on your preferences...[/{colors['primary']}]")
466
+
467
+ with self.console.status(f"[bold {colors['primary']}]Fetching from ArXiv..."):
468
+ new_papers = await self.fetch_papers_for_user(user_id)
469
+
470
+ if not new_papers:
471
+ self.console.print("[red]Failed to fetch new papers.[/red]")
472
+ return await self.show_paper_library(user_id)
473
+
474
+ # Save new papers
475
+ saved_count = 0
476
+ for paper in new_papers:
477
+ result = await self.save_paper(paper)
478
+ if result.get("success"):
479
+ saved_count += 1
480
+
481
+ self.console.print(f"[green]Fetched and saved {saved_count} new papers![/green]")
482
+
483
+ if Confirm.ask("View the updated library?"):
484
+ return await self.show_paper_library(user_id)
485
+ else:
486
+ return None
487
+
488
+ def _show_paper_details(self, paper: Dict[str, Any]):
489
+ """Show detailed information about a paper"""
490
+ title = paper.get("title", "Unknown Title")
491
+ authors = paper.get("authors", [])
492
+ abstract = paper.get("abstract", "No abstract available")
493
+ categories = paper.get("categories", [])
494
+ published = paper.get("published", paper.get("submitted", "Unknown"))
495
+
496
+ # Format authors
497
+ if isinstance(authors, list):
498
+ authors_str = ", ".join(authors)
499
+ else:
500
+ authors_str = str(authors)
501
+
502
+ # Format categories
503
+ if isinstance(categories, list):
504
+ categories_str = ", ".join(categories)
505
+ else:
506
+ categories_str = str(categories)
507
+
508
+ # Create detail panel
509
+ detail_text = f"""[bold]Title:[/bold] {title}
510
+
511
+ [bold]Authors:[/bold] {authors_str}
512
+
513
+ [bold]Published:[/bold] {published}
514
+
515
+ [bold]Categories:[/bold] {categories_str}
516
+
517
+ [bold]Abstract:[/bold]
518
+ {abstract[:500]}{'...' if len(abstract) > 500 else ''}"""
519
+
520
+ colors = get_theme_colors()
521
+ panel = Panel(
522
+ detail_text,
523
+ title=f"[bold {colors['primary']}]Paper Details[/bold {colors['primary']}]",
524
+ border_style=colors['primary'],
525
+ padding=(1, 2)
526
+ )
527
+
528
+ self.console.print(panel)
529
+
530
+ # ====================
531
+ # ENHANCED OPERATIONS
532
+ # ====================
533
+
534
+ async def get_paper_statistics(self, user_id: str = None) -> Dict[str, Any]:
535
+ """Get statistics about papers in the library"""
536
+ try:
537
+ db_client = self._get_database_client()
538
+
539
+ # Build filter
540
+ filter_query = {}
541
+ if user_id:
542
+ filter_query["user_id"] = user_id
543
+
544
+ # Get counts
545
+ total_papers = await db_client.db.papers.count_documents(filter_query)
546
+
547
+ # Get papers by category
548
+ pipeline = [
549
+ {"$match": filter_query},
550
+ {"$unwind": "$categories"},
551
+ {"$group": {"_id": "$categories", "count": {"$sum": 1}}},
552
+ {"$sort": {"count": -1}}
553
+ ]
554
+
555
+ category_stats = await db_client.db.papers.aggregate(pipeline).to_list(50)
556
+
557
+ # Get recent papers (last 30 days)
558
+ recent_date = datetime.utcnow() - timedelta(days=30)
559
+ recent_papers = await db_client.db.papers.count_documents({
560
+ **filter_query,
561
+ "fetched_at": {"$gte": recent_date}
562
+ })
563
+
564
+ return {
565
+ "success": True,
566
+ "total_papers": total_papers,
567
+ "recent_papers": recent_papers,
568
+ "categories": category_stats
569
+ }
570
+
571
+ except Exception as e:
572
+ logger.error("Failed to get paper statistics", error=str(e))
573
+ return {"success": False, "error": str(e)}
574
+
575
+
576
+ # Global instance
577
+ unified_paper_service = UnifiedPaperService()
578
+
579
+ # Backwards compatibility
580
+ paper_service = unified_paper_service
581
+ paper_library_manager = unified_paper_service
582
+ arxiv_service = unified_paper_service
583
+
584
+ # Export commonly used functions
585
+ save_paper = unified_paper_service.save_paper
586
+ get_paper_by_id = unified_paper_service.get_paper_by_id
587
+ fetch_papers_for_user = unified_paper_service.fetch_papers_for_user
588
+ show_paper_library = unified_paper_service.show_paper_library
589
+ search_papers = unified_paper_service.search_papers
590
+ fetch_trending_papers = unified_paper_service.fetch_trending_papers
591
+
592
+ __all__ = [
593
+ 'UnifiedPaperService',
594
+ 'unified_paper_service',
595
+ 'paper_service',
596
+ 'paper_library_manager',
597
+ 'arxiv_service',
598
+ 'save_paper',
599
+ 'get_paper_by_id',
600
+ 'fetch_papers_for_user',
601
+ 'show_paper_library',
602
+ 'search_papers',
603
+ 'fetch_trending_papers'
604
+ ]