vnai 2.0.3__py3-none-any.whl → 2.0.5__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/__init__.py +56 -81
- vnai/beam/__init__.py +3 -0
- vnai/beam/metrics.py +47 -67
- vnai/beam/pulse.py +21 -36
- vnai/beam/quota.py +112 -137
- vnai/flow/__init__.py +2 -4
- vnai/flow/queue.py +20 -31
- vnai/flow/relay.py +100 -111
- vnai/scope/__init__.py +2 -4
- vnai/scope/profile.py +110 -206
- vnai/scope/promo.py +222 -51
- vnai/scope/state.py +38 -64
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/METADATA +4 -17
- vnai-2.0.5.dist-info/RECORD +16 -0
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/WHEEL +1 -1
- vnai-2.0.3.dist-info/RECORD +0 -16
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/top_level.txt +0 -0
vnai/flow/relay.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
##
|
4
|
-
|
1
|
+
# vnai/flow/relay.py
|
2
|
+
# Data transmission system (formerly sync)
|
5
3
|
|
6
4
|
import time
|
7
5
|
import threading
|
@@ -13,7 +11,7 @@ from pathlib import Path
|
|
13
11
|
from typing import Dict, List, Any, Optional
|
14
12
|
|
15
13
|
class Conduit:
|
16
|
-
|
14
|
+
"""Handles system telemetry flow"""
|
17
15
|
|
18
16
|
_instance = None
|
19
17
|
_lock = threading.Lock()
|
@@ -26,13 +24,12 @@ class Conduit:
|
|
26
24
|
return cls._instance
|
27
25
|
|
28
26
|
def _initialize(self, webhook_url, buffer_size, sync_interval):
|
29
|
-
|
27
|
+
"""Initialize conduit"""
|
30
28
|
self.webhook_url = webhook_url
|
31
29
|
self.buffer_size = buffer_size
|
32
30
|
self.sync_interval = sync_interval
|
33
31
|
|
34
|
-
|
35
|
-
|
32
|
+
# Separate buffers for different data types
|
36
33
|
self.buffer = {
|
37
34
|
"function_calls": [],
|
38
35
|
"api_requests": [],
|
@@ -44,8 +41,7 @@ class Conduit:
|
|
44
41
|
self.sync_count = 0
|
45
42
|
self.failed_queue = []
|
46
43
|
|
47
|
-
|
48
|
-
|
44
|
+
# Home directory setup
|
49
45
|
self.home_dir = Path.home()
|
50
46
|
self.project_dir = self.home_dir / ".vnstock"
|
51
47
|
self.project_dir.mkdir(exist_ok=True)
|
@@ -53,31 +49,27 @@ class Conduit:
|
|
53
49
|
self.data_dir.mkdir(exist_ok=True)
|
54
50
|
self.config_path = self.data_dir / "relay_config.json"
|
55
51
|
|
56
|
-
|
57
|
-
|
52
|
+
# Get machine identifier from system profile
|
58
53
|
try:
|
59
54
|
from vnai.scope.profile import inspector
|
60
55
|
self.machine_id = inspector.fingerprint()
|
61
56
|
except:
|
62
57
|
self.machine_id = self._generate_fallback_id()
|
63
58
|
|
64
|
-
|
65
|
-
|
59
|
+
# Load config if exists
|
66
60
|
self._load_config()
|
67
61
|
|
68
|
-
|
69
|
-
|
62
|
+
# Start periodic sync
|
70
63
|
self._start_periodic_sync()
|
71
64
|
|
72
65
|
def _generate_fallback_id(self) -> str:
|
73
|
-
|
66
|
+
"""Generate a fallback machine identifier if profile is unavailable"""
|
74
67
|
try:
|
75
68
|
import platform
|
76
69
|
import hashlib
|
77
70
|
import uuid
|
78
71
|
|
79
|
-
|
80
|
-
|
72
|
+
# Try to get machine-specific information
|
81
73
|
system_info = platform.node() + platform.platform() + platform.processor()
|
82
74
|
return hashlib.md5(system_info.encode()).hexdigest()
|
83
75
|
except:
|
@@ -85,7 +77,7 @@ class Conduit:
|
|
85
77
|
return str(uuid.uuid4())
|
86
78
|
|
87
79
|
def _load_config(self):
|
88
|
-
|
80
|
+
"""Load configuration from file"""
|
89
81
|
if self.config_path.exists():
|
90
82
|
try:
|
91
83
|
with open(self.config_path, 'r') as f:
|
@@ -105,7 +97,7 @@ class Conduit:
|
|
105
97
|
pass
|
106
98
|
|
107
99
|
def _save_config(self):
|
108
|
-
|
100
|
+
"""Save configuration to file"""
|
109
101
|
config = {
|
110
102
|
'webhook_url': self.webhook_url,
|
111
103
|
'buffer_size': self.buffer_size,
|
@@ -121,7 +113,7 @@ class Conduit:
|
|
121
113
|
pass
|
122
114
|
|
123
115
|
def _start_periodic_sync(self):
|
124
|
-
|
116
|
+
"""Start periodic sync thread"""
|
125
117
|
def periodic_sync():
|
126
118
|
while True:
|
127
119
|
time.sleep(self.sync_interval)
|
@@ -131,9 +123,8 @@ class Conduit:
|
|
131
123
|
sync_thread.start()
|
132
124
|
|
133
125
|
def add_function_call(self, record):
|
134
|
-
|
135
|
-
|
136
|
-
|
126
|
+
"""Add function call record"""
|
127
|
+
# Ensure record is a dictionary
|
137
128
|
if not isinstance(record, dict):
|
138
129
|
record = {"value": str(record)}
|
139
130
|
|
@@ -142,9 +133,8 @@ class Conduit:
|
|
142
133
|
self._check_triggers("function_calls")
|
143
134
|
|
144
135
|
def add_api_request(self, record):
|
145
|
-
|
146
|
-
|
147
|
-
|
136
|
+
"""Add API request record"""
|
137
|
+
# Ensure record is a dictionary
|
148
138
|
if not isinstance(record, dict):
|
149
139
|
record = {"value": str(record)}
|
150
140
|
|
@@ -153,9 +143,8 @@ class Conduit:
|
|
153
143
|
self._check_triggers("api_requests")
|
154
144
|
|
155
145
|
def add_rate_limit(self, record):
|
156
|
-
|
157
|
-
|
158
|
-
|
146
|
+
"""Add rate limit record"""
|
147
|
+
# Ensure record is a dictionary
|
159
148
|
if not isinstance(record, dict):
|
160
149
|
record = {"value": str(record)}
|
161
150
|
|
@@ -164,36 +153,33 @@ class Conduit:
|
|
164
153
|
self._check_triggers("rate_limits")
|
165
154
|
|
166
155
|
def _check_triggers(self, record_type: str):
|
167
|
-
|
156
|
+
"""Check if any sync triggers are met"""
|
168
157
|
current_time = time.time()
|
169
158
|
should_trigger = False
|
170
159
|
trigger_reason = None
|
171
160
|
|
172
|
-
|
173
|
-
|
161
|
+
# Get total buffer size
|
174
162
|
total_records = sum(len(buffer) for buffer in self.buffer.values())
|
175
163
|
|
176
|
-
|
177
|
-
|
164
|
+
# SIZE TRIGGER: Buffer size threshold reached
|
178
165
|
if total_records >= self.buffer_size:
|
179
166
|
should_trigger = True
|
180
167
|
trigger_reason = "buffer_full"
|
181
168
|
|
182
|
-
|
183
|
-
|
184
|
-
|
169
|
+
# EVENT TRIGGER: Critical events (errors, rate limit warnings)
|
170
|
+
elif record_type == "rate_limits" and self.buffer["rate_limits"] and \
|
171
|
+
any(item.get("is_exceeded") for item in self.buffer["rate_limits"] if isinstance(item, dict)):
|
185
172
|
should_trigger = True
|
186
173
|
trigger_reason = "rate_limit_exceeded"
|
187
|
-
elif record_type == "function_calls" and self.buffer["function_calls"] and
|
174
|
+
elif record_type == "function_calls" and self.buffer["function_calls"] and \
|
175
|
+
any(not item.get("success") for item in self.buffer["function_calls"] if isinstance(item, dict)):
|
188
176
|
should_trigger = True
|
189
177
|
trigger_reason = "function_error"
|
190
178
|
|
191
|
-
|
192
|
-
|
179
|
+
# TIME-WEIGHTED RANDOM TRIGGER: More likely as time since last sync increases
|
193
180
|
else:
|
194
181
|
time_factor = min(1.0, (current_time - self.last_sync_time) / (self.sync_interval / 2))
|
195
|
-
if random.random() < 0.05 * time_factor:
|
196
|
-
|
182
|
+
if random.random() < 0.05 * time_factor: # 0-5% chance based on time
|
197
183
|
should_trigger = True
|
198
184
|
trigger_reason = "random_time_weighted"
|
199
185
|
|
@@ -205,48 +191,64 @@ class Conduit:
|
|
205
191
|
).start()
|
206
192
|
|
207
193
|
def queue(self, package, priority=None):
|
208
|
-
|
194
|
+
# --- Auto add 'segment' field to every payload ---
|
195
|
+
try:
|
196
|
+
from vnai.scope.promo import ContentManager
|
197
|
+
is_paid = ContentManager().is_paid_user
|
198
|
+
segment_val = "paid" if is_paid else "free"
|
199
|
+
except Exception:
|
200
|
+
segment_val = "free"
|
201
|
+
|
202
|
+
def ensure_segment(d):
|
203
|
+
if not isinstance(d, dict):
|
204
|
+
return d
|
205
|
+
d = dict(d) # tạo bản sao để không ảnh hưởng dict gốc
|
206
|
+
if "segment" not in d:
|
207
|
+
d["segment"] = segment_val
|
208
|
+
return d
|
209
|
+
# Add segment to package if not present
|
210
|
+
if isinstance(package, dict) and "segment" not in package:
|
211
|
+
package["segment"] = segment_val
|
212
|
+
# Add segment to data if exists and is dict
|
213
|
+
if isinstance(package, dict) and isinstance(package.get("data"), dict):
|
214
|
+
if "segment" not in package["data"]:
|
215
|
+
package["data"]["segment"] = segment_val
|
216
|
+
# --- End auto segment ---
|
217
|
+
|
218
|
+
"""Queue data package"""
|
209
219
|
if not package:
|
210
220
|
return False
|
211
221
|
|
212
|
-
|
213
|
-
|
222
|
+
# Handle non-dictionary packages
|
214
223
|
if not isinstance(package, dict):
|
215
|
-
self.add_function_call({"message": str(package)})
|
224
|
+
self.add_function_call(ensure_segment({"message": str(package)}))
|
216
225
|
return True
|
217
226
|
|
218
|
-
|
219
|
-
|
227
|
+
# Add timestamp if not present
|
220
228
|
if "timestamp" not in package:
|
221
229
|
package["timestamp"] = datetime.now().isoformat()
|
222
230
|
|
223
|
-
|
224
|
-
|
231
|
+
# Route based on package type
|
225
232
|
if "type" in package:
|
226
233
|
package_type = package["type"]
|
227
234
|
data = package.get("data", {})
|
228
235
|
|
229
|
-
|
230
|
-
|
236
|
+
# Remove system info if present to avoid duplication
|
231
237
|
if isinstance(data, dict) and "system" in data:
|
232
|
-
|
233
|
-
|
238
|
+
# Get machine_id for reference but don't duplicate the whole system info
|
234
239
|
machine_id = data["system"].get("machine_id")
|
235
240
|
data.pop("system")
|
236
241
|
if machine_id:
|
237
242
|
data["machine_id"] = machine_id
|
238
|
-
|
239
243
|
if package_type == "function":
|
240
|
-
self.add_function_call(data)
|
244
|
+
self.add_function_call(ensure_segment(data))
|
241
245
|
elif package_type == "api_request":
|
242
|
-
self.add_api_request(data)
|
246
|
+
self.add_api_request(ensure_segment(data))
|
243
247
|
elif package_type == "rate_limit":
|
244
|
-
self.add_rate_limit(data)
|
248
|
+
self.add_rate_limit(ensure_segment(data))
|
245
249
|
elif package_type == "system_info":
|
246
|
-
|
247
|
-
|
248
|
-
##
|
249
|
-
|
250
|
+
# For system info, we'll add it as a special function call
|
251
|
+
# but remove duplicated data
|
250
252
|
self.add_function_call({
|
251
253
|
"type": "system_info",
|
252
254
|
"commercial": data.get("commercial"),
|
@@ -254,83 +256,75 @@ class Conduit:
|
|
254
256
|
"timestamp": package.get("timestamp")
|
255
257
|
})
|
256
258
|
elif package_type == "metrics":
|
257
|
-
|
258
|
-
|
259
|
+
# Handle metrics package with multiple categories
|
259
260
|
metrics_data = data
|
260
261
|
for metric_type, metrics_list in metrics_data.items():
|
261
262
|
if isinstance(metrics_list, list):
|
262
263
|
if metric_type == "function":
|
263
264
|
for item in metrics_list:
|
264
|
-
self.add_function_call(item)
|
265
|
+
self.add_function_call(ensure_segment(item))
|
265
266
|
elif metric_type == "rate_limit":
|
266
267
|
for item in metrics_list:
|
267
|
-
self.add_rate_limit(item)
|
268
|
+
self.add_rate_limit(ensure_segment(item))
|
268
269
|
elif metric_type == "request":
|
269
270
|
for item in metrics_list:
|
270
|
-
self.add_api_request(item)
|
271
|
+
self.add_api_request(ensure_segment(item))
|
271
272
|
else:
|
272
|
-
|
273
|
-
|
274
|
-
|
273
|
+
# Default to function calls
|
274
|
+
if isinstance(data, dict) and data is not package:
|
275
|
+
self.add_function_call(ensure_segment(data))
|
276
|
+
else:
|
277
|
+
self.add_function_call(ensure_segment(package))
|
275
278
|
else:
|
276
|
-
|
277
|
-
|
278
|
-
self.add_function_call(package)
|
279
|
+
# No type specified, default to function call
|
280
|
+
self.add_function_call(ensure_segment(package))
|
279
281
|
|
280
|
-
|
281
|
-
|
282
|
+
# Handle high priority items
|
282
283
|
if priority == "high":
|
283
284
|
self.dispatch("high_priority")
|
284
285
|
|
285
286
|
return True
|
286
287
|
|
287
288
|
def dispatch(self, reason="manual"):
|
288
|
-
|
289
|
+
"""Send queued data"""
|
289
290
|
if not self.webhook_url:
|
290
291
|
return False
|
291
292
|
|
292
293
|
with self.lock:
|
293
|
-
|
294
|
-
|
294
|
+
# Check if all buffers are empty
|
295
295
|
if all(len(records) == 0 for records in self.buffer.values()):
|
296
296
|
return False
|
297
297
|
|
298
|
-
|
299
|
-
|
298
|
+
# Create a copy of the buffer for sending
|
300
299
|
data_to_send = {
|
301
300
|
"function_calls": self.buffer["function_calls"].copy(),
|
302
301
|
"api_requests": self.buffer["api_requests"].copy(),
|
303
302
|
"rate_limits": self.buffer["rate_limits"].copy()
|
304
303
|
}
|
305
304
|
|
306
|
-
|
307
|
-
|
305
|
+
# Clear buffer
|
308
306
|
self.buffer = {
|
309
307
|
"function_calls": [],
|
310
308
|
"api_requests": [],
|
311
309
|
"rate_limits": []
|
312
310
|
}
|
313
311
|
|
314
|
-
|
315
|
-
|
312
|
+
# Update sync time and count
|
316
313
|
self.last_sync_time = time.time()
|
317
314
|
self.sync_count += 1
|
318
315
|
self._save_config()
|
319
316
|
|
320
|
-
|
321
|
-
|
317
|
+
# Get environment information ONCE
|
322
318
|
try:
|
323
319
|
from vnai.scope.profile import inspector
|
324
320
|
environment_info = inspector.examine()
|
325
321
|
machine_id = environment_info.get("machine_id", self.machine_id)
|
326
322
|
except:
|
327
|
-
|
328
|
-
|
323
|
+
# Fallback if environment info isn't available
|
329
324
|
environment_info = {"machine_id": self.machine_id}
|
330
325
|
machine_id = self.machine_id
|
331
326
|
|
332
|
-
|
333
|
-
|
327
|
+
# Create payload with environment info only in metadata
|
334
328
|
payload = {
|
335
329
|
"analytics_data": data_to_send,
|
336
330
|
"metadata": {
|
@@ -347,8 +341,7 @@ class Conduit:
|
|
347
341
|
}
|
348
342
|
}
|
349
343
|
|
350
|
-
|
351
|
-
|
344
|
+
# Send data
|
352
345
|
success = self._send_data(payload)
|
353
346
|
|
354
347
|
if not success:
|
@@ -360,7 +353,7 @@ class Conduit:
|
|
360
353
|
return success
|
361
354
|
|
362
355
|
def _send_data(self, payload):
|
363
|
-
|
356
|
+
"""Send data to webhook"""
|
364
357
|
if not self.webhook_url:
|
365
358
|
return False
|
366
359
|
|
@@ -368,8 +361,7 @@ class Conduit:
|
|
368
361
|
response = requests.post(
|
369
362
|
self.webhook_url,
|
370
363
|
json=payload,
|
371
|
-
timeout=5
|
372
|
-
|
364
|
+
timeout=5 # 5 second timeout
|
373
365
|
)
|
374
366
|
|
375
367
|
return response.status_code == 200
|
@@ -377,7 +369,7 @@ class Conduit:
|
|
377
369
|
return False
|
378
370
|
|
379
371
|
def retry_failed(self):
|
380
|
-
|
372
|
+
"""Retry sending failed data"""
|
381
373
|
if not self.failed_queue:
|
382
374
|
return 0
|
383
375
|
|
@@ -396,20 +388,18 @@ class Conduit:
|
|
396
388
|
return success_count
|
397
389
|
|
398
390
|
def configure(self, webhook_url):
|
399
|
-
|
391
|
+
"""Configure webhook URL"""
|
400
392
|
with self.lock:
|
401
393
|
self.webhook_url = webhook_url
|
402
394
|
self._save_config()
|
403
395
|
return True
|
404
396
|
|
405
|
-
|
406
|
-
|
397
|
+
# Create singleton instance
|
407
398
|
conduit = Conduit()
|
408
399
|
|
409
|
-
|
410
|
-
|
400
|
+
# Exposed functions that match sync.py naming pattern
|
411
401
|
def track_function_call(function_name, source, execution_time, success=True, error=None, args=None):
|
412
|
-
|
402
|
+
"""Track function call (bridge to add_function_call)"""
|
413
403
|
record = {
|
414
404
|
"function": function_name,
|
415
405
|
"source": source,
|
@@ -422,8 +412,7 @@ def track_function_call(function_name, source, execution_time, success=True, err
|
|
422
412
|
record["error"] = error
|
423
413
|
|
424
414
|
if args:
|
425
|
-
|
426
|
-
|
415
|
+
# Sanitize arguments
|
427
416
|
sanitized_args = {}
|
428
417
|
if isinstance(args, dict):
|
429
418
|
for key, value in args.items():
|
@@ -438,7 +427,7 @@ def track_function_call(function_name, source, execution_time, success=True, err
|
|
438
427
|
conduit.add_function_call(record)
|
439
428
|
|
440
429
|
def track_rate_limit(source, limit_type, limit_value, current_usage, is_exceeded):
|
441
|
-
|
430
|
+
"""Track rate limit checks (bridge to add_rate_limit)"""
|
442
431
|
record = {
|
443
432
|
"source": source,
|
444
433
|
"limit_type": limit_type,
|
@@ -452,7 +441,7 @@ def track_rate_limit(source, limit_type, limit_value, current_usage, is_exceeded
|
|
452
441
|
conduit.add_rate_limit(record)
|
453
442
|
|
454
443
|
def track_api_request(endpoint, source, method, status_code, execution_time, request_size=0, response_size=0):
|
455
|
-
|
444
|
+
"""Track API requests (bridge to add_api_request)"""
|
456
445
|
record = {
|
457
446
|
"endpoint": endpoint,
|
458
447
|
"source": source,
|
@@ -467,13 +456,13 @@ def track_api_request(endpoint, source, method, status_code, execution_time, req
|
|
467
456
|
conduit.add_api_request(record)
|
468
457
|
|
469
458
|
def configure(webhook_url):
|
470
|
-
|
459
|
+
"""Configure webhook URL"""
|
471
460
|
return conduit.configure(webhook_url)
|
472
461
|
|
473
462
|
def sync_now():
|
474
|
-
|
463
|
+
"""Manually trigger synchronization"""
|
475
464
|
return conduit.dispatch("manual")
|
476
465
|
|
477
466
|
def retry_failed():
|
478
|
-
|
467
|
+
"""Retry failed synchronizations"""
|
479
468
|
return conduit.retry_failed()
|