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,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
|