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,967 @@
1
+ import asyncio
2
+ import threading
3
+ import warnings
4
+ from collections.abc import AsyncIterator, Iterator, Sequence
5
+ from contextlib import asynccontextmanager, contextmanager
6
+ from typing import Any
7
+
8
+ from coco_core.runnables import RunnableConfig
9
+ from coco_graph.checkpoint.base import (
10
+ WRITES_IDX_MAP,
11
+ ChannelVersions,
12
+ Checkpoint,
13
+ CheckpointMetadata,
14
+ CheckpointTuple,
15
+ get_serializable_checkpoint_metadata,
16
+ )
17
+ from coco_graph.checkpoint.serde.base import SerializerProtocol
18
+ from coco_graph.checkpoint.serde.types import TASKS
19
+ from psycopg import (
20
+ AsyncConnection,
21
+ AsyncCursor,
22
+ AsyncPipeline,
23
+ Capabilities,
24
+ Connection,
25
+ Cursor,
26
+ Pipeline,
27
+ )
28
+ from psycopg.rows import DictRow, dict_row
29
+ from psycopg.types.json import Jsonb
30
+ from psycopg_pool import AsyncConnectionPool, ConnectionPool
31
+
32
+ from coco_graph.checkpoint.postgres import _ainternal, _internal
33
+ from coco_graph.checkpoint.postgres.base import BasePostgresSaver
34
+
35
+ """
36
+ To add a new migration, add a new string to the MIGRATIONS list.
37
+ The position of the migration in the list is the version number.
38
+ """
39
+ MIGRATIONS = [
40
+ """CREATE TABLE IF NOT EXISTS checkpoint_migrations (
41
+ v INTEGER PRIMARY KEY
42
+ );""",
43
+ """CREATE TABLE IF NOT EXISTS checkpoints (
44
+ thread_id TEXT NOT NULL,
45
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
46
+ type TEXT,
47
+ checkpoint JSONB NOT NULL,
48
+ metadata JSONB NOT NULL DEFAULT '{}',
49
+ PRIMARY KEY (thread_id, checkpoint_ns)
50
+ );""",
51
+ """CREATE TABLE IF NOT EXISTS checkpoint_blobs (
52
+ thread_id TEXT NOT NULL,
53
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
54
+ channel TEXT NOT NULL,
55
+ type TEXT NOT NULL,
56
+ blob BYTEA,
57
+ PRIMARY KEY (thread_id, checkpoint_ns, channel)
58
+ );""",
59
+ """CREATE TABLE IF NOT EXISTS checkpoint_writes (
60
+ thread_id TEXT NOT NULL,
61
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
62
+ checkpoint_id TEXT NOT NULL,
63
+ task_id TEXT NOT NULL,
64
+ idx INTEGER NOT NULL,
65
+ channel TEXT NOT NULL,
66
+ type TEXT,
67
+ blob BYTEA NOT NULL,
68
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
69
+ );""",
70
+ """
71
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
72
+ """,
73
+ """
74
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id);
75
+ """,
76
+ """
77
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
78
+ """,
79
+ """
80
+ ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
81
+ """,
82
+ ]
83
+
84
+ SELECT_SQL = f"""
85
+ select
86
+ thread_id,
87
+ checkpoint,
88
+ checkpoint_ns,
89
+ metadata,
90
+ (
91
+ select array_agg(array[bl.channel::bytea, bl.type::bytea, bl.blob])
92
+ from jsonb_each_text(checkpoint -> 'channel_versions')
93
+ inner join checkpoint_blobs bl
94
+ on bl.thread_id = checkpoints.thread_id
95
+ and bl.checkpoint_ns = checkpoints.checkpoint_ns
96
+ and bl.channel = jsonb_each_text.key
97
+ ) as channel_values,
98
+ (
99
+ select
100
+ array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
101
+ from checkpoint_writes cw
102
+ where cw.thread_id = checkpoints.thread_id
103
+ and cw.checkpoint_ns = checkpoints.checkpoint_ns
104
+ and cw.checkpoint_id = (checkpoint->>'id')
105
+ ) as pending_writes,
106
+ (
107
+ select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_path, cw.task_id, cw.idx)
108
+ from checkpoint_writes cw
109
+ where cw.thread_id = checkpoints.thread_id
110
+ and cw.checkpoint_ns = checkpoints.checkpoint_ns
111
+ and cw.channel = '{TASKS}'
112
+ ) as pending_sends
113
+ from checkpoints """
114
+
115
+ UPSERT_CHECKPOINT_BLOBS_SQL = """
116
+ INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, type, blob)
117
+ VALUES (%s, %s, %s, %s, %s)
118
+ ON CONFLICT (thread_id, checkpoint_ns, channel) DO UPDATE SET
119
+ type = EXCLUDED.type,
120
+ blob = EXCLUDED.blob;
121
+ """
122
+
123
+ UPSERT_CHECKPOINTS_SQL = """
124
+ INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint, metadata)
125
+ VALUES (%s, %s, %s, %s)
126
+ ON CONFLICT (thread_id, checkpoint_ns)
127
+ DO UPDATE SET
128
+ checkpoint = EXCLUDED.checkpoint,
129
+ metadata = EXCLUDED.metadata;
130
+ """
131
+
132
+ UPSERT_CHECKPOINT_WRITES_SQL = """
133
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
134
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
135
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
136
+ channel = EXCLUDED.channel,
137
+ type = EXCLUDED.type,
138
+ blob = EXCLUDED.blob;
139
+ """
140
+
141
+ INSERT_CHECKPOINT_WRITES_SQL = """
142
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
143
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
144
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
145
+ """
146
+
147
+
148
+ def _dump_blobs(
149
+ serde: SerializerProtocol,
150
+ thread_id: str,
151
+ checkpoint_ns: str,
152
+ values: dict[str, Any],
153
+ versions: ChannelVersions,
154
+ ) -> list[tuple[str, str, str, str, bytes | None]]:
155
+ if not versions:
156
+ return []
157
+
158
+ return [
159
+ (
160
+ thread_id,
161
+ checkpoint_ns,
162
+ k,
163
+ *(serde.dumps_typed(values[k]) if k in values else ("empty", None)),
164
+ )
165
+ for k in versions
166
+ ]
167
+
168
+
169
+ class ShallowPostgresSaver(BasePostgresSaver):
170
+ """A checkpoint saver that uses Postgres to store checkpoints.
171
+
172
+ This checkpointer ONLY stores the most recent checkpoint and does NOT retain any history.
173
+ It is meant to be a light-weight drop-in replacement for the PostgresSaver that
174
+ supports most of the CocoGraph persistence functionality with the exception of time travel.
175
+ """
176
+
177
+ SELECT_SQL = SELECT_SQL
178
+ MIGRATIONS = MIGRATIONS
179
+ UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
180
+ UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
181
+ UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
182
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
183
+
184
+ lock: threading.Lock
185
+
186
+ def __init__(
187
+ self,
188
+ conn: _internal.Conn,
189
+ pipe: Pipeline | None = None,
190
+ serde: SerializerProtocol | None = None,
191
+ ) -> None:
192
+ warnings.warn(
193
+ "ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
194
+ "Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., durability='exit')`.",
195
+ DeprecationWarning,
196
+ stacklevel=2,
197
+ )
198
+ super().__init__(serde=serde)
199
+ if isinstance(conn, ConnectionPool) and pipe is not None:
200
+ raise ValueError(
201
+ "Pipeline should be used only with a single Connection, not ConnectionPool."
202
+ )
203
+
204
+ self.conn = conn
205
+ self.pipe = pipe
206
+ self.lock = threading.Lock()
207
+ self.supports_pipeline = Capabilities().has_pipeline()
208
+
209
+ @classmethod
210
+ @contextmanager
211
+ def from_conn_string(
212
+ cls, conn_string: str, *, pipeline: bool = False
213
+ ) -> Iterator["ShallowPostgresSaver"]:
214
+ """Create a new ShallowPostgresSaver instance from a connection string.
215
+
216
+ Args:
217
+ conn_string: The Postgres connection info string.
218
+ pipeline: whether to use Pipeline
219
+
220
+ Returns:
221
+ ShallowPostgresSaver: A new ShallowPostgresSaver instance.
222
+ """
223
+ with Connection.connect(
224
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
225
+ ) as conn:
226
+ if pipeline:
227
+ with conn.pipeline() as pipe:
228
+ yield cls(conn, pipe)
229
+ else:
230
+ yield cls(conn)
231
+
232
+ def setup(self) -> None:
233
+ """Set up the checkpoint database asynchronously.
234
+
235
+ This method creates the necessary tables in the Postgres database if they don't
236
+ already exist and runs database migrations. It MUST be called directly by the user
237
+ the first time checkpointer is used.
238
+ """
239
+ with self._cursor() as cur:
240
+ cur.execute(self.MIGRATIONS[0])
241
+ results = cur.execute(
242
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
243
+ )
244
+ row = results.fetchone()
245
+ if row is None:
246
+ version = -1
247
+ else:
248
+ version = row["v"]
249
+ for v, migration in zip(
250
+ range(version + 1, len(self.MIGRATIONS)),
251
+ self.MIGRATIONS[version + 1 :],
252
+ strict=False,
253
+ ):
254
+ cur.execute(migration)
255
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
256
+ if self.pipe:
257
+ self.pipe.sync()
258
+
259
+ def list(
260
+ self,
261
+ config: RunnableConfig | None,
262
+ *,
263
+ filter: dict[str, Any] | None = None,
264
+ before: RunnableConfig | None = None,
265
+ limit: int | None = None,
266
+ ) -> Iterator[CheckpointTuple]:
267
+ """List checkpoints from the database.
268
+
269
+ This method retrieves a list of checkpoint tuples from the Postgres database based
270
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
271
+ ONLY the most recent checkpoint.
272
+ """
273
+ where, args = self._search_where(config, filter, before)
274
+ query = self.SELECT_SQL + where
275
+ params = list(args)
276
+ if limit is not None:
277
+ query += " LIMIT %s"
278
+ params.append(int(limit))
279
+ with self._cursor() as cur:
280
+ cur.execute(query, params, binary=True)
281
+ for value in cur:
282
+ checkpoint: Checkpoint = {
283
+ **value["checkpoint"],
284
+ "channel_values": self._load_blobs(value["channel_values"]),
285
+ "pending_sends": [
286
+ self.serde.loads_typed((t.decode(), v))
287
+ for t, v in value["pending_sends"]
288
+ ]
289
+ if value["pending_sends"]
290
+ else [],
291
+ }
292
+ yield CheckpointTuple(
293
+ config={
294
+ "configurable": {
295
+ "thread_id": value["thread_id"],
296
+ "checkpoint_ns": value["checkpoint_ns"],
297
+ "checkpoint_id": checkpoint["id"],
298
+ }
299
+ },
300
+ checkpoint=checkpoint,
301
+ metadata=value["metadata"],
302
+ pending_writes=self._load_writes(value["pending_writes"]),
303
+ )
304
+
305
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
306
+ """Get a checkpoint tuple from the database.
307
+
308
+ This method retrieves a checkpoint tuple from the Postgres database based on the
309
+ provided config (matching the thread ID in the config).
310
+
311
+ Args:
312
+ config: The config to use for retrieving the checkpoint.
313
+
314
+ Returns:
315
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
316
+
317
+ Examples:
318
+
319
+ Basic:
320
+ >>> config = {"configurable": {"thread_id": "1"}}
321
+ >>> checkpoint_tuple = memory.get_tuple(config)
322
+ >>> print(checkpoint_tuple)
323
+ CheckpointTuple(...)
324
+
325
+ With timestamp:
326
+
327
+ >>> config = {
328
+ ... "configurable": {
329
+ ... "thread_id": "1",
330
+ ... "checkpoint_ns": "",
331
+ ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
332
+ ... }
333
+ ... }
334
+ >>> checkpoint_tuple = memory.get_tuple(config)
335
+ >>> print(checkpoint_tuple)
336
+ CheckpointTuple(...)
337
+ """
338
+ thread_id = config["configurable"]["thread_id"]
339
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
340
+ args = (thread_id, checkpoint_ns)
341
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s"
342
+
343
+ with self._cursor() as cur:
344
+ cur.execute(
345
+ self.SELECT_SQL + where,
346
+ args,
347
+ binary=True,
348
+ )
349
+
350
+ for value in cur:
351
+ checkpoint: Checkpoint = {
352
+ **value["checkpoint"],
353
+ "channel_values": self._load_blobs(value["channel_values"]),
354
+ "pending_sends": [
355
+ self.serde.loads_typed((t.decode(), v))
356
+ for t, v in value["pending_sends"]
357
+ ]
358
+ if value["pending_sends"]
359
+ else [],
360
+ }
361
+ return CheckpointTuple(
362
+ config={
363
+ "configurable": {
364
+ "thread_id": thread_id,
365
+ "checkpoint_ns": checkpoint_ns,
366
+ "checkpoint_id": checkpoint["id"],
367
+ }
368
+ },
369
+ checkpoint=checkpoint,
370
+ metadata=value["metadata"],
371
+ pending_writes=self._load_writes(value["pending_writes"]),
372
+ )
373
+
374
+ def put(
375
+ self,
376
+ config: RunnableConfig,
377
+ checkpoint: Checkpoint,
378
+ metadata: CheckpointMetadata,
379
+ new_versions: ChannelVersions,
380
+ ) -> RunnableConfig:
381
+ """Save a checkpoint to the database.
382
+
383
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
384
+ with the provided config. For ShallowPostgresSaver, this method saves ONLY the most recent
385
+ checkpoint and overwrites a previous checkpoint, if it exists.
386
+
387
+ Args:
388
+ config: The config to associate with the checkpoint.
389
+ checkpoint: The checkpoint to save.
390
+ metadata: Additional metadata to save with the checkpoint.
391
+ new_versions: New channel versions as of this write.
392
+
393
+ Returns:
394
+ RunnableConfig: Updated configuration after storing the checkpoint.
395
+
396
+ Examples:
397
+
398
+ >>> from coco_graph.checkpoint.postgres import ShallowPostgresSaver
399
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
400
+ >>> with ShallowPostgresSaver.from_conn_string(DB_URI) as memory:
401
+ >>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
402
+ >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
403
+ >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
404
+ >>> print(saved_config)
405
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
406
+ """
407
+ configurable = config["configurable"].copy()
408
+ thread_id = configurable.pop("thread_id")
409
+ checkpoint_ns = configurable.pop("checkpoint_ns")
410
+
411
+ copy = checkpoint.copy()
412
+ next_config = {
413
+ "configurable": {
414
+ "thread_id": thread_id,
415
+ "checkpoint_ns": checkpoint_ns,
416
+ "checkpoint_id": checkpoint["id"],
417
+ }
418
+ }
419
+
420
+ with self._cursor(pipeline=True) as cur:
421
+ cur.execute(
422
+ """DELETE FROM checkpoint_writes
423
+ WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id NOT IN (%s, %s)""",
424
+ (
425
+ thread_id,
426
+ checkpoint_ns,
427
+ checkpoint["id"],
428
+ configurable.get("checkpoint_id", ""),
429
+ ),
430
+ )
431
+ cur.executemany(
432
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
433
+ _dump_blobs(
434
+ self.serde,
435
+ thread_id,
436
+ checkpoint_ns,
437
+ copy.pop("channel_values"), # type: ignore[misc]
438
+ new_versions,
439
+ ),
440
+ )
441
+ cur.execute(
442
+ self.UPSERT_CHECKPOINTS_SQL,
443
+ (
444
+ thread_id,
445
+ checkpoint_ns,
446
+ Jsonb(copy),
447
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
448
+ ),
449
+ )
450
+ return next_config
451
+
452
+ def put_writes(
453
+ self,
454
+ config: RunnableConfig,
455
+ writes: Sequence[tuple[str, Any]],
456
+ task_id: str,
457
+ task_path: str = "",
458
+ ) -> None:
459
+ """Store intermediate writes linked to a checkpoint.
460
+
461
+ This method saves intermediate writes associated with a checkpoint to the Postgres database.
462
+
463
+ Args:
464
+ config: Configuration of the related checkpoint.
465
+ writes: List of writes to store.
466
+ task_id: Identifier for the task creating the writes.
467
+ """
468
+ query = (
469
+ self.UPSERT_CHECKPOINT_WRITES_SQL
470
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
471
+ else self.INSERT_CHECKPOINT_WRITES_SQL
472
+ )
473
+ with self._cursor(pipeline=True) as cur:
474
+ cur.executemany(
475
+ query,
476
+ self._dump_writes(
477
+ config["configurable"]["thread_id"],
478
+ config["configurable"]["checkpoint_ns"],
479
+ config["configurable"]["checkpoint_id"],
480
+ task_id,
481
+ task_path,
482
+ writes,
483
+ ),
484
+ )
485
+
486
+ @contextmanager
487
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
488
+ """Create a database cursor as a context manager.
489
+
490
+ Args:
491
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
492
+ Will be applied regardless of whether the ShallowPostgresSaver instance was initialized with a pipeline.
493
+ If pipeline mode is not supported, will fall back to using transaction context manager.
494
+ """
495
+ with _internal.get_connection(self.conn) as conn:
496
+ if self.pipe:
497
+ # a connection in pipeline mode can be used concurrently
498
+ # in multiple threads/coroutines, but only one cursor can be
499
+ # used at a time
500
+ try:
501
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
502
+ yield cur
503
+ finally:
504
+ if pipeline:
505
+ self.pipe.sync()
506
+ elif pipeline:
507
+ # a connection not in pipeline mode can only be used by one
508
+ # thread/coroutine at a time, so we acquire a lock
509
+ if self.supports_pipeline:
510
+ with (
511
+ self.lock,
512
+ conn.pipeline(),
513
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
514
+ ):
515
+ yield cur
516
+ else:
517
+ # Use connection's transaction context manager when pipeline mode not supported
518
+ with (
519
+ self.lock,
520
+ conn.transaction(),
521
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
522
+ ):
523
+ yield cur
524
+ else:
525
+ with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
526
+ yield cur
527
+
528
+
529
+ class AsyncShallowPostgresSaver(BasePostgresSaver):
530
+ """A checkpoint saver that uses Postgres to store checkpoints asynchronously.
531
+
532
+ This checkpointer ONLY stores the most recent checkpoint and does NOT retain any history.
533
+ It is meant to be a light-weight drop-in replacement for the AsyncPostgresSaver that
534
+ supports most of the CocoGraph persistence functionality with the exception of time travel.
535
+ """
536
+
537
+ SELECT_SQL = SELECT_SQL
538
+ MIGRATIONS = MIGRATIONS
539
+ UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
540
+ UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
541
+ UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
542
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
543
+ lock: asyncio.Lock
544
+
545
+ def __init__(
546
+ self,
547
+ conn: _ainternal.Conn,
548
+ pipe: AsyncPipeline | None = None,
549
+ serde: SerializerProtocol | None = None,
550
+ ) -> None:
551
+ warnings.warn(
552
+ "AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
553
+ "Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.",
554
+ DeprecationWarning,
555
+ stacklevel=2,
556
+ )
557
+ super().__init__(serde=serde)
558
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
559
+ raise ValueError(
560
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
561
+ )
562
+
563
+ self.conn = conn
564
+ self.pipe = pipe
565
+ self.lock = asyncio.Lock()
566
+ self.loop = asyncio.get_running_loop()
567
+ self.supports_pipeline = Capabilities().has_pipeline()
568
+
569
+ @classmethod
570
+ @asynccontextmanager
571
+ async def from_conn_string(
572
+ cls,
573
+ conn_string: str,
574
+ *,
575
+ pipeline: bool = False,
576
+ serde: SerializerProtocol | None = None,
577
+ ) -> AsyncIterator["AsyncShallowPostgresSaver"]:
578
+ """Create a new AsyncShallowPostgresSaver instance from a connection string.
579
+
580
+ Args:
581
+ conn_string: The Postgres connection info string.
582
+ pipeline: whether to use AsyncPipeline
583
+
584
+ Returns:
585
+ AsyncShallowPostgresSaver: A new AsyncShallowPostgresSaver instance.
586
+ """
587
+ async with await AsyncConnection.connect(
588
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
589
+ ) as conn:
590
+ if pipeline:
591
+ async with conn.pipeline() as pipe:
592
+ yield cls(conn=conn, pipe=pipe, serde=serde)
593
+ else:
594
+ yield cls(conn=conn, serde=serde)
595
+
596
+ async def setup(self) -> None:
597
+ """Set up the checkpoint database asynchronously.
598
+
599
+ This method creates the necessary tables in the Postgres database if they don't
600
+ already exist and runs database migrations. It MUST be called directly by the user
601
+ the first time checkpointer is used.
602
+ """
603
+ async with self._cursor() as cur:
604
+ await cur.execute(self.MIGRATIONS[0])
605
+ results = await cur.execute(
606
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
607
+ )
608
+ row = await results.fetchone()
609
+ if row is None:
610
+ version = -1
611
+ else:
612
+ version = row["v"]
613
+ for v, migration in zip(
614
+ range(version + 1, len(self.MIGRATIONS)),
615
+ self.MIGRATIONS[version + 1 :],
616
+ strict=False,
617
+ ):
618
+ await cur.execute(migration)
619
+ await cur.execute(
620
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
621
+ )
622
+ if self.pipe:
623
+ await self.pipe.sync()
624
+
625
+ async def alist(
626
+ self,
627
+ config: RunnableConfig | None,
628
+ *,
629
+ filter: dict[str, Any] | None = None,
630
+ before: RunnableConfig | None = None,
631
+ limit: int | None = None,
632
+ ) -> AsyncIterator[CheckpointTuple]:
633
+ """List checkpoints from the database asynchronously.
634
+
635
+ This method retrieves a list of checkpoint tuples from the Postgres database based
636
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
637
+ ONLY the most recent checkpoint.
638
+ """
639
+ where, args = self._search_where(config, filter, before)
640
+ query = self.SELECT_SQL + where
641
+ params = list(args)
642
+ if limit is not None:
643
+ query += " LIMIT %s"
644
+ params.append(int(limit))
645
+ async with self._cursor() as cur:
646
+ await cur.execute(query, params, binary=True)
647
+ async for value in cur:
648
+ checkpoint: Checkpoint = {
649
+ **value["checkpoint"],
650
+ "channel_values": self._load_blobs(value["channel_values"]),
651
+ "pending_sends": [
652
+ self.serde.loads_typed((t.decode(), v))
653
+ for t, v in value["pending_sends"]
654
+ ]
655
+ if value["pending_sends"]
656
+ else [],
657
+ }
658
+ yield CheckpointTuple(
659
+ config={
660
+ "configurable": {
661
+ "thread_id": value["thread_id"],
662
+ "checkpoint_ns": value["checkpoint_ns"],
663
+ "checkpoint_id": checkpoint["id"],
664
+ }
665
+ },
666
+ checkpoint=checkpoint,
667
+ metadata=value["metadata"],
668
+ pending_writes=await asyncio.to_thread(
669
+ self._load_writes, value["pending_writes"]
670
+ ),
671
+ )
672
+
673
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
674
+ """Get a checkpoint tuple from the database asynchronously.
675
+
676
+ This method retrieves a checkpoint tuple from the Postgres database based on the
677
+ provided config (matching the thread ID in the config).
678
+
679
+ Args:
680
+ config: The config to use for retrieving the checkpoint.
681
+
682
+ Returns:
683
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
684
+ """
685
+ thread_id = config["configurable"]["thread_id"]
686
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
687
+ args = (thread_id, checkpoint_ns)
688
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s"
689
+
690
+ async with self._cursor() as cur:
691
+ await cur.execute(
692
+ self.SELECT_SQL + where,
693
+ args,
694
+ binary=True,
695
+ )
696
+
697
+ async for value in cur:
698
+ checkpoint: Checkpoint = {
699
+ **value["checkpoint"],
700
+ "channel_values": self._load_blobs(value["channel_values"]),
701
+ "pending_sends": [
702
+ self.serde.loads_typed((t.decode(), v))
703
+ for t, v in value["pending_sends"]
704
+ ]
705
+ if value["pending_sends"]
706
+ else [],
707
+ }
708
+ return CheckpointTuple(
709
+ config={
710
+ "configurable": {
711
+ "thread_id": thread_id,
712
+ "checkpoint_ns": checkpoint_ns,
713
+ "checkpoint_id": checkpoint["id"],
714
+ }
715
+ },
716
+ checkpoint=checkpoint,
717
+ metadata=value["metadata"],
718
+ pending_writes=await asyncio.to_thread(
719
+ self._load_writes, value["pending_writes"]
720
+ ),
721
+ )
722
+
723
+ async def aput(
724
+ self,
725
+ config: RunnableConfig,
726
+ checkpoint: Checkpoint,
727
+ metadata: CheckpointMetadata,
728
+ new_versions: ChannelVersions,
729
+ ) -> RunnableConfig:
730
+ """Save a checkpoint to the database asynchronously.
731
+
732
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
733
+ with the provided config. For AsyncShallowPostgresSaver, this method saves ONLY the most recent
734
+ checkpoint and overwrites a previous checkpoint, if it exists.
735
+
736
+ Args:
737
+ config: The config to associate with the checkpoint.
738
+ checkpoint: The checkpoint to save.
739
+ metadata: Additional metadata to save with the checkpoint.
740
+ new_versions: New channel versions as of this write.
741
+
742
+ Returns:
743
+ RunnableConfig: Updated configuration after storing the checkpoint.
744
+ """
745
+ configurable = config["configurable"].copy()
746
+ thread_id = configurable.pop("thread_id")
747
+ checkpoint_ns = configurable.pop("checkpoint_ns")
748
+
749
+ copy = checkpoint.copy()
750
+ next_config = {
751
+ "configurable": {
752
+ "thread_id": thread_id,
753
+ "checkpoint_ns": checkpoint_ns,
754
+ "checkpoint_id": checkpoint["id"],
755
+ }
756
+ }
757
+
758
+ async with self._cursor(pipeline=True) as cur:
759
+ await cur.execute(
760
+ """DELETE FROM checkpoint_writes
761
+ WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id NOT IN (%s, %s)""",
762
+ (
763
+ thread_id,
764
+ checkpoint_ns,
765
+ checkpoint["id"],
766
+ configurable.get("checkpoint_id", ""),
767
+ ),
768
+ )
769
+ await cur.executemany(
770
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
771
+ _dump_blobs(
772
+ self.serde,
773
+ thread_id,
774
+ checkpoint_ns,
775
+ copy.pop("channel_values"), # type: ignore[misc]
776
+ new_versions,
777
+ ),
778
+ )
779
+ await cur.execute(
780
+ self.UPSERT_CHECKPOINTS_SQL,
781
+ (
782
+ thread_id,
783
+ checkpoint_ns,
784
+ Jsonb(copy),
785
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
786
+ ),
787
+ )
788
+ return next_config
789
+
790
+ async def aput_writes(
791
+ self,
792
+ config: RunnableConfig,
793
+ writes: Sequence[tuple[str, Any]],
794
+ task_id: str,
795
+ task_path: str = "",
796
+ ) -> None:
797
+ """Store intermediate writes linked to a checkpoint asynchronously.
798
+
799
+ This method saves intermediate writes associated with a checkpoint to the database.
800
+
801
+ Args:
802
+ config: Configuration of the related checkpoint.
803
+ writes: List of writes to store, each as (channel, value) pair.
804
+ task_id: Identifier for the task creating the writes.
805
+ """
806
+ query = (
807
+ self.UPSERT_CHECKPOINT_WRITES_SQL
808
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
809
+ else self.INSERT_CHECKPOINT_WRITES_SQL
810
+ )
811
+ params = await asyncio.to_thread(
812
+ self._dump_writes,
813
+ config["configurable"]["thread_id"],
814
+ config["configurable"]["checkpoint_ns"],
815
+ config["configurable"]["checkpoint_id"],
816
+ task_id,
817
+ task_path,
818
+ writes,
819
+ )
820
+ async with self._cursor(pipeline=True) as cur:
821
+ await cur.executemany(query, params)
822
+
823
+ @asynccontextmanager
824
+ async def _cursor(
825
+ self, *, pipeline: bool = False
826
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
827
+ """Create a database cursor as a context manager.
828
+
829
+ Args:
830
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
831
+ Will be applied regardless of whether the AsyncShallowPostgresSaver instance was initialized with a pipeline.
832
+ If pipeline mode is not supported, will fall back to using transaction context manager.
833
+ """
834
+ async with _ainternal.get_connection(self.conn) as conn:
835
+ if self.pipe:
836
+ # a connection in pipeline mode can be used concurrently
837
+ # in multiple threads/coroutines, but only one cursor can be
838
+ # used at a time
839
+ try:
840
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
841
+ yield cur
842
+ finally:
843
+ if pipeline:
844
+ await self.pipe.sync()
845
+ elif pipeline:
846
+ # a connection not in pipeline mode can only be used by one
847
+ # thread/coroutine at a time, so we acquire a lock
848
+ if self.supports_pipeline:
849
+ async with (
850
+ self.lock,
851
+ conn.pipeline(),
852
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
853
+ ):
854
+ yield cur
855
+ else:
856
+ # Use connection's transaction context manager when pipeline mode not supported
857
+ async with (
858
+ self.lock,
859
+ conn.transaction(),
860
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
861
+ ):
862
+ yield cur
863
+ else:
864
+ async with (
865
+ self.lock,
866
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
867
+ ):
868
+ yield cur
869
+
870
+ def list(
871
+ self,
872
+ config: RunnableConfig | None,
873
+ *,
874
+ filter: dict[str, Any] | None = None,
875
+ before: RunnableConfig | None = None,
876
+ limit: int | None = None,
877
+ ) -> Iterator[CheckpointTuple]:
878
+ """List checkpoints from the database.
879
+
880
+ This method retrieves a list of checkpoint tuples from the Postgres database based
881
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
882
+ ONLY the most recent checkpoint.
883
+ """
884
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
885
+ while True:
886
+ try:
887
+ yield asyncio.run_coroutine_threadsafe(
888
+ anext(aiter_), # type: ignore[arg-type]
889
+ self.loop,
890
+ ).result()
891
+ except StopAsyncIteration:
892
+ break
893
+
894
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
895
+ """Get a checkpoint tuple from the database.
896
+
897
+ This method retrieves a checkpoint tuple from the Postgres database based on the
898
+ provided config (matching the thread ID in the config).
899
+
900
+ Args:
901
+ config: The config to use for retrieving the checkpoint.
902
+
903
+ Returns:
904
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
905
+ """
906
+ try:
907
+ # check if we are in the main thread, only bg threads can block
908
+ # we don't check in other methods to avoid the overhead
909
+ if asyncio.get_running_loop() is self.loop:
910
+ raise asyncio.InvalidStateError(
911
+ "Synchronous calls to AsyncShallowPostgresSaver are only allowed from a "
912
+ "different thread. From the main thread, use the async interface."
913
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
914
+ "graph.ainvoke(...)`."
915
+ )
916
+ except RuntimeError:
917
+ pass
918
+ return asyncio.run_coroutine_threadsafe(
919
+ self.aget_tuple(config), self.loop
920
+ ).result()
921
+
922
+ def put(
923
+ self,
924
+ config: RunnableConfig,
925
+ checkpoint: Checkpoint,
926
+ metadata: CheckpointMetadata,
927
+ new_versions: ChannelVersions,
928
+ ) -> RunnableConfig:
929
+ """Save a checkpoint to the database.
930
+
931
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
932
+ with the provided config. For AsyncShallowPostgresSaver, this method saves ONLY the most recent
933
+ checkpoint and overwrites a previous checkpoint, if it exists.
934
+
935
+ Args:
936
+ config: The config to associate with the checkpoint.
937
+ checkpoint: The checkpoint to save.
938
+ metadata: Additional metadata to save with the checkpoint.
939
+ new_versions: New channel versions as of this write.
940
+
941
+ Returns:
942
+ RunnableConfig: Updated configuration after storing the checkpoint.
943
+ """
944
+ return asyncio.run_coroutine_threadsafe(
945
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
946
+ ).result()
947
+
948
+ def put_writes(
949
+ self,
950
+ config: RunnableConfig,
951
+ writes: Sequence[tuple[str, Any]],
952
+ task_id: str,
953
+ task_path: str = "",
954
+ ) -> None:
955
+ """Store intermediate writes linked to a checkpoint.
956
+
957
+ This method saves intermediate writes associated with a checkpoint to the database.
958
+
959
+ Args:
960
+ config: Configuration of the related checkpoint.
961
+ writes: List of writes to store, each as (channel, value) pair.
962
+ task_id: Identifier for the task creating the writes.
963
+ task_path: Path of the task creating the writes.
964
+ """
965
+ return asyncio.run_coroutine_threadsafe(
966
+ self.aput_writes(config, writes, task_id, task_path), self.loop
967
+ ).result()