AbstractIntegratedModule 0.9.6__tar.gz → 0.9.8__tar.gz
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.
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.py +377 -100
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractOptimizedModules.c +200 -200
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/README.md +1 -1
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/setup.py +1 -1
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/SOURCES.txt +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/dependency_links.txt +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/requires.txt +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/top_level.txt +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractOptimizedModules.pyx +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/MANIFEST.in +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/Cargo.toml +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/src/lib.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/libsqlite3-sys-ed07b882cd2aa5e2/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/serde_core-ebc15f2e9cad7f5f/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/target-lexicon-08527f45de28143d/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/libsqlite3-sys-bf0400df4523274c/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/serde_core-5cdb76131825e4af/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/target-lexicon-43eb95a0588bf457/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AbstractIntegratedModule
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.8
|
|
4
4
|
Summary: Library for Advanced Integrated Non-LLM AI Models - Optimized Backend Framework For Non-LLM AI Agent
|
|
5
5
|
Author: Micro-Novelty
|
|
6
6
|
Author-email: hernikpuspita5@gmail.com
|
|
@@ -42,7 +42,7 @@ https://github.com/Micro-Novelty/IntegratedPipeline-Specialized-Non-LLM-AI-Agent
|
|
|
42
42
|
#### Note: The README here you are reading is a direct copy from my README Repository, to download the necessary files, you can visit my Repository with the provided link above.
|
|
43
43
|
|
|
44
44
|
### Library Short Description:
|
|
45
|
-
- Development Stage: 0.9.
|
|
45
|
+
- Development Stage: 0.9.8 Official Release.
|
|
46
46
|
- Maintainer: Micro-Novelty.
|
|
47
47
|
- library Source-Code is Open-sourced on github.
|
|
48
48
|
- Purpose: Specifically Designed for providing Non-LLM AI Agent Framework for edge Devices, Optimized for ARM64 architecture.
|
{abstractintegratedmodule-0.9.6 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.py
RENAMED
|
@@ -1950,10 +1950,7 @@ class MLP:
|
|
|
1950
1950
|
|
|
1951
1951
|
if isinstance(self.pred_counts[c], (int, float)) and self.pred_counts[c] > 0:
|
|
1952
1952
|
error_rate = self.error_counts[c] / (self.pred_counts[c] + 1e-8)
|
|
1953
|
-
|
|
1954
|
-
# error_rate=0.0 → multiplier=1.0 (no change)
|
|
1955
|
-
# error_rate=0.5 → multiplier≈0.67
|
|
1956
|
-
# error_rate=1.0 → multiplier≈0.5
|
|
1953
|
+
|
|
1957
1954
|
reputation = 1.0 / (1.0 + error_rate)
|
|
1958
1955
|
if c < len(prob):
|
|
1959
1956
|
prob[c] *= reputation
|
|
@@ -3173,8 +3170,6 @@ class WeightedEnsemblePredictor:
|
|
|
3173
3170
|
|
|
3174
3171
|
|
|
3175
3172
|
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
3173
|
def explainability_prediction_batch(self, texts, mlp_probs, trans_probs, attn_weights, show_explanation=False):
|
|
3179
3174
|
results = []
|
|
3180
3175
|
for text in texts:
|
|
@@ -3186,7 +3181,6 @@ class WeightedEnsemblePredictor:
|
|
|
3186
3181
|
return results
|
|
3187
3182
|
|
|
3188
3183
|
|
|
3189
|
-
|
|
3190
3184
|
|
|
3191
3185
|
def predict_ensemble(self, input_ids, X_mlp, y_true, method='dynamic', embedded=False):
|
|
3192
3186
|
label_bins=None
|
|
@@ -3235,7 +3229,9 @@ class WeightedEnsemblePredictor:
|
|
|
3235
3229
|
elif method == 'meta':
|
|
3236
3230
|
# Meta-learner that decides weights
|
|
3237
3231
|
ensemble_probs = self._meta_ensemble(
|
|
3238
|
-
trans_probs, mlp_probs, attn_weights, X_mlp
|
|
3232
|
+
trans_probs, mlp_probs, attn_weights, X_mlp,
|
|
3233
|
+
lstm_probs=lstm_probs,
|
|
3234
|
+
lstm_weight_hint=lstm_weight_hint
|
|
3239
3235
|
)
|
|
3240
3236
|
|
|
3241
3237
|
elif method == 'calibration':
|
|
@@ -3439,7 +3435,6 @@ class WeightedEnsemblePredictor:
|
|
|
3439
3435
|
|
|
3440
3436
|
|
|
3441
3437
|
def _attention_weighted_ensemble(self, trans_probs, mlp_probs, attn_weights):
|
|
3442
|
-
|
|
3443
3438
|
if attn_weights is None:
|
|
3444
3439
|
return (trans_probs + mlp_probs) / 2
|
|
3445
3440
|
|
|
@@ -3448,8 +3443,8 @@ class WeightedEnsemblePredictor:
|
|
|
3448
3443
|
|
|
3449
3444
|
n_trans_classes = trans_probs.shape[1]
|
|
3450
3445
|
n_mlp_classes = mlp_probs.shape[1]
|
|
3451
|
-
|
|
3452
|
-
|
|
3446
|
+
|
|
3447
|
+
n_classes = max(n_trans_classes, n_mlp_classes)
|
|
3453
3448
|
for i in range(batch_size):
|
|
3454
3449
|
trans_row = np.zeros(n_classes)
|
|
3455
3450
|
mlp_row = np.zeros(n_classes)
|
|
@@ -3486,35 +3481,76 @@ class WeightedEnsemblePredictor:
|
|
|
3486
3481
|
|
|
3487
3482
|
return ensemble
|
|
3488
3483
|
|
|
3489
|
-
def _meta_ensemble(self, trans_probs, mlp_probs, attn_weights, X_mlp
|
|
3484
|
+
def _meta_ensemble(self, trans_probs, mlp_probs, attn_weights, X_mlp,
|
|
3485
|
+
lstm_probs=None, lstm_weight_hint=0.0):
|
|
3486
|
+
lstm_row = None
|
|
3487
|
+
|
|
3488
|
+
if trans_probs.ndim == 1: trans_probs = trans_probs[np.newaxis, :]
|
|
3489
|
+
if mlp_probs.ndim == 1: mlp_probs = mlp_probs[np.newaxis, :]
|
|
3490
|
+
|
|
3491
|
+
B = trans_probs.shape[0]
|
|
3492
|
+
|
|
3490
3493
|
batch_size = trans_probs.shape[0]
|
|
3491
|
-
n_classes = trans_probs.shape[1]
|
|
3492
3494
|
threshold_feature = 0.1 + self.pipeline.confidence_threshold
|
|
3493
|
-
|
|
3495
|
+
|
|
3494
3496
|
n_trans_classes = trans_probs.shape[1]
|
|
3495
3497
|
n_mlp_classes = mlp_probs.shape[1]
|
|
3496
|
-
|
|
3498
|
+
|
|
3499
|
+
has_lstm = lstm_probs is not None
|
|
3500
|
+
if has_lstm:
|
|
3501
|
+
lstm_probs = np.asarray(lstm_probs, dtype=np.float64)
|
|
3502
|
+
if lstm_probs.ndim == 1: lstm_probs = lstm_probs[np.newaxis, :]
|
|
3503
|
+
n_lstm = lstm_probs.shape[1]
|
|
3504
|
+
else:
|
|
3505
|
+
lstm_probs = np.zeros((B, 1), dtype=np.float64) # dummy, not used
|
|
3506
|
+
n_lstm = 0
|
|
3507
|
+
|
|
3508
|
+
n_classes = max(n_trans_classes, n_lstm, n_mlp_classes)
|
|
3509
|
+
print(f"🔄 Aligning classes: trans={n_trans_classes} mlp={n_mlp_classes} "
|
|
3510
|
+
f"lstm={n_lstm} → {n_classes}")
|
|
3511
|
+
|
|
3512
|
+
if isinstance(lstm_weight_hint, (int, float)):
|
|
3513
|
+
lstm_weight_hints = np.full(B, float(lstm_weight_hint), dtype=np.float64)
|
|
3514
|
+
else:
|
|
3515
|
+
lstm_weight_hints = np.asarray(lstm_weight_hint, dtype=np.float64)
|
|
3516
|
+
if lstm_weight_hints.ndim == 0:
|
|
3517
|
+
lstm_weight_hints = np.full(B, float(lstm_weight_hints))
|
|
3497
3518
|
|
|
3498
3519
|
# Create meta features
|
|
3499
3520
|
meta_features = []
|
|
3500
3521
|
for i in range(batch_size):
|
|
3501
3522
|
trans_row = np.zeros(n_classes)
|
|
3502
3523
|
mlp_row = np.zeros(n_classes)
|
|
3503
|
-
|
|
3524
|
+
|
|
3504
3525
|
trans_row[:n_trans_classes] = trans_probs[i]
|
|
3505
3526
|
mlp_row[:n_mlp_classes] = mlp_probs[i]
|
|
3506
|
-
|
|
3527
|
+
if has_lstm:
|
|
3528
|
+
lstm_row = np.zeros(n_classes)
|
|
3529
|
+
lstm_row[:n_lstm] = lstm_probs[i]
|
|
3530
|
+
lstm_row = lstm_row / (lstm_row.sum() + 1e-8)
|
|
3531
|
+
|
|
3507
3532
|
trans_row = trans_row / (trans_row.sum() + 1e-8)
|
|
3508
3533
|
mlp_row = mlp_row / (mlp_row.sum() + 1e-8)
|
|
3509
3534
|
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3535
|
+
if lstm_row is None:
|
|
3536
|
+
features = [
|
|
3537
|
+
np.max(trans_row), # Transformer confidence
|
|
3538
|
+
np.max(mlp_row), # MLP confidence
|
|
3539
|
+
np.std(trans_row), # Transformer spread
|
|
3540
|
+
np.std(mlp_row), # MLP spread
|
|
3541
|
+
1.0 if np.argmax(trans_row) == np.argmax(mlp_row) else 0.0, # Agreement
|
|
3542
|
+
]
|
|
3543
|
+
else:
|
|
3544
|
+
features = [
|
|
3545
|
+
np.max(trans_row), # Transformer confidence
|
|
3546
|
+
np.max(mlp_row), # MLP confidence
|
|
3547
|
+
np.max(lstm_row),
|
|
3548
|
+
np.std(trans_row), # Transformer spread
|
|
3549
|
+
np.std(mlp_row), # MLP spread
|
|
3550
|
+
np.std(lstm_row),
|
|
3551
|
+
1.0 if np.argmax(trans_row) == np.argmax(mlp_row) else 0.0, # Agreement
|
|
3552
|
+
]
|
|
3553
|
+
|
|
3518
3554
|
# Add attention stats if available
|
|
3519
3555
|
if attn_weights is not None and i < len(attn_weights):
|
|
3520
3556
|
attn = attn_weights[i]
|
|
@@ -3540,6 +3576,9 @@ class WeightedEnsemblePredictor:
|
|
|
3540
3576
|
mlp_conf = meta_features[i, 1]
|
|
3541
3577
|
agreement = meta_features[i, 4]
|
|
3542
3578
|
|
|
3579
|
+
trans_pred = int(np.argmax(trans_probs[i]))
|
|
3580
|
+
mlp_pred = int(np.argmax(mlp_probs[i]))
|
|
3581
|
+
|
|
3543
3582
|
# Boost weight when models agree
|
|
3544
3583
|
base_weight = threshold_feature + AME_sigmoid * agreement
|
|
3545
3584
|
|
|
@@ -3550,11 +3589,26 @@ class WeightedEnsemblePredictor:
|
|
|
3550
3589
|
else:
|
|
3551
3590
|
trans_weight = 1.0 - base_weight
|
|
3552
3591
|
mlp_weight = base_weight
|
|
3592
|
+
|
|
3593
|
+
if has_lstm:
|
|
3594
|
+
if lstm_row is None:
|
|
3595
|
+
lstm_row = np.zeros(n_classes)
|
|
3596
|
+
lstm_row[:n_lstm] = lstm_probs[i]
|
|
3597
|
+
lstm_row /= lstm_row.sum() + 1e-8
|
|
3553
3598
|
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3599
|
+
lstm_pred = int(np.argmax(lstm_probs[i]))
|
|
3600
|
+
la = 1.0 if (lstm_pred == trans_pred or lstm_pred == mlp_pred) \
|
|
3601
|
+
else self.pipeline.confidence_threshold
|
|
3602
|
+
lw = float(lstm_weight_hints[i]) * (1.0 + la) / 2.0
|
|
3603
|
+
total = trans_weight + mlp_weight + lw + 1e-8
|
|
3604
|
+
ensemble[i] = (trans_weight/total) * trans_row + \
|
|
3605
|
+
(mlp_weight/total) * mlp_row + \
|
|
3606
|
+
(lw/total) * lstm_row
|
|
3607
|
+
else:
|
|
3608
|
+
try:
|
|
3609
|
+
ensemble[i] = trans_weight * trans_row + mlp_weight * mlp_row
|
|
3610
|
+
except:
|
|
3611
|
+
ensemble = trans_weight * trans_row + mlp_weight * mlp_row
|
|
3558
3612
|
|
|
3559
3613
|
return ensemble
|
|
3560
3614
|
|
|
@@ -5962,7 +6016,7 @@ class AsyncMessageQueue:
|
|
|
5962
6016
|
async def publish(self, message: Message) -> Any:
|
|
5963
6017
|
await self._ensure_started()
|
|
5964
6018
|
|
|
5965
|
-
# plain increment
|
|
6019
|
+
# plain increment
|
|
5966
6020
|
self._counter += 1
|
|
5967
6021
|
counter = self._counter
|
|
5968
6022
|
|
|
@@ -12871,117 +12925,272 @@ class AccurateAnswerCache:
|
|
|
12871
12925
|
|
|
12872
12926
|
|
|
12873
12927
|
|
|
12874
|
-
|
|
12875
12928
|
class RateLimiter:
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12929
|
+
"""
|
|
12930
|
+
Token bucket rate limiter.
|
|
12931
|
+
|
|
12932
|
+
Supports both:
|
|
12933
|
+
- a single shared bucket (original behavior, backward compatible)
|
|
12934
|
+
- independent per-key buckets (e.g. per peer IP/agent_id),
|
|
12935
|
+
so one noisy peer cannot starve rate-limit capacity from others
|
|
12936
|
+
"""
|
|
12937
|
+
|
|
12938
|
+
def __init__(self, requests_per_minute: int = 60, per_key: bool = False,
|
|
12939
|
+
max_keys: int = 1000):
|
|
12940
|
+
#validate config, refuse a limiter that can never refill
|
|
12941
|
+
if requests_per_minute <= 0:
|
|
12942
|
+
raise ValueError(
|
|
12943
|
+
f"[-] requests_per_minute must be > 0, got {requests_per_minute}"
|
|
12944
|
+
)
|
|
12945
|
+
|
|
12879
12946
|
self.requests_per_minute = requests_per_minute
|
|
12880
|
-
self.
|
|
12881
|
-
self.
|
|
12947
|
+
self.per_key = per_key
|
|
12948
|
+
self.max_keys = max_keys
|
|
12882
12949
|
self._lock = threading.Lock()
|
|
12883
|
-
|
|
12884
|
-
|
|
12950
|
+
|
|
12951
|
+
if per_key:
|
|
12952
|
+
# one bucket per key, so peers don't share capacity here
|
|
12953
|
+
self._buckets: Dict[str, dict] = {}
|
|
12954
|
+
else:
|
|
12955
|
+
self.tokens = float(requests_per_minute)
|
|
12956
|
+
self.last_refill = time.time()
|
|
12957
|
+
|
|
12958
|
+
def acquire(self, key: str = None) -> bool:
|
|
12885
12959
|
with self._lock:
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12960
|
+
if self.per_key:
|
|
12961
|
+
return self._acquire_keyed(key or "_default")
|
|
12962
|
+
return self._acquire_global()
|
|
12963
|
+
|
|
12964
|
+
def _acquire_global(self) -> bool:
|
|
12965
|
+
now = time.time()
|
|
12966
|
+
elapsed = now - self.last_refill
|
|
12967
|
+
|
|
12968
|
+
# guarded against clock skew producing negative elapsed
|
|
12969
|
+
if elapsed < 0:
|
|
12970
|
+
logger.warning(
|
|
12971
|
+
f'[!] RateLimiter: system clock moved backward by '
|
|
12972
|
+
f'{-elapsed:.3f}s — ignoring this interval for refill'
|
|
12973
|
+
)
|
|
12974
|
+
elapsed = 0.0
|
|
12975
|
+
|
|
12976
|
+
new_tokens = elapsed * (self.requests_per_minute / 60.0)
|
|
12977
|
+
self.tokens = min(self.requests_per_minute, self.tokens + new_tokens)
|
|
12978
|
+
self.last_refill = now
|
|
12979
|
+
|
|
12980
|
+
if self.tokens >= 1:
|
|
12981
|
+
self.tokens -= 1
|
|
12982
|
+
return True
|
|
12983
|
+
return False
|
|
12984
|
+
|
|
12985
|
+
def _acquire_keyed(self, key: str) -> bool:
|
|
12986
|
+
now = time.time()
|
|
12987
|
+
|
|
12988
|
+
if key not in self._buckets:
|
|
12989
|
+
# cap number of tracked keys to prevent unbounded
|
|
12990
|
+
# growth from an attacker cycling through many fake peer IDs
|
|
12991
|
+
if len(self._buckets) >= self.max_keys:
|
|
12992
|
+
self._evict_oldest_bucket()
|
|
12993
|
+
|
|
12994
|
+
self._buckets[key] = {
|
|
12995
|
+
'tokens': float(self.requests_per_minute),
|
|
12996
|
+
'last_refill': now,
|
|
12997
|
+
}
|
|
12998
|
+
|
|
12999
|
+
bucket = self._buckets[key]
|
|
13000
|
+
elapsed = now - bucket['last_refill']
|
|
13001
|
+
|
|
13002
|
+
if elapsed < 0:
|
|
13003
|
+
logger.warning(
|
|
13004
|
+
f'[!] RateLimiter[{key}]: clock moved backward by '
|
|
13005
|
+
f'{-elapsed:.3f}s — ignoring interval'
|
|
13006
|
+
)
|
|
13007
|
+
elapsed = 0.0
|
|
13008
|
+
|
|
13009
|
+
new_tokens = elapsed * (self.requests_per_minute / 60.0)
|
|
13010
|
+
bucket['tokens'] = min(self.requests_per_minute, bucket['tokens'] + new_tokens)
|
|
13011
|
+
bucket['last_refill'] = now
|
|
13012
|
+
|
|
13013
|
+
if bucket['tokens'] >= 1:
|
|
13014
|
+
bucket['tokens'] -= 1
|
|
13015
|
+
return True
|
|
13016
|
+
return False
|
|
13017
|
+
|
|
13018
|
+
def _evict_oldest_bucket(self):
|
|
13019
|
+
"""Evict the least-recently-refilled bucket to bound memory use."""
|
|
13020
|
+
if not self._buckets:
|
|
13021
|
+
return
|
|
13022
|
+
oldest_key = min(
|
|
13023
|
+
self._buckets.keys(),
|
|
13024
|
+
key=lambda k: self._buckets[k]['last_refill']
|
|
13025
|
+
)
|
|
13026
|
+
del self._buckets[oldest_key]
|
|
13027
|
+
|
|
13028
|
+
def get_wait_time(self, key: str = None) -> float:
|
|
13029
|
+
"""
|
|
13030
|
+
Seconds until at least 1 token will be available.
|
|
13031
|
+
Useful for callers that want to back off.
|
|
13032
|
+
"""
|
|
13033
|
+
with self._lock:
|
|
13034
|
+
if self.per_key:
|
|
13035
|
+
bucket = self._buckets.get(key or "_default")
|
|
13036
|
+
tokens = bucket['tokens'] if bucket else self.requests_per_minute
|
|
13037
|
+
else:
|
|
13038
|
+
tokens = self.tokens
|
|
13039
|
+
|
|
13040
|
+
if tokens >= 1:
|
|
13041
|
+
return 0.0
|
|
13042
|
+
tokens_needed = 1 - tokens
|
|
13043
|
+
return tokens_needed / (self.requests_per_minute / 60.0)
|
|
13044
|
+
|
|
13045
|
+
def get_stats(self) -> Dict:
|
|
13046
|
+
"""Visibility into limiter state"""
|
|
13047
|
+
with self._lock:
|
|
13048
|
+
if self.per_key:
|
|
13049
|
+
return {
|
|
13050
|
+
'mode' : 'per_key',
|
|
13051
|
+
'tracked_keys' : len(self._buckets),
|
|
13052
|
+
'max_keys' : self.max_keys,
|
|
13053
|
+
'requests_per_minute': self.requests_per_minute,
|
|
13054
|
+
}
|
|
13055
|
+
return {
|
|
13056
|
+
'mode' : 'global',
|
|
13057
|
+
'current_tokens': round(self.tokens, 2),
|
|
13058
|
+
'requests_per_minute': self.requests_per_minute,
|
|
13059
|
+
}
|
|
13060
|
+
|
|
12897
13061
|
|
|
12898
13062
|
class InputSanitizer:
|
|
12899
|
-
|
|
12900
|
-
|
|
13063
|
+
"""Sanitize and validate inputs."""
|
|
13064
|
+
|
|
13065
|
+
# comprehensive control-char stripping including
|
|
13066
|
+
# newline/CR, since this class explicitly exists to protect
|
|
13067
|
+
# logging/serialization from injection
|
|
13068
|
+
_CONTROL_CHARS_PATTERN = re.compile(
|
|
13069
|
+
r'[\x00-\x1f\x7f]'
|
|
13070
|
+
)
|
|
13071
|
+
|
|
13072
|
+
# OPT-IN for genuinely
|
|
13073
|
+
# multi-line legitimate text (e.g. free-form descriptions).
|
|
13074
|
+
_CONTROL_CHARS_ALLOW_WHITESPACE = re.compile(
|
|
13075
|
+
r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'
|
|
13076
|
+
)
|
|
13077
|
+
|
|
12901
13078
|
@staticmethod
|
|
12902
|
-
def sanitize_text(text: str, max_length: int = 10000
|
|
13079
|
+
def sanitize_text(text: str, max_length: int = 10000,
|
|
13080
|
+
allow_newlines: bool = False) -> str:
|
|
12903
13081
|
if not isinstance(text, str):
|
|
12904
13082
|
raise SecurityError("[-] Input must be a string")
|
|
12905
|
-
|
|
12906
|
-
#
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
# Limit length
|
|
13083
|
+
|
|
13084
|
+
# check length BEFORE any transformation, so length
|
|
13085
|
+
# limits reflect what was actually submitted.
|
|
12910
13086
|
if len(text) > max_length:
|
|
12911
13087
|
raise SecurityError(f"[-] Text exceeds maximum length of {max_length}")
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
13088
|
+
|
|
13089
|
+
if allow_newlines:
|
|
13090
|
+
text = InputSanitizer._CONTROL_CHARS_ALLOW_WHITESPACE.sub('', text)
|
|
13091
|
+
# even when newlines are allowed, escape them for log safety
|
|
13092
|
+
else:
|
|
13093
|
+
text = InputSanitizer._CONTROL_CHARS_PATTERN.sub('', text)
|
|
13094
|
+
|
|
13095
|
+
escape_patterns = [
|
|
13096
|
+
r'\\x[0-9a-fA-F]{2}', # literal "\x41" style escapes
|
|
13097
|
+
r'\\u[0-9a-fA-F]{4}', # literal "\u0041" style escapes
|
|
12918
13098
|
]
|
|
12919
|
-
|
|
12920
|
-
for pattern in dangerous_patterns:
|
|
13099
|
+
for pattern in escape_patterns:
|
|
12921
13100
|
if re.search(pattern, text):
|
|
12922
|
-
|
|
13101
|
+
logger.warning(
|
|
13102
|
+
f'[!] sanitize_text: literal escape sequence pattern '
|
|
13103
|
+
f'detected and neutralized (pattern={pattern})'
|
|
13104
|
+
)
|
|
12923
13105
|
text = re.sub(pattern, '?', text)
|
|
12924
|
-
|
|
13106
|
+
|
|
12925
13107
|
return text.strip()
|
|
12926
|
-
|
|
13108
|
+
|
|
12927
13109
|
@staticmethod
|
|
12928
13110
|
def validate_batch_size(size: int, max_batch: int = 100) -> bool:
|
|
13111
|
+
# validated type before comparison to avoid a confusing
|
|
13112
|
+
# TypeError if a non-int slips through
|
|
13113
|
+
if not isinstance(size, int):
|
|
13114
|
+
raise SecurityError(f"[-] Batch size must be an integer, got {type(size).__name__}")
|
|
12929
13115
|
if size <= 0 or size > max_batch:
|
|
12930
13116
|
raise SecurityError(f"[-] Batch size must be between 1 and {max_batch}")
|
|
12931
13117
|
return True
|
|
12932
13118
|
|
|
13119
|
+
|
|
12933
13120
|
class ApiKeyManager:
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
13121
|
+
"""Manage API keys with rotation and bounded storage."""
|
|
13122
|
+
|
|
13123
|
+
MIN_KEY_LENGTH = 16 # minimum acceptable length for caller-supplied keys
|
|
13124
|
+
|
|
13125
|
+
def __init__(self, rotation_days: int = 30, max_keys: int = 10000):
|
|
13126
|
+
self.keys: Dict[str, dict] = {}
|
|
12938
13127
|
self.rotation_days = rotation_days
|
|
13128
|
+
self.max_keys = max_keys
|
|
12939
13129
|
self._lock = threading.Lock()
|
|
12940
|
-
|
|
13130
|
+
|
|
12941
13131
|
def generate_key(self, metadata: dict = None, key_value: str = None) -> str:
|
|
12942
|
-
# Generate a new API key
|
|
12943
13132
|
if key_value:
|
|
12944
|
-
#
|
|
13133
|
+
# reject obviously weak caller-supplied keys
|
|
13134
|
+
if not isinstance(key_value, str) or len(key_value) < self.MIN_KEY_LENGTH:
|
|
13135
|
+
raise SecurityError(
|
|
13136
|
+
f"[-] Provided key must be a string of at least "
|
|
13137
|
+
f"{self.MIN_KEY_LENGTH} characters"
|
|
13138
|
+
)
|
|
12945
13139
|
raw_key = key_value
|
|
12946
13140
|
else:
|
|
12947
|
-
|
|
12948
|
-
raw_key = secrets.token_urlsafe(32)
|
|
13141
|
+
raw_key = secrets.token_urlsafe(32)
|
|
12949
13142
|
|
|
12950
13143
|
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
12951
|
-
|
|
13144
|
+
|
|
12952
13145
|
with self._lock:
|
|
13146
|
+
# warn on collision
|
|
13147
|
+
if key_hash in self.keys and self.keys[key_hash].get('is_active'):
|
|
13148
|
+
logger.warning(
|
|
13149
|
+
f'[!] generate_key: key hash collision with an '
|
|
13150
|
+
f'already-active key — overwriting metadata'
|
|
13151
|
+
)
|
|
13152
|
+
|
|
13153
|
+
# evict oldest inactive entries if at capacity,
|
|
13154
|
+
if len(self.keys) >= self.max_keys:
|
|
13155
|
+
self._evict_oldest_inactive()
|
|
13156
|
+
|
|
12953
13157
|
self.keys[key_hash] = {
|
|
12954
13158
|
'created_at': datetime.now(),
|
|
12955
|
-
'last_used': None,
|
|
12956
|
-
'metadata': metadata or {},
|
|
12957
|
-
'is_active': True
|
|
13159
|
+
'last_used' : None,
|
|
13160
|
+
'metadata' : metadata or {},
|
|
13161
|
+
'is_active' : True
|
|
12958
13162
|
}
|
|
12959
|
-
|
|
13163
|
+
|
|
12960
13164
|
return raw_key
|
|
12961
|
-
|
|
13165
|
+
|
|
12962
13166
|
def validate_key(self, api_key: str) -> bool:
|
|
12963
|
-
#
|
|
12964
|
-
|
|
13167
|
+
# explicit type check before .encode(), avoids an
|
|
13168
|
+
# uncontrolled AttributeError on non-string input
|
|
13169
|
+
if not api_key or not isinstance(api_key, str):
|
|
12965
13170
|
return False
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
13171
|
+
|
|
13172
|
+
try:
|
|
13173
|
+
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
13174
|
+
except (UnicodeEncodeError, AttributeError):
|
|
13175
|
+
return False
|
|
13176
|
+
|
|
12969
13177
|
with self._lock:
|
|
12970
13178
|
key_info = self.keys.get(key_hash)
|
|
12971
13179
|
if not key_info or not key_info.get('is_active', False):
|
|
12972
13180
|
return False
|
|
12973
|
-
|
|
12974
|
-
# Check rotation
|
|
13181
|
+
|
|
12975
13182
|
age = (datetime.now() - key_info['created_at']).days
|
|
12976
13183
|
if age >= self.rotation_days:
|
|
12977
13184
|
key_info['is_active'] = False
|
|
12978
13185
|
return False
|
|
12979
|
-
|
|
13186
|
+
|
|
12980
13187
|
key_info['last_used'] = datetime.now()
|
|
12981
13188
|
return True
|
|
12982
|
-
|
|
13189
|
+
|
|
12983
13190
|
def revoke_key(self, api_key: str) -> bool:
|
|
12984
|
-
|
|
13191
|
+
if not api_key or not isinstance(api_key, str):
|
|
13192
|
+
return False
|
|
13193
|
+
|
|
12985
13194
|
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
12986
13195
|
with self._lock:
|
|
12987
13196
|
if key_hash in self.keys:
|
|
@@ -12989,6 +13198,54 @@ class ApiKeyManager:
|
|
|
12989
13198
|
return True
|
|
12990
13199
|
return False
|
|
12991
13200
|
|
|
13201
|
+
def cleanup_expired(self, grace_period_days: int = 7):
|
|
13202
|
+
"""
|
|
13203
|
+
periodic sweep to actually remove long-inactive keys.
|
|
13204
|
+
"""
|
|
13205
|
+
with self._lock:
|
|
13206
|
+
now = datetime.now()
|
|
13207
|
+
to_remove = [
|
|
13208
|
+
key_hash for key_hash, info in self.keys.items()
|
|
13209
|
+
if not info.get('is_active', False)
|
|
13210
|
+
and (now - info['created_at']).days >= (self.rotation_days + grace_period_days)
|
|
13211
|
+
]
|
|
13212
|
+
for key_hash in to_remove:
|
|
13213
|
+
del self.keys[key_hash]
|
|
13214
|
+
|
|
13215
|
+
if to_remove:
|
|
13216
|
+
logger.info(f'[=] Cleaned up {len(to_remove)} expired API key entries')
|
|
13217
|
+
|
|
13218
|
+
return len(to_remove)
|
|
13219
|
+
|
|
13220
|
+
def _evict_oldest_inactive(self):
|
|
13221
|
+
"""Evict the oldest inactive key to make room, called under lock."""
|
|
13222
|
+
inactive = [
|
|
13223
|
+
(h, info) for h, info in self.keys.items()
|
|
13224
|
+
if not info.get('is_active', False)
|
|
13225
|
+
]
|
|
13226
|
+
if inactive:
|
|
13227
|
+
oldest_hash = min(inactive, key=lambda x: x[1]['created_at'])[0]
|
|
13228
|
+
del self.keys[oldest_hash]
|
|
13229
|
+
else:
|
|
13230
|
+
# no inactive keys to evict and still at capacity
|
|
13231
|
+
logger.warning(
|
|
13232
|
+
f'[!] ApiKeyManager at max_keys={self.max_keys} capacity '
|
|
13233
|
+
f'with no inactive keys to evict — consider raising max_keys '
|
|
13234
|
+
f'or auditing why so many keys remain active'
|
|
13235
|
+
)
|
|
13236
|
+
|
|
13237
|
+
def get_stats(self) -> Dict:
|
|
13238
|
+
"""Visibility into key store health — same pattern as WorkerPool.get_health()."""
|
|
13239
|
+
with self._lock:
|
|
13240
|
+
active = sum(1 for k in self.keys.values() if k.get('is_active'))
|
|
13241
|
+
inactive = len(self.keys) - active
|
|
13242
|
+
return {
|
|
13243
|
+
'total_keys' : len(self.keys),
|
|
13244
|
+
'active_keys' : active,
|
|
13245
|
+
'inactive_keys': inactive,
|
|
13246
|
+
'at_capacity' : len(self.keys) >= self.max_keys,
|
|
13247
|
+
}
|
|
13248
|
+
|
|
12992
13249
|
class AsyncResultQueue:
|
|
12993
13250
|
"""
|
|
12994
13251
|
Complete result queue with integrated processor.
|
|
@@ -15474,7 +15731,7 @@ class PipelinePredictionManager:
|
|
|
15474
15731
|
if titles is not None and rules is not None:
|
|
15475
15732
|
print(f"[🔍] Preparing data for {len(titles)} titles with {len(rules)} length of rules.")
|
|
15476
15733
|
if X is None and y is None or X is None or y is None:
|
|
15477
|
-
print('[🔄] Creating automatic X samples because X is not provided manually.')
|
|
15734
|
+
print('[🔄] Creating automatic X samples because X Samples is not provided manually.')
|
|
15478
15735
|
datasets, X_gen = self.pipeline.data_preparation(titles, label_map)
|
|
15479
15736
|
_, y, _, _ = self.pipeline.mlp_training_features(rules, datasets)
|
|
15480
15737
|
else:
|
|
@@ -15735,7 +15992,7 @@ class PipelinePredictionManager:
|
|
|
15735
15992
|
num_classes = num_classes or mlp_probs.shape[1]
|
|
15736
15993
|
models_agree = False
|
|
15737
15994
|
|
|
15738
|
-
# ── MLP ─────────────────────────────────────────────
|
|
15995
|
+
# ── MLP layer ─────────────────────────────────────────────
|
|
15739
15996
|
mlp_class_idx = int(np.argmax(mlp_probs[i]))
|
|
15740
15997
|
is_valid_index = 0 <= mlp_class_idx < num_classes
|
|
15741
15998
|
|
|
@@ -15891,6 +16148,7 @@ class PipelinePredictionManager:
|
|
|
15891
16148
|
use_transformer=True, return_attention=False,
|
|
15892
16149
|
save_results=True, batch_size=2):
|
|
15893
16150
|
try:
|
|
16151
|
+
# ____ init temporary layer ____
|
|
15894
16152
|
eps = 1e-5
|
|
15895
16153
|
trans_probs = None
|
|
15896
16154
|
attn_weights = None
|
|
@@ -15993,6 +16251,7 @@ class PipelinePredictionManager:
|
|
|
15993
16251
|
|
|
15994
16252
|
if X is None or len(X) == 0 or isinstance(X, int) or (isinstance(X, np.ndarray) and X.size == 0):
|
|
15995
16253
|
# Get MLP predictions
|
|
16254
|
+
titles = None
|
|
15996
16255
|
if titles is not None and len(titles) > 0:
|
|
15997
16256
|
if isinstance(titles[0], tuple):
|
|
15998
16257
|
mlp_titles = [t[0] for t in titles]
|
|
@@ -16001,8 +16260,16 @@ class PipelinePredictionManager:
|
|
|
16001
16260
|
|
|
16002
16261
|
if not hasattr(self.pipeline, 'tfidf') or self.pipeline.tfidf is None:
|
|
16003
16262
|
self.pipeline.initialize_fitting(mlp_titles)
|
|
16004
|
-
|
|
16005
|
-
|
|
16263
|
+
|
|
16264
|
+
if isinstance(mlp_titles, (list, tuple, np.ndarray)):
|
|
16265
|
+
titles = mlp_titles[0]
|
|
16266
|
+
if isinstance(mlp_titles, (list, tuple, np.ndarray)):
|
|
16267
|
+
titles = titles[0]
|
|
16268
|
+
|
|
16269
|
+
if not isinstance(mlp_titles, list) or isinstance(titles, str):
|
|
16270
|
+
if titles is not None:
|
|
16271
|
+
mlp_titles = titles
|
|
16272
|
+
X = self.pipeline.tfidf.transform(mlp_titles).toarray()
|
|
16006
16273
|
|
|
16007
16274
|
if X_gen is not None:
|
|
16008
16275
|
X = X_gen
|
|
@@ -16336,9 +16603,19 @@ class PipelinePredictionManager:
|
|
|
16336
16603
|
}
|
|
16337
16604
|
results.append(result)
|
|
16338
16605
|
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
|
|
16606
|
+
if results is not None and isinstance(results[0], dict):
|
|
16607
|
+
try:
|
|
16608
|
+
final_probs = results[0]['final_probs'] if results else None
|
|
16609
|
+
final_class_idx = results[0]['predicted_idx'] if results else None
|
|
16610
|
+
agreement = results[0]['models_agree'] if results else None
|
|
16611
|
+
except:
|
|
16612
|
+
final_probs = results[0].get('final_probs', None)
|
|
16613
|
+
final_class_idx = results[0].get('predicted_idx', None)
|
|
16614
|
+
agreement = results[0].get('models_agree', None)
|
|
16615
|
+
else:
|
|
16616
|
+
final_probs = mlp_probs
|
|
16617
|
+
final_class_idx = target_pred_indices
|
|
16618
|
+
agreement = False
|
|
16342
16619
|
|
|
16343
16620
|
# Display results
|
|
16344
16621
|
verbose = False
|