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,159 @@
1
+ """Anthropic Messages-backed responder.
2
+
3
+ Same contract as :class:`OpenAIResponder`: returns
4
+ ``(Uncertainty, StructuredResponse)`` so the consult panel can capture
5
+ sub-claims, evidence references, and reasoning.
6
+
7
+ Install: ``pip install hypermind[anthropic]``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from hypermind.responders.base import (
15
+ LLMResponderError,
16
+ ResponderResult,
17
+ confidence_to_uncertainty,
18
+ )
19
+ from hypermind.responders.structured import (
20
+ STRUCTURED_PROMPT_SUFFIX,
21
+ StructuredResponse,
22
+ TokenUsage,
23
+ parse_structured_response,
24
+ )
25
+
26
+ _DEFAULT_SYSTEM = """You are a domain analyst on a federated intelligence panel.
27
+
28
+ For each query, respond with a single JSON object:
29
+ {
30
+ "confidence": <float in [0, 1]>,
31
+ "reasoning": "<one-paragraph rationale, ≤100 words>"
32
+ }
33
+
34
+ `confidence` is the probability that the answer to the question is YES.
35
+ 0.0 = certain NO, 1.0 = certain YES, 0.5 = no opinion. Use the full
36
+ range — do not anchor to 0.5 unless you genuinely have no information.
37
+
38
+ Respond ONLY with the JSON object, no surrounding prose."""
39
+
40
+
41
+ class AnthropicResponder:
42
+ """A consult responder backed by an Anthropic Claude model.
43
+
44
+ Example::
45
+
46
+ from anthropic import AsyncAnthropic
47
+ from hypermind.responders import AnthropicResponder
48
+
49
+ client = AsyncAnthropic()
50
+ responder = AnthropicResponder(
51
+ client=client,
52
+ model="claude-sonnet-4-6",
53
+ system_prompt="You are a biosecurity analyst...",
54
+ )
55
+
56
+ result = await alice.consult(
57
+ "Is this pathogen escape contained?",
58
+ panel_size=10,
59
+ responder=responder,
60
+ )
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ client: Any,
67
+ model: str = "claude-sonnet-4-6",
68
+ system_prompt: str = _DEFAULT_SYSTEM,
69
+ effective_n: float = 10.0,
70
+ max_tokens: int = 600,
71
+ temperature: float = 0.4,
72
+ capture_structured: bool = True,
73
+ cost_tracker: Any = None,
74
+ ) -> None:
75
+ self.client = client
76
+ self.model = model
77
+ self.system_prompt = system_prompt
78
+ self.effective_n = effective_n
79
+ self.max_tokens = max_tokens
80
+ self.temperature = temperature
81
+ self.capture_structured = capture_structured
82
+ self.cost_tracker = cost_tracker
83
+ self.last_response: tuple[float, str] | None = None
84
+ self.last_structured: StructuredResponse | None = None
85
+
86
+ def _build_system_prompt(self) -> str:
87
+ if not self.capture_structured:
88
+ return self.system_prompt
89
+ if "sub_claims" in self.system_prompt:
90
+ return self.system_prompt
91
+ return self.system_prompt + STRUCTURED_PROMPT_SUFFIX
92
+
93
+ async def __call__(self, agent_kid: bytes, query: str) -> ResponderResult:
94
+ system = self._build_system_prompt()
95
+ try:
96
+ resp = await self.client.messages.create(
97
+ model=self.model,
98
+ max_tokens=self.max_tokens,
99
+ temperature=self.temperature,
100
+ system=system,
101
+ messages=[{"role": "user", "content": query}],
102
+ )
103
+ except Exception as exc:
104
+ raise LLMResponderError(f"Anthropic call failed: {exc}") from exc
105
+
106
+ # Anthropic returns message.content as a list of content blocks
107
+ try:
108
+ text_block = next(b for b in resp.content if getattr(b, "type", None) == "text")
109
+ text = text_block.text
110
+ except (StopIteration, AttributeError) as exc:
111
+ raise LLMResponderError(f"Anthropic response had no text block: {exc}") from exc
112
+
113
+ # ---- Token usage ---------------------------------------------------
114
+ token_usage: TokenUsage | None = None
115
+ usage = getattr(resp, "usage", None)
116
+ if usage is not None:
117
+ prompt_t = int(getattr(usage, "input_tokens", 0) or 0)
118
+ completion_t = int(getattr(usage, "output_tokens", 0) or 0)
119
+ if self.cost_tracker is not None:
120
+ try:
121
+ token_usage = self.cost_tracker.record(
122
+ model=self.model,
123
+ prompt_tokens=prompt_t,
124
+ completion_tokens=completion_t,
125
+ agent_kid=agent_kid,
126
+ )
127
+ except Exception:
128
+ token_usage = TokenUsage(
129
+ model=self.model,
130
+ prompt_tokens=prompt_t,
131
+ completion_tokens=completion_t,
132
+ cost_usd=None,
133
+ )
134
+ else:
135
+ token_usage = TokenUsage(
136
+ model=self.model,
137
+ prompt_tokens=prompt_t,
138
+ completion_tokens=completion_t,
139
+ cost_usd=None,
140
+ )
141
+
142
+ # ---- Structured parse ----------------------------------------------
143
+ try:
144
+ structured = parse_structured_response(
145
+ text,
146
+ agent_kid=agent_kid,
147
+ token_usage=token_usage,
148
+ )
149
+ except ValueError as exc:
150
+ raise LLMResponderError(
151
+ f"Anthropic response did not contain a valid 'confidence' field: {exc}"
152
+ ) from exc
153
+
154
+ confidence = structured.confidence
155
+ self.last_response = (confidence, structured.reasoning)
156
+ self.last_structured = structured
157
+
158
+ unc = confidence_to_uncertainty(confidence, effective_n=self.effective_n)
159
+ return unc, structured
@@ -0,0 +1,162 @@
1
+ """Responder Protocol + helpers shared by every responder implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from collections.abc import Awaitable, Callable
7
+ from typing import Protocol
8
+
9
+ from hypermind.responders.structured import StructuredResponse
10
+ from hypermind.uncertainty import Uncertainty
11
+
12
+
13
+ class LLMResponderError(RuntimeError):
14
+ """Raised when an LLM call fails or returns an unparseable response.
15
+
16
+ The consult panel drops failed responders rather than aborting the
17
+ aggregation, so this is logged but not user-fatal in normal flow.
18
+ """
19
+
20
+
21
+ #: Result type a responder may return.
22
+ #:
23
+ #: Legacy responders return bare :class:`Uncertainty`. v0.6+ responders
24
+ #: may additionally return a :class:`StructuredResponse` paired with the
25
+ #: posterior so the consult panel can capture sub-claims, evidence
26
+ #: references, and reasoning text. The ``consult()`` dispatch in
27
+ #: :mod:`hypermind.agent` accepts either form transparently.
28
+ ResponderResult = Uncertainty | tuple[Uncertainty, StructuredResponse]
29
+
30
+ # Public type alias for ``consult(responder=...)``.
31
+ Responder = Callable[[bytes, str], Awaitable[ResponderResult]]
32
+
33
+
34
+ def unpack_responder_result(
35
+ result: ResponderResult,
36
+ ) -> tuple[Uncertainty, StructuredResponse | None]:
37
+ """Normalise a responder return value to ``(Uncertainty, StructuredResponse | None)``.
38
+
39
+ Backward-compatible: a bare :class:`Uncertainty` produces ``(unc, None)``
40
+ so existing :class:`StaticResponder` / :class:`SyntheticResponder`
41
+ callers continue to work unchanged.
42
+ """
43
+ if isinstance(result, Uncertainty):
44
+ return result, None
45
+ if isinstance(result, tuple) and len(result) == 2 and isinstance(result[0], Uncertainty):
46
+ unc, structured = result
47
+ if structured is None or isinstance(structured, StructuredResponse):
48
+ return unc, structured
49
+ raise TypeError(
50
+ "Responder must return Uncertainty or (Uncertainty, StructuredResponse), "
51
+ f"got {type(result).__name__}"
52
+ )
53
+
54
+
55
+ # --------------------------------------------------------------------------
56
+ # Confidence → Uncertainty mapping
57
+ # --------------------------------------------------------------------------
58
+
59
+
60
+ def confidence_to_uncertainty(
61
+ confidence: float,
62
+ *,
63
+ effective_n: float = 10.0,
64
+ calibration_adj: float = 1.0,
65
+ ) -> Uncertainty:
66
+ """Map a single ``confidence`` ∈ [0, 1] into a Beta posterior.
67
+
68
+ The mapping uses an *effective sample size*: a confidence of 0.8
69
+ with ``effective_n=10`` produces ``Beta(α=8, β=2)``. Tuning
70
+ ``effective_n`` controls how confidently the responder is asserting
71
+ its belief — 4 for vibes, 10 for educated guesses, 30 for genuinely
72
+ informed predictions, ≥100 for ground-truth-anchored claims.
73
+
74
+ ``calibration_adj`` is a multiplicative shrinkage applied to
75
+ ``effective_n``. Callers that track per-agent Brier history (the
76
+ closed-loop calibration kernel) pass values in roughly [0.5, 2.0]:
77
+ well-calibrated agents are *boosted* (>1.0), overconfident agents
78
+ are *shrunk* (<1.0). Default 1.0 is a no-op so legacy callers see
79
+ identical behaviour.
80
+
81
+ Clamps confidence to [0.01, 0.99] so α and β are strictly positive
82
+ (Beta is undefined at the endpoints).
83
+ """
84
+ c = max(0.01, min(0.99, float(confidence)))
85
+ raw_n = max(1.0, float(effective_n))
86
+ adj = max(0.1, float(calibration_adj)) # never collapse to 0
87
+ n = max(1.0, raw_n * adj)
88
+ alpha = c * n
89
+ beta = (1.0 - c) * n
90
+ # Floor at 1.0 — Beta(1, 1) is the uniform prior; values < 1 produce
91
+ # bimodal U-shaped posteriors which are not what callers usually want.
92
+ return Uncertainty.beta(
93
+ alpha=max(1.0, alpha),
94
+ beta=max(1.0, beta),
95
+ n_eff=n,
96
+ )
97
+
98
+
99
+ # --------------------------------------------------------------------------
100
+ # Responder Protocol
101
+ # --------------------------------------------------------------------------
102
+
103
+
104
+ class _ResponderProto(Protocol):
105
+ async def __call__(self, agent_kid: bytes, query: str) -> Uncertainty: ...
106
+
107
+
108
+ # --------------------------------------------------------------------------
109
+ # StaticResponder — every agent returns the same Uncertainty
110
+ # --------------------------------------------------------------------------
111
+
112
+
113
+ class StaticResponder:
114
+ """Return a fixed Uncertainty for every (agent, query) pair.
115
+
116
+ Useful for deterministic tests and benchmarks where you want to
117
+ isolate the consult aggregation logic from any answer variation.
118
+ """
119
+
120
+ def __init__(self, uncertainty: Uncertainty) -> None:
121
+ self._u = uncertainty
122
+
123
+ async def __call__(self, agent_kid: bytes, query: str) -> Uncertainty:
124
+ return self._u
125
+
126
+
127
+ # --------------------------------------------------------------------------
128
+ # SyntheticResponder — Beta-sampled per-agent personality
129
+ # --------------------------------------------------------------------------
130
+
131
+
132
+ class SyntheticResponder:
133
+ """Per-agent Beta(α, β) profile, deterministic by agent kid.
134
+
135
+ Each agent gets a stable (α, β) drawn from the configured ranges,
136
+ seeded by its kid so the same agent always answers the same way.
137
+ Used by the geopolitical simulation as a stand-in for real LLMs.
138
+
139
+ >>> resp = SyntheticResponder(alpha_range=(8, 12), beta_range=(2, 4))
140
+ >>> await resp(b"\\x01" * 32, "Will it rain?")
141
+ Uncertainty(...)
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ *,
147
+ alpha_range: tuple[float, float] = (5.0, 10.0),
148
+ beta_range: tuple[float, float] = (3.0, 6.0),
149
+ ) -> None:
150
+ self.alpha_range = alpha_range
151
+ self.beta_range = beta_range
152
+
153
+ async def __call__(self, agent_kid: bytes, query: str) -> Uncertainty:
154
+ # Stable per-agent seed so the same agent always returns the same
155
+ # posterior for the same query. (Hashing the query as well would
156
+ # be reasonable; we don't, so reputation drift between calls is
157
+ # purely from oracle resolution rather than answer variance.)
158
+ seed = int.from_bytes(agent_kid[:8], "big") if agent_kid else 0
159
+ rng = random.Random(seed)
160
+ alpha = rng.uniform(*self.alpha_range)
161
+ beta = rng.uniform(*self.beta_range)
162
+ return Uncertainty.beta(alpha=alpha, beta=beta)
@@ -0,0 +1,199 @@
1
+ """OpenAI-backed responder.
2
+
3
+ Each call to the responder spawns one Chat Completions request with a
4
+ strict JSON schema. The model returns
5
+ ``{"confidence": 0.x, "reasoning": "...", "sub_claims": [...], ...}``
6
+ which is parsed into ``Uncertainty.beta(...)`` paired with a
7
+ :class:`StructuredResponse`.
8
+
9
+ Install: ``pip install hypermind[openai]`` (adds ``openai>=1.0`` as a soft dep).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from hypermind.responders.base import (
17
+ LLMResponderError,
18
+ ResponderResult,
19
+ confidence_to_uncertainty,
20
+ )
21
+ from hypermind.responders.structured import (
22
+ STRUCTURED_PROMPT_SUFFIX,
23
+ StructuredResponse,
24
+ TokenUsage,
25
+ parse_structured_response,
26
+ )
27
+
28
+ _DEFAULT_SYSTEM = """You are a domain analyst on a federated intelligence panel.
29
+
30
+ For each query, respond with a single JSON object:
31
+ {
32
+ "confidence": <float in [0, 1]>,
33
+ "reasoning": "<one-paragraph rationale, ≤100 words>"
34
+ }
35
+
36
+ `confidence` is the probability that the answer to the question is YES.
37
+ 0.0 = certain NO, 1.0 = certain YES, 0.5 = no opinion. Use the full
38
+ range — do not anchor to 0.5 unless you genuinely have no information.
39
+
40
+ Respond ONLY with the JSON object, no surrounding prose."""
41
+
42
+
43
+ class OpenAIResponder:
44
+ """A consult responder backed by an OpenAI chat model.
45
+
46
+ Constructed with an OpenAI client, a model name, and an optional
47
+ system prompt that tells the model what role it is playing. Each
48
+ panel-member call spawns one chat completion.
49
+
50
+ When ``capture_structured=True`` (the default in v0.6+), the system
51
+ prompt is augmented with :data:`STRUCTURED_PROMPT_SUFFIX` and the
52
+ parsed sub-claims / evidence / alternatives are returned alongside
53
+ the Uncertainty. Backward-compat: the dispatch in :mod:`agent.consult`
54
+ accepts either ``Uncertainty`` or ``(Uncertainty, StructuredResponse)``
55
+ transparently.
56
+
57
+ Example::
58
+
59
+ from openai import AsyncOpenAI
60
+ from hypermind.responders import OpenAIResponder
61
+
62
+ client = AsyncOpenAI()
63
+ responder = OpenAIResponder(
64
+ client=client,
65
+ model="gpt-4o-mini",
66
+ system_prompt="You are a geopolitical analyst...",
67
+ )
68
+
69
+ result = await alice.consult(
70
+ "Will the cease-fire hold for 30 days?",
71
+ panel_size=10,
72
+ responder=responder,
73
+ capture_structured=True,
74
+ )
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ *,
80
+ client: Any,
81
+ model: str = "gpt-4o-mini",
82
+ system_prompt: str = _DEFAULT_SYSTEM,
83
+ effective_n: float = 10.0,
84
+ timeout_s: float = 30.0,
85
+ temperature: float = 0.4,
86
+ capture_structured: bool = True,
87
+ cost_tracker: Any = None,
88
+ ) -> None:
89
+ self.client = client
90
+ self.model = model
91
+ self.system_prompt = system_prompt
92
+ self.effective_n = effective_n
93
+ self.timeout_s = timeout_s
94
+ self.temperature = temperature
95
+ self.capture_structured = capture_structured
96
+ # Optional TokenCostTracker — when set, every call records its
97
+ # TokenUsage there in addition to attaching it to the
98
+ # StructuredResponse. See :mod:`hypermind.observability.cost`.
99
+ self.cost_tracker = cost_tracker
100
+ # Optional sink for the most recent LLM response (raw confidence
101
+ # + reasoning). Preserved for backward compat with v0.5 callers
102
+ # that read ``responder.last_response`` after each call.
103
+ self.last_response: tuple[float, str] | None = None
104
+ self.last_structured: StructuredResponse | None = None
105
+ self._cached_system: str = self._build_system_prompt()
106
+
107
+ def _build_system_prompt(self) -> str:
108
+ if not self.capture_structured:
109
+ return self.system_prompt
110
+ # Avoid double-suffixing if the caller already included it.
111
+ if "sub_claims" in self.system_prompt:
112
+ return self.system_prompt
113
+ return self.system_prompt + STRUCTURED_PROMPT_SUFFIX
114
+
115
+ async def __call__(self, agent_kid: bytes, query: str) -> ResponderResult:
116
+ system = self._cached_system
117
+
118
+ # OpenAI's `response_format=json_object` requires the literal
119
+ # token "json" somewhere in the messages — append a hint if
120
+ # missing so callers don't have to remember.
121
+ user_content = query
122
+ if "json" not in (user_content + system).lower():
123
+ user_content = (
124
+ f"{query}\n\n"
125
+ 'Respond with a json object: {"confidence": <0..1>, "reasoning": "..."}'
126
+ )
127
+
128
+ try:
129
+ resp = await self.client.chat.completions.create(
130
+ model=self.model,
131
+ messages=[
132
+ {"role": "system", "content": system},
133
+ {"role": "user", "content": user_content},
134
+ ],
135
+ response_format={"type": "json_object"},
136
+ temperature=self.temperature,
137
+ timeout=self.timeout_s,
138
+ )
139
+ except Exception as exc:
140
+ raise LLMResponderError(f"OpenAI call failed: {exc}") from exc
141
+
142
+ content = (resp.choices[0].message.content or "{}") if resp.choices else "{}"
143
+
144
+ # ---- Token usage --------------------------------------------------
145
+ token_usage: TokenUsage | None = None
146
+ usage = getattr(resp, "usage", None)
147
+ if usage is not None:
148
+ prompt_t = int(getattr(usage, "prompt_tokens", 0) or 0)
149
+ completion_t = int(getattr(usage, "completion_tokens", 0) or 0)
150
+ cost: float | None = None
151
+ if self.cost_tracker is not None:
152
+ # cost_tracker.record() returns the canonical TokenUsage
153
+ # with cost computed from PRICE_TABLE; fall back to a
154
+ # locally-built record if the tracker isn't attached.
155
+ try:
156
+ token_usage = self.cost_tracker.record(
157
+ model=self.model,
158
+ prompt_tokens=prompt_t,
159
+ completion_tokens=completion_t,
160
+ agent_kid=agent_kid,
161
+ )
162
+ except Exception:
163
+ token_usage = TokenUsage(
164
+ model=self.model,
165
+ prompt_tokens=prompt_t,
166
+ completion_tokens=completion_t,
167
+ cost_usd=None,
168
+ )
169
+ else:
170
+ token_usage = TokenUsage(
171
+ model=self.model,
172
+ prompt_tokens=prompt_t,
173
+ completion_tokens=completion_t,
174
+ cost_usd=cost,
175
+ )
176
+
177
+ # ---- Structured parse ---------------------------------------------
178
+ try:
179
+ structured = parse_structured_response(
180
+ content,
181
+ agent_kid=agent_kid,
182
+ token_usage=token_usage,
183
+ )
184
+ except ValueError as exc:
185
+ raise LLMResponderError(
186
+ f"OpenAI response did not contain a valid 'confidence' field: {exc}"
187
+ ) from exc
188
+
189
+ confidence = structured.confidence
190
+ # Preserve the v0.5 single-tuple sink for callers that read it.
191
+ self.last_response = (confidence, structured.reasoning)
192
+ self.last_structured = structured
193
+
194
+ unc = confidence_to_uncertainty(confidence, effective_n=self.effective_n)
195
+
196
+ # When capture_structured is False we still return a tuple — the
197
+ # dispatch unpacker handles both. Callers that want the bare
198
+ # Uncertainty back can read it from the tuple directly.
199
+ return unc, structured