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,254 @@
1
+ """Enhanced Configuration Manager with Database Integration"""
2
+
3
+ import os
4
+ import json
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Dict, Any, Optional
8
+ from rich.console import Console
9
+
10
+ # Import from parent directory
11
+ import sys
12
+ backend_path = Path(__file__).parent.parent.parent
13
+ sys.path.insert(0, str(backend_path))
14
+
15
+ from ..ui.global_theme_manager import global_theme_manager
16
+ from ...services.unified_user_service import unified_user_service
17
+
18
+ console = Console()
19
+
20
+ class DatabaseConfigManager:
21
+ """Manages CLI configuration with database integration for authenticated users"""
22
+
23
+ def __init__(self):
24
+ self.config_dir = Path.home() / ".arionxiv"
25
+ self.local_config_file = self.config_dir / "local_config.json"
26
+ self.config = {}
27
+ self._initialized = False
28
+
29
+ # Ensure directories exist
30
+ self.config_dir.mkdir(exist_ok=True)
31
+
32
+ def _ensure_initialized(self):
33
+ """Ensure basic initialization without async calls"""
34
+ if not self._initialized:
35
+ try:
36
+ self.config = self._load_local_config()
37
+ self._initialized = True
38
+ except Exception:
39
+ self.config = self.get_default_config()
40
+ self._initialized = True
41
+
42
+ async def load_config(self, quiet: bool = True) -> Dict[str, Any]:
43
+ """Load configuration (from database if authenticated, local otherwise)"""
44
+ try:
45
+ # Check if user is authenticated
46
+ if unified_user_service.is_authenticated():
47
+ return await self._load_from_database(quiet=quiet)
48
+ else:
49
+ return self._load_local_config(quiet=quiet)
50
+ except Exception:
51
+ # Silently fall back to defaults - no need to spam console
52
+ return self.get_default_config()
53
+
54
+ async def _load_from_database(self, quiet: bool = True) -> Dict[str, Any]:
55
+ """Load configuration - uses local config (hosted API handles cloud data)"""
56
+ # With hosted Vercel API, we don't need local MongoDB
57
+ # Settings are synced via API, local config is used for CLI preferences
58
+ return self._load_local_config(quiet=quiet)
59
+
60
+ def _load_local_config(self, quiet: bool = True) -> Dict[str, Any]:
61
+ """Load configuration from local file"""
62
+ try:
63
+ if self.local_config_file.exists():
64
+ with open(self.local_config_file, 'r') as f:
65
+ self.config = json.load(f)
66
+ else:
67
+ self.config = self.get_default_config()
68
+ self._save_local_config()
69
+
70
+ self.config["database_mode"] = False
71
+ return self.config
72
+
73
+ except Exception:
74
+ # Silently fall back to defaults
75
+ self.config = self.get_default_config()
76
+ self.config["database_mode"] = False
77
+ return self.config
78
+
79
+ def _save_local_config(self) -> bool:
80
+ """Save configuration to local file"""
81
+ try:
82
+ with open(self.local_config_file, 'w') as f:
83
+ json.dump(self.config, f, indent=2)
84
+ return True
85
+ except Exception as e:
86
+ console.print(f"[red]Error saving local config: {e}[/red]")
87
+ return False
88
+
89
+ async def save_config(self) -> bool:
90
+ """Save configuration (to database if authenticated, local otherwise)"""
91
+ try:
92
+ if unified_user_service.is_authenticated():
93
+ return await self._save_to_database()
94
+ else:
95
+ return self._save_local_config()
96
+ except Exception as e:
97
+ console.print(f"[red]Error saving config: {e}[/red]")
98
+ return False
99
+
100
+ async def _save_to_database(self) -> bool:
101
+ """Save configuration locally (hosted API handles cloud sync separately)"""
102
+ # With hosted Vercel API, we save locally and API syncs settings
103
+ return self._save_local_config()
104
+
105
+ def get_default_config(self) -> Dict[str, Any]:
106
+ """Get default configuration"""
107
+ return {
108
+ "user": {
109
+ "id": "",
110
+ "user_name": "",
111
+ "email": "",
112
+ "full_name": "",
113
+ "preferences": {
114
+ "categories": ["cs.AI", "cs.LG", "cs.CL"],
115
+ "keywords": [],
116
+ "max_daily_papers": 10,
117
+ "analysis_depth": "standard",
118
+ "auto_download": False,
119
+ "email_notifications": False
120
+ }
121
+ },
122
+ "display": {
123
+ "theme": "auto",
124
+ "theme_color": "blue",
125
+ "table_style": "grid",
126
+ "show_abstracts": True,
127
+ "max_abstract_length": 200,
128
+ "papers_per_page": 10
129
+ },
130
+ "paths": {
131
+ "downloads": str(self.config_dir / "downloads"),
132
+ "data": str(self.config_dir / "data"),
133
+ "cache": str(self.config_dir / "data" / "cache")
134
+ },
135
+ "first_time_user": True,
136
+ "database_mode": False
137
+ }
138
+
139
+ def get(self, key: str, default: Any = None) -> Any:
140
+ """Get configuration value using dot notation"""
141
+ self._ensure_initialized()
142
+ keys = key.split('.')
143
+ value = self.config
144
+ for k in keys:
145
+ if isinstance(value, dict) and k in value:
146
+ value = value[k]
147
+ else:
148
+ return default
149
+ return value
150
+
151
+ async def set(self, key: str, value: Any) -> bool:
152
+ """Set configuration value using dot notation"""
153
+ keys = key.split('.')
154
+ config = self.config
155
+ for k in keys[:-1]:
156
+ if k not in config:
157
+ config[k] = {}
158
+ config = config[k]
159
+ config[keys[-1]] = value
160
+ return await self.save_config()
161
+
162
+ async def update_user_preferences(self, preferences: Dict[str, Any]) -> bool:
163
+ """Update user preferences"""
164
+ if "user" not in self.config:
165
+ self.config["user"] = {}
166
+ if "preferences" not in self.config["user"]:
167
+ self.config["user"]["preferences"] = {}
168
+
169
+ self.config["user"]["preferences"].update(preferences)
170
+ return await self.save_config()
171
+
172
+ def is_theme_configured(self) -> bool:
173
+ """Check if theme color has been configured by user"""
174
+ # If authenticated, theme is always considered configured
175
+ if unified_user_service.is_authenticated():
176
+ return True
177
+ return self.get("display.theme_color_configured", False)
178
+
179
+ async def set_theme_color(self, color: str) -> bool:
180
+ """Set the theme color and mark as configured"""
181
+ success = await self.set("display.theme_color", color)
182
+ if success:
183
+ # Update global theme manager
184
+ try:
185
+ global_theme_manager.set_theme(color)
186
+ except Exception:
187
+ pass # Silently ignore if theme manager not available
188
+
189
+ if not unified_user_service.is_authenticated():
190
+ # Only set configured flag for local config
191
+ success = await self.set("display.theme_color_configured", True)
192
+ return success
193
+
194
+ def get_theme_color(self) -> str:
195
+ """Get the current theme color"""
196
+ self._ensure_initialized()
197
+ color = self.get("display.theme_color", "blue")
198
+
199
+ # Sync with global theme manager
200
+ try:
201
+ current = global_theme_manager.get_current_theme()
202
+ if current != color:
203
+ global_theme_manager.set_theme(color)
204
+ except Exception:
205
+ pass # Silently ignore if theme manager not available
206
+
207
+ return color
208
+
209
+ def is_authenticated(self) -> bool:
210
+ """Check if user is authenticated"""
211
+ try:
212
+ return unified_user_service.is_authenticated()
213
+ except Exception:
214
+ return False
215
+
216
+ def get_current_user(self) -> Optional[Dict[str, Any]]:
217
+ """Get current authenticated user"""
218
+ try:
219
+ return unified_user_service.get_current_user()
220
+ except Exception:
221
+ return None
222
+
223
+ def is_database_mode(self) -> bool:
224
+ """Check if running in database mode"""
225
+ self._ensure_initialized()
226
+ return self.get("database_mode", False)
227
+
228
+ async def reset_config(self) -> bool:
229
+ """Reset configuration to defaults"""
230
+ # Reset local config (hosted API handles cloud settings separately)
231
+ self.config = self.get_default_config()
232
+ return self._save_local_config()
233
+
234
+ # Global configuration manager instance
235
+ db_config_manager = DatabaseConfigManager()
236
+
237
+ # For backward compatibility
238
+ def DbConfigManager():
239
+ """Factory function to get config manager"""
240
+ return db_config_manager
241
+
242
+ # Async wrapper for backward compatibility
243
+ def load_config_sync():
244
+ """Synchronous wrapper for loading config"""
245
+ try:
246
+ loop = asyncio.get_event_loop()
247
+ return loop.run_until_complete(db_config_manager.load_config())
248
+ except:
249
+ return db_config_manager.get_default_config()
250
+
251
+ # Helper function for synchronous access to default config
252
+ def get_default_config():
253
+ """Get default config synchronously"""
254
+ return db_config_manager.get_default_config()
@@ -0,0 +1,206 @@
1
+ """
2
+ GitHub Actions Runner for ArionXiv Daily Dose
3
+
4
+ This module is executed by GitHub Actions to run daily dose for users
5
+ who have their scheduled time matching the current hour (UTC).
6
+
7
+ Usage:
8
+ python -m arionxiv.github_actions_runner
9
+
10
+ Environment Variables:
11
+ MONGODB_URI: MongoDB connection string (required)
12
+ OPENROUTER_API_KEY: OpenRouter API key for LLM (required, FREE tier available)
13
+ GEMINI_API_KEY: Gemini API key for embeddings (optional)
14
+ GROQ_API_KEY: Groq API key as fallback LLM (optional)
15
+ FORCE_HOUR: Force run for specific hour (optional, for testing)
16
+ """
17
+
18
+ import asyncio
19
+ import logging
20
+ import os
21
+ import sys
22
+ from datetime import datetime, timezone
23
+ from typing import List, Dict, Any
24
+
25
+ # Configure logging for GitHub Actions
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
29
+ handlers=[logging.StreamHandler(sys.stdout)]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ async def get_users_for_hour(hour: int) -> List[Dict[str, Any]]:
35
+ """
36
+ Get all users who have daily dose enabled and scheduled for the given hour.
37
+
38
+ Args:
39
+ hour: Hour in 24-hour format (0-23)
40
+
41
+ Returns:
42
+ List of user documents matching the scheduled hour
43
+ """
44
+ from .services.unified_database_service import unified_database_service
45
+
46
+ try:
47
+ # Connect to MongoDB
48
+ await unified_database_service.connect_mongodb()
49
+
50
+ # Query users with daily dose enabled and matching hour
51
+ # Time format is "HH:MM", so we match the hour part
52
+ hour_prefix = f"{hour:02d}:"
53
+
54
+ # Access users collection directly via db attribute
55
+ users_collection = unified_database_service.db.users
56
+
57
+ # Find users where:
58
+ # 1. Daily dose is enabled
59
+ # 2. Scheduled time starts with the current hour
60
+ query = {
61
+ "$or": [
62
+ # Vercel API format: settings.daily_dose.enabled and settings.daily_dose.scheduled_time
63
+ {
64
+ "settings.daily_dose.enabled": True,
65
+ "settings.daily_dose.scheduled_time": {"$regex": f"^{hour_prefix}"}
66
+ },
67
+ # New format: preferences.daily_dose.enabled and preferences.daily_dose.scheduled_time
68
+ {
69
+ "preferences.daily_dose.enabled": True,
70
+ "preferences.daily_dose.scheduled_time": {"$regex": f"^{hour_prefix}"}
71
+ },
72
+ # Legacy format: preferences.daily_dose_enabled and preferences.daily_dose_time
73
+ {
74
+ "preferences.daily_dose_enabled": True,
75
+ "preferences.daily_dose_time": {"$regex": f"^{hour_prefix}"}
76
+ }
77
+ ]
78
+ }
79
+
80
+ cursor = users_collection.find(query)
81
+ users = await cursor.to_list(length=None)
82
+
83
+ logger.info(f"Found {len(users)} users scheduled for hour {hour:02d}:00 UTC")
84
+ return users
85
+
86
+ except Exception as e:
87
+ logger.error(f"Error fetching users for hour {hour}: {e}")
88
+ return []
89
+
90
+
91
+ async def run_daily_dose_for_user(user_id: str, user_email: str) -> bool:
92
+ """
93
+ Run daily dose for a specific user.
94
+
95
+ Args:
96
+ user_id: MongoDB user ID
97
+ user_email: User email for logging
98
+
99
+ Returns:
100
+ True if successful, False otherwise
101
+ """
102
+ from .services.unified_daily_dose_service import unified_daily_dose_service
103
+
104
+ try:
105
+ logger.info(f"Running daily dose for user: {user_email}")
106
+
107
+ # execute_daily_dose is the correct method name
108
+ result = await unified_daily_dose_service.execute_daily_dose(user_id)
109
+
110
+ if result.get("success"):
111
+ papers_count = result.get("papers_count", 0)
112
+ logger.info(f"Daily dose completed for {user_email}: {papers_count} papers analyzed")
113
+ return True
114
+ else:
115
+ error = result.get("message", "Unknown error")
116
+ logger.error(f"Daily dose failed for {user_email}: {error}")
117
+ return False
118
+
119
+ except Exception as e:
120
+ logger.error(f"Exception running daily dose for {user_email}: {e}")
121
+ return False
122
+
123
+
124
+ async def main():
125
+ """Main entry point for GitHub Actions runner."""
126
+ from .services.unified_database_service import unified_database_service
127
+
128
+ logger.info("=" * 60)
129
+ logger.info("ArionXiv Daily Dose - GitHub Actions Runner")
130
+ logger.info("=" * 60)
131
+
132
+ # Check required environment variables
133
+ # OpenRouter is the primary LLM provider (FREE tier available)
134
+ required_vars = ["MONGODB_URI", "OPENROUTER_API_KEY"]
135
+ missing_vars = [var for var in required_vars if not os.environ.get(var)]
136
+
137
+ if missing_vars:
138
+ logger.error(f"Missing required environment variables: {', '.join(missing_vars)}")
139
+ logger.error("Please add these as GitHub Actions secrets.")
140
+ logger.error("Get a FREE OpenRouter API key at: https://openrouter.ai/")
141
+ sys.exit(1)
142
+
143
+ # Determine the hour to process
144
+ force_hour = os.environ.get("FORCE_HOUR", "").strip()
145
+
146
+ if force_hour:
147
+ try:
148
+ current_hour = int(force_hour)
149
+ if not 0 <= current_hour <= 23:
150
+ raise ValueError("Hour must be between 0 and 23")
151
+ logger.info(f"Using forced hour: {current_hour:02d}:00 UTC")
152
+ except ValueError as e:
153
+ logger.error(f"Invalid FORCE_HOUR value '{force_hour}': {e}")
154
+ sys.exit(1)
155
+ else:
156
+ current_hour = datetime.now(timezone.utc).hour
157
+ logger.info(f"Current time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC")
158
+ logger.info(f"Processing hour: {current_hour:02d}:00 UTC")
159
+
160
+ try:
161
+ # Get users scheduled for this hour
162
+ users = await get_users_for_hour(current_hour)
163
+
164
+ if not users:
165
+ logger.info(f"No users scheduled for {current_hour:02d}:00 UTC. Exiting.")
166
+ return
167
+
168
+ # Process each user
169
+ success_count = 0
170
+ failure_count = 0
171
+
172
+ for user in users:
173
+ user_id = str(user["_id"])
174
+ user_email = user.get("email", "unknown")
175
+
176
+ if await run_daily_dose_for_user(user_id, user_email):
177
+ success_count += 1
178
+ else:
179
+ failure_count += 1
180
+
181
+ # Small delay between users to avoid rate limiting
182
+ await asyncio.sleep(2)
183
+
184
+ # Summary
185
+ logger.info("=" * 60)
186
+ logger.info("Daily Dose Run Complete")
187
+ logger.info(f" Successful: {success_count}")
188
+ logger.info(f" Failed: {failure_count}")
189
+ logger.info(f" Total: {len(users)}")
190
+ logger.info("=" * 60)
191
+
192
+ # Exit with error if any failures
193
+ if failure_count > 0:
194
+ sys.exit(1)
195
+
196
+ finally:
197
+ # Always cleanup database connection
198
+ try:
199
+ await unified_database_service.disconnect()
200
+ logger.info("Database connection closed")
201
+ except Exception as e:
202
+ logger.warning(f"Error closing database connection: {e}")
203
+
204
+
205
+ if __name__ == "__main__":
206
+ asyncio.run(main())
arionxiv/main.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ Main CLI entry point for ArionXiv package
3
+ """
4
+
5
+ import sys
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def main():
11
+ """Main entry point for the arionxiv CLI command"""
12
+ try:
13
+ from .cli.main import cli
14
+ cli()
15
+ except KeyboardInterrupt:
16
+ logger.info("Operation cancelled by user")
17
+ sys.exit(0)
18
+ except Exception as e:
19
+ logger.error(f"CLI error: {e}", exc_info=True)
20
+ sys.exit(1)
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,9 @@
1
+ """Public interface for the prompts module."""
2
+
3
+ from .prompts import format_prompt, format_prompt_async, get_all_prompts
4
+
5
+ __all__ = [
6
+ 'format_prompt',
7
+ 'format_prompt_async',
8
+ 'get_all_prompts'
9
+ ]