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,192 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import (
5
+ Any,
6
+ NamedTuple,
7
+ TypeVar,
8
+ cast,
9
+ )
10
+
11
+ from coco_core.runnables import Runnable, RunnableConfig
12
+
13
+ from coco_graph._internal._constants import CONF, CONFIG_KEY_SEND, TASKS
14
+ from coco_graph._internal._runnable import RunnableCallable
15
+ from coco_graph._internal._typing import MISSING
16
+ from coco_graph.errors import InvalidUpdateError
17
+ from coco_graph.types import Send
18
+
19
+ TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None]
20
+ R = TypeVar("R", bound=Runnable)
21
+
22
+ SKIP_WRITE = object()
23
+ PASSTHROUGH = object()
24
+
25
+
26
+ class ChannelWriteEntry(NamedTuple):
27
+ channel: str
28
+ """Channel name to write to."""
29
+ value: Any = PASSTHROUGH
30
+ """Value to write, or PASSTHROUGH to use the input."""
31
+ skip_none: bool = False
32
+ """Whether to skip writing if the value is None."""
33
+ mapper: Callable | None = None
34
+ """Function to transform the value before writing."""
35
+
36
+
37
+ class ChannelWriteTupleEntry(NamedTuple):
38
+ mapper: Callable[[Any], Sequence[tuple[str, Any]] | None]
39
+ """Function to extract tuples from value."""
40
+ value: Any = PASSTHROUGH
41
+ """Value to write, or PASSTHROUGH to use the input."""
42
+ static: Sequence[tuple[str, Any, str | None]] | None = None
43
+ """Optional, declared writes for static analysis."""
44
+
45
+
46
+ class ChannelWrite(RunnableCallable):
47
+ """Implements the logic for sending writes to CONFIG_KEY_SEND.
48
+ Can be used as a runnable or as a static method to call imperatively."""
49
+
50
+ writes: list[ChannelWriteEntry | ChannelWriteTupleEntry | Send]
51
+ """Sequence of write entries or Send objects to write."""
52
+
53
+ def __init__(
54
+ self,
55
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
56
+ *,
57
+ tags: Sequence[str] | None = None,
58
+ ):
59
+ super().__init__(
60
+ func=self._write,
61
+ afunc=self._awrite,
62
+ name=None,
63
+ tags=tags,
64
+ trace=False,
65
+ )
66
+ self.writes = cast(
67
+ list[ChannelWriteEntry | ChannelWriteTupleEntry | Send], writes
68
+ )
69
+
70
+ def get_name(self, suffix: str | None = None, *, name: str | None = None) -> str:
71
+ if not name:
72
+ name = f"ChannelWrite<{','.join(w.channel if isinstance(w, ChannelWriteEntry) else '...' if isinstance(w, ChannelWriteTupleEntry) else w.node for w in self.writes)}>"
73
+ return super().get_name(suffix, name=name)
74
+
75
+ def _write(self, input: Any, config: RunnableConfig) -> None:
76
+ writes = [
77
+ ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
78
+ if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
79
+ else ChannelWriteTupleEntry(write.mapper, input)
80
+ if isinstance(write, ChannelWriteTupleEntry) and write.value is PASSTHROUGH
81
+ else write
82
+ for write in self.writes
83
+ ]
84
+ self.do_write(
85
+ config,
86
+ writes,
87
+ )
88
+ return input
89
+
90
+ async def _awrite(self, input: Any, config: RunnableConfig) -> None:
91
+ writes = [
92
+ ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
93
+ if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
94
+ else ChannelWriteTupleEntry(write.mapper, input)
95
+ if isinstance(write, ChannelWriteTupleEntry) and write.value is PASSTHROUGH
96
+ else write
97
+ for write in self.writes
98
+ ]
99
+ self.do_write(
100
+ config,
101
+ writes,
102
+ )
103
+ return input
104
+
105
+ @staticmethod
106
+ def do_write(
107
+ config: RunnableConfig,
108
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
109
+ allow_passthrough: bool = True,
110
+ ) -> None:
111
+ # validate
112
+ for w in writes:
113
+ if isinstance(w, ChannelWriteEntry):
114
+ if w.channel == TASKS:
115
+ raise InvalidUpdateError(
116
+ "Cannot write to the reserved channel TASKS"
117
+ )
118
+ if w.value is PASSTHROUGH and not allow_passthrough:
119
+ raise InvalidUpdateError("PASSTHROUGH value must be replaced")
120
+ if isinstance(w, ChannelWriteTupleEntry):
121
+ if w.value is PASSTHROUGH and not allow_passthrough:
122
+ raise InvalidUpdateError("PASSTHROUGH value must be replaced")
123
+ # if we want to persist writes found before hitting a ParentCommand
124
+ # can move this to a finally block
125
+ write: TYPE_SEND = config[CONF][CONFIG_KEY_SEND]
126
+ write(_assemble_writes(writes))
127
+
128
+ @staticmethod
129
+ def is_writer(runnable: Runnable) -> bool:
130
+ """Used by PregelNode to distinguish between writers and other runnables."""
131
+ return (
132
+ isinstance(runnable, ChannelWrite)
133
+ or getattr(runnable, "_is_channel_writer", MISSING) is not MISSING
134
+ )
135
+
136
+ @staticmethod
137
+ def get_static_writes(
138
+ runnable: Runnable,
139
+ ) -> Sequence[tuple[str, Any, str | None]] | None:
140
+ """Used to get conditional writes a writer declares for static analysis."""
141
+ if isinstance(runnable, ChannelWrite):
142
+ return [
143
+ w
144
+ for entry in runnable.writes
145
+ if isinstance(entry, ChannelWriteTupleEntry) and entry.static
146
+ for w in entry.static
147
+ ] or None
148
+ elif writes := getattr(runnable, "_is_channel_writer", MISSING):
149
+ if writes is not MISSING:
150
+ writes = cast(
151
+ Sequence[tuple[ChannelWriteEntry | Send, str | None]],
152
+ writes,
153
+ )
154
+ entries = [e for e, _ in writes]
155
+ labels = [la for _, la in writes]
156
+ return [(*t, la) for t, la in zip(_assemble_writes(entries), labels)]
157
+
158
+ @staticmethod
159
+ def register_writer(
160
+ runnable: R,
161
+ static: Sequence[tuple[ChannelWriteEntry | Send, str | None]] | None = None,
162
+ ) -> R:
163
+ """Used to mark a runnable as a writer, so that it can be detected by is_writer.
164
+ Instances of ChannelWrite are automatically marked as writers.
165
+ Optionally, a list of declared writes can be passed for static analysis."""
166
+ # using object.__setattr__ to work around objects that override __setattr__
167
+ # eg. pydantic models and dataclasses
168
+ object.__setattr__(runnable, "_is_channel_writer", static)
169
+ return runnable
170
+
171
+
172
+ def _assemble_writes(
173
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
174
+ ) -> list[tuple[str, Any]]:
175
+ """Assembles the writes into a list of tuples."""
176
+ tuples: list[tuple[str, Any]] = []
177
+ for w in writes:
178
+ if isinstance(w, Send):
179
+ tuples.append((TASKS, w))
180
+ elif isinstance(w, ChannelWriteTupleEntry):
181
+ if ww := w.mapper(w.value):
182
+ tuples.extend(ww)
183
+ elif isinstance(w, ChannelWriteEntry):
184
+ value = w.mapper(w.value) if w.mapper is not None else w.value
185
+ if value is SKIP_WRITE:
186
+ continue
187
+ if w.skip_none and value is None:
188
+ continue
189
+ tuples.append((w.channel, value))
190
+ else:
191
+ raise ValueError(f"Invalid write entry: {w}")
192
+ return tuples
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
4
+ from dataclasses import asdict
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from coco_core.runnables import RunnableConfig
9
+ from coco_graph.checkpoint.base import (
10
+ EXCLUDED_METADATA_KEYS,
11
+ CheckpointMetadata,
12
+ PendingWrite,
13
+ )
14
+
15
+ from coco_graph._internal._config import filter_to_user_tags, patch_checkpoint_map
16
+ from coco_graph._internal._constants import (
17
+ CONF,
18
+ CONFIG_KEY_CHECKPOINT_NS,
19
+ ERROR,
20
+ INTERRUPT,
21
+ NS_END,
22
+ NS_SEP,
23
+ RETURN,
24
+ )
25
+ from coco_graph._internal._typing import MISSING
26
+ from coco_graph.channels.base import BaseChannel
27
+ from coco_graph.constants import TAG_HIDDEN
28
+ from coco_graph.pregel._io import read_channels
29
+ from coco_graph.types import (
30
+ CheckpointPayload,
31
+ PregelExecutableTask,
32
+ PregelTask,
33
+ StateSnapshot,
34
+ TaskPayload,
35
+ TaskResultPayload,
36
+ )
37
+
38
+ TASK_NAMESPACE = UUID("6ba7b831-9dad-11d1-80b4-00c04fd430c8")
39
+
40
+
41
+ def map_debug_tasks(tasks: Iterable[PregelExecutableTask]) -> Iterator[TaskPayload]:
42
+ """Produce "task" events for stream_mode=debug."""
43
+ for task in tasks:
44
+ if task.config is not None and TAG_HIDDEN in task.config.get("tags", []):
45
+ continue
46
+
47
+ payload: TaskPayload = {
48
+ "id": task.id,
49
+ "name": task.name,
50
+ "input": task.input,
51
+ "triggers": task.triggers,
52
+ }
53
+ # Forward user-meaningful metadata only — drop coco_graph's internal
54
+ # framework keys (coco_graph_node/step/triggers/path/checkpoint_ns,
55
+ # thread_id, ...), which are redundant with the task's own fields and
56
+ # namespace. Keys like `lc_agent_name`, `ls_integration`, and any
57
+ # user-supplied metadata ride along. Filtered config tags are folded in
58
+ # under `tags`, mirroring the messages stream handler. (The comprehension
59
+ # also yields a fresh dict, so mutating `md` doesn't touch task.config.)
60
+ if task.config is not None:
61
+ md = {
62
+ k: v
63
+ for k, v in (task.config.get("metadata") or {}).items()
64
+ if k not in EXCLUDED_METADATA_KEYS
65
+ }
66
+ filtered_tags = filter_to_user_tags(task.config.get("tags"))
67
+ if filtered_tags is not None:
68
+ md["tags"] = filtered_tags
69
+ if md:
70
+ payload["metadata"] = md
71
+ yield payload
72
+
73
+
74
+ def is_multiple_channel_write(value: Any) -> bool:
75
+ """Return True if the payload already wraps multiple writes from the same channel."""
76
+ return (
77
+ isinstance(value, dict)
78
+ and "$writes" in value
79
+ and isinstance(value["$writes"], list)
80
+ )
81
+
82
+
83
+ def map_task_result_writes(writes: Sequence[tuple[str, Any]]) -> dict[str, Any]:
84
+ """Folds task writes into a result dict and aggregates multiple writes to the same channel.
85
+
86
+ If the channel contains a single write, we record the write in the result dict as `{channel: write}`
87
+ If the channel contains multiple writes, we record the writes in the result dict as `{channel: {'$writes': [write1, write2, ...]}}`"""
88
+
89
+ result: dict[str, Any] = {}
90
+ for channel, value in writes:
91
+ existing = result.get(channel)
92
+
93
+ if existing is not None:
94
+ channel_writes = (
95
+ existing["$writes"]
96
+ if is_multiple_channel_write(existing)
97
+ else [existing]
98
+ )
99
+ channel_writes.append(value)
100
+ result[channel] = {"$writes": channel_writes}
101
+ else:
102
+ result[channel] = value
103
+ return result
104
+
105
+
106
+ def map_debug_task_results(
107
+ task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]],
108
+ stream_keys: str | Sequence[str],
109
+ ) -> Iterator[TaskResultPayload]:
110
+ """Produce "task_result" events for stream_mode=debug."""
111
+ stream_channels_list = (
112
+ [stream_keys] if isinstance(stream_keys, str) else stream_keys
113
+ )
114
+ task, writes = task_tup
115
+ yield {
116
+ "id": task.id,
117
+ "name": task.name,
118
+ "error": next((w[1] for w in writes if w[0] == ERROR), None),
119
+ "result": map_task_result_writes(
120
+ [w for w in writes if w[0] in stream_channels_list or w[0] == RETURN]
121
+ ),
122
+ "interrupts": [
123
+ asdict(v)
124
+ for w in writes
125
+ if w[0] == INTERRUPT
126
+ for v in (w[1] if isinstance(w[1], Sequence) else [w[1]])
127
+ ],
128
+ }
129
+
130
+
131
+ def rm_pregel_keys(config: RunnableConfig | None) -> RunnableConfig | None:
132
+ """Remove pregel-specific keys from the config."""
133
+ if config is None:
134
+ return config
135
+ return {
136
+ "configurable": {
137
+ k: v
138
+ for k, v in config.get("configurable", {}).items()
139
+ if not k.startswith("__pregel_")
140
+ }
141
+ }
142
+
143
+
144
+ def map_debug_checkpoint(
145
+ config: RunnableConfig,
146
+ channels: Mapping[str, BaseChannel],
147
+ stream_channels: str | Sequence[str],
148
+ metadata: CheckpointMetadata,
149
+ tasks: Iterable[PregelExecutableTask],
150
+ pending_writes: list[PendingWrite],
151
+ parent_config: RunnableConfig | None,
152
+ output_keys: str | Sequence[str],
153
+ ) -> Iterator[CheckpointPayload]:
154
+ """Produce "checkpoint" events for stream_mode=debug."""
155
+
156
+ parent_ns = config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
157
+ task_states: dict[str, RunnableConfig | StateSnapshot] = {}
158
+
159
+ for task in tasks:
160
+ if not task.subgraphs:
161
+ continue
162
+
163
+ # assemble checkpoint_ns for this task
164
+ task_ns = f"{task.name}{NS_END}{task.id}"
165
+ if parent_ns:
166
+ task_ns = f"{parent_ns}{NS_SEP}{task_ns}"
167
+
168
+ # set config as signal that subgraph checkpoints exist
169
+ task_states[task.id] = {
170
+ CONF: {
171
+ "thread_id": config[CONF]["thread_id"],
172
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
173
+ }
174
+ }
175
+
176
+ yield {
177
+ "config": rm_pregel_keys(patch_checkpoint_map(config, metadata)),
178
+ "parent_config": rm_pregel_keys(patch_checkpoint_map(parent_config, metadata)),
179
+ "values": read_channels(channels, stream_channels),
180
+ "metadata": metadata,
181
+ "next": [t.name for t in tasks],
182
+ "tasks": [
183
+ {
184
+ "id": t.id,
185
+ "name": t.name,
186
+ "error": t.error,
187
+ "state": t.state,
188
+ }
189
+ if t.error
190
+ else {
191
+ "id": t.id,
192
+ "name": t.name,
193
+ "result": t.result,
194
+ "interrupts": tuple(asdict(i) for i in t.interrupts),
195
+ "state": t.state,
196
+ }
197
+ if t.result
198
+ else {
199
+ "id": t.id,
200
+ "name": t.name,
201
+ "interrupts": tuple(asdict(i) for i in t.interrupts),
202
+ "state": t.state,
203
+ }
204
+ for t in tasks_w_writes(tasks, pending_writes, task_states, output_keys)
205
+ ],
206
+ }
207
+
208
+
209
+ def tasks_w_writes(
210
+ tasks: Iterable[PregelTask | PregelExecutableTask],
211
+ pending_writes: list[PendingWrite] | None,
212
+ states: dict[str, RunnableConfig | StateSnapshot] | None,
213
+ output_keys: str | Sequence[str],
214
+ ) -> tuple[PregelTask, ...]:
215
+ """Apply writes / subgraph states to tasks to be returned in a StateSnapshot."""
216
+ pending_writes = pending_writes or []
217
+ out: list[PregelTask] = []
218
+ for task in tasks:
219
+ rtn = next(
220
+ (
221
+ val
222
+ for tid, chan, val in pending_writes
223
+ if tid == task.id and chan == RETURN
224
+ ),
225
+ MISSING,
226
+ )
227
+ task_error = next(
228
+ (exc for tid, n, exc in pending_writes if tid == task.id and n == ERROR),
229
+ None,
230
+ )
231
+ task_interrupts = tuple(
232
+ v
233
+ for tid, n, vv in pending_writes
234
+ if tid == task.id and n == INTERRUPT
235
+ for v in (vv if isinstance(vv, Sequence) else [vv])
236
+ )
237
+
238
+ task_writes = [
239
+ (chan, val)
240
+ for tid, chan, val in pending_writes
241
+ if tid == task.id and chan not in (ERROR, INTERRUPT, RETURN)
242
+ ]
243
+
244
+ if rtn is not MISSING:
245
+ task_result = rtn
246
+ elif isinstance(output_keys, str):
247
+ # unwrap single channel writes to just the write value
248
+ filtered_writes = [
249
+ (chan, val) for chan, val in task_writes if chan == output_keys
250
+ ]
251
+ mapped_writes = map_task_result_writes(filtered_writes)
252
+ task_result = mapped_writes.get(str(output_keys)) if mapped_writes else None
253
+ else:
254
+ if isinstance(output_keys, str):
255
+ output_keys = [output_keys]
256
+ # map task result writes to the desired output channels
257
+ # repeateed writes to the same channel are aggregated into: {'$writes': [write1, write2, ...]}
258
+ filtered_writes = [
259
+ (chan, val) for chan, val in task_writes if chan in output_keys
260
+ ]
261
+ mapped_writes = map_task_result_writes(filtered_writes)
262
+ task_result = mapped_writes if filtered_writes else {}
263
+
264
+ has_writes = rtn is not MISSING or any(
265
+ w[0] == task.id and w[1] not in (ERROR, INTERRUPT) for w in pending_writes
266
+ )
267
+
268
+ out.append(
269
+ PregelTask(
270
+ task.id,
271
+ task.name,
272
+ task.path,
273
+ task_error,
274
+ task_interrupts,
275
+ states.get(task.id) if states else None,
276
+ task_result if has_writes else None,
277
+ )
278
+ )
279
+ return tuple(out)
280
+
281
+
282
+ COLOR_MAPPING = {
283
+ "black": "0;30",
284
+ "red": "0;31",
285
+ "green": "0;32",
286
+ "yellow": "0;33",
287
+ "blue": "0;34",
288
+ "magenta": "0;35",
289
+ "cyan": "0;36",
290
+ "white": "0;37",
291
+ "gray": "1;30",
292
+ }
293
+
294
+
295
+ def get_colored_text(text: str, color: str) -> str:
296
+ """Get colored text."""
297
+ return f"\033[1;3{COLOR_MAPPING[color]}m{text}\033[0m"
298
+
299
+
300
+ def get_bolded_text(text: str) -> str:
301
+ """Get bolded text."""
302
+ return f"\033[1m{text}\033[0m"