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,294 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import Any, NamedTuple, cast
6
+
7
+ from coco_core.runnables.config import RunnableConfig
8
+ from coco_core.runnables.graph import Graph, Node
9
+ from coco_graph.checkpoint.base import BaseCheckpointSaver
10
+
11
+ from coco_graph._internal._constants import CONF, CONFIG_KEY_SEND, INPUT
12
+ from coco_graph.channels.base import BaseChannel
13
+ from coco_graph.channels.last_value import LastValueAfterFinish
14
+ from coco_graph.constants import END, START
15
+ from coco_graph.managed.base import ManagedValueSpec
16
+ from coco_graph.pregel._algo import (
17
+ PregelTaskWrites,
18
+ apply_writes,
19
+ increment,
20
+ prepare_next_tasks,
21
+ )
22
+ from coco_graph.pregel._checkpoint import channels_from_checkpoint, empty_checkpoint
23
+ from coco_graph.pregel._io import map_input
24
+ from coco_graph.pregel._read import PregelNode
25
+ from coco_graph.pregel._write import ChannelWrite
26
+ from coco_graph.types import All, Checkpointer
27
+
28
+
29
+ class Edge(NamedTuple):
30
+ source: str
31
+ target: str
32
+ conditional: bool
33
+ data: str | None
34
+
35
+
36
+ class TriggerEdge(NamedTuple):
37
+ source: str
38
+ conditional: bool
39
+ data: str | None
40
+
41
+
42
+ def draw_graph(
43
+ config: RunnableConfig,
44
+ *,
45
+ nodes: dict[str, PregelNode],
46
+ specs: dict[str, BaseChannel | ManagedValueSpec],
47
+ input_channels: str | Sequence[str],
48
+ interrupt_after_nodes: All | Sequence[str],
49
+ interrupt_before_nodes: All | Sequence[str],
50
+ trigger_to_nodes: Mapping[str, Sequence[str]],
51
+ checkpointer: Checkpointer,
52
+ subgraphs: dict[str, Graph],
53
+ limit: int = 250,
54
+ ) -> Graph:
55
+ """Get the graph for this Pregel instance.
56
+
57
+ Args:
58
+ config: The configuration to use for the graph.
59
+ subgraphs: The subgraphs to include in the graph.
60
+ checkpointer: The checkpointer to use for the graph.
61
+
62
+ Returns:
63
+ The graph for this Pregel instance.
64
+ """
65
+ # (src, dest, is_conditional, label)
66
+ edges: set[Edge] = set()
67
+
68
+ step = -1
69
+ checkpoint = empty_checkpoint()
70
+ get_next_version = (
71
+ checkpointer.get_next_version
72
+ if isinstance(checkpointer, BaseCheckpointSaver)
73
+ else increment
74
+ )
75
+ channels, managed = channels_from_checkpoint(
76
+ specs,
77
+ checkpoint,
78
+ )
79
+ static_seen: set[Any] = set()
80
+ sources: dict[str, set[TriggerEdge]] = {}
81
+ step_sources: dict[str, set[TriggerEdge]] = {}
82
+ static_declared_writes: dict[str, set[TriggerEdge]] = defaultdict(set)
83
+ # remove node mappers
84
+ nodes = {
85
+ k: v.copy(update={"mapper": None}) if v.mapper is not None else v
86
+ for k, v in nodes.items()
87
+ }
88
+ # apply input writes
89
+ input_writes = list(map_input(input_channels, {}))
90
+ updated_channels = apply_writes(
91
+ checkpoint,
92
+ channels,
93
+ [
94
+ PregelTaskWrites((), INPUT, input_writes, []),
95
+ ],
96
+ get_next_version,
97
+ trigger_to_nodes,
98
+ )
99
+ # prepare first tasks
100
+ tasks = prepare_next_tasks(
101
+ checkpoint,
102
+ [],
103
+ nodes,
104
+ channels,
105
+ managed,
106
+ config,
107
+ step,
108
+ -1,
109
+ for_execution=True,
110
+ store=None,
111
+ checkpointer=None,
112
+ manager=None,
113
+ trigger_to_nodes=trigger_to_nodes,
114
+ updated_channels=updated_channels,
115
+ )
116
+ start_tasks = tasks
117
+ # run the pregel loop
118
+ for step in range(step, limit):
119
+ if not tasks:
120
+ break
121
+ conditionals: dict[tuple[str, str, Any], str | None] = {}
122
+ # run task writers
123
+ for task in tasks.values():
124
+ for w in task.writers:
125
+ # apply regular writes
126
+ if isinstance(w, ChannelWrite):
127
+ empty_input = (
128
+ cast(BaseChannel, specs["__root__"]).ValueType()
129
+ if "__root__" in specs
130
+ else None
131
+ )
132
+ w.invoke(empty_input, task.config)
133
+ # apply conditional writes declared for static analysis, only once
134
+ if w not in static_seen:
135
+ static_seen.add(w)
136
+ # apply static writes
137
+ if writes := ChannelWrite.get_static_writes(w):
138
+ # END writes are not written, but become edges directly
139
+ for t in writes:
140
+ if t[0] == END:
141
+ edges.add(Edge(task.name, t[0], True, t[2]))
142
+ writes = [t for t in writes if t[0] != END]
143
+ conditionals.update(
144
+ {(task.name, t[0], t[1] or None): t[2] for t in writes}
145
+ )
146
+ # record static writes for edge creation
147
+ for t in writes:
148
+ static_declared_writes[task.name].add(
149
+ TriggerEdge(t[0], True, t[2])
150
+ )
151
+ task.config[CONF][CONFIG_KEY_SEND]([t[:2] for t in writes])
152
+ # collect sources
153
+ step_sources = {}
154
+ for task in tasks.values():
155
+ task_edges = {
156
+ TriggerEdge(
157
+ w[0],
158
+ (task.name, w[0], w[1] or None) in conditionals,
159
+ conditionals.get((task.name, w[0], w[1] or None)),
160
+ )
161
+ for w in task.writes
162
+ }
163
+ task_edges |= static_declared_writes.get(task.name, set())
164
+ step_sources[task.name] = task_edges
165
+ sources.update(step_sources)
166
+ # invert triggers
167
+ trigger_to_sources: dict[str, set[TriggerEdge]] = defaultdict(set)
168
+ for src, triggers in sources.items():
169
+ for trigger, cond, label in triggers:
170
+ trigger_to_sources[trigger].add(TriggerEdge(src, cond, label))
171
+ # apply writes
172
+ updated_channels = apply_writes(
173
+ checkpoint, channels, tasks.values(), get_next_version, trigger_to_nodes
174
+ )
175
+ # prepare next tasks
176
+ tasks = prepare_next_tasks(
177
+ checkpoint,
178
+ [],
179
+ nodes,
180
+ channels,
181
+ managed,
182
+ config,
183
+ step,
184
+ limit,
185
+ for_execution=True,
186
+ store=None,
187
+ checkpointer=None,
188
+ manager=None,
189
+ trigger_to_nodes=trigger_to_nodes,
190
+ updated_channels=updated_channels,
191
+ )
192
+ # collect deferred nodes
193
+ deferred_nodes: set[str] = set()
194
+ edges_to_deferred_nodes: set[Edge] = set()
195
+ for channel, item in channels.items():
196
+ if isinstance(item, LastValueAfterFinish):
197
+ deferred_node = channel.split(":", 2)[-1]
198
+ deferred_nodes.add(deferred_node)
199
+ # collect edges
200
+ for task in tasks.values():
201
+ added = False
202
+ for trigger in task.triggers:
203
+ for src, cond, label in sorted(trigger_to_sources[trigger]):
204
+ # record edge to be reviewed later
205
+ if task.name in deferred_nodes:
206
+ edges_to_deferred_nodes.add(Edge(src, task.name, cond, label))
207
+ edges.add(Edge(src, task.name, cond, label))
208
+ # if the edge is from this step, skip adding the implicit edges
209
+ if (trigger, cond, label) in step_sources.get(src, set()):
210
+ added = True
211
+ else:
212
+ sources[src].discard(TriggerEdge(trigger, cond, label))
213
+ # if no edges from this step, add implicit edges from all previous tasks
214
+ if not added:
215
+ for src in step_sources:
216
+ edges.add(Edge(src, task.name, True, None))
217
+
218
+ # assemble the graph
219
+ graph = Graph()
220
+ # add nodes
221
+ for name, node in nodes.items():
222
+ metadata = dict(node.metadata or {})
223
+ if name in deferred_nodes:
224
+ metadata["defer"] = True
225
+ if name in interrupt_before_nodes and name in interrupt_after_nodes:
226
+ metadata["__interrupt"] = "before,after"
227
+ elif name in interrupt_before_nodes:
228
+ metadata["__interrupt"] = "before"
229
+ elif name in interrupt_after_nodes:
230
+ metadata["__interrupt"] = "after"
231
+ graph.add_node(node.bound, name, metadata=metadata or None)
232
+ # add start node
233
+ if START not in nodes:
234
+ graph.add_node(None, START)
235
+ for task in start_tasks.values():
236
+ add_edge(graph, START, task.name)
237
+ # add discovered edges
238
+ for src, dest, is_conditional, label in sorted(edges):
239
+ add_edge(
240
+ graph,
241
+ src,
242
+ dest,
243
+ data=label if label != dest else None,
244
+ conditional=is_conditional,
245
+ )
246
+ # add end edges
247
+ termini = {d for _, d, _, _ in edges if d != END}.difference(
248
+ s for s, _, _, _ in edges
249
+ )
250
+ end_edge_exists = any(d == END for _, d, _, _ in edges)
251
+ if termini:
252
+ for src in sorted(termini):
253
+ add_edge(graph, src, END)
254
+ elif len(step_sources) == 1 and not end_edge_exists:
255
+ for src in sorted(step_sources):
256
+ add_edge(graph, src, END, conditional=True)
257
+ # replace subgraphs
258
+ for name, subgraph in subgraphs.items():
259
+ if (
260
+ len(subgraph.nodes) > 1
261
+ and name in graph.nodes
262
+ and subgraph.first_node()
263
+ and subgraph.last_node()
264
+ ):
265
+ subgraph.trim_first_node()
266
+ subgraph.trim_last_node()
267
+ # replace the node with the subgraph
268
+ graph.nodes.pop(name)
269
+ first, last = graph.extend(subgraph, prefix=name)
270
+ for idx, edge in enumerate(graph.edges):
271
+ if edge.source == name:
272
+ edge = edge.copy(source=cast(Node, last).id)
273
+ if edge.target == name:
274
+ edge = edge.copy(target=cast(Node, first).id)
275
+ graph.edges[idx] = edge
276
+
277
+ return graph
278
+
279
+
280
+ def add_edge(
281
+ graph: Graph,
282
+ source: str,
283
+ target: str,
284
+ *,
285
+ data: Any | None = None,
286
+ conditional: bool = False,
287
+ ) -> None:
288
+ """Add an edge to the graph."""
289
+ for edge in graph.edges:
290
+ if edge.source == source and edge.target == target:
291
+ return
292
+ if target not in graph.nodes and target == END:
293
+ graph.add_node(None, END)
294
+ graph.add_edge(graph.nodes[source], graph.nodes[target], data, conditional)
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import concurrent.futures
5
+ import time
6
+ from collections.abc import Awaitable, Callable, Coroutine
7
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
8
+ from contextvars import copy_context
9
+ from types import TracebackType
10
+ from typing import (
11
+ Protocol,
12
+ TypeVar,
13
+ cast,
14
+ )
15
+
16
+ from coco_core.runnables import RunnableConfig
17
+ from coco_core.runnables.config import get_executor_for_config
18
+ from typing_extensions import ParamSpec
19
+
20
+ from coco_graph._internal._future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe
21
+ from coco_graph.errors import GraphBubbleUp
22
+
23
+ P = ParamSpec("P")
24
+ T = TypeVar("T")
25
+
26
+
27
+ class Submit(Protocol[P, T]):
28
+ def __call__( # type: ignore[valid-type]
29
+ self,
30
+ fn: Callable[P, T],
31
+ *args: P.args,
32
+ __name__: str | None = None,
33
+ __cancel_on_exit__: bool = False,
34
+ __reraise_on_exit__: bool = True,
35
+ __next_tick__: bool = False,
36
+ **kwargs: P.kwargs,
37
+ ) -> concurrent.futures.Future[T]: ...
38
+
39
+
40
+ class BackgroundExecutor(AbstractContextManager):
41
+ """A context manager that runs sync tasks in the background.
42
+ Uses a thread pool executor to delegate tasks to separate threads.
43
+ On exit,
44
+ - cancels any (not yet started) tasks with `__cancel_on_exit__=True`
45
+ - waits for all tasks to finish
46
+ - re-raises the first exception from tasks with `__reraise_on_exit__=True`"""
47
+
48
+ def __init__(self, config: RunnableConfig) -> None:
49
+ self.stack = ExitStack()
50
+ self.executor = self.stack.enter_context(get_executor_for_config(config))
51
+ # mapping of Future to (__cancel_on_exit__, __reraise_on_exit__) flags
52
+ self.tasks: dict[concurrent.futures.Future, tuple[bool, bool]] = {}
53
+
54
+ def submit( # type: ignore[valid-type]
55
+ self,
56
+ fn: Callable[P, T],
57
+ *args: P.args,
58
+ __name__: str | None = None, # currently not used in sync version
59
+ __cancel_on_exit__: bool = False, # for sync, can cancel only if not started
60
+ __reraise_on_exit__: bool = True,
61
+ __next_tick__: bool = False,
62
+ **kwargs: P.kwargs,
63
+ ) -> concurrent.futures.Future[T]:
64
+ ctx = copy_context()
65
+ if __next_tick__:
66
+ task = cast(
67
+ concurrent.futures.Future[T],
68
+ self.executor.submit(next_tick, ctx.run, fn, *args, **kwargs), # type: ignore[arg-type]
69
+ )
70
+ else:
71
+ task = self.executor.submit(ctx.run, fn, *args, **kwargs)
72
+ self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
73
+ # add a callback to remove the task from the tasks dict when it's done
74
+ task.add_done_callback(self.done)
75
+ return task
76
+
77
+ def done(self, task: concurrent.futures.Future) -> None:
78
+ """Remove the task from the tasks dict when it's done."""
79
+ try:
80
+ task.result()
81
+ except GraphBubbleUp:
82
+ # This exception is an interruption signal, not an error
83
+ # so we don't want to re-raise it on exit
84
+ self.tasks.pop(task)
85
+ except BaseException:
86
+ pass
87
+ else:
88
+ self.tasks.pop(task)
89
+
90
+ def __enter__(self) -> Submit:
91
+ return self.submit
92
+
93
+ def __exit__(
94
+ self,
95
+ exc_type: type[BaseException] | None,
96
+ exc_value: BaseException | None,
97
+ traceback: TracebackType | None,
98
+ ) -> bool | None:
99
+ # copy the tasks as done() callback may modify the dict
100
+ tasks = self.tasks.copy()
101
+ # cancel all tasks that should be cancelled
102
+ for task, (cancel, _) in tasks.items():
103
+ if cancel:
104
+ task.cancel()
105
+ # wait for all tasks to finish
106
+ if pending := {t for t in tasks if not t.done()}:
107
+ concurrent.futures.wait(pending)
108
+ # shutdown the executor
109
+ self.stack.__exit__(exc_type, exc_value, traceback)
110
+ # if there's already an exception being raised, don't raise another one
111
+ if exc_type is None:
112
+ # re-raise the first exception that occurred in a task
113
+ for task, (_, reraise) in tasks.items():
114
+ if not reraise:
115
+ continue
116
+ try:
117
+ task.result()
118
+ except concurrent.futures.CancelledError:
119
+ pass
120
+
121
+
122
+ class AsyncBackgroundExecutor(AbstractAsyncContextManager):
123
+ """A context manager that runs async tasks in the background.
124
+ Uses the current event loop to delegate tasks to asyncio tasks.
125
+ On exit,
126
+ - cancels any tasks with `__cancel_on_exit__=True`
127
+ - waits for all tasks to finish
128
+ - re-raises the first exception from tasks with `__reraise_on_exit__=True`
129
+ ignoring CancelledError"""
130
+
131
+ def __init__(self, config: RunnableConfig) -> None:
132
+ self.tasks: dict[asyncio.Future, tuple[bool, bool]] = {}
133
+ self.sentinel = object()
134
+ self.loop = asyncio.get_running_loop()
135
+ if max_concurrency := config.get("max_concurrency"):
136
+ self.semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
137
+ max_concurrency
138
+ )
139
+ else:
140
+ self.semaphore = None
141
+
142
+ def submit( # type: ignore[valid-type]
143
+ self,
144
+ fn: Callable[P, Awaitable[T]],
145
+ *args: P.args,
146
+ __name__: str | None = None,
147
+ __cancel_on_exit__: bool = False,
148
+ __reraise_on_exit__: bool = True,
149
+ __next_tick__: bool = False, # noop in async (always True)
150
+ **kwargs: P.kwargs,
151
+ ) -> asyncio.Future[T]:
152
+ coro = cast(Coroutine[None, None, T], fn(*args, **kwargs))
153
+ if self.semaphore:
154
+ coro = gated(self.semaphore, coro)
155
+ if CONTEXT_NOT_SUPPORTED:
156
+ task = run_coroutine_threadsafe(
157
+ coro, self.loop, name=__name__, lazy=__next_tick__
158
+ )
159
+ else:
160
+ task = run_coroutine_threadsafe(
161
+ coro,
162
+ self.loop,
163
+ name=__name__,
164
+ context=copy_context(),
165
+ lazy=__next_tick__,
166
+ )
167
+ self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
168
+ task.add_done_callback(self.done)
169
+ return task
170
+
171
+ def done(self, task: asyncio.Future) -> None:
172
+ try:
173
+ if exc := task.exception():
174
+ # This exception is an interruption signal, not an error
175
+ # so we don't want to re-raise it on exit
176
+ if isinstance(exc, GraphBubbleUp):
177
+ self.tasks.pop(task)
178
+ else:
179
+ self.tasks.pop(task)
180
+ except asyncio.CancelledError:
181
+ self.tasks.pop(task)
182
+
183
+ async def __aenter__(self) -> Submit:
184
+ return self.submit
185
+
186
+ async def __aexit__(
187
+ self,
188
+ exc_type: type[BaseException] | None,
189
+ exc_value: BaseException | None,
190
+ traceback: TracebackType | None,
191
+ ) -> None:
192
+ # copy the tasks as done() callback may modify the dict
193
+ tasks = self.tasks.copy()
194
+ # cancel all tasks that should be cancelled
195
+ for task, (cancel, _) in tasks.items():
196
+ if cancel:
197
+ task.cancel(self.sentinel)
198
+ # wait for all tasks to finish
199
+ if tasks:
200
+ await asyncio.wait(tasks)
201
+ # if there's already an exception being raised, don't raise another one
202
+ if exc_type is None:
203
+ # re-raise the first exception that occurred in a task
204
+ for task, (_, reraise) in tasks.items():
205
+ if not reraise:
206
+ continue
207
+ try:
208
+ if exc := task.exception():
209
+ raise exc
210
+ except asyncio.CancelledError:
211
+ pass
212
+
213
+
214
+ async def gated(semaphore: asyncio.Semaphore, coro: Coroutine[None, None, T]) -> T:
215
+ """A coroutine that waits for a semaphore before running another coroutine."""
216
+ async with semaphore:
217
+ return await coro
218
+
219
+
220
+ def next_tick(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
221
+ """A function that yields control to other threads before running another function."""
222
+ time.sleep(0)
223
+ return fn(*args, **kwargs)
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Iterator, Mapping, Sequence
5
+ from typing import Any, Literal
6
+
7
+ from coco_graph._internal._constants import (
8
+ ERROR,
9
+ INTERRUPT,
10
+ NULL_TASK_ID,
11
+ RESUME,
12
+ RETURN,
13
+ TASKS,
14
+ )
15
+ from coco_graph._internal._typing import EMPTY_SEQ, MISSING
16
+ from coco_graph.channels.base import BaseChannel, EmptyChannelError
17
+ from coco_graph.constants import START, TAG_HIDDEN
18
+ from coco_graph.errors import InvalidUpdateError
19
+ from coco_graph.pregel._log import logger
20
+ from coco_graph.types import Command, PregelExecutableTask, Send
21
+
22
+
23
+ def read_channel(
24
+ channels: Mapping[str, BaseChannel],
25
+ chan: str,
26
+ *,
27
+ catch: bool = True,
28
+ ) -> Any:
29
+ try:
30
+ return channels[chan].get()
31
+ except EmptyChannelError:
32
+ if catch:
33
+ return None
34
+ else:
35
+ raise
36
+
37
+
38
+ def read_channels(
39
+ channels: Mapping[str, BaseChannel],
40
+ select: Sequence[str] | str,
41
+ *,
42
+ skip_empty: bool = True,
43
+ ) -> dict[str, Any] | Any:
44
+ if isinstance(select, str):
45
+ return read_channel(channels, select)
46
+ else:
47
+ values: dict[str, Any] = {}
48
+ for k in select:
49
+ try:
50
+ values[k] = read_channel(channels, k, catch=not skip_empty)
51
+ except EmptyChannelError:
52
+ pass
53
+ return values
54
+
55
+
56
+ def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
57
+ """Map input chunk to a sequence of pending writes in the form (channel, value)."""
58
+ if cmd.graph == Command.PARENT:
59
+ raise InvalidUpdateError("There is no parent graph")
60
+ if cmd.goto:
61
+ if isinstance(cmd.goto, (tuple, list)):
62
+ sends = cmd.goto
63
+ else:
64
+ sends = [cmd.goto]
65
+ for send in sends:
66
+ if isinstance(send, Send):
67
+ yield (NULL_TASK_ID, TASKS, send)
68
+ elif isinstance(send, str):
69
+ yield (NULL_TASK_ID, f"branch:to:{send}", START)
70
+ else:
71
+ raise TypeError(
72
+ f"In Command.goto, expected Send/str, got {type(send).__name__}"
73
+ )
74
+ if cmd.resume is not None:
75
+ yield (NULL_TASK_ID, RESUME, cmd.resume)
76
+ if cmd.update:
77
+ for k, v in cmd._update_as_tuples():
78
+ yield (NULL_TASK_ID, k, v)
79
+
80
+
81
+ def map_input(
82
+ input_channels: str | Sequence[str],
83
+ chunk: dict[str, Any] | Any | None,
84
+ ) -> Iterator[tuple[str, Any]]:
85
+ """Map input chunk to a sequence of pending writes in the form (channel, value)."""
86
+ if chunk is None:
87
+ return
88
+ elif isinstance(input_channels, str):
89
+ yield (input_channels, chunk)
90
+ else:
91
+ if not isinstance(chunk, dict):
92
+ raise TypeError(f"Expected chunk to be a dict, got {type(chunk).__name__}")
93
+ for k in chunk:
94
+ if k in input_channels:
95
+ yield (k, chunk[k])
96
+ else:
97
+ logger.warning(f"Input channel {k} not found in {input_channels}")
98
+
99
+
100
+ def map_output_values(
101
+ output_channels: str | Sequence[str],
102
+ pending_writes: Literal[True] | Sequence[tuple[str, Any]],
103
+ channels: Mapping[str, BaseChannel],
104
+ ) -> Iterator[dict[str, Any] | Any]:
105
+ """Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
106
+ if isinstance(output_channels, str):
107
+ if pending_writes is True or any(
108
+ chan == output_channels for chan, _ in pending_writes
109
+ ):
110
+ yield read_channel(channels, output_channels)
111
+ else:
112
+ if pending_writes is True or {
113
+ c for c, _ in pending_writes if c in output_channels
114
+ }:
115
+ yield read_channels(channels, output_channels)
116
+
117
+
118
+ def map_output_updates(
119
+ output_channels: str | Sequence[str],
120
+ tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
121
+ cached: bool = False,
122
+ ) -> Iterator[dict[str, Any | dict[str, Any]]]:
123
+ """Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
124
+ output_tasks = [
125
+ (t, ww)
126
+ for t, ww in tasks
127
+ if (not t.config or TAG_HIDDEN not in t.config.get("tags", EMPTY_SEQ))
128
+ and ww[0][0] != ERROR
129
+ and ww[0][0] != INTERRUPT
130
+ ]
131
+ if not output_tasks:
132
+ return
133
+ updated: list[tuple[str, Any]] = []
134
+ for task, writes in output_tasks:
135
+ rtn = next((value for chan, value in writes if chan == RETURN), MISSING)
136
+ if rtn is not MISSING:
137
+ updated.append((task.name, rtn))
138
+ elif isinstance(output_channels, str):
139
+ updated.extend(
140
+ (task.name, value) for chan, value in writes if chan == output_channels
141
+ )
142
+ elif any(chan in output_channels for chan, _ in writes):
143
+ counts = Counter(chan for chan, _ in writes)
144
+ if any(counts[chan] > 1 for chan in output_channels):
145
+ updated.extend(
146
+ (
147
+ task.name,
148
+ {chan: value},
149
+ )
150
+ for chan, value in writes
151
+ if chan in output_channels
152
+ )
153
+ else:
154
+ updated.append(
155
+ (
156
+ task.name,
157
+ {
158
+ chan: value
159
+ for chan, value in writes
160
+ if chan in output_channels
161
+ },
162
+ )
163
+ )
164
+ grouped: dict[str, Any] = {t.name: [] for t, _ in output_tasks}
165
+ for node, value in updated:
166
+ grouped[node].append(value)
167
+ for node, value in grouped.items():
168
+ if len(value) == 0:
169
+ grouped[node] = None
170
+ if len(value) == 1:
171
+ grouped[node] = value[0]
172
+ if cached:
173
+ grouped["__metadata__"] = {"cached": cached}
174
+ yield grouped
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("coco_graph")