postrule 1.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.
- postrule/__init__.py +212 -0
- postrule/_examples/01_hello_world.py +59 -0
- postrule/_examples/17_exception_handling.py +180 -0
- postrule/_examples/19_autoresearch_loop.py +269 -0
- postrule/_examples/20_verifier_default.py +91 -0
- postrule/_examples/21_tournament.py +163 -0
- postrule/_examples/_stubs.py +194 -0
- postrule/_packing.py +280 -0
- postrule/analyzer.py +1603 -0
- postrule/auth.py +147 -0
- postrule/autoresearch.py +646 -0
- postrule/benchmarks/__init__.py +54 -0
- postrule/benchmarks/harness.py +745 -0
- postrule/benchmarks/loaders.py +375 -0
- postrule/benchmarks/rules.py +155 -0
- postrule/bundled.py +344 -0
- postrule/cli.py +2223 -0
- postrule/cloud/__init__.py +42 -0
- postrule/cloud/registry.py +97 -0
- postrule/cloud/report/__init__.py +69 -0
- postrule/cloud/report/aggregator.py +353 -0
- postrule/cloud/report/charts.py +348 -0
- postrule/cloud/report/discovery.py +414 -0
- postrule/cloud/report/hypotheses.py +310 -0
- postrule/cloud/report/render_markdown.py +394 -0
- postrule/cloud/report/summary.py +367 -0
- postrule/cloud/sync.py +81 -0
- postrule/cloud/team_corpus.py +94 -0
- postrule/cloud/verdict_telemetry.py +464 -0
- postrule/core.py +2681 -0
- postrule/decorator.py +273 -0
- postrule/gates.py +580 -0
- postrule/image_rules.py +65 -0
- postrule/insights/__init__.py +103 -0
- postrule/insights/_paths.py +44 -0
- postrule/insights/disclosure.py +67 -0
- postrule/insights/enrollment.py +132 -0
- postrule/insights/events.py +340 -0
- postrule/insights/fingerprint.py +136 -0
- postrule/insights/tuned_defaults.py +333 -0
- postrule/license.py +199 -0
- postrule/lifters/__init__.py +108 -0
- postrule/lifters/branch.py +1575 -0
- postrule/lifters/evidence.py +1338 -0
- postrule/mcp_server.py +509 -0
- postrule/ml.py +583 -0
- postrule/ml_strategy.py +137 -0
- postrule/models.py +574 -0
- postrule/py.typed +0 -0
- postrule/refresh.py +267 -0
- postrule/research.py +425 -0
- postrule/roi.py +285 -0
- postrule/storage.py +1516 -0
- postrule/switch_class.py +480 -0
- postrule/telemetry.py +167 -0
- postrule/verdicts.py +758 -0
- postrule/viz.py +259 -0
- postrule/wrap.py +233 -0
- postrule-1.1.0.dist-info/METADATA +675 -0
- postrule-1.1.0.dist-info/RECORD +67 -0
- postrule-1.1.0.dist-info/WHEEL +4 -0
- postrule-1.1.0.dist-info/entry_points.txt +2 -0
- postrule-1.1.0.dist-info/licenses/LICENSE-APACHE +190 -0
- postrule-1.1.0.dist-info/licenses/LICENSE-BSL +139 -0
- postrule-1.1.0.dist-info/licenses/LICENSE.md +52 -0
- postrule-1.1.0.dist-info/licenses/LICENSING.md +84 -0
- postrule-1.1.0.dist-info/licenses/NOTICE +27 -0
postrule/__init__.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Copyright (c) 2026 B-Tree Labs
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Postrule — graduated-autonomy classification primitive.
|
|
5
|
+
|
|
6
|
+
Six-phase lifecycle from hand-written rule to learned classifier,
|
|
7
|
+
with a statistical transition gate at every phase, a safety-critical
|
|
8
|
+
cap that refuses construction in the highest-autonomy phase for
|
|
9
|
+
authorization-class decisions, a circuit breaker that reverts to the
|
|
10
|
+
rule on ML failure, and shadow-path isolation that keeps observational
|
|
11
|
+
classifiers from affecting user-visible output.
|
|
12
|
+
|
|
13
|
+
Core public API: :class:`LearnedSwitch`, :func:`ml_switch` decorator,
|
|
14
|
+
:class:`Phase`, :class:`SwitchConfig`, :class:`ClassificationRecord`,
|
|
15
|
+
:class:`FileStorage`, and the language model/ML protocol interfaces.
|
|
16
|
+
|
|
17
|
+
Tooling (analyzer, ROI reporter, AST-based `wrap_function`, viz,
|
|
18
|
+
research runners) ships in submodules — import directly:
|
|
19
|
+
``from postrule.analyzer import analyze``, ``from postrule.roi import
|
|
20
|
+
compute_switch_roi``, etc.
|
|
21
|
+
|
|
22
|
+
See README.md and https://postrule.ai.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "1.1.0"
|
|
26
|
+
|
|
27
|
+
from postrule.autoresearch import (
|
|
28
|
+
CandidateHarness,
|
|
29
|
+
CandidateReport,
|
|
30
|
+
Tournament,
|
|
31
|
+
TournamentReport,
|
|
32
|
+
)
|
|
33
|
+
from postrule.core import (
|
|
34
|
+
BulkVerdict,
|
|
35
|
+
BulkVerdictSummary,
|
|
36
|
+
ClassificationRecord,
|
|
37
|
+
ClassificationResult,
|
|
38
|
+
Label,
|
|
39
|
+
LearnedSwitch,
|
|
40
|
+
Phase,
|
|
41
|
+
SwitchConfig,
|
|
42
|
+
SwitchStatus,
|
|
43
|
+
Verdict,
|
|
44
|
+
)
|
|
45
|
+
from postrule.decorator import ml_switch
|
|
46
|
+
from postrule.gates import (
|
|
47
|
+
AccuracyMarginGate,
|
|
48
|
+
CompositeGate,
|
|
49
|
+
Gate,
|
|
50
|
+
GateDecision,
|
|
51
|
+
ManualGate,
|
|
52
|
+
McNemarGate,
|
|
53
|
+
MinVolumeGate,
|
|
54
|
+
next_phase,
|
|
55
|
+
prev_phase,
|
|
56
|
+
)
|
|
57
|
+
from postrule.ml import (
|
|
58
|
+
ImagePixelLogRegHead,
|
|
59
|
+
MLHead,
|
|
60
|
+
MLHeadFactory,
|
|
61
|
+
MLPrediction,
|
|
62
|
+
SklearnTextHead,
|
|
63
|
+
TfidfGradientBoostingHead,
|
|
64
|
+
TfidfHeadBase,
|
|
65
|
+
TfidfLinearSVCHead,
|
|
66
|
+
TfidfMultinomialNBHead,
|
|
67
|
+
available_ml_heads,
|
|
68
|
+
make_ml_head,
|
|
69
|
+
register_ml_head,
|
|
70
|
+
)
|
|
71
|
+
from postrule.ml_strategy import (
|
|
72
|
+
CardinalityMLHeadStrategy,
|
|
73
|
+
FixedMLHeadStrategy,
|
|
74
|
+
MLHeadStrategy,
|
|
75
|
+
)
|
|
76
|
+
from postrule.models import (
|
|
77
|
+
AnthropicAdapter,
|
|
78
|
+
AnthropicAsyncAdapter,
|
|
79
|
+
LlamafileAdapter,
|
|
80
|
+
LlamafileAsyncAdapter,
|
|
81
|
+
ModelClassifier,
|
|
82
|
+
ModelPrediction,
|
|
83
|
+
OllamaAdapter,
|
|
84
|
+
OllamaAsyncAdapter,
|
|
85
|
+
OpenAIAdapter,
|
|
86
|
+
OpenAIAsyncAdapter,
|
|
87
|
+
)
|
|
88
|
+
from postrule.storage import (
|
|
89
|
+
BoundedInMemoryStorage,
|
|
90
|
+
FileStorage,
|
|
91
|
+
InMemoryStorage,
|
|
92
|
+
ResilientStorage,
|
|
93
|
+
SqliteStorage,
|
|
94
|
+
Storage,
|
|
95
|
+
StorageBase,
|
|
96
|
+
deserialize_record,
|
|
97
|
+
flock_supported,
|
|
98
|
+
serialize_record,
|
|
99
|
+
)
|
|
100
|
+
from postrule.switch_class import Switch
|
|
101
|
+
from postrule.telemetry import (
|
|
102
|
+
ListEmitter,
|
|
103
|
+
NullEmitter,
|
|
104
|
+
StdoutEmitter,
|
|
105
|
+
TelemetryEmitter,
|
|
106
|
+
)
|
|
107
|
+
from postrule.verdicts import (
|
|
108
|
+
CallableVerdictSource,
|
|
109
|
+
HumanReviewerSource,
|
|
110
|
+
JudgeCommittee,
|
|
111
|
+
JudgeSource,
|
|
112
|
+
NoVerifierAvailableError,
|
|
113
|
+
VerdictSource,
|
|
114
|
+
WebhookVerdictSource,
|
|
115
|
+
default_verifier,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Default-on hosted-API verdict telemetry. Best-effort, fails silent.
|
|
119
|
+
# Auto-installs the cloud emitter as the process-wide default iff the
|
|
120
|
+
# user is signed in (``~/.postrule/credentials`` exists) AND has not
|
|
121
|
+
# opted out via ``$POSTRULE_NO_TELEMETRY``. The decision happens in
|
|
122
|
+
# ``maybe_install`` itself; this block just triggers the check.
|
|
123
|
+
#
|
|
124
|
+
# The import + call is wrapped so a missing optional dependency, a
|
|
125
|
+
# broken credentials file, or a malformed env override never aborts
|
|
126
|
+
# ``import postrule``. The decision path is observability-unaware: if
|
|
127
|
+
# anything here misfires, the user gets a NullEmitter and life goes
|
|
128
|
+
# on. Out of scope for this block (handled by other agents): the
|
|
129
|
+
# sign-up flow's consent banner that announces the default-on
|
|
130
|
+
# decision to the user.
|
|
131
|
+
try: # pragma: no cover — observability hook; intentionally fails silent
|
|
132
|
+
from postrule.cloud import verdict_telemetry as _verdict_telemetry # pragma: no cover
|
|
133
|
+
|
|
134
|
+
_verdict_telemetry.maybe_install() # pragma: no cover
|
|
135
|
+
except BaseException: # noqa: BLE001 — observability hook, fails silent # pragma: no cover
|
|
136
|
+
pass # pragma: no cover
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"AccuracyMarginGate",
|
|
140
|
+
"AnthropicAdapter",
|
|
141
|
+
"AnthropicAsyncAdapter",
|
|
142
|
+
"BoundedInMemoryStorage",
|
|
143
|
+
"BulkVerdict",
|
|
144
|
+
"BulkVerdictSummary",
|
|
145
|
+
"CandidateHarness",
|
|
146
|
+
"CandidateReport",
|
|
147
|
+
"CardinalityMLHeadStrategy",
|
|
148
|
+
"CompositeGate",
|
|
149
|
+
"ClassificationRecord",
|
|
150
|
+
"ClassificationResult",
|
|
151
|
+
"FileStorage",
|
|
152
|
+
"FixedMLHeadStrategy",
|
|
153
|
+
"Gate",
|
|
154
|
+
"GateDecision",
|
|
155
|
+
"ImagePixelLogRegHead",
|
|
156
|
+
"InMemoryStorage",
|
|
157
|
+
"Label",
|
|
158
|
+
"LearnedSwitch",
|
|
159
|
+
"ListEmitter",
|
|
160
|
+
"LlamafileAdapter",
|
|
161
|
+
"LlamafileAsyncAdapter",
|
|
162
|
+
"MLHead",
|
|
163
|
+
"MLHeadFactory",
|
|
164
|
+
"MLHeadStrategy",
|
|
165
|
+
"MLPrediction",
|
|
166
|
+
"ManualGate",
|
|
167
|
+
"McNemarGate",
|
|
168
|
+
"MinVolumeGate",
|
|
169
|
+
"ModelClassifier",
|
|
170
|
+
"ModelPrediction",
|
|
171
|
+
"NullEmitter",
|
|
172
|
+
"OllamaAdapter",
|
|
173
|
+
"OllamaAsyncAdapter",
|
|
174
|
+
"OpenAIAdapter",
|
|
175
|
+
"OpenAIAsyncAdapter",
|
|
176
|
+
"Phase",
|
|
177
|
+
"ResilientStorage",
|
|
178
|
+
"SklearnTextHead",
|
|
179
|
+
"SqliteStorage",
|
|
180
|
+
"TfidfGradientBoostingHead",
|
|
181
|
+
"TfidfHeadBase",
|
|
182
|
+
"TfidfLinearSVCHead",
|
|
183
|
+
"TfidfMultinomialNBHead",
|
|
184
|
+
"StdoutEmitter",
|
|
185
|
+
"Storage",
|
|
186
|
+
"StorageBase",
|
|
187
|
+
"Switch",
|
|
188
|
+
"SwitchConfig",
|
|
189
|
+
"SwitchStatus",
|
|
190
|
+
"CallableVerdictSource",
|
|
191
|
+
"HumanReviewerSource",
|
|
192
|
+
"JudgeCommittee",
|
|
193
|
+
"JudgeSource",
|
|
194
|
+
"TelemetryEmitter",
|
|
195
|
+
"Tournament",
|
|
196
|
+
"TournamentReport",
|
|
197
|
+
"Verdict",
|
|
198
|
+
"VerdictSource",
|
|
199
|
+
"NoVerifierAvailableError",
|
|
200
|
+
"WebhookVerdictSource",
|
|
201
|
+
"__version__",
|
|
202
|
+
"available_ml_heads",
|
|
203
|
+
"default_verifier",
|
|
204
|
+
"deserialize_record",
|
|
205
|
+
"flock_supported",
|
|
206
|
+
"make_ml_head",
|
|
207
|
+
"ml_switch",
|
|
208
|
+
"next_phase",
|
|
209
|
+
"prev_phase",
|
|
210
|
+
"register_ml_head",
|
|
211
|
+
"serialize_record",
|
|
212
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright (c) 2026 B-Tree Labs
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Postrule hello-world — rule + dispatch in the smallest form.
|
|
4
|
+
|
|
5
|
+
Run: `python examples/01_hello_world.py`
|
|
6
|
+
|
|
7
|
+
Pass ``labels=`` as a dict mapping label → action, call
|
|
8
|
+
``rule.dispatch(input)``, done. Each dict entry is a
|
|
9
|
+
**label-based conditional expression** — "when the classifier's
|
|
10
|
+
output equals this label, evaluate this action."
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from postrule import ml_switch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def send_to_engineering(ticket: dict) -> str:
|
|
19
|
+
"""Action fired when the classifier returns ``label="bug"``."""
|
|
20
|
+
return f"engineering ← {ticket['title']}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def send_to_product(ticket: dict) -> str:
|
|
24
|
+
"""Action fired when the classifier returns ``label="feature_request"``."""
|
|
25
|
+
return f"product ← {ticket['title']}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def send_to_support(ticket: dict) -> str:
|
|
29
|
+
"""Action fired when the classifier returns ``label="question"``."""
|
|
30
|
+
return f"support ← {ticket['title']}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@ml_switch(
|
|
34
|
+
labels={
|
|
35
|
+
"bug": send_to_engineering,
|
|
36
|
+
"feature_request": send_to_product,
|
|
37
|
+
"question": send_to_support,
|
|
38
|
+
},
|
|
39
|
+
# `author` omitted — auto-derives to "@__main__:triage_rule".
|
|
40
|
+
)
|
|
41
|
+
def triage_rule(ticket: dict) -> str:
|
|
42
|
+
"""Classify one ticket into exactly one of the declared labels."""
|
|
43
|
+
title = (ticket.get("title") or "").lower()
|
|
44
|
+
if "crash" in title or "error" in title:
|
|
45
|
+
return "bug"
|
|
46
|
+
if title.endswith("?"):
|
|
47
|
+
return "question"
|
|
48
|
+
return "feature_request"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
samples = [
|
|
53
|
+
{"title": "app crashes on login"},
|
|
54
|
+
{"title": "how do I reset my password?"},
|
|
55
|
+
{"title": "add dark mode"},
|
|
56
|
+
]
|
|
57
|
+
for sample in samples:
|
|
58
|
+
c = triage_rule.dispatch(sample)
|
|
59
|
+
print(f"{sample['title']:40s} → label={c.label:18s} → action={c.action_result}")
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Copyright (c) 2026 B-Tree Labs
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Using a Postrule switch as an exception-handling dispatcher.
|
|
4
|
+
|
|
5
|
+
Run: `python examples/17_exception_handling.py`
|
|
6
|
+
|
|
7
|
+
A try/except tree that picks among retry, fallback, escalate,
|
|
8
|
+
and drop is a classifier — input is ``(exception, context)``,
|
|
9
|
+
output is one of a fixed strategy set. Wrapping the dispatch in
|
|
10
|
+
a :class:`LearnedSwitch` records every decision on the outcome
|
|
11
|
+
log and lets a learned policy graduate against the hand-written
|
|
12
|
+
rule once enough outcomes accumulate.
|
|
13
|
+
|
|
14
|
+
Day 0 (RULE)
|
|
15
|
+
Hand-written dispatch on exception type + HTTP status.
|
|
16
|
+
Conservative defaults — always retry on 5xx, never retry on
|
|
17
|
+
4xx, escalate ``RuntimeError`` to the queue.
|
|
18
|
+
|
|
19
|
+
Day N (MODEL_SHADOW → ML_PRIMARY)
|
|
20
|
+
As outcomes accumulate (did the retry succeed? did the
|
|
21
|
+
fallback produce an acceptable answer?), the ML head learns
|
|
22
|
+
endpoint-specific patterns the rule can't see:
|
|
23
|
+
"endpoint X's 503s clear in 2 s; endpoint Y's are permanent"
|
|
24
|
+
or "this auth error on tenant Z is actually a billing
|
|
25
|
+
suspension, route to CS." The rule remains the floor (paper
|
|
26
|
+
§7.1) — a buggy learned policy cannot remove the
|
|
27
|
+
"503 → retry" baseline.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import random
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
|
|
35
|
+
from postrule import LearnedSwitch, Verdict
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class FailureContext:
|
|
40
|
+
"""Everything a classifier sees about a failure."""
|
|
41
|
+
|
|
42
|
+
exception_type: str
|
|
43
|
+
http_status: int | None = None
|
|
44
|
+
endpoint: str = ""
|
|
45
|
+
attempt: int = 1
|
|
46
|
+
elapsed_ms: float = 0.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# The four strategies the classifier chooses between. Each is a
|
|
50
|
+
# distinct post-failure action. "retry" means loop + backoff;
|
|
51
|
+
# "fallback" means route to cache/default; "escalate" means push
|
|
52
|
+
# onto the ops queue; "drop" means log and continue (non-critical).
|
|
53
|
+
STRATEGIES = ["retry", "fallback", "escalate", "drop"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handling_rule(ctx: FailureContext) -> str:
|
|
57
|
+
"""Day-0 exception-handling dispatch.
|
|
58
|
+
|
|
59
|
+
Keyword-matched on exception type + HTTP status. Conservative
|
|
60
|
+
by design — anything ambiguous escalates.
|
|
61
|
+
"""
|
|
62
|
+
# Transient server-side — retry bounded.
|
|
63
|
+
if ctx.http_status in (502, 503, 504) and ctx.attempt < 3:
|
|
64
|
+
return "retry"
|
|
65
|
+
# Client-side auth / permission — not retryable; needs a human.
|
|
66
|
+
if ctx.http_status in (401, 403):
|
|
67
|
+
return "escalate"
|
|
68
|
+
# Validation errors on submitted data — the caller's problem.
|
|
69
|
+
if ctx.exception_type == "ValueError":
|
|
70
|
+
return "drop"
|
|
71
|
+
# Anything timing-out — fall back to a cached answer rather than
|
|
72
|
+
# burning more budget.
|
|
73
|
+
if ctx.exception_type == "TimeoutError":
|
|
74
|
+
return "fallback"
|
|
75
|
+
# Runtime / programmer errors — don't retry; a human should look.
|
|
76
|
+
if ctx.exception_type in ("RuntimeError", "KeyError", "AttributeError"):
|
|
77
|
+
return "escalate"
|
|
78
|
+
# Default: one-shot retry then give up.
|
|
79
|
+
return "retry" if ctx.attempt < 2 else "escalate"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
# Dispatch table: each strategy has a real action. In production
|
|
84
|
+
# these would call your retry loop, cache lookup, pager queue, etc.
|
|
85
|
+
def do_retry(ctx: FailureContext) -> str:
|
|
86
|
+
return f"requeued {ctx.endpoint} (attempt {ctx.attempt + 1})"
|
|
87
|
+
|
|
88
|
+
def do_fallback(ctx: FailureContext) -> str:
|
|
89
|
+
return f"served cached response for {ctx.endpoint}"
|
|
90
|
+
|
|
91
|
+
def do_escalate(ctx: FailureContext) -> str:
|
|
92
|
+
return f"pushed {ctx.endpoint} + {ctx.exception_type} to ops queue"
|
|
93
|
+
|
|
94
|
+
def do_drop(ctx: FailureContext) -> str:
|
|
95
|
+
return f"logged {ctx.exception_type} and continued"
|
|
96
|
+
|
|
97
|
+
sw = LearnedSwitch(
|
|
98
|
+
rule=handling_rule,
|
|
99
|
+
labels={
|
|
100
|
+
"retry": do_retry,
|
|
101
|
+
"fallback": do_fallback,
|
|
102
|
+
"escalate": do_escalate,
|
|
103
|
+
"drop": do_drop,
|
|
104
|
+
},
|
|
105
|
+
auto_advance=False, # deterministic example output
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Simulated failure stream. In production this feeds off your
|
|
109
|
+
# real exception traffic — a decorator on your HTTP client, a
|
|
110
|
+
# middleware on your message handler, an ``except`` block that
|
|
111
|
+
# calls ``sw.dispatch(ctx)`` instead of inlining its own ladder.
|
|
112
|
+
failures = [
|
|
113
|
+
FailureContext("HTTPError", http_status=503, endpoint="/api/v1/users", attempt=1),
|
|
114
|
+
FailureContext("HTTPError", http_status=401, endpoint="/api/v1/billing", attempt=1),
|
|
115
|
+
FailureContext("TimeoutError", endpoint="/api/v1/search", attempt=1, elapsed_ms=5000),
|
|
116
|
+
FailureContext("ValueError", endpoint="/api/v1/parse", attempt=1),
|
|
117
|
+
FailureContext("RuntimeError", endpoint="/api/v1/internal", attempt=1),
|
|
118
|
+
FailureContext("HTTPError", http_status=504, endpoint="/api/v1/export", attempt=3),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
print("Day 0: rule-based exception handling")
|
|
122
|
+
print("-" * 78)
|
|
123
|
+
for ctx in failures:
|
|
124
|
+
result = sw.dispatch(ctx)
|
|
125
|
+
print(
|
|
126
|
+
f" {ctx.exception_type:14s} "
|
|
127
|
+
f"status={str(ctx.http_status or '-'):>4s} "
|
|
128
|
+
f"attempt={ctx.attempt} "
|
|
129
|
+
f"-> {result.label:9s} {result.action_result}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# As outcomes accumulate, reviewers label whether each strategy
|
|
133
|
+
# actually worked. Simulated here; in production, a downstream
|
|
134
|
+
# signal (did the retry succeed? did the escalated ticket get
|
|
135
|
+
# resolved as a real bug?) feeds record_verdict.
|
|
136
|
+
print("\nLabeling outcomes (simulated downstream signal)...")
|
|
137
|
+
records = sw.storage.load_records(sw.name)
|
|
138
|
+
for r in records:
|
|
139
|
+
# Toy oracle: retries on 5xx worked; escalations on 4xx worked;
|
|
140
|
+
# fallbacks on timeouts worked; drops on ValueError worked.
|
|
141
|
+
# Everything else is "incorrect" — the rule overreached.
|
|
142
|
+
ctx = r.input
|
|
143
|
+
rule_pick = r.label
|
|
144
|
+
ok = (
|
|
145
|
+
(rule_pick == "retry" and ctx.http_status in (502, 503, 504))
|
|
146
|
+
or (rule_pick == "escalate" and ctx.http_status in (401, 403))
|
|
147
|
+
or (rule_pick == "fallback" and ctx.exception_type == "TimeoutError")
|
|
148
|
+
or (rule_pick == "drop" and ctx.exception_type == "ValueError")
|
|
149
|
+
or (
|
|
150
|
+
rule_pick == "escalate"
|
|
151
|
+
and ctx.exception_type in ("RuntimeError", "KeyError", "AttributeError")
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
sw.record_verdict(
|
|
155
|
+
input=ctx,
|
|
156
|
+
label=rule_pick,
|
|
157
|
+
outcome=Verdict.CORRECT.value if ok else Verdict.INCORRECT.value,
|
|
158
|
+
source="downstream-signal",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
status = sw.status()
|
|
162
|
+
print(
|
|
163
|
+
f"outcome log: total={status.outcomes_total}, "
|
|
164
|
+
f"correct={status.outcomes_correct}, "
|
|
165
|
+
f"incorrect={status.outcomes_incorrect}"
|
|
166
|
+
)
|
|
167
|
+
print(
|
|
168
|
+
"\nNext step (not shown here): as the log grows, an ML head "
|
|
169
|
+
"trained on (ctx → correct_strategy) pairs graduates into\n"
|
|
170
|
+
"MODEL_SHADOW / MODEL_PRIMARY. The rule stays as the safety "
|
|
171
|
+
"floor; the learned policy fires only when the evidence gate\n"
|
|
172
|
+
"confirms it beats the rule head-to-head with p < 0.05. See "
|
|
173
|
+
"examples/06_ml_primary.py for the full lifecycle."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
# Seed for reproducible output.
|
|
179
|
+
random.seed(0)
|
|
180
|
+
main()
|