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,329 @@
1
+ """Derivations of standard content blocks from Amazon (Bedrock Converse) content."""
2
+
3
+ import base64
4
+ from collections.abc import Iterator
5
+ from typing import Any, cast
6
+
7
+ from coco_core.messages import AIMessage, AIMessageChunk
8
+ from coco_core.messages import content as types
9
+
10
+
11
+ def _bytes_to_b64_str(bytes_: bytes) -> str:
12
+ return base64.b64encode(bytes_).decode("utf-8")
13
+
14
+
15
+ def _populate_extras(
16
+ standard_block: types.ContentBlock, block: dict[str, Any], known_fields: set[str]
17
+ ) -> types.ContentBlock:
18
+ """Mutate a block, populating extras."""
19
+ if standard_block.get("type") == "non_standard":
20
+ return standard_block
21
+
22
+ for key, value in block.items():
23
+ if key not in known_fields:
24
+ if "extras" not in standard_block:
25
+ # Below type-ignores are because mypy thinks a non-standard block can
26
+ # get here, although we exclude them above.
27
+ standard_block["extras"] = {} # type: ignore[typeddict-unknown-key]
28
+ standard_block["extras"][key] = value # type: ignore[typeddict-item]
29
+
30
+ return standard_block
31
+
32
+
33
+ def _convert_to_v1_from_converse_input(
34
+ content: list[types.ContentBlock],
35
+ ) -> list[types.ContentBlock]:
36
+ """Convert Bedrock Converse format blocks to v1 format.
37
+
38
+ During the `content_blocks` parsing process, we wrap blocks not recognized as a v1
39
+ block as a `'non_standard'` block with the original block stored in the `value`
40
+ field. This function attempts to unpack those blocks and convert any blocks that
41
+ might be Converse format to v1 ContentBlocks.
42
+
43
+ If conversion fails, the block is left as a `'non_standard'` block.
44
+
45
+ Args:
46
+ content: List of content blocks to process.
47
+
48
+ Returns:
49
+ Updated list with Converse blocks converted to v1 format.
50
+ """
51
+
52
+ def _iter_blocks() -> Iterator[types.ContentBlock]:
53
+ blocks: list[dict[str, Any]] = [
54
+ cast("dict[str, Any]", block)
55
+ if block.get("type") != "non_standard"
56
+ else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
57
+ for block in content
58
+ ]
59
+ for block in blocks:
60
+ num_keys = len(block)
61
+
62
+ if num_keys == 1 and (text := block.get("text")):
63
+ yield {"type": "text", "text": text}
64
+
65
+ elif (
66
+ num_keys == 1
67
+ and (document := block.get("document"))
68
+ and isinstance(document, dict)
69
+ and "format" in document
70
+ ):
71
+ if document.get("format") == "pdf":
72
+ if "bytes" in document.get("source", {}):
73
+ file_block: types.FileContentBlock = {
74
+ "type": "file",
75
+ "base64": _bytes_to_b64_str(document["source"]["bytes"]),
76
+ "mime_type": "application/pdf",
77
+ }
78
+ _populate_extras(file_block, document, {"format", "source"})
79
+ yield file_block
80
+
81
+ else:
82
+ yield {"type": "non_standard", "value": block}
83
+
84
+ elif document["format"] == "txt":
85
+ if "text" in document.get("source", {}):
86
+ plain_text_block: types.PlainTextContentBlock = {
87
+ "type": "text-plain",
88
+ "text": document["source"]["text"],
89
+ "mime_type": "text/plain",
90
+ }
91
+ _populate_extras(
92
+ plain_text_block, document, {"format", "source"}
93
+ )
94
+ yield plain_text_block
95
+ else:
96
+ yield {"type": "non_standard", "value": block}
97
+
98
+ else:
99
+ yield {"type": "non_standard", "value": block}
100
+
101
+ elif (
102
+ num_keys == 1
103
+ and (image := block.get("image"))
104
+ and isinstance(image, dict)
105
+ and "format" in image
106
+ ):
107
+ if "bytes" in image.get("source", {}):
108
+ image_block: types.ImageContentBlock = {
109
+ "type": "image",
110
+ "base64": _bytes_to_b64_str(image["source"]["bytes"]),
111
+ "mime_type": f"image/{image['format']}",
112
+ }
113
+ _populate_extras(image_block, image, {"format", "source"})
114
+ yield image_block
115
+
116
+ else:
117
+ yield {"type": "non_standard", "value": block}
118
+
119
+ elif block.get("type") in types.KNOWN_BLOCK_TYPES:
120
+ yield cast("types.ContentBlock", block)
121
+
122
+ else:
123
+ yield {"type": "non_standard", "value": block}
124
+
125
+ return list(_iter_blocks())
126
+
127
+
128
+ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
129
+ standard_citation: types.Citation = {"type": "citation"}
130
+ if "title" in citation:
131
+ standard_citation["title"] = citation["title"]
132
+ if (
133
+ (source_content := citation.get("source_content"))
134
+ and isinstance(source_content, list)
135
+ and all(isinstance(item, dict) for item in source_content)
136
+ ):
137
+ standard_citation["cited_text"] = "".join(
138
+ item.get("text", "") for item in source_content
139
+ )
140
+
141
+ known_fields = {"type", "source_content", "title", "index", "extras"}
142
+
143
+ for key, value in citation.items():
144
+ if key not in known_fields:
145
+ if "extras" not in standard_citation:
146
+ standard_citation["extras"] = {}
147
+ standard_citation["extras"][key] = value
148
+
149
+ return standard_citation
150
+
151
+
152
+ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]:
153
+ """Convert Bedrock Converse message content to v1 format."""
154
+ if (
155
+ message.content == ""
156
+ and not message.additional_kwargs
157
+ and not message.tool_calls
158
+ ):
159
+ # Converse outputs multiple chunks containing response metadata
160
+ return []
161
+
162
+ # Translated through a local list so that reading `content_blocks` leaves
163
+ # `message.content` untouched.
164
+ content: list[str | dict[str, Any]] = (
165
+ [{"type": "text", "text": message.content}]
166
+ if isinstance(message.content, str)
167
+ else message.content
168
+ )
169
+
170
+ def _iter_blocks() -> Iterator[types.ContentBlock]:
171
+ for block in content:
172
+ if not isinstance(block, dict):
173
+ continue
174
+ block_type = block.get("type")
175
+
176
+ if block_type == "text":
177
+ if citations := block.get("citations"):
178
+ text_block: types.TextContentBlock = {
179
+ "type": "text",
180
+ "text": block.get("text", ""),
181
+ "annotations": [_convert_citation_to_v1(a) for a in citations],
182
+ }
183
+ else:
184
+ text_block = {"type": "text", "text": block["text"]}
185
+ if "index" in block:
186
+ text_block["index"] = block["index"]
187
+ yield text_block
188
+
189
+ elif block_type == "reasoning_content":
190
+ reasoning_block: types.ReasoningContentBlock = {"type": "reasoning"}
191
+ if reasoning_content := block.get("reasoning_content"):
192
+ if reasoning := reasoning_content.get("text"):
193
+ reasoning_block["reasoning"] = reasoning
194
+ if signature := reasoning_content.get("signature"):
195
+ if "extras" not in reasoning_block:
196
+ reasoning_block["extras"] = {}
197
+ reasoning_block["extras"]["signature"] = signature
198
+
199
+ if "index" in block:
200
+ reasoning_block["index"] = block["index"]
201
+
202
+ known_fields = {"type", "reasoning_content", "index", "extras"}
203
+ for key in block:
204
+ if key not in known_fields:
205
+ if "extras" not in reasoning_block:
206
+ reasoning_block["extras"] = {}
207
+ reasoning_block["extras"][key] = block[key]
208
+ yield reasoning_block
209
+
210
+ elif block_type == "tool_use":
211
+ if (
212
+ isinstance(message, AIMessageChunk)
213
+ and len(message.tool_call_chunks) == 1
214
+ and message.chunk_position != "last"
215
+ ):
216
+ # Isolated chunk
217
+ chunk = message.tool_call_chunks[0]
218
+ tool_call_chunk = types.ToolCallChunk(
219
+ name=chunk.get("name"),
220
+ id=chunk.get("id"),
221
+ args=chunk.get("args"),
222
+ type="tool_call_chunk",
223
+ )
224
+ index = chunk.get("index")
225
+ if index is not None:
226
+ tool_call_chunk["index"] = index
227
+ yield tool_call_chunk
228
+ else:
229
+ tool_call_block: types.ToolCall | None = None
230
+ # Non-streaming or gathered chunk
231
+ if len(message.tool_calls) == 1:
232
+ tool_call_block = {
233
+ "type": "tool_call",
234
+ "name": message.tool_calls[0]["name"],
235
+ "args": message.tool_calls[0]["args"],
236
+ "id": message.tool_calls[0].get("id"),
237
+ }
238
+ elif call_id := block.get("id"):
239
+ for tc in message.tool_calls:
240
+ if tc.get("id") == call_id:
241
+ tool_call_block = {
242
+ "type": "tool_call",
243
+ "name": tc["name"],
244
+ "args": tc["args"],
245
+ "id": tc.get("id"),
246
+ }
247
+ break
248
+ if not tool_call_block:
249
+ tool_call_block = {
250
+ "type": "tool_call",
251
+ "name": block.get("name", ""),
252
+ "args": block.get("input", {}),
253
+ "id": block.get("id", ""),
254
+ }
255
+ if "index" in block:
256
+ tool_call_block["index"] = block["index"]
257
+ yield tool_call_block
258
+
259
+ elif (
260
+ block_type == "input_json_delta"
261
+ and isinstance(message, AIMessageChunk)
262
+ and len(message.tool_call_chunks) == 1
263
+ ):
264
+ chunk = message.tool_call_chunks[0]
265
+ tool_call_chunk = types.ToolCallChunk(
266
+ name=chunk.get("name"),
267
+ id=chunk.get("id"),
268
+ args=chunk.get("args"),
269
+ type="tool_call_chunk",
270
+ )
271
+ index = chunk.get("index")
272
+ if index is not None:
273
+ tool_call_chunk["index"] = index
274
+ yield tool_call_chunk
275
+
276
+ else:
277
+ if "index" in block:
278
+ # Copy before lifting `index` off the block.
279
+ value = block.copy()
280
+ index = value.pop("index")
281
+ new_block: types.NonStandardContentBlock = {
282
+ "type": "non_standard",
283
+ "value": value,
284
+ "index": index,
285
+ }
286
+ else:
287
+ new_block = {"type": "non_standard", "value": block}
288
+ yield new_block
289
+
290
+ return list(_iter_blocks())
291
+
292
+
293
+ def translate_content(message: AIMessage) -> list[types.ContentBlock]:
294
+ """Derive standard content blocks from a message with Bedrock Converse content.
295
+
296
+ Args:
297
+ message: The message to translate.
298
+
299
+ Returns:
300
+ The derived content blocks.
301
+ """
302
+ return _convert_to_v1_from_converse(message)
303
+
304
+
305
+ def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
306
+ """Derive standard content blocks from a chunk with Bedrock Converse content.
307
+
308
+ Args:
309
+ message: The message chunk to translate.
310
+
311
+ Returns:
312
+ The derived content blocks.
313
+ """
314
+ return _convert_to_v1_from_converse(message)
315
+
316
+
317
+ def _register_bedrock_converse_translator() -> None:
318
+ """Register the Bedrock Converse translator with the central registry.
319
+
320
+ Run automatically when the module is imported.
321
+ """
322
+ from coco_core.messages.block_translators import ( # noqa: PLC0415
323
+ register_translator,
324
+ )
325
+
326
+ register_translator("bedrock_converse", translate_content, translate_content_chunk)
327
+
328
+
329
+ _register_bedrock_converse_translator()
@@ -0,0 +1,303 @@
1
+ """Derivations of standard content blocks from Coco v0 multimodal content."""
2
+
3
+ from typing import Any, cast
4
+
5
+ from coco_core.messages import content as types
6
+
7
+
8
+ def _convert_v0_multimodal_input_to_v1(
9
+ content: list[types.ContentBlock],
10
+ ) -> list[types.ContentBlock]:
11
+ """Convert v0 multimodal blocks to v1 format.
12
+
13
+ During the `content_blocks` parsing process, we wrap blocks not recognized as a v1
14
+ block as a `'non_standard'` block with the original block stored in the `value`
15
+ field. This function attempts to unpack those blocks and convert any v0 format
16
+ blocks to v1 format.
17
+
18
+ If conversion fails, the block is left as a `'non_standard'` block.
19
+
20
+ Args:
21
+ content: List of content blocks to process.
22
+
23
+ Returns:
24
+ v1 content blocks.
25
+ """
26
+ converted_blocks = []
27
+ unpacked_blocks: list[dict[str, Any]] = [
28
+ cast("dict[str, Any]", block)
29
+ if block.get("type") != "non_standard"
30
+ else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
31
+ for block in content
32
+ ]
33
+ for block in unpacked_blocks:
34
+ if block.get("type") in {"image", "audio", "file"} and "source_type" in block:
35
+ converted_block = _convert_legacy_v0_content_block_to_v1(block)
36
+ converted_blocks.append(cast("types.ContentBlock", converted_block))
37
+ elif block.get("type") in types.KNOWN_BLOCK_TYPES:
38
+ # Guard in case this function is used outside of the .content_blocks flow
39
+ converted_blocks.append(cast("types.ContentBlock", block))
40
+ else:
41
+ converted_blocks.append({"type": "non_standard", "value": block})
42
+
43
+ return converted_blocks
44
+
45
+
46
+ def _convert_legacy_v0_content_block_to_v1(
47
+ block: dict[str, Any],
48
+ ) -> types.ContentBlock | dict[str, Any]:
49
+ """Convert a Coco v0 content block to v1 format.
50
+
51
+ Preserves unknown keys as extras to avoid data loss.
52
+
53
+ Returns the original block unchanged if it's not in v0 format.
54
+ """
55
+
56
+ def _extract_v0_extras(
57
+ block_dict: dict[str, Any], known_keys: set[str]
58
+ ) -> dict[str, Any]:
59
+ """Extract unknown keys from v0 block to preserve as extras.
60
+
61
+ Args:
62
+ block_dict: The original v0 block dictionary.
63
+ known_keys: Set of keys known to be part of the v0 format for this block.
64
+
65
+ Returns:
66
+ A dictionary of extra keys not part of the known v0 format.
67
+ """
68
+ return {k: v for k, v in block_dict.items() if k not in known_keys}
69
+
70
+ # Check if this is actually a v0 format block
71
+ block_type = block.get("type")
72
+ if block_type not in {"image", "audio", "file"} or "source_type" not in block:
73
+ # Not a v0 format block, return unchanged
74
+ return block
75
+
76
+ if block.get("type") == "image":
77
+ source_type = block.get("source_type")
78
+ if source_type == "url":
79
+ # image-url
80
+ known_keys = {"mime_type", "type", "source_type", "url"}
81
+ extras = _extract_v0_extras(block, known_keys)
82
+ if "id" in block:
83
+ return types.create_image_block(
84
+ url=block["url"],
85
+ mime_type=block.get("mime_type"),
86
+ id=block["id"],
87
+ **extras,
88
+ )
89
+
90
+ # Don't construct with an ID if not present in original block
91
+ v1_image_url = types.ImageContentBlock(type="image", url=block["url"])
92
+ if block.get("mime_type"):
93
+ v1_image_url["mime_type"] = block["mime_type"]
94
+
95
+ v1_image_url["extras"] = {}
96
+ for key, value in extras.items():
97
+ if value is not None:
98
+ v1_image_url["extras"][key] = value
99
+ if v1_image_url["extras"] == {}:
100
+ del v1_image_url["extras"]
101
+
102
+ return v1_image_url
103
+ if source_type == "base64":
104
+ # image-base64
105
+ known_keys = {"mime_type", "type", "source_type", "data"}
106
+ extras = _extract_v0_extras(block, known_keys)
107
+ if "id" in block:
108
+ return types.create_image_block(
109
+ base64=block["data"],
110
+ mime_type=block.get("mime_type"),
111
+ id=block["id"],
112
+ **extras,
113
+ )
114
+
115
+ v1_image_base64 = types.ImageContentBlock(
116
+ type="image", base64=block["data"]
117
+ )
118
+ if block.get("mime_type"):
119
+ v1_image_base64["mime_type"] = block["mime_type"]
120
+
121
+ v1_image_base64["extras"] = {}
122
+ for key, value in extras.items():
123
+ if value is not None:
124
+ v1_image_base64["extras"][key] = value
125
+ if v1_image_base64["extras"] == {}:
126
+ del v1_image_base64["extras"]
127
+
128
+ return v1_image_base64
129
+ if source_type == "id":
130
+ # image-id
131
+ known_keys = {"type", "source_type", "id"}
132
+ extras = _extract_v0_extras(block, known_keys)
133
+ # For id `source_type`, `id` is the file reference, not block ID
134
+ v1_image_id = types.ImageContentBlock(type="image", file_id=block["id"])
135
+
136
+ v1_image_id["extras"] = {}
137
+ for key, value in extras.items():
138
+ if value is not None:
139
+ v1_image_id["extras"][key] = value
140
+ if v1_image_id["extras"] == {}:
141
+ del v1_image_id["extras"]
142
+
143
+ return v1_image_id
144
+ elif block.get("type") == "audio":
145
+ source_type = block.get("source_type")
146
+ if source_type == "url":
147
+ # audio-url
148
+ known_keys = {"mime_type", "type", "source_type", "url"}
149
+ extras = _extract_v0_extras(block, known_keys)
150
+ if "id" in block:
151
+ return types.create_audio_block(
152
+ url=block["url"],
153
+ mime_type=block.get("mime_type"),
154
+ id=block["id"],
155
+ **extras,
156
+ )
157
+
158
+ # Don't construct with an ID if not present in original block
159
+ v1_audio_url: types.AudioContentBlock = types.AudioContentBlock(
160
+ type="audio", url=block["url"]
161
+ )
162
+ if block.get("mime_type"):
163
+ v1_audio_url["mime_type"] = block["mime_type"]
164
+
165
+ v1_audio_url["extras"] = {}
166
+ for key, value in extras.items():
167
+ if value is not None:
168
+ v1_audio_url["extras"][key] = value
169
+ if v1_audio_url["extras"] == {}:
170
+ del v1_audio_url["extras"]
171
+
172
+ return v1_audio_url
173
+ if source_type == "base64":
174
+ # audio-base64
175
+ known_keys = {"mime_type", "type", "source_type", "data"}
176
+ extras = _extract_v0_extras(block, known_keys)
177
+ if "id" in block:
178
+ return types.create_audio_block(
179
+ base64=block["data"],
180
+ mime_type=block.get("mime_type"),
181
+ id=block["id"],
182
+ **extras,
183
+ )
184
+
185
+ v1_audio_base64: types.AudioContentBlock = types.AudioContentBlock(
186
+ type="audio", base64=block["data"]
187
+ )
188
+ if block.get("mime_type"):
189
+ v1_audio_base64["mime_type"] = block["mime_type"]
190
+
191
+ v1_audio_base64["extras"] = {}
192
+ for key, value in extras.items():
193
+ if value is not None:
194
+ v1_audio_base64["extras"][key] = value
195
+ if v1_audio_base64["extras"] == {}:
196
+ del v1_audio_base64["extras"]
197
+
198
+ return v1_audio_base64
199
+ if source_type == "id":
200
+ # audio-id
201
+ known_keys = {"type", "source_type", "id"}
202
+ extras = _extract_v0_extras(block, known_keys)
203
+ v1_audio_id: types.AudioContentBlock = types.AudioContentBlock(
204
+ type="audio", file_id=block["id"]
205
+ )
206
+
207
+ v1_audio_id["extras"] = {}
208
+ for key, value in extras.items():
209
+ if value is not None:
210
+ v1_audio_id["extras"][key] = value
211
+ if v1_audio_id["extras"] == {}:
212
+ del v1_audio_id["extras"]
213
+
214
+ return v1_audio_id
215
+ elif block.get("type") == "file":
216
+ source_type = block.get("source_type")
217
+ if source_type == "url":
218
+ # file-url
219
+ known_keys = {"mime_type", "type", "source_type", "url"}
220
+ extras = _extract_v0_extras(block, known_keys)
221
+ if "id" in block:
222
+ return types.create_file_block(
223
+ url=block["url"],
224
+ mime_type=block.get("mime_type"),
225
+ id=block["id"],
226
+ **extras,
227
+ )
228
+
229
+ v1_file_url: types.FileContentBlock = types.FileContentBlock(
230
+ type="file", url=block["url"]
231
+ )
232
+ if block.get("mime_type"):
233
+ v1_file_url["mime_type"] = block["mime_type"]
234
+
235
+ v1_file_url["extras"] = {}
236
+ for key, value in extras.items():
237
+ if value is not None:
238
+ v1_file_url["extras"][key] = value
239
+ if v1_file_url["extras"] == {}:
240
+ del v1_file_url["extras"]
241
+
242
+ return v1_file_url
243
+ if source_type == "base64":
244
+ # file-base64
245
+ known_keys = {"mime_type", "type", "source_type", "data"}
246
+ extras = _extract_v0_extras(block, known_keys)
247
+ if "id" in block:
248
+ return types.create_file_block(
249
+ base64=block["data"],
250
+ mime_type=block.get("mime_type"),
251
+ id=block["id"],
252
+ **extras,
253
+ )
254
+
255
+ v1_file_base64: types.FileContentBlock = types.FileContentBlock(
256
+ type="file", base64=block["data"]
257
+ )
258
+ if block.get("mime_type"):
259
+ v1_file_base64["mime_type"] = block["mime_type"]
260
+
261
+ v1_file_base64["extras"] = {}
262
+ for key, value in extras.items():
263
+ if value is not None:
264
+ v1_file_base64["extras"][key] = value
265
+ if v1_file_base64["extras"] == {}:
266
+ del v1_file_base64["extras"]
267
+
268
+ return v1_file_base64
269
+ if source_type == "id":
270
+ # file-id
271
+ known_keys = {"type", "source_type", "id"}
272
+ extras = _extract_v0_extras(block, known_keys)
273
+ return types.create_file_block(file_id=block["id"], **extras)
274
+ if source_type == "text":
275
+ # file-text
276
+ known_keys = {"mime_type", "type", "source_type", "url"}
277
+ extras = _extract_v0_extras(block, known_keys)
278
+ if "id" in block:
279
+ return types.create_plaintext_block(
280
+ # In v0, URL points to the text file content
281
+ # TODO: attribute this claim
282
+ text=block["url"],
283
+ id=block["id"],
284
+ **extras,
285
+ )
286
+
287
+ v1_file_text: types.PlainTextContentBlock = types.PlainTextContentBlock(
288
+ type="text-plain", text=block["url"], mime_type="text/plain"
289
+ )
290
+ if block.get("mime_type"):
291
+ v1_file_text["mime_type"] = block["mime_type"]
292
+
293
+ v1_file_text["extras"] = {}
294
+ for key, value in extras.items():
295
+ if value is not None:
296
+ v1_file_text["extras"][key] = value
297
+ if v1_file_text["extras"] == {}:
298
+ del v1_file_text["extras"]
299
+
300
+ return v1_file_text
301
+
302
+ # If we can't convert, return the block unchanged
303
+ return block