reliability-gate 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.
- reliability_gate/__init__.py +53 -0
- reliability_gate/client.py +636 -0
- reliability_gate/decision.py +273 -0
- reliability_gate/exceptions.py +18 -0
- reliability_gate-0.1.0.dist-info/METADATA +575 -0
- reliability_gate-0.1.0.dist-info/RECORD +9 -0
- reliability_gate-0.1.0.dist-info/WHEEL +5 -0
- reliability_gate-0.1.0.dist-info/licenses/LICENSE +201 -0
- reliability_gate-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReliabilityGate
|
|
3
|
+
===============
|
|
4
|
+
An anti-gameable permission-to-act layer for autonomous agents.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from reliability_gate import ReliabilityGate, AbstentionRequired
|
|
8
|
+
|
|
9
|
+
gate = ReliabilityGate(api_key="my-project", agent_id="gpt-4o")
|
|
10
|
+
gate.observe(prediction=72.0, actual=68.5, domain="finance")
|
|
11
|
+
gate.should_act() # → True/False — governs the next decision
|
|
12
|
+
|
|
13
|
+
(Prototyped internally under the codename "Wayne Brain"; the public name is
|
|
14
|
+
ReliabilityGate. Legacy names remain only as deprecated aliases.)
|
|
15
|
+
"""
|
|
16
|
+
from reliability_gate.client import ( # noqa: F401
|
|
17
|
+
ReliabilityGate,
|
|
18
|
+
ReliabilityGateClient,
|
|
19
|
+
CISResult,
|
|
20
|
+
ReliabilityGateError,
|
|
21
|
+
AbstentionRequired,
|
|
22
|
+
APIError,
|
|
23
|
+
ConnectionError,
|
|
24
|
+
# Aliases legacy dépréciés (codename interne) :
|
|
25
|
+
WayneBrain,
|
|
26
|
+
WayneBrainError,
|
|
27
|
+
CognitiveLayer,
|
|
28
|
+
)
|
|
29
|
+
from reliability_gate.decision import ( # noqa: F401
|
|
30
|
+
ReliabilityDecision,
|
|
31
|
+
decide,
|
|
32
|
+
OBSERVE,
|
|
33
|
+
ADVISORY,
|
|
34
|
+
HARD_GATE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ReliabilityGate",
|
|
41
|
+
"ReliabilityGateClient",
|
|
42
|
+
"ReliabilityDecision",
|
|
43
|
+
"decide",
|
|
44
|
+
"CISResult",
|
|
45
|
+
"ReliabilityGateError",
|
|
46
|
+
"AbstentionRequired",
|
|
47
|
+
"APIError",
|
|
48
|
+
"ConnectionError",
|
|
49
|
+
"OBSERVE",
|
|
50
|
+
"ADVISORY",
|
|
51
|
+
"HARD_GATE",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReliabilityGate — Python client
|
|
3
|
+
================================
|
|
4
|
+
An anti-gameable permission-to-act layer for autonomous agents.
|
|
5
|
+
Plug into any LLM in 3 lines — measure real reliability, abstain when unreliable.
|
|
6
|
+
|
|
7
|
+
Quickstart:
|
|
8
|
+
from reliability_gate import ReliabilityGate, AbstentionRequired
|
|
9
|
+
|
|
10
|
+
gate = ReliabilityGate(api_key="my-project", agent_id="gpt-4o")
|
|
11
|
+
|
|
12
|
+
# After each LLM decision — submit the real outcome
|
|
13
|
+
gate.observe(prediction=72.0, actual=68.5, domain="finance")
|
|
14
|
+
|
|
15
|
+
# Before each decision — automatic gate
|
|
16
|
+
if not gate.should_act():
|
|
17
|
+
return route_to_human(task)
|
|
18
|
+
|
|
19
|
+
# Or use the decorator — raises AbstentionRequired if unreliable
|
|
20
|
+
@gate.guard()
|
|
21
|
+
def call_llm(prompt: str) -> str:
|
|
22
|
+
return llm.complete(prompt)
|
|
23
|
+
|
|
24
|
+
Advisory usage (no hard gating): call should_act()/cis() and log the verdict
|
|
25
|
+
without enforcing it, or use @gate.guard(on_abstain="log") which warns but lets
|
|
26
|
+
the call through. Switch to on_abstain="raise" once you trust the signal.
|
|
27
|
+
|
|
28
|
+
Requires: httpx (pip install httpx) or stdlib urllib as fallback.
|
|
29
|
+
|
|
30
|
+
Historical note: this package was prototyped internally under the codename
|
|
31
|
+
"Wayne Brain". The public name is ReliabilityGate; the legacy `sdk.wayne_cog`
|
|
32
|
+
import path and the `WayneBrain` class name remain as deprecated aliases only.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import functools
|
|
37
|
+
import json
|
|
38
|
+
import time
|
|
39
|
+
from typing import Any, Callable
|
|
40
|
+
from urllib.parse import quote
|
|
41
|
+
|
|
42
|
+
from reliability_gate.decision import ReliabilityDecision, decide, ADVISORY
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import httpx
|
|
46
|
+
_HAS_HTTPX = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
_HAS_HTTPX = False
|
|
49
|
+
|
|
50
|
+
# ── Exceptions ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
class ReliabilityGateError(Exception):
|
|
53
|
+
"""Base client error."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AbstentionRequired(ReliabilityGateError):
|
|
57
|
+
"""Raised by @gate.guard() when the agent should not act.
|
|
58
|
+
|
|
59
|
+
Catch this to route the task to a human reviewer.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
try:
|
|
63
|
+
result = call_llm(prompt)
|
|
64
|
+
except AbstentionRequired as e:
|
|
65
|
+
route_to_human(task, cis=e.cis, reason=e.verdict)
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, agent_id: str, cis: float, verdict: str, advice: str = "") -> None:
|
|
68
|
+
self.agent_id = agent_id
|
|
69
|
+
self.cis = cis
|
|
70
|
+
self.verdict = verdict
|
|
71
|
+
self.advice = advice
|
|
72
|
+
super().__init__(
|
|
73
|
+
f"Agent '{agent_id}' must abstain — CIS={cis:.3f} ({verdict}). {advice}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class APIError(ReliabilityGateError):
|
|
78
|
+
"""ReliabilityGate API returned an error."""
|
|
79
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
80
|
+
self.status_code = status_code
|
|
81
|
+
self.detail = detail
|
|
82
|
+
super().__init__(f"ReliabilityGate API {status_code}: {detail}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ConnectionError(ReliabilityGateError): # noqa: A001
|
|
86
|
+
"""Cannot reach the ReliabilityGate API."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── CIS Result dataclass (dict-compatible) ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
class CISResult(dict):
|
|
92
|
+
"""CIS response with typed accessors.
|
|
93
|
+
|
|
94
|
+
Behaves like a regular dict (JSON-serializable) but also provides
|
|
95
|
+
attribute access for the most common fields.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
result = gate.cis()
|
|
99
|
+
print(result.score) # 0.712
|
|
100
|
+
print(result.verdict) # "calibrated"
|
|
101
|
+
print(result.should_act) # True
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def score(self) -> float:
|
|
106
|
+
return float(self.get("cis", 0.0))
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def verdict(self) -> str:
|
|
110
|
+
return str(self.get("verdict", "no_data"))
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def should_act(self) -> bool:
|
|
114
|
+
return not self.get("should_abstain", True)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def n_outcomes(self) -> int:
|
|
118
|
+
return int(self.get("n_outcomes", 0))
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def advice(self) -> str:
|
|
122
|
+
return str(self.get("advice", ""))
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def components(self) -> dict[str, float]:
|
|
126
|
+
return self.get("components", {})
|
|
127
|
+
|
|
128
|
+
def __repr__(self) -> str:
|
|
129
|
+
return (
|
|
130
|
+
f"CISResult(score={self.score:.3f}, verdict={self.verdict!r}, "
|
|
131
|
+
f"n_outcomes={self.n_outcomes}, should_act={self.should_act})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# NB : `ReliabilityDecision` (verdict action-aware) est défini dans
|
|
136
|
+
# `reliability_gate.decision` et importé en tête de module. `CISResult` reste le
|
|
137
|
+
# type de réponse du score CIS — ce sont deux objets distincts.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Main client ───────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
class ReliabilityGate:
|
|
143
|
+
"""ReliabilityGate client — permission-to-act layer for autonomous agents.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
api_key: Your API key (= tenant ID in the MVP).
|
|
147
|
+
agent_id: Unique identifier for this agent.
|
|
148
|
+
base_url: ReliabilityGate API URL (default: http://localhost:8001).
|
|
149
|
+
timeout: HTTP timeout in seconds (default: 5s — keep low for gate calls).
|
|
150
|
+
retries: Number of retries on transient errors (default: 2).
|
|
151
|
+
|
|
152
|
+
enforcement_mode: Default enforcement for action gating
|
|
153
|
+
("observe" | "advisory" | "hard_gate").
|
|
154
|
+
Default "advisory" — never blocks; surfaces a
|
|
155
|
+
recommendation (observe-first friendly).
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
gate = ReliabilityGate(api_key="my-company", agent_id="gpt-4o-finance")
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
api_key: str,
|
|
164
|
+
agent_id: str,
|
|
165
|
+
base_url: str = "http://localhost:8001",
|
|
166
|
+
timeout: float = 5.0,
|
|
167
|
+
retries: int = 2,
|
|
168
|
+
enforcement_mode: str = ADVISORY,
|
|
169
|
+
) -> None:
|
|
170
|
+
self.api_key = api_key
|
|
171
|
+
self.agent_id = agent_id
|
|
172
|
+
self.base_url = base_url.rstrip("/")
|
|
173
|
+
self.timeout = timeout
|
|
174
|
+
self.retries = retries
|
|
175
|
+
self.enforcement_mode = enforcement_mode
|
|
176
|
+
self._headers = {
|
|
177
|
+
"X-API-Key": api_key,
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
def _request(self, method: str, path: str, body: dict | None = None) -> dict:
|
|
184
|
+
url = f"{self.base_url}{path}"
|
|
185
|
+
last_exc: Exception | None = None
|
|
186
|
+
|
|
187
|
+
for attempt in range(self.retries + 1):
|
|
188
|
+
try:
|
|
189
|
+
return self._do_request(method, url, body)
|
|
190
|
+
except (APIError, AbstentionRequired):
|
|
191
|
+
raise # ne pas retenter les erreurs métier
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
last_exc = exc
|
|
194
|
+
if attempt < self.retries:
|
|
195
|
+
time.sleep(0.3 * (2 ** attempt)) # backoff exponentiel
|
|
196
|
+
|
|
197
|
+
raise ConnectionError(f"Cannot reach ReliabilityGate API at {url}: {last_exc}") from last_exc
|
|
198
|
+
|
|
199
|
+
def _do_request(self, method: str, url: str, body: dict | None) -> dict:
|
|
200
|
+
data = json.dumps(body).encode() if body else None
|
|
201
|
+
|
|
202
|
+
if _HAS_HTTPX:
|
|
203
|
+
fn = httpx.post if method == "POST" else httpx.get
|
|
204
|
+
kwargs: dict[str, Any] = {"headers": self._headers, "timeout": self.timeout}
|
|
205
|
+
if method == "POST":
|
|
206
|
+
kwargs["content"] = data
|
|
207
|
+
resp = fn(url, **kwargs)
|
|
208
|
+
if resp.status_code >= 400:
|
|
209
|
+
raise APIError(resp.status_code, resp.text[:200])
|
|
210
|
+
return resp.json()
|
|
211
|
+
|
|
212
|
+
# Fallback stdlib (zero dependencies)
|
|
213
|
+
import urllib.request as _urllib
|
|
214
|
+
import urllib.error as _urlerr
|
|
215
|
+
req = _urllib.Request(url, data=data, headers=self._headers, method=method)
|
|
216
|
+
try:
|
|
217
|
+
with _urllib.urlopen(req, timeout=self.timeout) as r:
|
|
218
|
+
return json.loads(r.read())
|
|
219
|
+
except _urlerr.HTTPError as e:
|
|
220
|
+
raise APIError(e.code, e.read().decode()[:200]) from e
|
|
221
|
+
except OSError as e:
|
|
222
|
+
raise ConnectionError(str(e)) from e
|
|
223
|
+
|
|
224
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def observe(
|
|
227
|
+
self,
|
|
228
|
+
prediction: float | None = None,
|
|
229
|
+
actual: float | None = None,
|
|
230
|
+
domain: str = "general",
|
|
231
|
+
source: str = "",
|
|
232
|
+
abstained: bool = False,
|
|
233
|
+
action: str | None = None,
|
|
234
|
+
metadata: dict[str, Any] | None = None,
|
|
235
|
+
) -> dict:
|
|
236
|
+
"""Submit a real outcome after an agent interaction.
|
|
237
|
+
|
|
238
|
+
Call this every time your agent makes a prediction and you later
|
|
239
|
+
observe the ground truth. ReliabilityGate uses these outcomes to
|
|
240
|
+
continuously recalibrate the agent's CIS.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
prediction: Value the agent predicted (0–100 scale).
|
|
244
|
+
actual: Real observed value (0–100 scale).
|
|
245
|
+
domain: Business domain (e.g. "finance", "legal", "support").
|
|
246
|
+
source: Source identifier (URL, document ID, etc.).
|
|
247
|
+
abstained: True if the agent chose not to predict.
|
|
248
|
+
action: Action type this outcome relates to (e.g. "send_email").
|
|
249
|
+
Enables action-aware gating via should_act(action=...).
|
|
250
|
+
metadata: Any additional key-value pairs to store.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Dict with updated CIS: {"cis_updated": 0.72, "verdict": "calibrated", ...}
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
gate.observe(prediction=72.0, actual=68.5, domain="finance")
|
|
257
|
+
gate.observe(prediction=80.0, actual=78.0, action="send_email")
|
|
258
|
+
gate.observe(abstained=True, domain="legal")
|
|
259
|
+
"""
|
|
260
|
+
return self._request("POST", "/observe", {
|
|
261
|
+
"agent_id": self.agent_id,
|
|
262
|
+
"prediction": prediction,
|
|
263
|
+
"actual": actual,
|
|
264
|
+
"domain": domain,
|
|
265
|
+
"source": source,
|
|
266
|
+
"abstained": abstained,
|
|
267
|
+
"action": action,
|
|
268
|
+
"metadata": metadata or {},
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
def cis(self) -> CISResult:
|
|
272
|
+
"""Return the current Cognitive Integrity Score for this agent.
|
|
273
|
+
|
|
274
|
+
Returns a CISResult with typed accessors:
|
|
275
|
+
result.score → float ∈ [0, 1]
|
|
276
|
+
result.verdict → "trusted" | "calibrated" | "learning" | "unreliable"
|
|
277
|
+
result.should_act → bool (False = agent should abstain)
|
|
278
|
+
result.components → {"mae_score": 0.81, "skill_score": 0.67, ...}
|
|
279
|
+
|
|
280
|
+
Example:
|
|
281
|
+
result = gate.cis()
|
|
282
|
+
print(f"CIS: {result.score} ({result.verdict})")
|
|
283
|
+
if not result.should_act:
|
|
284
|
+
route_to_human(task)
|
|
285
|
+
"""
|
|
286
|
+
raw = self._request("GET", f"/cis/{self.agent_id}")
|
|
287
|
+
return CISResult(raw)
|
|
288
|
+
|
|
289
|
+
def cis_for_action(self, action: str) -> CISResult:
|
|
290
|
+
"""Return the CIS computed over outcomes filtered to a single action type.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
gate.cis_for_action("send_email").score
|
|
294
|
+
"""
|
|
295
|
+
raw = self._request("GET", f"/cis/{self.agent_id}?action={quote(action, safe='')}")
|
|
296
|
+
return CISResult(raw)
|
|
297
|
+
|
|
298
|
+
def should_act(
|
|
299
|
+
self,
|
|
300
|
+
action: str | None = None,
|
|
301
|
+
risk_level: str = "medium",
|
|
302
|
+
enforcement_mode: str | None = None,
|
|
303
|
+
min_cis: float | None = None,
|
|
304
|
+
) -> "bool | ReliabilityDecision":
|
|
305
|
+
"""Gate check — has the agent earned the right to act?
|
|
306
|
+
|
|
307
|
+
Two modes (backward compatible):
|
|
308
|
+
|
|
309
|
+
- **Agent-only (legacy)** — `should_act()` with no `action` returns a
|
|
310
|
+
plain ``bool``: True if the agent is globally reliable enough.
|
|
311
|
+
- **Action-aware** — `should_act(action="send_email", ...)` returns a
|
|
312
|
+
:class:`ReliabilityDecision` (truthy iff ``allow``) that says whether
|
|
313
|
+
the agent has earned the right to perform *that specific action*.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
action: Action type to gate (e.g. "send_email"). None →
|
|
317
|
+
legacy agent-only bool.
|
|
318
|
+
risk_level: "low" | "medium" | "customer_visible" | "high" |
|
|
319
|
+
"irreversible" | "destructive" | "financial".
|
|
320
|
+
Unknown → treated as high (fail-closed).
|
|
321
|
+
enforcement_mode: "observe" | "advisory" | "hard_gate". Defaults to
|
|
322
|
+
the client's enforcement_mode ("advisory"). Only
|
|
323
|
+
"hard_gate" can return allow=False.
|
|
324
|
+
min_cis: (legacy path only) optional CIS threshold override.
|
|
325
|
+
|
|
326
|
+
Example (action-aware):
|
|
327
|
+
decision = gate.should_act(action="send_email",
|
|
328
|
+
risk_level="customer_visible",
|
|
329
|
+
enforcement_mode="hard_gate")
|
|
330
|
+
if decision.allow:
|
|
331
|
+
send_email()
|
|
332
|
+
else:
|
|
333
|
+
print(decision.reason)
|
|
334
|
+
"""
|
|
335
|
+
# ── Legacy agent-only path → bool ──────────────────────────────────────
|
|
336
|
+
if action is None:
|
|
337
|
+
try:
|
|
338
|
+
result = self.cis()
|
|
339
|
+
if min_cis is not None:
|
|
340
|
+
return result.score >= min_cis
|
|
341
|
+
return result.should_act
|
|
342
|
+
except ConnectionError:
|
|
343
|
+
return False # fail-safe: API unreachable → do not act
|
|
344
|
+
|
|
345
|
+
# ── Action-aware path → ReliabilityDecision ────────────────────────────
|
|
346
|
+
mode = (enforcement_mode or self.enforcement_mode)
|
|
347
|
+
try:
|
|
348
|
+
global_cis = self.cis()
|
|
349
|
+
action_cis = self.cis_for_action(action)
|
|
350
|
+
cis_score = global_cis.score
|
|
351
|
+
action_score = action_cis.score if action_cis.n_outcomes > 0 else None
|
|
352
|
+
sample_size = action_cis.n_outcomes
|
|
353
|
+
except ConnectionError:
|
|
354
|
+
# Fail-safe : API injoignable → 0 preuve. En hard_gate cela bloque les
|
|
355
|
+
# actions risquées (CIS 0 < seuil) ; en observe/advisory, ne bloque pas.
|
|
356
|
+
cis_score, action_score, sample_size = 0.0, None, 0
|
|
357
|
+
|
|
358
|
+
return decide(
|
|
359
|
+
agent_id=self.agent_id,
|
|
360
|
+
action=action,
|
|
361
|
+
risk_level=risk_level,
|
|
362
|
+
cis_score=cis_score,
|
|
363
|
+
action_score=action_score,
|
|
364
|
+
sample_size=sample_size,
|
|
365
|
+
enforcement_mode=mode,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def guard(
|
|
369
|
+
self,
|
|
370
|
+
on_abstain: str = "raise",
|
|
371
|
+
min_cis: float | None = None,
|
|
372
|
+
) -> Callable:
|
|
373
|
+
"""Decorator — automatically gates the function on agent reliability.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
on_abstain: What to do when agent should abstain:
|
|
377
|
+
"raise" → raises AbstentionRequired (default — hard gate)
|
|
378
|
+
"none" → returns None silently
|
|
379
|
+
"log" → logs a warning, lets the call through (advisory mode)
|
|
380
|
+
min_cis: Optional CIS threshold override.
|
|
381
|
+
|
|
382
|
+
Advisory usage: start with on_abstain="log" to observe the gate's
|
|
383
|
+
verdicts without enforcing them; switch to "raise" once trusted.
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
@gate.guard()
|
|
387
|
+
def call_llm(prompt: str) -> str:
|
|
388
|
+
return openai.complete(prompt)
|
|
389
|
+
|
|
390
|
+
# Advisory (logs but does not block):
|
|
391
|
+
@gate.guard(on_abstain="log", min_cis=0.65)
|
|
392
|
+
def risky_decision(data: dict) -> dict:
|
|
393
|
+
...
|
|
394
|
+
"""
|
|
395
|
+
def decorator(fn: Callable) -> Callable:
|
|
396
|
+
@functools.wraps(fn)
|
|
397
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
398
|
+
result = self.cis()
|
|
399
|
+
should = result.score >= min_cis if min_cis is not None else result.should_act
|
|
400
|
+
|
|
401
|
+
if not should:
|
|
402
|
+
if on_abstain == "raise":
|
|
403
|
+
raise AbstentionRequired(
|
|
404
|
+
agent_id=self.agent_id,
|
|
405
|
+
cis=result.score,
|
|
406
|
+
verdict=result.verdict,
|
|
407
|
+
advice=result.advice,
|
|
408
|
+
)
|
|
409
|
+
elif on_abstain == "none":
|
|
410
|
+
return None
|
|
411
|
+
elif on_abstain == "log":
|
|
412
|
+
import warnings
|
|
413
|
+
warnings.warn(
|
|
414
|
+
f"[ReliabilityGate] Agent '{self.agent_id}' is unreliable "
|
|
415
|
+
f"(CIS={result.score:.3f}, {result.verdict}) — proceeding anyway.",
|
|
416
|
+
stacklevel=2,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return fn(*args, **kwargs)
|
|
420
|
+
return wrapper
|
|
421
|
+
return decorator
|
|
422
|
+
|
|
423
|
+
def calibrate(self, url: str, domain: str = "web") -> dict:
|
|
424
|
+
"""Run a full real calibration cycle on a public URL.
|
|
425
|
+
|
|
426
|
+
ReliabilityGate will:
|
|
427
|
+
1. Predict extraction yield based on past history
|
|
428
|
+
2. Fetch the URL (read-only, no login, no writes)
|
|
429
|
+
3. Measure real extraction yield
|
|
430
|
+
4. Persist outcome and return updated CIS
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
gate.calibrate("https://news.ycombinator.com", domain="tech")
|
|
434
|
+
"""
|
|
435
|
+
return self._request("POST", "/calibrate", {
|
|
436
|
+
"agent_id": self.agent_id,
|
|
437
|
+
"url": url,
|
|
438
|
+
"domain": domain,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
# ── Commit-Reveal (anti-triche) ──────────────────────────────────────────
|
|
442
|
+
#
|
|
443
|
+
# QUAND UTILISER ?
|
|
444
|
+
# Utilisez commit-reveal quand la vérifiabilité est critique :
|
|
445
|
+
# finance, compliance, audit. Pour du dev/test, POST /observe suffit.
|
|
446
|
+
#
|
|
447
|
+
# FLOW EN 3 ÉTAPES :
|
|
448
|
+
# 1. gate.commit(prediction=72.5) → verrouille la prédiction
|
|
449
|
+
# 2. ... observer le résultat réel ...
|
|
450
|
+
# 3. gate.reveal(actual=68.0) → ReliabilityGate vérifie et persiste
|
|
451
|
+
#
|
|
452
|
+
# RACCOURCI :
|
|
453
|
+
# gate.observe_verified(prediction=72.5, actual=68.0)
|
|
454
|
+
# → fait le commit + reveal en un seul appel (pour les cas simples)
|
|
455
|
+
|
|
456
|
+
def commit(self, prediction: float, domain: str = "general") -> dict:
|
|
457
|
+
"""Lock a prediction BEFORE observing the real outcome (anti-cheat).
|
|
458
|
+
|
|
459
|
+
Computes SHA-256(prediction|nonce) and sends the hash to ReliabilityGate.
|
|
460
|
+
The server stores the hash; the prediction cannot be changed after this call.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
prediction: The value your agent predicted (0-100 scale).
|
|
464
|
+
domain: Business domain (e.g. "finance", "legal").
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Dict with commit_id and nonce — you'll need both for reveal().
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
commit = gate.commit(prediction=72.5, domain="finance")
|
|
471
|
+
# ... wait for the real outcome ...
|
|
472
|
+
gate.reveal(commit_id=commit["commit_id"], nonce=commit["nonce"],
|
|
473
|
+
prediction=72.5, actual=68.0)
|
|
474
|
+
"""
|
|
475
|
+
import hashlib
|
|
476
|
+
import secrets
|
|
477
|
+
|
|
478
|
+
# Génère un nonce aléatoire (32 hex chars = 128 bits d'entropie)
|
|
479
|
+
# Le nonce empêche quiconque (même le serveur) de deviner la prédiction
|
|
480
|
+
nonce = secrets.token_hex(16)
|
|
481
|
+
|
|
482
|
+
# Hash la prédiction avec le nonce — c'est ce hash qui est envoyé au serveur
|
|
483
|
+
# Format : sha256("72.5|a1b2c3d4e5f6...")
|
|
484
|
+
prediction_hash = hashlib.sha256(
|
|
485
|
+
f"{prediction}|{nonce}".encode("utf-8")
|
|
486
|
+
).hexdigest()
|
|
487
|
+
|
|
488
|
+
result = self._request("POST", "/commit", {
|
|
489
|
+
"agent_id": self.agent_id,
|
|
490
|
+
"prediction_hash": prediction_hash,
|
|
491
|
+
"domain": domain,
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
# On retourne le nonce au client pour qu'il puisse faire le reveal
|
|
495
|
+
# IMPORTANT : le client DOIT conserver le nonce, le serveur ne le connaît pas
|
|
496
|
+
result["nonce"] = nonce
|
|
497
|
+
result["prediction"] = prediction
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
def reveal(
|
|
501
|
+
self,
|
|
502
|
+
commit_id: str,
|
|
503
|
+
prediction: float,
|
|
504
|
+
nonce: str,
|
|
505
|
+
actual: float,
|
|
506
|
+
metadata: dict[str, Any] | None = None,
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""Reveal a committed prediction and submit the real outcome.
|
|
509
|
+
|
|
510
|
+
The server verifies that SHA-256(prediction|nonce) matches the stored hash.
|
|
511
|
+
If it matches → verified outcome persisted. If not → rejected (cheat detected).
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
commit_id: The ID returned by commit().
|
|
515
|
+
prediction: The SAME prediction you committed (must match the hash).
|
|
516
|
+
nonce: The SAME nonce returned by commit().
|
|
517
|
+
actual: The real observed value (0-100 scale).
|
|
518
|
+
metadata: Optional additional key-value pairs.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Dict with verified=True and updated CIS.
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
APIError(400): If the hash doesn't match (cheat detected).
|
|
525
|
+
APIError(404): If the commit_id is expired or invalid.
|
|
526
|
+
"""
|
|
527
|
+
return self._request("POST", "/reveal", {
|
|
528
|
+
"commit_id": commit_id,
|
|
529
|
+
"prediction": prediction,
|
|
530
|
+
"nonce": nonce,
|
|
531
|
+
"actual": actual,
|
|
532
|
+
"metadata": metadata or {},
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
def observe_verified(
|
|
536
|
+
self,
|
|
537
|
+
prediction: float,
|
|
538
|
+
actual: float,
|
|
539
|
+
domain: str = "general",
|
|
540
|
+
metadata: dict[str, Any] | None = None,
|
|
541
|
+
) -> dict:
|
|
542
|
+
"""Shortcut: commit + reveal in one call (verified outcome).
|
|
543
|
+
|
|
544
|
+
Combines commit() and reveal() for cases where the prediction and
|
|
545
|
+
actual values are both known at the same time (e.g. batch processing,
|
|
546
|
+
historical data ingestion with proof).
|
|
547
|
+
|
|
548
|
+
The outcome will be flagged as verified=True.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
prediction: Value the agent predicted (0-100 scale).
|
|
552
|
+
actual: Real observed value (0-100 scale).
|
|
553
|
+
domain: Business domain.
|
|
554
|
+
metadata: Optional additional data.
|
|
555
|
+
|
|
556
|
+
Example:
|
|
557
|
+
# Simple — one line, cryptographically verified
|
|
558
|
+
gate.observe_verified(prediction=72.5, actual=68.0, domain="finance")
|
|
559
|
+
"""
|
|
560
|
+
commit = self.commit(prediction=prediction, domain=domain)
|
|
561
|
+
return self.reveal(
|
|
562
|
+
commit_id=commit["commit_id"],
|
|
563
|
+
prediction=prediction,
|
|
564
|
+
nonce=commit["nonce"],
|
|
565
|
+
actual=actual,
|
|
566
|
+
metadata=metadata,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def agents(self) -> list[dict]:
|
|
570
|
+
"""List all agents in your tenant with their current CIS.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
List of dicts: [{"agent_id": "...", "cis": 0.72, "verdict": "calibrated"}, ...]
|
|
574
|
+
"""
|
|
575
|
+
raw = self._request("GET", "/agents")
|
|
576
|
+
return raw.get("agents", [])
|
|
577
|
+
|
|
578
|
+
def __repr__(self) -> str:
|
|
579
|
+
return f"ReliabilityGate(agent_id={self.agent_id!r}, base_url={self.base_url!r})"
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# ── Aliases ───────────────────────────────────────────────────────────────────
|
|
583
|
+
# `ReliabilityGateClient` : alias explicite pour ceux qui préfèrent un nom suffixé.
|
|
584
|
+
ReliabilityGateClient = ReliabilityGate
|
|
585
|
+
|
|
586
|
+
# Aliases legacy (codename interne "Wayne Brain") — dépréciés, conservés pour
|
|
587
|
+
# compatibilité ; les docs publiques n'utilisent que ReliabilityGate.
|
|
588
|
+
WayneBrain = ReliabilityGate
|
|
589
|
+
CognitiveLayer = ReliabilityGate
|
|
590
|
+
WayneBrainError = ReliabilityGateError
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ── CLI demo ─────────────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
if __name__ == "__main__":
|
|
596
|
+
"""Quick demo — run with: python -m reliability_gate.client"""
|
|
597
|
+
import sys
|
|
598
|
+
|
|
599
|
+
print("ReliabilityGate — SDK demo\n" + "=" * 40)
|
|
600
|
+
|
|
601
|
+
gate = ReliabilityGate(
|
|
602
|
+
api_key="sdk-demo",
|
|
603
|
+
agent_id="demo-agent",
|
|
604
|
+
base_url="http://localhost:8001",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
print("\n1. Submitting 10 outcomes (agent learning)...")
|
|
608
|
+
scenarios = [
|
|
609
|
+
(50, 20), (50, 80), (45, 55),
|
|
610
|
+
(48, 52), (50, 53), (51, 52),
|
|
611
|
+
(52, 53), (52, 52), (53, 53),
|
|
612
|
+
(53, 54),
|
|
613
|
+
]
|
|
614
|
+
for i, (pred, actual) in enumerate(scenarios, 1):
|
|
615
|
+
r = gate.observe(prediction=float(pred), actual=float(actual), domain="demo")
|
|
616
|
+
print(f" [{i:2d}] pred={pred} actual={actual} → CIS={r['cis_updated']:.3f} ({r['verdict']})")
|
|
617
|
+
|
|
618
|
+
print("\n2. Current CIS:")
|
|
619
|
+
result = gate.cis()
|
|
620
|
+
print(f" {result}")
|
|
621
|
+
print(f" Should act autonomously: {result.should_act}")
|
|
622
|
+
|
|
623
|
+
print("\n3. Testing @guard decorator...")
|
|
624
|
+
|
|
625
|
+
@gate.guard(on_abstain="none")
|
|
626
|
+
def risky_llm_call(prompt: str) -> str | None:
|
|
627
|
+
return f"Response to: {prompt}"
|
|
628
|
+
|
|
629
|
+
output = risky_llm_call("What is the market outlook?")
|
|
630
|
+
if output is None:
|
|
631
|
+
print(" → Agent abstained (CIS too low). Task routed to human.")
|
|
632
|
+
else:
|
|
633
|
+
print(f" → Agent acted: {output!r}")
|
|
634
|
+
|
|
635
|
+
print("\n✅ Demo complete.")
|
|
636
|
+
sys.exit(0)
|