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,583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Client for ArionXiv CLI
|
|
3
|
+
Handles communication with the ArionXiv backend server
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Default API URL - the hosted ArionXiv backend on Vercel
|
|
15
|
+
DEFAULT_API_URL = "https://arion-xiv.vercel.app"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APIClientError(Exception):
|
|
19
|
+
"""Custom exception for API client errors"""
|
|
20
|
+
def __init__(self, message: str, status_code: int = None, details: Dict = None):
|
|
21
|
+
self.message = message
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.details = details or {}
|
|
24
|
+
super().__init__(self.message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArionXivAPIClient:
|
|
28
|
+
"""
|
|
29
|
+
API client for ArionXiv backend.
|
|
30
|
+
Provides methods for all API endpoints.
|
|
31
|
+
Uses httpx for async HTTP requests.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, base_url: str = None):
|
|
35
|
+
self.base_url = base_url or os.getenv("ARIONXIV_API_URL", DEFAULT_API_URL)
|
|
36
|
+
self._token: Optional[str] = None
|
|
37
|
+
self._token_file = Path.home() / ".arionxiv" / "token.json"
|
|
38
|
+
self._httpx_client = None
|
|
39
|
+
self._load_token()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def httpx_client(self):
|
|
43
|
+
"""Lazy load httpx client"""
|
|
44
|
+
if self._httpx_client is None:
|
|
45
|
+
try:
|
|
46
|
+
import httpx
|
|
47
|
+
self._httpx_client = httpx.AsyncClient(
|
|
48
|
+
base_url=self.base_url,
|
|
49
|
+
timeout=30.0
|
|
50
|
+
)
|
|
51
|
+
except ImportError:
|
|
52
|
+
logger.warning("httpx not installed, API client will not work")
|
|
53
|
+
raise ImportError("httpx is required for API client. Install with: pip install httpx")
|
|
54
|
+
return self._httpx_client
|
|
55
|
+
|
|
56
|
+
def _load_token(self) -> None:
|
|
57
|
+
"""Load stored token from file"""
|
|
58
|
+
try:
|
|
59
|
+
if self._token_file.exists():
|
|
60
|
+
data = json.loads(self._token_file.read_text())
|
|
61
|
+
self._token = data.get("token")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.debug(f"Could not load token: {e}")
|
|
64
|
+
|
|
65
|
+
def _save_token(self, token: str) -> None:
|
|
66
|
+
"""Save token to file"""
|
|
67
|
+
try:
|
|
68
|
+
self._token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
self._token_file.write_text(json.dumps({"token": token}))
|
|
70
|
+
self._token = token
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.warning(f"Could not save token: {e}")
|
|
73
|
+
|
|
74
|
+
def _clear_token(self) -> None:
|
|
75
|
+
"""Clear stored token"""
|
|
76
|
+
try:
|
|
77
|
+
if self._token_file.exists():
|
|
78
|
+
self._token_file.unlink()
|
|
79
|
+
self._token = None
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Could not clear token: {e}")
|
|
82
|
+
|
|
83
|
+
def _get_headers(self, auth_required: bool = True) -> Dict[str, str]:
|
|
84
|
+
"""Get request headers"""
|
|
85
|
+
headers = {"Content-Type": "application/json"}
|
|
86
|
+
if auth_required and self._token:
|
|
87
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
88
|
+
return headers
|
|
89
|
+
|
|
90
|
+
async def _handle_response(self, response) -> Dict[str, Any]:
|
|
91
|
+
"""Handle API response"""
|
|
92
|
+
try:
|
|
93
|
+
data = response.json()
|
|
94
|
+
except Exception:
|
|
95
|
+
data = {"message": response.text}
|
|
96
|
+
|
|
97
|
+
if response.status_code >= 400:
|
|
98
|
+
# Extract error message from various possible formats
|
|
99
|
+
error_msg = data.get("detail", data.get("error", data.get("message", "")))
|
|
100
|
+
if isinstance(error_msg, dict):
|
|
101
|
+
error_msg = error_msg.get("message", "") or str(error_msg) if error_msg else ""
|
|
102
|
+
if not error_msg or error_msg == "{}":
|
|
103
|
+
error_msg = f"API error {response.status_code}"
|
|
104
|
+
|
|
105
|
+
raise APIClientError(
|
|
106
|
+
message=str(error_msg),
|
|
107
|
+
status_code=response.status_code,
|
|
108
|
+
details=data
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
# =========================================================================
|
|
114
|
+
# AUTHENTICATION ENDPOINTS
|
|
115
|
+
# =========================================================================
|
|
116
|
+
|
|
117
|
+
async def register(
|
|
118
|
+
self,
|
|
119
|
+
email: str,
|
|
120
|
+
user_name: str,
|
|
121
|
+
password: str,
|
|
122
|
+
full_name: str = ""
|
|
123
|
+
) -> Dict[str, Any]:
|
|
124
|
+
"""Register a new user"""
|
|
125
|
+
response = await self.httpx_client.post(
|
|
126
|
+
"/auth/register",
|
|
127
|
+
json={
|
|
128
|
+
"email": email,
|
|
129
|
+
"user_name": user_name,
|
|
130
|
+
"password": password,
|
|
131
|
+
"full_name": full_name
|
|
132
|
+
},
|
|
133
|
+
headers=self._get_headers(auth_required=False)
|
|
134
|
+
)
|
|
135
|
+
return await self._handle_response(response)
|
|
136
|
+
|
|
137
|
+
async def login(self, identifier: str, password: str) -> Dict[str, Any]:
|
|
138
|
+
"""Login user and store token"""
|
|
139
|
+
response = await self.httpx_client.post(
|
|
140
|
+
"/auth/login",
|
|
141
|
+
json={"identifier": identifier, "password": password},
|
|
142
|
+
headers=self._get_headers(auth_required=False)
|
|
143
|
+
)
|
|
144
|
+
data = await self._handle_response(response)
|
|
145
|
+
|
|
146
|
+
if data.get("success") and data.get("token"):
|
|
147
|
+
self._save_token(data["token"])
|
|
148
|
+
|
|
149
|
+
return data
|
|
150
|
+
|
|
151
|
+
async def logout(self) -> Dict[str, Any]:
|
|
152
|
+
"""Logout user and clear token"""
|
|
153
|
+
try:
|
|
154
|
+
response = await self.httpx_client.post(
|
|
155
|
+
"/auth/logout",
|
|
156
|
+
headers=self._get_headers()
|
|
157
|
+
)
|
|
158
|
+
await self._handle_response(response)
|
|
159
|
+
except Exception:
|
|
160
|
+
pass # Logout should always succeed client-side
|
|
161
|
+
|
|
162
|
+
self._clear_token()
|
|
163
|
+
return {"success": True, "message": "Logged out"}
|
|
164
|
+
|
|
165
|
+
async def refresh_token(self) -> Dict[str, Any]:
|
|
166
|
+
"""Refresh authentication token"""
|
|
167
|
+
if not self._token:
|
|
168
|
+
raise APIClientError("No token to refresh", status_code=401)
|
|
169
|
+
|
|
170
|
+
response = await self.httpx_client.post(
|
|
171
|
+
"/auth/refresh",
|
|
172
|
+
json={"token": self._token},
|
|
173
|
+
headers=self._get_headers(auth_required=False)
|
|
174
|
+
)
|
|
175
|
+
data = await self._handle_response(response)
|
|
176
|
+
|
|
177
|
+
if data.get("success") and data.get("token"):
|
|
178
|
+
self._save_token(data["token"])
|
|
179
|
+
|
|
180
|
+
return data
|
|
181
|
+
|
|
182
|
+
def is_authenticated(self) -> bool:
|
|
183
|
+
"""Check if user has a stored token"""
|
|
184
|
+
return self._token is not None
|
|
185
|
+
|
|
186
|
+
# =========================================================================
|
|
187
|
+
# USER ENDPOINTS
|
|
188
|
+
# =========================================================================
|
|
189
|
+
|
|
190
|
+
async def get_profile(self) -> Dict[str, Any]:
|
|
191
|
+
"""Get current user profile"""
|
|
192
|
+
response = await self.httpx_client.get(
|
|
193
|
+
"/auth/profile",
|
|
194
|
+
headers=self._get_headers()
|
|
195
|
+
)
|
|
196
|
+
return await self._handle_response(response)
|
|
197
|
+
|
|
198
|
+
async def get_settings(self) -> Dict[str, Any]:
|
|
199
|
+
"""Get user settings"""
|
|
200
|
+
response = await self.httpx_client.get(
|
|
201
|
+
"/settings",
|
|
202
|
+
headers=self._get_headers()
|
|
203
|
+
)
|
|
204
|
+
return await self._handle_response(response)
|
|
205
|
+
|
|
206
|
+
async def update_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
|
207
|
+
"""Update user settings"""
|
|
208
|
+
response = await self.httpx_client.put(
|
|
209
|
+
"/settings",
|
|
210
|
+
json=settings,
|
|
211
|
+
headers=self._get_headers()
|
|
212
|
+
)
|
|
213
|
+
return await self._handle_response(response)
|
|
214
|
+
|
|
215
|
+
# =========================================================================
|
|
216
|
+
# PAPER ENDPOINTS
|
|
217
|
+
# =========================================================================
|
|
218
|
+
|
|
219
|
+
async def search_papers(
|
|
220
|
+
self,
|
|
221
|
+
query: str,
|
|
222
|
+
max_results: int = 10,
|
|
223
|
+
category: str = None
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""Search arXiv papers"""
|
|
226
|
+
params = {"query": query, "max_results": max_results}
|
|
227
|
+
if category:
|
|
228
|
+
params["category"] = category
|
|
229
|
+
|
|
230
|
+
response = await self.httpx_client.get(
|
|
231
|
+
"/papers/search",
|
|
232
|
+
params=params,
|
|
233
|
+
headers=self._get_headers()
|
|
234
|
+
)
|
|
235
|
+
return await self._handle_response(response)
|
|
236
|
+
|
|
237
|
+
async def fetch_paper(self, arxiv_id: str) -> Dict[str, Any]:
|
|
238
|
+
"""Fetch a paper from arXiv"""
|
|
239
|
+
response = await self.httpx_client.post(
|
|
240
|
+
f"/papers/{arxiv_id}/fetch",
|
|
241
|
+
headers=self._get_headers()
|
|
242
|
+
)
|
|
243
|
+
return await self._handle_response(response)
|
|
244
|
+
|
|
245
|
+
async def analyze_paper(self, paper_id: str) -> Dict[str, Any]:
|
|
246
|
+
"""Analyze a paper"""
|
|
247
|
+
response = await self.httpx_client.post(
|
|
248
|
+
f"/papers/{paper_id}/analyze",
|
|
249
|
+
headers=self._get_headers()
|
|
250
|
+
)
|
|
251
|
+
return await self._handle_response(response)
|
|
252
|
+
|
|
253
|
+
async def get_user_papers(
|
|
254
|
+
self,
|
|
255
|
+
limit: int = 20,
|
|
256
|
+
skip: int = 0
|
|
257
|
+
) -> Dict[str, Any]:
|
|
258
|
+
"""Get user's papers"""
|
|
259
|
+
response = await self.httpx_client.get(
|
|
260
|
+
"/papers/user",
|
|
261
|
+
params={"limit": limit, "skip": skip},
|
|
262
|
+
headers=self._get_headers()
|
|
263
|
+
)
|
|
264
|
+
return await self._handle_response(response)
|
|
265
|
+
|
|
266
|
+
# =========================================================================
|
|
267
|
+
# LIBRARY ENDPOINTS
|
|
268
|
+
# =========================================================================
|
|
269
|
+
|
|
270
|
+
async def get_library(
|
|
271
|
+
self,
|
|
272
|
+
limit: int = 20,
|
|
273
|
+
skip: int = 0
|
|
274
|
+
) -> Dict[str, Any]:
|
|
275
|
+
"""Get user's library"""
|
|
276
|
+
response = await self.httpx_client.get(
|
|
277
|
+
"/library",
|
|
278
|
+
params={"limit": limit, "skip": skip},
|
|
279
|
+
headers=self._get_headers()
|
|
280
|
+
)
|
|
281
|
+
return await self._handle_response(response)
|
|
282
|
+
|
|
283
|
+
async def add_to_library(
|
|
284
|
+
self,
|
|
285
|
+
arxiv_id: str,
|
|
286
|
+
title: str = "",
|
|
287
|
+
authors: List[str] = None,
|
|
288
|
+
categories: List[str] = None,
|
|
289
|
+
abstract: str = "",
|
|
290
|
+
tags: List[str] = None,
|
|
291
|
+
notes: str = ""
|
|
292
|
+
) -> Dict[str, Any]:
|
|
293
|
+
"""Add paper to library"""
|
|
294
|
+
response = await self.httpx_client.post(
|
|
295
|
+
"/library",
|
|
296
|
+
json={
|
|
297
|
+
"arxiv_id": arxiv_id,
|
|
298
|
+
"title": title,
|
|
299
|
+
"authors": authors or [],
|
|
300
|
+
"categories": categories or [],
|
|
301
|
+
"abstract": abstract,
|
|
302
|
+
"tags": tags or [],
|
|
303
|
+
"notes": notes
|
|
304
|
+
},
|
|
305
|
+
headers=self._get_headers()
|
|
306
|
+
)
|
|
307
|
+
return await self._handle_response(response)
|
|
308
|
+
|
|
309
|
+
async def remove_from_library(self, arxiv_id: str) -> Dict[str, Any]:
|
|
310
|
+
"""Remove paper from library"""
|
|
311
|
+
response = await self.httpx_client.delete(
|
|
312
|
+
f"/library/{arxiv_id}",
|
|
313
|
+
headers=self._get_headers()
|
|
314
|
+
)
|
|
315
|
+
return await self._handle_response(response)
|
|
316
|
+
|
|
317
|
+
async def update_library_paper(
|
|
318
|
+
self,
|
|
319
|
+
arxiv_id: str,
|
|
320
|
+
tags: List[str] = None,
|
|
321
|
+
notes: str = None
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
"""Update paper in library"""
|
|
324
|
+
response = await self.httpx_client.put(
|
|
325
|
+
f"/library/{arxiv_id}",
|
|
326
|
+
json={"tags": tags, "notes": notes},
|
|
327
|
+
headers=self._get_headers()
|
|
328
|
+
)
|
|
329
|
+
return await self._handle_response(response)
|
|
330
|
+
|
|
331
|
+
async def search_library(self, query: str) -> Dict[str, Any]:
|
|
332
|
+
"""Search user's library"""
|
|
333
|
+
response = await self.httpx_client.get(
|
|
334
|
+
"/library/search",
|
|
335
|
+
params={"query": query},
|
|
336
|
+
headers=self._get_headers()
|
|
337
|
+
)
|
|
338
|
+
return await self._handle_response(response)
|
|
339
|
+
|
|
340
|
+
# =========================================================================
|
|
341
|
+
# CHAT ENDPOINTS
|
|
342
|
+
# =========================================================================
|
|
343
|
+
|
|
344
|
+
async def create_chat_session(
|
|
345
|
+
self,
|
|
346
|
+
paper_id: str,
|
|
347
|
+
title: str = None
|
|
348
|
+
) -> Dict[str, Any]:
|
|
349
|
+
"""Create a new chat session"""
|
|
350
|
+
response = await self.httpx_client.post(
|
|
351
|
+
"/chat/session",
|
|
352
|
+
json={"paper_id": paper_id, "title": title},
|
|
353
|
+
headers=self._get_headers()
|
|
354
|
+
)
|
|
355
|
+
return await self._handle_response(response)
|
|
356
|
+
|
|
357
|
+
async def update_chat_session(
|
|
358
|
+
self,
|
|
359
|
+
session_id: str,
|
|
360
|
+
messages: List[Dict[str, Any]]
|
|
361
|
+
) -> Dict[str, Any]:
|
|
362
|
+
"""Update chat session with new messages"""
|
|
363
|
+
response = await self.httpx_client.put(
|
|
364
|
+
f"/chat/session/{session_id}",
|
|
365
|
+
json=messages,
|
|
366
|
+
headers=self._get_headers()
|
|
367
|
+
)
|
|
368
|
+
return await self._handle_response(response)
|
|
369
|
+
|
|
370
|
+
async def send_chat_message(
|
|
371
|
+
self,
|
|
372
|
+
message: str,
|
|
373
|
+
paper_id: str,
|
|
374
|
+
session_id: str = None,
|
|
375
|
+
context: str = None,
|
|
376
|
+
paper_title: str = None
|
|
377
|
+
) -> Dict[str, Any]:
|
|
378
|
+
"""Send a chat message with optional RAG context"""
|
|
379
|
+
payload = {
|
|
380
|
+
"message": message,
|
|
381
|
+
"paper_id": paper_id,
|
|
382
|
+
"session_id": session_id
|
|
383
|
+
}
|
|
384
|
+
# Include context if provided (for RAG-enhanced responses)
|
|
385
|
+
if context:
|
|
386
|
+
payload["context"] = context
|
|
387
|
+
if paper_title:
|
|
388
|
+
payload["paper_title"] = paper_title
|
|
389
|
+
|
|
390
|
+
response = await self.httpx_client.post(
|
|
391
|
+
"/chat/message",
|
|
392
|
+
json=payload,
|
|
393
|
+
headers=self._get_headers()
|
|
394
|
+
)
|
|
395
|
+
return await self._handle_response(response)
|
|
396
|
+
|
|
397
|
+
async def get_chat_sessions(self, active_only: bool = True) -> Dict[str, Any]:
|
|
398
|
+
"""Get user's chat sessions"""
|
|
399
|
+
response = await self.httpx_client.get(
|
|
400
|
+
"/chat/sessions",
|
|
401
|
+
params={"active_only": active_only},
|
|
402
|
+
headers=self._get_headers()
|
|
403
|
+
)
|
|
404
|
+
return await self._handle_response(response)
|
|
405
|
+
|
|
406
|
+
async def get_chat_session(self, session_id: str) -> Dict[str, Any]:
|
|
407
|
+
"""Get a specific chat session"""
|
|
408
|
+
response = await self.httpx_client.get(
|
|
409
|
+
f"/chat/session/{session_id}",
|
|
410
|
+
headers=self._get_headers()
|
|
411
|
+
)
|
|
412
|
+
return await self._handle_response(response)
|
|
413
|
+
|
|
414
|
+
async def delete_chat_session(self, session_id: str) -> Dict[str, Any]:
|
|
415
|
+
"""Delete a chat session"""
|
|
416
|
+
response = await self.httpx_client.delete(
|
|
417
|
+
f"/chat/session/{session_id}",
|
|
418
|
+
headers=self._get_headers()
|
|
419
|
+
)
|
|
420
|
+
return await self._handle_response(response)
|
|
421
|
+
|
|
422
|
+
# =========================================================================
|
|
423
|
+
# DAILY DOSE ENDPOINTS
|
|
424
|
+
# =========================================================================
|
|
425
|
+
|
|
426
|
+
async def get_daily_analysis(self, date: str = None) -> Dict[str, Any]:
|
|
427
|
+
"""Get daily analysis for today or specified date"""
|
|
428
|
+
params = {}
|
|
429
|
+
if date:
|
|
430
|
+
params["date"] = date
|
|
431
|
+
|
|
432
|
+
response = await self.httpx_client.get(
|
|
433
|
+
"/daily",
|
|
434
|
+
params=params,
|
|
435
|
+
headers=self._get_headers()
|
|
436
|
+
)
|
|
437
|
+
return await self._handle_response(response)
|
|
438
|
+
|
|
439
|
+
async def get_daily_dose_settings(self) -> Dict[str, Any]:
|
|
440
|
+
"""Get daily dose settings"""
|
|
441
|
+
response = await self.httpx_client.get(
|
|
442
|
+
"/daily/settings",
|
|
443
|
+
headers=self._get_headers()
|
|
444
|
+
)
|
|
445
|
+
return await self._handle_response(response)
|
|
446
|
+
|
|
447
|
+
async def update_daily_dose_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
|
448
|
+
"""Update daily dose settings"""
|
|
449
|
+
response = await self.httpx_client.put(
|
|
450
|
+
"/daily/settings",
|
|
451
|
+
json=settings,
|
|
452
|
+
headers=self._get_headers()
|
|
453
|
+
)
|
|
454
|
+
return await self._handle_response(response)
|
|
455
|
+
|
|
456
|
+
async def run_daily_dose(self) -> Dict[str, Any]:
|
|
457
|
+
"""Run daily dose analysis"""
|
|
458
|
+
response = await self.httpx_client.post(
|
|
459
|
+
"/daily/run",
|
|
460
|
+
headers=self._get_headers()
|
|
461
|
+
)
|
|
462
|
+
return await self._handle_response(response)
|
|
463
|
+
|
|
464
|
+
# =========================================================================
|
|
465
|
+
# EMBEDDINGS CACHE ENDPOINTS
|
|
466
|
+
# =========================================================================
|
|
467
|
+
|
|
468
|
+
async def get_embeddings(self, paper_id: str) -> Dict[str, Any]:
|
|
469
|
+
"""Get cached embeddings for a paper"""
|
|
470
|
+
response = await self.httpx_client.get(
|
|
471
|
+
f"/embeddings/{paper_id}",
|
|
472
|
+
headers=self._get_headers()
|
|
473
|
+
)
|
|
474
|
+
return await self._handle_response(response)
|
|
475
|
+
|
|
476
|
+
async def save_embeddings(self, paper_id: str, embeddings: List, chunks: List) -> Dict[str, Any]:
|
|
477
|
+
"""Save embeddings for a paper in batches to avoid size limits"""
|
|
478
|
+
import json
|
|
479
|
+
|
|
480
|
+
# Stay under 3MB per request to provide safety margin for Vercel's 4.5MB payload limit
|
|
481
|
+
MAX_BATCH_SIZE_BYTES = 3_000_000
|
|
482
|
+
|
|
483
|
+
# Dynamically calculate batch size based on actual embedding dimensions
|
|
484
|
+
# Embeddings can vary (384 dims vs 1536 dims), so we estimate from first chunk
|
|
485
|
+
if embeddings and chunks:
|
|
486
|
+
sample_payload = json.dumps({"embeddings": [embeddings[0]], "chunks": [chunks[0]]})
|
|
487
|
+
chunk_size = len(sample_payload)
|
|
488
|
+
batch_size = max(10, min(100, MAX_BATCH_SIZE_BYTES // chunk_size))
|
|
489
|
+
else:
|
|
490
|
+
batch_size = 50 # Default fallback
|
|
491
|
+
|
|
492
|
+
# If total payload is small, send in one request
|
|
493
|
+
payload = {"embeddings": embeddings, "chunks": chunks}
|
|
494
|
+
total_size = len(json.dumps(payload))
|
|
495
|
+
if total_size < MAX_BATCH_SIZE_BYTES:
|
|
496
|
+
response = await self.httpx_client.post(
|
|
497
|
+
f"/embeddings/{paper_id}",
|
|
498
|
+
json={**payload, "batch_index": 0, "total_batches": 1},
|
|
499
|
+
headers=self._get_headers()
|
|
500
|
+
)
|
|
501
|
+
return await self._handle_response(response)
|
|
502
|
+
|
|
503
|
+
# Send in batches for large papers
|
|
504
|
+
total_chunks = len(embeddings)
|
|
505
|
+
total_batches = (total_chunks + batch_size - 1) // batch_size
|
|
506
|
+
|
|
507
|
+
for i in range(0, total_chunks, batch_size):
|
|
508
|
+
batch_embeddings = embeddings[i:i + batch_size]
|
|
509
|
+
batch_chunks = chunks[i:i + batch_size]
|
|
510
|
+
batch_index = i // batch_size
|
|
511
|
+
|
|
512
|
+
response = await self.httpx_client.post(
|
|
513
|
+
f"/embeddings/{paper_id}",
|
|
514
|
+
json={
|
|
515
|
+
"embeddings": batch_embeddings,
|
|
516
|
+
"chunks": batch_chunks,
|
|
517
|
+
"batch_index": batch_index,
|
|
518
|
+
"total_batches": total_batches
|
|
519
|
+
},
|
|
520
|
+
headers=self._get_headers()
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
result = await self._handle_response(response)
|
|
524
|
+
if not result.get("success"):
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
return {"success": True, "message": f"Saved {total_chunks} embeddings in {total_batches} batches"}
|
|
528
|
+
|
|
529
|
+
# =========================================================================
|
|
530
|
+
# HEALTH CHECK
|
|
531
|
+
# =========================================================================
|
|
532
|
+
|
|
533
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
534
|
+
"""Check API health"""
|
|
535
|
+
response = await self.httpx_client.get("/health")
|
|
536
|
+
return await self._handle_response(response)
|
|
537
|
+
|
|
538
|
+
async def close(self):
|
|
539
|
+
"""Close the HTTP client"""
|
|
540
|
+
if self._httpx_client:
|
|
541
|
+
await self._httpx_client.aclose()
|
|
542
|
+
self._httpx_client = None
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# Global instance
|
|
546
|
+
api_client = ArionXivAPIClient()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# Convenience functions for direct import
|
|
550
|
+
async def login(identifier: str, password: str) -> Dict[str, Any]:
|
|
551
|
+
"""Login convenience function"""
|
|
552
|
+
return await api_client.login(identifier, password)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def register(
|
|
556
|
+
email: str,
|
|
557
|
+
user_name: str,
|
|
558
|
+
password: str,
|
|
559
|
+
full_name: str = ""
|
|
560
|
+
) -> Dict[str, Any]:
|
|
561
|
+
"""Register convenience function"""
|
|
562
|
+
return await api_client.register(email, user_name, password, full_name)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def logout() -> Dict[str, Any]:
|
|
566
|
+
"""Logout convenience function"""
|
|
567
|
+
return await api_client.logout()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def is_authenticated() -> bool:
|
|
571
|
+
"""Check authentication status"""
|
|
572
|
+
return api_client.is_authenticated()
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
__all__ = [
|
|
576
|
+
'ArionXivAPIClient',
|
|
577
|
+
'APIClientError',
|
|
578
|
+
'api_client',
|
|
579
|
+
'login',
|
|
580
|
+
'register',
|
|
581
|
+
'logout',
|
|
582
|
+
'is_authenticated'
|
|
583
|
+
]
|