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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Cross-sim learning metrics for the SimLab.
|
|
2
|
+
|
|
3
|
+
Exposes three pure functions that power the "Learning" tab in SimDetail:
|
|
4
|
+
|
|
5
|
+
find_related_sims(topic) — sims on a related topic, ranked by Brier
|
|
6
|
+
compute_knowledge_reuse(sim_id) — how much prior knowledge this sim reused
|
|
7
|
+
get_persona_trajectory(persona) — Brier trajectory across sims for one persona
|
|
8
|
+
|
|
9
|
+
These are read-only; they never mutate any registry record.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .registry import list_sims, load_status, load_trace, sim_dir
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Text-similarity helpers
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_STOPWORDS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"a", "an", "the", "in", "on", "at", "to", "for", "of", "and", "or",
|
|
26
|
+
"but", "is", "are", "was", "were", "be", "been", "will", "would",
|
|
27
|
+
"could", "should", "may", "might", "do", "does", "did", "have",
|
|
28
|
+
"has", "had", "that", "this", "these", "those", "with", "by", "from",
|
|
29
|
+
"than", "more", "less", "very", "also", "as", "up", "about", "into",
|
|
30
|
+
"through", "between", "during", "can", "not", "if",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_TOKEN_RE = re.compile(r"[a-z0-9]+")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tokenize(text: str) -> set[str]:
|
|
38
|
+
tokens = _TOKEN_RE.findall(text.lower())
|
|
39
|
+
return {t for t in tokens if t not in _STOPWORDS and len(t) > 1}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _jaccard(a: set[str], b: set[str]) -> float:
|
|
43
|
+
if not a and not b:
|
|
44
|
+
return 1.0
|
|
45
|
+
union = a | b
|
|
46
|
+
if not union:
|
|
47
|
+
return 0.0
|
|
48
|
+
return len(a & b) / len(union)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _extract_persona_slug(agent_name: str) -> str:
|
|
52
|
+
"""Strip trailing replica suffix ('-01', '-02', ...) from an agent name."""
|
|
53
|
+
return re.sub(r"-\d+$", "", agent_name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# find_related_sims
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_related_sims(topic: str, *, limit: int = 5) -> list[dict]:
|
|
62
|
+
"""Return up to *limit* completed sims on a related topic.
|
|
63
|
+
|
|
64
|
+
Exact-match sims are ranked before partial-match. Within each group
|
|
65
|
+
results are ordered by mean_brier ascending (best calibrated first).
|
|
66
|
+
Sims with no oracle resolutions are included but ranked last.
|
|
67
|
+
"""
|
|
68
|
+
if not topic or not topic.strip():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
topic_norm = topic.strip()
|
|
72
|
+
topic_tokens = _tokenize(topic_norm)
|
|
73
|
+
|
|
74
|
+
results: list[dict] = []
|
|
75
|
+
for sim in list_sims(limit=2000):
|
|
76
|
+
if sim.state != "done":
|
|
77
|
+
continue
|
|
78
|
+
sim_topic = (sim.topic or "").strip()
|
|
79
|
+
if not sim_topic:
|
|
80
|
+
# Fall back to the topic stored in trace.json
|
|
81
|
+
trace = load_trace(sim.sim_id)
|
|
82
|
+
if trace:
|
|
83
|
+
sim_topic = (trace.get("topic") or "").strip()
|
|
84
|
+
if not sim_topic:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Jaccard similarity
|
|
88
|
+
sim_tokens = _tokenize(sim_topic)
|
|
89
|
+
j = _jaccard(topic_tokens, sim_tokens)
|
|
90
|
+
if j < 0.15:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
status = load_status(sim.sim_id)
|
|
94
|
+
mean_brier = status.get("mean_brier") # set by _handle_resolve_outcomes
|
|
95
|
+
n_resolved = status.get("n_resolved", 0)
|
|
96
|
+
|
|
97
|
+
results.append(
|
|
98
|
+
{
|
|
99
|
+
"sim_id": sim.sim_id,
|
|
100
|
+
"topic": sim_topic,
|
|
101
|
+
"similarity": round(j, 4),
|
|
102
|
+
"exact_match": sim_topic.lower() == topic_norm.lower(),
|
|
103
|
+
"mean_brier": mean_brier,
|
|
104
|
+
"n_resolved": n_resolved,
|
|
105
|
+
"n_questions": sim.n_questions,
|
|
106
|
+
"n_agents": sim.n_agents,
|
|
107
|
+
"finished_at": sim.finished_at,
|
|
108
|
+
"model": sim.model,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Sort: exact match first, then by similarity desc, then by brier asc
|
|
113
|
+
# (None brier → rank after real Brier scores)
|
|
114
|
+
def _sort_key(r: dict) -> tuple:
|
|
115
|
+
exact = 0 if r["exact_match"] else 1
|
|
116
|
+
brier = r["mean_brier"] if r["mean_brier"] is not None else 9999.0
|
|
117
|
+
return (exact, -r["similarity"], brier)
|
|
118
|
+
|
|
119
|
+
results.sort(key=_sort_key)
|
|
120
|
+
return results[:limit]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# compute_knowledge_reuse
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
_TOP_K = 5 # number of top-Brier personas selected when reuse kicks in
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def compute_knowledge_reuse(sim_id: str) -> dict | None:
|
|
131
|
+
"""Compute cross-sim learning stats for a completed sim.
|
|
132
|
+
|
|
133
|
+
Returns None if the sim directory doesn't exist or is not done.
|
|
134
|
+
Returns a dict with:
|
|
135
|
+
prior_sims_used int — number of related prior sims found
|
|
136
|
+
claims_available_from_prior int — total claims in those sims
|
|
137
|
+
n_personas int — persona replicas in this sim
|
|
138
|
+
n_questions int — questions in this sim
|
|
139
|
+
delib_rounds int — deliberation rounds (>=1)
|
|
140
|
+
actual_llm_calls int — estimated actual LLM calls
|
|
141
|
+
optimized_llm_calls int — estimated calls if TOP_K selection used
|
|
142
|
+
calls_saved_potential int — difference
|
|
143
|
+
pct_reduction float — fraction saved (0-1)
|
|
144
|
+
related_sims list — from find_related_sims (up to 5)
|
|
145
|
+
"""
|
|
146
|
+
if not sim_dir(sim_id).exists():
|
|
147
|
+
return None
|
|
148
|
+
status = load_status(sim_id)
|
|
149
|
+
if status.get("state") != "done":
|
|
150
|
+
return None
|
|
151
|
+
trace = load_trace(sim_id)
|
|
152
|
+
if trace is None:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
topic = trace.get("topic", "")
|
|
156
|
+
n_agents: int = trace.get("n_agents") or 0
|
|
157
|
+
n_questions: int = trace.get("n_questions") or len(trace.get("panel_records") or [])
|
|
158
|
+
delib_rounds: int = trace.get("n_deliberation_rounds") or 1
|
|
159
|
+
|
|
160
|
+
# Actual LLM calls: each agent answers each question once per round
|
|
161
|
+
actual_llm_calls = n_agents * n_questions * max(1, delib_rounds)
|
|
162
|
+
|
|
163
|
+
# If we had selected only TOP_K agents based on prior Brier, how many calls?
|
|
164
|
+
top_k = min(_TOP_K, n_agents)
|
|
165
|
+
optimized_llm_calls = top_k * n_questions * max(1, delib_rounds)
|
|
166
|
+
calls_saved_potential = actual_llm_calls - optimized_llm_calls
|
|
167
|
+
pct_reduction = calls_saved_potential / actual_llm_calls if actual_llm_calls > 0 else 0.0
|
|
168
|
+
|
|
169
|
+
# Find related sims (exclude self)
|
|
170
|
+
related = [r for r in find_related_sims(topic, limit=6) if r["sim_id"] != sim_id][:5]
|
|
171
|
+
claims_available = 0
|
|
172
|
+
for rel in related:
|
|
173
|
+
rel_trace = load_trace(rel["sim_id"])
|
|
174
|
+
if rel_trace:
|
|
175
|
+
for pr in rel_trace.get("panel_records") or []:
|
|
176
|
+
claims_available += len(pr.get("claims") or [])
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"prior_sims_used": len(related),
|
|
180
|
+
"claims_available_from_prior": claims_available,
|
|
181
|
+
"n_personas": n_agents,
|
|
182
|
+
"n_questions": n_questions,
|
|
183
|
+
"delib_rounds": delib_rounds,
|
|
184
|
+
"actual_llm_calls": actual_llm_calls,
|
|
185
|
+
"optimized_llm_calls": optimized_llm_calls,
|
|
186
|
+
"calls_saved_potential": calls_saved_potential,
|
|
187
|
+
"pct_reduction": round(pct_reduction, 4),
|
|
188
|
+
"related_sims": related,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# get_persona_trajectory
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_persona_trajectory(persona: str) -> dict:
|
|
198
|
+
"""Return the Brier trajectory for a persona slug across all completed sims.
|
|
199
|
+
|
|
200
|
+
The trajectory is a list of per-sim observations, sorted by sim finish time.
|
|
201
|
+
Each observation includes the mean Brier for that persona across all agents
|
|
202
|
+
whose name matches the slug (after stripping the replica suffix).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
{
|
|
206
|
+
"persona": str,
|
|
207
|
+
"n_sims": int,
|
|
208
|
+
"mean_brier_all": float | None, # lifetime mean across observations
|
|
209
|
+
"trajectory": [
|
|
210
|
+
{
|
|
211
|
+
"sim_id": str,
|
|
212
|
+
"topic": str,
|
|
213
|
+
"sim_n": int, # sequential run number (1-based)
|
|
214
|
+
"mean_brier": float,
|
|
215
|
+
"n_resolved": int,
|
|
216
|
+
"finished_at": int | None,
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
"""
|
|
221
|
+
observations: list[dict] = []
|
|
222
|
+
for sim in list_sims(limit=2000):
|
|
223
|
+
if sim.state != "done":
|
|
224
|
+
continue
|
|
225
|
+
trace = load_trace(sim.sim_id)
|
|
226
|
+
if trace is None:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Look at agent_brier_scores stored on trace
|
|
230
|
+
abs_scores: dict[str, Any] = trace.get("agent_brier_scores") or {}
|
|
231
|
+
persona_briers: list[float] = []
|
|
232
|
+
persona_n: int = 0
|
|
233
|
+
|
|
234
|
+
for agent_name, scores in abs_scores.items():
|
|
235
|
+
slug = _extract_persona_slug(agent_name)
|
|
236
|
+
if slug != persona:
|
|
237
|
+
continue
|
|
238
|
+
mb = scores.get("mean_brier")
|
|
239
|
+
n = scores.get("n", 0)
|
|
240
|
+
if mb is not None and n > 0:
|
|
241
|
+
persona_briers.append(mb)
|
|
242
|
+
persona_n += n
|
|
243
|
+
|
|
244
|
+
# Also scan per_agent columns in panel_records if no abs_scores yet
|
|
245
|
+
if not persona_briers:
|
|
246
|
+
panel_records = trace.get("panel_records") or []
|
|
247
|
+
resolved_panels = [pr for pr in panel_records if pr.get("ground_truth") is not None]
|
|
248
|
+
for pr in resolved_panels:
|
|
249
|
+
gt = float(pr.get("ground_truth", 0))
|
|
250
|
+
for claim in pr.get("claims") or []:
|
|
251
|
+
slug = _extract_persona_slug(claim.get("agent", ""))
|
|
252
|
+
if slug != persona:
|
|
253
|
+
continue
|
|
254
|
+
conf = claim.get("confidence")
|
|
255
|
+
if conf is not None:
|
|
256
|
+
try:
|
|
257
|
+
brier = (float(conf) - gt) ** 2
|
|
258
|
+
persona_briers.append(brier)
|
|
259
|
+
persona_n += 1
|
|
260
|
+
except (TypeError, ValueError):
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
if not persona_briers:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
mean_brier = sum(persona_briers) / len(persona_briers)
|
|
267
|
+
observations.append(
|
|
268
|
+
{
|
|
269
|
+
"sim_id": sim.sim_id,
|
|
270
|
+
"topic": sim.topic or "",
|
|
271
|
+
"mean_brier": round(mean_brier, 6),
|
|
272
|
+
"n_resolved": persona_n,
|
|
273
|
+
"finished_at": sim.finished_at,
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Sort chronologically
|
|
278
|
+
observations.sort(key=lambda o: (o["finished_at"] or 0))
|
|
279
|
+
|
|
280
|
+
# Assign sequential run numbers
|
|
281
|
+
for i, obs in enumerate(observations, 1):
|
|
282
|
+
obs["sim_n"] = i
|
|
283
|
+
|
|
284
|
+
# Lifetime mean
|
|
285
|
+
if observations:
|
|
286
|
+
mean_brier_all = sum(o["mean_brier"] for o in observations) / len(observations)
|
|
287
|
+
else:
|
|
288
|
+
mean_brier_all = None
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"persona": persona,
|
|
292
|
+
"n_sims": len(observations),
|
|
293
|
+
"mean_brier_all": round(mean_brier_all, 6) if mean_brier_all is not None else None,
|
|
294
|
+
"trajectory": observations,
|
|
295
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Deliberation-mode registry for SimLab.
|
|
2
|
+
|
|
3
|
+
A *mode* selects the deliberation shape and result schema for a sim run.
|
|
4
|
+
The default mode is ``binary-forecast`` — the legacy persona-panel shape
|
|
5
|
+
(consensus probability vs. ``ground_truth ∈ [0,1]``). Additional modes
|
|
6
|
+
(``governance-committee``, ``what-if-tree``, ...) plug in here without
|
|
7
|
+
touching the engine entry point in :mod:`hypermind.simlab.scenario`.
|
|
8
|
+
|
|
9
|
+
Each mode is a :class:`ModeSpec` with:
|
|
10
|
+
|
|
11
|
+
* ``slug`` — stable identifier used by ``SimConfig.mode``.
|
|
12
|
+
* ``label`` — human-readable name for the SPA dropdown.
|
|
13
|
+
* ``input_schema`` — JSON schema (draft-2020-12 subset) describing the
|
|
14
|
+
fields the user submits via ``SimConfig.mode_payload``. ``None`` means
|
|
15
|
+
the mode reuses the legacy SimConfig fields directly.
|
|
16
|
+
* ``result_schema`` — JSON schema for the trace's mode-specific output.
|
|
17
|
+
* ``run_fn`` — async callable ``(args, recorder) -> None`` that
|
|
18
|
+
matches the signature of the legacy ``_scenario_impl.main``. The mode
|
|
19
|
+
is responsible for writing ``trace.json`` to ``args.trace``.
|
|
20
|
+
|
|
21
|
+
Public API:
|
|
22
|
+
|
|
23
|
+
register(spec) — register a mode (idempotent on slug)
|
|
24
|
+
get_mode(slug) -> ModeSpec — lookup; raises KeyError if unknown
|
|
25
|
+
list_modes() -> list[ModeSpec]
|
|
26
|
+
is_registered_mode(slug) -> bool
|
|
27
|
+
|
|
28
|
+
The default ``binary-forecast`` mode is registered at import time.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Awaitable, Callable
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
# Run signature: (args: SimpleNamespace-like, recorder: Recorder | None) -> None.
|
|
38
|
+
# Kept loose so modes can accept additional kwargs without touching this type.
|
|
39
|
+
ModeRunFn = Callable[..., Awaitable[None]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ModeSpec:
|
|
44
|
+
slug: str
|
|
45
|
+
label: str
|
|
46
|
+
description: str
|
|
47
|
+
input_schema: dict[str, Any] | None
|
|
48
|
+
result_schema: dict[str, Any] | None
|
|
49
|
+
run_fn: ModeRunFn
|
|
50
|
+
# Whether this mode reuses the legacy SimConfig fields (personas,
|
|
51
|
+
# questions, n_questions, ...) for its inputs. ``False`` means the
|
|
52
|
+
# mode reads only ``mode_payload``. The schema-driven SimNew form
|
|
53
|
+
# uses this to decide whether to render legacy controls.
|
|
54
|
+
uses_legacy_fields: bool = True
|
|
55
|
+
# Optional list of role slugs the mode expects to be assignable.
|
|
56
|
+
expected_roles: list[str] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"slug": self.slug,
|
|
61
|
+
"label": self.label,
|
|
62
|
+
"description": self.description,
|
|
63
|
+
"input_schema": self.input_schema,
|
|
64
|
+
"result_schema": self.result_schema,
|
|
65
|
+
"uses_legacy_fields": self.uses_legacy_fields,
|
|
66
|
+
"expected_roles": list(self.expected_roles),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_REGISTRY: dict[str, ModeSpec] = {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def register(spec: ModeSpec) -> None:
|
|
74
|
+
"""Register a mode. Re-registering the same slug overwrites — useful
|
|
75
|
+
in tests but should not happen in production code paths."""
|
|
76
|
+
_REGISTRY[spec.slug] = spec
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_mode(slug: str) -> ModeSpec:
|
|
80
|
+
if slug not in _REGISTRY:
|
|
81
|
+
raise KeyError(f"unknown mode {slug!r}; registered: {sorted(_REGISTRY.keys())}")
|
|
82
|
+
return _REGISTRY[slug]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def list_modes() -> list[ModeSpec]:
|
|
86
|
+
return sorted(_REGISTRY.values(), key=lambda s: s.slug)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_registered_mode(slug: str) -> bool:
|
|
90
|
+
return slug in _REGISTRY
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- Default registrations -------------------------------------------------
|
|
94
|
+
# Imported for side effect; each module calls ``register(...)`` at import.
|
|
95
|
+
from . import ( # noqa: E402
|
|
96
|
+
aar, # noqa: F401
|
|
97
|
+
backtest, # noqa: F401
|
|
98
|
+
binary_forecast, # noqa: F401
|
|
99
|
+
conformance, # noqa: F401
|
|
100
|
+
evaluation, # noqa: F401
|
|
101
|
+
governance, # noqa: F401
|
|
102
|
+
redteam, # noqa: F401
|
|
103
|
+
tabletop, # noqa: F401
|
|
104
|
+
tournament, # noqa: F401
|
|
105
|
+
whatif, # noqa: F401
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
"ModeRunFn",
|
|
110
|
+
"ModeSpec",
|
|
111
|
+
"get_mode",
|
|
112
|
+
"is_registered_mode",
|
|
113
|
+
"list_modes",
|
|
114
|
+
"register",
|
|
115
|
+
]
|