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,298 @@
1
+ """Utility to convert a user provided function into a Runnable with a ChannelWrite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import concurrent.futures
6
+ import functools
7
+ import inspect
8
+ import sys
9
+ import types
10
+ from collections.abc import Awaitable, Callable, Generator, Sequence
11
+ from datetime import timedelta
12
+ from typing import Any, Generic, TypeVar, cast
13
+
14
+ from coco_core.runnables import Runnable
15
+ from typing_extensions import ParamSpec
16
+
17
+ from coco_graph._internal._constants import CONF, CONFIG_KEY_CALL, RETURN
18
+ from coco_graph._internal._runnable import (
19
+ RunnableCallable,
20
+ RunnableSeq,
21
+ is_async_callable,
22
+ run_in_executor,
23
+ )
24
+ from coco_graph._internal._timeout import (
25
+ coerce_timeout_policy,
26
+ sync_timeout_unsupported,
27
+ )
28
+ from coco_graph.config import get_config
29
+ from coco_graph.pregel._write import ChannelWrite, ChannelWriteEntry
30
+ from coco_graph.types import CachePolicy, RetryPolicy, TimeoutPolicy
31
+
32
+ ##
33
+ # Utilities borrowed from cloudpickle.
34
+ # https://github.com/cloudpipe/cloudpickle/blob/6220b0ce83ffee5e47e06770a1ee38ca9e47c850/cloudpickle/cloudpickle.py#L265
35
+
36
+
37
+ def _getattribute(obj: Any, name: str) -> Any:
38
+ parent = None
39
+ for subpath in name.split("."):
40
+ if subpath == "<locals>":
41
+ raise AttributeError(f"Can't get local attribute {name!r} on {obj!r}")
42
+ try:
43
+ parent = obj
44
+ obj = getattr(obj, subpath)
45
+ except AttributeError:
46
+ raise AttributeError(f"Can't get attribute {name!r} on {obj!r}") from None
47
+ return obj, parent
48
+
49
+
50
+ def _whichmodule(obj: Any, name: str) -> str | None:
51
+ """Find the module an object belongs to.
52
+
53
+ This function differs from ``pickle.whichmodule`` in two ways:
54
+ - it does not mangle the cases where obj's module is __main__ and obj was
55
+ not found in any module.
56
+ - Errors arising during module introspection are ignored, as those errors
57
+ are considered unwanted side effects.
58
+ """
59
+ module_name = getattr(obj, "__module__", None)
60
+
61
+ if module_name is not None:
62
+ return module_name
63
+ # Protect the iteration by using a copy of sys.modules against dynamic
64
+ # modules that trigger imports of other modules upon calls to getattr or
65
+ # other threads importing at the same time.
66
+ for module_name, module in sys.modules.copy().items():
67
+ # Some modules such as coverage can inject non-module objects inside
68
+ # sys.modules
69
+ if (
70
+ module_name == "__main__"
71
+ or module_name == "__mp_main__"
72
+ or module is None
73
+ or not isinstance(module, types.ModuleType)
74
+ ):
75
+ continue
76
+ try:
77
+ if _getattribute(module, name)[0] is obj:
78
+ return module_name
79
+ except Exception:
80
+ pass
81
+ return None
82
+
83
+
84
+ def identifier(obj: Any, name: str | None = None) -> str | None:
85
+ """Return the module and name of an object."""
86
+ from coco_graph._internal._runnable import RunnableCallable, RunnableSeq
87
+ from coco_graph.pregel._read import PregelNode
88
+
89
+ if isinstance(obj, PregelNode):
90
+ obj = obj.bound
91
+ if isinstance(obj, RunnableSeq):
92
+ obj = obj.steps[0]
93
+ if isinstance(obj, RunnableCallable):
94
+ obj = obj.func
95
+ if name is None:
96
+ name = getattr(obj, "__qualname__", None)
97
+ if name is None: # pragma: no cover
98
+ # This used to be needed for Python 2.7 support but is probably not
99
+ # needed anymore. However we keep the __name__ introspection in case
100
+ # users of cloudpickle rely on this old behavior for unknown reasons.
101
+ name = getattr(obj, "__name__", None)
102
+ if name is None:
103
+ return None
104
+
105
+ module_name = getattr(obj, "__module__", None)
106
+ if module_name is None:
107
+ # In this case, obj.__module__ is None. obj is thus treated as dynamic.
108
+ return None
109
+
110
+ return f"{module_name}.{name}"
111
+
112
+
113
+ def _lookup_module_and_qualname(
114
+ obj: Any, name: str | None = None
115
+ ) -> tuple[types.ModuleType, str] | None:
116
+ if name is None:
117
+ name = getattr(obj, "__qualname__", None)
118
+ if name is None: # pragma: no cover
119
+ # This used to be needed for Python 2.7 support but is probably not
120
+ # needed anymore. However we keep the __name__ introspection in case
121
+ # users of cloudpickle rely on this old behavior for unknown reasons.
122
+ name = getattr(obj, "__name__", None)
123
+ if name is None:
124
+ return None
125
+
126
+ module_name = _whichmodule(obj, name)
127
+
128
+ if module_name is None:
129
+ # In this case, obj.__module__ is None AND obj was not found in any
130
+ # imported module. obj is thus treated as dynamic.
131
+ return None
132
+
133
+ if module_name == "__main__":
134
+ return None
135
+
136
+ # Note: if module_name is in sys.modules, the corresponding module is
137
+ # assumed importable at unpickling time. See #357
138
+ module = sys.modules.get(module_name, None)
139
+ if module is None:
140
+ # The main reason why obj's module would not be imported is that this
141
+ # module has been dynamically created, using for example
142
+ # types.ModuleType. The other possibility is that module was removed
143
+ # from sys.modules after obj was created/imported. But this case is not
144
+ # supported, as the standard pickle does not support it either.
145
+ return None
146
+
147
+ try:
148
+ obj2, parent = _getattribute(module, name)
149
+ except AttributeError:
150
+ # obj was not found inside the module it points to
151
+ return None
152
+ if obj2 is not obj:
153
+ return None
154
+ return module, name
155
+
156
+
157
+ def _explode_args_trace_inputs(
158
+ sig: inspect.Signature, input: tuple[tuple[Any, ...], dict[str, Any]]
159
+ ) -> dict[str, Any]:
160
+ args, kwargs = input
161
+ bound = sig.bind_partial(*args, **kwargs)
162
+ bound.apply_defaults()
163
+ arguments = dict(bound.arguments)
164
+ arguments.pop("self", None)
165
+ arguments.pop("cls", None)
166
+ for param_name, param in sig.parameters.items():
167
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
168
+ # Update with the **kwargs, and remove the original entry
169
+ # This is to help flatten out keyword arguments
170
+ if param_name in arguments:
171
+ arguments.update(arguments.pop(param_name))
172
+ return arguments
173
+
174
+
175
+ def get_runnable_for_entrypoint(func: Callable[..., Any]) -> Runnable:
176
+ key = (func, False)
177
+ if key in CACHE:
178
+ return CACHE[key]
179
+ else:
180
+ if is_async_callable(func):
181
+ run = RunnableCallable(
182
+ None, func, name=func.__name__, trace=False, recurse=False
183
+ )
184
+ else:
185
+ afunc = functools.update_wrapper(
186
+ functools.partial(run_in_executor, None, func), func
187
+ )
188
+ run = RunnableCallable(
189
+ func,
190
+ afunc,
191
+ name=func.__name__,
192
+ trace=False,
193
+ recurse=False,
194
+ )
195
+ if not _lookup_module_and_qualname(func):
196
+ return run
197
+ return CACHE.setdefault(key, run)
198
+
199
+
200
+ def get_runnable_for_task(func: Callable[..., Any]) -> Runnable:
201
+ key = (func, True)
202
+ if key in CACHE:
203
+ return CACHE[key]
204
+ else:
205
+ if hasattr(func, "__name__"):
206
+ name = func.__name__
207
+ elif hasattr(func, "func"):
208
+ name = func.func.__name__
209
+ elif hasattr(func, "__class__"):
210
+ name = func.__class__.__name__
211
+ else:
212
+ name = str(func)
213
+
214
+ if is_async_callable(func):
215
+ run = RunnableCallable(
216
+ None,
217
+ func,
218
+ explode_args=True,
219
+ name=name,
220
+ trace=False,
221
+ recurse=False,
222
+ )
223
+ else:
224
+ run = RunnableCallable(
225
+ func,
226
+ functools.wraps(func)(functools.partial(run_in_executor, None, func)),
227
+ explode_args=True,
228
+ name=name,
229
+ trace=False,
230
+ recurse=False,
231
+ )
232
+ seq = RunnableSeq(
233
+ run,
234
+ ChannelWrite([ChannelWriteEntry(RETURN)]),
235
+ name=name,
236
+ trace_inputs=functools.partial(
237
+ _explode_args_trace_inputs, inspect.signature(func)
238
+ ),
239
+ )
240
+ if not _lookup_module_and_qualname(func):
241
+ return seq
242
+ return CACHE.setdefault(key, seq)
243
+
244
+
245
+ CACHE: dict[tuple[Callable[..., Any], bool], Runnable] = {}
246
+
247
+
248
+ P = ParamSpec("P")
249
+ P1 = TypeVar("P1")
250
+ T = TypeVar("T")
251
+
252
+
253
+ class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
254
+ def __await__(self) -> Generator[T, None, T]:
255
+ yield cast(T, ...)
256
+
257
+
258
+ def call(
259
+ func: Callable[P, Awaitable[T]] | Callable[P, T],
260
+ *args: Any,
261
+ retry_policy: Sequence[RetryPolicy] | None = None,
262
+ cache_policy: CachePolicy | None = None,
263
+ timeout: float | timedelta | TimeoutPolicy | None = None,
264
+ **kwargs: Any,
265
+ ) -> SyncAsyncFuture[T]:
266
+ return _call_with_options(
267
+ func,
268
+ args,
269
+ kwargs,
270
+ retry_policy=retry_policy,
271
+ cache_policy=cache_policy,
272
+ timeout=coerce_timeout_policy(timeout),
273
+ )
274
+
275
+
276
+ def _call_with_options(
277
+ func: Callable[P, Awaitable[T]] | Callable[P, T],
278
+ args: tuple[Any, ...],
279
+ kwargs: dict[str, Any],
280
+ *,
281
+ retry_policy: Sequence[RetryPolicy] | None = None,
282
+ cache_policy: CachePolicy | None = None,
283
+ timeout: TimeoutPolicy | None = None,
284
+ ) -> SyncAsyncFuture[T]:
285
+ if timeout is not None and not is_async_callable(func):
286
+ name = getattr(func, "__name__", func.__class__.__name__)
287
+ raise sync_timeout_unsupported(name, kind="Task")
288
+ config = get_config()
289
+ impl = config[CONF][CONFIG_KEY_CALL]
290
+ fut = impl(
291
+ func,
292
+ (args, kwargs),
293
+ retry_policy=retry_policy,
294
+ cache_policy=cache_policy,
295
+ callbacks=config["callbacks"],
296
+ timeout=timeout,
297
+ )
298
+ return fut
@@ -0,0 +1,331 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from collections.abc import Callable, Iterable, Mapping
5
+ from datetime import datetime, timezone
6
+ from typing import Any, cast
7
+
8
+ from coco_core.runnables import RunnableConfig
9
+ from coco_graph.checkpoint.base import (
10
+ BaseCheckpointSaver,
11
+ Checkpoint,
12
+ )
13
+ from coco_graph.checkpoint.base.id import uuid6
14
+ from coco_graph.checkpoint.serde.types import _DeltaSnapshot
15
+
16
+ from coco_graph._internal._config import DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
17
+ from coco_graph._internal._constants import PUSH
18
+ from coco_graph._internal._typing import MISSING
19
+ from coco_graph.channels.base import BaseChannel
20
+ from coco_graph.channels.delta import DeltaChannel
21
+ from coco_graph.managed.base import ManagedValueMapping, ManagedValueSpec
22
+
23
+ LATEST_VERSION = 4
24
+
25
+ GetNextVersion = Callable[[Any, None], Any]
26
+
27
+
28
+ def empty_checkpoint() -> Checkpoint:
29
+ return Checkpoint(
30
+ v=LATEST_VERSION,
31
+ id=str(uuid6(clock_seq=-2)),
32
+ ts=datetime.now(timezone.utc).isoformat(),
33
+ channel_values={},
34
+ channel_versions={},
35
+ versions_seen={},
36
+ )
37
+
38
+
39
+ def exit_delta_task_id(step: int, task_id: str) -> str:
40
+ """Synthetic task id for exit-mode DeltaChannel writes.
41
+
42
+ Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
43
+ preserves chronological order while remaining a valid RFC UUID (required by
44
+ Postgres `checkpoint_writes.task_id uuid` columns).
45
+ """
46
+ parts = str(uuid.UUID(task_id)).split("-")
47
+ return f"{step:08d}-{parts[1]}-{parts[2]}-{parts[3]}-{parts[4]}"
48
+
49
+
50
+ def delta_channels_to_snapshot(
51
+ channels: Mapping[str, BaseChannel],
52
+ counters_since_delta_snapshot: Mapping[str, tuple[int, int]],
53
+ ) -> set[str]:
54
+ """Return the set of DeltaChannel names that should snapshot now.
55
+
56
+ A channel snapshots when EITHER its accumulated update count reaches
57
+ `snapshot_frequency` OR the total supersteps since its last snapshot
58
+ reaches `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. This is a pure
59
+ predicate — no mutation.
60
+ """
61
+ result: set[str] = set()
62
+ for name, ch in channels.items():
63
+ if not isinstance(ch, DeltaChannel) or not ch.is_available():
64
+ continue
65
+ updates, supersteps = counters_since_delta_snapshot.get(name, (0, 0))
66
+ if (
67
+ updates >= ch.snapshot_frequency
68
+ or supersteps >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
69
+ ):
70
+ result.add(name)
71
+ return result
72
+
73
+
74
+ def get_updated_channels_from_tasks(
75
+ run_tasks: Iterable[Any],
76
+ ) -> set[str]:
77
+ """Channel names written by an update_state superstep (excluding PUSH)."""
78
+ return {c for task in run_tasks for c, _ in task.writes if c != PUSH}
79
+
80
+
81
+ def get_delta_channels_from_all_channels(
82
+ channels: Mapping[str, BaseChannel],
83
+ ) -> set[str]:
84
+ """DeltaChannels to snapshot on the first update_state of a fresh thread."""
85
+ return {
86
+ k
87
+ for k, ch in channels.items()
88
+ if isinstance(ch, DeltaChannel) and ch.is_available()
89
+ }
90
+
91
+
92
+ def create_metadata_for_update_state_api(
93
+ channels: Mapping[str, BaseChannel],
94
+ updated_channels: set[str],
95
+ *,
96
+ prev_metadata: Mapping[str, Any] | None,
97
+ ) -> dict[str, tuple[int, int]]:
98
+ """Advance ``counters_since_delta_snapshot`` for update_state on a non-fresh thread.
99
+
100
+ Mirrors the per-superstep counter bump in ``_loop._put_checkpoint``.
101
+ """
102
+ prev_counters = dict(
103
+ (prev_metadata or {}).get("counters_since_delta_snapshot") or {}
104
+ )
105
+ new_counters: dict[str, tuple[int, int]] = {}
106
+ for ch_name, ch in channels.items():
107
+ if not isinstance(ch, DeltaChannel):
108
+ continue
109
+ u, s = prev_counters.get(ch_name, (0, 0))
110
+ s += 1
111
+ if ch_name in updated_channels:
112
+ u += 1
113
+ new_counters[ch_name] = (u, s)
114
+ return new_counters
115
+
116
+
117
+ def create_checkpoint_plan_for_update_state_api(
118
+ channels: Mapping[str, BaseChannel],
119
+ updated_channels: set[str],
120
+ *,
121
+ step: int,
122
+ parents: dict[str, Any],
123
+ saved_metadata: Mapping[str, Any] | None,
124
+ is_fresh_thread: bool,
125
+ ) -> tuple[set[str], dict[str, Any]]:
126
+ """Return ``(channels_to_snapshot, metadata)`` for an update_state head."""
127
+ metadata: dict[str, Any] = {
128
+ "source": "update",
129
+ "step": step,
130
+ "parents": parents,
131
+ }
132
+ if is_fresh_thread:
133
+ return get_delta_channels_from_all_channels(channels), metadata
134
+
135
+ new_counters = create_metadata_for_update_state_api(
136
+ channels,
137
+ updated_channels,
138
+ prev_metadata=saved_metadata,
139
+ )
140
+ channels_to_snapshot = delta_channels_to_snapshot(channels, new_counters)
141
+ for k in channels_to_snapshot:
142
+ new_counters[k] = (0, 0)
143
+ non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
144
+ if non_zero:
145
+ metadata["counters_since_delta_snapshot"] = non_zero
146
+ return channels_to_snapshot, metadata
147
+
148
+
149
+ def create_checkpoint(
150
+ checkpoint: Checkpoint,
151
+ channels: Mapping[str, BaseChannel] | None,
152
+ step: int,
153
+ *,
154
+ id: str | None = None,
155
+ updated_channels: set[str] | None = None,
156
+ get_next_version: GetNextVersion | None = None,
157
+ channels_to_snapshot: set[str] | None = None,
158
+ ) -> Checkpoint:
159
+ """Build a new Checkpoint from the previous one and live channel state.
160
+
161
+ For each name in `channels_to_snapshot`, a `_DeltaSnapshot(value)` blob
162
+ is written into `channel_values[k]`. Other delta channels are omitted
163
+ from `channel_values` — the ancestor walk reconstructs their state
164
+ from `checkpoint_writes`. Callers compute the set via
165
+ `delta_channels_to_snapshot(channels, counters)`; defaults to empty
166
+ (no snapshots) when not provided.
167
+ """
168
+ ts = datetime.now(timezone.utc).isoformat()
169
+ channels_to_snapshot = channels_to_snapshot or set()
170
+ if channels is None:
171
+ values = checkpoint["channel_values"]
172
+ channel_versions = checkpoint["channel_versions"]
173
+ else:
174
+ values = {}
175
+ channel_versions = dict(checkpoint["channel_versions"])
176
+ for k in channels:
177
+ if k not in channel_versions:
178
+ continue
179
+ ch = channels[k]
180
+ if k in channels_to_snapshot:
181
+ # Callers force a full snapshot blob here: exit mode when a
182
+ # delta channel reaches its snapshot cadence, and update_state
183
+ # on a fresh thread (no ancestor to replay writes from). The
184
+ # manual version-bump below only applies to the exit-mode case.
185
+ #
186
+ # In exit mode, the snapshot decision is deferred to exit
187
+ # time (intermediate steps have do_checkpoint=False). The
188
+ # channel's count may have reached snapshot_frequency over
189
+ # several supersteps, but the LAST superstep may not have
190
+ # written to this channel. In that case apply_writes()
191
+ # (in _algo.py) didn't bump this channel's version, so
192
+ # saver.put() wouldn't include it in new_versions and
193
+ # the snapshot blob would be silently dropped. The manual
194
+ # bump below closes the gap. In sync/async durability this
195
+ # branch is effectively dead code (the step that pushes
196
+ # the count to freq always writes the channel).
197
+ if get_next_version is not None and (
198
+ updated_channels is None or k not in updated_channels
199
+ ):
200
+ channel_versions[k] = get_next_version(channel_versions[k], None)
201
+ values[k] = _DeltaSnapshot(ch.get())
202
+ else:
203
+ v = ch.checkpoint()
204
+ if v is not MISSING:
205
+ values[k] = v
206
+ return Checkpoint(
207
+ v=LATEST_VERSION,
208
+ ts=ts,
209
+ id=id or str(uuid6(clock_seq=step)),
210
+ channel_values=values,
211
+ channel_versions=channel_versions,
212
+ versions_seen=checkpoint["versions_seen"],
213
+ updated_channels=None if updated_channels is None else sorted(updated_channels),
214
+ )
215
+
216
+
217
+ def _needs_replay(spec: BaseChannel, stored: object) -> bool:
218
+ """True if `spec` is a `DeltaChannel` and no value is stored at this
219
+ checkpoint, requiring an ancestor walk to reconstruct.
220
+
221
+ `_DeltaSnapshot` blobs and plain values (migration) resolve directly via
222
+ `from_checkpoint` — only absence (`MISSING`) triggers replay.
223
+ """
224
+ if not isinstance(spec, DeltaChannel):
225
+ return False
226
+ return stored is MISSING
227
+
228
+
229
+ def channels_from_checkpoint(
230
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
231
+ checkpoint: Checkpoint,
232
+ *,
233
+ saver: BaseCheckpointSaver | None = None,
234
+ config: RunnableConfig | None = None,
235
+ ) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
236
+ """Hydrate channels from a checkpoint.
237
+
238
+ For most channels, `spec.from_checkpoint(checkpoint["channel_values"][k])`
239
+ is sufficient. `DeltaChannel` is the exception: when the channel is
240
+ absent from `channel_values`, an ancestor walk via
241
+ `saver.get_delta_channel_history` is required to find the nearest seed
242
+ (`_DeltaSnapshot` blob or pre-migration plain value) and accumulate
243
+ the writes between it and the target. All delta channels needing
244
+ replay are batched into a single saver call.
245
+ """
246
+ channel_specs: dict[str, BaseChannel] = {}
247
+ managed_specs: dict[str, ManagedValueSpec] = {}
248
+ for k, v in specs.items():
249
+ if isinstance(v, BaseChannel):
250
+ channel_specs[k] = v
251
+ else:
252
+ managed_specs[k] = v
253
+
254
+ delta_channels: list[str] = [
255
+ k
256
+ for k, spec in channel_specs.items()
257
+ if _needs_replay(spec, checkpoint["channel_values"].get(k, MISSING))
258
+ ]
259
+ histories: Mapping[str, Any] = {}
260
+ if delta_channels and saver is not None and config is not None:
261
+ histories = saver.get_delta_channel_history(
262
+ config=config, channels=delta_channels
263
+ )
264
+
265
+ channels: dict[str, BaseChannel] = {}
266
+ for k, spec in channel_specs.items():
267
+ ch: BaseChannel
268
+ if k in histories:
269
+ delta_spec = cast(DeltaChannel, spec)
270
+ history = histories[k]
271
+ replay_ch = delta_spec.from_checkpoint(history.get("seed", MISSING))
272
+ replay_ch.replay_writes(history["writes"])
273
+ ch = replay_ch
274
+ else:
275
+ ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
276
+ channels[k] = ch
277
+ return channels, managed_specs
278
+
279
+
280
+ async def achannels_from_checkpoint(
281
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
282
+ checkpoint: Checkpoint,
283
+ *,
284
+ saver: BaseCheckpointSaver | None = None,
285
+ config: RunnableConfig | None = None,
286
+ ) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
287
+ """Async version of `channels_from_checkpoint`. See docstring there."""
288
+ channel_specs: dict[str, BaseChannel] = {}
289
+ managed_specs: dict[str, ManagedValueSpec] = {}
290
+ for k, v in specs.items():
291
+ if isinstance(v, BaseChannel):
292
+ channel_specs[k] = v
293
+ else:
294
+ managed_specs[k] = v
295
+
296
+ delta_channels: list[str] = [
297
+ k
298
+ for k, spec in channel_specs.items()
299
+ if _needs_replay(spec, checkpoint["channel_values"].get(k, MISSING))
300
+ ]
301
+ histories: Mapping[str, Any] = {}
302
+ if delta_channels and saver is not None and config is not None:
303
+ histories = await saver.aget_delta_channel_history(
304
+ config=config, channels=delta_channels
305
+ )
306
+
307
+ channels: dict[str, BaseChannel] = {}
308
+ for k, spec in channel_specs.items():
309
+ ch: BaseChannel
310
+ if k in histories:
311
+ delta_spec = cast(DeltaChannel, spec)
312
+ history = histories[k]
313
+ replay_ch = delta_spec.from_checkpoint(history.get("seed", MISSING))
314
+ replay_ch.replay_writes(history["writes"])
315
+ ch = replay_ch
316
+ else:
317
+ ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
318
+ channels[k] = ch
319
+ return channels, managed_specs
320
+
321
+
322
+ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
323
+ return Checkpoint(
324
+ v=checkpoint["v"],
325
+ ts=checkpoint["ts"],
326
+ id=checkpoint["id"],
327
+ channel_values=checkpoint["channel_values"].copy(),
328
+ channel_versions=checkpoint["channel_versions"].copy(),
329
+ versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
330
+ updated_channels=checkpoint.get("updated_channels", None),
331
+ )
File without changes