vnai 0.1.4__py3-none-any.whl → 2.0.1__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.
vnai/beam/quota.py ADDED
@@ -0,0 +1,478 @@
1
+ # vnai/beam/quota.py
2
+ # Resource allocation and management (formerly rate_limiter)
3
+
4
+ import time
5
+ import functools
6
+ import threading
7
+ from collections import defaultdict
8
+ from datetime import datetime
9
+
10
+ class RateLimitExceeded(Exception):
11
+ """Custom exception for rate limit violations."""
12
+ def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None):
13
+ self.resource_type = resource_type
14
+ self.limit_type = limit_type
15
+ self.current_usage = current_usage
16
+ self.limit_value = limit_value
17
+ self.retry_after = retry_after
18
+
19
+ # Create a user-friendly message
20
+ message = f"Bạn đã gửi quá nhiều request tới {resource_type}. "
21
+ if retry_after:
22
+ message += f"Vui lòng thử lại sau {round(retry_after)} giây."
23
+ else:
24
+ message += "Vui lòng thêm thời gian chờ giữa các lần gửi request."
25
+
26
+ super().__init__(message)
27
+
28
+ class Guardian:
29
+ """Ensures optimal resource allocation"""
30
+
31
+ _instance = None
32
+ _lock = threading.Lock()
33
+
34
+ def __new__(cls):
35
+ with cls._lock:
36
+ if cls._instance is None:
37
+ cls._instance = super(Guardian, cls).__new__(cls)
38
+ cls._instance._initialize()
39
+ return cls._instance
40
+
41
+ def _initialize(self):
42
+ """Initialize guardian"""
43
+ self.resource_limits = defaultdict(lambda: defaultdict(int))
44
+ self.usage_counters = defaultdict(lambda: defaultdict(list))
45
+
46
+ # Define resource limits
47
+ self.resource_limits["default"] = {"min": 60, "hour": 3000}
48
+ self.resource_limits["TCBS"] = {"min": 60, "hour": 3000}
49
+ self.resource_limits["VCI"] = {"min": 60, "hour": 3000}
50
+
51
+ def verify(self, operation_id, resource_type="default"):
52
+ """Verify resource availability before operation"""
53
+ current_time = time.time()
54
+
55
+ # Get limits for this resource type (or use default)
56
+ limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
57
+
58
+ # Check minute limit
59
+ minute_cutoff = current_time - 60
60
+ self.usage_counters[resource_type]["min"] = [
61
+ t for t in self.usage_counters[resource_type]["min"]
62
+ if t > minute_cutoff
63
+ ]
64
+
65
+ minute_usage = len(self.usage_counters[resource_type]["min"])
66
+ minute_exceeded = minute_usage >= limits["min"]
67
+
68
+ if minute_exceeded:
69
+ # Track limit check through metrics module
70
+ from vnai.beam.metrics import collector
71
+ collector.record(
72
+ "rate_limit",
73
+ {
74
+ "resource_type": resource_type,
75
+ "limit_type": "min",
76
+ "limit_value": limits["min"],
77
+ "current_usage": minute_usage,
78
+ "is_exceeded": True
79
+ },
80
+ priority="high"
81
+ )
82
+ # Raise custom exception with retry information
83
+ raise RateLimitExceeded(
84
+ resource_type=resource_type,
85
+ limit_type="min",
86
+ current_usage=minute_usage,
87
+ limit_value=limits["min"],
88
+ retry_after=60 - (current_time % 60) # Seconds until the minute rolls over
89
+ )
90
+
91
+ # Check hour limit
92
+ hour_cutoff = current_time - 3600
93
+ self.usage_counters[resource_type]["hour"] = [
94
+ t for t in self.usage_counters[resource_type]["hour"]
95
+ if t > hour_cutoff
96
+ ]
97
+
98
+ hour_usage = len(self.usage_counters[resource_type]["hour"])
99
+ hour_exceeded = hour_usage >= limits["hour"]
100
+
101
+ # Track rate limit check
102
+ from vnai.beam.metrics import collector
103
+ collector.record(
104
+ "rate_limit",
105
+ {
106
+ "resource_type": resource_type,
107
+ "limit_type": "hour" if hour_exceeded else "min",
108
+ "limit_value": limits["hour"] if hour_exceeded else limits["min"],
109
+ "current_usage": hour_usage if hour_exceeded else minute_usage,
110
+ "is_exceeded": hour_exceeded
111
+ }
112
+ )
113
+
114
+ if hour_exceeded:
115
+ # Raise custom exception with retry information
116
+ raise RateLimitExceeded(
117
+ resource_type=resource_type,
118
+ limit_type="hour",
119
+ current_usage=hour_usage,
120
+ limit_value=limits["hour"],
121
+ retry_after=3600 - (current_time % 3600) # Seconds until the hour rolls over
122
+ )
123
+
124
+ # Record this request
125
+ self.usage_counters[resource_type]["min"].append(current_time)
126
+ self.usage_counters[resource_type]["hour"].append(current_time)
127
+ return True
128
+
129
+ def usage(self, resource_type="default"):
130
+ """Get current usage percentage for resource limits"""
131
+ current_time = time.time()
132
+ limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
133
+
134
+ # Clean old timestamps
135
+ minute_cutoff = current_time - 60
136
+ hour_cutoff = current_time - 3600
137
+
138
+ self.usage_counters[resource_type]["min"] = [
139
+ t for t in self.usage_counters[resource_type]["min"]
140
+ if t > minute_cutoff
141
+ ]
142
+
143
+ self.usage_counters[resource_type]["hour"] = [
144
+ t for t in self.usage_counters[resource_type]["hour"]
145
+ if t > hour_cutoff
146
+ ]
147
+
148
+ # Calculate percentages
149
+ minute_usage = len(self.usage_counters[resource_type]["min"])
150
+ hour_usage = len(self.usage_counters[resource_type]["hour"])
151
+
152
+ minute_percentage = (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0
153
+ hour_percentage = (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0
154
+
155
+ # Return the higher percentage
156
+ return max(minute_percentage, hour_percentage)
157
+
158
+ def get_limit_status(self, resource_type="default"):
159
+ """Get detailed information about current limit status"""
160
+ current_time = time.time()
161
+ limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
162
+
163
+ # Clean old timestamps
164
+ minute_cutoff = current_time - 60
165
+ hour_cutoff = current_time - 3600
166
+
167
+ minute_usage = len([t for t in self.usage_counters[resource_type]["min"] if t > minute_cutoff])
168
+ hour_usage = len([t for t in self.usage_counters[resource_type]["hour"] if t > hour_cutoff])
169
+
170
+ return {
171
+ "resource_type": resource_type,
172
+ "minute_limit": {
173
+ "usage": minute_usage,
174
+ "limit": limits["min"],
175
+ "percentage": (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0,
176
+ "remaining": max(0, limits["min"] - minute_usage),
177
+ "reset_in_seconds": 60 - (current_time % 60)
178
+ },
179
+ "hour_limit": {
180
+ "usage": hour_usage,
181
+ "limit": limits["hour"],
182
+ "percentage": (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0,
183
+ "remaining": max(0, limits["hour"] - hour_usage),
184
+ "reset_in_seconds": 3600 - (current_time % 3600)
185
+ }
186
+ }
187
+
188
+ # Create singleton instance
189
+ guardian = Guardian()
190
+
191
+ class CleanErrorContext:
192
+ """Context manager to clean up tracebacks for rate limits"""
193
+ # Class variable to track if a message has been displayed recently
194
+ _last_message_time = 0
195
+ _message_cooldown = 5 # Only show a message every 5 seconds
196
+
197
+ def __enter__(self):
198
+ return self
199
+
200
+ def __exit__(self, exc_type, exc_val, exc_tb):
201
+ if exc_type is RateLimitExceeded:
202
+ current_time = time.time()
203
+
204
+ # Only print the message if enough time has passed since the last one
205
+ if current_time - CleanErrorContext._last_message_time >= CleanErrorContext._message_cooldown:
206
+ print(f"\n⚠️ {str(exc_val)}\n")
207
+ CleanErrorContext._last_message_time = current_time
208
+
209
+ # Re-raise the exception more forcefully to ensure it propagates
210
+ # This will bypass any try/except blocks that might be catching RateLimitExceeded
211
+ import sys
212
+ sys.exit(f"Rate limit exceeded. {str(exc_val)} Process terminated.")
213
+
214
+ # The line below won't be reached, but we keep it for clarity
215
+ return False
216
+ return False
217
+
218
+
219
+ def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_cooldown=150, content_trigger_threshold=3,
220
+ max_retries=2, backoff_factor=2, debug=False):
221
+ """
222
+ Decorator that optimizes function execution, tracks metrics, and detects loop patterns for ad opportunities.
223
+
224
+ Features:
225
+ - Resource verification
226
+ - Performance metrics collection
227
+ - Loop detection for ad/content opportunities
228
+ - Automatic retry with exponential backoff for rate limit errors
229
+
230
+ Args:
231
+ resource_type: Type of resource used by function ("network", "database", "cpu", "memory", "io", "default")
232
+ loop_threshold: Number of calls within time_window to consider as a loop (min: 2)
233
+ time_window: Time period in seconds to consider for loop detection
234
+ ad_cooldown: Minimum seconds between showing ads for the same function
235
+ content_trigger_threshold: Number of consecutive loop detections before triggering content (min: 1)
236
+ max_retries: Maximum number of times to retry when rate limits are hit
237
+ backoff_factor: Base factor for exponential backoff (wait time = backoff_factor^retry_count)
238
+ debug: When True, prints diagnostic information about loop detection
239
+
240
+ Examples:
241
+ @optimize
242
+ def simple_function():
243
+ return "result"
244
+
245
+ @optimize("network")
246
+ def fetch_stock_data(symbol):
247
+ # Makes network calls
248
+ return data
249
+
250
+ @optimize("database", loop_threshold=4, time_window=10)
251
+ def query_financial_data(params):
252
+ # Database queries
253
+ return results
254
+ """
255
+ # Handle case where decorator is used without arguments: @optimize
256
+ if callable(resource_type):
257
+ func = resource_type
258
+ return _create_wrapper(func, 'default', loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
259
+ max_retries, backoff_factor, debug)
260
+
261
+ # Basic validation
262
+ if loop_threshold < 2:
263
+ raise ValueError(f"loop_threshold must be at least 2, got {loop_threshold}")
264
+ if time_window <= 0:
265
+ raise ValueError(f"time_window must be positive, got {time_window}")
266
+ if content_trigger_threshold < 1:
267
+ raise ValueError(f"content_trigger_threshold must be at least 1, got {content_trigger_threshold}")
268
+ if max_retries < 0:
269
+ raise ValueError(f"max_retries must be non-negative, got {max_retries}")
270
+ if backoff_factor <= 0:
271
+ raise ValueError(f"backoff_factor must be positive, got {backoff_factor}")
272
+
273
+ # Return the actual decorator
274
+ def decorator(func):
275
+ return _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
276
+ max_retries, backoff_factor, debug)
277
+ return decorator
278
+
279
+ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
280
+ max_retries, backoff_factor, debug):
281
+ """Creates the function wrapper with call tracking for loop detection"""
282
+ # Static storage for each decorated function instance
283
+ call_history = []
284
+ last_ad_time = 0
285
+ consecutive_loop_detections = 0
286
+ session_displayed = False # Track if we've displayed an ad in this session
287
+ session_start_time = time.time()
288
+ session_timeout = 1800 # 30 minutes for session expiration
289
+
290
+ @functools.wraps(func)
291
+ def wrapper(*args, **kwargs):
292
+ nonlocal last_ad_time, consecutive_loop_detections, session_displayed, session_start_time
293
+ current_time = time.time()
294
+ content_triggered = False
295
+
296
+ # Reset session if it has expired
297
+ if current_time - session_start_time > session_timeout:
298
+ session_displayed = False
299
+ session_start_time = current_time
300
+
301
+ # For automatic retries with rate limits
302
+ retries = 0
303
+ while True:
304
+ # ===== LOOP DETECTION LOGIC =====
305
+ # Add current call to history
306
+ call_history.append(current_time)
307
+
308
+ # Prune old calls outside the time window
309
+ while call_history and current_time - call_history[0] > time_window:
310
+ call_history.pop(0)
311
+
312
+ # Check if we're in a loop pattern
313
+ loop_detected = len(call_history) >= loop_threshold
314
+
315
+ if debug and loop_detected:
316
+ print(f"[OPTIMIZE] Đã phát hiện vòng lặp cho {func.__name__}: {len(call_history)} lần gọi trong {time_window}s")
317
+
318
+ # Handle loop detection
319
+ if loop_detected:
320
+ consecutive_loop_detections += 1
321
+ if debug:
322
+ print(f"[OPTIMIZE] Số lần phát hiện vòng lặp liên tiếp: {consecutive_loop_detections}/{content_trigger_threshold}")
323
+ else:
324
+ consecutive_loop_detections = 0
325
+
326
+ # Determine if we should show content - add session_displayed check
327
+ should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and \
328
+ (current_time - last_ad_time >= ad_cooldown) and \
329
+ not session_displayed
330
+
331
+ # Handle content opportunity
332
+ if should_show_content:
333
+ last_ad_time = current_time
334
+ consecutive_loop_detections = 0
335
+ content_triggered = True
336
+ session_displayed = True # Mark that we've displayed in this session
337
+
338
+ if debug:
339
+ print(f"[OPTIMIZE] Đã kích hoạt nội dung cho {func.__name__}")
340
+
341
+ # Trigger content display using promo manager with "loop" context
342
+ try:
343
+ from vnai.scope.promo import manager
344
+
345
+ # Get environment if available
346
+ try:
347
+ from vnai.scope.profile import inspector
348
+ environment = inspector.examine().get("environment", None)
349
+ manager.present_content(environment=environment, context="loop")
350
+ except ImportError:
351
+ manager.present_content(context="loop")
352
+
353
+ except ImportError:
354
+ # Fallback if content manager is not available
355
+ print(f"Phát hiện vòng lặp: Hàm '{func.__name__}' đang được gọi trong một vòng lặp")
356
+ except Exception as e:
357
+ # Don't let content errors affect the main function
358
+ if debug:
359
+ print(f"[OPTIMIZE] Lỗi khi hiển thị nội dung: {str(e)}")
360
+
361
+ # ===== RESOURCE VERIFICATION =====
362
+ try:
363
+ # Use a context manager to clean up the traceback
364
+ with CleanErrorContext():
365
+ guardian.verify(func.__name__, resource_type)
366
+
367
+ except RateLimitExceeded as e:
368
+ # Record the rate limit error
369
+ from vnai.beam.metrics import collector
370
+ collector.record(
371
+ "error",
372
+ {
373
+ "function": func.__name__,
374
+ "error": str(e),
375
+ "context": "resource_verification",
376
+ "resource_type": resource_type,
377
+ "retry_attempt": retries
378
+ },
379
+ priority="high"
380
+ )
381
+
382
+ # Display rate limit content ONLY if we haven't shown any content this session
383
+ if not session_displayed:
384
+ try:
385
+ from vnai.scope.promo import manager
386
+ try:
387
+ from vnai.scope.profile import inspector
388
+ environment = inspector.examine().get("environment", None)
389
+ manager.present_content(environment=environment, context="loop")
390
+ session_displayed = True # Mark that we've displayed
391
+ last_ad_time = current_time
392
+ except ImportError:
393
+ manager.present_content(context="loop")
394
+ session_displayed = True
395
+ last_ad_time = current_time
396
+ except Exception:
397
+ pass # Don't let content errors affect the retry logic
398
+
399
+ # Continue with retry logic
400
+ if retries < max_retries:
401
+ wait_time = backoff_factor ** retries
402
+ retries += 1
403
+
404
+ # If the exception has a retry_after value, use that instead
405
+ if hasattr(e, "retry_after") and e.retry_after:
406
+ wait_time = min(wait_time, e.retry_after)
407
+
408
+ if debug:
409
+ print(f"[OPTIMIZE] Đã đạt giới hạn tốc độ cho {func.__name__}, thử lại sau {wait_time} giây (lần thử {retries}/{max_retries})")
410
+
411
+ time.sleep(wait_time)
412
+ continue # Retry the call
413
+ else:
414
+ # No more retries, re-raise the exception
415
+ raise
416
+
417
+ # ===== FUNCTION EXECUTION & METRICS =====
418
+ start_time = time.time()
419
+ success = False
420
+ error = None
421
+
422
+ try:
423
+ # Execute the original function
424
+ result = func(*args, **kwargs)
425
+ success = True
426
+ return result
427
+ except Exception as e:
428
+ error = str(e)
429
+ raise
430
+ finally:
431
+ # Calculate execution metrics
432
+ execution_time = time.time() - start_time
433
+
434
+ # Record metrics
435
+ try:
436
+ from vnai.beam.metrics import collector
437
+ collector.record(
438
+ "function",
439
+ {
440
+ "function": func.__name__,
441
+ "resource_type": resource_type,
442
+ "execution_time": execution_time,
443
+ "success": success,
444
+ "error": error,
445
+ "in_loop": loop_detected,
446
+ "loop_depth": len(call_history),
447
+ "content_triggered": content_triggered,
448
+ "timestamp": datetime.now().isoformat(),
449
+ "retry_count": retries if retries > 0 else None
450
+ }
451
+ )
452
+
453
+ # Record content opportunity metrics if detected
454
+ if content_triggered:
455
+ collector.record(
456
+ "ad_opportunity",
457
+ {
458
+ "function": func.__name__,
459
+ "resource_type": resource_type,
460
+ "call_frequency": len(call_history) / time_window,
461
+ "consecutive_loops": consecutive_loop_detections,
462
+ "timestamp": datetime.now().isoformat()
463
+ }
464
+ )
465
+ except ImportError:
466
+ # Metrics module not available, just continue
467
+ pass
468
+
469
+ # If we got here, the function executed successfully, so break the retry loop
470
+ break
471
+
472
+ return wrapper
473
+
474
+
475
+ # Helper function for getting the current rate limit status
476
+ def rate_limit_status(resource_type="default"):
477
+ """Get the current rate limit status for a resource type"""
478
+ return guardian.get_limit_status(resource_type)
vnai/flow/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # vnai/flow/__init__.py
2
+ # Data flow and transmission management
3
+
4
+ from vnai.flow.relay import conduit, configure
5
+ from vnai.flow.queue import buffer
vnai/flow/queue.py ADDED
@@ -0,0 +1,134 @@
1
+ # vnai/flow/queue.py
2
+ # Data buffering system
3
+
4
+ import time
5
+ import threading
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ class Buffer:
11
+ """Manages data buffering with persistence"""
12
+
13
+ _instance = None
14
+ _lock = threading.Lock()
15
+
16
+ def __new__(cls):
17
+ with cls._lock:
18
+ if cls._instance is None:
19
+ cls._instance = super(Buffer, cls).__new__(cls)
20
+ cls._instance._initialize()
21
+ return cls._instance
22
+
23
+ def _initialize(self):
24
+ """Initialize buffer"""
25
+ self.data = []
26
+ self.lock = threading.Lock()
27
+ self.max_size = 1000
28
+ self.backup_interval = 300 # 5 minutes
29
+
30
+ # Setup data directory
31
+ self.home_dir = Path.home()
32
+ self.project_dir = self.home_dir / ".vnstock"
33
+ self.project_dir.mkdir(exist_ok=True)
34
+ self.data_dir = self.project_dir / 'data'
35
+ self.data_dir.mkdir(exist_ok=True)
36
+ self.backup_path = self.data_dir / "buffer_backup.json"
37
+
38
+ # Load from backup if exists
39
+ self._load_from_backup()
40
+
41
+ # Start backup thread
42
+ self._start_backup_thread()
43
+
44
+ def _load_from_backup(self):
45
+ """Load data from backup file"""
46
+ if self.backup_path.exists():
47
+ try:
48
+ with open(self.backup_path, 'r') as f:
49
+ backup_data = json.load(f)
50
+
51
+ with self.lock:
52
+ self.data = backup_data
53
+ except:
54
+ pass
55
+
56
+ def _save_to_backup(self):
57
+ """Save data to backup file"""
58
+ with self.lock:
59
+ if not self.data:
60
+ return
61
+
62
+ try:
63
+ with open(self.backup_path, 'w') as f:
64
+ json.dump(self.data, f)
65
+ except:
66
+ pass
67
+
68
+ def _start_backup_thread(self):
69
+ """Start background backup thread"""
70
+ def backup_task():
71
+ while True:
72
+ time.sleep(self.backup_interval)
73
+ self._save_to_backup()
74
+
75
+ backup_thread = threading.Thread(target=backup_task, daemon=True)
76
+ backup_thread.start()
77
+
78
+ def add(self, item, category=None):
79
+ """Add item to buffer"""
80
+ with self.lock:
81
+ # Add metadata
82
+ if isinstance(item, dict):
83
+ if "timestamp" not in item:
84
+ item["timestamp"] = datetime.now().isoformat()
85
+ if category:
86
+ item["category"] = category
87
+
88
+ # Add to buffer
89
+ self.data.append(item)
90
+
91
+ # Trim if exceeds max size
92
+ if len(self.data) > self.max_size:
93
+ self.data = self.data[-self.max_size:]
94
+
95
+ # Save to backup if buffer gets large
96
+ if len(self.data) % 100 == 0:
97
+ self._save_to_backup()
98
+
99
+ return len(self.data)
100
+
101
+ def get(self, count=None, category=None):
102
+ """Get items from buffer with optional filtering"""
103
+ with self.lock:
104
+ if category:
105
+ filtered_data = [item for item in self.data if item.get("category") == category]
106
+ else:
107
+ filtered_data = self.data.copy()
108
+
109
+ if count:
110
+ return filtered_data[:count]
111
+ else:
112
+ return filtered_data
113
+
114
+ def clear(self, category=None):
115
+ """Clear buffer, optionally by category"""
116
+ with self.lock:
117
+ if category:
118
+ self.data = [item for item in self.data if item.get("category") != category]
119
+ else:
120
+ self.data = []
121
+
122
+ self._save_to_backup()
123
+ return len(self.data)
124
+
125
+ def size(self, category=None):
126
+ """Get buffer size, optionally by category"""
127
+ with self.lock:
128
+ if category:
129
+ return len([item for item in self.data if item.get("category") == category])
130
+ else:
131
+ return len(self.data)
132
+
133
+ # Create singleton instance
134
+ buffer = Buffer()