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,869 @@
1
+ """Methods for creating function specs in the style of OpenAI Functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import copy
7
+ import inspect
8
+ import logging
9
+ import types
10
+ import typing
11
+ import uuid
12
+ from typing import (
13
+ TYPE_CHECKING,
14
+ Annotated,
15
+ Any,
16
+ Literal,
17
+ Union,
18
+ cast,
19
+ get_args,
20
+ get_origin,
21
+ get_type_hints,
22
+ )
23
+
24
+ import typing_extensions
25
+ from pydantic import BaseModel
26
+ from pydantic.errors import PydanticInvalidForJsonSchema
27
+ from pydantic.v1 import BaseModel as BaseModelV1
28
+ from pydantic.v1 import Field as Field_v1
29
+ from pydantic.v1 import create_model as create_model_v1
30
+ from typing_extensions import TypedDict, is_typeddict
31
+
32
+ from coco_core._api import beta
33
+ from coco_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
34
+ from coco_core.utils.json_schema import dereference_refs
35
+ from coco_core.utils.pydantic import is_basemodel_subclass
36
+
37
+ if TYPE_CHECKING:
38
+ from collections.abc import Callable, Mapping
39
+
40
+ from coco_core.tools import BaseTool
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ PYTHON_TO_JSON_TYPES = {
45
+ "str": "string",
46
+ "int": "integer",
47
+ "float": "number",
48
+ "bool": "boolean",
49
+ }
50
+
51
+ _ORIGIN_MAP: dict[type, Any] = {
52
+ dict: dict,
53
+ list: list,
54
+ tuple: tuple,
55
+ set: set,
56
+ collections.abc.Iterable: typing.Iterable,
57
+ collections.abc.Mapping: typing.Mapping,
58
+ collections.abc.Sequence: typing.Sequence,
59
+ collections.abc.MutableMapping: typing.MutableMapping,
60
+ types.UnionType: Union,
61
+ }
62
+
63
+
64
+ class FunctionDescription(TypedDict):
65
+ """Representation of a callable function to send to an LLM."""
66
+
67
+ name: str
68
+ """The name of the function."""
69
+
70
+ description: str
71
+ """A description of the function."""
72
+
73
+ parameters: dict[str, Any]
74
+ """The parameters of the function."""
75
+
76
+
77
+ class ToolDescription(TypedDict):
78
+ """Representation of a callable function to the OpenAI API."""
79
+
80
+ type: Literal["function"]
81
+ """The type of the tool."""
82
+
83
+ function: FunctionDescription
84
+ """The function description."""
85
+
86
+
87
+ def _rm_titles(kv: dict[str, Any], prev_key: str = "") -> dict[str, Any]:
88
+ """Recursively removes `'title'` fields from a JSON schema dictionary.
89
+
90
+ Remove `'title'` fields from the input JSON schema dictionary,
91
+ except when a `'title'` appears within a property definition under `'properties'`.
92
+
93
+ Args:
94
+ kv: The input JSON schema as a dictionary.
95
+ prev_key: The key from the parent dictionary, used to identify context.
96
+
97
+ Returns:
98
+ A new dictionary with appropriate `'title'` fields removed.
99
+ """
100
+ new_kv = {}
101
+
102
+ for k, v in kv.items():
103
+ if k == "title":
104
+ # If the value is a nested dict and part of a property under "properties",
105
+ # preserve the title but continue recursion
106
+ if isinstance(v, dict) and prev_key == "properties":
107
+ new_kv[k] = _rm_titles(v, k)
108
+ else:
109
+ # Otherwise, remove this "title" key
110
+ continue
111
+ elif isinstance(v, dict):
112
+ # Recurse into nested dictionaries
113
+ new_kv[k] = _rm_titles(v, k)
114
+ else:
115
+ # Leave non-dict values untouched
116
+ new_kv[k] = v
117
+
118
+ return new_kv
119
+
120
+
121
+ def _convert_json_schema_to_openai_function(
122
+ schema: dict[str, Any],
123
+ *,
124
+ name: str | None = None,
125
+ description: str | None = None,
126
+ rm_titles: bool = True,
127
+ ) -> FunctionDescription:
128
+ """Converts a Pydantic model to a function description for the OpenAI API.
129
+
130
+ Args:
131
+ schema: The JSON schema to convert.
132
+ name: The name of the function.
133
+
134
+ If not provided, the title of the schema will be used.
135
+ description: The description of the function.
136
+
137
+ If not provided, the description of the schema will be used.
138
+ rm_titles: Whether to remove titles from the schema.
139
+
140
+ Returns:
141
+ The function description.
142
+ """
143
+ schema = dereference_refs(schema)
144
+ if "definitions" in schema: # pydantic 1
145
+ schema.pop("definitions", None)
146
+ if "$defs" in schema: # pydantic 2
147
+ schema.pop("$defs", None)
148
+ title = schema.pop("title", "")
149
+ default_description = schema.pop("description", "")
150
+ return {
151
+ "name": name or title,
152
+ "description": description or default_description,
153
+ "parameters": _rm_titles(schema) if rm_titles else schema,
154
+ }
155
+
156
+
157
+ def _convert_pydantic_to_openai_function(
158
+ model: type,
159
+ *,
160
+ name: str | None = None,
161
+ description: str | None = None,
162
+ rm_titles: bool = True,
163
+ ) -> FunctionDescription:
164
+ """Converts a Pydantic model to a function description for the OpenAI API.
165
+
166
+ Args:
167
+ model: The Pydantic model to convert.
168
+ name: The name of the function.
169
+
170
+ If not provided, the title of the schema will be used.
171
+ description: The description of the function.
172
+
173
+ If not provided, the description of the schema will be used.
174
+ rm_titles: Whether to remove titles from the schema.
175
+
176
+ Raises:
177
+ TypeError: If the model is not a Pydantic model.
178
+ TypeError: If the model contains types that cannot be converted to JSON schema.
179
+
180
+ Returns:
181
+ The function description.
182
+ """
183
+ try:
184
+ if hasattr(model, "model_json_schema"):
185
+ schema = model.model_json_schema() # Pydantic 2
186
+ elif hasattr(model, "schema"):
187
+ schema = model.schema() # Pydantic 1
188
+ else:
189
+ msg = "Model must be a Pydantic model."
190
+ raise TypeError(msg)
191
+ except PydanticInvalidForJsonSchema as e:
192
+ model_name = getattr(model, "__name__", str(model))
193
+ msg = (
194
+ f"Failed to generate JSON schema for '{model_name}': {e}\n\n"
195
+ "Tool argument schemas must be JSON-serializable. If your schema includes "
196
+ "custom Python classes, consider:\n"
197
+ " 1. Converting them to Pydantic models with JSON-compatible fields\n"
198
+ " 2. Using primitive types (str, int, float, bool, list, dict) instead\n"
199
+ " 3. Passing the data as serialized JSON strings\n\n"
200
+ )
201
+ raise PydanticInvalidForJsonSchema(msg) from e
202
+ return _convert_json_schema_to_openai_function(
203
+ schema, name=name, description=description, rm_titles=rm_titles
204
+ )
205
+
206
+
207
+ def _get_python_function_name(function: Callable[..., Any]) -> str:
208
+ """Get the name of a Python function."""
209
+ return function.__name__
210
+
211
+
212
+ def _convert_python_function_to_openai_function(
213
+ function: Callable[..., Any],
214
+ ) -> FunctionDescription:
215
+ """Convert a Python function to an OpenAI function-calling API compatible dict.
216
+
217
+ Assumes the Python function has type hints and a docstring with a description. If
218
+ the docstring has Google Python style argument descriptions, these will be included
219
+ as well.
220
+
221
+ Args:
222
+ function: The Python function to convert.
223
+
224
+ Returns:
225
+ The OpenAI function description.
226
+ """
227
+ # Import locally to prevent circular import
228
+ from coco_core.tools.base import create_schema_from_function # noqa: PLC0415
229
+
230
+ func_name = _get_python_function_name(function)
231
+ model = create_schema_from_function(
232
+ func_name,
233
+ function,
234
+ filter_args=(),
235
+ parse_docstring=True,
236
+ error_on_invalid_docstring=False,
237
+ include_injected=False,
238
+ )
239
+ return _convert_pydantic_to_openai_function(
240
+ model,
241
+ name=func_name,
242
+ description=model.__doc__,
243
+ )
244
+
245
+
246
+ def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescription:
247
+ visited: dict[type, type] = {}
248
+
249
+ model = cast(
250
+ "type[BaseModel]",
251
+ _convert_any_typed_dicts_to_pydantic(typed_dict, visited=visited),
252
+ )
253
+ return _convert_pydantic_to_openai_function(model)
254
+
255
+
256
+ _MAX_TYPED_DICT_RECURSION = 25
257
+
258
+
259
+ def _convert_any_typed_dicts_to_pydantic(
260
+ type_: type,
261
+ *,
262
+ visited: dict[type, type],
263
+ depth: int = 0,
264
+ ) -> type:
265
+ if type_ in visited:
266
+ return visited[type_]
267
+ if depth >= _MAX_TYPED_DICT_RECURSION:
268
+ return type_
269
+ if is_typeddict(type_):
270
+ typed_dict = type_
271
+ docstring = inspect.getdoc(typed_dict)
272
+ # Use get_type_hints to properly resolve forward references and
273
+ # string annotations in Python 3.14+ (PEP 649 deferred annotations).
274
+ # include_extras=True preserves Annotated metadata.
275
+ try:
276
+ annotations_ = get_type_hints(typed_dict, include_extras=True)
277
+ except Exception:
278
+ # Fallback for edge cases where get_type_hints might fail
279
+ annotations_ = typed_dict.__annotations__
280
+ description, arg_descriptions = _parse_google_docstring(
281
+ docstring, list(annotations_)
282
+ )
283
+ fields: dict[str, Any] = {}
284
+ for arg, arg_type in annotations_.items():
285
+ if get_origin(arg_type) in {Annotated, typing_extensions.Annotated}:
286
+ annotated_args = get_args(arg_type)
287
+ new_arg_type = _convert_any_typed_dicts_to_pydantic(
288
+ annotated_args[0], depth=depth + 1, visited=visited
289
+ )
290
+ field_kwargs = dict(
291
+ zip(("default", "description"), annotated_args[1:], strict=False)
292
+ )
293
+ if (field_desc := field_kwargs.get("description")) and not isinstance(
294
+ field_desc, str
295
+ ):
296
+ msg = (
297
+ f"Invalid annotation for field {arg}. Third argument to "
298
+ f"Annotated must be a string description, received value of "
299
+ f"type {type(field_desc)}."
300
+ )
301
+ raise ValueError(msg)
302
+ if arg_desc := arg_descriptions.get(arg):
303
+ field_kwargs["description"] = arg_desc
304
+ fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
305
+ else:
306
+ new_arg_type = _convert_any_typed_dicts_to_pydantic(
307
+ arg_type, depth=depth + 1, visited=visited
308
+ )
309
+ field_kwargs = {"default": ...}
310
+ if arg_desc := arg_descriptions.get(arg):
311
+ field_kwargs["description"] = arg_desc
312
+ fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
313
+ model = cast(
314
+ "type[BaseModelV1]", create_model_v1(typed_dict.__name__, **fields)
315
+ )
316
+ model.__doc__ = description
317
+ visited[typed_dict] = model
318
+ return model
319
+ if (origin := get_origin(type_)) and (type_args := get_args(type_)):
320
+ subscriptable_origin = _py_38_safe_origin(origin)
321
+ type_args = tuple(
322
+ _convert_any_typed_dicts_to_pydantic(arg, depth=depth + 1, visited=visited)
323
+ for arg in type_args
324
+ )
325
+ return cast("type", subscriptable_origin[type_args]) # type: ignore[index]
326
+ return type_
327
+
328
+
329
+ def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
330
+ """Format tool into the OpenAI function API.
331
+
332
+ Args:
333
+ tool: The tool to format.
334
+
335
+ Raises:
336
+ ValueError: If the tool call schema is not supported.
337
+
338
+ Returns:
339
+ The function description.
340
+ """
341
+ # Import locally to prevent circular import
342
+ from coco_core.tools import Tool # noqa: PLC0415
343
+
344
+ is_simple_oai_tool = isinstance(tool, Tool) and not tool.args_schema
345
+ if tool.tool_call_schema and not is_simple_oai_tool:
346
+ if isinstance(tool.tool_call_schema, dict):
347
+ return _convert_json_schema_to_openai_function(
348
+ tool.tool_call_schema, name=tool.name, description=tool.description
349
+ )
350
+ if issubclass(tool.tool_call_schema, (BaseModel, BaseModelV1)):
351
+ return _convert_pydantic_to_openai_function(
352
+ tool.tool_call_schema, name=tool.name, description=tool.description
353
+ )
354
+ error_msg = ( # type: ignore[unreachable]
355
+ f"Unsupported tool call schema: {tool.tool_call_schema}. "
356
+ "Tool call schema must be a JSON schema dict or a Pydantic model."
357
+ )
358
+ raise ValueError(error_msg)
359
+ return {
360
+ "name": tool.name,
361
+ "description": tool.description,
362
+ "parameters": {
363
+ # This is a hack to get around the fact that some tools
364
+ # do not expose an args_schema, and expect an argument
365
+ # which is a string.
366
+ # And Open AI does not support an array type for the
367
+ # parameters.
368
+ "properties": {
369
+ "__arg1": {"title": "__arg1", "type": "string"},
370
+ },
371
+ "required": ["__arg1"],
372
+ "type": "object",
373
+ },
374
+ }
375
+
376
+
377
+ def convert_to_openai_function(
378
+ function: Mapping[str, Any] | type | Callable[..., Any] | BaseTool,
379
+ *,
380
+ strict: bool | None = None,
381
+ ) -> dict[str, Any]:
382
+ """Convert a raw function/class to an OpenAI function.
383
+
384
+ Args:
385
+ function: A dictionary, Pydantic `BaseModel` class, `TypedDict` class, a
386
+ Coco `Tool` object, or a Python function.
387
+
388
+ If a dictionary is passed in, it is assumed to already be a valid OpenAI
389
+ function, a JSON schema with top-level `title` key specified, an Anthropic
390
+ format tool, or an Amazon Bedrock Converse format tool.
391
+ strict: If `True`, model output is guaranteed to exactly match the JSON Schema
392
+ provided in the function definition.
393
+
394
+ If `None`, `strict` argument will not be included in function definition.
395
+
396
+ Returns:
397
+ A dict version of the passed in function which is compatible with the OpenAI
398
+ function-calling API.
399
+
400
+ Raises:
401
+ ValueError: If function is not in a supported format.
402
+
403
+ !!! warning "Behavior changed in `trustcoco-core` 0.3.16"
404
+
405
+ `description` and `parameters` keys are now optional. Only `name` is
406
+ required and guaranteed to be part of the output.
407
+ """
408
+ # Import locally to prevent circular import
409
+ from coco_core.tools import BaseTool # noqa: PLC0415
410
+
411
+ if strict and isinstance(function, dict):
412
+ function = copy.deepcopy(function)
413
+
414
+ # an Anthropic format tool
415
+ if isinstance(function, dict) and all(
416
+ k in function for k in ("name", "input_schema")
417
+ ):
418
+ oai_function = {
419
+ "name": function["name"],
420
+ "parameters": function["input_schema"],
421
+ }
422
+ if "description" in function:
423
+ oai_function["description"] = function["description"]
424
+ # an Amazon Bedrock Converse format tool
425
+ elif isinstance(function, dict) and "toolSpec" in function:
426
+ oai_function = {
427
+ "name": function["toolSpec"]["name"],
428
+ "parameters": function["toolSpec"]["inputSchema"]["json"],
429
+ }
430
+ if "description" in function["toolSpec"]:
431
+ oai_function["description"] = function["toolSpec"]["description"]
432
+ # already in OpenAI function format
433
+ elif isinstance(function, dict) and "name" in function:
434
+ oai_function = {
435
+ k: v
436
+ for k, v in function.items()
437
+ if k in {"name", "description", "parameters", "strict"}
438
+ }
439
+ # a JSON schema with title and description
440
+ elif isinstance(function, dict) and "title" in function:
441
+ function_copy = function.copy()
442
+ oai_function = {"name": function_copy.pop("title")}
443
+ if "description" in function_copy:
444
+ oai_function["description"] = function_copy.pop("description")
445
+ if function_copy and "properties" in function_copy:
446
+ oai_function["parameters"] = function_copy
447
+ elif isinstance(function, type) and is_basemodel_subclass(function):
448
+ oai_function = cast(
449
+ "dict[str, Any]", _convert_pydantic_to_openai_function(function)
450
+ )
451
+ elif is_typeddict(function):
452
+ oai_function = cast(
453
+ "dict[str, Any]",
454
+ _convert_typed_dict_to_openai_function(cast("type", function)),
455
+ )
456
+ elif isinstance(function, BaseTool):
457
+ oai_function = cast("dict[str, Any]", _format_tool_to_openai_function(function))
458
+ elif callable(function):
459
+ oai_function = cast(
460
+ "dict[str, Any]", _convert_python_function_to_openai_function(function)
461
+ )
462
+ else:
463
+ if isinstance(function, dict) and (
464
+ "type" in function or "properties" in function
465
+ ):
466
+ msg = (
467
+ f"Unsupported function\n\n{function}\n\nTo use a JSON schema as a "
468
+ "function, it must have a top-level 'title' key to be used as the "
469
+ "function name."
470
+ )
471
+ raise ValueError(msg)
472
+ msg = (
473
+ f"Unsupported function\n\n{function}\n\nFunctions must be passed in"
474
+ " as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"
475
+ " either be in OpenAI function format or valid JSON schema with top-level"
476
+ " 'title' key."
477
+ )
478
+ raise ValueError(msg)
479
+
480
+ if strict is not None:
481
+ if "strict" in oai_function and oai_function["strict"] != strict:
482
+ msg = (
483
+ f"Tool/function already has a 'strict' key with value "
484
+ f"{oai_function['strict']} which is different from the explicit "
485
+ f"`strict` arg received {strict=}."
486
+ )
487
+ raise ValueError(msg)
488
+ oai_function["strict"] = strict
489
+ if strict:
490
+ oai_function["parameters"] = _recursive_set_additional_properties_false(
491
+ oai_function["parameters"]
492
+ )
493
+ return oai_function
494
+
495
+
496
+ # List of well known tools supported by OpenAI's chat models or responses API.
497
+ # These tools are not expected to be supported by other chat model providers
498
+ # that conform to the OpenAI function-calling API.
499
+ _WellKnownOpenAITools = (
500
+ "function",
501
+ "file_search",
502
+ "computer",
503
+ "computer_use_preview",
504
+ "code_interpreter",
505
+ "mcp",
506
+ "image_generation",
507
+ "web_search_preview",
508
+ "web_search",
509
+ "tool_search",
510
+ "apply_patch",
511
+ "namespace",
512
+ )
513
+
514
+
515
+ def convert_to_openai_tool(
516
+ tool: Mapping[str, Any] | type[BaseModel] | Callable[..., Any] | BaseTool,
517
+ *,
518
+ strict: bool | None = None,
519
+ ) -> dict[str, Any]:
520
+ """Convert a tool-like object to an OpenAI tool schema.
521
+
522
+ [OpenAI tool schema reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools)
523
+
524
+ Args:
525
+ tool: Either a dictionary, a `pydantic.BaseModel` class, Python function, or
526
+ `BaseTool`.
527
+
528
+ If a dictionary is passed in, it is assumed to already be a valid OpenAI
529
+ function, a JSON schema with top-level `title` key specified, an Anthropic
530
+ format tool, or an Amazon Bedrock Converse format tool.
531
+ strict: If `True`, model output is guaranteed to exactly match the JSON Schema
532
+ provided in the function definition.
533
+
534
+ If `None`, `strict` argument will not be included in tool definition.
535
+
536
+ Returns:
537
+ A dict version of the passed in tool which is compatible with the OpenAI
538
+ tool-calling API.
539
+
540
+ !!! warning "Behavior changed in `trustcoco-core` 0.3.16"
541
+
542
+ `description` and `parameters` keys are now optional. Only `name` is
543
+ required and guaranteed to be part of the output.
544
+
545
+ !!! warning "Behavior changed in `trustcoco-core` 0.3.44"
546
+
547
+ Return OpenAI Responses API-style tools unchanged. This includes
548
+ any dict with `"type"` in `"file_search"`, `"function"`,
549
+ `"computer_use_preview"`, `"web_search_preview"`, `"apply_patch"`.
550
+
551
+ !!! warning "Behavior changed in `trustcoco-core` 0.3.63"
552
+
553
+ Added support for OpenAI's image generation built-in tool.
554
+ """
555
+ # Import locally to prevent circular import
556
+ from coco_core.tools import Tool # noqa: PLC0415
557
+
558
+ if isinstance(tool, dict):
559
+ if tool.get("type") in _WellKnownOpenAITools:
560
+ return tool
561
+ # As of 03.12.25 can be "web_search_preview" or "web_search_preview_2025_03_11"
562
+ if (tool.get("type") or "").startswith("web_search_preview"):
563
+ return tool
564
+ if isinstance(tool, Tool) and (tool.metadata or {}).get("type") == "custom_tool":
565
+ oai_tool = {
566
+ "type": "custom",
567
+ "name": tool.name,
568
+ "description": tool.description,
569
+ }
570
+ if tool.metadata is not None and "format" in tool.metadata:
571
+ oai_tool["format"] = tool.metadata["format"]
572
+ return oai_tool
573
+ oai_function = convert_to_openai_function(tool, strict=strict)
574
+ return {"type": "function", "function": oai_function}
575
+
576
+
577
+ def convert_to_json_schema(
578
+ schema: dict[str, Any] | type[BaseModel] | Callable[..., Any] | BaseTool,
579
+ *,
580
+ strict: bool | None = None,
581
+ ) -> dict[str, Any]:
582
+ """Convert a schema representation to a JSON schema.
583
+
584
+ Args:
585
+ schema: The schema to convert.
586
+ strict: If `True`, model output is guaranteed to exactly match the JSON Schema
587
+ provided in the function definition.
588
+
589
+ If `None`, `strict` argument will not be included in function definition.
590
+
591
+ Raises:
592
+ ValueError: If the input is not a valid OpenAI-format tool.
593
+
594
+ Returns:
595
+ A JSON schema representation of the input schema.
596
+ """
597
+ openai_tool = convert_to_openai_tool(schema, strict=strict)
598
+ if (
599
+ not isinstance(openai_tool, dict)
600
+ or "function" not in openai_tool
601
+ or "name" not in openai_tool["function"]
602
+ ):
603
+ error_message = "Input must be a valid OpenAI-format tool."
604
+ raise ValueError(error_message)
605
+
606
+ openai_function = openai_tool["function"]
607
+ json_schema = {}
608
+ json_schema["title"] = openai_function["name"]
609
+
610
+ if "description" in openai_function:
611
+ json_schema["description"] = openai_function["description"]
612
+
613
+ if "parameters" in openai_function:
614
+ parameters = openai_function["parameters"].copy()
615
+ json_schema.update(parameters)
616
+
617
+ return json_schema
618
+
619
+
620
+ @beta()
621
+ def tool_example_to_messages(
622
+ input: str,
623
+ tool_calls: list[BaseModel],
624
+ tool_outputs: list[str] | None = None,
625
+ *,
626
+ ai_response: str | None = None,
627
+ ) -> list[BaseMessage]:
628
+ """Convert an example into a list of messages that can be fed into an LLM.
629
+
630
+ This code is an adapter that converts a single example to a list of messages
631
+ that can be fed into a chat model.
632
+
633
+ The list of messages per example by default corresponds to:
634
+
635
+ 1. `HumanMessage`: contains the content from which content should be extracted.
636
+ 2. `AIMessage`: contains the extracted information from the model
637
+ 3. `ToolMessage`: contains confirmation to the model that the model requested a
638
+ tool correctly.
639
+
640
+ If `ai_response` is specified, there will be a final `AIMessage` with that
641
+ response.
642
+
643
+ The `ToolMessage` is required because some chat models are hyper-optimized for
644
+ agents rather than for an extraction use case.
645
+
646
+ Args:
647
+ input: The user input
648
+ tool_calls: Tool calls represented as Pydantic BaseModels
649
+ tool_outputs: Tool call outputs.
650
+
651
+ Does not need to be provided.
652
+
653
+ If not provided, a placeholder value will be inserted.
654
+ ai_response: If provided, content for a final `AIMessage`.
655
+
656
+ Returns:
657
+ A list of messages
658
+
659
+ Examples:
660
+ ```python
661
+ from typing import Optional
662
+ from pydantic import BaseModel, Field
663
+ from coco_openai import ChatOpenAI
664
+
665
+
666
+ class Person(BaseModel):
667
+ '''Information about a person.'''
668
+
669
+ name: str | None = Field(..., description="The name of the person")
670
+ hair_color: str | None = Field(
671
+ ..., description="The color of the person's hair if known"
672
+ )
673
+ height_in_meters: str | None = Field(..., description="Height in METERS")
674
+
675
+
676
+ examples = [
677
+ (
678
+ "The ocean is vast and blue. It's more than 20,000 feet deep.",
679
+ Person(name=None, height_in_meters=None, hair_color=None),
680
+ ),
681
+ (
682
+ "Fiona traveled far from France to Spain.",
683
+ Person(name="Fiona", height_in_meters=None, hair_color=None),
684
+ ),
685
+ ]
686
+
687
+
688
+ messages = []
689
+
690
+ for txt, tool_call in examples:
691
+ messages.extend(tool_example_to_messages(txt, [tool_call]))
692
+ ```
693
+ """
694
+ messages: list[BaseMessage] = [HumanMessage(content=input)]
695
+
696
+ openai_tool_calls = [
697
+ {
698
+ "id": str(uuid.uuid4()),
699
+ "type": "function",
700
+ "function": {
701
+ # The name of the function right now corresponds to the name
702
+ # of the Pydantic model. This is implicit in the API right now,
703
+ # and will be improved over time.
704
+ "name": tool_call.__class__.__name__,
705
+ "arguments": tool_call.model_dump_json(),
706
+ },
707
+ }
708
+ for tool_call in tool_calls
709
+ ]
710
+
711
+ messages.append(
712
+ AIMessage(content="", additional_kwargs={"tool_calls": openai_tool_calls})
713
+ )
714
+ if tool_outputs is not None and len(tool_outputs) != len(openai_tool_calls):
715
+ msg = (
716
+ f"The number of tool_outputs ({len(tool_outputs)}) must match the number "
717
+ f"of tool_calls ({len(openai_tool_calls)}). Got {len(tool_outputs)} "
718
+ f"output(s) for {len(openai_tool_calls)} tool call(s)."
719
+ )
720
+ raise ValueError(msg)
721
+ tool_outputs = tool_outputs or ["You have correctly called this tool."] * len(
722
+ openai_tool_calls
723
+ )
724
+ for output, tool_call_dict in zip(tool_outputs, openai_tool_calls, strict=False):
725
+ messages.append(ToolMessage(content=output, tool_call_id=tool_call_dict["id"]))
726
+
727
+ if ai_response:
728
+ messages.append(AIMessage(content=ai_response))
729
+ return messages
730
+
731
+
732
+ _MIN_DOCSTRING_BLOCKS = 2
733
+
734
+
735
+ def _parse_google_docstring(
736
+ docstring: str | None,
737
+ args: list[str],
738
+ *,
739
+ error_on_invalid_docstring: bool = False,
740
+ ) -> tuple[str, dict[str, str]]:
741
+ """Parse the function and argument descriptions from the docstring of a function.
742
+
743
+ Assumes the function docstring follows Google Python style guide.
744
+
745
+ Args:
746
+ docstring: The docstring to parse.
747
+ args: The list of argument names to extract descriptions for.
748
+ error_on_invalid_docstring: Whether to raise an error if the docstring is
749
+ invalid.
750
+
751
+ Returns:
752
+ A tuple of the function description and a dictionary of argument descriptions.
753
+ """
754
+ if docstring:
755
+ docstring_blocks = docstring.split("\n\n")
756
+ if error_on_invalid_docstring:
757
+ filtered_annotations = {
758
+ arg
759
+ for arg in args
760
+ if arg not in {"run_manager", "callbacks", "runtime", "return"}
761
+ }
762
+ if filtered_annotations and (
763
+ len(docstring_blocks) < _MIN_DOCSTRING_BLOCKS
764
+ or not any(block.startswith("Args:") for block in docstring_blocks[1:])
765
+ ):
766
+ msg = "Found invalid Google-Style docstring."
767
+ raise ValueError(msg)
768
+ descriptors = []
769
+ args_block = None
770
+ past_descriptors = False
771
+ for block in docstring_blocks:
772
+ if block.startswith("Args:"):
773
+ args_block = block
774
+ break
775
+ if block.startswith(("Returns:", "Example:")):
776
+ # Don't break in case Args come after
777
+ past_descriptors = True
778
+ elif not past_descriptors:
779
+ descriptors.append(block)
780
+ else:
781
+ continue
782
+ description = " ".join(descriptors).strip()
783
+ else:
784
+ if error_on_invalid_docstring:
785
+ msg = "Found invalid Google-Style docstring."
786
+ raise ValueError(msg)
787
+ description = ""
788
+ args_block = None
789
+ arg_descriptions: dict[str, str] = {}
790
+ if args_block:
791
+ arg: str | None = None
792
+ # Base indentation, latched once from the first argument line, lets us
793
+ # distinguish new argument definitions from continuation lines. This
794
+ # assumes Google-style uniform indentation of argument names: a line
795
+ # indented deeper than the first argument is treated as a continuation
796
+ # (even if it contains a colon), so a more-indented later `name:` line
797
+ # in a malformed, non-uniformly-indented block folds into the previous
798
+ # argument rather than starting a new one.
799
+ arg_indent: int | None = None
800
+ for line in args_block.split("\n")[1:]:
801
+ if not line.strip():
802
+ continue
803
+ current_indent = len(line) - len(line.lstrip())
804
+ if arg_indent is None and ":" in line:
805
+ arg_indent = current_indent
806
+ is_continuation = arg_indent is not None and current_indent > arg_indent
807
+ if arg is not None and is_continuation:
808
+ arg_descriptions[arg] += " " + line.strip()
809
+ elif ":" in line:
810
+ arg, desc = line.split(":", maxsplit=1)
811
+ arg = arg.strip()
812
+ arg_name, _, annotations_ = arg.partition(" ")
813
+ if annotations_.startswith("(") and annotations_.endswith(")"):
814
+ arg = arg_name
815
+ arg_descriptions[arg] = desc.strip()
816
+ elif arg:
817
+ arg_descriptions[arg] += " " + line.strip()
818
+ return description, arg_descriptions
819
+
820
+
821
+ def _py_38_safe_origin(origin: type) -> type:
822
+ return cast("type", _ORIGIN_MAP.get(origin, origin))
823
+
824
+
825
+ def _recursive_set_additional_properties_false(
826
+ schema: dict[str, Any],
827
+ ) -> dict[str, Any]:
828
+ if isinstance(schema, dict):
829
+ # OpenAI strict mode requires every property to appear in `required` at every
830
+ # level of nesting. Without this, nested object schemas are rejected.
831
+ properties = schema.get("properties")
832
+ if isinstance(properties, dict) and properties:
833
+ schema["required"] = list(properties.keys())
834
+
835
+ # Check if 'required' is a key at the current level or if the schema is empty,
836
+ # in which case additionalProperties still needs to be specified.
837
+ if (
838
+ "required" in schema
839
+ or ("properties" in schema and not schema["properties"])
840
+ # Since Pydantic 2.11, it will always add `additionalProperties: True`
841
+ # for arbitrary dictionary schemas
842
+ # See: https://pydantic.dev/articles/pydantic-v2-11-release#changes
843
+ # If it is already set to True, we need override it to False
844
+ or "additionalProperties" in schema
845
+ ):
846
+ schema["additionalProperties"] = False
847
+
848
+ # Recursively check 'properties' and 'items' if they exist
849
+ if "anyOf" in schema:
850
+ for sub_schema in schema["anyOf"]:
851
+ _recursive_set_additional_properties_false(sub_schema)
852
+ # Pydantic <2.9 wraps a referenced model field in 'allOf' when it has
853
+ # sibling keys (e.g. 'description'), instead of merging them directly.
854
+ if "allOf" in schema:
855
+ for sub_schema in schema["allOf"]:
856
+ _recursive_set_additional_properties_false(sub_schema)
857
+ if "properties" in schema:
858
+ for sub_schema in schema["properties"].values():
859
+ _recursive_set_additional_properties_false(sub_schema)
860
+ if "items" in schema:
861
+ _recursive_set_additional_properties_false(schema["items"])
862
+ # Raw JSON schemas may keep nested objects in `$defs` and reference them via
863
+ # `$ref`; walk those definitions too so they're made strict.
864
+ for defs_key in ("$defs", "definitions"):
865
+ if isinstance(schema.get(defs_key), dict):
866
+ for sub_schema in schema[defs_key].values():
867
+ _recursive_set_additional_properties_false(sub_schema)
868
+
869
+ return schema