hypermind 0.11.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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
hypermind/errors.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Exception hierarchy + structured `BlockReason`.
|
|
2
|
+
|
|
3
|
+
Each exception carries a `telemetry` dict matching the spec/13-observability.md
|
|
4
|
+
log schema, so errors emitted from the SDK boundary are immediately
|
|
5
|
+
operationally legible without re-wrapping.
|
|
6
|
+
|
|
7
|
+
`BlockReason` (this module) is the structured-refusal contract surfaced by
|
|
8
|
+
`HyperMindAgent.why_blocked(...)` and (eventually, post-WS-F) attached to every
|
|
9
|
+
`HyperMindError` via the `reason=` keyword argument. The `[rule_id] spec_ref:
|
|
10
|
+
hint` `__str__` format is a stability contract: log-parsing regexes
|
|
11
|
+
`r"\\[([A-Z0-9-]+)\\]"` MUST keep working across minor versions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import timedelta
|
|
19
|
+
from typing import Any, Literal
|
|
20
|
+
|
|
21
|
+
# -----------------------------------------------------------------------
|
|
22
|
+
# BlockReason — structured refusal carrying rule_id + spec_ref
|
|
23
|
+
# -----------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class BlockReason:
|
|
28
|
+
"""Structured refusal reason — every Blocked exception carries one.
|
|
29
|
+
|
|
30
|
+
Per draft-hypermind-scitt-contested-claims-profile §16 (Error Taxonomy).
|
|
31
|
+
The bracketed ``[rule_id]`` prefix in ``__str__`` is part of the stable
|
|
32
|
+
contract; log-parsing regexes ``r"\\[([A-Z0-9-]+)\\]"`` MUST keep working
|
|
33
|
+
across minor versions.
|
|
34
|
+
|
|
35
|
+
The legacy fields (``human_explanation``, ``current_value``,
|
|
36
|
+
``required_value``, ``retry_after_seconds``) are retained for backward
|
|
37
|
+
compatibility with `HyperMindAgent.why_blocked(...)` consumers shipped in
|
|
38
|
+
v0.1.0a0; new code should use ``hint`` and ``telemetry`` instead.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
rule_id: str
|
|
42
|
+
spec_ref: str
|
|
43
|
+
docs_url: str | None = None
|
|
44
|
+
hint: str | None = None
|
|
45
|
+
telemetry: Mapping[str, Any] = field(default_factory=dict)
|
|
46
|
+
# Legacy (v0.1.0a0) compatibility fields — retained so existing callers of
|
|
47
|
+
# `why_blocked()` keep working unchanged. New code SHOULD prefer `hint`.
|
|
48
|
+
human_explanation: str | None = None
|
|
49
|
+
current_value: Any | None = None
|
|
50
|
+
required_value: Any | None = None
|
|
51
|
+
retry_after_seconds: float | None = None
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
# Prefer the new `hint` field; fall back to legacy `human_explanation`
|
|
55
|
+
# so v0.1.0a0 call sites that still populate the old field render a
|
|
56
|
+
# useful message instead of "(no hint)".
|
|
57
|
+
message = self.hint or self.human_explanation or "(no hint)"
|
|
58
|
+
return f"[{self.rule_id}] {self.spec_ref}: {message}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -----------------------------------------------------------------------
|
|
62
|
+
# Exception hierarchy
|
|
63
|
+
# -----------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HyperMindError(Exception):
|
|
67
|
+
"""Base class for all SDK errors.
|
|
68
|
+
|
|
69
|
+
Optionally carries a structured ``BlockReason`` so callers can branch on
|
|
70
|
+
``e.rule_id`` instead of scraping the message. Either pass the reason as
|
|
71
|
+
the first positional argument (``raise HyperMindError(reason)``) or via
|
|
72
|
+
the ``reason=`` keyword.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
reason: BlockReason | None = None
|
|
76
|
+
rule_id: str | None = None
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
message: str | BlockReason,
|
|
81
|
+
*,
|
|
82
|
+
reason: BlockReason | None = None,
|
|
83
|
+
telemetry: dict[str, Any] | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
if isinstance(message, BlockReason):
|
|
86
|
+
self.reason = message
|
|
87
|
+
self.rule_id = message.rule_id
|
|
88
|
+
super().__init__(str(message))
|
|
89
|
+
else:
|
|
90
|
+
self.reason = reason
|
|
91
|
+
self.rule_id = reason.rule_id if reason is not None else None
|
|
92
|
+
super().__init__(message)
|
|
93
|
+
self.telemetry: dict[str, Any] = telemetry or {}
|
|
94
|
+
self.telemetry.setdefault("event_type", type(self).__name__)
|
|
95
|
+
if self.rule_id is not None:
|
|
96
|
+
self.telemetry.setdefault("rule_id", self.rule_id)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class NetworkError(HyperMindError):
|
|
100
|
+
"""libp2p dial failure, gossip drop, partition suspected."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AuthError(HyperMindError):
|
|
104
|
+
"""Capability-token / caveat-chain failure."""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
message: str,
|
|
109
|
+
*,
|
|
110
|
+
failed_caveat: str | None = None,
|
|
111
|
+
reason: BlockReason | None = None,
|
|
112
|
+
telemetry: dict[str, Any] | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
super().__init__(message, reason=reason, telemetry=telemetry)
|
|
115
|
+
self.failed_caveat: str | None = failed_caveat
|
|
116
|
+
if failed_caveat is not None:
|
|
117
|
+
self.telemetry["failed_caveat"] = failed_caveat
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class DisputeError(HyperMindError):
|
|
121
|
+
"""Malformed dispute, bond-scope mismatch, or frivolous-window violation."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RateLimitError(HyperMindError):
|
|
125
|
+
"""Capability rate-caveat exceeded. Raised by ``_require_cap`` when a
|
|
126
|
+
``rate`` caveat on the agent's ``cap_token`` is exhausted for this action
|
|
127
|
+
(P1-03 / HM-RATE-CAP-001).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class RatePauseError(HyperMindError):
|
|
132
|
+
"""Gossip rate-cap hit. Carries `retry_after`."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
message: str,
|
|
137
|
+
*,
|
|
138
|
+
retry_after: timedelta,
|
|
139
|
+
reason: BlockReason | None = None,
|
|
140
|
+
telemetry: dict[str, Any] | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
super().__init__(message, reason=reason, telemetry=telemetry)
|
|
143
|
+
self.retry_after: timedelta = retry_after
|
|
144
|
+
self.telemetry["retry_after_seconds"] = retry_after.total_seconds()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class RampError(HyperMindError):
|
|
148
|
+
"""30-day ramp not yet complete or paused."""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
message: str,
|
|
153
|
+
*,
|
|
154
|
+
state: Literal["pending", "paused", "frozen"],
|
|
155
|
+
reason: BlockReason | None = None,
|
|
156
|
+
telemetry: dict[str, Any] | None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
super().__init__(message, reason=reason, telemetry=telemetry)
|
|
159
|
+
self.state = state
|
|
160
|
+
self.telemetry["ramp_state"] = state
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class KeyCompromiseError(HyperMindError):
|
|
164
|
+
"""Signing key has a `compromise_window_start` declared."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
message: str,
|
|
169
|
+
*,
|
|
170
|
+
compromise_window_start: str,
|
|
171
|
+
reason: BlockReason | None = None,
|
|
172
|
+
telemetry: dict[str, Any] | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
super().__init__(message, reason=reason, telemetry=telemetry)
|
|
175
|
+
self.compromise_window_start = compromise_window_start
|
|
176
|
+
self.telemetry["compromise_window_start"] = compromise_window_start
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UnresolvedPartitionError(HyperMindError):
|
|
180
|
+
"""Dispute hit UNRESOLVED_PARTITION terminal — heal-merge produced contradictory closes."""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ConformanceError(HyperMindError):
|
|
184
|
+
"""Wire-level conformance violation — typically thrown by verifier."""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ProtocolVersionError(HyperMindError):
|
|
188
|
+
"""Statement's `protocol_version` is below the namespace's `min_protocol_version`.
|
|
189
|
+
|
|
190
|
+
Carries `rule_id="HM-PROTO-VER-001"` per ROADMAP §0.5.1–0.5.3
|
|
191
|
+
(versioning bridge milestone). Raised pre-signature-verify so the
|
|
192
|
+
refusal is auditable as a policy decision, not a crypto failure.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
message: str,
|
|
198
|
+
*,
|
|
199
|
+
statement_version: int,
|
|
200
|
+
min_required: int,
|
|
201
|
+
reason: BlockReason | None = None,
|
|
202
|
+
telemetry: dict[str, Any] | None = None,
|
|
203
|
+
) -> None:
|
|
204
|
+
if reason is None:
|
|
205
|
+
reason = BlockReason(
|
|
206
|
+
rule_id="HM-PROTO-VER-001",
|
|
207
|
+
spec_ref="spec/17-upgrade-v1.md §protocol-version-negotiation",
|
|
208
|
+
hint=message,
|
|
209
|
+
telemetry={
|
|
210
|
+
"statement_version": statement_version,
|
|
211
|
+
"min_required": min_required,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
super().__init__(message, reason=reason, telemetry=telemetry)
|
|
215
|
+
self.statement_version = statement_version
|
|
216
|
+
self.min_required = min_required
|
|
217
|
+
self.telemetry["statement_version"] = statement_version
|
|
218
|
+
self.telemetry["min_required"] = min_required
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Evaluation primitives — calibration, comparison, replay.
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
* :class:`BrierScoreTracker` — per-(agent, topic) Brier score history
|
|
5
|
+
and reliability diagram, fed by ``oracle_resolve()``.
|
|
6
|
+
* :func:`compare_responders` (Phase 5) — A/B model comparison harness.
|
|
7
|
+
* :func:`replay_audit_log` (Phase 5) — re-derive verdicts from a
|
|
8
|
+
signed audit log to verify determinism.
|
|
9
|
+
|
|
10
|
+
The Brier tracker is wired into :class:`ReputationKernel` so every
|
|
11
|
+
oracle resolution automatically records the calibration delta. Operators
|
|
12
|
+
read the data via the management API at ``GET /v1/calibration``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from hypermind.eval.calibration import (
|
|
18
|
+
BrierEntry,
|
|
19
|
+
BrierScoreTracker,
|
|
20
|
+
ReliabilityBin,
|
|
21
|
+
brier_score,
|
|
22
|
+
calibration_adjustment,
|
|
23
|
+
reliability_diagram,
|
|
24
|
+
)
|
|
25
|
+
from hypermind.eval.comparison import (
|
|
26
|
+
ComparisonReport,
|
|
27
|
+
ResponderResult,
|
|
28
|
+
compare_responders,
|
|
29
|
+
)
|
|
30
|
+
from hypermind.eval.replay import (
|
|
31
|
+
ReplayResult,
|
|
32
|
+
replay_audit_log,
|
|
33
|
+
replay_lines,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"BrierEntry",
|
|
38
|
+
"BrierScoreTracker",
|
|
39
|
+
"ComparisonReport",
|
|
40
|
+
"ReliabilityBin",
|
|
41
|
+
"ReplayResult",
|
|
42
|
+
"ResponderResult",
|
|
43
|
+
"brier_score",
|
|
44
|
+
"calibration_adjustment",
|
|
45
|
+
"compare_responders",
|
|
46
|
+
"reliability_diagram",
|
|
47
|
+
"replay_audit_log",
|
|
48
|
+
"replay_lines",
|
|
49
|
+
]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Closed-loop calibration tracking.
|
|
2
|
+
|
|
3
|
+
When an oracle resolves a claim, the agent that made the prediction
|
|
4
|
+
should be measured: did its stated 70% confidence match observed
|
|
5
|
+
frequencies of YES outcomes? :class:`BrierScoreTracker` records the
|
|
6
|
+
(predicted, outcome) pair, computes per-(agent, topic) Brier scores,
|
|
7
|
+
and produces reliability diagrams.
|
|
8
|
+
|
|
9
|
+
The tracker also derives a :func:`calibration_adjustment` shrinkage
|
|
10
|
+
multiplier ∈ [0.5, 2.0] that callers pass to
|
|
11
|
+
:func:`hypermind.responders.confidence_to_uncertainty` so that
|
|
12
|
+
well-calibrated agents have *sharper* posteriors and overconfident
|
|
13
|
+
agents have *softer* ones — reputation is no longer just "were you
|
|
14
|
+
right?", it's "was your confidence calibrated?".
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Brier-score primitive
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def brier_score(predicted: float, outcome: float) -> float:
|
|
29
|
+
"""Squared error between predicted probability and binary outcome.
|
|
30
|
+
|
|
31
|
+
Lower is better. ``brier_score(0.9, 1.0) == 0.01`` (well-calibrated),
|
|
32
|
+
``brier_score(0.9, 0.0) == 0.81`` (overconfident).
|
|
33
|
+
"""
|
|
34
|
+
p = max(0.0, min(1.0, float(predicted)))
|
|
35
|
+
o = max(0.0, min(1.0, float(outcome)))
|
|
36
|
+
return (p - o) ** 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# History entries
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class BrierEntry:
|
|
46
|
+
"""One (predicted, outcome, Brier) record."""
|
|
47
|
+
|
|
48
|
+
ts_wall_ms: int
|
|
49
|
+
predicted_confidence: float
|
|
50
|
+
outcome: float
|
|
51
|
+
brier_score: float
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict:
|
|
54
|
+
return {
|
|
55
|
+
"ts_wall_ms": self.ts_wall_ms,
|
|
56
|
+
"predicted_confidence": self.predicted_confidence,
|
|
57
|
+
"outcome": self.outcome,
|
|
58
|
+
"brier_score": self.brier_score,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ReliabilityBin:
|
|
64
|
+
"""One bin of a reliability diagram."""
|
|
65
|
+
|
|
66
|
+
lower: float
|
|
67
|
+
upper: float
|
|
68
|
+
mean_confidence: float
|
|
69
|
+
mean_outcome: float
|
|
70
|
+
count: int
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def gap(self) -> float:
|
|
74
|
+
"""Calibration gap for this bin (signed: positive = overconfident)."""
|
|
75
|
+
return self.mean_confidence - self.mean_outcome
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict:
|
|
78
|
+
return {
|
|
79
|
+
"lower": self.lower,
|
|
80
|
+
"upper": self.upper,
|
|
81
|
+
"mean_confidence": self.mean_confidence,
|
|
82
|
+
"mean_outcome": self.mean_outcome,
|
|
83
|
+
"count": self.count,
|
|
84
|
+
"gap": self.gap,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def reliability_diagram(
|
|
89
|
+
entries: Iterable[BrierEntry],
|
|
90
|
+
*,
|
|
91
|
+
n_bins: int = 10,
|
|
92
|
+
) -> list[ReliabilityBin]:
|
|
93
|
+
"""Build a reliability diagram from BrierEntry history.
|
|
94
|
+
|
|
95
|
+
Returns a list of ``n_bins`` :class:`ReliabilityBin` records. Empty
|
|
96
|
+
bins are omitted from the output. A perfectly-calibrated agent has
|
|
97
|
+
every bin's ``gap`` near zero.
|
|
98
|
+
"""
|
|
99
|
+
if n_bins < 1:
|
|
100
|
+
raise ValueError("n_bins must be >= 1")
|
|
101
|
+
|
|
102
|
+
rows = list(entries)
|
|
103
|
+
if not rows:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
edges = [i / n_bins for i in range(n_bins + 1)]
|
|
107
|
+
bins: list[list[BrierEntry]] = [[] for _ in range(n_bins)]
|
|
108
|
+
for r in rows:
|
|
109
|
+
# Find which bucket the predicted confidence falls into.
|
|
110
|
+
# Edge case: confidence == 1.0 → last bin.
|
|
111
|
+
c = max(0.0, min(0.9999999, r.predicted_confidence))
|
|
112
|
+
idx = int(c * n_bins)
|
|
113
|
+
bins[idx].append(r)
|
|
114
|
+
|
|
115
|
+
out: list[ReliabilityBin] = []
|
|
116
|
+
for i, b in enumerate(bins):
|
|
117
|
+
if not b:
|
|
118
|
+
continue
|
|
119
|
+
mean_conf = sum(e.predicted_confidence for e in b) / len(b)
|
|
120
|
+
mean_outcome = sum(e.outcome for e in b) / len(b)
|
|
121
|
+
out.append(
|
|
122
|
+
ReliabilityBin(
|
|
123
|
+
lower=edges[i],
|
|
124
|
+
upper=edges[i + 1],
|
|
125
|
+
mean_confidence=mean_conf,
|
|
126
|
+
mean_outcome=mean_outcome,
|
|
127
|
+
count=len(b),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# calibration_adjustment — drives the shrinkage multiplier
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
#: Brier value at which we treat the agent as "well-calibrated" — boost above this.
|
|
138
|
+
WELL_CALIBRATED_BRIER = 0.10
|
|
139
|
+
#: Brier value above which we treat the agent as "overconfident" — shrink hard.
|
|
140
|
+
OVERCONFIDENT_BRIER = 0.25
|
|
141
|
+
#: Multiplier bounds — never zero (pure shrinkage) or unbounded (pure boost).
|
|
142
|
+
ADJ_MIN = 0.5
|
|
143
|
+
ADJ_MAX = 2.0
|
|
144
|
+
#: Minimum observations before we adjust at all.
|
|
145
|
+
MIN_OBSERVATIONS = 5
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def calibration_adjustment(mean_brier: float | None, n_observations: int) -> float:
|
|
149
|
+
"""Map a mean Brier score to a multiplicative effective_n adjustment.
|
|
150
|
+
|
|
151
|
+
Returns 1.0 (no adjustment) if there are too few observations. Otherwise:
|
|
152
|
+
|
|
153
|
+
mean_brier ≤ 0.10 → boost (multiplier > 1.0, up to 2.0)
|
|
154
|
+
mean_brier ≥ 0.25 → shrink (multiplier < 1.0, down to 0.5)
|
|
155
|
+
between → linear interpolation
|
|
156
|
+
|
|
157
|
+
Tuning rationale:
|
|
158
|
+
- Brier ≤ 0.10 is what well-calibrated forecasters achieve on
|
|
159
|
+
binary questions in academic studies (e.g. Tetlock's superforecasters).
|
|
160
|
+
- Brier ≥ 0.25 is roughly worse than random for a 50/50 question
|
|
161
|
+
(Brier of a fair coin on a 50/50 → 0.25), so the agent's
|
|
162
|
+
confidence is not informative and should be down-weighted.
|
|
163
|
+
"""
|
|
164
|
+
if mean_brier is None or n_observations < MIN_OBSERVATIONS:
|
|
165
|
+
return 1.0
|
|
166
|
+
|
|
167
|
+
b = max(0.0, float(mean_brier))
|
|
168
|
+
if b <= WELL_CALIBRATED_BRIER:
|
|
169
|
+
# Map [0.0, 0.10] → [ADJ_MAX, 1.0]
|
|
170
|
+
frac = b / WELL_CALIBRATED_BRIER
|
|
171
|
+
return ADJ_MAX - frac * (ADJ_MAX - 1.0)
|
|
172
|
+
if b >= OVERCONFIDENT_BRIER:
|
|
173
|
+
return ADJ_MIN
|
|
174
|
+
# Linear from 1.0 (at b=0.10) to ADJ_MIN (at b=0.25)
|
|
175
|
+
span = OVERCONFIDENT_BRIER - WELL_CALIBRATED_BRIER
|
|
176
|
+
frac = (b - WELL_CALIBRATED_BRIER) / span
|
|
177
|
+
return 1.0 - frac * (1.0 - ADJ_MIN)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# BrierScoreTracker
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
#: Cap on per-(agent, topic) history length to bound memory.
|
|
185
|
+
MAX_HISTORY_PER_KEY = 200
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class BrierScoreTracker:
|
|
189
|
+
"""Per-(agent, topic) Brier-score history and aggregates.
|
|
190
|
+
|
|
191
|
+
Lives on :class:`ReputationKernel` (one tracker per kernel) so
|
|
192
|
+
every oracle resolution can call :meth:`record` without plumbing.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self, *, max_history_per_key: int = MAX_HISTORY_PER_KEY) -> None:
|
|
196
|
+
self.max_history_per_key = max_history_per_key
|
|
197
|
+
self._history: dict[tuple[bytes, str], list[BrierEntry]] = {}
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
# Write path
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def record(
|
|
204
|
+
self,
|
|
205
|
+
agent_kid: bytes,
|
|
206
|
+
topic: str,
|
|
207
|
+
predicted_confidence: float,
|
|
208
|
+
outcome: float,
|
|
209
|
+
*,
|
|
210
|
+
ts_wall_ms: int | None = None,
|
|
211
|
+
) -> BrierEntry:
|
|
212
|
+
"""Append one observation. Returns the newly-built BrierEntry."""
|
|
213
|
+
key = (bytes(agent_kid), str(topic))
|
|
214
|
+
ts = int(time.time() * 1000) if ts_wall_ms is None else int(ts_wall_ms)
|
|
215
|
+
entry = BrierEntry(
|
|
216
|
+
ts_wall_ms=ts,
|
|
217
|
+
predicted_confidence=float(predicted_confidence),
|
|
218
|
+
outcome=float(outcome),
|
|
219
|
+
brier_score=brier_score(predicted_confidence, outcome),
|
|
220
|
+
)
|
|
221
|
+
bucket = self._history.setdefault(key, [])
|
|
222
|
+
bucket.append(entry)
|
|
223
|
+
if len(bucket) > self.max_history_per_key:
|
|
224
|
+
# Keep the most recent N
|
|
225
|
+
self._history[key] = bucket[-self.max_history_per_key :]
|
|
226
|
+
return entry
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
# Read path
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def history(
|
|
233
|
+
self,
|
|
234
|
+
agent_kid: bytes,
|
|
235
|
+
topic: str,
|
|
236
|
+
*,
|
|
237
|
+
since_ms: int | None = None,
|
|
238
|
+
) -> list[BrierEntry]:
|
|
239
|
+
rows = self._history.get((bytes(agent_kid), str(topic)), [])
|
|
240
|
+
if since_ms is None:
|
|
241
|
+
return list(rows)
|
|
242
|
+
return [r for r in rows if r.ts_wall_ms >= since_ms]
|
|
243
|
+
|
|
244
|
+
def mean_brier(
|
|
245
|
+
self,
|
|
246
|
+
agent_kid: bytes,
|
|
247
|
+
topic: str,
|
|
248
|
+
*,
|
|
249
|
+
since_ms: int | None = None,
|
|
250
|
+
) -> float | None:
|
|
251
|
+
rows = self.history(agent_kid, topic, since_ms=since_ms)
|
|
252
|
+
if not rows:
|
|
253
|
+
return None
|
|
254
|
+
return sum(r.brier_score for r in rows) / len(rows)
|
|
255
|
+
|
|
256
|
+
def reliability_diagram(
|
|
257
|
+
self,
|
|
258
|
+
agent_kid: bytes,
|
|
259
|
+
topic: str,
|
|
260
|
+
*,
|
|
261
|
+
n_bins: int = 10,
|
|
262
|
+
) -> list[ReliabilityBin]:
|
|
263
|
+
return reliability_diagram(
|
|
264
|
+
self.history(agent_kid, topic),
|
|
265
|
+
n_bins=n_bins,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def adjustment(self, agent_kid: bytes, topic: str) -> float:
|
|
269
|
+
"""The shrinkage multiplier for this agent/topic — applied at consult time.
|
|
270
|
+
|
|
271
|
+
Returns 1.0 when there is insufficient history to adjust.
|
|
272
|
+
"""
|
|
273
|
+
rows = self.history(agent_kid, topic)
|
|
274
|
+
if len(rows) < MIN_OBSERVATIONS:
|
|
275
|
+
return 1.0
|
|
276
|
+
m = sum(r.brier_score for r in rows) / len(rows)
|
|
277
|
+
return calibration_adjustment(m, len(rows))
|
|
278
|
+
|
|
279
|
+
def to_dict(self, agent_kid: bytes, topic: str) -> dict:
|
|
280
|
+
rows = self.history(agent_kid, topic)
|
|
281
|
+
return {
|
|
282
|
+
"agent_kid": bytes(agent_kid).hex(),
|
|
283
|
+
"topic": topic,
|
|
284
|
+
"n_observations": len(rows),
|
|
285
|
+
"mean_brier": self.mean_brier(agent_kid, topic),
|
|
286
|
+
"calibration_adjustment": self.adjustment(agent_kid, topic),
|
|
287
|
+
"reliability_diagram": [
|
|
288
|
+
b.to_dict() for b in self.reliability_diagram(agent_kid, topic)
|
|
289
|
+
],
|
|
290
|
+
"history": [r.to_dict() for r in rows[-50:]], # tail
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
__all__ = [
|
|
295
|
+
"ADJ_MAX",
|
|
296
|
+
"ADJ_MIN",
|
|
297
|
+
"MAX_HISTORY_PER_KEY",
|
|
298
|
+
"MIN_OBSERVATIONS",
|
|
299
|
+
"OVERCONFIDENT_BRIER",
|
|
300
|
+
"WELL_CALIBRATED_BRIER",
|
|
301
|
+
"BrierEntry",
|
|
302
|
+
"BrierScoreTracker",
|
|
303
|
+
"ReliabilityBin",
|
|
304
|
+
"brier_score",
|
|
305
|
+
"calibration_adjustment",
|
|
306
|
+
"reliability_diagram",
|
|
307
|
+
]
|