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
|
@@ -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
|
+
]
|