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,610 @@
1
+ """Derivations of standard content blocks from Google (GenAI) content."""
2
+
3
+ import base64
4
+ import re
5
+ from collections.abc import Iterator
6
+ from typing import Any, cast
7
+
8
+ from coco_core.messages import AIMessage, AIMessageChunk
9
+ from coco_core.messages import content as types
10
+ from coco_core.messages.content import Citation, create_citation
11
+
12
+ try:
13
+ import filetype # type: ignore[import-not-found]
14
+
15
+ _HAS_FILETYPE = True
16
+ except ImportError:
17
+ _HAS_FILETYPE = False
18
+
19
+
20
+ def _bytes_to_b64_str(bytes_: bytes) -> str:
21
+ """Convert bytes to base64 encoded string."""
22
+ return base64.b64encode(bytes_).decode("utf-8")
23
+
24
+
25
+ def translate_grounding_metadata_to_citations(
26
+ grounding_metadata: dict[str, Any],
27
+ ) -> list[Citation]:
28
+ """Translate Google AI grounding metadata to Coco Citations.
29
+
30
+ Args:
31
+ grounding_metadata: Google AI grounding metadata containing web search
32
+ queries, grounding chunks, and grounding supports.
33
+
34
+ Returns:
35
+ List of Citation content blocks derived from the grounding metadata.
36
+
37
+ Example:
38
+ >>> metadata = {
39
+ ... "web_search_queries": ["UEFA Euro 2024 winner"],
40
+ ... "grounding_chunks": [
41
+ ... {
42
+ ... "web": {
43
+ ... "uri": "https://uefa.com/euro2024",
44
+ ... "title": "UEFA Euro 2024 Results",
45
+ ... }
46
+ ... }
47
+ ... ],
48
+ ... "grounding_supports": [
49
+ ... {
50
+ ... "segment": {
51
+ ... "start_index": 0,
52
+ ... "end_index": 47,
53
+ ... "text": "Spain won the UEFA Euro 2024 championship",
54
+ ... },
55
+ ... "grounding_chunk_indices": [0],
56
+ ... }
57
+ ... ],
58
+ ... }
59
+ >>> citations = translate_grounding_metadata_to_citations(metadata)
60
+ >>> len(citations)
61
+ 1
62
+ >>> citations[0]["url"]
63
+ 'https://uefa.com/euro2024'
64
+ """
65
+ if not grounding_metadata:
66
+ return []
67
+
68
+ grounding_chunks = grounding_metadata.get("grounding_chunks", [])
69
+ grounding_supports = grounding_metadata.get("grounding_supports", [])
70
+ web_search_queries = grounding_metadata.get("web_search_queries", [])
71
+
72
+ citations: list[Citation] = []
73
+
74
+ for support in grounding_supports:
75
+ segment = support.get("segment", {})
76
+ chunk_indices = support.get("grounding_chunk_indices", [])
77
+
78
+ start_index = segment.get("start_index")
79
+ end_index = segment.get("end_index")
80
+ cited_text = segment.get("text")
81
+
82
+ # Create a citation for each referenced chunk
83
+ for chunk_index in chunk_indices:
84
+ if chunk_index < len(grounding_chunks):
85
+ chunk = grounding_chunks[chunk_index]
86
+
87
+ # Handle web and maps grounding
88
+ web_info = chunk.get("web") or {}
89
+ maps_info = chunk.get("maps") or {}
90
+
91
+ # Extract citation info depending on source
92
+ url = maps_info.get("uri") or web_info.get("uri")
93
+ title = maps_info.get("title") or web_info.get("title")
94
+
95
+ # Note: confidence_scores is a legacy field from Gemini 2.0 and earlier
96
+ # that indicated confidence (0.0-1.0) for each grounding chunk.
97
+ #
98
+ # In Gemini 2.5+, this field is always None/empty and should be ignored.
99
+ extras_metadata = {
100
+ "web_search_queries": web_search_queries,
101
+ "grounding_chunk_index": chunk_index,
102
+ "confidence_scores": support.get("confidence_scores") or [],
103
+ }
104
+
105
+ # Add maps-specific metadata if present
106
+ if maps_info.get("placeId"):
107
+ extras_metadata["place_id"] = maps_info["placeId"]
108
+
109
+ citation = create_citation(
110
+ url=url,
111
+ title=title,
112
+ start_index=start_index,
113
+ end_index=end_index,
114
+ cited_text=cited_text,
115
+ google_ai_metadata=extras_metadata,
116
+ )
117
+ citations.append(citation)
118
+
119
+ return citations
120
+
121
+
122
+ def _convert_to_v1_from_genai_input(
123
+ content: list[types.ContentBlock],
124
+ ) -> list[types.ContentBlock]:
125
+ """Convert Google GenAI format blocks to v1 format.
126
+
127
+ Called when message isn't an `AIMessage` or `model_provider` isn't set on
128
+ `response_metadata`.
129
+
130
+ During the `content_blocks` parsing process, we wrap blocks not recognized as a v1
131
+ block as a `'non_standard'` block with the original block stored in the `value`
132
+ field. This function attempts to unpack those blocks and convert any blocks that
133
+ might be GenAI format to v1 ContentBlocks.
134
+
135
+ If conversion fails, the block is left as a `'non_standard'` block.
136
+
137
+ Args:
138
+ content: List of content blocks to process.
139
+
140
+ Returns:
141
+ Updated list with GenAI blocks converted to v1 format.
142
+ """
143
+
144
+ def _iter_blocks() -> Iterator[types.ContentBlock]:
145
+ blocks: list[dict[str, Any]] = [
146
+ cast("dict[str, Any]", block)
147
+ if block.get("type") != "non_standard"
148
+ else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
149
+ for block in content
150
+ ]
151
+ for block in blocks:
152
+ num_keys = len(block)
153
+ block_type = block.get("type")
154
+
155
+ if num_keys == 1 and (text := block.get("text")):
156
+ # This is probably a TextContentBlock
157
+ yield {"type": "text", "text": text}
158
+
159
+ elif (
160
+ num_keys == 1
161
+ and (document := block.get("document"))
162
+ and isinstance(document, dict)
163
+ and "format" in document
164
+ ):
165
+ # Handle document format conversion
166
+ doc_format = document.get("format")
167
+ source = document.get("source", {})
168
+
169
+ if doc_format == "pdf" and "bytes" in source:
170
+ # PDF document with byte data
171
+ file_block: types.FileContentBlock = {
172
+ "type": "file",
173
+ "base64": source["bytes"]
174
+ if isinstance(source["bytes"], str)
175
+ else _bytes_to_b64_str(source["bytes"]),
176
+ "mime_type": "application/pdf",
177
+ }
178
+ # Preserve extra fields
179
+ extras = {
180
+ key: value
181
+ for key, value in document.items()
182
+ if key not in {"format", "source"}
183
+ }
184
+ if extras:
185
+ file_block["extras"] = extras
186
+ yield file_block
187
+
188
+ elif doc_format == "txt" and "text" in source:
189
+ # Text document
190
+ plain_text_block: types.PlainTextContentBlock = {
191
+ "type": "text-plain",
192
+ "text": source["text"],
193
+ "mime_type": "text/plain",
194
+ }
195
+ # Preserve extra fields
196
+ extras = {
197
+ key: value
198
+ for key, value in document.items()
199
+ if key not in {"format", "source"}
200
+ }
201
+ if extras:
202
+ plain_text_block["extras"] = extras
203
+ yield plain_text_block
204
+
205
+ else:
206
+ # Unknown document format
207
+ yield {"type": "non_standard", "value": block}
208
+
209
+ elif (
210
+ num_keys == 1
211
+ and (image := block.get("image"))
212
+ and isinstance(image, dict)
213
+ and "format" in image
214
+ ):
215
+ # Handle image format conversion
216
+ img_format = image.get("format")
217
+ source = image.get("source", {})
218
+
219
+ if "bytes" in source:
220
+ # Image with byte data
221
+ image_block: types.ImageContentBlock = {
222
+ "type": "image",
223
+ "base64": source["bytes"]
224
+ if isinstance(source["bytes"], str)
225
+ else _bytes_to_b64_str(source["bytes"]),
226
+ "mime_type": f"image/{img_format}",
227
+ }
228
+ # Preserve extra fields
229
+ extras = {}
230
+ for key, value in image.items():
231
+ if key not in {"format", "source"}:
232
+ extras[key] = value
233
+ if extras:
234
+ image_block["extras"] = extras
235
+ yield image_block
236
+
237
+ else:
238
+ # Image without byte data
239
+ yield {"type": "non_standard", "value": block}
240
+
241
+ elif block_type == "file_data" and "file_uri" in block:
242
+ # Handle FileData URI-based content
243
+ uri_file_block: types.FileContentBlock = {
244
+ "type": "file",
245
+ "url": block["file_uri"],
246
+ }
247
+ if mime_type := block.get("mime_type"):
248
+ uri_file_block["mime_type"] = mime_type
249
+ yield uri_file_block
250
+
251
+ elif block_type == "function_call" and "name" in block:
252
+ # Handle function calls
253
+ tool_call_block: types.ToolCall = {
254
+ "type": "tool_call",
255
+ "name": block["name"],
256
+ "args": block.get("args", {}),
257
+ "id": block.get("id", ""),
258
+ }
259
+ yield tool_call_block
260
+
261
+ elif block_type == "executable_code":
262
+ server_tool_call_input: types.ServerToolCall = {
263
+ "type": "server_tool_call",
264
+ "name": "code_interpreter",
265
+ "args": {
266
+ "code": block.get("executable_code", ""),
267
+ "language": block.get("language", "python"),
268
+ },
269
+ "id": block.get("id", ""),
270
+ }
271
+ yield server_tool_call_input
272
+
273
+ elif block_type == "code_execution_result":
274
+ outcome = block.get("outcome", 1)
275
+ status = "success" if outcome == 1 else "error"
276
+ server_tool_result_input: types.ServerToolResult = {
277
+ "type": "server_tool_result",
278
+ "tool_call_id": block.get("tool_call_id", ""),
279
+ "status": status, # type: ignore[typeddict-item]
280
+ "output": block.get("code_execution_result", ""),
281
+ }
282
+ if outcome is not None:
283
+ server_tool_result_input["extras"] = {"outcome": outcome}
284
+ yield server_tool_result_input
285
+
286
+ elif block.get("type") in types.KNOWN_BLOCK_TYPES:
287
+ # We see a standard block type, so we just cast it, even if
288
+ # we don't fully understand it. This may be dangerous, but
289
+ # it's better than losing information.
290
+ yield cast("types.ContentBlock", block)
291
+
292
+ else:
293
+ # We don't understand this block at all.
294
+ yield {"type": "non_standard", "value": block}
295
+
296
+ return list(_iter_blocks())
297
+
298
+
299
+ def _tool_call_indices(message: AIMessage) -> dict[str, str]:
300
+ """Map tool call id to its streaming index.
301
+
302
+ ChatGoogleGenerativeAI AIMessage .content does not carry function call parts, so
303
+ we recover sequencing information from the tool_call_chunks field.
304
+
305
+ Args:
306
+ message: The message whose tool call chunks should be indexed.
307
+
308
+ Returns:
309
+ Mapping of tool call id to prefixed index, for chunks that have both.
310
+ """
311
+ indices: dict[str, str] = {}
312
+ for chunk in getattr(message, "tool_call_chunks", None) or []:
313
+ id_ = chunk.get("id")
314
+ index = chunk.get("index")
315
+ if id_ and index is not None:
316
+ indices[id_] = f"lc_tc_{index}" # prefix to keep disjoint w other indices
317
+ return indices
318
+
319
+
320
+ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
321
+ """Convert Google GenAI message content to v1 format.
322
+
323
+ Calling `.content_blocks` on an `AIMessage` where `response_metadata.model_provider`
324
+ is set to `'google_genai'` will invoke this function to parse the content into
325
+ standard content blocks for returning.
326
+
327
+ Args:
328
+ message: The `AIMessage` or `AIMessageChunk` to convert.
329
+
330
+ Returns:
331
+ List of standard content blocks derived from the message content.
332
+ """
333
+ if isinstance(message.content, str):
334
+ # String content -> TextContentBlock (only add if non-empty in case of audio)
335
+ string_blocks: list[types.ContentBlock] = []
336
+ if message.content:
337
+ string_blocks.append({"type": "text", "text": message.content})
338
+
339
+ # Add any missing tool calls from message.tool_calls field
340
+ content_tool_call_ids = {
341
+ block.get("id")
342
+ for block in string_blocks
343
+ if isinstance(block, dict) and block.get("type") == "tool_call"
344
+ }
345
+ tool_call_indices = _tool_call_indices(message)
346
+ for tool_call in message.tool_calls:
347
+ id_ = tool_call.get("id")
348
+ if id_ and id_ not in content_tool_call_ids:
349
+ string_tool_call_block: types.ToolCall = {
350
+ "type": "tool_call",
351
+ "id": id_,
352
+ "name": tool_call["name"],
353
+ "args": tool_call["args"],
354
+ }
355
+ if (index := tool_call_indices.get(id_)) is not None:
356
+ string_tool_call_block["index"] = index
357
+ string_blocks.append(string_tool_call_block)
358
+
359
+ # Handle audio from additional_kwargs if present (for empty content cases)
360
+ audio_data = message.additional_kwargs.get("audio")
361
+ if audio_data and isinstance(audio_data, bytes):
362
+ audio_block: types.AudioContentBlock = {
363
+ "type": "audio",
364
+ "base64": _bytes_to_b64_str(audio_data),
365
+ "mime_type": "audio/wav", # Default to WAV for Google GenAI
366
+ }
367
+ string_blocks.append(audio_block)
368
+
369
+ grounding_metadata = message.response_metadata.get("grounding_metadata")
370
+ if grounding_metadata:
371
+ citations = translate_grounding_metadata_to_citations(grounding_metadata)
372
+
373
+ for block in string_blocks:
374
+ if block["type"] == "text" and citations:
375
+ # Add citations to the first text block only
376
+ block["annotations"] = cast("list[types.Annotation]", citations)
377
+ break
378
+
379
+ return string_blocks
380
+
381
+ if not isinstance(message.content, list):
382
+ # Unexpected content type, attempt to represent as text
383
+ return [{"type": "text", "text": str(message.content)}] # type: ignore[unreachable]
384
+
385
+ converted_blocks: list[types.ContentBlock] = []
386
+
387
+ for item in message.content:
388
+ # Where this item's blocks start, so they can inherit its `index` below.
389
+ block_start = len(converted_blocks)
390
+ if isinstance(item, str):
391
+ # Conversation history strings
392
+
393
+ # Citations are handled below after all blocks are converted
394
+ converted_blocks.append({"type": "text", "text": item}) # TextContentBlock
395
+
396
+ elif isinstance(item, dict):
397
+ item_type = item.get("type")
398
+ if item_type == "image_url":
399
+ # Convert image_url to standard image block (base64)
400
+ # (since the original implementation returned as url-base64 CC style)
401
+ image_url = item.get("image_url", {})
402
+ url = image_url.get("url", "")
403
+ if url:
404
+ # Extract base64 data
405
+ match = re.match(r"data:([^;]+);base64,(.+)", url)
406
+ if match:
407
+ # Data URI provided
408
+ mime_type, base64_data = match.groups()
409
+ converted_blocks.append(
410
+ {
411
+ "type": "image",
412
+ "base64": base64_data,
413
+ "mime_type": mime_type,
414
+ }
415
+ )
416
+ else:
417
+ # Assume it's raw base64 without data URI
418
+ try:
419
+ # Validate base64 and decode for MIME type detection
420
+ decoded_bytes = base64.b64decode(url, validate=True)
421
+
422
+ image_url_b64_block = {
423
+ "type": "image",
424
+ "base64": url,
425
+ }
426
+
427
+ if _HAS_FILETYPE:
428
+ # Guess MIME type based on file bytes
429
+ mime_type = None
430
+ kind = filetype.guess(decoded_bytes)
431
+ if kind:
432
+ mime_type = kind.mime
433
+ if mime_type:
434
+ image_url_b64_block["mime_type"] = mime_type
435
+
436
+ converted_blocks.append(
437
+ cast("types.ImageContentBlock", image_url_b64_block)
438
+ )
439
+ except Exception:
440
+ # Not valid base64, treat as non-standard
441
+ converted_blocks.append(
442
+ {
443
+ "type": "non_standard",
444
+ "value": item,
445
+ }
446
+ )
447
+ else:
448
+ # This likely won't be reached according to previous implementations
449
+ converted_blocks.append({"type": "non_standard", "value": item})
450
+ msg = "Image URL not a data URI; appending as non-standard block."
451
+ raise ValueError(msg)
452
+ elif item_type == "function_call":
453
+ # Handle Google GenAI function calls
454
+ function_call_block: types.ToolCall = {
455
+ "type": "tool_call",
456
+ "name": item.get("name", ""),
457
+ "args": item.get("args", {}),
458
+ "id": item.get("id", ""),
459
+ }
460
+ converted_blocks.append(function_call_block)
461
+ elif item_type == "file_data":
462
+ # Handle FileData URI-based content
463
+ file_block: types.FileContentBlock = {
464
+ "type": "file",
465
+ "url": item.get("file_uri", ""),
466
+ }
467
+ if mime_type := item.get("mime_type"):
468
+ file_block["mime_type"] = mime_type
469
+ converted_blocks.append(file_block)
470
+ elif item_type == "thinking":
471
+ # Handling for the 'thinking' type we package thoughts as
472
+ reasoning_block: types.ReasoningContentBlock = {
473
+ "type": "reasoning",
474
+ "reasoning": item.get("thinking", ""),
475
+ }
476
+ if signature := item.get("signature"):
477
+ reasoning_block["extras"] = {"signature": signature}
478
+
479
+ converted_blocks.append(reasoning_block)
480
+ elif item_type == "executable_code":
481
+ # Convert to standard server tool call block at the moment
482
+ server_tool_call_block: types.ServerToolCall = {
483
+ "type": "server_tool_call",
484
+ "name": "code_interpreter",
485
+ "args": {
486
+ "code": item.get("executable_code", ""),
487
+ "language": item.get("language", "python"), # Default to python
488
+ },
489
+ "id": item.get("id", ""),
490
+ }
491
+ converted_blocks.append(server_tool_call_block)
492
+ elif item_type == "code_execution_result":
493
+ # Map outcome to status: OUTCOME_OK (1) → success, else → error
494
+ outcome = item.get("outcome", 1)
495
+ status = "success" if outcome == 1 else "error"
496
+ server_tool_result_block: types.ServerToolResult = {
497
+ "type": "server_tool_result",
498
+ "tool_call_id": item.get("tool_call_id", ""),
499
+ "status": status, # type: ignore[typeddict-item]
500
+ "output": item.get("code_execution_result", ""),
501
+ }
502
+ server_tool_result_block["extras"] = {"block_type": item_type}
503
+ # Preserve original outcome in extras
504
+ if outcome is not None:
505
+ server_tool_result_block["extras"]["outcome"] = outcome
506
+ converted_blocks.append(server_tool_result_block)
507
+ elif item_type == "text":
508
+ converted_blocks.append(cast("types.TextContentBlock", item))
509
+ else:
510
+ # Unknown type, preserve as non-standard
511
+ converted_blocks.append({"type": "non_standard", "value": item})
512
+ else:
513
+ # Non-dict, non-string content
514
+ converted_blocks.append({"type": "non_standard", "value": item}) # type: ignore[unreachable]
515
+
516
+ # Each item yields exactly one block today. Guarded, not assumed:
517
+ # duplicate indices would merge the blocks back together.
518
+ new_blocks = converted_blocks[block_start:]
519
+ if (
520
+ len(new_blocks) == 1
521
+ and isinstance(item, dict)
522
+ and (index := item.get("index")) is not None
523
+ ):
524
+ new_blocks[0].setdefault("index", index)
525
+
526
+ grounding_metadata = message.response_metadata.get("grounding_metadata")
527
+ if grounding_metadata:
528
+ citations = translate_grounding_metadata_to_citations(grounding_metadata)
529
+
530
+ for i, block in enumerate(converted_blocks):
531
+ if block["type"] == "text" and citations:
532
+ # Add citations to text blocks (only the first text block).
533
+ # Replaced rather than assigned into: `block` is the caller's own
534
+ # dict from `message.content`.
535
+ converted_blocks[i] = cast(
536
+ "types.TextContentBlock",
537
+ {**block, "annotations": citations},
538
+ )
539
+ break
540
+
541
+ # Audio is stored on the message.additional_kwargs
542
+ audio_data = message.additional_kwargs.get("audio")
543
+ if audio_data and isinstance(audio_data, bytes):
544
+ audio_block_kwargs: types.AudioContentBlock = {
545
+ "type": "audio",
546
+ "base64": _bytes_to_b64_str(audio_data),
547
+ "mime_type": "audio/wav", # Default to WAV for Google GenAI
548
+ }
549
+ converted_blocks.append(audio_block_kwargs)
550
+
551
+ # Add any missing tool calls from message.tool_calls field
552
+ content_tool_call_ids = {
553
+ block.get("id")
554
+ for block in converted_blocks
555
+ if isinstance(block, dict) and block.get("type") == "tool_call"
556
+ }
557
+ tool_call_indices = _tool_call_indices(message)
558
+ for tool_call in message.tool_calls:
559
+ id_ = tool_call.get("id")
560
+ if id_ and id_ not in content_tool_call_ids:
561
+ missing_tool_call_block: types.ToolCall = {
562
+ "type": "tool_call",
563
+ "id": id_,
564
+ "name": tool_call["name"],
565
+ "args": tool_call["args"],
566
+ }
567
+ if (index := tool_call_indices.get(id_)) is not None:
568
+ missing_tool_call_block["index"] = index
569
+ converted_blocks.append(missing_tool_call_block)
570
+
571
+ return converted_blocks
572
+
573
+
574
+ def translate_content(message: AIMessage) -> list[types.ContentBlock]:
575
+ """Derive standard content blocks from a message with Google (GenAI) content.
576
+
577
+ Args:
578
+ message: The message to translate.
579
+
580
+ Returns:
581
+ The derived content blocks.
582
+ """
583
+ return _convert_to_v1_from_genai(message)
584
+
585
+
586
+ def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
587
+ """Derive standard content blocks from a chunk with Google (GenAI) content.
588
+
589
+ Args:
590
+ message: The message chunk to translate.
591
+
592
+ Returns:
593
+ The derived content blocks.
594
+ """
595
+ return _convert_to_v1_from_genai(message)
596
+
597
+
598
+ def _register_google_genai_translator() -> None:
599
+ """Register the Google (GenAI) translator with the central registry.
600
+
601
+ Run automatically when the module is imported.
602
+ """
603
+ from coco_core.messages.block_translators import ( # noqa: PLC0415
604
+ register_translator,
605
+ )
606
+
607
+ register_translator("google_genai", translate_content, translate_content_chunk)
608
+
609
+
610
+ _register_google_genai_translator()
@@ -0,0 +1,21 @@
1
+ """Derivations of standard content blocks from Google (VertexAI) content."""
2
+
3
+ from coco_core.messages.block_translators.google_genai import (
4
+ translate_content,
5
+ translate_content_chunk,
6
+ )
7
+
8
+
9
+ def _register_google_vertexai_translator() -> None:
10
+ """Register the Google (VertexAI) translator with the central registry.
11
+
12
+ Run automatically when the module is imported.
13
+ """
14
+ from coco_core.messages.block_translators import ( # noqa: PLC0415
15
+ register_translator,
16
+ )
17
+
18
+ register_translator("google_vertexai", translate_content, translate_content_chunk)
19
+
20
+
21
+ _register_google_vertexai_translator()