simurg 1.0.0__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.
- simurg/__init__.py +51 -0
- simurg/core.py +83 -0
- simurg/data/__init__.py +6 -0
- simurg/data/dataset.py +92 -0
- simurg/data/evaluate.py +159 -0
- simurg/data/generate_dataset.py +167 -0
- simurg/data/synth.py +101 -0
- simurg/detection/__init__.py +6 -0
- simurg/detection/detectors.py +114 -0
- simurg/detection/fusion.py +103 -0
- simurg/detection/rules.py +68 -0
- simurg/detection/sentinel.py +157 -0
- simurg/features.py +194 -0
- simurg/guard_dashboard.py +331 -0
- simurg/guard_ui/index.html +433 -0
- simurg/integrations/__init__.py +6 -0
- simurg/integrations/openai_guard.py +153 -0
- simurg/learning/__init__.py +6 -0
- simurg/learning/custom.py +203 -0
- simurg/learning/model.py +69 -0
- simurg/signals/__init__.py +6 -0
- simurg/signals/calibrate.py +50 -0
- simurg/signals/changepoint.py +38 -0
- simurg/signals/ngram_lm.py +71 -0
- simurg/signals/simhash.py +62 -0
- simurg/signals/sketch.py +70 -0
- simurg/training/__init__.py +6 -0
- simurg/training/train_live.py +198 -0
- simurg/training/train_ui/index.html +196 -0
- simurg/weights/simurg_calib.json +1 -0
- simurg/weights/simurg_model.json +1 -0
- simurg-1.0.0.dist-info/METADATA +498 -0
- simurg-1.0.0.dist-info/RECORD +36 -0
- simurg-1.0.0.dist-info/WHEEL +5 -0
- simurg-1.0.0.dist-info/licenses/LICENSE +190 -0
- simurg-1.0.0.dist-info/top_level.txt +1 -0
simurg/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# Online (in-stream, token-time) detection of LLM decoding corruption —
|
|
8
|
+
# repetition collapse, cross-lingual drift, training-data regurgitation, structural
|
|
9
|
+
# breakdown, semantic discontinuity — with onset localization and a zero-leak
|
|
10
|
+
# hold/release/abort protocol. A pluggable five-detector ensemble (rules + n-gram
|
|
11
|
+
# surprise + Count-Min repetition + SimHash drift + entropy) fused under conformal
|
|
12
|
+
# calibration, plus an online-learnable tier.
|
|
13
|
+
#
|
|
14
|
+
# Host integration is five lines:
|
|
15
|
+
# from simurg import Simurg, OnlineLogReg
|
|
16
|
+
# s = Simurg(model=OnlineLogReg.load("weights/simurg_model.json")) # model optional
|
|
17
|
+
# for token in llm_stream:
|
|
18
|
+
# v = s.feed(token)
|
|
19
|
+
# if v.state == "corrupt":
|
|
20
|
+
# abort_and_retry(reason=v.reasons, onset=v.onset_char); break
|
|
21
|
+
# ui.write(v.released)
|
|
22
|
+
# final = s.finish(); ui.write(final.released)
|
|
23
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
from .core import (DRIFT, REGISTRY, REGURGITATION, REPETITION, SEMANTIC,
|
|
25
|
+
STRUCTURAL, TAXONOMY, Detector, DetectorScore, stable_hash)
|
|
26
|
+
from .features import StreamFeatures
|
|
27
|
+
from .signals.calibrate import RobustEWMA
|
|
28
|
+
from .signals.ngram_lm import OnlineCharNGram
|
|
29
|
+
from .signals.sketch import CountMinSketch, RepetitionTracker
|
|
30
|
+
from .signals.simhash import RollingSimHash
|
|
31
|
+
from .detection.rules import rule_verdict
|
|
32
|
+
from .detection.fusion import ConformalEnsemble
|
|
33
|
+
from .detection.sentinel import CLEAN, CORRUPT, SUSPECT, Simurg, Verdict
|
|
34
|
+
from .learning.model import OnlineLogReg
|
|
35
|
+
from .learning.custom import (CustomLearnedDetector, DetectabilityReport,
|
|
36
|
+
LexiconDetector, fit_custom_detector)
|
|
37
|
+
from .integrations.openai_guard import GuardedLLM
|
|
38
|
+
|
|
39
|
+
__version__ = "1.0.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"Simurg", "Verdict", "StreamFeatures", "OnlineLogReg", "ConformalEnsemble",
|
|
43
|
+
"GuardedLLM",
|
|
44
|
+
"fit_custom_detector", "DetectabilityReport", "CustomLearnedDetector",
|
|
45
|
+
"LexiconDetector",
|
|
46
|
+
"Detector", "DetectorScore", "REGISTRY", "rule_verdict", "stable_hash",
|
|
47
|
+
"OnlineCharNGram", "CountMinSketch", "RepetitionTracker", "RollingSimHash",
|
|
48
|
+
"RobustEWMA",
|
|
49
|
+
"CLEAN", "SUSPECT", "CORRUPT",
|
|
50
|
+
"REPETITION", "DRIFT", "REGURGITATION", "STRUCTURAL", "SEMANTIC", "TAXONOMY",
|
|
51
|
+
]
|
simurg/core.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# core — shared contracts of the package: the corruption taxonomy, the
|
|
8
|
+
# `DetectorScore` / `Signal` value objects, the `Detector` protocol every
|
|
9
|
+
# detector implements, a `DetectorRegistry` for plug-in extensibility, and a
|
|
10
|
+
# process-stable hash used by the sketch/SimHash estimators (so results are
|
|
11
|
+
# reproducible across runs — important for the benchmark and the paper).
|
|
12
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Callable, Dict, List, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
# ── corruption taxonomy ──────────────────────────────────────────────────────
|
|
20
|
+
REPETITION = "repetition_collapse"
|
|
21
|
+
DRIFT = "cross_lingual_drift"
|
|
22
|
+
REGURGITATION = "regurgitation"
|
|
23
|
+
STRUCTURAL = "structural_breakdown"
|
|
24
|
+
SEMANTIC = "semantic_discontinuity" # new axis surfaced by the SimHash detector
|
|
25
|
+
TAXONOMY = (REPETITION, DRIFT, REGURGITATION, STRUCTURAL, SEMANTIC)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def stable_hash(s: str, seed: int = 0) -> int:
|
|
29
|
+
"""Deterministic 64-bit hash (blake2b). Unlike Python's built-in ``hash`` it
|
|
30
|
+
is stable across processes/runs, so sketch buckets and SimHash bits are
|
|
31
|
+
reproducible — a hard requirement for a benchmark others can replicate."""
|
|
32
|
+
key = seed.to_bytes(8, "little") if seed else b""
|
|
33
|
+
return int.from_bytes(
|
|
34
|
+
hashlib.blake2b(s.encode("utf-8", "ignore"), key=key, digest_size=8).digest(),
|
|
35
|
+
"little")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DetectorScore:
|
|
40
|
+
"""One detector's read on the current stream state."""
|
|
41
|
+
name: str
|
|
42
|
+
p: float # corruption probability contributed [0,1]
|
|
43
|
+
reasons: List[str] = field(default_factory=list)
|
|
44
|
+
classes: List[str] = field(default_factory=list) # taxonomy labels
|
|
45
|
+
features: Dict[str, float] = field(default_factory=dict) # exposed numeric signals
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class Detector(Protocol):
|
|
50
|
+
"""A detector is a cheap read over the shared incremental `StreamState`.
|
|
51
|
+
It never re-scans text — all O(1)-amortized work happens in StreamState.feed;
|
|
52
|
+
`evaluate` only interprets the already-computed signals. This is what keeps
|
|
53
|
+
a five-detector ensemble as cheap as a single pass."""
|
|
54
|
+
name: str
|
|
55
|
+
|
|
56
|
+
def evaluate(self, state) -> DetectorScore: # noqa: D401
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DetectorRegistry:
|
|
61
|
+
"""Plug-in registry so open-source contributors can add a detector without
|
|
62
|
+
touching the sentinel: `@registry.register` a factory, and it joins the
|
|
63
|
+
ensemble. The fusion layer weights whatever is registered."""
|
|
64
|
+
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self._factories: Dict[str, Callable[[], Detector]] = {}
|
|
67
|
+
|
|
68
|
+
def register(self, name: str):
|
|
69
|
+
def deco(factory: Callable[[], Detector]):
|
|
70
|
+
self._factories[name] = factory
|
|
71
|
+
return factory
|
|
72
|
+
return deco
|
|
73
|
+
|
|
74
|
+
def build(self, names=None) -> List[Detector]:
|
|
75
|
+
names = names or list(self._factories)
|
|
76
|
+
return [self._factories[n]() for n in names if n in self._factories]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def names(self):
|
|
80
|
+
return tuple(self._factories)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
REGISTRY = DetectorRegistry()
|
simurg/data/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
simurg/data/dataset.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# dataset — benchmark dataset builder. Clean corpus = REAL production texts (AI
|
|
8
|
+
# answers + desk reports read from the deployment DB — AZ/EN/RU macro-economics
|
|
9
|
+
# prose with legitimate figures, the hardest possible negatives), with bundled
|
|
10
|
+
# fallback seeds when the DB is offline. Positives = synthetic CorruptBench
|
|
11
|
+
# corruptions of held-out clean texts, with exact onset ground truth. This module
|
|
12
|
+
# only READS the DB; it never imports or mutates backend runtime code.
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
|
|
19
|
+
from .synth import CLASSES, corrupt
|
|
20
|
+
|
|
21
|
+
_FALLBACK = [
|
|
22
|
+
("Büdcə xərclərinin 20% artımı qeyri-neft buraxılış kəsirini -1.64%-dən -0.44%-ə "
|
|
23
|
+
"yaxşılaşdırır, ÜDM-in illik artımı 2.94%-dən 3.78%-ə yüksəlir. İnflyasiya 5.45%-dən "
|
|
24
|
+
"5.71%-ə qalxsa da 2-6% dəhlizində qalır və Mərkəzi Bank faiz dərəcəsini sabit saxlayır. "
|
|
25
|
+
"Tikinti və nəqliyyat sektorları fiskal impulsdan ən çox faydalanan sahələrdir. ") * 6,
|
|
26
|
+
("The non-oil output gap improves from -1.6% in the baseline to -0.4%, a +1.2pp effect "
|
|
27
|
+
"of the shock. GDP grows 1.93% in 2026 versus 1.45% baseline, while inflation stays at "
|
|
28
|
+
"5.71%, inside the 2-6% corridor. The current account remains near 7.1% of GDP and the "
|
|
29
|
+
"policy rate rises only 15 basis points, so monetary conditions stay accommodative. ") * 6,
|
|
30
|
+
("Повышение бюджетных расходов на 20% ускоряет рост ВВП до 1.93% против 1.45% в базовом "
|
|
31
|
+
"сценарии. Инфляция остаётся в коридоре 2-6%, а ненефтяной разрыв выпуска сокращается "
|
|
32
|
+
"с -1.64% до -0.44%. Строительство и транспорт получают наибольший фискальный импульс. ") * 6,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_clean_corpus(min_len: int = 700, limit: int = 400) -> list[str]:
|
|
37
|
+
"""Clean-corpus loader, three tiers:
|
|
38
|
+
1. ``SIMURG_CORPUS_JSONL`` env var → path to a .jsonl with a ``text`` field
|
|
39
|
+
per line (bring your own corpus — e.g. the CorruptBench-HF clean rows);
|
|
40
|
+
2. optional deployment hook: the Black Swan production DB when this package
|
|
41
|
+
sits inside that repo (silently skipped anywhere else);
|
|
42
|
+
3. bundled fallback seed texts."""
|
|
43
|
+
texts: list[str] = []
|
|
44
|
+
corpus_path = os.environ.get("SIMURG_CORPUS_JSONL")
|
|
45
|
+
if corpus_path and os.path.exists(corpus_path):
|
|
46
|
+
import json
|
|
47
|
+
with open(corpus_path, encoding="utf-8") as f:
|
|
48
|
+
for line in f:
|
|
49
|
+
try:
|
|
50
|
+
t = json.loads(line).get("text") or ""
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
if len(t) >= min_len:
|
|
54
|
+
texts.append(t)
|
|
55
|
+
if len(texts) >= limit:
|
|
56
|
+
break
|
|
57
|
+
if not texts:
|
|
58
|
+
try: # deployment hook — absent in the open-source layout, and that's fine
|
|
59
|
+
from backend.db import engine
|
|
60
|
+
from sqlalchemy import text as _t
|
|
61
|
+
with engine.connect() as c:
|
|
62
|
+
rows = c.execute(_t(
|
|
63
|
+
"SELECT content, payload->>'report' FROM chat_messages "
|
|
64
|
+
"WHERE sender='AI' ORDER BY created_at DESC LIMIT :lim"),
|
|
65
|
+
{"lim": limit}).fetchall()
|
|
66
|
+
for content, report in rows:
|
|
67
|
+
for t in (content, report):
|
|
68
|
+
if t and len(t) >= min_len:
|
|
69
|
+
texts.append(t)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
if len(texts) < 10:
|
|
73
|
+
texts += _FALLBACK
|
|
74
|
+
return texts
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build(seed: int = 7, per_class: int = 60):
|
|
78
|
+
"""Returns (streams, meta): streams = list of (text, label, onset, cls);
|
|
79
|
+
clean streams have label 0, onset None."""
|
|
80
|
+
rng = random.Random(seed)
|
|
81
|
+
corpus = [t for t in load_clean_corpus() if len(t) >= 700]
|
|
82
|
+
rng.shuffle(corpus)
|
|
83
|
+
n_clean_pool = max(8, len(corpus) // 2)
|
|
84
|
+
clean_pool, corrupt_pool = corpus[:n_clean_pool], corpus[n_clean_pool:] or corpus
|
|
85
|
+
streams = [(t, 0, None, "clean") for t in clean_pool]
|
|
86
|
+
for cls in CLASSES:
|
|
87
|
+
for _ in range(per_class):
|
|
88
|
+
base = rng.choice(corrupt_pool)
|
|
89
|
+
text, onset, c = corrupt(base, cls, rng)
|
|
90
|
+
streams.append((text, 1, onset, c))
|
|
91
|
+
rng.shuffle(streams)
|
|
92
|
+
return streams
|
simurg/data/evaluate.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# evaluate — the end-to-end benchmark & trainer. Builds CorruptBench, trains the
|
|
8
|
+
# learned tier on StreamState checkpoints, derives conformal thresholds from clean
|
|
9
|
+
# streams, and reports the paper's results table: stream TPR/FPR, AUROC, per-class
|
|
10
|
+
# recall, detection latency past onset, onset-localization error, zero-leak rate
|
|
11
|
+
# and throughput. Persists `simurg_model.json` (weights) + `simurg_calib.json`
|
|
12
|
+
# (conformal thresholds). Run: python3 -m simurg.evaluate
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from .dataset import build
|
|
23
|
+
from ..features import StreamFeatures
|
|
24
|
+
from ..detection.fusion import ConformalEnsemble
|
|
25
|
+
from ..learning.model import OnlineLogReg
|
|
26
|
+
from ..detection.sentinel import CORRUPT, Simurg
|
|
27
|
+
from .synth import CLASSES
|
|
28
|
+
|
|
29
|
+
_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "weights")
|
|
30
|
+
_MODEL_PATH = os.path.join(_DIR, "simurg_model.json")
|
|
31
|
+
_CALIB_PATH = os.path.join(_DIR, "simurg_calib.json")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _checkpoints(text: str, every: int = 300):
|
|
35
|
+
f = StreamFeatures()
|
|
36
|
+
out = []
|
|
37
|
+
for i in range(0, len(text), every):
|
|
38
|
+
f.feed(text[i:i + every])
|
|
39
|
+
if f.total_len >= 350:
|
|
40
|
+
f.freeze_baseline()
|
|
41
|
+
out.append((f.total_len, f.vector()))
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _train_model(streams):
|
|
46
|
+
X, y = [], []
|
|
47
|
+
for text, label, onset, _cls in streams:
|
|
48
|
+
for pos, vec in _checkpoints(text):
|
|
49
|
+
if label == 0:
|
|
50
|
+
X.append(vec); y.append(0)
|
|
51
|
+
elif pos >= (onset or 0) + 250:
|
|
52
|
+
X.append(vec); y.append(1)
|
|
53
|
+
elif pos <= (onset or 0):
|
|
54
|
+
X.append(vec); y.append(0)
|
|
55
|
+
model = OnlineLogReg(len(StreamFeatures.VECTOR))
|
|
56
|
+
model.partial_fit(X, y, epochs=6)
|
|
57
|
+
return model, len(X), int(sum(y))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run_stream(text, model, fusion):
|
|
61
|
+
s = Simurg(model=model, fusion=fusion)
|
|
62
|
+
forwarded = 0
|
|
63
|
+
for i in range(0, len(text), 40):
|
|
64
|
+
v = s.feed(text[i:i + 40])
|
|
65
|
+
if v.state != CORRUPT:
|
|
66
|
+
forwarded += len(v.released)
|
|
67
|
+
else:
|
|
68
|
+
return v, s, forwarded
|
|
69
|
+
return s.finish(), s, forwarded
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _auroc(scores, labels) -> float:
|
|
73
|
+
order = sorted(zip(scores, labels))
|
|
74
|
+
pos = sum(labels); neg = len(labels) - pos
|
|
75
|
+
if not pos or not neg:
|
|
76
|
+
return float("nan")
|
|
77
|
+
rank_sum, rank = 0.0, 1
|
|
78
|
+
for _s, l in order:
|
|
79
|
+
if l == 1:
|
|
80
|
+
rank_sum += rank
|
|
81
|
+
rank += 1
|
|
82
|
+
return (rank_sum - pos * (pos + 1) / 2) / (pos * neg)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run(seed: int = 7, save: bool = False):
|
|
86
|
+
rng = random.Random(seed)
|
|
87
|
+
streams = build(seed=seed)
|
|
88
|
+
rng.shuffle(streams)
|
|
89
|
+
n_test = max(20, len(streams) // 3)
|
|
90
|
+
test, train = streams[:n_test], streams[n_test:]
|
|
91
|
+
|
|
92
|
+
model, n_vec, n_pos = _train_model(train)
|
|
93
|
+
|
|
94
|
+
# conformal calibration: fused scores of CLEAN training streams → thresholds
|
|
95
|
+
fusion = ConformalEnsemble()
|
|
96
|
+
clean_scores = []
|
|
97
|
+
for text, label, _o, _c in train:
|
|
98
|
+
if label == 0:
|
|
99
|
+
v, _s, _f = _run_stream(text, model, fusion)
|
|
100
|
+
clean_scores.append(v.p_corrupt)
|
|
101
|
+
fusion.calibrate(clean_scores, alpha=0.02, suspect_alpha=0.1)
|
|
102
|
+
|
|
103
|
+
print(f"dataset: {len(streams)} streams ({sum(1 for s in streams if s[1]==1)} corrupt) | "
|
|
104
|
+
f"train vectors: {n_vec} ({n_pos} pos) | features: {len(StreamFeatures.VECTOR)} | "
|
|
105
|
+
f"test: {len(test)}")
|
|
106
|
+
print("conformal thresholds:", {k: round(v, 3) for k, v in fusion.to_dict().items()})
|
|
107
|
+
print("learned weights:", {k: round(float(w), 2)
|
|
108
|
+
for k, w in zip(StreamFeatures.VECTOR, model.w)})
|
|
109
|
+
|
|
110
|
+
scores, labels, latencies, onset_errs = [], [], [], []
|
|
111
|
+
flagged = total_corrupt = false_alarms = clean_n = leaks_blocked = early = 0
|
|
112
|
+
cls_hits = {c: [0, 0] for c in CLASSES}
|
|
113
|
+
t0 = time.monotonic(); chars = 0
|
|
114
|
+
|
|
115
|
+
for text, label, onset, cls in test:
|
|
116
|
+
v, s, forwarded = _run_stream(text, model, fusion)
|
|
117
|
+
chars += len(text)
|
|
118
|
+
scores.append(v.p_corrupt); labels.append(label)
|
|
119
|
+
if label == 1:
|
|
120
|
+
total_corrupt += 1; cls_hits[cls][1] += 1
|
|
121
|
+
if (onset or 0) < 350:
|
|
122
|
+
early += 1
|
|
123
|
+
if v.state == CORRUPT:
|
|
124
|
+
flagged += 1; cls_hits[cls][0] += 1
|
|
125
|
+
latencies.append(max(0, s.f.total_len - (onset or 0)))
|
|
126
|
+
if v.onset_char is not None and onset is not None:
|
|
127
|
+
onset_errs.append(abs(v.onset_char - onset))
|
|
128
|
+
if (onset or 0) < 350 and forwarded == 0:
|
|
129
|
+
leaks_blocked += 1
|
|
130
|
+
else:
|
|
131
|
+
clean_n += 1
|
|
132
|
+
if v.state == CORRUPT:
|
|
133
|
+
false_alarms += 1
|
|
134
|
+
dt = time.monotonic() - t0
|
|
135
|
+
|
|
136
|
+
print("\n── results ─────────────────────────────────────────────")
|
|
137
|
+
print(f"stream TPR: {flagged}/{total_corrupt} = {flagged/max(1,total_corrupt):.3f}")
|
|
138
|
+
print(f"stream FPR: {false_alarms}/{clean_n} = {false_alarms/max(1,clean_n):.3f}")
|
|
139
|
+
print(f"AUROC: {_auroc(scores, labels):.3f}")
|
|
140
|
+
for c, (hit, tot) in cls_hits.items():
|
|
141
|
+
if tot:
|
|
142
|
+
print(f" recall[{c}]: {hit}/{tot} = {hit/tot:.2f}")
|
|
143
|
+
if latencies:
|
|
144
|
+
print(f"detection latency past onset: median {int(np.median(latencies))}, "
|
|
145
|
+
f"p90 {int(np.percentile(latencies, 90))} chars")
|
|
146
|
+
if onset_errs:
|
|
147
|
+
print(f"onset localization |err|: median {int(np.median(onset_errs))} chars")
|
|
148
|
+
print(f"zero-leak (onset<hold): {leaks_blocked}/{early}")
|
|
149
|
+
print(f"throughput: {chars/max(dt,1e-6):,.0f} chars/sec")
|
|
150
|
+
|
|
151
|
+
if save:
|
|
152
|
+
model.save(_MODEL_PATH); fusion.save(_CALIB_PATH)
|
|
153
|
+
print(f"\nsaved → {os.path.basename(_MODEL_PATH)}, {os.path.basename(_CALIB_PATH)}")
|
|
154
|
+
return model, fusion
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
import sys
|
|
159
|
+
run(save="--save" in sys.argv)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# hf_dataset/generate_dataset.py — builds the open-source CorruptBench release
|
|
8
|
+
# for HuggingFace. Clean seed texts are GENERATED (not taken from production
|
|
9
|
+
# chats — no user data leaves the deployment) by HAL-X's wahoo-1.5-preview 12B
|
|
10
|
+
# model across three languages and a spread of analytic topics; a stratified
|
|
11
|
+
# subset is corrupted by the simurg.synth injectors with exact onset ground
|
|
12
|
+
# truth. Output: corruptbench_train.jsonl / corruptbench_test.jsonl (+ stats).
|
|
13
|
+
# Split is BY SEED TEXT, so a corrupt variant never shares its clean source with
|
|
14
|
+
# the other split. Run: python3 -m simurg.data.generate_dataset
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import random
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
import urllib.request
|
|
24
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
|
+
|
|
26
|
+
from simurg.data.synth import CLASSES, corrupt
|
|
27
|
+
|
|
28
|
+
# Any OpenAI-compatible /v1/chat/completions endpoint can generate the clean
|
|
29
|
+
# seed corpus — point these env vars at yours.
|
|
30
|
+
URL = os.environ.get("SIMURG_GEN_URL", "http://localhost:8000/v1/chat/completions")
|
|
31
|
+
MODEL = os.environ.get("SIMURG_GEN_MODEL", "wahoo-1.5-preview")
|
|
32
|
+
OUT = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
N_SEEDS = 200
|
|
34
|
+
CORRUPT_PER_SEED = 3
|
|
35
|
+
TEST_FRAC = 0.2
|
|
36
|
+
SEED = 20260718
|
|
37
|
+
|
|
38
|
+
LANGS = [("en", "English"), ("az", "Azerbaijani"), ("ru", "Russian")]
|
|
39
|
+
TOPICS = [
|
|
40
|
+
"inflation dynamics and monetary policy in a small open economy",
|
|
41
|
+
"the fiscal transmission of a budget expenditure shock to non-oil sectors",
|
|
42
|
+
"how oil price movements propagate to the current account and the exchange rate",
|
|
43
|
+
"credit market conditions, overdue loans and financial stability",
|
|
44
|
+
"tourism and transport sectors under geopolitical risk",
|
|
45
|
+
"a quarterly macroeconomic outlook for an oil-exporting economy",
|
|
46
|
+
"the labor market implications of a construction boom",
|
|
47
|
+
"central bank policy corridors and interest rate decisions",
|
|
48
|
+
"diversification strategies for a resource-dependent economy",
|
|
49
|
+
"exchange rate pass-through to consumer prices",
|
|
50
|
+
"public investment multipliers and infrastructure spending",
|
|
51
|
+
"banking sector liquidity and deposit rate dynamics",
|
|
52
|
+
"agricultural output volatility and food price inflation",
|
|
53
|
+
"trade balance dynamics under changing global demand",
|
|
54
|
+
"sovereign wealth fund transfers and fiscal sustainability",
|
|
55
|
+
]
|
|
56
|
+
STYLES = [
|
|
57
|
+
"an analytical desk report with a few concrete figures",
|
|
58
|
+
"a policy briefing for a ministry, with sector detail",
|
|
59
|
+
"an explanatory note for a general audience",
|
|
60
|
+
"a quarterly bulletin section, moderately technical",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
_print_lock = threading.Lock()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _gen_one(idx: int, rng_seed: int) -> dict | None:
|
|
67
|
+
rng = random.Random(rng_seed)
|
|
68
|
+
lang_code, lang_name = LANGS[idx % len(LANGS)]
|
|
69
|
+
topic = rng.choice(TOPICS)
|
|
70
|
+
style = rng.choice(STYLES)
|
|
71
|
+
words = rng.randint(350, 750)
|
|
72
|
+
prompt = (f"Write {style} about {topic}. Length: roughly {words} words. "
|
|
73
|
+
f"Write ENTIRELY in {lang_name}. Use a professional economist's voice; "
|
|
74
|
+
f"you may include a handful of plausible illustrative figures "
|
|
75
|
+
f"(percentages, growth rates). Do not add any preamble or title markers "
|
|
76
|
+
f"beyond normal markdown headings.")
|
|
77
|
+
body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}],
|
|
78
|
+
"max_tokens": 1600, "temperature": 0.9, "top_p": 0.95}
|
|
79
|
+
for attempt in range(3):
|
|
80
|
+
try:
|
|
81
|
+
req = urllib.request.Request(
|
|
82
|
+
URL, data=json.dumps(body).encode(),
|
|
83
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
84
|
+
with urllib.request.urlopen(req, timeout=180) as r:
|
|
85
|
+
d = json.loads(r.read())
|
|
86
|
+
text = (d["choices"][0]["message"].get("content") or "").strip()
|
|
87
|
+
if len(text) >= 900:
|
|
88
|
+
return {"seed_id": idx, "language": lang_code, "topic": topic,
|
|
89
|
+
"style": style, "text": text}
|
|
90
|
+
time.sleep(1)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
with _print_lock:
|
|
93
|
+
print(f" seed {idx} attempt {attempt+1} failed: {e}")
|
|
94
|
+
time.sleep(2 + attempt * 3)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
rng = random.Random(SEED)
|
|
100
|
+
print(f"generating {N_SEEDS} clean seed texts with {MODEL} …")
|
|
101
|
+
seeds = []
|
|
102
|
+
t0 = time.monotonic()
|
|
103
|
+
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
104
|
+
futs = {ex.submit(_gen_one, i, SEED + i): i for i in range(N_SEEDS)}
|
|
105
|
+
for n, fut in enumerate(as_completed(futs), 1):
|
|
106
|
+
res = fut.result()
|
|
107
|
+
if res:
|
|
108
|
+
seeds.append(res)
|
|
109
|
+
if n % 20 == 0:
|
|
110
|
+
print(f" {n}/{N_SEEDS} done ({time.monotonic()-t0:.0f}s), "
|
|
111
|
+
f"ok={len(seeds)}")
|
|
112
|
+
print(f"clean seeds: {len(seeds)} in {time.monotonic()-t0:.0f}s")
|
|
113
|
+
|
|
114
|
+
# split BY SEED (no leakage), stratified by language
|
|
115
|
+
rng.shuffle(seeds)
|
|
116
|
+
n_test = int(len(seeds) * TEST_FRAC)
|
|
117
|
+
for i, s in enumerate(seeds):
|
|
118
|
+
s["split"] = "test" if i < n_test else "train"
|
|
119
|
+
|
|
120
|
+
# build examples: 1 clean + CORRUPT_PER_SEED corrupt per seed
|
|
121
|
+
examples = []
|
|
122
|
+
counter = 0
|
|
123
|
+
for s in seeds:
|
|
124
|
+
counter += 1
|
|
125
|
+
examples.append({
|
|
126
|
+
"id": f"cb-{counter:05d}", "split": s["split"],
|
|
127
|
+
"text": s["text"], "label": 0, "corruption_class": "clean",
|
|
128
|
+
"onset_char": None, "language": s["language"], "topic": s["topic"],
|
|
129
|
+
"n_chars": len(s["text"]), "seed_model": MODEL,
|
|
130
|
+
})
|
|
131
|
+
classes = rng.sample(list(CLASSES), k=min(CORRUPT_PER_SEED, len(CLASSES)))
|
|
132
|
+
for cls in classes:
|
|
133
|
+
counter += 1
|
|
134
|
+
crng = random.Random(hash((s["seed_id"], cls)) & 0xFFFFFFFF)
|
|
135
|
+
text_c, onset, _ = corrupt(s["text"], cls, crng, onset_frac=(0.05, 0.75))
|
|
136
|
+
examples.append({
|
|
137
|
+
"id": f"cb-{counter:05d}", "split": s["split"],
|
|
138
|
+
"text": text_c, "label": 1, "corruption_class": cls,
|
|
139
|
+
"onset_char": onset, "language": s["language"], "topic": s["topic"],
|
|
140
|
+
"n_chars": len(text_c), "seed_model": MODEL,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
rng.shuffle(examples)
|
|
144
|
+
stats = {"total": len(examples)}
|
|
145
|
+
for split in ("train", "test"):
|
|
146
|
+
rows = [e for e in examples if e["split"] == split]
|
|
147
|
+
path = os.path.join(OUT, f"corruptbench_{split}.jsonl")
|
|
148
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
149
|
+
for e in rows:
|
|
150
|
+
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
151
|
+
stats[split] = {
|
|
152
|
+
"n": len(rows),
|
|
153
|
+
"clean": sum(1 for e in rows if e["label"] == 0),
|
|
154
|
+
"corrupt": sum(1 for e in rows if e["label"] == 1),
|
|
155
|
+
"by_class": {c: sum(1 for e in rows if e["corruption_class"] == c)
|
|
156
|
+
for c in ("clean",) + tuple(CLASSES)},
|
|
157
|
+
"by_lang": {l: sum(1 for e in rows if e["language"] == l)
|
|
158
|
+
for l, _ in LANGS},
|
|
159
|
+
}
|
|
160
|
+
print(f"wrote {path} ({len(rows)} rows)")
|
|
161
|
+
with open(os.path.join(OUT, "stats.json"), "w", encoding="utf-8") as f:
|
|
162
|
+
json.dump(stats, f, indent=2, ensure_ascii=False)
|
|
163
|
+
print(json.dumps(stats, indent=2))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
main()
|
simurg/data/synth.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
#
|
|
7
|
+
# synth — the "CorruptBench" synthetic corruption generator. Injects a corruption
|
|
8
|
+
# of a chosen taxonomy class into CLEAN text at a controlled onset position,
|
|
9
|
+
# yielding (corrupt_text, onset_char, class) triples with EXACT ground truth for
|
|
10
|
+
# onset-localization and detection-latency benchmarks. Payloads are modeled on
|
|
11
|
+
# REAL production derails (table echo with '#REF!', Chinese-news drift, English
|
|
12
|
+
# README regurgitation, Persian name-list loops) — not toy noise.
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import random
|
|
17
|
+
|
|
18
|
+
from ..core import DRIFT, REGURGITATION, REPETITION, STRUCTURAL
|
|
19
|
+
|
|
20
|
+
# ── payload corpora (modeled on observed derails) ────────────────────────────
|
|
21
|
+
_CJK = ("由国立大学金融科技研发团队主导开发的智能合约审计系统今日正式宣布完成测试并开放企业试用"
|
|
22
|
+
"这套系统结合了人工智能与区块链技术能够自动侦测合约中的潜在漏洞大幅提升审计效率"
|
|
23
|
+
"根据团队表示传统的合约审计需要大量的人力而智能审计系统可以在短时间内完成分析")
|
|
24
|
+
_ARABIC = "بهرههای محمد رضایی سید علیزاده در بخصوص په دې نه سره بادي مورد بررسی قرار گرفت "
|
|
25
|
+
_README = ("## How to use\n1. Install Python packages\n```bash\npip install -r requirements.txt\n```\n"
|
|
26
|
+
"2. Set the API Key\nCreate a .env file in the root directory:\n```\nAPI_KEY=your_key_here\n```\n"
|
|
27
|
+
"3. Run the application\n```bash\npython main.py\n```\nFeatures\n- Multi-threat detection\n"
|
|
28
|
+
"- Reentrancy attacks\n- Integer overflow/underflow\n- Detailed report\n")
|
|
29
|
+
_NEWS = ("2024年01月25日 記者報導 台灣的金融科技領域迎來了一項重大突破 "
|
|
30
|
+
"The system uses deep learning to analyze code, flagging potential vulnerabilities "
|
|
31
|
+
"and suggesting fixes. This is particularly relevant given the increasing complexity. ")
|
|
32
|
+
|
|
33
|
+
_SERIES = ["MCI", "Foreign Demand", "Geopolitical Risk", "Consumer Credit",
|
|
34
|
+
"Tax Gap (lag)", "Budget Exp.", "Lag / Expectation / Residual", "Actual"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _table_echo(rng: random.Random, n_chars: int) -> str:
|
|
38
|
+
out = ["#REF! "]
|
|
39
|
+
while sum(len(p) for p in out) < n_chars:
|
|
40
|
+
name = rng.choice(_SERIES)
|
|
41
|
+
vals = " ".join(f"{rng.uniform(-3, 3):.2f}" if rng.random() < 0.4 else "0.00"
|
|
42
|
+
for _ in range(rng.randint(12, 30)))
|
|
43
|
+
out.append(f"{name} {vals} ")
|
|
44
|
+
return "".join(out)[:n_chars]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _repetition_loop(rng: random.Random, base: str, n_chars: int) -> str:
|
|
48
|
+
words = [w for w in base.split() if len(w) > 3]
|
|
49
|
+
phrase = " ".join(rng.sample(words, min(4, len(words)))) if len(words) >= 4 \
|
|
50
|
+
else "the same phrase again"
|
|
51
|
+
sep = rng.choice(["\n", " ", " — ", "\n• "])
|
|
52
|
+
unit = phrase + sep
|
|
53
|
+
return (unit * (n_chars // len(unit) + 1))[:n_chars]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _drift(rng: random.Random, n_chars: int) -> str:
|
|
57
|
+
src = _CJK if rng.random() < 0.6 else _ARABIC * 6
|
|
58
|
+
start = rng.randrange(0, max(1, len(src) - 50))
|
|
59
|
+
body = (src[start:] + src)[: n_chars]
|
|
60
|
+
return body
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _regurgitation(rng: random.Random, n_chars: int) -> str:
|
|
64
|
+
src = _README if rng.random() < 0.6 else _NEWS * 3
|
|
65
|
+
return (src * (n_chars // len(src) + 1))[:n_chars]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _structural(rng: random.Random, n_chars: int) -> str:
|
|
69
|
+
if rng.random() < 0.5:
|
|
70
|
+
return _table_echo(rng, n_chars)
|
|
71
|
+
frags = ["#### ", "``` ", "===== ", "________ ", "J) Comb $## = 2 2Community ",
|
|
72
|
+
"original}.original/m4/. # , 1 ", "{ # ,) $## 8P8 pack # { . mM # 1 ", "─" * 20 + " "]
|
|
73
|
+
out = []
|
|
74
|
+
while sum(len(p) for p in out) < n_chars:
|
|
75
|
+
out.append(rng.choice(frags))
|
|
76
|
+
return "".join(out)[:n_chars]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_INJECTORS = {
|
|
80
|
+
REPETITION: _repetition_loop,
|
|
81
|
+
DRIFT: _drift,
|
|
82
|
+
REGURGITATION: _regurgitation,
|
|
83
|
+
STRUCTURAL: _structural,
|
|
84
|
+
}
|
|
85
|
+
CLASSES = tuple(_INJECTORS)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def corrupt(clean: str, cls: str, rng: random.Random,
|
|
89
|
+
onset_frac: tuple[float, float] = (0.0, 0.7)) -> tuple[str, int, str]:
|
|
90
|
+
"""Inject a `cls` corruption into `clean`. Returns (text, onset_char, cls).
|
|
91
|
+
onset position is uniform in onset_frac of the clean length; the corruption
|
|
92
|
+
replaces the remainder (mirrors production: after collapse the model never
|
|
93
|
+
returns to the topic)."""
|
|
94
|
+
lo, hi = onset_frac
|
|
95
|
+
onset = int(len(clean) * rng.uniform(lo, hi))
|
|
96
|
+
tail_len = max(400, len(clean) - onset)
|
|
97
|
+
if cls == REPETITION:
|
|
98
|
+
payload = _INJECTORS[cls](rng, clean[:max(onset, 200)], tail_len)
|
|
99
|
+
else:
|
|
100
|
+
payload = _INJECTORS[cls](rng, tail_len)
|
|
101
|
+
return clean[:onset] + payload, onset, cls
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# SIMURG · Streaming Integrity Monitor & Universal Regeneration Guard
|
|
3
|
+
#
|
|
4
|
+
# Developed by doofZ (a.k.a Farid Aghayev from HAL-X AI)
|
|
5
|
+
# Co-Founder & Head of AI at HAL-X AI.
|
|
6
|
+
# ═════════════════════════════════════════════════════════════════════════════
|