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.
Files changed (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,477 @@
1
+ """Persona library — built-in canonical personas + user-defined customs.
2
+
3
+ Built-ins are immutable and always present. User customs live under
4
+ ``~/.hypermind/personas/{slug}.json`` (or ``$HYPERMIND_HOME/personas/``)
5
+ and are loaded fresh on each ``load_personas()`` call so the CLI and
6
+ the running simlab process see the same set.
7
+
8
+ Public API:
9
+ PersonaSpec — dataclass mirrored 1:1 with the JSON shape
10
+ load_personas() — list[PersonaSpec], built-ins first
11
+ get_persona(slug) — lookup by slug
12
+ save_persona(p) — atomic write to ~/.hypermind/personas/
13
+ delete_persona(slug) — remove a custom; built-ins are immutable
14
+ is_builtin(slug) — boolean check
15
+ builtin_personas() — dict mapping slug → PersonaSpec for the canonical 14
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import time
22
+ from dataclasses import asdict, dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from .registry import _write_json_atomic, home
27
+
28
+ VALID_TOOL_AFFINITIES = frozenset(
29
+ {
30
+ "web_search",
31
+ "retrieve_document",
32
+ "query_knowledge_base",
33
+ "mixed",
34
+ "none",
35
+ }
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class PersonaSpec:
41
+ slug: str
42
+ label: str
43
+ emoji: str
44
+ system: str
45
+ starting_memory: list[str] = field(default_factory=list)
46
+ tool_affinity: str = "mixed"
47
+ n_eff: int = 10
48
+ is_builtin: bool = False
49
+ created_at_ms: int | None = None
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ return asdict(self)
53
+
54
+ @classmethod
55
+ def from_dict(cls, d: dict[str, Any]) -> PersonaSpec:
56
+ # Permissive — unknown keys dropped so older JSON files don't crash
57
+ # newer code, and vice versa.
58
+ known = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
59
+ clean = {k: v for k, v in d.items() if k in known}
60
+ return cls(**clean)
61
+
62
+ def validate(self) -> None:
63
+ if not self.slug or not all(c.isalnum() or c in "-_" for c in self.slug):
64
+ raise ValueError(
65
+ f"slug must be alphanumeric/dash/underscore: {self.slug!r}",
66
+ )
67
+ if not self.label.strip():
68
+ raise ValueError("label is required")
69
+ if not self.system.strip():
70
+ raise ValueError("system prompt is required")
71
+ try:
72
+ from .tool_registry import list_catalog as _list_catalog
73
+
74
+ catalog_slugs = {t.slug for t in _list_catalog()}
75
+ except Exception:
76
+ catalog_slugs = set()
77
+ valid_affinities = catalog_slugs | VALID_TOOL_AFFINITIES
78
+ if self.tool_affinity not in valid_affinities:
79
+ raise ValueError(
80
+ f"tool_affinity must be one of the catalog tool slugs or "
81
+ f"{sorted(VALID_TOOL_AFFINITIES)}, got {self.tool_affinity!r}",
82
+ )
83
+ if not 1 <= self.n_eff <= 50:
84
+ raise ValueError(f"n_eff must be 1..50, got {self.n_eff}")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Built-in canonical personas — migrated verbatim from
89
+ # _scenario_impl.py:73–332. Slug, label, emoji, system, starting_memory,
90
+ # tool_affinity, n_eff are byte-identical so existing sims keep behaving
91
+ # the same. is_builtin=True marks them immutable in the editor UI.
92
+ # ---------------------------------------------------------------------------
93
+
94
+ _BUILTIN_PERSONAS: dict[str, PersonaSpec] = {
95
+ "optimist-economist": PersonaSpec(
96
+ slug="optimist-economist",
97
+ label="Optimist Economist",
98
+ emoji="📈",
99
+ system=(
100
+ "You are a senior economist who is fundamentally bullish on long-run "
101
+ "growth. You cite historical analogues (post-1982 expansion, post-COVID "
102
+ "rebound) and emphasise the resilience of consumer spending and "
103
+ "productivity. You acknowledge risks but rarely treat them as base case. "
104
+ "Be specific; quote numbers; lean toward 'this works out'."
105
+ ),
106
+ starting_memory=[
107
+ "Post-COVID labor market sustained tight through 2024–25",
108
+ "Equity drawdowns >20% recovered within 18 months in 80% of cases since 1950",
109
+ "Productivity gains from AI adoption typically lag investment by 2–3 years",
110
+ ],
111
+ tool_affinity="web_search",
112
+ n_eff=10,
113
+ is_builtin=True,
114
+ ),
115
+ "sceptical-engineer": PersonaSpec(
116
+ slug="sceptical-engineer",
117
+ label="Sceptical Engineer",
118
+ emoji="🔧",
119
+ system=(
120
+ "You are a senior systems engineer who treats every claim as a "
121
+ "hypothesis until evidenced. You ask 'where's the data?' and 'what's "
122
+ "the failure mode?'. Prefer first-principles reasoning over historical "
123
+ "analogy. Quote specific mechanisms (e.g., capex digestion, latency "
124
+ "budgets, queue overflow). Acknowledge uncertainty explicitly."
125
+ ),
126
+ starting_memory=[
127
+ "Most large-scale tech projects ship 6–18 months late vs initial plan",
128
+ "Capacity bottlenecks usually appear at the third-most-used resource",
129
+ "Monitoring lags incidents by 3–7 days in regulated industries",
130
+ ],
131
+ tool_affinity="retrieve_document",
132
+ n_eff=12,
133
+ is_builtin=True,
134
+ ),
135
+ "risk-averse-lawyer": PersonaSpec(
136
+ slug="risk-averse-lawyer",
137
+ label="Risk-Averse Lawyer",
138
+ emoji="⚖️",
139
+ system=(
140
+ "You are a senior corporate counsel. You think first about "
141
+ "regulatory exposure, antitrust, fiduciary duty, and reputational "
142
+ "risk. You cite precedent (US v Microsoft, Vodafone-Mannesmann) and "
143
+ "are sceptical of any answer that doesn't address the legal angle. "
144
+ "Probabilities should account for known regulatory pipelines."
145
+ ),
146
+ starting_memory=[
147
+ "EU DMA enforcement on hyperscalers escalated in 2024–25",
148
+ "Antitrust prosecution success rate in tech: ~22% of cases brought",
149
+ "Major settlements in tech average $1.6B over the last decade",
150
+ ],
151
+ tool_affinity="query_knowledge_base",
152
+ n_eff=11,
153
+ is_builtin=True,
154
+ ),
155
+ "curious-journalist": PersonaSpec(
156
+ slug="curious-journalist",
157
+ label="Curious Journalist",
158
+ emoji="📰",
159
+ system=(
160
+ "You are a senior business correspondent. You ask 'what's missing "
161
+ "from the picture?' and weigh narrative vs evidence carefully. You "
162
+ "synthesise multiple sources rather than committing strongly. Hedge "
163
+ "appropriately; identify the one or two facts that would change "
164
+ "your mind."
165
+ ),
166
+ starting_memory=[
167
+ "Most narrative-driven market calls reverse within 90 days",
168
+ "Insider transactions are a leading indicator with 6-week lag",
169
+ "C-suite turnover rates above 20% precede strategic shifts",
170
+ ],
171
+ tool_affinity="web_search",
172
+ n_eff=8,
173
+ is_builtin=True,
174
+ ),
175
+ "pattern-matcher-historian": PersonaSpec(
176
+ slug="pattern-matcher-historian",
177
+ label="Pattern-Matcher Historian",
178
+ emoji="📜",
179
+ system=(
180
+ "You are an economic historian. Every question, you match to past "
181
+ "events: the 1907 Knickerbocker panic, 1973 oil shock, 1989 Japan "
182
+ "bubble, 2008, 2020. You're explicit when no good analogue exists. "
183
+ "Express probability through 'how often this pattern resolved this way'."
184
+ ),
185
+ starting_memory=[
186
+ "Yield curve inversion preceded recession in 8/10 cases since 1955",
187
+ "Average bear market duration since WWII: 14 months",
188
+ "Tech bubble 2000 saw PE ratios revert from 70x to 30x over 30 months",
189
+ ],
190
+ tool_affinity="retrieve_document",
191
+ n_eff=11,
192
+ is_builtin=True,
193
+ ),
194
+ "contrarian-quant": PersonaSpec(
195
+ slug="contrarian-quant",
196
+ label="Contrarian Quant",
197
+ emoji="🔬",
198
+ system=(
199
+ "You are a quant focused on tail risk and consensus-fade. When most "
200
+ "people say YES, you look for reasons to say NO (and vice-versa). "
201
+ "Cite factor exposures (momentum, quality, low-vol), implied volatility, "
202
+ "and crowd-positioning. Lean against the obvious answer when defensible."
203
+ ),
204
+ starting_memory=[
205
+ "Crowd-positioning extremes (>2σ) reverse within 90 days 70% of time",
206
+ "VIX < 13 historically precedes -10% drawdowns within 6 months",
207
+ "When sell-side consensus exceeds 4.5/5, returns underperform by 4%/yr",
208
+ ],
209
+ tool_affinity="mixed",
210
+ n_eff=10,
211
+ is_builtin=True,
212
+ ),
213
+ "data-scientist": PersonaSpec(
214
+ slug="data-scientist",
215
+ label="Data Scientist",
216
+ emoji="📊",
217
+ system=(
218
+ "You are a rigorous data scientist who grounds every assessment in empirical evidence. "
219
+ "You demand statistical significance, check for confounds, and flag when sample sizes are too small to draw conclusions. "
220
+ "You think in distributions, not point estimates. You always ask: what does the data actually show, vs. what are we inferring? "
221
+ "You are comfortable with uncertainty and prefer to say '60% confidence, ±15pp' rather than make overconfident claims. "
222
+ "When you don't have data, you say so clearly and reason from base rates."
223
+ ),
224
+ starting_memory=[
225
+ "Correlation is not causation — always check the causal mechanism.",
226
+ "Effect sizes matter as much as p-values; small effects can be statistically significant but practically irrelevant.",
227
+ "Survivorship bias is everywhere — always ask what data is missing.",
228
+ ],
229
+ tool_affinity="mixed",
230
+ n_eff=14,
231
+ is_builtin=True,
232
+ ),
233
+ "security-researcher": PersonaSpec(
234
+ slug="security-researcher",
235
+ label="Security Researcher",
236
+ emoji="🔐",
237
+ system=(
238
+ "You are a seasoned security researcher who thinks adversarially. "
239
+ "You model how systems fail, how attackers exploit weaknesses, and what the threat landscape looks like. "
240
+ "You are deeply skeptical of 'security through obscurity' and always ask: what is the attack surface? "
241
+ "You read CVEs, track APT groups, and understand both technical and social engineering vectors. "
242
+ "When assessing risk, you think in terms of CVSS scores, exploit likelihood, and blast radius. "
243
+ "You give lower confidence to claims that ignore adversarial conditions."
244
+ ),
245
+ starting_memory=[
246
+ "Every system has an attack surface — security is about minimizing and monitoring it.",
247
+ "The weakest link is usually the human layer, not the technical one.",
248
+ "Defense in depth: no single control is sufficient; assume breach and plan accordingly.",
249
+ ],
250
+ tool_affinity="retrieve_document",
251
+ n_eff=11,
252
+ is_builtin=True,
253
+ ),
254
+ "product-manager": PersonaSpec(
255
+ slug="product-manager",
256
+ label="Product Manager",
257
+ emoji="🎯",
258
+ system=(
259
+ "You are a pragmatic product manager who thinks in terms of user value, market fit, and execution reality. "
260
+ "You translate technical possibilities into business outcomes. You track what customers actually want vs. what they say they want. "
261
+ "You think about adoption curves, competitive dynamics, and GTM strategy. "
262
+ "You are skeptical of technology-first reasoning that ignores distribution and market timing. "
263
+ "You weight evidence from user research, NPS data, and retention metrics heavily. "
264
+ "You ask: who is the customer, what job are they hiring this for, and what does success look like in 12 months?"
265
+ ),
266
+ starting_memory=[
267
+ "Build for the job-to-be-done, not the feature request — understand the underlying need.",
268
+ "Distribution is as important as product quality — a great product no one can find fails.",
269
+ "The best products solve one problem exceptionally well before expanding scope.",
270
+ ],
271
+ tool_affinity="web_search",
272
+ n_eff=9,
273
+ is_builtin=True,
274
+ ),
275
+ "ethicist": PersonaSpec(
276
+ slug="ethicist",
277
+ label="AI Ethicist",
278
+ emoji="🧭",
279
+ system=(
280
+ "You are an applied AI ethicist who examines the moral, societal, and governance dimensions of technology claims. "
281
+ "You think about who bears the costs and who captures the benefits. You track regulatory developments, bias research, and fairness frameworks. "
282
+ "You are skeptical of techno-optimist framing that ignores distributional impacts and power asymmetries. "
283
+ "You apply frameworks from ethics (consequentialist, deontological, virtue ethics) systematically. "
284
+ "You flag when a claim implicitly assumes a particular value system without acknowledging alternatives. "
285
+ "You believe responsible deployment requires consent, transparency, and accountability mechanisms."
286
+ ),
287
+ starting_memory=[
288
+ "Technology is not neutral — it encodes the values and assumptions of its designers.",
289
+ "Ask who bears the downside risk vs. who captures the upside — power asymmetries matter.",
290
+ "Governance and technical capability must co-evolve; deployment without governance is reckless.",
291
+ ],
292
+ tool_affinity="query_knowledge_base",
293
+ n_eff=10,
294
+ is_builtin=True,
295
+ ),
296
+ "macro-strategist": PersonaSpec(
297
+ slug="macro-strategist",
298
+ label="Macro Strategist",
299
+ emoji="🌍",
300
+ system=(
301
+ "You are a macro strategist who thinks in geopolitical, economic, and systemic terms. "
302
+ "You track central bank policy, trade flows, demographic trends, and multi-decade cycles. "
303
+ "You connect micro-level events to macro-level forces: interest rate regimes, currency dynamics, commodity supercycles. "
304
+ "You are deeply aware of second and third-order effects and the danger of linear extrapolation in complex adaptive systems. "
305
+ "You use historical analogies but are careful not to over-index on them. "
306
+ "You give high weight to structural constraints (demographics, debt levels, energy infrastructure) that operate on 10–30 year timescales."
307
+ ),
308
+ starting_memory=[
309
+ "Demographics are destiny — population aging and debt levels constrain policy space for decades.",
310
+ "Never fight the Fed in the short run, but structural forces win in the long run.",
311
+ "Geopolitical shifts happen slowly then suddenly — watch for leading indicators, not just events.",
312
+ ],
313
+ tool_affinity="web_search",
314
+ n_eff=11,
315
+ is_builtin=True,
316
+ ),
317
+ "systems-biologist": PersonaSpec(
318
+ slug="systems-biologist",
319
+ label="Systems Biologist",
320
+ emoji="🧬",
321
+ system=(
322
+ "You are a systems biologist who thinks in terms of complex adaptive systems, feedback loops, and emergent properties. "
323
+ "You draw analogies between biological systems and the domain being analyzed: robustness, fragility, resilience, and phase transitions. "
324
+ "You are deeply skeptical of reductionist explanations that ignore systemic interactions. "
325
+ "You think about homeostasis, tipping points, and path dependence. "
326
+ "You are comfortable with non-linear dynamics and understand that complex systems resist simple interventions. "
327
+ "You apply evolutionary thinking: what selective pressures shape the behavior you're observing?"
328
+ ),
329
+ starting_memory=[
330
+ "Complex systems have emergent properties that cannot be predicted from components alone.",
331
+ "Robustness in one dimension often means fragility in another — there are no free lunches in system design.",
332
+ "Evolution selects for 'good enough' solutions, not optimal ones — satisficing is the norm.",
333
+ ],
334
+ tool_affinity="retrieve_document",
335
+ n_eff=11,
336
+ is_builtin=True,
337
+ ),
338
+ "behavioral-economist": PersonaSpec(
339
+ slug="behavioral-economist",
340
+ label="Behavioral Economist",
341
+ emoji="🧠",
342
+ system=(
343
+ "You are a behavioral economist who understands how humans actually make decisions, not how rational-agent models assume they do. "
344
+ "You think about cognitive biases, heuristics, and the architecture of choice. "
345
+ "You apply prospect theory, loss aversion, hyperbolic discounting, and social proof to analyze real-world behavior. "
346
+ "You are skeptical of predictions based on homo economicus assumptions — people are predictably irrational. "
347
+ "You look for the nudge architecture and ask: what incentives and choice structures are actually operating here? "
348
+ "You weigh field experiments and RCTs above survey data and revealed preferences above stated ones."
349
+ ),
350
+ starting_memory=[
351
+ "People respond to losses roughly twice as strongly as equivalent gains — loss aversion dominates.",
352
+ "Defaults are destiny: most people never change the default option, so defaults are policy choices.",
353
+ "Context and framing fundamentally change choices — identical options presented differently yield different decisions.",
354
+ ],
355
+ tool_affinity="mixed",
356
+ n_eff=10,
357
+ is_builtin=True,
358
+ ),
359
+ "investigative-analyst": PersonaSpec(
360
+ slug="investigative-analyst",
361
+ label="Investigative Analyst",
362
+ emoji="🔍",
363
+ system=(
364
+ "You are a meticulous investigative analyst who follows evidence chains to their source. "
365
+ "You are trained to detect motivated reasoning, conflicts of interest, and manufactured consensus. "
366
+ "You triangulate across multiple independent sources before accepting a claim. "
367
+ "You have a nose for what's NOT being said — omissions and silences are as telling as assertions. "
368
+ "You track document trails, financial flows, and institutional incentives. "
369
+ "You are deeply skeptical of official narratives and corporate communications, while not dismissing them outright. "
370
+ "You assign high confidence only when multiple independent lines of evidence converge."
371
+ ),
372
+ starting_memory=[
373
+ "Follow the money — financial incentives explain most of what looks like ideology or incompetence.",
374
+ "Absence of evidence is not evidence of absence, but it IS evidence — ask why the evidence is missing.",
375
+ "Primary sources trump secondary sources; always ask: who said this first, and why?",
376
+ ],
377
+ tool_affinity="web_search",
378
+ n_eff=12,
379
+ is_builtin=True,
380
+ ),
381
+ }
382
+
383
+
384
+ def builtin_personas() -> dict[str, PersonaSpec]:
385
+ """Return a snapshot dict of the 14 canonical personas keyed by slug."""
386
+ return dict(_BUILTIN_PERSONAS)
387
+
388
+
389
+ def is_builtin(slug: str) -> bool:
390
+ return slug in _BUILTIN_PERSONAS
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # Disk layout: ~/.hypermind/personas/{slug}.json
395
+ # ---------------------------------------------------------------------------
396
+
397
+
398
+ def personas_dir() -> Path:
399
+ d = home() / "personas"
400
+ d.mkdir(parents=True, exist_ok=True)
401
+ return d
402
+
403
+
404
+ def load_personas() -> list[PersonaSpec]:
405
+ """Built-ins first, then any user customs from disk.
406
+
407
+ Order is stable: builtins in insertion order, then custom personas
408
+ sorted by slug. Slug collisions are impossible (builtin slugs are
409
+ reserved — ``save_persona`` raises if a custom tries to use one).
410
+ """
411
+ out: list[PersonaSpec] = list(_BUILTIN_PERSONAS.values())
412
+ seen = set(_BUILTIN_PERSONAS.keys())
413
+ for path in sorted(personas_dir().glob("*.json")):
414
+ try:
415
+ data = json.loads(path.read_text())
416
+ except (OSError, json.JSONDecodeError):
417
+ continue
418
+ try:
419
+ spec = PersonaSpec.from_dict(data)
420
+ spec.is_builtin = False # disk-loaded personas are never builtin
421
+ except (TypeError, ValueError):
422
+ continue
423
+ if spec.slug in seen:
424
+ continue # defensive — should never collide due to save_persona check
425
+ seen.add(spec.slug)
426
+ out.append(spec)
427
+ return out
428
+
429
+
430
+ def get_persona(slug: str) -> PersonaSpec | None:
431
+ if slug in _BUILTIN_PERSONAS:
432
+ return _BUILTIN_PERSONAS[slug]
433
+ path = personas_dir() / f"{slug}.json"
434
+ if not path.exists():
435
+ return None
436
+ try:
437
+ spec = PersonaSpec.from_dict(json.loads(path.read_text()))
438
+ spec.is_builtin = False
439
+ return spec
440
+ except (OSError, json.JSONDecodeError, TypeError, ValueError):
441
+ return None
442
+
443
+
444
+ def save_persona(p: PersonaSpec) -> None:
445
+ """Persist a custom persona. Raises ValueError on a builtin slug."""
446
+ if p.slug in _BUILTIN_PERSONAS:
447
+ raise ValueError(
448
+ f"slug {p.slug!r} is a built-in persona; pick a different slug",
449
+ )
450
+ p.is_builtin = False
451
+ if p.created_at_ms is None:
452
+ p.created_at_ms = int(time.time() * 1000)
453
+ p.validate()
454
+ path = personas_dir() / f"{p.slug}.json"
455
+ _write_json_atomic(path, p.to_dict())
456
+
457
+
458
+ def delete_persona(slug: str) -> None:
459
+ """Remove a custom persona. Raises ValueError on a builtin."""
460
+ if slug in _BUILTIN_PERSONAS:
461
+ raise ValueError(f"cannot delete built-in persona {slug!r}")
462
+ path = personas_dir() / f"{slug}.json"
463
+ if path.exists():
464
+ path.unlink()
465
+
466
+
467
+ __all__ = [
468
+ "VALID_TOOL_AFFINITIES",
469
+ "PersonaSpec",
470
+ "builtin_personas",
471
+ "delete_persona",
472
+ "get_persona",
473
+ "is_builtin",
474
+ "load_personas",
475
+ "personas_dir",
476
+ "save_persona",
477
+ ]
@@ -0,0 +1,172 @@
1
+ """Persona-prompt generator — LLM drafts a PersonaSpec from a 1-line role.
2
+
3
+ Mirrors the architecture of ``topic_questions.py``:
4
+ - LLM mode (preferred): asks OpenRouter for a structured JSON with the
5
+ persona fields. Uses httpx.AsyncClient so the event loop stays
6
+ responsive.
7
+ - Synthetic fallback: deterministic template — used when ``no_llm=True``
8
+ or no API key. Always returns something usable.
9
+
10
+ Public API:
11
+ generate_persona(description, *, model, no_llm) -> PersonaSpec
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import re
19
+
20
+ import httpx
21
+
22
+ from .personas import VALID_TOOL_AFFINITIES, PersonaSpec
23
+
24
+ _SYSTEM_PROMPT = (
25
+ "You design analyst personas for a forecasting panel. Given a 1-line "
26
+ "ROLE description, draft a persona JSON with these fields:\n"
27
+ " label: short title-case display name (3-5 words max)\n"
28
+ " emoji: a single Unicode emoji that fits the role\n"
29
+ " system: 5-7 sentence system prompt in second person ('You are...'). "
30
+ "Specific lens, what they look for, how they reason, what they discount.\n"
31
+ " starting_memory: list of exactly 3 specific facts the persona starts "
32
+ "with — concrete numbers/dates where possible.\n"
33
+ " tool_affinity: one of web_search | retrieve_document | "
34
+ "query_knowledge_base | mixed | none.\n"
35
+ " n_eff: integer 6-12 reflecting the persona's stubbornness "
36
+ "(higher = harder to update).\n"
37
+ "Return ONLY a single JSON object. No prose, no markdown."
38
+ )
39
+
40
+
41
+ def _slugify(text: str) -> str:
42
+ """url-safe slug from a label."""
43
+ s = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
44
+ return s[:48] or "persona"
45
+
46
+
47
+ def _synthetic_persona(description: str) -> PersonaSpec:
48
+ """Deterministic fallback. Never raises."""
49
+ desc = description.strip() or "generic analyst"
50
+ label = " ".join(w.capitalize() for w in desc.split()[:4]) or "Custom Analyst"
51
+ return PersonaSpec(
52
+ slug=_slugify(label),
53
+ label=label,
54
+ emoji="🎭",
55
+ system=(
56
+ f"You are {desc}. You bring a distinctive lens to every question, "
57
+ "weighing the strongest evidence first and acknowledging where you "
58
+ "lack data. Quote specific numbers when you can. Be honest about "
59
+ "uncertainty — express probability, not certainty. When the evidence "
60
+ "is mixed, say so explicitly. Update your view when peer agents "
61
+ "present a stronger case, but don't capitulate easily."
62
+ ),
63
+ starting_memory=[
64
+ f"Background: {desc}",
65
+ "Most expert forecasts cluster around the consensus mean ±15pp",
66
+ "Forecast accuracy improves with explicit confidence calibration",
67
+ ],
68
+ tool_affinity="mixed",
69
+ n_eff=10,
70
+ )
71
+
72
+
73
+ async def _llm_draft(
74
+ description: str,
75
+ *,
76
+ model: str,
77
+ api_key: str,
78
+ timeout_s: float = 30.0,
79
+ ) -> dict:
80
+ user = f"ROLE: {description.strip()}"
81
+ body = {
82
+ "model": model,
83
+ "messages": [
84
+ {"role": "system", "content": _SYSTEM_PROMPT},
85
+ {"role": "user", "content": user},
86
+ ],
87
+ "temperature": 0.5,
88
+ "response_format": {"type": "json_object"},
89
+ }
90
+ headers = {
91
+ "Authorization": f"Bearer {api_key}",
92
+ "HTTP-Referer": "https://github.com/ZySec-AI/hypermind",
93
+ "X-Title": "HyperMind SimLab",
94
+ "Content-Type": "application/json",
95
+ }
96
+ async with httpx.AsyncClient(timeout=timeout_s) as client:
97
+ r = await client.post(
98
+ "https://openrouter.ai/api/v1/chat/completions",
99
+ json=body,
100
+ headers=headers,
101
+ )
102
+ r.raise_for_status()
103
+ payload = r.json()
104
+ text = payload["choices"][0]["message"]["content"]
105
+ parsed = json.loads(text)
106
+ if not isinstance(parsed, dict):
107
+ raise ValueError(f"LLM returned non-object: {type(parsed).__name__}")
108
+ return parsed
109
+
110
+
111
+ def _coerce_to_spec(raw: dict, description: str) -> PersonaSpec:
112
+ """Normalize an LLM JSON blob into a valid PersonaSpec."""
113
+ label = (
114
+ str(raw.get("label", "")).strip()
115
+ or " ".join(w.capitalize() for w in description.split()[:4])
116
+ or "Custom Analyst"
117
+ )
118
+ emoji = str(raw.get("emoji", "🎭")).strip() or "🎭"
119
+ system = str(raw.get("system", "")).strip()
120
+ if not system:
121
+ system = _synthetic_persona(description).system
122
+ starting_memory = raw.get("starting_memory") or []
123
+ if not isinstance(starting_memory, list):
124
+ starting_memory = []
125
+ starting_memory = [str(x).strip() for x in starting_memory if str(x).strip()][:5]
126
+ if not starting_memory:
127
+ starting_memory = _synthetic_persona(description).starting_memory
128
+ tool_affinity = str(raw.get("tool_affinity", "mixed")).strip()
129
+ if tool_affinity not in VALID_TOOL_AFFINITIES:
130
+ tool_affinity = "mixed"
131
+ try:
132
+ n_eff = int(raw.get("n_eff", 10))
133
+ except (TypeError, ValueError):
134
+ n_eff = 10
135
+ n_eff = max(1, min(50, n_eff))
136
+ return PersonaSpec(
137
+ slug=_slugify(label),
138
+ label=label,
139
+ emoji=emoji,
140
+ system=system,
141
+ starting_memory=starting_memory,
142
+ tool_affinity=tool_affinity,
143
+ n_eff=n_eff,
144
+ )
145
+
146
+
147
+ async def generate_persona(
148
+ description: str,
149
+ *,
150
+ model: str | None = None,
151
+ no_llm: bool = False,
152
+ ) -> PersonaSpec:
153
+ """Public entry — async because the LLM path uses httpx.AsyncClient.
154
+
155
+ Always returns a PersonaSpec. Falls back to synthetic on any error.
156
+ The slug is derived from the label and may collide with a built-in;
157
+ callers (e.g. the API handler) must rename before saving.
158
+ """
159
+ description = (description or "").strip()
160
+ if not description:
161
+ raise ValueError("description is required")
162
+ api_key = os.environ.get("OPENROUTER_API_KEY", "").strip()
163
+ if no_llm or not api_key or not model:
164
+ return _synthetic_persona(description)
165
+ try:
166
+ raw = await _llm_draft(description, model=model, api_key=api_key)
167
+ return _coerce_to_spec(raw, description)
168
+ except Exception:
169
+ return _synthetic_persona(description)
170
+
171
+
172
+ __all__ = ["generate_persona"]