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.
Files changed (96) hide show
  1. cngx/__init__.py +54 -0
  2. cngx/__main__.py +6 -0
  3. cngx/calibration/__init__.py +32 -0
  4. cngx/calibration/confidence.py +251 -0
  5. cngx/calibration/profiles.py +625 -0
  6. cngx/capture/__init__.py +26 -0
  7. cngx/capture/adapters/__init__.py +31 -0
  8. cngx/capture/adapters/base.py +131 -0
  9. cngx/capture/adapters/claude.py +522 -0
  10. cngx/capture/adapters/gemini.py +698 -0
  11. cngx/capture/adapters/mock.py +421 -0
  12. cngx/capture/adapters/openai.py +535 -0
  13. cngx/capture/tracer.py +340 -0
  14. cngx/cli/__init__.py +5 -0
  15. cngx/cli/capture.py +162 -0
  16. cngx/cli/check_cmd.py +134 -0
  17. cngx/cli/demo.py +427 -0
  18. cngx/cli/diff.py +167 -0
  19. cngx/cli/drift.py +182 -0
  20. cngx/cli/enforce.py +297 -0
  21. cngx/cli/gate.py +305 -0
  22. cngx/cli/history.py +236 -0
  23. cngx/cli/main.py +326 -0
  24. cngx/cli/pin.py +172 -0
  25. cngx/cli/quickstart_cmd.py +120 -0
  26. cngx/cli/regression_cmd.py +102 -0
  27. cngx/cli/report_cmd.py +189 -0
  28. cngx/cli/submit_cmd.py +351 -0
  29. cngx/cli/watch.py +90 -0
  30. cngx/cli/wrap.py +178 -0
  31. cngx/contracts/__init__.py +50 -0
  32. cngx/contracts/schema.py +418 -0
  33. cngx/contracts/validator.py +607 -0
  34. cngx/core/__init__.py +36 -0
  35. cngx/core/config.py +139 -0
  36. cngx/core/exceptions.py +127 -0
  37. cngx/core/models.py +331 -0
  38. cngx/diff/__init__.py +6 -0
  39. cngx/diff/engine.py +531 -0
  40. cngx/diff/formatter.py +225 -0
  41. cngx/drift/__init__.py +7 -0
  42. cngx/drift/batch.py +268 -0
  43. cngx/drift/detector.py +473 -0
  44. cngx/drift/legacy.py +125 -0
  45. cngx/drift/legacy_streaming.py +129 -0
  46. cngx/drift/paired.py +203 -0
  47. cngx/drift/scoring.py +185 -0
  48. cngx/drift/semantic.py +183 -0
  49. cngx/drift/session.py +66 -0
  50. cngx/drift/streaming.py +251 -0
  51. cngx/drift/trajectory.py +175 -0
  52. cngx/enforcement/__init__.py +11 -0
  53. cngx/enforcement/gate.py +392 -0
  54. cngx/enforcement/github_action.py +173 -0
  55. cngx/fingerprint/__init__.py +11 -0
  56. cngx/fingerprint/extractor.py +184 -0
  57. cngx/fingerprint/metrics.py +361 -0
  58. cngx/fingerprint/normalizer.py +172 -0
  59. cngx/observability/__init__.py +18 -0
  60. cngx/observability/logging.py +225 -0
  61. cngx/observability/metrics.py +242 -0
  62. cngx/observability/otel.py +124 -0
  63. cngx/providers/__init__.py +14 -0
  64. cngx/providers/base.py +143 -0
  65. cngx/providers/rate_limiter.py +160 -0
  66. cngx/providers/retry.py +263 -0
  67. cngx/proxy/__init__.py +14 -0
  68. cngx/proxy/analysis.py +330 -0
  69. cngx/proxy/app.py +174 -0
  70. cngx/proxy/config.py +37 -0
  71. cngx/proxy/events.py +73 -0
  72. cngx/proxy/server.py +18 -0
  73. cngx/py.typed +0 -0
  74. cngx/security/__init__.py +5 -0
  75. cngx/security/regex_sandbox.py +200 -0
  76. cngx/server/__init__.py +5 -0
  77. cngx/server/app.py +549 -0
  78. cngx/storage/__init__.py +5 -0
  79. cngx/storage/backend.py +732 -0
  80. cngx/storage/database.py +896 -0
  81. cngx/system_demo/__init__.py +40 -0
  82. cngx/system_demo/pipeline.py +579 -0
  83. cngx/system_demo/runner.py +337 -0
  84. cngx/system_demo/scenarios.py +424 -0
  85. cngx/tui/__init__.py +5 -0
  86. cngx/tui/alerts.py +61 -0
  87. cngx/tui/dashboard.py +165 -0
  88. cngx/versioning/__init__.py +7 -0
  89. cngx/versioning/baseline.py +132 -0
  90. cngx/versioning/pinning.py +61 -0
  91. cngx/versioning/store.py +116 -0
  92. cngx-0.1.0.dist-info/METADATA +332 -0
  93. cngx-0.1.0.dist-info/RECORD +96 -0
  94. cngx-0.1.0.dist-info/WHEEL +4 -0
  95. cngx-0.1.0.dist-info/entry_points.txt +2 -0
  96. 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,6 @@
1
+ """Allow ``python -m cngx`` and PyInstaller entry."""
2
+
3
+ from cngx.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -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
+ ]