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/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
- elif record_type == "rate_limits" and self.buffer["rate_limits"] and any(item.get("is_exceeded") for item in self.buffer["rate_limits"] if isinstance(item, dict)):
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 any(not item.get("success") for item in self.buffer["function_calls"] if isinstance(item, dict)):
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
- self.add_function_call(data)
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()
vnai/scope/__init__.py CHANGED
@@ -1,7 +1,5 @@
1
- ##
2
-
3
- ##
4
-
1
+ # vnai/scope/__init__.py
2
+ # Environment detection and state tracking
5
3
 
6
4
  from vnai.scope.profile import inspector
7
5
  from vnai.scope.state import tracker, record