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,444 @@
|
|
|
1
|
+
"""Tool-using responders — let LLMs ground claims in retrieved evidence.
|
|
2
|
+
|
|
3
|
+
A :class:`ToolCallingResponder` wraps an OpenAI-compatible chat model
|
|
4
|
+
that supports function-calling. When the model emits tool calls, the
|
|
5
|
+
router invokes the registered Python callables, feeds the results back
|
|
6
|
+
as ``role=tool`` messages, and re-asks for the final JSON answer.
|
|
7
|
+
|
|
8
|
+
Built-in tools (importable from this module):
|
|
9
|
+
* :func:`web_search` — tiny stub web-search that callers replace with
|
|
10
|
+
a real search engine in production. The default implementation
|
|
11
|
+
returns a "no results" stub so the panel still degrades gracefully.
|
|
12
|
+
* :func:`retrieve_document` — caller-supplied document store lookup.
|
|
13
|
+
* :func:`query_knowledge_base` — caller-supplied vector-DB lookup.
|
|
14
|
+
|
|
15
|
+
The default tool implementations are deliberately *stubs* — the SDK
|
|
16
|
+
never ships an opinionated retrieval backend. Operators wire their own
|
|
17
|
+
``Tool`` objects and pass them via ``tools=[...]``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Awaitable, Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from hypermind.responders.base import (
|
|
29
|
+
LLMResponderError,
|
|
30
|
+
ResponderResult,
|
|
31
|
+
confidence_to_uncertainty,
|
|
32
|
+
)
|
|
33
|
+
from hypermind.responders.structured import (
|
|
34
|
+
STRUCTURED_PROMPT_SUFFIX,
|
|
35
|
+
StructuredResponse,
|
|
36
|
+
TokenUsage,
|
|
37
|
+
parse_structured_response,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Tool dataclass
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Tool:
|
|
47
|
+
"""One callable tool exposed to the LLM.
|
|
48
|
+
|
|
49
|
+
``handler`` receives a JSON dict (the LLM-supplied arguments) and
|
|
50
|
+
must return a string (the tool result). Async or sync callables
|
|
51
|
+
both supported — async wins if both are present.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
description: str
|
|
56
|
+
parameters_schema: dict[str, Any]
|
|
57
|
+
handler: Callable[[dict[str, Any]], Awaitable[str] | str]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Built-in stub tools — operators replace these in production
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _stub_web_search(args: dict[str, Any]) -> str:
|
|
66
|
+
"""Default ``web_search`` stub: returns a 'no results' message.
|
|
67
|
+
|
|
68
|
+
Production deployments override by passing a real handler:
|
|
69
|
+
|
|
70
|
+
>>> from duckduckgo_search import AsyncDDGS
|
|
71
|
+
>>> async def real_search(args): ...
|
|
72
|
+
"""
|
|
73
|
+
query = args.get("query", "")
|
|
74
|
+
return json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"query": query,
|
|
77
|
+
"results": [],
|
|
78
|
+
"note": "no real web_search backend configured; results unavailable",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def web_search(handler: Callable[[dict[str, Any]], Awaitable[str] | str] | None = None) -> Tool:
|
|
84
|
+
"""Build a ``web_search`` tool with the given handler (or stub)."""
|
|
85
|
+
return Tool(
|
|
86
|
+
name="web_search",
|
|
87
|
+
description="Search the public web for recent information about a topic.",
|
|
88
|
+
parameters_schema={
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"query": {"type": "string", "description": "Search query."},
|
|
92
|
+
"max_results": {"type": "integer", "default": 5},
|
|
93
|
+
},
|
|
94
|
+
"required": ["query"],
|
|
95
|
+
},
|
|
96
|
+
handler=handler or _stub_web_search,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _stub_retrieve_document(args: dict[str, Any]) -> str:
|
|
101
|
+
doc_id = args.get("doc_id", "")
|
|
102
|
+
return json.dumps(
|
|
103
|
+
{
|
|
104
|
+
"doc_id": doc_id,
|
|
105
|
+
"found": False,
|
|
106
|
+
"note": "no document store configured",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def retrieve_document(
|
|
112
|
+
handler: Callable[[dict[str, Any]], Awaitable[str] | str] | None = None,
|
|
113
|
+
) -> Tool:
|
|
114
|
+
return Tool(
|
|
115
|
+
name="retrieve_document",
|
|
116
|
+
description="Retrieve a document by ID from the configured store.",
|
|
117
|
+
parameters_schema={
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"doc_id": {"type": "string", "description": "Stable document identifier."},
|
|
121
|
+
},
|
|
122
|
+
"required": ["doc_id"],
|
|
123
|
+
},
|
|
124
|
+
handler=handler or _stub_retrieve_document,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _stub_query_knowledge_base(args: dict[str, Any]) -> str:
|
|
129
|
+
query = args.get("query", "")
|
|
130
|
+
return json.dumps(
|
|
131
|
+
{
|
|
132
|
+
"query": query,
|
|
133
|
+
"matches": [],
|
|
134
|
+
"note": "no knowledge_base backend configured",
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def query_knowledge_base(
|
|
140
|
+
handler: Callable[[dict[str, Any]], Awaitable[str] | str] | None = None,
|
|
141
|
+
) -> Tool:
|
|
142
|
+
return Tool(
|
|
143
|
+
name="query_knowledge_base",
|
|
144
|
+
description="Vector-search the configured knowledge base for relevant facts.",
|
|
145
|
+
parameters_schema={
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"query": {"type": "string"},
|
|
149
|
+
"top_k": {"type": "integer", "default": 5},
|
|
150
|
+
},
|
|
151
|
+
"required": ["query"],
|
|
152
|
+
},
|
|
153
|
+
handler=handler or _stub_query_knowledge_base,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# ToolCallingResponder
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
_DEFAULT_TOOL_SYSTEM = """You are a domain analyst on a federated intelligence panel.
|
|
162
|
+
|
|
163
|
+
You have access to tools (web_search, retrieve_document, query_knowledge_base)
|
|
164
|
+
that you may call BEFORE forming your final answer. Use a tool only if
|
|
165
|
+
your reasoning genuinely depends on information you don't already know;
|
|
166
|
+
do not invoke tools merely to be thorough — they cost time and money.
|
|
167
|
+
|
|
168
|
+
After you have all the information you need, respond with a single json
|
|
169
|
+
object containing your final answer. Do not call any further tools after
|
|
170
|
+
producing the json answer."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_MAX_TOOL_ROUNDS = 4
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ToolCallingResponder:
|
|
177
|
+
"""OpenAI-compatible function-calling responder.
|
|
178
|
+
|
|
179
|
+
Compatible with any chat model whose SDK exposes ``tools=[...]`` and
|
|
180
|
+
returns ``message.tool_calls``. Used in production with OpenAI,
|
|
181
|
+
Anthropic (via OpenAI-compatible adapter), or OpenRouter.
|
|
182
|
+
|
|
183
|
+
Each call may run multiple rounds: the LLM emits tool calls, we
|
|
184
|
+
execute them and feed results back, then ask for the final JSON
|
|
185
|
+
answer. ``max_tool_rounds`` caps this loop so a misbehaving model
|
|
186
|
+
can't burn unlimited tokens.
|
|
187
|
+
|
|
188
|
+
Costs (token usage and tool-call counts) are emitted to the
|
|
189
|
+
recorder if one is attached.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
client: Any,
|
|
196
|
+
model: str = "gpt-4o-mini",
|
|
197
|
+
tools: list[Tool] | None = None,
|
|
198
|
+
system_prompt: str = _DEFAULT_TOOL_SYSTEM,
|
|
199
|
+
effective_n: float = 10.0,
|
|
200
|
+
timeout_s: float = 60.0,
|
|
201
|
+
temperature: float = 0.4,
|
|
202
|
+
max_tool_rounds: int = _MAX_TOOL_ROUNDS,
|
|
203
|
+
cost_tracker: Any = None,
|
|
204
|
+
recorder: Any = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
self.client = client
|
|
207
|
+
self.model = model
|
|
208
|
+
self.tools: list[Tool] = list(tools or [])
|
|
209
|
+
self.system_prompt = system_prompt
|
|
210
|
+
self.effective_n = effective_n
|
|
211
|
+
self.timeout_s = timeout_s
|
|
212
|
+
self.temperature = temperature
|
|
213
|
+
self.max_tool_rounds = max(1, int(max_tool_rounds))
|
|
214
|
+
self.cost_tracker = cost_tracker
|
|
215
|
+
self.recorder = recorder
|
|
216
|
+
self.last_tool_calls: list[dict[str, Any]] = []
|
|
217
|
+
self.last_structured: StructuredResponse | None = None
|
|
218
|
+
|
|
219
|
+
# ----- tool dispatch -------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def _tool_specs(self) -> list[dict[str, Any]]:
|
|
222
|
+
"""OpenAI-style tools array."""
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
"type": "function",
|
|
226
|
+
"function": {
|
|
227
|
+
"name": t.name,
|
|
228
|
+
"description": t.description,
|
|
229
|
+
"parameters": t.parameters_schema,
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
for t in self.tools
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
async def _execute_tool(self, name: str, arguments_json: str) -> str:
|
|
236
|
+
tool = next((t for t in self.tools if t.name == name), None)
|
|
237
|
+
if tool is None:
|
|
238
|
+
return json.dumps({"error": f"unknown tool {name!r}"})
|
|
239
|
+
try:
|
|
240
|
+
args = json.loads(arguments_json) if arguments_json else {}
|
|
241
|
+
except json.JSONDecodeError as exc:
|
|
242
|
+
return json.dumps({"error": f"invalid tool args: {exc}"})
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
handler_result = tool.handler(args)
|
|
246
|
+
if hasattr(handler_result, "__await__"):
|
|
247
|
+
handler_result = await handler_result # type: ignore[assignment]
|
|
248
|
+
return str(handler_result)
|
|
249
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
250
|
+
raise
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
return json.dumps(
|
|
253
|
+
{
|
|
254
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
255
|
+
"tool": name,
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _emit(self, event_type: str, fields: dict[str, Any]) -> None:
|
|
260
|
+
if self.recorder is None:
|
|
261
|
+
return
|
|
262
|
+
try:
|
|
263
|
+
self.recorder.emit(event_type=event_type, severity="info", fields=fields)
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
def _accumulate_token_usage(
|
|
268
|
+
self,
|
|
269
|
+
accum: TokenUsage | None,
|
|
270
|
+
usage_obj: Any,
|
|
271
|
+
agent_kid: bytes,
|
|
272
|
+
) -> TokenUsage | None:
|
|
273
|
+
if usage_obj is None:
|
|
274
|
+
return accum
|
|
275
|
+
prompt_t = int(getattr(usage_obj, "prompt_tokens", 0) or 0)
|
|
276
|
+
completion_t = int(getattr(usage_obj, "completion_tokens", 0) or 0)
|
|
277
|
+
if self.cost_tracker is not None:
|
|
278
|
+
try:
|
|
279
|
+
this_call = self.cost_tracker.record(
|
|
280
|
+
model=self.model,
|
|
281
|
+
prompt_tokens=prompt_t,
|
|
282
|
+
completion_tokens=completion_t,
|
|
283
|
+
agent_kid=agent_kid,
|
|
284
|
+
)
|
|
285
|
+
except Exception:
|
|
286
|
+
this_call = TokenUsage(
|
|
287
|
+
model=self.model,
|
|
288
|
+
prompt_tokens=prompt_t,
|
|
289
|
+
completion_tokens=completion_t,
|
|
290
|
+
cost_usd=None,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
this_call = TokenUsage(
|
|
294
|
+
model=self.model,
|
|
295
|
+
prompt_tokens=prompt_t,
|
|
296
|
+
completion_tokens=completion_t,
|
|
297
|
+
cost_usd=None,
|
|
298
|
+
)
|
|
299
|
+
if accum is None:
|
|
300
|
+
return this_call
|
|
301
|
+
return TokenUsage(
|
|
302
|
+
model=self.model,
|
|
303
|
+
prompt_tokens=accum.prompt_tokens + this_call.prompt_tokens,
|
|
304
|
+
completion_tokens=accum.completion_tokens + this_call.completion_tokens,
|
|
305
|
+
cost_usd=(accum.cost_usd or 0) + (this_call.cost_usd or 0)
|
|
306
|
+
if accum.cost_usd is not None or this_call.cost_usd is not None
|
|
307
|
+
else None,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# ----- main loop -----------------------------------------------------
|
|
311
|
+
|
|
312
|
+
async def __call__(self, agent_kid: bytes, query: str) -> ResponderResult:
|
|
313
|
+
system = self.system_prompt
|
|
314
|
+
if "sub_claims" not in system:
|
|
315
|
+
system = system + STRUCTURED_PROMPT_SUFFIX
|
|
316
|
+
|
|
317
|
+
# Some chat APIs require literal "json" in the messages when
|
|
318
|
+
# response_format=json_object — not used in the tool loop, but the
|
|
319
|
+
# final-answer round may use it. We always inject the hint.
|
|
320
|
+
user_content = (
|
|
321
|
+
f"{query}\n\n"
|
|
322
|
+
"Use tools if needed; otherwise respond with the final "
|
|
323
|
+
'json object: {"confidence": <0..1>, "reasoning": "..."}'
|
|
324
|
+
)
|
|
325
|
+
messages: list[dict[str, Any]] = [
|
|
326
|
+
{"role": "system", "content": system},
|
|
327
|
+
{"role": "user", "content": user_content},
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
token_usage: TokenUsage | None = None
|
|
331
|
+
tool_calls_made: list[dict[str, Any]] = []
|
|
332
|
+
|
|
333
|
+
for round_idx in range(self.max_tool_rounds + 1):
|
|
334
|
+
try:
|
|
335
|
+
kwargs: dict[str, Any] = {
|
|
336
|
+
"model": self.model,
|
|
337
|
+
"messages": messages,
|
|
338
|
+
"temperature": self.temperature,
|
|
339
|
+
"timeout": self.timeout_s,
|
|
340
|
+
}
|
|
341
|
+
# Only offer tools if we still have rounds to use them.
|
|
342
|
+
if round_idx < self.max_tool_rounds and self.tools:
|
|
343
|
+
kwargs["tools"] = self._tool_specs()
|
|
344
|
+
kwargs["tool_choice"] = "auto"
|
|
345
|
+
# Force JSON only on the final-answer round
|
|
346
|
+
if round_idx == self.max_tool_rounds or not self.tools:
|
|
347
|
+
kwargs["response_format"] = {"type": "json_object"}
|
|
348
|
+
resp = await self.client.chat.completions.create(**kwargs)
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
raise LLMResponderError(f"OpenAI call failed: {exc}") from exc
|
|
351
|
+
|
|
352
|
+
token_usage = self._accumulate_token_usage(
|
|
353
|
+
token_usage,
|
|
354
|
+
getattr(resp, "usage", None),
|
|
355
|
+
agent_kid,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
choice = resp.choices[0]
|
|
359
|
+
msg = choice.message
|
|
360
|
+
tool_calls = getattr(msg, "tool_calls", None) or []
|
|
361
|
+
|
|
362
|
+
if not tool_calls:
|
|
363
|
+
# Final answer turn
|
|
364
|
+
content = msg.content or "{}"
|
|
365
|
+
try:
|
|
366
|
+
structured = parse_structured_response(
|
|
367
|
+
content,
|
|
368
|
+
agent_kid=agent_kid,
|
|
369
|
+
token_usage=token_usage,
|
|
370
|
+
)
|
|
371
|
+
except ValueError as exc:
|
|
372
|
+
raise LLMResponderError(
|
|
373
|
+
f"Tool responder produced no parseable answer: {exc}"
|
|
374
|
+
) from exc
|
|
375
|
+
|
|
376
|
+
self.last_structured = structured
|
|
377
|
+
self.last_tool_calls = tool_calls_made
|
|
378
|
+
unc = confidence_to_uncertainty(
|
|
379
|
+
structured.confidence,
|
|
380
|
+
effective_n=self.effective_n,
|
|
381
|
+
)
|
|
382
|
+
return unc, structured
|
|
383
|
+
|
|
384
|
+
# Execute every tool call; append results as role=tool messages
|
|
385
|
+
messages.append(
|
|
386
|
+
{
|
|
387
|
+
"role": "assistant",
|
|
388
|
+
"content": msg.content,
|
|
389
|
+
"tool_calls": [
|
|
390
|
+
{
|
|
391
|
+
"id": tc.id,
|
|
392
|
+
"type": "function",
|
|
393
|
+
"function": {
|
|
394
|
+
"name": tc.function.name,
|
|
395
|
+
"arguments": tc.function.arguments,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
for tc in tool_calls
|
|
399
|
+
],
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
for tc in tool_calls:
|
|
404
|
+
tool_name = tc.function.name
|
|
405
|
+
args_json = tc.function.arguments
|
|
406
|
+
tool_result = await self._execute_tool(tool_name, args_json)
|
|
407
|
+
tool_calls_made.append(
|
|
408
|
+
{
|
|
409
|
+
"round": round_idx,
|
|
410
|
+
"tool": tool_name,
|
|
411
|
+
"args_json": args_json,
|
|
412
|
+
"result_truncated": tool_result[:500],
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
self._emit(
|
|
416
|
+
"tool_call",
|
|
417
|
+
{
|
|
418
|
+
"tool": tool_name,
|
|
419
|
+
"round": round_idx,
|
|
420
|
+
"agent_kid": agent_kid.hex(),
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
messages.append(
|
|
424
|
+
{
|
|
425
|
+
"role": "tool",
|
|
426
|
+
"tool_call_id": tc.id,
|
|
427
|
+
"content": tool_result,
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Exhausted rounds without a final answer
|
|
432
|
+
raise LLMResponderError(
|
|
433
|
+
f"ToolCallingResponder exceeded {self.max_tool_rounds} tool rounds "
|
|
434
|
+
"without producing a final JSON answer"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
__all__ = [
|
|
439
|
+
"Tool",
|
|
440
|
+
"ToolCallingResponder",
|
|
441
|
+
"query_knowledge_base",
|
|
442
|
+
"retrieve_document",
|
|
443
|
+
"web_search",
|
|
444
|
+
]
|