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,344 @@
1
+ """
2
+ Unified Prompt Service for ArionXiv
3
+ Manages LLM prompts from MongoDB with on-demand loading and TTL caching
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from typing import Dict, Any, Optional, List, Tuple
9
+ from datetime import datetime, timedelta
10
+ from .unified_database_service import unified_database_service
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class UnifiedPromptService:
16
+ """
17
+ Service for managing LLM prompts stored in MongoDB.
18
+ Prompts are loaded on-demand and cached with TTL.
19
+ """
20
+
21
+ def __init__(self):
22
+ # Admin username for prompt management
23
+ self.admin_user_name = os.getenv("ADMIN_USER_NAME", "ariondas")
24
+
25
+ # Cache for prompts with TTL - stores {prompt_name: (template, expires_at)}
26
+ self._prompt_cache: Dict[str, Tuple[str, datetime]] = {}
27
+ self._cache_ttl_seconds = 300 # 5 minutes TTL
28
+
29
+ def _is_admin(self, user_name: str) -> bool:
30
+ """Check if user is admin"""
31
+ if not user_name:
32
+ return False
33
+ return user_name.lower() == self.admin_user_name.lower()
34
+
35
+ def _is_cached(self, prompt_name: str) -> bool:
36
+ """Check if prompt is cached and not expired"""
37
+ if prompt_name not in self._prompt_cache:
38
+ return False
39
+
40
+ _, expires_at = self._prompt_cache[prompt_name]
41
+ return datetime.utcnow() < expires_at
42
+
43
+ def _get_from_cache(self, prompt_name: str) -> Optional[str]:
44
+ """Get prompt from cache if valid"""
45
+ if self._is_cached(prompt_name):
46
+ template, _ = self._prompt_cache[prompt_name]
47
+ return template
48
+ return None
49
+
50
+ def _add_to_cache(self, prompt_name: str, template: str):
51
+ """Add prompt to cache with TTL"""
52
+ expires_at = datetime.utcnow() + timedelta(seconds=self._cache_ttl_seconds)
53
+ self._prompt_cache[prompt_name] = (template, expires_at)
54
+
55
+ async def save_prompt(
56
+ self,
57
+ prompt_name: str,
58
+ template: str,
59
+ user_name: str = None
60
+ ) -> Dict[str, Any]:
61
+ """
62
+ Save or update a prompt in database (admin only)
63
+
64
+ Args:
65
+ prompt_name: Unique key for the prompt
66
+ template: The prompt template string
67
+ user_name: Username of user (must be admin)
68
+ """
69
+ if not user_name or not self._is_admin(user_name):
70
+ return {
71
+ "success": False,
72
+ "error": "Only admin can save prompts"
73
+ }
74
+
75
+ try:
76
+ prompt_doc = {
77
+ "prompt_name": prompt_name,
78
+ "template": template,
79
+ "updated_at": datetime.utcnow(),
80
+ "updated_by": user_name
81
+ }
82
+
83
+ # Upsert - create or update
84
+ result = await unified_database_service.db.prompts.update_one(
85
+ {"prompt_name": prompt_name},
86
+ {"$set": prompt_doc, "$setOnInsert": {"created_at": datetime.utcnow()}},
87
+ upsert=True
88
+ )
89
+
90
+ # Invalidate cache for this prompt
91
+ if prompt_name in self._prompt_cache:
92
+ del self._prompt_cache[prompt_name]
93
+
94
+ return {
95
+ "success": True,
96
+ "modified": result.modified_count > 0,
97
+ "created": result.upserted_id is not None
98
+ }
99
+
100
+ except Exception as e:
101
+ logger.error(f"Failed to save prompt {prompt_name}: {str(e)}")
102
+ return {
103
+ "success": False,
104
+ "error": str(e)
105
+ }
106
+
107
+ async def get_prompts_batch(self, prompt_names: List[str]) -> Dict[str, str]:
108
+ """
109
+ Get multiple prompts in a single query with caching.
110
+ Falls back to DEFAULT_PROMPTS if database is unavailable.
111
+ Returns dict of {prompt_name: template}
112
+ """
113
+ from ..prompts.prompts import DEFAULT_PROMPTS
114
+
115
+ result = {}
116
+ to_fetch = []
117
+
118
+ # Check cache first
119
+ for name in prompt_names:
120
+ cached = self._get_from_cache(name)
121
+ if cached:
122
+ result[name] = cached
123
+ else:
124
+ to_fetch.append(name)
125
+
126
+ # If no database connection, use fallback prompts
127
+ if unified_database_service.db is None:
128
+ for name in to_fetch:
129
+ if name in DEFAULT_PROMPTS:
130
+ result[name] = DEFAULT_PROMPTS[name]
131
+ self._add_to_cache(name, DEFAULT_PROMPTS[name])
132
+ return result
133
+
134
+ # Fetch uncached prompts from database
135
+ if to_fetch:
136
+ try:
137
+ cursor = unified_database_service.db.prompts.find(
138
+ {"prompt_name": {"$in": to_fetch}}
139
+ )
140
+ prompts = await cursor.to_list(length=None)
141
+
142
+ for prompt in prompts:
143
+ name = prompt["prompt_name"]
144
+ template = prompt["template"]
145
+ result[name] = template
146
+ self._add_to_cache(name, template)
147
+
148
+ # For any prompts not found in DB, use fallback
149
+ for name in to_fetch:
150
+ if name not in result and name in DEFAULT_PROMPTS:
151
+ result[name] = DEFAULT_PROMPTS[name]
152
+ self._add_to_cache(name, DEFAULT_PROMPTS[name])
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed to fetch prompts batch: {str(e)}")
156
+ # Fall back to DEFAULT_PROMPTS on error
157
+ for name in to_fetch:
158
+ if name in DEFAULT_PROMPTS:
159
+ result[name] = DEFAULT_PROMPTS[name]
160
+
161
+ return result
162
+
163
+ async def get_prompt(self, name: str) -> Dict[str, Any]:
164
+ """
165
+ Get a single prompt by name with TTL caching.
166
+ Falls back to DEFAULT_PROMPTS if database is unavailable.
167
+ """
168
+ from ..prompts.prompts import DEFAULT_PROMPTS
169
+
170
+ try:
171
+ # Check cache first
172
+ cached_template = self._get_from_cache(name)
173
+ if cached_template:
174
+ return {
175
+ "success": True,
176
+ "template": cached_template,
177
+ "cached": True
178
+ }
179
+
180
+ # If no database connection, use fallback prompts
181
+ if unified_database_service.db is None:
182
+ if name in DEFAULT_PROMPTS:
183
+ template = DEFAULT_PROMPTS[name]
184
+ self._add_to_cache(name, template)
185
+ return {
186
+ "success": True,
187
+ "template": template,
188
+ "cached": False,
189
+ "fallback": True
190
+ }
191
+ else:
192
+ return {
193
+ "success": False,
194
+ "error": f"Prompt '{name}' not found in defaults"
195
+ }
196
+
197
+ # Query database
198
+ prompt = await unified_database_service.find_one(
199
+ "prompts",
200
+ {"prompt_name": name}
201
+ )
202
+
203
+ if prompt:
204
+ template = prompt["template"]
205
+ self._add_to_cache(name, template)
206
+
207
+ return {
208
+ "success": True,
209
+ "template": template,
210
+ "cached": False
211
+ }
212
+ else:
213
+ # Fall back to DEFAULT_PROMPTS if not in database
214
+ if name in DEFAULT_PROMPTS:
215
+ template = DEFAULT_PROMPTS[name]
216
+ self._add_to_cache(name, template)
217
+ return {
218
+ "success": True,
219
+ "template": template,
220
+ "cached": False,
221
+ "fallback": True
222
+ }
223
+ return {
224
+ "success": False,
225
+ "error": f"Prompt '{name}' not found"
226
+ }
227
+
228
+ except Exception as e:
229
+ logger.error(f"Failed to get prompt {name}: {str(e)}")
230
+ # Fall back to DEFAULT_PROMPTS on error
231
+ if name in DEFAULT_PROMPTS:
232
+ template = DEFAULT_PROMPTS[name]
233
+ self._add_to_cache(name, template)
234
+ return {
235
+ "success": True,
236
+ "template": template,
237
+ "cached": False,
238
+ "fallback": True
239
+ }
240
+ return {
241
+ "success": False,
242
+ "error": f"Failed to get prompt: {str(e)}"
243
+ }
244
+
245
+ async def list_all_prompts(self, user_name: str = None) -> Dict[str, Any]:
246
+ """
247
+ List all available prompts (admin only for viewing all)
248
+ """
249
+ if not user_name or not self._is_admin(user_name):
250
+ return {
251
+ "success": False,
252
+ "error": "Admin only"
253
+ }
254
+
255
+ try:
256
+ cursor = unified_database_service.db.prompts.find({}).sort("prompt_name", 1)
257
+ prompts = await cursor.to_list(length=None)
258
+
259
+ return {
260
+ "success": True,
261
+ "prompts": [
262
+ {
263
+ "prompt_name": p["prompt_name"],
264
+ "updated_at": p.get("updated_at"),
265
+ "template_length": len(p.get("template", ""))
266
+ }
267
+ for p in prompts
268
+ ]
269
+ }
270
+
271
+ except Exception as e:
272
+ logger.error(f"Failed to list prompts: {str(e)}")
273
+ return {
274
+ "success": False,
275
+ "error": str(e)
276
+ }
277
+
278
+ async def delete_prompt(self, prompt_name: str, user_name: str = None) -> Dict[str, Any]:
279
+ """
280
+ Delete a prompt (admin only)
281
+ """
282
+ if not user_name or not self._is_admin(user_name):
283
+ return {
284
+ "success": False,
285
+ "error": "Admin only"
286
+ }
287
+
288
+ try:
289
+ result = await unified_database_service.db.prompts.delete_one(
290
+ {"prompt_name": prompt_name}
291
+ )
292
+
293
+ # Remove from cache
294
+ if prompt_name in self._prompt_cache:
295
+ del self._prompt_cache[prompt_name]
296
+
297
+ return {
298
+ "success": result.deleted_count > 0,
299
+ "deleted": result.deleted_count
300
+ }
301
+
302
+ except Exception as e:
303
+ logger.error(f"Failed to delete prompt {prompt_name}: {str(e)}")
304
+ return {
305
+ "success": False,
306
+ "error": str(e)
307
+ }
308
+
309
+ async def format_prompt(self, prompt_name: str, **kwargs) -> str:
310
+ """
311
+ Get and format a prompt with variables
312
+ """
313
+ result = await self.get_prompt(prompt_name)
314
+
315
+ if not result["success"]:
316
+ raise ValueError(f"Prompt not found: {prompt_name}")
317
+
318
+ template = result["template"]
319
+
320
+ try:
321
+ return template.format(**kwargs)
322
+ except KeyError as e:
323
+ raise ValueError(f"Missing variable for prompt '{prompt_name}': {e}")
324
+
325
+ def clear_cache(self, prompt_name: str = None):
326
+ """Clear cache for specific prompt or all prompts"""
327
+ if prompt_name:
328
+ if prompt_name in self._prompt_cache:
329
+ del self._prompt_cache[prompt_name]
330
+ else:
331
+ self._prompt_cache = {}
332
+
333
+
334
+ # Global instance
335
+ unified_prompt_service = UnifiedPromptService()
336
+
337
+ # Export for convenience
338
+ prompt_service = unified_prompt_service
339
+
340
+ __all__ = [
341
+ 'UnifiedPromptService',
342
+ 'unified_prompt_service',
343
+ 'prompt_service'
344
+ ]