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,716 @@
|
|
|
1
|
+
"""Knowledge — federation memory aggregated across all sims.
|
|
2
|
+
|
|
3
|
+
Every sim under ``~/.hypermind/sims/`` produces durable, signed artefacts:
|
|
4
|
+
claims, citations, disputes, syntheses, vouches, oracle resolutions,
|
|
5
|
+
reputation snapshots. This module walks the registry, parses every
|
|
6
|
+
trace.json, and builds an in-memory index that the UI can drill into.
|
|
7
|
+
|
|
8
|
+
Public API:
|
|
9
|
+
|
|
10
|
+
build_knowledge_index() -> KnowledgeIndex
|
|
11
|
+
KnowledgeIndex.overview() → high-level KPIs
|
|
12
|
+
KnowledgeIndex.list_claims(…) → paginated, filterable
|
|
13
|
+
KnowledgeIndex.list_agents() → federation roster (cumulative stats)
|
|
14
|
+
KnowledgeIndex.list_topics() → category clusters
|
|
15
|
+
KnowledgeIndex.claim(kid) → full provenance (citations + disputes
|
|
16
|
+
+ source sim + outcome if resolved)
|
|
17
|
+
|
|
18
|
+
The index is rebuilt lazily — call ``invalidate()`` after a sim
|
|
19
|
+
finishes to force a re-scan on the next read.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import threading
|
|
26
|
+
from collections import defaultdict
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from .registry import sims_dir
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Record shapes (compact — full trace stays on disk)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ClaimRecord:
|
|
39
|
+
kid: str
|
|
40
|
+
agent: str
|
|
41
|
+
persona: str
|
|
42
|
+
query: str
|
|
43
|
+
confidence: float
|
|
44
|
+
ts_wall_ms: int
|
|
45
|
+
sim_id: str
|
|
46
|
+
citations_received: int = 0
|
|
47
|
+
disputes_received: int = 0
|
|
48
|
+
resolved_outcome: bool | None = None # None = not yet resolved
|
|
49
|
+
ground_truth: float | None = None
|
|
50
|
+
category: str = "general" # filled from question's category if available
|
|
51
|
+
reasoning: str = "" # the agent's freetext reasoning (when available)
|
|
52
|
+
sim_topic: str = "" # the sim's topic (when sim was topic-driven)
|
|
53
|
+
panel_mean: float | None = None # the panel's aggregate mean for this Q
|
|
54
|
+
panel_spread: float | None = None # spread σ for this Q
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"kid": self.kid,
|
|
59
|
+
"agent": self.agent,
|
|
60
|
+
"persona": self.persona,
|
|
61
|
+
"query": self.query,
|
|
62
|
+
"confidence": self.confidence,
|
|
63
|
+
"ts_wall_ms": self.ts_wall_ms,
|
|
64
|
+
"sim_id": self.sim_id,
|
|
65
|
+
"citations_received": self.citations_received,
|
|
66
|
+
"disputes_received": self.disputes_received,
|
|
67
|
+
"resolved_outcome": self.resolved_outcome,
|
|
68
|
+
"ground_truth": self.ground_truth,
|
|
69
|
+
"category": self.category,
|
|
70
|
+
"reasoning": self.reasoning,
|
|
71
|
+
"sim_topic": self.sim_topic,
|
|
72
|
+
"panel_mean": self.panel_mean,
|
|
73
|
+
"panel_spread": self.panel_spread,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AgentAggregate:
|
|
79
|
+
name: str # persona-replica id, e.g. "optimist-economist-01"
|
|
80
|
+
persona: str
|
|
81
|
+
runs_participated: int = 0
|
|
82
|
+
claims_published: int = 0
|
|
83
|
+
citations_received: int = 0
|
|
84
|
+
disputes_received: int = 0
|
|
85
|
+
citations_made: int = 0
|
|
86
|
+
disputes_made: int = 0
|
|
87
|
+
vouches_received: int = 0
|
|
88
|
+
vouches_given: int = 0
|
|
89
|
+
oracle_correct: int = 0
|
|
90
|
+
oracle_total: int = 0
|
|
91
|
+
last_reputation: float | None = None
|
|
92
|
+
last_seen_ts_ms: int = 0
|
|
93
|
+
sims: set[str] = field(default_factory=set)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def accuracy(self) -> float | None:
|
|
97
|
+
return self.oracle_correct / self.oracle_total if self.oracle_total else None
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
return {
|
|
101
|
+
"name": self.name,
|
|
102
|
+
"persona": self.persona,
|
|
103
|
+
"runs_participated": self.runs_participated,
|
|
104
|
+
"claims_published": self.claims_published,
|
|
105
|
+
"citations_received": self.citations_received,
|
|
106
|
+
"disputes_received": self.disputes_received,
|
|
107
|
+
"citations_made": self.citations_made,
|
|
108
|
+
"disputes_made": self.disputes_made,
|
|
109
|
+
"vouches_received": self.vouches_received,
|
|
110
|
+
"vouches_given": self.vouches_given,
|
|
111
|
+
"oracle_correct": self.oracle_correct,
|
|
112
|
+
"oracle_total": self.oracle_total,
|
|
113
|
+
"accuracy": self.accuracy,
|
|
114
|
+
"last_reputation": self.last_reputation,
|
|
115
|
+
"last_seen_ts_ms": self.last_seen_ts_ms,
|
|
116
|
+
"sims": sorted(self.sims),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class TopicAggregate:
|
|
122
|
+
topic: str # category slug (e.g. "regulation", "growth", "primary")
|
|
123
|
+
claim_count: int = 0
|
|
124
|
+
citation_count: int = 0
|
|
125
|
+
dispute_count: int = 0
|
|
126
|
+
mean_confidence_sum: float = 0.0 # accumulators; finalize on render
|
|
127
|
+
confidence_n: int = 0
|
|
128
|
+
sims: set[str] = field(default_factory=set)
|
|
129
|
+
sample_queries: list[str] = field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def mean_confidence(self) -> float | None:
|
|
133
|
+
return self.mean_confidence_sum / self.confidence_n if self.confidence_n else None
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def contested_ratio(self) -> float:
|
|
137
|
+
return self.dispute_count / self.claim_count if self.claim_count else 0.0
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> dict[str, Any]:
|
|
140
|
+
return {
|
|
141
|
+
"topic": self.topic,
|
|
142
|
+
"claim_count": self.claim_count,
|
|
143
|
+
"citation_count": self.citation_count,
|
|
144
|
+
"dispute_count": self.dispute_count,
|
|
145
|
+
"mean_confidence": self.mean_confidence,
|
|
146
|
+
"contested_ratio": self.contested_ratio,
|
|
147
|
+
"n_sims": len(self.sims),
|
|
148
|
+
"sample_queries": self.sample_queries[:5],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class KnowledgeIndex:
|
|
154
|
+
claims: dict[str, ClaimRecord] = field(default_factory=dict)
|
|
155
|
+
agents: dict[str, AgentAggregate] = field(default_factory=dict)
|
|
156
|
+
topics: dict[str, TopicAggregate] = field(default_factory=dict)
|
|
157
|
+
# kid → list of citations/disputes against it (built once)
|
|
158
|
+
citations_to: dict[str, list[dict[str, Any]]] = field(default_factory=lambda: defaultdict(list))
|
|
159
|
+
disputes_to: dict[str, list[dict[str, Any]]] = field(default_factory=lambda: defaultdict(list))
|
|
160
|
+
n_sims: int = 0
|
|
161
|
+
last_built_ts_ms: int = 0
|
|
162
|
+
|
|
163
|
+
# ---- summaries ----
|
|
164
|
+
def overview(self) -> dict[str, Any]:
|
|
165
|
+
total_citations = sum(c.citations_received for c in self.claims.values())
|
|
166
|
+
total_disputes = sum(c.disputes_received for c in self.claims.values())
|
|
167
|
+
resolved = [c for c in self.claims.values() if c.resolved_outcome is not None]
|
|
168
|
+
accuracy = (
|
|
169
|
+
sum(1 for c in resolved if (c.confidence > 0.5) == c.resolved_outcome) / len(resolved)
|
|
170
|
+
if resolved
|
|
171
|
+
else None
|
|
172
|
+
)
|
|
173
|
+
# Top contributors
|
|
174
|
+
top_agents = sorted(self.agents.values(), key=lambda a: a.claims_published, reverse=True)[
|
|
175
|
+
:8
|
|
176
|
+
]
|
|
177
|
+
top_topics = sorted(self.topics.values(), key=lambda t: t.claim_count, reverse=True)[:8]
|
|
178
|
+
return {
|
|
179
|
+
"n_sims": self.n_sims,
|
|
180
|
+
"n_agents": len(self.agents),
|
|
181
|
+
"n_claims": len(self.claims),
|
|
182
|
+
"n_resolved": len(resolved),
|
|
183
|
+
"total_citations": total_citations,
|
|
184
|
+
"total_disputes": total_disputes,
|
|
185
|
+
"n_topics": len(self.topics),
|
|
186
|
+
"panel_accuracy": accuracy,
|
|
187
|
+
"last_built_ts_ms": self.last_built_ts_ms,
|
|
188
|
+
"top_agents": [a.to_dict() for a in top_agents],
|
|
189
|
+
"top_topics": [t.to_dict() for t in top_topics],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def list_claims(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
q: str = "",
|
|
196
|
+
persona: str = "",
|
|
197
|
+
topic: str = "",
|
|
198
|
+
sim_id: str = "",
|
|
199
|
+
limit: int = 100,
|
|
200
|
+
offset: int = 0,
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
items = list(self.claims.values())
|
|
203
|
+
if q:
|
|
204
|
+
ql = q.lower()
|
|
205
|
+
items = [c for c in items if ql in c.query.lower() or ql in c.kid.lower()]
|
|
206
|
+
if persona:
|
|
207
|
+
items = [c for c in items if c.persona == persona]
|
|
208
|
+
if topic:
|
|
209
|
+
items = [c for c in items if c.category == topic]
|
|
210
|
+
if sim_id:
|
|
211
|
+
items = [c for c in items if c.sim_id == sim_id]
|
|
212
|
+
items.sort(key=lambda c: c.ts_wall_ms, reverse=True)
|
|
213
|
+
total = len(items)
|
|
214
|
+
page = items[offset : offset + limit]
|
|
215
|
+
return {
|
|
216
|
+
"total": total,
|
|
217
|
+
"offset": offset,
|
|
218
|
+
"limit": limit,
|
|
219
|
+
"items": [c.to_dict() for c in page],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def list_agents(self) -> list[dict[str, Any]]:
|
|
223
|
+
return [
|
|
224
|
+
a.to_dict()
|
|
225
|
+
for a in sorted(
|
|
226
|
+
self.agents.values(),
|
|
227
|
+
key=lambda x: x.claims_published,
|
|
228
|
+
reverse=True,
|
|
229
|
+
)
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
def list_topics(self) -> list[dict[str, Any]]:
|
|
233
|
+
return [
|
|
234
|
+
t.to_dict()
|
|
235
|
+
for t in sorted(
|
|
236
|
+
self.topics.values(),
|
|
237
|
+
key=lambda x: x.claim_count,
|
|
238
|
+
reverse=True,
|
|
239
|
+
)
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
def ask(self, query: str, *, top_k: int = 20, min_overlap: int = 1) -> dict[str, Any]:
|
|
243
|
+
"""Search across all claims with text similar to `query` and
|
|
244
|
+
compute an aggregated verdict. Plain word-overlap scoring — no
|
|
245
|
+
embeddings, no LLM round-trip. Cheap, deterministic, on-disk only.
|
|
246
|
+
|
|
247
|
+
Returns a payload the UI's "Ask the federation" tab renders:
|
|
248
|
+
{
|
|
249
|
+
query: str,
|
|
250
|
+
n_matched: int,
|
|
251
|
+
mean_confidence: float | None,
|
|
252
|
+
verdict_label: str,
|
|
253
|
+
agreement: str,
|
|
254
|
+
top_claims: [ClaimRecord.to_dict() ...], # most-relevant
|
|
255
|
+
sim_ids: [str ...], # contributing sims
|
|
256
|
+
personas: {persona: count, ...}, # who weighed in
|
|
257
|
+
}
|
|
258
|
+
"""
|
|
259
|
+
if not query.strip():
|
|
260
|
+
return {
|
|
261
|
+
"query": query,
|
|
262
|
+
"n_matched": 0,
|
|
263
|
+
"items": [],
|
|
264
|
+
"sim_ids": [],
|
|
265
|
+
"mean_confidence": None,
|
|
266
|
+
"verdict_label": "",
|
|
267
|
+
"agreement": "",
|
|
268
|
+
"personas": {},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Simple bag-of-words overlap. Lowercase, drop punctuation,
|
|
272
|
+
# drop tiny stopwords. Score = number of overlapping content words.
|
|
273
|
+
STOP = {
|
|
274
|
+
"the",
|
|
275
|
+
"a",
|
|
276
|
+
"an",
|
|
277
|
+
"of",
|
|
278
|
+
"to",
|
|
279
|
+
"in",
|
|
280
|
+
"on",
|
|
281
|
+
"is",
|
|
282
|
+
"are",
|
|
283
|
+
"be",
|
|
284
|
+
"will",
|
|
285
|
+
"and",
|
|
286
|
+
"or",
|
|
287
|
+
"for",
|
|
288
|
+
"with",
|
|
289
|
+
"vs",
|
|
290
|
+
"as",
|
|
291
|
+
"by",
|
|
292
|
+
"this",
|
|
293
|
+
"that",
|
|
294
|
+
"do",
|
|
295
|
+
"does",
|
|
296
|
+
"did",
|
|
297
|
+
"would",
|
|
298
|
+
"could",
|
|
299
|
+
"should",
|
|
300
|
+
"have",
|
|
301
|
+
"has",
|
|
302
|
+
"had",
|
|
303
|
+
"from",
|
|
304
|
+
"at",
|
|
305
|
+
"if",
|
|
306
|
+
"than",
|
|
307
|
+
"what",
|
|
308
|
+
"when",
|
|
309
|
+
"who",
|
|
310
|
+
"how",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def tokenize(text: str) -> set[str]:
|
|
314
|
+
t = ""
|
|
315
|
+
for ch in text.lower():
|
|
316
|
+
t += ch if ch.isalnum() else " "
|
|
317
|
+
return {w for w in t.split() if len(w) > 2 and w not in STOP}
|
|
318
|
+
|
|
319
|
+
q_words = tokenize(query)
|
|
320
|
+
if not q_words:
|
|
321
|
+
return {
|
|
322
|
+
"query": query,
|
|
323
|
+
"n_matched": 0,
|
|
324
|
+
"items": [],
|
|
325
|
+
"sim_ids": [],
|
|
326
|
+
"mean_confidence": None,
|
|
327
|
+
"verdict_label": "",
|
|
328
|
+
"agreement": "",
|
|
329
|
+
"personas": {},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
scored: list[tuple[int, ClaimRecord]] = []
|
|
333
|
+
for c in self.claims.values():
|
|
334
|
+
cw = tokenize(c.query)
|
|
335
|
+
overlap = len(q_words & cw)
|
|
336
|
+
if overlap >= min_overlap:
|
|
337
|
+
scored.append((overlap, c))
|
|
338
|
+
|
|
339
|
+
scored.sort(key=lambda x: (-x[0], -x[1].ts_wall_ms))
|
|
340
|
+
matched = [c for _, c in scored[:top_k]]
|
|
341
|
+
if not matched:
|
|
342
|
+
return {
|
|
343
|
+
"query": query,
|
|
344
|
+
"n_matched": 0,
|
|
345
|
+
"items": [],
|
|
346
|
+
"sim_ids": [],
|
|
347
|
+
"mean_confidence": None,
|
|
348
|
+
"verdict_label": "NO MATCH",
|
|
349
|
+
"agreement": "no prior claims found",
|
|
350
|
+
"personas": {},
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Aggregate: confidence-weighted mean (don't double-count
|
|
354
|
+
# identical claim copies from replicas — group by query).
|
|
355
|
+
confidences = [c.confidence for c in matched]
|
|
356
|
+
mean = sum(confidences) / len(confidences)
|
|
357
|
+
# Spread = max - min as a quick "agreement" proxy
|
|
358
|
+
spread = (max(confidences) - min(confidences)) if confidences else 0.0
|
|
359
|
+
|
|
360
|
+
if mean >= 0.7:
|
|
361
|
+
label, agreement = "YES", "strong agreement"
|
|
362
|
+
elif mean >= 0.55:
|
|
363
|
+
label, agreement = "LEAN YES", "leans yes"
|
|
364
|
+
elif mean > 0.45:
|
|
365
|
+
label, agreement = "SPLIT", "no clear consensus"
|
|
366
|
+
elif mean > 0.3:
|
|
367
|
+
label, agreement = "LEAN NO", "leans no"
|
|
368
|
+
else:
|
|
369
|
+
label, agreement = "NO", "strong disagreement"
|
|
370
|
+
|
|
371
|
+
if spread < 0.15:
|
|
372
|
+
agreement = "strong consensus"
|
|
373
|
+
elif spread > 0.35:
|
|
374
|
+
agreement = "sharp disagreement"
|
|
375
|
+
|
|
376
|
+
personas: dict[str, int] = {}
|
|
377
|
+
sims: dict[str, int] = {}
|
|
378
|
+
for c in matched:
|
|
379
|
+
personas[c.persona] = personas.get(c.persona, 0) + 1
|
|
380
|
+
sims[c.sim_id] = sims.get(c.sim_id, 0) + 1
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
"query": query,
|
|
384
|
+
"n_matched": len(scored),
|
|
385
|
+
"items": [c.to_dict() for c in matched],
|
|
386
|
+
"sim_ids": list(sims.keys()),
|
|
387
|
+
"mean_confidence": mean,
|
|
388
|
+
"spread": spread,
|
|
389
|
+
"verdict_label": label,
|
|
390
|
+
"agreement": agreement,
|
|
391
|
+
"personas": personas,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
def claim(self, kid: str) -> dict[str, Any] | None:
|
|
395
|
+
c = self.claims.get(kid)
|
|
396
|
+
if c is None:
|
|
397
|
+
# Allow partial-prefix lookup since UI shows truncated kids
|
|
398
|
+
for k, rec in self.claims.items():
|
|
399
|
+
if k.startswith(kid):
|
|
400
|
+
c = rec
|
|
401
|
+
break
|
|
402
|
+
if c is None:
|
|
403
|
+
return None
|
|
404
|
+
return {
|
|
405
|
+
"claim": c.to_dict(),
|
|
406
|
+
"citations": self.citations_to.get(c.kid, []),
|
|
407
|
+
"disputes": self.disputes_to.get(c.kid, []),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# Builder
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _category_for_query(query: str, questions: list[dict]) -> str:
|
|
417
|
+
"""Look up the question's category by query text (since claims only
|
|
418
|
+
carry the query, not the category)."""
|
|
419
|
+
for q in questions:
|
|
420
|
+
if q.get("query") == query:
|
|
421
|
+
return str(q.get("category", "general"))
|
|
422
|
+
return "general"
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _ingest_trace(idx: KnowledgeIndex, sim_id: str, trace: dict[str, Any]) -> None:
|
|
426
|
+
"""Pull a single trace.json into the index. Side-effect: mutates idx."""
|
|
427
|
+
questions = trace.get("questions") or []
|
|
428
|
+
panel_records = trace.get("panel_records") or []
|
|
429
|
+
|
|
430
|
+
# Pre-build query→category lookup to avoid O(N×Q) linear scans
|
|
431
|
+
_query_to_cat: dict[str, str] = {
|
|
432
|
+
(q.get("query") or ""): str(q.get("category", "general"))
|
|
433
|
+
for q in questions
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Track which agents participated in this sim (for runs_participated)
|
|
437
|
+
sim_agents: set[str] = set()
|
|
438
|
+
|
|
439
|
+
# ---- Claims (from panel_records[].claims) ----
|
|
440
|
+
sim_topic = trace.get("topic") or ""
|
|
441
|
+
for pr in panel_records:
|
|
442
|
+
cat = _query_to_cat.get(pr.get("query") or "", "general")
|
|
443
|
+
# Build reasoning lookup: per_agent rows have name+reasoning;
|
|
444
|
+
# match to claims by agent name + query.
|
|
445
|
+
reasoning_by_agent: dict[str, str] = {}
|
|
446
|
+
for pa in pr.get("per_agent") or []:
|
|
447
|
+
name = pa.get("agent") or pa.get("name") or ""
|
|
448
|
+
r = pa.get("reasoning")
|
|
449
|
+
if name and r:
|
|
450
|
+
reasoning_by_agent[name] = r
|
|
451
|
+
panel_mean = pr.get("mean_confidence")
|
|
452
|
+
panel_spread = pr.get("spread")
|
|
453
|
+
for c in pr.get("claims") or []:
|
|
454
|
+
kid = c.get("kid")
|
|
455
|
+
if not kid:
|
|
456
|
+
continue
|
|
457
|
+
agent_name = c.get("agent", "")
|
|
458
|
+
rec = ClaimRecord(
|
|
459
|
+
kid=kid,
|
|
460
|
+
agent=agent_name,
|
|
461
|
+
persona=c.get("persona", ""),
|
|
462
|
+
query=c.get("query", ""),
|
|
463
|
+
confidence=float(c.get("confidence", 0.0)),
|
|
464
|
+
ts_wall_ms=int(c.get("ts_wall_ms", 0)),
|
|
465
|
+
sim_id=sim_id,
|
|
466
|
+
category=cat,
|
|
467
|
+
reasoning=reasoning_by_agent.get(agent_name, ""),
|
|
468
|
+
sim_topic=sim_topic,
|
|
469
|
+
panel_mean=panel_mean,
|
|
470
|
+
panel_spread=panel_spread,
|
|
471
|
+
)
|
|
472
|
+
idx.claims[kid] = rec
|
|
473
|
+
sim_agents.add(rec.agent)
|
|
474
|
+
# Touch agent aggregate
|
|
475
|
+
ag = idx.agents.setdefault(
|
|
476
|
+
rec.agent,
|
|
477
|
+
AgentAggregate(name=rec.agent, persona=rec.persona),
|
|
478
|
+
)
|
|
479
|
+
ag.persona = rec.persona # keep persona fresh
|
|
480
|
+
ag.claims_published += 1
|
|
481
|
+
ag.last_seen_ts_ms = max(ag.last_seen_ts_ms, rec.ts_wall_ms)
|
|
482
|
+
ag.sims.add(sim_id)
|
|
483
|
+
# Touch topic
|
|
484
|
+
tp = idx.topics.setdefault(cat, TopicAggregate(topic=cat))
|
|
485
|
+
tp.claim_count += 1
|
|
486
|
+
tp.mean_confidence_sum += rec.confidence
|
|
487
|
+
tp.confidence_n += 1
|
|
488
|
+
tp.sims.add(sim_id)
|
|
489
|
+
if pr.get("query") and pr["query"] not in tp.sample_queries:
|
|
490
|
+
tp.sample_queries.append(pr["query"])
|
|
491
|
+
|
|
492
|
+
# ---- Citations (panel_records[].citations) ----
|
|
493
|
+
for pr in panel_records:
|
|
494
|
+
cat = _query_to_cat.get(pr.get("query") or "", "general")
|
|
495
|
+
for c in pr.get("citations") or []:
|
|
496
|
+
target_kid = c.get("to_claim_kid")
|
|
497
|
+
from_agent = c.get("from_agent", "")
|
|
498
|
+
to_agent = c.get("to_agent", "")
|
|
499
|
+
sim_agents.add(from_agent)
|
|
500
|
+
sim_agents.add(to_agent)
|
|
501
|
+
entry = {
|
|
502
|
+
"sim_id": sim_id,
|
|
503
|
+
"from_agent": from_agent,
|
|
504
|
+
"from_persona": c.get("from_persona", ""),
|
|
505
|
+
"to_agent": to_agent,
|
|
506
|
+
"to_persona": c.get("to_persona", ""),
|
|
507
|
+
"to_claim_kid": target_kid,
|
|
508
|
+
"their_conf": c.get("their_conf"),
|
|
509
|
+
"my_conf": c.get("my_conf"),
|
|
510
|
+
"delta": c.get("delta"),
|
|
511
|
+
}
|
|
512
|
+
if target_kid:
|
|
513
|
+
idx.citations_to[target_kid].append(entry)
|
|
514
|
+
if target_kid in idx.claims:
|
|
515
|
+
idx.claims[target_kid].citations_received += 1
|
|
516
|
+
if to_agent:
|
|
517
|
+
idx.agents.setdefault(
|
|
518
|
+
to_agent,
|
|
519
|
+
AgentAggregate(name=to_agent, persona=c.get("to_persona", "")),
|
|
520
|
+
).citations_received += 1
|
|
521
|
+
if from_agent:
|
|
522
|
+
idx.agents.setdefault(
|
|
523
|
+
from_agent,
|
|
524
|
+
AgentAggregate(name=from_agent, persona=c.get("from_persona", "")),
|
|
525
|
+
).citations_made += 1
|
|
526
|
+
tp = idx.topics.setdefault(cat, TopicAggregate(topic=cat))
|
|
527
|
+
tp.citation_count += 1
|
|
528
|
+
|
|
529
|
+
# ---- Disputes (panel_records[].disputes) ----
|
|
530
|
+
for pr in panel_records:
|
|
531
|
+
cat = _query_to_cat.get(pr.get("query") or "", "general")
|
|
532
|
+
for d in pr.get("disputes") or []:
|
|
533
|
+
target_kid = d.get("target_claim_kid") or d.get("to_claim_kid")
|
|
534
|
+
from_agent = d.get("from_agent") or d.get("disputer", "")
|
|
535
|
+
target_agent = d.get("target_agent") or d.get("to_agent", "")
|
|
536
|
+
sim_agents.add(from_agent)
|
|
537
|
+
entry = {
|
|
538
|
+
"sim_id": sim_id,
|
|
539
|
+
"from_agent": from_agent,
|
|
540
|
+
"from_persona": d.get("from_persona", ""),
|
|
541
|
+
"target_agent": target_agent,
|
|
542
|
+
"target_persona": d.get("target_persona") or d.get("to_persona", ""),
|
|
543
|
+
"target_claim_kid": target_kid,
|
|
544
|
+
"delta": d.get("delta"),
|
|
545
|
+
}
|
|
546
|
+
if target_kid:
|
|
547
|
+
idx.disputes_to[target_kid].append(entry)
|
|
548
|
+
if target_kid in idx.claims:
|
|
549
|
+
idx.claims[target_kid].disputes_received += 1
|
|
550
|
+
if target_agent:
|
|
551
|
+
idx.agents.setdefault(
|
|
552
|
+
target_agent,
|
|
553
|
+
AgentAggregate(name=target_agent, persona=d.get("target_persona") or ""),
|
|
554
|
+
).disputes_received += 1
|
|
555
|
+
if from_agent:
|
|
556
|
+
idx.agents.setdefault(
|
|
557
|
+
from_agent,
|
|
558
|
+
AgentAggregate(name=from_agent, persona=d.get("from_persona") or ""),
|
|
559
|
+
).disputes_made += 1
|
|
560
|
+
tp = idx.topics.setdefault(cat, TopicAggregate(topic=cat))
|
|
561
|
+
tp.dispute_count += 1
|
|
562
|
+
|
|
563
|
+
# ---- Vouches ----
|
|
564
|
+
for v in trace.get("vouches") or []:
|
|
565
|
+
voucher = v.get("voucher", "")
|
|
566
|
+
vouchee = v.get("vouchee", "")
|
|
567
|
+
if vouchee:
|
|
568
|
+
idx.agents.setdefault(
|
|
569
|
+
vouchee,
|
|
570
|
+
AgentAggregate(name=vouchee, persona=v.get("vouchee_persona", "")),
|
|
571
|
+
).vouches_received += 1
|
|
572
|
+
if voucher:
|
|
573
|
+
idx.agents.setdefault(
|
|
574
|
+
voucher,
|
|
575
|
+
AgentAggregate(name=voucher, persona=v.get("voucher_persona", "")),
|
|
576
|
+
).vouches_given += 1
|
|
577
|
+
|
|
578
|
+
# ---- Oracle resolutions: outcome + accuracy ----
|
|
579
|
+
for r in trace.get("oracle_resolutions") or []:
|
|
580
|
+
kid = r.get("resolved_kid")
|
|
581
|
+
outcome = r.get("outcome")
|
|
582
|
+
gt = r.get("ground_truth")
|
|
583
|
+
publisher = r.get("publisher", "")
|
|
584
|
+
predicted = r.get("predicted_confidence", 0.5)
|
|
585
|
+
if kid and kid in idx.claims:
|
|
586
|
+
idx.claims[kid].resolved_outcome = bool(outcome) if outcome is not None else None
|
|
587
|
+
if gt is not None:
|
|
588
|
+
idx.claims[kid].ground_truth = float(gt)
|
|
589
|
+
if publisher:
|
|
590
|
+
ag = idx.agents.setdefault(
|
|
591
|
+
publisher,
|
|
592
|
+
AgentAggregate(name=publisher, persona=r.get("publisher_persona", "")),
|
|
593
|
+
)
|
|
594
|
+
ag.oracle_total += 1
|
|
595
|
+
if outcome is not None and (predicted > 0.5) == bool(outcome):
|
|
596
|
+
ag.oracle_correct += 1
|
|
597
|
+
|
|
598
|
+
# ---- Reputation: pick the latest after_oracle score per agent ----
|
|
599
|
+
rs = trace.get("reputation_snapshots") or {}
|
|
600
|
+
for snap in rs.get("after_oracle") or []:
|
|
601
|
+
name = snap.get("agent")
|
|
602
|
+
if not name:
|
|
603
|
+
continue
|
|
604
|
+
ag = idx.agents.setdefault(
|
|
605
|
+
name,
|
|
606
|
+
AgentAggregate(name=name, persona=snap.get("persona", "")),
|
|
607
|
+
)
|
|
608
|
+
ag.last_reputation = float(snap.get("score", 0.0))
|
|
609
|
+
|
|
610
|
+
# Mark every agent that participated in this sim
|
|
611
|
+
for name in sim_agents:
|
|
612
|
+
if name:
|
|
613
|
+
ag = idx.agents.setdefault(name, AgentAggregate(name=name, persona=""))
|
|
614
|
+
ag.runs_participated += 1
|
|
615
|
+
ag.sims.add(sim_id)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# ---------------------------------------------------------------------------
|
|
619
|
+
# Public builder + cache
|
|
620
|
+
# ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
_CACHE: KnowledgeIndex | None = None
|
|
623
|
+
_CACHE_LOCK = threading.Lock()
|
|
624
|
+
_CACHE_SEEN_SIMS: set[str] = set()
|
|
625
|
+
_INVALIDATION_COUNTER: int = 0
|
|
626
|
+
_CACHE_COUNTER_AT_BUILD: int = -1
|
|
627
|
+
|
|
628
|
+
# Per-sim trace cache: completed sims never change, so once parsed we
|
|
629
|
+
# keep the raw trace dict to avoid re-reading trace.json on every rebuild.
|
|
630
|
+
# Capped at 500 entries; oldest entry is evicted when full.
|
|
631
|
+
_knowledge_cache: dict[str, Any] = {} # sim_id -> parsed trace dict
|
|
632
|
+
_knowledge_cache_lock = threading.Lock()
|
|
633
|
+
_KNOWLEDGE_CACHE_MAX = 500
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _get_cached_claims(sim_id: str) -> Any:
|
|
637
|
+
with _knowledge_cache_lock:
|
|
638
|
+
return _knowledge_cache.get(sim_id)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _set_cached_claims(sim_id: str, trace: Any) -> None:
|
|
642
|
+
with _knowledge_cache_lock:
|
|
643
|
+
if len(_knowledge_cache) >= _KNOWLEDGE_CACHE_MAX:
|
|
644
|
+
# Evict the oldest entry (first key in insertion order)
|
|
645
|
+
oldest = next(iter(_knowledge_cache))
|
|
646
|
+
del _knowledge_cache[oldest]
|
|
647
|
+
_knowledge_cache[sim_id] = trace
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def build_knowledge_index() -> KnowledgeIndex:
|
|
651
|
+
"""Walk the registry and build a fresh index. Cached; cheap if no
|
|
652
|
+
new sims arrived since the last build.
|
|
653
|
+
"""
|
|
654
|
+
global _CACHE, _CACHE_SEEN_SIMS, _CACHE_COUNTER_AT_BUILD
|
|
655
|
+
|
|
656
|
+
# O(1) fast path — nothing has changed since last build
|
|
657
|
+
with _CACHE_LOCK:
|
|
658
|
+
if _CACHE is not None and _CACHE_COUNTER_AT_BUILD == _INVALIDATION_COUNTER:
|
|
659
|
+
return _CACHE
|
|
660
|
+
|
|
661
|
+
sdir = sims_dir()
|
|
662
|
+
|
|
663
|
+
# Snapshot which sim-dirs are currently on disk + their states.
|
|
664
|
+
# We rebuild fully if the set of done sims changed.
|
|
665
|
+
current_done: set[str] = set()
|
|
666
|
+
for d in sdir.iterdir():
|
|
667
|
+
if not d.is_dir():
|
|
668
|
+
continue
|
|
669
|
+
status_path = d / "status.json"
|
|
670
|
+
if not status_path.exists():
|
|
671
|
+
continue
|
|
672
|
+
try:
|
|
673
|
+
status = json.loads(status_path.read_text())
|
|
674
|
+
except (OSError, json.JSONDecodeError):
|
|
675
|
+
continue
|
|
676
|
+
if status.get("state") == "done" and (d / "trace.json").exists():
|
|
677
|
+
current_done.add(d.name)
|
|
678
|
+
|
|
679
|
+
# Second fast path — done-sim set unchanged
|
|
680
|
+
with _CACHE_LOCK:
|
|
681
|
+
if _CACHE is not None and current_done == _CACHE_SEEN_SIMS:
|
|
682
|
+
return _CACHE
|
|
683
|
+
|
|
684
|
+
# Build index OUTSIDE the lock so concurrent readers aren't blocked
|
|
685
|
+
idx = KnowledgeIndex()
|
|
686
|
+
for sim_id in sorted(current_done):
|
|
687
|
+
# Check the per-sim trace cache first — completed sims never change.
|
|
688
|
+
trace = _get_cached_claims(sim_id)
|
|
689
|
+
if trace is None:
|
|
690
|
+
trace_path = sdir / sim_id / "trace.json"
|
|
691
|
+
try:
|
|
692
|
+
trace = json.loads(trace_path.read_text())
|
|
693
|
+
except (OSError, json.JSONDecodeError):
|
|
694
|
+
continue
|
|
695
|
+
_set_cached_claims(sim_id, trace)
|
|
696
|
+
_ingest_trace(idx, sim_id, trace)
|
|
697
|
+
idx.n_sims = len(current_done)
|
|
698
|
+
import time
|
|
699
|
+
idx.last_built_ts_ms = int(time.time() * 1000)
|
|
700
|
+
|
|
701
|
+
# Swap under lock with double-check to avoid clobbering a concurrent build
|
|
702
|
+
with _CACHE_LOCK:
|
|
703
|
+
if _CACHE is None or current_done != _CACHE_SEEN_SIMS:
|
|
704
|
+
_CACHE = idx
|
|
705
|
+
_CACHE_SEEN_SIMS = current_done
|
|
706
|
+
_CACHE_COUNTER_AT_BUILD = _INVALIDATION_COUNTER
|
|
707
|
+
return idx
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def invalidate() -> None:
|
|
711
|
+
"""Force the next ``build_knowledge_index()`` call to rebuild."""
|
|
712
|
+
global _CACHE, _CACHE_SEEN_SIMS, _INVALIDATION_COUNTER
|
|
713
|
+
with _CACHE_LOCK:
|
|
714
|
+
_CACHE = None
|
|
715
|
+
_CACHE_SEEN_SIMS = set()
|
|
716
|
+
_INVALIDATION_COUNTER += 1
|