cngx 0.1.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.
- cngx/__init__.py +54 -0
- cngx/__main__.py +6 -0
- cngx/calibration/__init__.py +32 -0
- cngx/calibration/confidence.py +251 -0
- cngx/calibration/profiles.py +625 -0
- cngx/capture/__init__.py +26 -0
- cngx/capture/adapters/__init__.py +31 -0
- cngx/capture/adapters/base.py +131 -0
- cngx/capture/adapters/claude.py +522 -0
- cngx/capture/adapters/gemini.py +698 -0
- cngx/capture/adapters/mock.py +421 -0
- cngx/capture/adapters/openai.py +535 -0
- cngx/capture/tracer.py +340 -0
- cngx/cli/__init__.py +5 -0
- cngx/cli/capture.py +162 -0
- cngx/cli/check_cmd.py +134 -0
- cngx/cli/demo.py +427 -0
- cngx/cli/diff.py +167 -0
- cngx/cli/drift.py +182 -0
- cngx/cli/enforce.py +297 -0
- cngx/cli/gate.py +305 -0
- cngx/cli/history.py +236 -0
- cngx/cli/main.py +326 -0
- cngx/cli/pin.py +172 -0
- cngx/cli/quickstart_cmd.py +120 -0
- cngx/cli/regression_cmd.py +102 -0
- cngx/cli/report_cmd.py +189 -0
- cngx/cli/submit_cmd.py +351 -0
- cngx/cli/watch.py +90 -0
- cngx/cli/wrap.py +178 -0
- cngx/contracts/__init__.py +50 -0
- cngx/contracts/schema.py +418 -0
- cngx/contracts/validator.py +607 -0
- cngx/core/__init__.py +36 -0
- cngx/core/config.py +139 -0
- cngx/core/exceptions.py +127 -0
- cngx/core/models.py +331 -0
- cngx/diff/__init__.py +6 -0
- cngx/diff/engine.py +531 -0
- cngx/diff/formatter.py +225 -0
- cngx/drift/__init__.py +7 -0
- cngx/drift/batch.py +268 -0
- cngx/drift/detector.py +473 -0
- cngx/drift/legacy.py +125 -0
- cngx/drift/legacy_streaming.py +129 -0
- cngx/drift/paired.py +203 -0
- cngx/drift/scoring.py +185 -0
- cngx/drift/semantic.py +183 -0
- cngx/drift/session.py +66 -0
- cngx/drift/streaming.py +251 -0
- cngx/drift/trajectory.py +175 -0
- cngx/enforcement/__init__.py +11 -0
- cngx/enforcement/gate.py +392 -0
- cngx/enforcement/github_action.py +173 -0
- cngx/fingerprint/__init__.py +11 -0
- cngx/fingerprint/extractor.py +184 -0
- cngx/fingerprint/metrics.py +361 -0
- cngx/fingerprint/normalizer.py +172 -0
- cngx/observability/__init__.py +18 -0
- cngx/observability/logging.py +225 -0
- cngx/observability/metrics.py +242 -0
- cngx/observability/otel.py +124 -0
- cngx/providers/__init__.py +14 -0
- cngx/providers/base.py +143 -0
- cngx/providers/rate_limiter.py +160 -0
- cngx/providers/retry.py +263 -0
- cngx/proxy/__init__.py +14 -0
- cngx/proxy/analysis.py +330 -0
- cngx/proxy/app.py +174 -0
- cngx/proxy/config.py +37 -0
- cngx/proxy/events.py +73 -0
- cngx/proxy/server.py +18 -0
- cngx/py.typed +0 -0
- cngx/security/__init__.py +5 -0
- cngx/security/regex_sandbox.py +200 -0
- cngx/server/__init__.py +5 -0
- cngx/server/app.py +549 -0
- cngx/storage/__init__.py +5 -0
- cngx/storage/backend.py +732 -0
- cngx/storage/database.py +896 -0
- cngx/system_demo/__init__.py +40 -0
- cngx/system_demo/pipeline.py +579 -0
- cngx/system_demo/runner.py +337 -0
- cngx/system_demo/scenarios.py +424 -0
- cngx/tui/__init__.py +5 -0
- cngx/tui/alerts.py +61 -0
- cngx/tui/dashboard.py +165 -0
- cngx/versioning/__init__.py +7 -0
- cngx/versioning/baseline.py +132 -0
- cngx/versioning/pinning.py +61 -0
- cngx/versioning/store.py +116 -0
- cngx-0.1.0.dist-info/METADATA +332 -0
- cngx-0.1.0.dist-info/RECORD +96 -0
- cngx-0.1.0.dist-info/WHEEL +4 -0
- cngx-0.1.0.dist-info/entry_points.txt +2 -0
- cngx-0.1.0.dist-info/licenses/LICENSE +21 -0
cngx/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cngx, behavioral contract enforcement for LLM systems.
|
|
3
|
+
|
|
4
|
+
Capture reasoning traces, extract behavioral fingerprints, validate YAML
|
|
5
|
+
contracts, detect drift, and gate deployments in CI/CD.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "cngx Contributors"
|
|
10
|
+
|
|
11
|
+
from cngx.calibration.confidence import ConfidenceCalibrator
|
|
12
|
+
from cngx.capture.adapters.base import StreamChunk
|
|
13
|
+
from cngx.capture.tracer import CngxTracer
|
|
14
|
+
from cngx.core.models import (
|
|
15
|
+
Baseline,
|
|
16
|
+
BehavioralFingerprint,
|
|
17
|
+
BehaviorChange,
|
|
18
|
+
BehaviorDiff,
|
|
19
|
+
DriftReport,
|
|
20
|
+
EvalResult,
|
|
21
|
+
ReasoningTrace,
|
|
22
|
+
)
|
|
23
|
+
from cngx.diff.engine import DiffEngine
|
|
24
|
+
from cngx.drift.detector import DriftDetector
|
|
25
|
+
from cngx.enforcement import EnforcementGate, GitHubActionGenerator
|
|
26
|
+
from cngx.fingerprint.extractor import FingerprintExtractor
|
|
27
|
+
from cngx.providers import ProviderConfig, RateLimiter, RetryConfig, retry_with_backoff
|
|
28
|
+
from cngx.versioning.baseline import BaselineManager
|
|
29
|
+
from cngx.versioning.pinning import PinningManager
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"__version__",
|
|
33
|
+
"ReasoningTrace",
|
|
34
|
+
"BehavioralFingerprint",
|
|
35
|
+
"BehaviorDiff",
|
|
36
|
+
"BehaviorChange",
|
|
37
|
+
"Baseline",
|
|
38
|
+
"DriftReport",
|
|
39
|
+
"EvalResult",
|
|
40
|
+
"CngxTracer",
|
|
41
|
+
"StreamChunk",
|
|
42
|
+
"FingerprintExtractor",
|
|
43
|
+
"DiffEngine",
|
|
44
|
+
"DriftDetector",
|
|
45
|
+
"BaselineManager",
|
|
46
|
+
"PinningManager",
|
|
47
|
+
"ProviderConfig",
|
|
48
|
+
"RateLimiter",
|
|
49
|
+
"RetryConfig",
|
|
50
|
+
"retry_with_backoff",
|
|
51
|
+
"ConfidenceCalibrator",
|
|
52
|
+
"EnforcementGate",
|
|
53
|
+
"GitHubActionGenerator",
|
|
54
|
+
]
|
cngx/__main__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Model-agnostic calibration, per-model behavioral profiles and adaptive thresholds.
|
|
2
|
+
|
|
3
|
+
cngx must work across ALL LLMs (GPT-4o, Gemini, Claude, Llama, Mistral, etc.).
|
|
4
|
+
Each model family has different reasoning styles:
|
|
5
|
+
- GPT-4o uses concise, structured reasoning
|
|
6
|
+
- Gemini 2.5 Flash uses verbose thinking tokens
|
|
7
|
+
- Claude uses <thinking> blocks with explicit self-correction
|
|
8
|
+
- Open-source models vary wildly
|
|
9
|
+
|
|
10
|
+
This module provides:
|
|
11
|
+
1. ModelProfile, behavioral baseline per model family
|
|
12
|
+
2. AdaptiveThresholds, thresholds that adjust based on model profiles
|
|
13
|
+
3. CalibrationEngine, learns profiles from observed data
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from cngx.calibration.profiles import (
|
|
17
|
+
AdaptiveThresholds,
|
|
18
|
+
CalibrationEngine,
|
|
19
|
+
ModelFamily,
|
|
20
|
+
ModelProfile,
|
|
21
|
+
get_adaptive_thresholds,
|
|
22
|
+
get_profile,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ModelProfile",
|
|
27
|
+
"ModelFamily",
|
|
28
|
+
"AdaptiveThresholds",
|
|
29
|
+
"CalibrationEngine",
|
|
30
|
+
"get_profile",
|
|
31
|
+
"get_adaptive_thresholds",
|
|
32
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Confidence calibration module.
|
|
2
|
+
|
|
3
|
+
Computes calibration metrics (Brier score, ECE, calibration curves)
|
|
4
|
+
for model confidence estimates derived from behavioral fingerprints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import math
|
|
11
|
+
import statistics
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("cngx.calibration.confidence")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CalibrationMetrics:
|
|
20
|
+
"""Calibration metrics for a set of predictions."""
|
|
21
|
+
|
|
22
|
+
brier_score: float # 0.0 (perfect) to 1.0 (worst)
|
|
23
|
+
expected_calibration_error: float # ECE
|
|
24
|
+
maximum_calibration_error: float # MCE
|
|
25
|
+
overconfidence_ratio: float # % predictions with confidence > accuracy
|
|
26
|
+
underconfidence_ratio: float # % predictions with confidence < accuracy
|
|
27
|
+
curve_data: list[dict] = field(default_factory=list) # Calibration curve
|
|
28
|
+
num_samples: int = 0
|
|
29
|
+
num_bins: int = 10
|
|
30
|
+
details: dict = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ConfidenceEstimate:
|
|
35
|
+
"""A single confidence estimate with ground truth."""
|
|
36
|
+
|
|
37
|
+
predicted_confidence: float # Model's estimated confidence (0-1)
|
|
38
|
+
actual_correct: bool # Ground truth: was the answer correct?
|
|
39
|
+
model: str = ""
|
|
40
|
+
task_id: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfidenceCalibrator:
|
|
44
|
+
"""Compute calibration metrics for model confidence estimates.
|
|
45
|
+
|
|
46
|
+
Confidence is derived from observable behavioral signals:
|
|
47
|
+
- Hedging ratio (lower hedging → higher confidence)
|
|
48
|
+
- Verification steps (more verification → higher confidence)
|
|
49
|
+
- Uncertainty markers (fewer → higher confidence)
|
|
50
|
+
- Consistency across paraphrases
|
|
51
|
+
|
|
52
|
+
This module measures how well those confidence signals
|
|
53
|
+
correlate with actual correctness.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, num_bins: int = 10):
|
|
57
|
+
self.num_bins = num_bins
|
|
58
|
+
|
|
59
|
+
def compute_metrics(
|
|
60
|
+
self,
|
|
61
|
+
estimates: list[ConfidenceEstimate],
|
|
62
|
+
) -> CalibrationMetrics:
|
|
63
|
+
"""Compute full calibration metrics.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
estimates: List of confidence estimates with ground truth
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
CalibrationMetrics with Brier score, ECE, etc.
|
|
70
|
+
"""
|
|
71
|
+
if not estimates:
|
|
72
|
+
return CalibrationMetrics(
|
|
73
|
+
brier_score=0.0,
|
|
74
|
+
expected_calibration_error=0.0,
|
|
75
|
+
maximum_calibration_error=0.0,
|
|
76
|
+
overconfidence_ratio=0.0,
|
|
77
|
+
underconfidence_ratio=0.0,
|
|
78
|
+
num_samples=0,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
n = len(estimates)
|
|
82
|
+
|
|
83
|
+
# Brier score
|
|
84
|
+
brier = self._brier_score(estimates)
|
|
85
|
+
|
|
86
|
+
# Binned calibration
|
|
87
|
+
bins = self._bin_estimates(estimates)
|
|
88
|
+
ece = self._expected_calibration_error(bins, n)
|
|
89
|
+
mce = self._maximum_calibration_error(bins)
|
|
90
|
+
|
|
91
|
+
# Over/under confidence
|
|
92
|
+
overconf, underconf = self._confidence_bias(bins)
|
|
93
|
+
|
|
94
|
+
# Calibration curve data
|
|
95
|
+
curve_data = self._calibration_curve(bins)
|
|
96
|
+
|
|
97
|
+
return CalibrationMetrics(
|
|
98
|
+
brier_score=brier,
|
|
99
|
+
expected_calibration_error=ece,
|
|
100
|
+
maximum_calibration_error=mce,
|
|
101
|
+
overconfidence_ratio=overconf,
|
|
102
|
+
underconfidence_ratio=underconf,
|
|
103
|
+
curve_data=curve_data,
|
|
104
|
+
num_samples=n,
|
|
105
|
+
num_bins=self.num_bins,
|
|
106
|
+
details={
|
|
107
|
+
"mean_confidence": statistics.mean(e.predicted_confidence for e in estimates),
|
|
108
|
+
"mean_accuracy": sum(1 for e in estimates if e.actual_correct) / n,
|
|
109
|
+
"bins_with_data": sum(1 for b in bins if b["count"] > 0),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def estimate_confidence(
|
|
114
|
+
self,
|
|
115
|
+
fingerprint: dict,
|
|
116
|
+
output: str = "",
|
|
117
|
+
) -> float:
|
|
118
|
+
"""Estimate confidence from behavioral fingerprint.
|
|
119
|
+
|
|
120
|
+
Uses observable signals to estimate how confident
|
|
121
|
+
the model was in its answer.
|
|
122
|
+
"""
|
|
123
|
+
signals = []
|
|
124
|
+
|
|
125
|
+
# Hedging ratio: lower hedging → higher confidence
|
|
126
|
+
hedging = fingerprint.get("hedging_ratio", 0.5)
|
|
127
|
+
signals.append(1.0 - min(1.0, hedging))
|
|
128
|
+
|
|
129
|
+
# Verification steps: more verification → higher confidence
|
|
130
|
+
verification = fingerprint.get("verification_steps", 0)
|
|
131
|
+
signals.append(min(1.0, verification * 0.2))
|
|
132
|
+
|
|
133
|
+
# Uncertainty markers: fewer → higher confidence
|
|
134
|
+
uncertainty = fingerprint.get("uncertainty_markers", 0)
|
|
135
|
+
confidence_markers = fingerprint.get("confidence_markers", 0)
|
|
136
|
+
total_markers = uncertainty + confidence_markers
|
|
137
|
+
if total_markers > 0:
|
|
138
|
+
signals.append(confidence_markers / total_markers)
|
|
139
|
+
else:
|
|
140
|
+
signals.append(0.5)
|
|
141
|
+
|
|
142
|
+
# Correction count: fewer corrections → higher confidence
|
|
143
|
+
corrections = fingerprint.get("correction_count", 0)
|
|
144
|
+
signals.append(max(0.0, 1.0 - corrections * 0.15))
|
|
145
|
+
|
|
146
|
+
# Structured output: structured → higher confidence
|
|
147
|
+
if fingerprint.get("structured_output"):
|
|
148
|
+
signals.append(0.8)
|
|
149
|
+
else:
|
|
150
|
+
signals.append(0.5)
|
|
151
|
+
|
|
152
|
+
# Output length consistency
|
|
153
|
+
depth = fingerprint.get("depth", 1)
|
|
154
|
+
if depth > 0:
|
|
155
|
+
tokens_per_step = fingerprint.get("tokens_per_step", 50)
|
|
156
|
+
# Very consistent step length → higher confidence
|
|
157
|
+
if 20 < tokens_per_step < 200:
|
|
158
|
+
signals.append(0.7)
|
|
159
|
+
else:
|
|
160
|
+
signals.append(0.4)
|
|
161
|
+
|
|
162
|
+
return statistics.mean(signals) if signals else 0.5
|
|
163
|
+
|
|
164
|
+
def _brier_score(self, estimates: list[ConfidenceEstimate]) -> float:
|
|
165
|
+
"""Compute Brier score: mean squared error of confidence vs outcome.
|
|
166
|
+
|
|
167
|
+
Brier = (1/N) * Σ(confidence_i - outcome_i)²
|
|
168
|
+
where outcome_i is 1 if correct, 0 otherwise.
|
|
169
|
+
"""
|
|
170
|
+
return statistics.mean(
|
|
171
|
+
(e.predicted_confidence - (1.0 if e.actual_correct else 0.0)) ** 2 for e in estimates
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _bin_estimates(self, estimates: list[ConfidenceEstimate]) -> list[dict]:
|
|
175
|
+
"""Bin estimates by confidence level."""
|
|
176
|
+
bins = [
|
|
177
|
+
{
|
|
178
|
+
"lower": i / self.num_bins,
|
|
179
|
+
"upper": (i + 1) / self.num_bins,
|
|
180
|
+
"confidences": [],
|
|
181
|
+
"accuracies": [],
|
|
182
|
+
"count": 0,
|
|
183
|
+
}
|
|
184
|
+
for i in range(self.num_bins)
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
for e in estimates:
|
|
188
|
+
bin_idx = min(
|
|
189
|
+
int(e.predicted_confidence * self.num_bins),
|
|
190
|
+
self.num_bins - 1,
|
|
191
|
+
)
|
|
192
|
+
bins[bin_idx]["confidences"].append(e.predicted_confidence)
|
|
193
|
+
bins[bin_idx]["accuracies"].append(1.0 if e.actual_correct else 0.0)
|
|
194
|
+
bins[bin_idx]["count"] += 1
|
|
195
|
+
|
|
196
|
+
# Compute bin statistics
|
|
197
|
+
for b in bins:
|
|
198
|
+
if b["count"] > 0:
|
|
199
|
+
b["mean_confidence"] = statistics.mean(b["confidences"])
|
|
200
|
+
b["mean_accuracy"] = statistics.mean(b["accuracies"])
|
|
201
|
+
b["calibration_gap"] = abs(b["mean_confidence"] - b["mean_accuracy"])
|
|
202
|
+
else:
|
|
203
|
+
b["mean_confidence"] = (b["lower"] + b["upper"]) / 2
|
|
204
|
+
b["mean_accuracy"] = 0.0
|
|
205
|
+
b["calibration_gap"] = 0.0
|
|
206
|
+
|
|
207
|
+
return bins
|
|
208
|
+
|
|
209
|
+
def _expected_calibration_error(self, bins: list[dict], total: int) -> float:
|
|
210
|
+
"""Compute Expected Calibration Error (ECE).
|
|
211
|
+
|
|
212
|
+
ECE = Σ (|B_m|/N) * |acc(B_m) - conf(B_m)|
|
|
213
|
+
"""
|
|
214
|
+
if total == 0:
|
|
215
|
+
return 0.0
|
|
216
|
+
return sum((b["count"] / total) * b["calibration_gap"] for b in bins if b["count"] > 0)
|
|
217
|
+
|
|
218
|
+
def _maximum_calibration_error(self, bins: list[dict]) -> float:
|
|
219
|
+
"""Compute Maximum Calibration Error (MCE)."""
|
|
220
|
+
gaps = [b["calibration_gap"] for b in bins if b["count"] > 0]
|
|
221
|
+
return max(gaps) if gaps else 0.0
|
|
222
|
+
|
|
223
|
+
def _confidence_bias(self, bins: list[dict]) -> tuple[float, float]:
|
|
224
|
+
"""Compute overconfidence and underconfidence ratios."""
|
|
225
|
+
total_bins = sum(1 for b in bins if b["count"] > 0)
|
|
226
|
+
if total_bins == 0:
|
|
227
|
+
return 0.0, 0.0
|
|
228
|
+
|
|
229
|
+
overconf = sum(
|
|
230
|
+
1 for b in bins if b["count"] > 0 and b["mean_confidence"] > b["mean_accuracy"]
|
|
231
|
+
)
|
|
232
|
+
underconf = sum(
|
|
233
|
+
1 for b in bins if b["count"] > 0 and b["mean_confidence"] < b["mean_accuracy"]
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return overconf / total_bins, underconf / total_bins
|
|
237
|
+
|
|
238
|
+
def _calibration_curve(self, bins: list[dict]) -> list[dict]:
|
|
239
|
+
"""Generate calibration curve data points."""
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
"bin_lower": b["lower"],
|
|
243
|
+
"bin_upper": b["upper"],
|
|
244
|
+
"mean_predicted": b["mean_confidence"],
|
|
245
|
+
"mean_actual": b["mean_accuracy"],
|
|
246
|
+
"count": b["count"],
|
|
247
|
+
"gap": b["calibration_gap"],
|
|
248
|
+
}
|
|
249
|
+
for b in bins
|
|
250
|
+
if b["count"] > 0
|
|
251
|
+
]
|