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