coco-framework 0.1.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 (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,1948 @@
1
+ """Base classes and utilities for Coco tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import inspect
7
+ import json
8
+ import logging
9
+ import typing
10
+ import warnings
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Callable, Mapping, Sequence
13
+ from inspect import signature
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Annotated,
17
+ Any,
18
+ Literal,
19
+ TypeVar,
20
+ cast,
21
+ get_args,
22
+ get_origin,
23
+ )
24
+
25
+ import typing_extensions
26
+ from pydantic import (
27
+ BaseModel,
28
+ ConfigDict,
29
+ Field,
30
+ PrivateAttr,
31
+ PydanticDeprecationWarning,
32
+ SkipValidation,
33
+ ValidationError,
34
+ validate_arguments,
35
+ )
36
+ from pydantic.fields import FieldInfo
37
+ from pydantic.v1 import BaseModel as BaseModelV1
38
+ from pydantic.v1 import ValidationError as ValidationErrorV1
39
+ from pydantic.v1 import validate_arguments as validate_arguments_v1
40
+ from typing_extensions import Self, get_type_hints, override
41
+
42
+ from coco_core.callbacks import (
43
+ AsyncCallbackManager,
44
+ CallbackManager,
45
+ Callbacks,
46
+ )
47
+ from coco_core.messages.tool import ToolCall, ToolMessage, ToolOutputMixin
48
+ from coco_core.runnables import (
49
+ RunnableConfig,
50
+ RunnableSerializable,
51
+ ensure_config,
52
+ patch_config,
53
+ run_in_executor,
54
+ )
55
+ from coco_core.runnables.config import set_config_context
56
+ from coco_core.runnables.utils import coro_with_context
57
+ from coco_core.utils.function_calling import (
58
+ _parse_google_docstring,
59
+ _py_38_safe_origin,
60
+ )
61
+ from coco_core.utils.pydantic import (
62
+ TypeBaseModel,
63
+ _create_subset_model,
64
+ get_fields,
65
+ is_basemodel_subclass,
66
+ is_pydantic_v1_subclass,
67
+ is_pydantic_v2_subclass,
68
+ model_json_schema,
69
+ )
70
+
71
+ if TYPE_CHECKING:
72
+ import uuid
73
+
74
+ FILTERED_ARGS = ("run_manager", "callbacks")
75
+ TOOL_MESSAGE_BLOCK_TYPES = (
76
+ "text",
77
+ "image_url",
78
+ "image",
79
+ "json",
80
+ "search_result",
81
+ "custom_tool_call_output",
82
+ "document",
83
+ "file",
84
+ )
85
+
86
+ _logger = logging.getLogger(__name__)
87
+
88
+
89
+ class SchemaAnnotationError(TypeError):
90
+ """Raised when `args_schema` is missing or has an incorrect type annotation."""
91
+
92
+
93
+ def _is_annotated_type(typ: type[Any]) -> bool:
94
+ """Check if a type is an `Annotated` type.
95
+
96
+ Args:
97
+ typ: The type to check.
98
+
99
+ Returns:
100
+ `True` if the type is an `Annotated` type, `False` otherwise.
101
+ """
102
+ return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}
103
+
104
+
105
+ def _get_annotation_description(arg_type: type) -> str | None:
106
+ """Extract description from an `Annotated` type.
107
+
108
+ Checks for string annotations and `FieldInfo` objects with descriptions.
109
+
110
+ Args:
111
+ arg_type: The type to extract description from.
112
+
113
+ Returns:
114
+ The description string if found, `None` otherwise.
115
+ """
116
+ if _is_annotated_type(arg_type):
117
+ annotated_args = get_args(arg_type)
118
+ for annotation in annotated_args[1:]:
119
+ if isinstance(annotation, str):
120
+ return annotation
121
+ if isinstance(annotation, FieldInfo) and annotation.description:
122
+ return annotation.description
123
+ return None
124
+
125
+
126
+ def _parse_python_function_docstring(
127
+ function: Callable[..., Any],
128
+ annotations: dict[str, Any],
129
+ *,
130
+ error_on_invalid_docstring: bool = False,
131
+ ) -> tuple[str, dict[str, str]]:
132
+ """Parse function and argument descriptions from a docstring.
133
+
134
+ Assumes the function docstring follows Google Python style guide.
135
+
136
+ Args:
137
+ function: The function to parse the docstring from.
138
+ annotations: Type annotations for the function parameters.
139
+ error_on_invalid_docstring: Whether to raise an error on invalid docstring.
140
+
141
+ Returns:
142
+ A tuple containing the function description and argument descriptions.
143
+ """
144
+ docstring = inspect.getdoc(function)
145
+ return _parse_google_docstring(
146
+ docstring,
147
+ list(annotations),
148
+ error_on_invalid_docstring=error_on_invalid_docstring,
149
+ )
150
+
151
+
152
+ def _validate_docstring_args_against_annotations(
153
+ arg_descriptions: dict[str, str], annotations: dict[str, Any]
154
+ ) -> None:
155
+ """Validate that docstring arguments match function annotations.
156
+
157
+ Args:
158
+ arg_descriptions: Arguments described in the docstring.
159
+ annotations: Type annotations from the function signature.
160
+
161
+ Raises:
162
+ ValueError: If a docstring argument is not found in function signature.
163
+ """
164
+ for docstring_arg in arg_descriptions:
165
+ if docstring_arg not in annotations:
166
+ msg = f"Arg {docstring_arg} in docstring not found in function signature."
167
+ raise ValueError(msg)
168
+
169
+
170
+ def _infer_arg_descriptions(
171
+ fn: Callable[..., Any],
172
+ *,
173
+ parse_docstring: bool = False,
174
+ error_on_invalid_docstring: bool = False,
175
+ ) -> tuple[str, dict[str, str]]:
176
+ """Infer argument descriptions from function docstring and annotations.
177
+
178
+ Args:
179
+ fn: The function to infer descriptions from.
180
+ parse_docstring: Whether to parse the docstring for descriptions.
181
+ error_on_invalid_docstring: Whether to raise error on invalid docstring.
182
+
183
+ Returns:
184
+ A tuple containing the function description and argument descriptions.
185
+ """
186
+ annotations = get_type_hints(fn, include_extras=True)
187
+ if parse_docstring:
188
+ description, arg_descriptions = _parse_python_function_docstring(
189
+ fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring
190
+ )
191
+ else:
192
+ description = inspect.getdoc(fn) or ""
193
+ arg_descriptions = {}
194
+ if parse_docstring:
195
+ _validate_docstring_args_against_annotations(arg_descriptions, annotations)
196
+ for arg, arg_type in annotations.items():
197
+ if arg in arg_descriptions:
198
+ continue
199
+ if desc := _get_annotation_description(arg_type):
200
+ arg_descriptions[arg] = desc
201
+ return description, arg_descriptions
202
+
203
+
204
+ def _is_pydantic_annotation(annotation: Any, pydantic_version: str = "v2") -> bool:
205
+ """Check if a type annotation is a Pydantic model.
206
+
207
+ Args:
208
+ annotation: The type annotation to check.
209
+ pydantic_version: The Pydantic version to check against (`'v1'` or `'v2'`).
210
+
211
+ Returns:
212
+ `True` if the annotation is a Pydantic model, `False` otherwise.
213
+ """
214
+ base_model_class = BaseModelV1 if pydantic_version == "v1" else BaseModel
215
+ try:
216
+ return issubclass(annotation, base_model_class)
217
+ except TypeError:
218
+ return False
219
+
220
+
221
+ def _function_annotations_are_pydantic_v1(
222
+ signature: inspect.Signature, func: Callable[..., Any]
223
+ ) -> bool:
224
+ """Check if all Pydantic annotations in a function are from v1.
225
+
226
+ Args:
227
+ signature: The function signature to check.
228
+ func: The function being checked.
229
+
230
+ Returns:
231
+ True if all Pydantic annotations are from v1, `False` otherwise.
232
+
233
+ Raises:
234
+ NotImplementedError: If the function contains mixed v1 and v2 annotations.
235
+ """
236
+ any_v1_annotations = any(
237
+ _is_pydantic_annotation(parameter.annotation, pydantic_version="v1")
238
+ for parameter in signature.parameters.values()
239
+ )
240
+ any_v2_annotations = any(
241
+ _is_pydantic_annotation(parameter.annotation, pydantic_version="v2")
242
+ for parameter in signature.parameters.values()
243
+ )
244
+ if any_v1_annotations and any_v2_annotations:
245
+ msg = (
246
+ f"Function {func} contains a mix of Pydantic v1 and v2 annotations. "
247
+ "Only one version of Pydantic annotations per function is supported."
248
+ )
249
+ raise NotImplementedError(msg)
250
+ return any_v1_annotations and not any_v2_annotations
251
+
252
+
253
+ class _SchemaConfig:
254
+ """Configuration for Pydantic models generated from function signatures."""
255
+
256
+ extra: str = "forbid"
257
+ """Whether to allow extra fields in the model."""
258
+
259
+ arbitrary_types_allowed: bool = True
260
+ """Whether to allow arbitrary types in the model."""
261
+
262
+
263
+ def create_schema_from_function(
264
+ model_name: str,
265
+ func: Callable[..., Any],
266
+ *,
267
+ filter_args: Sequence[str] | None = None,
268
+ parse_docstring: bool = False,
269
+ error_on_invalid_docstring: bool = False,
270
+ include_injected: bool = True,
271
+ ) -> TypeBaseModel:
272
+ """Create a Pydantic schema from a function's signature.
273
+
274
+ Args:
275
+ model_name: Name to assign to the generated Pydantic schema.
276
+ func: Function to generate the schema from.
277
+ filter_args: Optional list of arguments to exclude from the schema.
278
+
279
+ Defaults to `FILTERED_ARGS`.
280
+ parse_docstring: Whether to parse the function's docstring for descriptions
281
+ for each argument.
282
+ error_on_invalid_docstring: If `parse_docstring` is provided, configure
283
+ whether to raise `ValueError` on invalid Google Style docstrings.
284
+ include_injected: Whether to include injected arguments in the schema.
285
+
286
+ Defaults to `True`, since we want to include them in the schema when
287
+ *validating* tool inputs.
288
+
289
+ Returns:
290
+ A Pydantic model with the same arguments as the function.
291
+ """
292
+ sig = inspect.signature(func)
293
+
294
+ if _function_annotations_are_pydantic_v1(sig, func):
295
+ validated = validate_arguments_v1(func, config=_SchemaConfig) # type: ignore[call-overload]
296
+ else:
297
+ # https://docs.pydantic.dev/latest/usage/validation_decorator/
298
+ with warnings.catch_warnings():
299
+ # We are using deprecated functionality here.
300
+ # This code should be re-written to simply construct a Pydantic model
301
+ # using inspect.signature and create_model.
302
+ warnings.simplefilter("ignore", category=PydanticDeprecationWarning)
303
+ validated = validate_arguments(func, config=_SchemaConfig) # type: ignore[operator]
304
+
305
+ # Let's ignore `self` and `cls` arguments for class and instance methods
306
+ # If qualified name has a ".", then it likely belongs in a class namespace
307
+ in_class = bool(func.__qualname__ and "." in func.__qualname__)
308
+
309
+ has_args = False
310
+ has_kwargs = False
311
+
312
+ for param in sig.parameters.values():
313
+ if param.kind == param.VAR_POSITIONAL:
314
+ has_args = True
315
+ elif param.kind == param.VAR_KEYWORD:
316
+ has_kwargs = True
317
+
318
+ inferred_model = validated.model
319
+
320
+ if filter_args:
321
+ # Copy to avoid mutating the caller's sequence below.
322
+ filter_args_ = list(filter_args)
323
+ else:
324
+ # Handle classmethods and instance methods
325
+ existing_params: list[str] = list(sig.parameters.keys())
326
+ if existing_params and existing_params[0] in {"self", "cls"} and in_class:
327
+ filter_args_ = [existing_params[0], *list(FILTERED_ARGS)]
328
+ else:
329
+ filter_args_ = list(FILTERED_ARGS)
330
+
331
+ # Exclude injected args from the schema regardless of whether `filter_args`
332
+ # was provided. Injected args (e.g. `InjectedToolArg`, `ToolRuntime`) are
333
+ # supplied at runtime rather than by the model, so they should not appear in
334
+ # the generated schema.
335
+ if not include_injected:
336
+ for existing_param in sig.parameters:
337
+ if (
338
+ _is_injected_arg_type(sig.parameters[existing_param].annotation)
339
+ and existing_param not in filter_args_
340
+ ):
341
+ filter_args_.append(existing_param)
342
+
343
+ description, arg_descriptions = _infer_arg_descriptions(
344
+ func,
345
+ parse_docstring=parse_docstring,
346
+ error_on_invalid_docstring=error_on_invalid_docstring,
347
+ )
348
+ # Pydantic adds placeholder virtual fields we need to strip
349
+ valid_properties = []
350
+ for field in get_fields(inferred_model):
351
+ if not has_args and field == "args":
352
+ continue
353
+ if not has_kwargs and field == "kwargs":
354
+ continue
355
+
356
+ if field == "v__duplicate_kwargs": # Internal pydantic field
357
+ continue
358
+
359
+ if field not in filter_args_:
360
+ valid_properties.append(field)
361
+
362
+ return _create_subset_model(
363
+ model_name,
364
+ inferred_model,
365
+ list(valid_properties),
366
+ descriptions=arg_descriptions,
367
+ fn_description=description,
368
+ )
369
+
370
+
371
+ class ToolException(Exception): # noqa: N818
372
+ """Exception thrown when a tool execution error occurs.
373
+
374
+ This exception allows tools to signal errors without stopping the agent.
375
+
376
+ The error is handled according to the tool's `handle_tool_error` setting, and the
377
+ result is returned as an observation to the agent.
378
+ """
379
+
380
+
381
+ ArgsSchema = TypeBaseModel | dict[str, Any]
382
+ MessageContentBlock = str | dict[str, Any]
383
+ """A single message content block: plain text or a structured block.
384
+
385
+ A dict block is only considered valid at runtime when its `type` key is one of
386
+ `TOOL_MESSAGE_BLOCK_TYPES` (see `_is_message_content_block`); the static type
387
+ intentionally stays broad because block payloads vary by provider format.
388
+ """
389
+ ToolExceptionHandlerOutput = str | Sequence[MessageContentBlock]
390
+ """Content returned by a `handle_tool_error` callable.
391
+
392
+ Error handlers may return plain text or a sequence of structured message
393
+ content blocks. When the original tool call includes a `tool_call_id`, this
394
+ content is normalized to the content of a `ToolMessage` with `status="error"`.
395
+ """
396
+
397
+ _EMPTY_SET: frozenset[str] = frozenset()
398
+
399
+
400
+ _TOOL_CALL_SCHEMA_FIELDS = frozenset({"name", "description", "args_schema"})
401
+ """Fields the memoized `tool_call_schema` is built from; reassignment clears it."""
402
+
403
+
404
+ def _patch_json_schema_cache(model_cls: type) -> None:
405
+ """Patch `model_json_schema` (or `schema` for pydantic v1) to cache.
406
+
407
+ Pydantic regenerates the full JSON-schema dict on every
408
+ `model_json_schema()` call — there is no per-class cache. When the
409
+ model class is stable (memoized on a `BaseTool` instance), this patch
410
+ caches the dict on the class so repeated calls return instantly.
411
+
412
+ Only calls with all-default arguments are cached; any explicit arguments
413
+ bypass the cache and delegate to the original method.
414
+ """
415
+ method_name = (
416
+ "model_json_schema" if hasattr(model_cls, "model_json_schema") else "schema"
417
+ )
418
+ orig = getattr(model_cls, method_name)
419
+
420
+ def _cached_json_schema(cls: type, *args: Any, **kwargs: Any) -> dict[str, Any]:
421
+ if not args and not kwargs:
422
+ cached = cls.__dict__.get("_json_schema_cache")
423
+ if cached is not None:
424
+ return cast("dict[str, Any]", cached)
425
+ result = orig(*args, **kwargs)
426
+ if not args and not kwargs:
427
+ cls._json_schema_cache = result # type: ignore[attr-defined]
428
+ return cast("dict[str, Any]", result)
429
+
430
+ setattr(model_cls, method_name, classmethod(_cached_json_schema))
431
+
432
+
433
+ class BaseTool(RunnableSerializable[str | dict[str, Any] | ToolCall, Any]):
434
+ """Base class for all Coco tools.
435
+
436
+ This abstract class defines the interface that all Coco tools must implement.
437
+
438
+ Tools are components that can be called by agents to perform specific actions.
439
+ """
440
+
441
+ def __init_subclass__(cls, **kwargs: Any) -> None:
442
+ """Validate the tool class definition during subclass creation.
443
+
444
+ Args:
445
+ **kwargs: Additional keyword arguments passed to the parent class.
446
+
447
+ Raises:
448
+ SchemaAnnotationError: If `args_schema` has incorrect type annotation.
449
+ """
450
+ super().__init_subclass__(**kwargs)
451
+
452
+ args_schema_type = cls.__annotations__.get("args_schema", None)
453
+
454
+ if args_schema_type is not None and args_schema_type == BaseModel:
455
+ # Throw errors for common mis-annotations.
456
+ # TODO: Use get_args / get_origin and fully
457
+ # specify valid annotations.
458
+ typehint_mandate = """
459
+ class ChildTool(BaseTool):
460
+ ...
461
+ args_schema: Type[BaseModel] = SchemaClass
462
+ ..."""
463
+ name = cls.__name__
464
+ msg = (
465
+ f"Tool definition for {name} must include valid type annotations"
466
+ f" for argument 'args_schema' to behave as expected.\n"
467
+ f"Expected annotation of 'Type[BaseModel]'"
468
+ f" but got '{args_schema_type}'.\n"
469
+ f"Expected class looks like:\n"
470
+ f"{typehint_mandate}"
471
+ )
472
+ raise SchemaAnnotationError(msg)
473
+
474
+ name: str
475
+ """The unique name of the tool that clearly communicates its purpose."""
476
+
477
+ description: str
478
+ """Used to tell the model how/when/why to use the tool.
479
+
480
+ You can provide few-shot examples as a part of the description.
481
+ """
482
+
483
+ args_schema: Annotated[ArgsSchema | None, SkipValidation()] = Field(
484
+ default=None, description="The tool schema."
485
+ )
486
+ """Pydantic model class to validate and parse the tool's input arguments.
487
+
488
+ Args schema should be either:
489
+
490
+ - A subclass of `pydantic.BaseModel`.
491
+ - A subclass of `pydantic.v1.BaseModel` if accessing v1 namespace in pydantic 2
492
+ - A JSON schema dict
493
+ """
494
+
495
+ return_direct: bool = False
496
+ """Whether to return the tool's output directly.
497
+
498
+ Setting this to `True` means that after the tool is called, the `AgentExecutor` will
499
+ stop looping.
500
+ """
501
+
502
+ verbose: bool = False
503
+ """Whether to log the tool's progress."""
504
+
505
+ callbacks: Callbacks = Field(default=None, exclude=True)
506
+ """Callbacks to be called during tool execution."""
507
+
508
+ tags: list[str] | None = None
509
+ """Optional list of tags associated with the tool.
510
+
511
+ These tags will be associated with each call to this tool,
512
+ and passed as arguments to the handlers defined in `callbacks`.
513
+
514
+ You can use these to, e.g., identify a specific instance of a tool with its use
515
+ case.
516
+ """
517
+
518
+ metadata: dict[str, Any] | None = None
519
+ """Optional metadata associated with the tool.
520
+
521
+ This metadata will be associated with each call to this tool,
522
+ and passed as arguments to the handlers defined in `callbacks`.
523
+
524
+ You can use these to, e.g., identify a specific instance of a tool with its usecase.
525
+ """
526
+
527
+ handle_tool_error: (
528
+ bool | str | Callable[[ToolException], ToolExceptionHandlerOutput] | None
529
+ ) = False
530
+ """Handle `ToolException` raised by tool execution.
531
+
532
+ If `False`, the exception is re-raised. If `True`, the exception message is
533
+ returned as tool output. If a string is passed, that string is returned
534
+ as tool output. If a callable is passed, it receives the exception and
535
+ its return value is used as the tool output.
536
+
537
+ Callable handlers may return either a string or a list of message
538
+ content blocks. If the tool was invoked with a `tool_call_id`, the handled
539
+ content is wrapped in a `ToolMessage` with `status="error"`.
540
+ """
541
+
542
+ handle_validation_error: (
543
+ bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None
544
+ ) = False
545
+ """Handle the content of the `ValidationError` thrown."""
546
+
547
+ response_format: Literal["content", "content_and_artifact"] = "content"
548
+ """The tool response format.
549
+
550
+ If `'content'` then the output of the tool is interpreted as the contents of a
551
+ `ToolMessage`. If `'content_and_artifact'` then the output is expected to be a
552
+ two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.
553
+ """
554
+
555
+ extras: dict[str, Any] | None = None
556
+ """Optional provider-specific extra fields for the tool.
557
+
558
+ This is used to pass provider-specific configuration that doesn't fit into
559
+ standard tool fields.
560
+
561
+ Example:
562
+ Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),
563
+ [`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),
564
+ or `input_examples`.
565
+
566
+ ```python
567
+ @tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})
568
+ def my_tool(x: str) -> str:
569
+ return x
570
+ ```
571
+ """
572
+
573
+ def __init__(self, **kwargs: Any) -> None:
574
+ """Initialize the tool.
575
+
576
+ Raises:
577
+ TypeError: If `args_schema` is not a subclass of pydantic `BaseModel` or
578
+ `dict`.
579
+ """
580
+ if (
581
+ "args_schema" in kwargs
582
+ and kwargs["args_schema"] is not None
583
+ and not is_basemodel_subclass(kwargs["args_schema"])
584
+ and not isinstance(kwargs["args_schema"], dict)
585
+ ):
586
+ msg = (
587
+ "args_schema must be a subclass of pydantic BaseModel or "
588
+ f"a JSON schema dict. Got: {kwargs['args_schema']}."
589
+ )
590
+ raise TypeError(msg)
591
+ super().__init__(**kwargs)
592
+
593
+ model_config = ConfigDict(
594
+ arbitrary_types_allowed=True,
595
+ )
596
+
597
+ @property
598
+ def is_single_input(self) -> bool:
599
+ """Check if the tool accepts only a single input argument.
600
+
601
+ Returns:
602
+ `True` if the tool has only one input argument, `False` otherwise.
603
+ """
604
+ keys = {k for k in self.args if k != "kwargs"}
605
+ return len(keys) == 1
606
+
607
+ @property
608
+ def args(self) -> dict[str, Any]:
609
+ """Get the tool's input arguments schema.
610
+
611
+ Returns:
612
+ `dict` containing the tool's argument properties.
613
+ """
614
+ if isinstance(self.args_schema, dict):
615
+ json_schema = self.args_schema
616
+ else:
617
+ input_schema = self.tool_call_schema
618
+ if isinstance(input_schema, dict):
619
+ json_schema = input_schema
620
+ else:
621
+ json_schema = model_json_schema(input_schema)
622
+ return cast("dict[str, Any]", json_schema["properties"])
623
+
624
+ _tool_call_schema_memo: ArgsSchema | None = PrivateAttr(default=None)
625
+ """Memoized `tool_call_schema` result.
626
+
627
+ Building the subset model is expensive, and pydantic does not cache
628
+ `model_json_schema()` per class, so agent loops would otherwise pay full
629
+ schema generation for every tool on every model call. The subset model
630
+ class is memoized here and its `model_json_schema`/`schema` method is
631
+ patched to cache the generated dict, so both costs are paid only once per
632
+ tool instance.
633
+ Cleared whenever `name`, `description`, or `args_schema` is reassigned (see
634
+ `__setattr__` and `model_copy`).
635
+ """
636
+
637
+ @override
638
+ def __setattr__(self, name: str, value: Any) -> None:
639
+ """Clear the tool-call schema memo when an input to it is reassigned."""
640
+ super().__setattr__(name, value)
641
+ if name in _TOOL_CALL_SCHEMA_FIELDS and self.__pydantic_private__ is not None:
642
+ self._tool_call_schema_memo = None
643
+
644
+ @override
645
+ def model_copy(
646
+ self, *, update: Mapping[str, Any] | None = None, deep: bool = False
647
+ ) -> Self:
648
+ """Copy the tool, clearing the schema memo if `update` affects it.
649
+
650
+ `model_copy` writes `update` directly to the copy's `__dict__` without
651
+ going through `__setattr__`, and private attributes (including the
652
+ memo) carry over to the copy, so the memo is cleared here when the
653
+ update touches one of the fields the schema is built from.
654
+ """
655
+ copied = super().model_copy(update=update, deep=deep)
656
+ if update and not _TOOL_CALL_SCHEMA_FIELDS.isdisjoint(update):
657
+ copied._tool_call_schema_memo = None # noqa: SLF001
658
+ return copied
659
+
660
+ def __getstate__(self) -> dict[Any, Any]:
661
+ """Drop the tool-call schema memo when pickling.
662
+
663
+ The memoized subset model is a dynamically created class that cannot be
664
+ pickled by reference; it is rebuilt lazily on next access.
665
+ """
666
+ state = super().__getstate__()
667
+ private = state.get("__pydantic_private__")
668
+ if private and private.get("_tool_call_schema_memo") is not None:
669
+ state = dict(state)
670
+ state["__pydantic_private__"] = {
671
+ **private,
672
+ "_tool_call_schema_memo": None,
673
+ }
674
+ return state
675
+
676
+ @property
677
+ def tool_call_schema(self) -> ArgsSchema:
678
+ """Get the schema for tool calls, excluding injected arguments.
679
+
680
+ Returns:
681
+ The schema that should be used for tool calls from language models.
682
+
683
+ The returned model class is memoized per tool instance (invalidated
684
+ when `name`, `description`, or `args_schema` is reassigned) so
685
+ repeated access does not regenerate the class. The class's
686
+ `model_json_schema` method is also patched to cache the generated
687
+ schema dict, since pydantic does not cache it per class.
688
+ """
689
+ if isinstance(self.args_schema, dict):
690
+ if self.description:
691
+ return {
692
+ **self.args_schema,
693
+ "description": self.description,
694
+ }
695
+
696
+ return self.args_schema
697
+
698
+ if (memo := self._tool_call_schema_memo) is not None:
699
+ return memo
700
+
701
+ full_schema = self.get_input_schema()
702
+ fields = []
703
+
704
+ # Accommodates a condition where forward references were not resolved
705
+ # during model construction. At introspection time, we fail fast if
706
+ # the model schema is not complete so the underlying serialized schema
707
+ # doesn't narrow the propreties in the tool json schema to an empty dict
708
+ if (
709
+ is_pydantic_v2_subclass(full_schema)
710
+ and not full_schema.__pydantic_complete__
711
+ ):
712
+ full_schema.model_rebuild()
713
+
714
+ for name, type_ in get_all_basemodel_annotations(full_schema).items():
715
+ if not _is_injected_arg_type(type_):
716
+ fields.append(name)
717
+ subset_model = _create_subset_model(
718
+ self.name, full_schema, fields, fn_description=self.description
719
+ )
720
+ _patch_json_schema_cache(subset_model)
721
+ self._tool_call_schema_memo = subset_model
722
+ return subset_model
723
+
724
+ @functools.cached_property
725
+ def _injected_args_keys(self) -> frozenset[str]:
726
+ # Inspect the tool's `_run` (falling back to `_arun` for async-only
727
+ # subclasses) for directly injected args like `ToolRuntime` or args
728
+ # annotated with `InjectedToolArg`. These are supplied at call time
729
+ # rather than by the model, so they must be excluded from the schema and
730
+ # re-injected during execution. `StructuredTool` overrides this to
731
+ # inspect its wrapped `func`/`coroutine` instead.
732
+ for method in (self._run, self._arun):
733
+ keys = _get_injected_args_keys_from_signature(method)
734
+ if keys:
735
+ return keys
736
+ return _EMPTY_SET
737
+
738
+ # --- Runnable ---
739
+
740
+ @override
741
+ def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
742
+ """The tool's input schema.
743
+
744
+ Args:
745
+ config: The configuration for the tool.
746
+
747
+ Returns:
748
+ The input schema for the tool.
749
+ """
750
+ if self.args_schema is not None:
751
+ if isinstance(self.args_schema, dict):
752
+ return super().get_input_schema(config)
753
+ return self.args_schema
754
+ return create_schema_from_function(self.name, self._run)
755
+
756
+ @override
757
+ def invoke(
758
+ self,
759
+ input: str | dict[str, Any] | ToolCall,
760
+ config: RunnableConfig | None = None,
761
+ **kwargs: Any,
762
+ ) -> Any:
763
+ tool_input, kwargs = _prep_run_args(input, config, **kwargs)
764
+ return self.run(tool_input, **kwargs)
765
+
766
+ @override
767
+ async def ainvoke(
768
+ self,
769
+ input: str | dict[str, Any] | ToolCall,
770
+ config: RunnableConfig | None = None,
771
+ **kwargs: Any,
772
+ ) -> Any:
773
+ tool_input, kwargs = _prep_run_args(input, config, **kwargs)
774
+ return await self.arun(tool_input, **kwargs)
775
+
776
+ # --- Tool ---
777
+
778
+ def _parse_input(
779
+ self, tool_input: str | dict[str, Any], tool_call_id: str | None
780
+ ) -> str | dict[str, Any]:
781
+ """Parse and validate tool input using the args schema.
782
+
783
+ Args:
784
+ tool_input: The raw input to the tool.
785
+ tool_call_id: The ID of the tool call, if available.
786
+
787
+ Returns:
788
+ The parsed and validated input.
789
+
790
+ Raises:
791
+ ValueError: If `string` input is provided with JSON schema `args_schema`.
792
+ ValueError: If `InjectedToolCallId` is required but `tool_call_id` is not
793
+ provided.
794
+ TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.
795
+ """
796
+ input_args = self.args_schema
797
+
798
+ if isinstance(tool_input, str):
799
+ if input_args is not None:
800
+ if isinstance(input_args, dict):
801
+ msg = (
802
+ "String tool inputs are not allowed when "
803
+ "using tools with JSON schema args_schema."
804
+ )
805
+ raise ValueError(msg)
806
+ key_ = next(iter(get_fields(input_args).keys()))
807
+ if issubclass(input_args, BaseModel):
808
+ input_args.model_validate({key_: tool_input})
809
+ elif issubclass(input_args, BaseModelV1):
810
+ input_args.parse_obj({key_: tool_input})
811
+ else:
812
+ msg = f"args_schema must be a Pydantic BaseModel, got {input_args}" # type: ignore[unreachable]
813
+ raise TypeError(msg)
814
+ return tool_input
815
+
816
+ if input_args is not None:
817
+ if isinstance(input_args, dict):
818
+ return tool_input
819
+ result: BaseModel | BaseModelV1
820
+ if issubclass(input_args, BaseModel):
821
+ # Check args_schema for InjectedToolCallId
822
+ for k, v in get_all_basemodel_annotations(input_args).items():
823
+ if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
824
+ if tool_call_id is None:
825
+ msg = (
826
+ "When tool includes an InjectedToolCallId "
827
+ "argument, tool must always be invoked with a full "
828
+ "model ToolCall of the form: {'args': {...}, "
829
+ "'name': '...', 'type': 'tool_call', "
830
+ "'tool_call_id': '...'}"
831
+ )
832
+ raise ValueError(msg)
833
+ tool_input[k] = tool_call_id
834
+ result_v2 = input_args.model_validate(tool_input)
835
+ result_dict = result_v2.model_dump()
836
+ provided_fields = result_v2.model_fields_set
837
+ result = result_v2
838
+ elif issubclass(input_args, BaseModelV1):
839
+ # Check args_schema for InjectedToolCallId
840
+ for k, v in get_all_basemodel_annotations(input_args).items():
841
+ if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
842
+ if tool_call_id is None:
843
+ msg = (
844
+ "When tool includes an InjectedToolCallId "
845
+ "argument, tool must always be invoked with a full "
846
+ "model ToolCall of the form: {'args': {...}, "
847
+ "'name': '...', 'type': 'tool_call', "
848
+ "'tool_call_id': '...'}"
849
+ )
850
+ raise ValueError(msg)
851
+ tool_input[k] = tool_call_id
852
+ result_v1 = input_args.parse_obj(tool_input)
853
+ result_dict = result_v1.dict()
854
+ provided_fields = result_v1.__fields_set__
855
+ result = result_v1
856
+ else:
857
+ msg = ( # type: ignore[unreachable]
858
+ f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
859
+ )
860
+ raise NotImplementedError(msg)
861
+
862
+ # Include fields from tool_input, fields provided through Pydantic aliases,
863
+ # plus fields with explicit defaults. This applies Pydantic defaults (like
864
+ # Field(default=1)) while excluding synthetic "args"/"kwargs" fields that
865
+ # Pydantic creates for *args/**kwargs.
866
+ field_info = get_fields(input_args)
867
+ validated_input = {}
868
+ for k in result_dict:
869
+ if k in tool_input:
870
+ # Field was provided in input - include it (validated)
871
+ validated_input[k] = getattr(result, k)
872
+ elif k in provided_fields:
873
+ # Field was provided through a Pydantic alias - include it.
874
+ validated_input[k] = getattr(result, k)
875
+ elif k in field_info and k not in {"args", "kwargs"}:
876
+ # Check if field has an explicit default defined in the schema.
877
+ # Exclude "args"/"kwargs" as these are synthetic fields for variadic
878
+ # parameters that should not be passed as keyword arguments.
879
+ fi = field_info[k]
880
+ # Pydantic v2 uses is_required() method, v1 uses required attribute
881
+ has_default = (
882
+ not fi.is_required()
883
+ if hasattr(fi, "is_required")
884
+ else not getattr(fi, "required", True)
885
+ )
886
+ if has_default:
887
+ validated_input[k] = getattr(result, k)
888
+
889
+ for k in self._injected_args_keys:
890
+ if k in tool_input:
891
+ validated_input[k] = tool_input[k]
892
+ elif k == "tool_call_id":
893
+ if tool_call_id is None:
894
+ msg = (
895
+ "When tool includes an InjectedToolCallId "
896
+ "argument, tool must always be invoked with a full "
897
+ "model ToolCall of the form: {'args': {...}, "
898
+ "'name': '...', 'type': 'tool_call', "
899
+ "'tool_call_id': '...'}"
900
+ )
901
+ raise ValueError(msg)
902
+ validated_input[k] = tool_call_id
903
+
904
+ return validated_input
905
+
906
+ return tool_input
907
+
908
+ @abstractmethod
909
+ def _run(self, *args: Any, **kwargs: Any) -> Any:
910
+ """Use the tool.
911
+
912
+ Add `run_manager: CallbackManagerForToolRun | None = None` to child
913
+ implementations to enable tracing.
914
+
915
+ Returns:
916
+ The result of the tool execution.
917
+ """
918
+
919
+ async def _arun(self, *args: Any, **kwargs: Any) -> Any:
920
+ """Use the tool asynchronously.
921
+
922
+ Add `run_manager: AsyncCallbackManagerForToolRun | None = None` to child
923
+ implementations to enable tracing.
924
+
925
+ Returns:
926
+ The result of the tool execution.
927
+ """
928
+ if kwargs.get("run_manager") and signature(self._run).parameters.get(
929
+ "run_manager"
930
+ ):
931
+ kwargs["run_manager"] = kwargs["run_manager"].get_sync()
932
+ return await run_in_executor(None, self._run, *args, **kwargs)
933
+
934
+ def _filter_injected_args(self, tool_input: dict[str, Any]) -> dict[str, Any]:
935
+ """Filter out injected tool arguments from the input dictionary.
936
+
937
+ Injected arguments are those annotated with `InjectedToolArg` or its
938
+ subclasses, or arguments in `FILTERED_ARGS` like `run_manager` and callbacks.
939
+
940
+ Args:
941
+ tool_input: The tool input dictionary to filter.
942
+
943
+ Returns:
944
+ A filtered dictionary with injected arguments removed.
945
+ """
946
+ # Start with filtered args from the constant
947
+ filtered_keys = set[str](FILTERED_ARGS)
948
+
949
+ # Add injected args from function signature (e.g., ToolRuntime parameters)
950
+ filtered_keys.update(self._injected_args_keys)
951
+
952
+ # If we have an args_schema, use it to identify injected args
953
+ # Skip if args_schema is a dict (JSON Schema) as it's not a Pydantic model
954
+ if self.args_schema is not None and not isinstance(self.args_schema, dict):
955
+ try:
956
+ annotations = get_all_basemodel_annotations(self.args_schema)
957
+ for field_name, field_type in annotations.items():
958
+ if _is_injected_arg_type(field_type):
959
+ filtered_keys.add(field_name)
960
+ except Exception:
961
+ # If we can't get annotations, just use FILTERED_ARGS
962
+ _logger.debug(
963
+ "Failed to get args_schema annotations for filtering.",
964
+ exc_info=True,
965
+ )
966
+
967
+ # Filter out the injected keys from tool_input
968
+ return {k: v for k, v in tool_input.items() if k not in filtered_keys}
969
+
970
+ def _to_args_and_kwargs(
971
+ self, tool_input: str | dict[str, Any], tool_call_id: str | None
972
+ ) -> tuple[tuple[str, ...], dict[str, Any]]:
973
+ """Convert tool input to positional and keyword arguments.
974
+
975
+ Args:
976
+ tool_input: The input to the tool.
977
+ tool_call_id: The ID of the tool call, if available.
978
+
979
+ Returns:
980
+ A tuple of `(positional_args, keyword_args)` for the tool.
981
+
982
+ Raises:
983
+ TypeError: If the tool input type is invalid.
984
+ """
985
+ if (
986
+ self.args_schema is not None
987
+ and isinstance(self.args_schema, type)
988
+ and is_basemodel_subclass(self.args_schema)
989
+ and not get_fields(self.args_schema)
990
+ ):
991
+ # StructuredTool with no args
992
+ return (), {}
993
+ tool_input = self._parse_input(tool_input, tool_call_id)
994
+ # For backwards compatibility, if run_input is a string,
995
+ # pass as a positional argument.
996
+ if isinstance(tool_input, str):
997
+ return (tool_input,), {}
998
+ if isinstance(tool_input, dict):
999
+ # Make a shallow copy of the input to allow downstream code
1000
+ # to modify the root level of the input without affecting the
1001
+ # original input.
1002
+ # This is used by the tool to inject run time information like
1003
+ # the callback manager.
1004
+ return (), tool_input.copy()
1005
+ # This code path is not expected to be reachable.
1006
+ msg = f"Invalid tool input type: {type(tool_input)}" # type: ignore[unreachable]
1007
+ raise TypeError(msg)
1008
+
1009
+ def run(
1010
+ self,
1011
+ tool_input: str | dict[str, Any],
1012
+ verbose: bool | None = None, # noqa: FBT001
1013
+ start_color: str | None = "green",
1014
+ color: str | None = "green",
1015
+ callbacks: Callbacks = None,
1016
+ *,
1017
+ tags: list[str] | None = None,
1018
+ metadata: dict[str, Any] | None = None,
1019
+ run_name: str | None = None,
1020
+ run_id: uuid.UUID | None = None,
1021
+ config: RunnableConfig | None = None,
1022
+ tool_call_id: str | None = None,
1023
+ **kwargs: Any,
1024
+ ) -> Any:
1025
+ """Run the tool.
1026
+
1027
+ Args:
1028
+ tool_input: The input to the tool.
1029
+ verbose: Whether to log the tool's progress.
1030
+ start_color: The color to use when starting the tool.
1031
+ color: The color to use when ending the tool.
1032
+ callbacks: Callbacks to be called during tool execution.
1033
+ tags: Optional list of tags associated with the tool.
1034
+ metadata: Optional metadata associated with the tool.
1035
+ run_name: The name of the run.
1036
+ run_id: The id of the run.
1037
+ config: The configuration for the tool.
1038
+ tool_call_id: The id of the tool call.
1039
+ **kwargs: Keyword arguments to be passed to tool callbacks (event handler)
1040
+
1041
+ Returns:
1042
+ The output of the tool.
1043
+
1044
+ Raises:
1045
+ ToolException: If an error occurs during tool execution.
1046
+ """
1047
+ callback_manager = CallbackManager.configure(
1048
+ callbacks,
1049
+ self.callbacks,
1050
+ self.verbose or bool(verbose),
1051
+ tags,
1052
+ self.tags,
1053
+ metadata,
1054
+ self.metadata,
1055
+ )
1056
+
1057
+ # Filter out injected arguments from callback inputs
1058
+ filtered_tool_input = (
1059
+ self._filter_injected_args(tool_input)
1060
+ if isinstance(tool_input, dict)
1061
+ else None
1062
+ )
1063
+
1064
+ # Use filtered inputs for the input_str parameter as well
1065
+ tool_input_str = (
1066
+ tool_input
1067
+ if isinstance(tool_input, str)
1068
+ else str(
1069
+ filtered_tool_input if filtered_tool_input is not None else tool_input
1070
+ )
1071
+ )
1072
+
1073
+ run_manager = callback_manager.on_tool_start(
1074
+ {"name": self.name, "description": self.description},
1075
+ tool_input_str,
1076
+ color=start_color,
1077
+ name=run_name,
1078
+ run_id=run_id,
1079
+ inputs=filtered_tool_input,
1080
+ tool_call_id=tool_call_id,
1081
+ **kwargs,
1082
+ )
1083
+
1084
+ content = None
1085
+ artifact = None
1086
+ status = "success"
1087
+ error_to_raise: Exception | KeyboardInterrupt | None = None
1088
+ try:
1089
+ child_config = patch_config(config, callbacks=run_manager.get_child())
1090
+ with set_config_context(child_config) as context:
1091
+ tool_args, tool_kwargs = self._to_args_and_kwargs(
1092
+ tool_input, tool_call_id
1093
+ )
1094
+ if signature(self._run).parameters.get("run_manager"):
1095
+ tool_kwargs |= {"run_manager": run_manager}
1096
+ if config_param := _get_runnable_config_param(self._run):
1097
+ tool_kwargs |= {config_param: config}
1098
+ response = context.run(self._run, *tool_args, **tool_kwargs)
1099
+ if self.response_format == "content_and_artifact":
1100
+ msg = (
1101
+ "Since response_format='content_and_artifact' "
1102
+ "a two-tuple of the message content and raw tool output is "
1103
+ f"expected. Instead, generated response is of type: "
1104
+ f"{type(response)}."
1105
+ )
1106
+ if not isinstance(response, tuple):
1107
+ error_to_raise = ValueError(msg)
1108
+ else:
1109
+ try:
1110
+ content, artifact = response
1111
+ except ValueError:
1112
+ error_to_raise = ValueError(msg)
1113
+ else:
1114
+ content = response
1115
+ except (ValidationError, ValidationErrorV1) as e:
1116
+ if not self.handle_validation_error:
1117
+ error_to_raise = e
1118
+ else:
1119
+ content = _handle_validation_error(e, flag=self.handle_validation_error)
1120
+ status = "error"
1121
+ except ToolException as e:
1122
+ if not self.handle_tool_error:
1123
+ error_to_raise = e
1124
+ else:
1125
+ content = _handle_tool_error(e, flag=self.handle_tool_error)
1126
+ status = "error"
1127
+ except (Exception, KeyboardInterrupt) as e:
1128
+ error_to_raise = e
1129
+
1130
+ if error_to_raise:
1131
+ run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
1132
+ raise error_to_raise
1133
+ output = _format_output(content, artifact, tool_call_id, self.name, status)
1134
+ run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
1135
+ return output
1136
+
1137
+ async def arun(
1138
+ self,
1139
+ tool_input: str | dict[str, Any],
1140
+ verbose: bool | None = None, # noqa: FBT001
1141
+ start_color: str | None = "green",
1142
+ color: str | None = "green",
1143
+ callbacks: Callbacks = None,
1144
+ *,
1145
+ tags: list[str] | None = None,
1146
+ metadata: dict[str, Any] | None = None,
1147
+ run_name: str | None = None,
1148
+ run_id: uuid.UUID | None = None,
1149
+ config: RunnableConfig | None = None,
1150
+ tool_call_id: str | None = None,
1151
+ **kwargs: Any,
1152
+ ) -> Any:
1153
+ """Run the tool asynchronously.
1154
+
1155
+ Args:
1156
+ tool_input: The input to the tool.
1157
+ verbose: Whether to log the tool's progress.
1158
+ start_color: The color to use when starting the tool.
1159
+ color: The color to use when ending the tool.
1160
+ callbacks: Callbacks to be called during tool execution.
1161
+ tags: Optional list of tags associated with the tool.
1162
+ metadata: Optional metadata associated with the tool.
1163
+ run_name: The name of the run.
1164
+ run_id: The id of the run.
1165
+ config: The configuration for the tool.
1166
+ tool_call_id: The id of the tool call.
1167
+ **kwargs: Keyword arguments to be passed to tool callbacks
1168
+
1169
+ Returns:
1170
+ The output of the tool.
1171
+
1172
+ Raises:
1173
+ ToolException: If an error occurs during tool execution.
1174
+ """
1175
+ callback_manager = AsyncCallbackManager.configure(
1176
+ callbacks,
1177
+ self.callbacks,
1178
+ self.verbose or bool(verbose),
1179
+ tags,
1180
+ self.tags,
1181
+ metadata,
1182
+ self.metadata,
1183
+ )
1184
+
1185
+ # Filter out injected arguments from callback inputs
1186
+ filtered_tool_input = (
1187
+ self._filter_injected_args(tool_input)
1188
+ if isinstance(tool_input, dict)
1189
+ else None
1190
+ )
1191
+
1192
+ # Use filtered inputs for the input_str parameter as well
1193
+ tool_input_str = (
1194
+ tool_input
1195
+ if isinstance(tool_input, str)
1196
+ else str(
1197
+ filtered_tool_input if filtered_tool_input is not None else tool_input
1198
+ )
1199
+ )
1200
+
1201
+ run_manager = await callback_manager.on_tool_start(
1202
+ {"name": self.name, "description": self.description},
1203
+ tool_input_str,
1204
+ color=start_color,
1205
+ name=run_name,
1206
+ run_id=run_id,
1207
+ inputs=filtered_tool_input,
1208
+ tool_call_id=tool_call_id,
1209
+ **kwargs,
1210
+ )
1211
+ content = None
1212
+ artifact = None
1213
+ status = "success"
1214
+ error_to_raise: Exception | KeyboardInterrupt | None = None
1215
+ try:
1216
+ tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)
1217
+ child_config = patch_config(config, callbacks=run_manager.get_child())
1218
+ with set_config_context(child_config) as context:
1219
+ func_to_check = (
1220
+ self._run if self.__class__._arun is BaseTool._arun else self._arun # noqa: SLF001
1221
+ )
1222
+ if signature(func_to_check).parameters.get("run_manager"):
1223
+ tool_kwargs["run_manager"] = run_manager
1224
+ if config_param := _get_runnable_config_param(func_to_check):
1225
+ tool_kwargs[config_param] = config
1226
+
1227
+ coro = self._arun(*tool_args, **tool_kwargs)
1228
+ response = await coro_with_context(coro, context)
1229
+ if self.response_format == "content_and_artifact":
1230
+ msg = (
1231
+ "Since response_format='content_and_artifact' "
1232
+ "a two-tuple of the message content and raw tool output is "
1233
+ f"expected. Instead, generated response is of type: "
1234
+ f"{type(response)}."
1235
+ )
1236
+ if not isinstance(response, tuple):
1237
+ error_to_raise = ValueError(msg)
1238
+ else:
1239
+ try:
1240
+ content, artifact = response
1241
+ except ValueError:
1242
+ error_to_raise = ValueError(msg)
1243
+ else:
1244
+ content = response
1245
+ except (ValidationError, ValidationErrorV1) as e:
1246
+ if not self.handle_validation_error:
1247
+ error_to_raise = e
1248
+ else:
1249
+ content = _handle_validation_error(e, flag=self.handle_validation_error)
1250
+ status = "error"
1251
+ except ToolException as e:
1252
+ if not self.handle_tool_error:
1253
+ error_to_raise = e
1254
+ else:
1255
+ content = _handle_tool_error(e, flag=self.handle_tool_error)
1256
+ status = "error"
1257
+ except (Exception, KeyboardInterrupt) as e:
1258
+ error_to_raise = e
1259
+
1260
+ if error_to_raise:
1261
+ await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
1262
+ raise error_to_raise
1263
+
1264
+ output = _format_output(content, artifact, tool_call_id, self.name, status)
1265
+ await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
1266
+ return output
1267
+
1268
+
1269
+ def _is_tool_call(x: Any) -> bool:
1270
+ """Check if the input is a tool call dictionary.
1271
+
1272
+ Args:
1273
+ x: The input to check.
1274
+
1275
+ Returns:
1276
+ `True` if the input is a tool call, `False` otherwise.
1277
+ """
1278
+ return isinstance(x, dict) and x.get("type") == "tool_call"
1279
+
1280
+
1281
+ def _handle_validation_error(
1282
+ e: ValidationError | ValidationErrorV1,
1283
+ *,
1284
+ flag: Literal[True] | str | Callable[[ValidationError | ValidationErrorV1], str],
1285
+ ) -> str:
1286
+ """Handle validation errors based on the configured flag.
1287
+
1288
+ Args:
1289
+ e: The validation error that occurred.
1290
+ flag: How to handle the error (`bool`, `str`, or `Callable`).
1291
+
1292
+ Returns:
1293
+ The error message to return.
1294
+
1295
+ Raises:
1296
+ ValueError: If the flag type is unexpected.
1297
+ """
1298
+ if isinstance(flag, bool):
1299
+ content = "Tool input validation error"
1300
+ elif isinstance(flag, str):
1301
+ content = flag
1302
+ elif callable(flag):
1303
+ content = flag(e)
1304
+ else:
1305
+ msg = ( # type: ignore[unreachable]
1306
+ f"Got unexpected type of `handle_validation_error`. Expected bool, "
1307
+ f"str or callable. Received: {flag}"
1308
+ )
1309
+ raise ValueError(msg) # noqa: TRY004
1310
+ return content
1311
+
1312
+
1313
+ def _handle_tool_error(
1314
+ e: ToolException,
1315
+ *,
1316
+ flag: Literal[True]
1317
+ | str
1318
+ | Callable[[ToolException], ToolExceptionHandlerOutput]
1319
+ | None,
1320
+ ) -> ToolExceptionHandlerOutput:
1321
+ """Convert a `ToolException` into handled tool output content.
1322
+
1323
+ Args:
1324
+ e: The tool exception that occurred.
1325
+ flag: How to handle the error. `True` uses the exception message, a string
1326
+ replaces the message, and a callable computes replacement content from
1327
+ the exception.
1328
+
1329
+ Returns:
1330
+ The handled error content. This may be plain text or structured message
1331
+ content blocks; callers pass it through normal tool
1332
+ output formatting.
1333
+
1334
+ Raises:
1335
+ ValueError: If the flag type is unexpected.
1336
+ """
1337
+ if isinstance(flag, bool):
1338
+ content = e.args[0] if e.args else "Tool execution error"
1339
+ elif isinstance(flag, str):
1340
+ content = flag
1341
+ elif callable(flag):
1342
+ content = flag(e)
1343
+ else:
1344
+ msg = (
1345
+ f"Got unexpected type of `handle_tool_error`. Expected bool, str "
1346
+ f"or callable. Received: {flag}"
1347
+ )
1348
+ raise ValueError(msg) # noqa: TRY004
1349
+ return content
1350
+
1351
+
1352
+ def _prep_run_args(
1353
+ value: str | dict[str, Any] | ToolCall,
1354
+ config: RunnableConfig | None,
1355
+ **kwargs: Any,
1356
+ ) -> tuple[str | dict[str, Any], dict[str, Any]]:
1357
+ """Prepare arguments for tool execution.
1358
+
1359
+ Args:
1360
+ value: The input value (`str`, `dict`, or `ToolCall`).
1361
+ config: The runnable configuration.
1362
+ **kwargs: Additional keyword arguments.
1363
+
1364
+ Returns:
1365
+ A tuple of `(tool_input, run_kwargs)`.
1366
+ """
1367
+ config = ensure_config(config)
1368
+ tool_input: str | dict[str, Any]
1369
+ if _is_tool_call(value):
1370
+ tool_call_id: str | None = cast("ToolCall", value)["id"]
1371
+ tool_input = cast("ToolCall", value)["args"].copy()
1372
+ else:
1373
+ tool_call_id = None
1374
+ tool_input = cast("str | dict[str, Any]", value)
1375
+ return (
1376
+ tool_input,
1377
+ dict(
1378
+ callbacks=config.get("callbacks"),
1379
+ tags=config.get("tags"),
1380
+ metadata=config.get("metadata"),
1381
+ run_name=config.get("run_name"),
1382
+ run_id=config.pop("run_id", None),
1383
+ config=config,
1384
+ tool_call_id=tool_call_id,
1385
+ **kwargs,
1386
+ ),
1387
+ )
1388
+
1389
+
1390
+ def _format_output(
1391
+ content: Any,
1392
+ artifact: Any,
1393
+ tool_call_id: str | None,
1394
+ name: str,
1395
+ status: str,
1396
+ ) -> ToolOutputMixin | Any:
1397
+ """Format tool output as a `ToolMessage` if appropriate.
1398
+
1399
+ Args:
1400
+ content: The main content of the tool output.
1401
+ artifact: Any artifact data from the tool.
1402
+ tool_call_id: The ID of the tool call.
1403
+ name: The name of the tool.
1404
+ status: The execution status.
1405
+
1406
+ Returns:
1407
+ The formatted output, either as a `ToolMessage`, the original content,
1408
+ or an unchanged list of `ToolOutputMixin` instances.
1409
+ """
1410
+ if (
1411
+ isinstance(content, list)
1412
+ and content
1413
+ and all(isinstance(item, ToolOutputMixin) for item in content)
1414
+ ):
1415
+ return content
1416
+ if isinstance(content, ToolOutputMixin) or tool_call_id is None:
1417
+ return content
1418
+ normalized_content = _normalize_message_content(content)
1419
+ content = _stringify(content) if normalized_content is None else normalized_content
1420
+ return ToolMessage(
1421
+ content,
1422
+ artifact=artifact,
1423
+ tool_call_id=tool_call_id,
1424
+ name=name,
1425
+ status=status,
1426
+ )
1427
+
1428
+
1429
+ def _normalize_message_content(obj: Any) -> str | list[MessageContentBlock] | None:
1430
+ """Coerce valid message content to the shape expected by `ToolMessage`.
1431
+
1432
+ A string passes through unchanged; any `Sequence` of valid content blocks
1433
+ (e.g. a list or tuple) is materialized into a `list`. Returning `None`
1434
+ signals the caller (`_format_output`) that `obj` is not message content and
1435
+ should be stringified instead.
1436
+
1437
+ Args:
1438
+ obj: The object to normalize.
1439
+
1440
+ Returns:
1441
+ The normalized content, or `None` if `obj` is not valid message content.
1442
+ """
1443
+ if isinstance(obj, str):
1444
+ return obj
1445
+ # Validate lazily before materializing: `all` short-circuits on the first
1446
+ # invalid element, so a large non-content sequence (e.g. `range(10**12)`)
1447
+ # falls back to stringification without allocating it.
1448
+ if isinstance(obj, Sequence) and all(_is_message_content_block(e) for e in obj):
1449
+ return list(obj)
1450
+ return None
1451
+
1452
+
1453
+ def _is_message_content_block(obj: Any) -> bool:
1454
+ """Check if object is a valid message content block.
1455
+
1456
+ Validates content blocks for OpenAI or Anthropic format.
1457
+
1458
+ Args:
1459
+ obj: The object to check.
1460
+
1461
+ Returns:
1462
+ `True` if the object is a valid content block, `False` otherwise.
1463
+ """
1464
+ if isinstance(obj, str):
1465
+ return True
1466
+ if isinstance(obj, dict):
1467
+ return obj.get("type", None) in TOOL_MESSAGE_BLOCK_TYPES
1468
+ return False
1469
+
1470
+
1471
+ def _stringify(content: Any) -> str:
1472
+ """Convert content to string, preferring JSON format.
1473
+
1474
+ Args:
1475
+ content: The content to stringify.
1476
+
1477
+ Returns:
1478
+ String representation of the content.
1479
+ """
1480
+ try:
1481
+ return json.dumps(content, ensure_ascii=False)
1482
+ except Exception:
1483
+ return str(content)
1484
+
1485
+
1486
+ def _describe_callable(func: Any) -> str:
1487
+ """Return a readable identifier for a callable, for use in log messages."""
1488
+ return getattr(func, "__qualname__", None) or repr(func)
1489
+
1490
+
1491
+ def _get_type_hints(
1492
+ func: Callable[..., Any], *, include_extras: bool = False
1493
+ ) -> dict[str, type] | None:
1494
+ """Get type hints from a function, handling partial functions.
1495
+
1496
+ Args:
1497
+ func: The function to get type hints from.
1498
+ include_extras: Whether to preserve `Annotated` metadata in the resolved
1499
+ hints (e.g. to detect `Annotated[..., InjectedToolArg]`).
1500
+
1501
+ Returns:
1502
+ `dict` of type hints, or `None` if extraction fails.
1503
+ """
1504
+ if isinstance(func, functools.partial):
1505
+ func = func.func
1506
+ try:
1507
+ return get_type_hints(func, include_extras=include_extras)
1508
+ except Exception:
1509
+ _logger.debug(
1510
+ "Failed to resolve type hints for %s.",
1511
+ _describe_callable(func),
1512
+ exc_info=True,
1513
+ )
1514
+ return None
1515
+
1516
+
1517
+ def _get_class_signature_source(cls: type) -> Callable[..., Any] | None:
1518
+ """Return the callable whose parameters `signature(cls)` describes.
1519
+
1520
+ A class's own annotations describe its attributes, not its constructor
1521
+ parameters, so a class is never its own annotation owner. Constructor
1522
+ precedence mirrors `inspect.signature`: a metaclass `__call__` override,
1523
+ then a `__new__`/`__init__` the class defines itself, then an inherited one.
1524
+
1525
+ Args:
1526
+ cls: The class being inspected.
1527
+
1528
+ Returns:
1529
+ The constructor whose annotations describe `signature(cls)`, or `None`
1530
+ when only slot wrappers inherited from `object` are available.
1531
+ """
1532
+ metaclass = type(cls)
1533
+ if metaclass is not type:
1534
+ metaclass_call = inspect.getattr_static(metaclass, "__call__", None)
1535
+ if inspect.isfunction(metaclass_call):
1536
+ return metaclass_call
1537
+ for attr in ("__new__", "__init__"):
1538
+ if attr in cls.__dict__:
1539
+ return cast("Callable[..., Any]", getattr(cls, attr))
1540
+ for attr in ("__new__", "__init__"):
1541
+ # `object.__new__`/`object.__init__` are slot wrappers carrying no
1542
+ # useful annotations, so only inherited Python constructors qualify.
1543
+ inherited = getattr(cls, attr, None)
1544
+ if inspect.isfunction(inherited):
1545
+ return cast("Callable[..., Any]", inherited)
1546
+ return None
1547
+
1548
+
1549
+ def _get_type_hints_source(
1550
+ func: Callable[..., Any],
1551
+ ) -> Callable[..., Any] | None:
1552
+ """Return the callable that owns the annotations for an effective signature.
1553
+
1554
+ Unwrapping stops as soon as a callable declares its own `__signature__` (or
1555
+ is a bound method), because that declaration -- not the annotations of
1556
+ whatever it wraps -- defines the effective signature.
1557
+
1558
+ Args:
1559
+ func: The callable being inspected.
1560
+
1561
+ Returns:
1562
+ The function or method whose annotations describe `signature(func)`, or
1563
+ `None` when an explicit `__signature__` supplies the annotations or no
1564
+ annotation owner can be identified.
1565
+ """
1566
+ func = inspect.unwrap(
1567
+ func,
1568
+ stop=lambda wrapped: (
1569
+ hasattr(wrapped, "__signature__") or inspect.ismethod(wrapped)
1570
+ ),
1571
+ )
1572
+ if inspect.ismethod(func):
1573
+ return _get_type_hints_source(func.__func__)
1574
+ if getattr(func, "__signature__", None) is not None:
1575
+ return None
1576
+ if isinstance(func, functools.partial):
1577
+ return _get_type_hints_source(func.func)
1578
+ if inspect.isclass(func):
1579
+ constructor = _get_class_signature_source(func)
1580
+ if constructor is None:
1581
+ return None
1582
+ return _get_type_hints_source(constructor)
1583
+ if not inspect.isroutine(func):
1584
+ callable_obj = cast("Any", func)
1585
+ return _get_type_hints_source(cast("Callable[..., Any]", callable_obj.__call__))
1586
+ return func
1587
+
1588
+
1589
+ def _get_callable_globals(func: Callable[..., Any]) -> dict[str, Any]:
1590
+ """Return the globals namespace associated with a callable.
1591
+
1592
+ Explicit `__signature__` objects store annotations separately from a
1593
+ callable's `__annotations__`. String annotations on such signatures
1594
+ still need the callable's defining globals to be resolved.
1595
+
1596
+ Args:
1597
+ func: The callable whose defining globals to locate.
1598
+
1599
+ Returns:
1600
+ The callable's globals namespace, or an empty `dict` when none can be
1601
+ located (e.g. classes, builtins, and other C-implemented callables), in
1602
+ which case no string annotation will resolve.
1603
+ """
1604
+ if isinstance(func, functools.partial):
1605
+ return _get_callable_globals(func.func)
1606
+ if inspect.ismethod(func):
1607
+ return _get_callable_globals(func.__func__)
1608
+ globalns = getattr(func, "__globals__", None)
1609
+ if isinstance(globalns, dict):
1610
+ return globalns
1611
+ if not inspect.isroutine(func) and not inspect.isclass(func):
1612
+ callable_obj = cast("Any", func)
1613
+ return _get_callable_globals(cast("Callable[..., Any]", callable_obj.__call__))
1614
+ return {}
1615
+
1616
+
1617
+ def _get_injected_args_keys_from_signature(func: Callable[..., Any]) -> frozenset[str]:
1618
+ """Identify injected-argument parameters of a callable.
1619
+
1620
+ Resolve annotations with `get_type_hints` (rather than reading raw
1621
+ `signature` annotations) so postponed annotations -- e.g. from
1622
+ `from __future__ import annotations` or quoted forward references -- are
1623
+ recognized. `get_type_hints` resolves every annotation at once and raises
1624
+ on the first failure, so any parameter it leaves uncovered is retried on its
1625
+ own: an unrelated unresolvable forward reference must not disable injection
1626
+ for parameters whose annotations do resolve. Each distinct string annotation
1627
+ is resolved at most once per call, failures included.
1628
+
1629
+ A *string* annotation naming a type that exists only in the callable's
1630
+ defining local scope cannot be resolved here, since only globals are
1631
+ available. Such a parameter keeps its raw string annotation, which
1632
+ `_is_injected_arg_type` never classifies as injected, so it stays visible to
1633
+ the model rather than being injected at call time. Non-string annotations
1634
+ are unaffected, since `signature` yields the live object.
1635
+
1636
+ Args:
1637
+ func: The function (or bound method) whose signature to inspect.
1638
+
1639
+ Returns:
1640
+ `frozenset` of parameter names annotated as injected arguments.
1641
+ """
1642
+ params = signature(func).parameters
1643
+ hint_source = _get_type_hints_source(func)
1644
+ hints = (
1645
+ _get_type_hints(hint_source, include_extras=True)
1646
+ if hint_source is not None
1647
+ else None
1648
+ ) or {}
1649
+ globalns: dict[str, Any] | None = None
1650
+ resolved_annotations: dict[str, Any] = {}
1651
+ keys = set()
1652
+ for name, param in params.items():
1653
+ annotation = hints.get(name, param.annotation)
1654
+ if isinstance(annotation, str):
1655
+ if globalns is None:
1656
+ globalns = _get_callable_globals(
1657
+ func if hint_source is None else hint_source
1658
+ )
1659
+ if annotation not in resolved_annotations:
1660
+ resolved_annotations[annotation] = _resolve_forward_ref(
1661
+ annotation, globalns
1662
+ )
1663
+ resolved = resolved_annotations[annotation]
1664
+ if resolved is None:
1665
+ _logger.debug(
1666
+ "Could not resolve annotation %r for parameter %r of %s; it "
1667
+ "will not be treated as an injected argument.",
1668
+ annotation,
1669
+ name,
1670
+ _describe_callable(func),
1671
+ )
1672
+ else:
1673
+ annotation = resolved
1674
+ if _is_injected_arg_type(annotation):
1675
+ keys.add(name)
1676
+ return frozenset(keys)
1677
+
1678
+
1679
+ def _resolve_forward_ref(annotation: str, globalns: dict[str, Any]) -> Any:
1680
+ """Resolve a single string annotation, returning `None` on failure.
1681
+
1682
+ Uses a temporary annotated function so each annotation can be passed through
1683
+ the public `get_type_hints` API independently.
1684
+
1685
+ Args:
1686
+ annotation: The raw string annotation to resolve.
1687
+ globalns: The globals namespace to resolve names against.
1688
+
1689
+ Returns:
1690
+ The resolved type, or `None` if the annotation cannot be resolved.
1691
+ """
1692
+
1693
+ def _annotation_holder() -> None:
1694
+ pass
1695
+
1696
+ _annotation_holder.__annotations__ = {"value": annotation}
1697
+ try:
1698
+ return get_type_hints(
1699
+ _annotation_holder,
1700
+ globalns=globalns,
1701
+ include_extras=True,
1702
+ )["value"]
1703
+ except Exception:
1704
+ _logger.debug("Failed to resolve annotation %r.", annotation, exc_info=True)
1705
+ return None
1706
+
1707
+
1708
+ def _get_runnable_config_param(func: Callable[..., Any]) -> str | None:
1709
+ """Find the parameter name for `RunnableConfig` in a function.
1710
+
1711
+ Args:
1712
+ func: The function to check.
1713
+
1714
+ Returns:
1715
+ The parameter name for `RunnableConfig`, or `None` if not found.
1716
+ """
1717
+ type_hints = _get_type_hints(func)
1718
+ if not type_hints:
1719
+ return None
1720
+ for name, type_ in type_hints.items():
1721
+ if type_ is RunnableConfig:
1722
+ return name
1723
+ return None
1724
+
1725
+
1726
+ class InjectedToolArg:
1727
+ """Annotation for tool arguments that are injected at runtime.
1728
+
1729
+ Tool arguments annotated with this class are not included in the tool
1730
+ schema sent to language models and are instead injected during execution.
1731
+ """
1732
+
1733
+
1734
+ class _DirectlyInjectedToolArg:
1735
+ """Annotation for tool arguments that are injected at runtime.
1736
+
1737
+ Injected via direct type annotation, rather than annotated metadata.
1738
+
1739
+ For example, `ToolRuntime` is a directly injected argument.
1740
+
1741
+ Note the direct annotation rather than the verbose alternative:
1742
+ `Annotated[ToolRuntime, InjectedRuntime]`
1743
+
1744
+ ```python
1745
+ from coco_core.tools import tool, ToolRuntime
1746
+
1747
+
1748
+ @tool
1749
+ def foo(x: int, runtime: ToolRuntime) -> str:
1750
+ # use runtime.state, runtime.context, runtime.store, etc.
1751
+ ...
1752
+ ```
1753
+ """
1754
+
1755
+
1756
+ class InjectedToolCallId(InjectedToolArg):
1757
+ """Annotation for injecting the tool call ID.
1758
+
1759
+ This annotation is used to mark a tool parameter that should receive the tool call
1760
+ ID at runtime.
1761
+
1762
+ ```python
1763
+ from typing import Annotated
1764
+ from coco_core.messages import ToolMessage
1765
+ from coco_core.tools import tool, InjectedToolCallId
1766
+
1767
+ @tool
1768
+ def foo(
1769
+ x: int, tool_call_id: Annotated[str, InjectedToolCallId]
1770
+ ) -> ToolMessage:
1771
+ \"\"\"Return x.\"\"\"
1772
+ return ToolMessage(
1773
+ str(x),
1774
+ artifact=x,
1775
+ name="foo",
1776
+ tool_call_id=tool_call_id
1777
+ )
1778
+ ```
1779
+ """
1780
+
1781
+
1782
+ def _is_directly_injected_arg_type(type_: Any) -> bool:
1783
+ """Check if a type annotation indicates a directly injected argument.
1784
+
1785
+ This is currently only used for `ToolRuntime`.
1786
+
1787
+ Checks if either the annotation itself is a subclass of `_DirectlyInjectedToolArg`
1788
+ or the origin of the annotation is a subclass of `_DirectlyInjectedToolArg`.
1789
+
1790
+ For example, `ToolRuntime` or `ToolRuntime[ContextT, StateT]` would both return
1791
+ `True`.
1792
+ """
1793
+ return (
1794
+ isinstance(type_, type) and issubclass(type_, _DirectlyInjectedToolArg)
1795
+ ) or (
1796
+ (origin := get_origin(type_)) is not None
1797
+ and isinstance(origin, type)
1798
+ and issubclass(origin, _DirectlyInjectedToolArg)
1799
+ )
1800
+
1801
+
1802
+ def _is_injected_arg_type(
1803
+ type_: type | TypeVar, injected_type: type[InjectedToolArg] | None = None
1804
+ ) -> bool:
1805
+ """Check if a type annotation indicates an injected argument.
1806
+
1807
+ Args:
1808
+ type_: The type annotation to check.
1809
+ injected_type: The specific injected type to check for.
1810
+
1811
+ Returns:
1812
+ `True` if the type is an injected argument, `False` otherwise.
1813
+ """
1814
+ if injected_type is None:
1815
+ # if no injected type is specified,
1816
+ # check if the type is a directly injected argument
1817
+ if _is_directly_injected_arg_type(type_):
1818
+ return True
1819
+ injected_type = InjectedToolArg
1820
+
1821
+ # if the type is an Annotated type, check if annotated metadata
1822
+ # is an intance or subclass of the injected type
1823
+ return any(
1824
+ isinstance(arg, injected_type)
1825
+ or (isinstance(arg, type) and issubclass(arg, injected_type))
1826
+ for arg in get_args(type_)[1:]
1827
+ )
1828
+
1829
+
1830
+ def get_all_basemodel_annotations(
1831
+ cls: TypeBaseModel | Any, *, default_to_bound: bool = True
1832
+ ) -> dict[str, type | TypeVar]:
1833
+ """Get all annotations from a Pydantic `BaseModel` and its parents.
1834
+
1835
+ Args:
1836
+ cls: The Pydantic `BaseModel` class.
1837
+ default_to_bound: Whether to default to the bound of a `TypeVar` if it exists.
1838
+
1839
+ Returns:
1840
+ `dict` of field names to their type annotations.
1841
+ """
1842
+ orig_bases: tuple[type, ...]
1843
+ # cls has no subscript: cls = FooBar
1844
+ if isinstance(cls, type):
1845
+ fields = get_fields(cls)
1846
+ alias_map = {field.alias: name for name, field in fields.items() if field.alias}
1847
+
1848
+ annotations: dict[str, type | TypeVar] = {}
1849
+ for name, param in inspect.signature(cls).parameters.items():
1850
+ # Exclude hidden init args added by pydantic Config. For example if
1851
+ # BaseModel(extra="allow") then "extra_data" will part of init sig.
1852
+ if name not in fields and name not in alias_map:
1853
+ continue
1854
+ field_name = alias_map.get(name, name)
1855
+ annotations[field_name] = param.annotation
1856
+ orig_bases = getattr(cls, "__orig_bases__", ())
1857
+ # cls has subscript: cls = FooBar[int]
1858
+ else:
1859
+ annotations = get_all_basemodel_annotations(
1860
+ get_origin(cls), default_to_bound=False
1861
+ )
1862
+ orig_bases = (cls,)
1863
+
1864
+ # Pydantic v2 automatically resolves inherited generics, Pydantic v1 does not.
1865
+ if not (isinstance(cls, type) and is_pydantic_v2_subclass(cls)):
1866
+ # if cls = FooBar inherits from Baz[str], orig_bases will contain Baz[str]
1867
+ # if cls = FooBar inherits from Baz, orig_bases will contain Baz
1868
+ # if cls = FooBar[int], orig_bases will contain FooBar[int]
1869
+ for parent in orig_bases:
1870
+ # if class = FooBar inherits from Baz, parent = Baz
1871
+ if isinstance(parent, type) and is_pydantic_v1_subclass(parent):
1872
+ annotations.update(
1873
+ get_all_basemodel_annotations(parent, default_to_bound=False)
1874
+ )
1875
+ continue
1876
+
1877
+ parent_origin = get_origin(parent)
1878
+
1879
+ # if class = FooBar inherits from non-pydantic class
1880
+ if not parent_origin:
1881
+ continue
1882
+
1883
+ # if class = FooBar inherits from Baz[str]:
1884
+ # parent = class Baz[str],
1885
+ # parent_origin = class Baz,
1886
+ # generic_type_vars = (type vars in Baz)
1887
+ # generic_map = {type var in Baz: str}
1888
+ generic_type_vars: tuple[TypeVar, ...] = getattr(
1889
+ parent_origin, "__parameters__", ()
1890
+ )
1891
+ generic_map = dict(zip(generic_type_vars, get_args(parent), strict=False))
1892
+ for field in getattr(parent_origin, "__annotations__", {}):
1893
+ annotations[field] = _replace_type_vars(
1894
+ annotations[field], generic_map, default_to_bound=default_to_bound
1895
+ )
1896
+
1897
+ return {
1898
+ k: _replace_type_vars(v, default_to_bound=default_to_bound)
1899
+ for k, v in annotations.items()
1900
+ }
1901
+
1902
+
1903
+ def _replace_type_vars(
1904
+ type_: type | TypeVar,
1905
+ generic_map: dict[TypeVar, type] | None = None,
1906
+ *,
1907
+ default_to_bound: bool = True,
1908
+ ) -> type | TypeVar:
1909
+ """Replace `TypeVar`s in a type annotation with concrete types.
1910
+
1911
+ Args:
1912
+ type_: The type annotation to process.
1913
+ generic_map: Mapping of `TypeVar`s to concrete types.
1914
+ default_to_bound: Whether to use `TypeVar` bounds as defaults.
1915
+
1916
+ Returns:
1917
+ The type with `TypeVar`s replaced.
1918
+ """
1919
+ generic_map = generic_map or {}
1920
+ if isinstance(type_, TypeVar):
1921
+ if type_ in generic_map:
1922
+ return generic_map[type_]
1923
+ if default_to_bound:
1924
+ return type_.__bound__ if type_.__bound__ is not None else Any
1925
+ return type_
1926
+ if (origin := get_origin(type_)) and (args := get_args(type_)):
1927
+ new_args = tuple(
1928
+ _replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)
1929
+ for arg in args
1930
+ )
1931
+ return cast("type", _py_38_safe_origin(origin)[new_args]) # type: ignore[index]
1932
+ return type_
1933
+
1934
+
1935
+ class BaseToolkit(BaseModel, ABC):
1936
+ """Base class for toolkits containing related tools.
1937
+
1938
+ A toolkit is a collection of related tools that can be used together to accomplish a
1939
+ specific task or work with a particular system.
1940
+ """
1941
+
1942
+ @abstractmethod
1943
+ def get_tools(self) -> list[BaseTool]:
1944
+ """Get all tools in the toolkit.
1945
+
1946
+ Returns:
1947
+ List of tools contained in this toolkit.
1948
+ """