consequa-x 0.1.0a2__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.
- consequa_x/__init__.py +22 -0
- consequa_x/__main__.py +3 -0
- consequa_x/adapters/__init__.py +5 -0
- consequa_x/adapters/ollama.py +350 -0
- consequa_x/attribution.py +407 -0
- consequa_x/benchmark/__init__.py +1 -0
- consequa_x/benchmark/assessment_metrics.py +54 -0
- consequa_x/benchmark/calibration.py +787 -0
- consequa_x/benchmark/calibration_export.py +139 -0
- consequa_x/benchmark/calibration_mechanisms.py +266 -0
- consequa_x/benchmark/decision_coalition.py +254 -0
- consequa_x/benchmark/decision_contrastive.py +448 -0
- consequa_x/benchmark/decision_replay.py +329 -0
- consequa_x/benchmark/decision_shapley.py +628 -0
- consequa_x/benchmark/evaluation.py +187 -0
- consequa_x/benchmark/interaction_export.py +220 -0
- consequa_x/benchmark/interaction_mechanisms.py +192 -0
- consequa_x/benchmark/interval_comparison.py +276 -0
- consequa_x/benchmark/intervals.py +164 -0
- consequa_x/benchmark/payments.py +426 -0
- consequa_x/benchmark/policy_experiment.py +298 -0
- consequa_x/benchmark/policy_runner.py +234 -0
- consequa_x/benchmark/runner.py +241 -0
- consequa_x/benchmark/study.py +183 -0
- consequa_x/benchmark/targets.py +149 -0
- consequa_x/benchmark/templates.py +102 -0
- consequa_x/cli.py +1191 -0
- consequa_x/explanation.py +130 -0
- consequa_x/graph.py +142 -0
- consequa_x/models.py +532 -0
- consequa_x/policies.py +248 -0
- consequa_x/py.typed +0 -0
- consequa_x/schema.py +159 -0
- consequa_x/schemas/trace-v0.1.json +543 -0
- consequa_x/verification.py +192 -0
- consequa_x-0.1.0a2.dist-info/METADATA +303 -0
- consequa_x-0.1.0a2.dist-info/RECORD +41 -0
- consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
- consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
- consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
- consequa_x-0.1.0a2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Paired replay estimates for action selection and verified consequences.
|
|
2
|
+
|
|
3
|
+
An effect is ``baseline_rate - intervention_rate``. Positive values mean that
|
|
4
|
+
neutralizing/replacing the candidate reduced the target rate; negative values
|
|
5
|
+
mean that it increased it. These estimates are signed experimental effects,
|
|
6
|
+
not normalized responsibility shares or proof that a trace edge is causal.
|
|
7
|
+
|
|
8
|
+
Each runner call must start an isolated world and be reproducible for the same
|
|
9
|
+
intervention and seed. The schedule consists of ``seed, ..., seed + samples -
|
|
10
|
+
1``. One factual replay per seed is reused across candidates. Matching seeds
|
|
11
|
+
pairs the original and changed worlds; independent random streams across seeds
|
|
12
|
+
are the runner's responsibility. An interval describes this replay experiment,
|
|
13
|
+
not generalization to other agents, prompts, providers, or environments.
|
|
14
|
+
|
|
15
|
+
Intervals subtract Bonferroni-adjusted marginal Wilson bounds: each marginal
|
|
16
|
+
interval has nominal 97.5% coverage, giving nominal conservative 95% joint
|
|
17
|
+
coverage without assuming independence between the paired worlds. Wilson
|
|
18
|
+
coverage is approximate, especially at small sample counts; this is not an
|
|
19
|
+
exact finite-sample guarantee. The deliberately broad interval does not model
|
|
20
|
+
within-pair covariance. Missing consequence outcomes refuse the entire estimate
|
|
21
|
+
instead of turning unknowns into failures or selecting only complete pairs.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from math import isclose, isfinite, sqrt
|
|
28
|
+
from statistics import NormalDist
|
|
29
|
+
from typing import Any, Iterable, Literal, Protocol, runtime_checkable
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ReplayTarget = Literal["action", "consequence", "action_linked_consequence"]
|
|
33
|
+
REPLAY_TARGETS = ("action", "consequence", "action_linked_consequence")
|
|
34
|
+
_TARGETS = frozenset(REPLAY_TARGETS)
|
|
35
|
+
_TARGET_ATTRIBUTES = {
|
|
36
|
+
"action": "action_selected",
|
|
37
|
+
"consequence": "consequence_satisfied",
|
|
38
|
+
"action_linked_consequence": "action_linked_consequence_satisfied",
|
|
39
|
+
}
|
|
40
|
+
_INTERVENTION_KINDS = frozenset({"neutralize", "replace"})
|
|
41
|
+
_CONTEXT_TYPES = frozenset({"user_input", "system_instruction", "retrieval", "memory_read"})
|
|
42
|
+
# Two two-sided 97.5% Wilson intervals form a nominal 95% joint bound.
|
|
43
|
+
_WILSON_Z = NormalDist().inv_cdf(0.9875)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _nonempty_string(value: object, name: str) -> str:
|
|
47
|
+
if not isinstance(value, str) or not value.strip():
|
|
48
|
+
raise ValueError(f"{name} must be a nonempty string")
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _integer(value: object, name: str, *, minimum: int | None = None) -> int:
|
|
53
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
54
|
+
raise ValueError(f"{name} must be an integer, not a boolean")
|
|
55
|
+
if minimum is not None and value < minimum:
|
|
56
|
+
raise ValueError(f"{name} must be at least {minimum}")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _finite_number(value: object, name: str, low: float, high: float) -> float:
|
|
61
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
62
|
+
raise ValueError(f"{name} must be a finite number")
|
|
63
|
+
try:
|
|
64
|
+
number = float(value)
|
|
65
|
+
except (OverflowError, ValueError) as exc:
|
|
66
|
+
raise ValueError(f"{name} must be a finite number") from exc
|
|
67
|
+
if not isfinite(number) or not low <= number <= high:
|
|
68
|
+
raise ValueError(f"{name} must be finite and between {low} and {high}")
|
|
69
|
+
return number
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _copy_replacement(value: object, active: set[int] | None = None) -> object:
|
|
73
|
+
"""Validate and detach an ordinary JSON payload from its caller."""
|
|
74
|
+
|
|
75
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
76
|
+
return value
|
|
77
|
+
if isinstance(value, float):
|
|
78
|
+
if not isfinite(value):
|
|
79
|
+
raise ValueError("replacement cannot contain nonfinite numbers")
|
|
80
|
+
return value
|
|
81
|
+
if not isinstance(value, (dict, list)):
|
|
82
|
+
raise TypeError("replacement must contain only JSON values")
|
|
83
|
+
if active is None:
|
|
84
|
+
active = set()
|
|
85
|
+
identity = id(value)
|
|
86
|
+
if identity in active:
|
|
87
|
+
raise ValueError("replacement cannot contain cyclic references")
|
|
88
|
+
active.add(identity)
|
|
89
|
+
try:
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
if any(not isinstance(key, str) for key in value):
|
|
92
|
+
raise TypeError("replacement object keys must be strings")
|
|
93
|
+
return {key: _copy_replacement(item, active) for key, item in value.items()}
|
|
94
|
+
return [_copy_replacement(item, active) for item in value]
|
|
95
|
+
finally:
|
|
96
|
+
active.remove(identity)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class Intervention:
|
|
101
|
+
"""A typed replay change, validated against history by the runner.
|
|
102
|
+
|
|
103
|
+
``neutralize`` uses the runner's declared neutral context representation.
|
|
104
|
+
``replace`` uses its declared semantic replacement rules. Deleting an
|
|
105
|
+
arbitrary history item is not a supported intervention. A null replacement
|
|
106
|
+
is allowed for ``replace`` when the runner's schema defines its meaning.
|
|
107
|
+
Replacement payloads must be ordinary JSON values and are copied on
|
|
108
|
+
construction. Each replay also receives a fresh copy, so a runner cannot
|
|
109
|
+
mutate the payload used by a later sample.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
event_id: str
|
|
113
|
+
kind: str = "neutralize"
|
|
114
|
+
replacement: object = None
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
_nonempty_string(self.event_id, "event_id")
|
|
118
|
+
if not isinstance(self.kind, str) or self.kind not in _INTERVENTION_KINDS:
|
|
119
|
+
raise ValueError("kind must be 'neutralize' or 'replace'")
|
|
120
|
+
if self.kind == "neutralize" and self.replacement is not None:
|
|
121
|
+
raise ValueError("neutralize does not accept a replacement")
|
|
122
|
+
object.__setattr__(self, "replacement", _copy_replacement(self.replacement))
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
|
125
|
+
from .models import to_dict
|
|
126
|
+
|
|
127
|
+
return to_dict(self)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class ReplayOutcome:
|
|
132
|
+
"""A policy decision and an independently verified world-state outcome.
|
|
133
|
+
|
|
134
|
+
``None`` means the consequence remains unknown. A runner must not report
|
|
135
|
+
a tool's response status or an agent's self-assessment as verification.
|
|
136
|
+
The optional action-linked target requires evidence tying a qualifying
|
|
137
|
+
consequence to the registered action. Ordinary state satisfaction alone
|
|
138
|
+
does not establish that link. Older runners leave this target unknown.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
action_selected: bool
|
|
142
|
+
consequence_satisfied: bool | None
|
|
143
|
+
action_linked_consequence_satisfied: bool | None = None
|
|
144
|
+
|
|
145
|
+
def __post_init__(self) -> None:
|
|
146
|
+
if not isinstance(self.action_selected, bool):
|
|
147
|
+
raise ValueError("action_selected must be a boolean")
|
|
148
|
+
if self.consequence_satisfied is not None and not isinstance(
|
|
149
|
+
self.consequence_satisfied, bool
|
|
150
|
+
):
|
|
151
|
+
raise ValueError("consequence_satisfied must be a boolean or None")
|
|
152
|
+
if self.action_linked_consequence_satisfied is not None and not isinstance(
|
|
153
|
+
self.action_linked_consequence_satisfied, bool
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("action_linked_consequence_satisfied must be a boolean or None")
|
|
156
|
+
|
|
157
|
+
def target_value(self, target: str) -> bool | None:
|
|
158
|
+
"""Read one declared replay target without deriving unknown outcomes."""
|
|
159
|
+
if not isinstance(target, str) or target not in _TARGETS:
|
|
160
|
+
raise ValueError(f"target must be one of {REPLAY_TARGETS!r}")
|
|
161
|
+
return getattr(self, _TARGET_ATTRIBUTES[target])
|
|
162
|
+
|
|
163
|
+
def to_dict(self) -> dict[str, Any]:
|
|
164
|
+
from .models import to_dict
|
|
165
|
+
|
|
166
|
+
record = to_dict(self)
|
|
167
|
+
# Preserve the original two-field export for old runners. The optional
|
|
168
|
+
# target is absent when unsupported or unknown, never encoded as false.
|
|
169
|
+
if self.action_linked_consequence_satisfied is None:
|
|
170
|
+
record.pop("action_linked_consequence_satisfied")
|
|
171
|
+
return record
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@runtime_checkable
|
|
175
|
+
class ReplayRunner(Protocol):
|
|
176
|
+
"""A resettable experiment, with shared randomness keyed by seed.
|
|
177
|
+
|
|
178
|
+
Every call starts from the same initial conditions and an independent world.
|
|
179
|
+
A call with ``None`` runs the factual context. Intervention validation and
|
|
180
|
+
verification of the consequence belong to the runner. It must preserve a
|
|
181
|
+
stable mapping from seed to exogenous randomness in both paired worlds.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def run(self, intervention: Intervention | None, seed: int) -> ReplayOutcome: ...
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class EffectEstimate:
|
|
189
|
+
"""An unnormalized rate difference from complete paired replay outcomes."""
|
|
190
|
+
|
|
191
|
+
event_id: str
|
|
192
|
+
target: str
|
|
193
|
+
baseline_rate: float
|
|
194
|
+
intervention_rate: float
|
|
195
|
+
effect: float
|
|
196
|
+
confidence_interval: tuple[float, float]
|
|
197
|
+
samples: int
|
|
198
|
+
seeds: tuple[int, ...]
|
|
199
|
+
unknown_pairs: int = 0
|
|
200
|
+
|
|
201
|
+
def __post_init__(self) -> None:
|
|
202
|
+
_nonempty_string(self.event_id, "event_id")
|
|
203
|
+
if not isinstance(self.target, str) or self.target not in _TARGETS:
|
|
204
|
+
raise ValueError(f"target must be one of {REPLAY_TARGETS!r}")
|
|
205
|
+
baseline = _finite_number(self.baseline_rate, "baseline_rate", 0.0, 1.0)
|
|
206
|
+
changed = _finite_number(self.intervention_rate, "intervention_rate", 0.0, 1.0)
|
|
207
|
+
effect = _finite_number(self.effect, "effect", -1.0, 1.0)
|
|
208
|
+
if not isclose(effect, baseline - changed, rel_tol=0.0, abs_tol=1e-12):
|
|
209
|
+
raise ValueError("effect must equal baseline_rate - intervention_rate")
|
|
210
|
+
if not isinstance(self.confidence_interval, tuple) or len(self.confidence_interval) != 2:
|
|
211
|
+
raise ValueError("confidence_interval must be a two-element tuple")
|
|
212
|
+
lower = _finite_number(self.confidence_interval[0], "interval lower bound", -1.0, 1.0)
|
|
213
|
+
upper = _finite_number(self.confidence_interval[1], "interval upper bound", -1.0, 1.0)
|
|
214
|
+
if lower > upper or not lower <= effect <= upper:
|
|
215
|
+
raise ValueError("confidence_interval must be ordered and contain effect")
|
|
216
|
+
_integer(self.samples, "samples", minimum=2)
|
|
217
|
+
if not isinstance(self.seeds, tuple) or len(self.seeds) != self.samples:
|
|
218
|
+
raise ValueError("seeds must be a tuple with one seed per sample")
|
|
219
|
+
for seed in self.seeds:
|
|
220
|
+
_integer(seed, "seed")
|
|
221
|
+
if len(set(self.seeds)) != self.samples:
|
|
222
|
+
raise ValueError("sample seeds must be distinct")
|
|
223
|
+
_integer(self.unknown_pairs, "unknown_pairs", minimum=0)
|
|
224
|
+
if self.unknown_pairs:
|
|
225
|
+
raise ValueError("a full estimate cannot contain unknown pairs")
|
|
226
|
+
|
|
227
|
+
def to_dict(self) -> dict[str, Any]:
|
|
228
|
+
from .models import to_dict
|
|
229
|
+
|
|
230
|
+
return to_dict(self)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class InsufficientEvidenceError(ValueError):
|
|
234
|
+
"""A complete-pair estimate was refused because outcomes are missing."""
|
|
235
|
+
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
event_id: str,
|
|
240
|
+
target: str,
|
|
241
|
+
samples: int,
|
|
242
|
+
known_pairs: int,
|
|
243
|
+
unknown_pairs: int,
|
|
244
|
+
seeds: tuple[int, ...],
|
|
245
|
+
) -> None:
|
|
246
|
+
self.event_id = event_id
|
|
247
|
+
self.target = target
|
|
248
|
+
self.samples = samples
|
|
249
|
+
self.known_pairs = known_pairs
|
|
250
|
+
self.unknown_pairs = unknown_pairs
|
|
251
|
+
self.seeds = seeds
|
|
252
|
+
super().__init__(
|
|
253
|
+
f"Cannot estimate {target} effect for {event_id!r}: "
|
|
254
|
+
f"{unknown_pairs} of {samples} paired outcomes are unknown "
|
|
255
|
+
f"({known_pairs} known pairs). No estimate is produced; "
|
|
256
|
+
"unknown outcomes are neither failures nor discarded samples."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def to_dict(self) -> dict[str, Any]:
|
|
260
|
+
return {
|
|
261
|
+
"error": type(self).__name__,
|
|
262
|
+
"event_id": self.event_id,
|
|
263
|
+
"target": self.target,
|
|
264
|
+
"samples": self.samples,
|
|
265
|
+
"known_pairs": self.known_pairs,
|
|
266
|
+
"unknown_pairs": self.unknown_pairs,
|
|
267
|
+
"seeds": list(self.seeds),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _wilson_bounds(successes: int, samples: int) -> tuple[float, float]:
|
|
272
|
+
rate = successes / samples
|
|
273
|
+
z_squared = _WILSON_Z * _WILSON_Z
|
|
274
|
+
denominator = 1.0 + z_squared / samples
|
|
275
|
+
center = (rate + z_squared / (2.0 * samples)) / denominator
|
|
276
|
+
half_width = (
|
|
277
|
+
_WILSON_Z
|
|
278
|
+
* sqrt(rate * (1.0 - rate) / samples + z_squared / (4.0 * samples * samples))
|
|
279
|
+
/ denominator
|
|
280
|
+
)
|
|
281
|
+
# Floating-point cancellation can exclude an observed rate of exactly
|
|
282
|
+
# zero or one. The analytic Wilson endpoints are exact at these extremes.
|
|
283
|
+
lower = 0.0 if successes == 0 else max(0.0, center - half_width)
|
|
284
|
+
upper = 1.0 if successes == samples else min(1.0, center + half_width)
|
|
285
|
+
return lower, upper
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _checked_run(
|
|
289
|
+
runner: ReplayRunner, intervention: Intervention | None, seed: int
|
|
290
|
+
) -> ReplayOutcome:
|
|
291
|
+
replay_intervention = (
|
|
292
|
+
None
|
|
293
|
+
if intervention is None
|
|
294
|
+
else Intervention(intervention.event_id, intervention.kind, intervention.replacement)
|
|
295
|
+
)
|
|
296
|
+
outcome = runner.run(replay_intervention, seed)
|
|
297
|
+
if not isinstance(outcome, ReplayOutcome):
|
|
298
|
+
raise TypeError("ReplayRunner.run must return ReplayOutcome")
|
|
299
|
+
return outcome
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def leave_one_out(
|
|
303
|
+
runner: ReplayRunner,
|
|
304
|
+
interventions: Iterable[Intervention],
|
|
305
|
+
*,
|
|
306
|
+
samples: int = 20,
|
|
307
|
+
seed: int = 0,
|
|
308
|
+
target: str = "consequence",
|
|
309
|
+
) -> tuple[EffectEstimate, ...]:
|
|
310
|
+
"""Replay each single intervention against the same factual seed schedule.
|
|
311
|
+
|
|
312
|
+
The output preserves intervention order and contains no responsibility
|
|
313
|
+
normalization. For either consequence target, even one unknown pair raises
|
|
314
|
+
:class:`InsufficientEvidenceError`; no partial tuple is returned. For the
|
|
315
|
+
action target, an unknown consequence does not obscure a known decision.
|
|
316
|
+
No intervention or baseline replay is run when candidates are empty.
|
|
317
|
+
Candidate event IDs must be unique: an estimate identifies a single change
|
|
318
|
+
to one event. Each interval applies to its own estimate and does not adjust
|
|
319
|
+
for comparisons across multiple candidates.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
_integer(samples, "samples", minimum=2)
|
|
323
|
+
_integer(seed, "seed")
|
|
324
|
+
if not isinstance(target, str) or target not in _TARGETS:
|
|
325
|
+
raise ValueError(f"target must be one of {REPLAY_TARGETS!r}")
|
|
326
|
+
if not callable(getattr(runner, "run", None)):
|
|
327
|
+
raise TypeError("runner must implement run(intervention, seed)")
|
|
328
|
+
candidates = tuple(interventions)
|
|
329
|
+
if any(not isinstance(candidate, Intervention) for candidate in candidates):
|
|
330
|
+
raise TypeError("interventions must contain Intervention records")
|
|
331
|
+
if not candidates:
|
|
332
|
+
return ()
|
|
333
|
+
if len({candidate.event_id for candidate in candidates}) != len(candidates):
|
|
334
|
+
raise ValueError("interventions must have unique event_id values")
|
|
335
|
+
# Snapshot before running any world, then keep that snapshot private from
|
|
336
|
+
# the runner; mutable caller-owned payloads cannot change later samples.
|
|
337
|
+
candidates = tuple(
|
|
338
|
+
Intervention(candidate.event_id, candidate.kind, candidate.replacement)
|
|
339
|
+
for candidate in candidates
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
seeds = tuple(seed + offset for offset in range(samples))
|
|
343
|
+
originals = tuple(_checked_run(runner, None, replay_seed) for replay_seed in seeds)
|
|
344
|
+
estimates: list[EffectEstimate] = []
|
|
345
|
+
for candidate in candidates:
|
|
346
|
+
changed = tuple(_checked_run(runner, candidate, replay_seed) for replay_seed in seeds)
|
|
347
|
+
factual_values = tuple(outcome.target_value(target) for outcome in originals)
|
|
348
|
+
changed_values = tuple(outcome.target_value(target) for outcome in changed)
|
|
349
|
+
if target != "action":
|
|
350
|
+
unknown_pairs = sum(
|
|
351
|
+
factual is None or counterfactual is None
|
|
352
|
+
for factual, counterfactual in zip(factual_values, changed_values)
|
|
353
|
+
)
|
|
354
|
+
if unknown_pairs:
|
|
355
|
+
raise InsufficientEvidenceError(
|
|
356
|
+
event_id=candidate.event_id,
|
|
357
|
+
target=target,
|
|
358
|
+
samples=samples,
|
|
359
|
+
known_pairs=samples - unknown_pairs,
|
|
360
|
+
unknown_pairs=unknown_pairs,
|
|
361
|
+
seeds=seeds,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
baseline_count = sum(value is True for value in factual_values)
|
|
365
|
+
intervention_count = sum(value is True for value in changed_values)
|
|
366
|
+
baseline_rate = baseline_count / samples
|
|
367
|
+
intervention_rate = intervention_count / samples
|
|
368
|
+
factual_bounds = _wilson_bounds(baseline_count, samples)
|
|
369
|
+
changed_bounds = _wilson_bounds(intervention_count, samples)
|
|
370
|
+
interval = (
|
|
371
|
+
max(-1.0, factual_bounds[0] - changed_bounds[1]),
|
|
372
|
+
min(1.0, factual_bounds[1] - changed_bounds[0]),
|
|
373
|
+
)
|
|
374
|
+
estimates.append(
|
|
375
|
+
EffectEstimate(
|
|
376
|
+
event_id=candidate.event_id,
|
|
377
|
+
target=target,
|
|
378
|
+
baseline_rate=baseline_rate,
|
|
379
|
+
intervention_rate=intervention_rate,
|
|
380
|
+
effect=baseline_rate - intervention_rate,
|
|
381
|
+
confidence_interval=interval,
|
|
382
|
+
samples=samples,
|
|
383
|
+
seeds=seeds,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
return tuple(estimates)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def recency_attribution(events: Iterable[Any], *, before_ms: int) -> str | None:
|
|
390
|
+
"""Return the last pre-decision context ID as a noncausal heuristic.
|
|
391
|
+
|
|
392
|
+
Eligible types are user input, system instruction, retrieval, and memory
|
|
393
|
+
read. Events at or after ``before_ms`` are excluded. Ties retain the later
|
|
394
|
+
event in input order. This function never assigns a score or builds a causal
|
|
395
|
+
graph link. The caller supplies the decision boundary explicitly.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
_integer(before_ms, "before_ms", minimum=0)
|
|
399
|
+
latest_id: str | None = None
|
|
400
|
+
latest_time = -1
|
|
401
|
+
for event in events:
|
|
402
|
+
event_time = _integer(event.time_ms, "event.time_ms", minimum=0)
|
|
403
|
+
event_type = getattr(event.type, "value", event.type)
|
|
404
|
+
if event_type in _CONTEXT_TYPES and event_time < before_ms and event_time >= latest_time:
|
|
405
|
+
latest_id = _nonempty_string(event.id, "event.id")
|
|
406
|
+
latest_time = event_time
|
|
407
|
+
return latest_id
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Controlled fixtures for Consequa-X research experiments."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Final reported-assessment coverage and hindsight alignment for completed runs."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
from ..models import AssessmentStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _rate(numerator: int, denominator: int) -> dict:
|
|
9
|
+
return {
|
|
10
|
+
"numerator": numerator,
|
|
11
|
+
"denominator": denominator,
|
|
12
|
+
"value": numerator / denominator if denominator else None,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assessment_metrics(records: Iterable[tuple[AssessmentStatus, str]]) -> dict:
|
|
17
|
+
"""Summarize completed final assessments with explicit rate denominators.
|
|
18
|
+
|
|
19
|
+
Alignment must be the existing origin-aware explanation result, rather than
|
|
20
|
+
a comparison with state satisfaction alone. These are hindsight alignment
|
|
21
|
+
metrics, not oracle-policy correctness or policy-visible evidence support.
|
|
22
|
+
Counts describe the structured status field, not claims in rationale text.
|
|
23
|
+
Unknown assessments are abstentions, never consistent claims. Partial
|
|
24
|
+
assessments remain separate from definite success/failure claims. Callers
|
|
25
|
+
omit failed and unstarted runs, which have no completed final assessment.
|
|
26
|
+
"""
|
|
27
|
+
counts = {status.value: 0 for status in AssessmentStatus}
|
|
28
|
+
aligned = {label: 0 for label in ("consistent", "contradicted", "unassessable")}
|
|
29
|
+
completed = 0
|
|
30
|
+
for status, alignment in records:
|
|
31
|
+
if not isinstance(status, AssessmentStatus):
|
|
32
|
+
raise ValueError("Reported assessment must be an AssessmentStatus")
|
|
33
|
+
if alignment not in aligned:
|
|
34
|
+
raise ValueError("Assessment alignment must come from an explanation")
|
|
35
|
+
completed += 1
|
|
36
|
+
counts[status.value] += 1
|
|
37
|
+
if status in {AssessmentStatus.SUCCESS, AssessmentStatus.FAILURE}:
|
|
38
|
+
aligned[alignment] += 1
|
|
39
|
+
claims = counts["success"] + counts["failure"]
|
|
40
|
+
assessable = aligned["consistent"] + aligned["contradicted"]
|
|
41
|
+
return {
|
|
42
|
+
"completed_assessments": completed,
|
|
43
|
+
"reported_assessment": counts,
|
|
44
|
+
"definite_claims": claims,
|
|
45
|
+
"abstentions": counts["unknown"],
|
|
46
|
+
"partial_assessments": counts["partial"],
|
|
47
|
+
"assessable_claims": assessable,
|
|
48
|
+
"unassessable_claims": aligned["unassessable"],
|
|
49
|
+
"consistent_claims": aligned["consistent"],
|
|
50
|
+
"contradicted_claims": aligned["contradicted"],
|
|
51
|
+
"claim_coverage": _rate(claims, completed),
|
|
52
|
+
"assessable_claim_coverage": _rate(assessable, completed),
|
|
53
|
+
"contradiction_rate": _rate(aligned["contradicted"], assessable),
|
|
54
|
+
}
|