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,592 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections.abc import AsyncIterator, Callable, Iterable, Sequence
6
+ from contextlib import asynccontextmanager
7
+ from types import TracebackType
8
+ from typing import Any, cast
9
+
10
+ import orjson
11
+ from coco_graph.store.base import (
12
+ GetOp,
13
+ ListNamespacesOp,
14
+ Op,
15
+ PutOp,
16
+ Result,
17
+ SearchOp,
18
+ )
19
+ from coco_graph.store.base.batch import AsyncBatchedBaseStore
20
+ from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
21
+ from psycopg.rows import DictRow, dict_row
22
+ from psycopg_pool import AsyncConnectionPool
23
+
24
+ from coco_graph.checkpoint.postgres import _ainternal
25
+ from coco_graph.store.postgres.base import (
26
+ PLACEHOLDER,
27
+ BasePostgresStore,
28
+ PoolConfig,
29
+ PostgresIndexConfig,
30
+ Row,
31
+ TTLConfig,
32
+ _decode_ns_bytes,
33
+ _ensure_index_config,
34
+ _group_ops,
35
+ _row_to_item,
36
+ _row_to_search_item,
37
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
43
+ """Asynchronous Postgres-backed store with optional vector search using pgvector.
44
+
45
+ !!! example "Examples"
46
+ Basic setup and usage:
47
+ ```python
48
+ from coco_graph.store.postgres import AsyncPostgresStore
49
+
50
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
51
+
52
+ async with AsyncPostgresStore.from_conn_string(conn_string) as store:
53
+ await store.setup() # Run migrations. Done once
54
+
55
+ # Store and retrieve data
56
+ await store.aput(("users", "123"), "prefs", {"theme": "dark"})
57
+ item = await store.aget(("users", "123"), "prefs")
58
+ ```
59
+
60
+ Vector search using Coco embeddings:
61
+ ```python
62
+ from coco.embeddings import init_embeddings
63
+ from coco_graph.store.postgres import AsyncPostgresStore
64
+
65
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
66
+
67
+ async with AsyncPostgresStore.from_conn_string(
68
+ conn_string,
69
+ index={
70
+ "dims": 1536,
71
+ "embed": init_embeddings("openai:text-embedding-3-small"),
72
+ "fields": ["text"] # specify which fields to embed. Default is the whole serialized value
73
+ }
74
+ ) as store:
75
+ await store.setup() # Run migrations. Done once
76
+
77
+ # Store documents
78
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
79
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
80
+ await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
81
+
82
+ # Search by similarity
83
+ results = await store.asearch(("docs",), query="programming guides", limit=2)
84
+ ```
85
+
86
+ Using connection pooling for better performance:
87
+ ```python
88
+ from coco_graph.store.postgres import AsyncPostgresStore, PoolConfig
89
+
90
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
91
+
92
+ async with AsyncPostgresStore.from_conn_string(
93
+ conn_string,
94
+ pool_config=PoolConfig(
95
+ min_size=5,
96
+ max_size=20
97
+ )
98
+ ) as store:
99
+ await store.setup() # Run migrations. Done once
100
+ # Use store with connection pooling...
101
+ ```
102
+
103
+ Warning:
104
+ Make sure to:
105
+ 1. Call `setup()` before first use to create necessary tables and indexes
106
+ 2. Have the pgvector extension available to use vector search
107
+ 3. Use Python 3.10+ for async functionality
108
+
109
+ Note:
110
+ Semantic search is disabled by default. You can enable it by providing an `index` configuration
111
+ when creating the store. Without this configuration, all `index` arguments passed to
112
+ `put` or `aput` will have no effect.
113
+
114
+ Note:
115
+ If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
116
+ the background task that removes expired items. Call `stop_ttl_sweeper()` to properly
117
+ clean up resources when you're done with the store.
118
+ """
119
+
120
+ __slots__ = (
121
+ "_deserializer",
122
+ "pipe",
123
+ "lock",
124
+ "supports_pipeline",
125
+ "index_config",
126
+ "embeddings",
127
+ "ttl_config",
128
+ "_ttl_sweeper_task",
129
+ "_ttl_stop_event",
130
+ )
131
+ supports_ttl: bool = True
132
+
133
+ def __init__(
134
+ self,
135
+ conn: _ainternal.Conn,
136
+ *,
137
+ pipe: AsyncPipeline | None = None,
138
+ deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
139
+ index: PostgresIndexConfig | None = None,
140
+ ttl: TTLConfig | None = None,
141
+ ) -> None:
142
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
143
+ raise ValueError(
144
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
145
+ )
146
+ super().__init__()
147
+ self._deserializer = deserializer
148
+ self.conn = conn
149
+ self.pipe = pipe
150
+ self.lock = asyncio.Lock()
151
+ self.loop = asyncio.get_running_loop()
152
+ self.supports_pipeline = Capabilities().has_pipeline()
153
+ self.index_config = index
154
+ if self.index_config:
155
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
156
+ else:
157
+ self.embeddings = None
158
+
159
+ self.ttl_config = ttl
160
+ self._ttl_sweeper_task: asyncio.Task[None] | None = None
161
+ self._ttl_stop_event = asyncio.Event()
162
+
163
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
164
+ grouped_ops, num_ops = _group_ops(ops)
165
+ results: list[Result] = [None] * num_ops
166
+
167
+ if self.pipe:
168
+ async with self.pipe:
169
+ await self._execute_batch(grouped_ops, results)
170
+ else:
171
+ await self._execute_batch(grouped_ops, results)
172
+
173
+ return results
174
+
175
+ @classmethod
176
+ @asynccontextmanager
177
+ async def from_conn_string(
178
+ cls,
179
+ conn_string: str,
180
+ *,
181
+ pipeline: bool = False,
182
+ pool_config: PoolConfig | None = None,
183
+ index: PostgresIndexConfig | None = None,
184
+ ttl: TTLConfig | None = None,
185
+ ) -> AsyncIterator[AsyncPostgresStore]:
186
+ """Create a new AsyncPostgresStore instance from a connection string.
187
+
188
+ Args:
189
+ conn_string: The Postgres connection info string.
190
+ pipeline: Whether to use AsyncPipeline (only for single connections)
191
+ pool_config: Configuration for the connection pool.
192
+ If provided, will create a connection pool and use it instead of a single connection.
193
+ This overrides the `pipeline` argument.
194
+ index: The embedding config.
195
+
196
+ Returns:
197
+ AsyncPostgresStore: A new AsyncPostgresStore instance.
198
+ """
199
+ if pool_config is not None:
200
+ pc = pool_config.copy()
201
+ async with cast(
202
+ AsyncConnectionPool[AsyncConnection[DictRow]],
203
+ AsyncConnectionPool(
204
+ conn_string,
205
+ min_size=pc.pop("min_size", 1),
206
+ max_size=pc.pop("max_size", None),
207
+ kwargs={
208
+ "autocommit": True,
209
+ "prepare_threshold": 0,
210
+ "row_factory": dict_row,
211
+ **(pc.pop("kwargs", None) or {}),
212
+ },
213
+ **cast(dict, pc),
214
+ ),
215
+ ) as pool:
216
+ yield cls(conn=pool, index=index, ttl=ttl)
217
+ else:
218
+ async with await AsyncConnection.connect(
219
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
220
+ ) as conn:
221
+ if pipeline:
222
+ async with conn.pipeline() as pipe:
223
+ yield cls(conn=conn, pipe=pipe, index=index, ttl=ttl)
224
+ else:
225
+ yield cls(conn=conn, index=index, ttl=ttl)
226
+
227
+ async def setup(self) -> None:
228
+ """Set up the store database asynchronously.
229
+
230
+ This method creates the necessary tables in the Postgres database if they don't
231
+ already exist and runs database migrations. It MUST be called directly by the user
232
+ the first time the store is used.
233
+ """
234
+
235
+ async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
236
+ await cur.execute(
237
+ f"""
238
+ CREATE TABLE IF NOT EXISTS {table} (
239
+ v INTEGER PRIMARY KEY
240
+ )
241
+ """
242
+ )
243
+ await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
244
+ row = cast(dict, await cur.fetchone())
245
+ if row is None:
246
+ version = -1
247
+ else:
248
+ version = row["v"]
249
+ return version
250
+
251
+ async with self._cursor() as cur:
252
+ version = await _get_version(cur, table="store_migrations")
253
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
254
+ await cur.execute(sql)
255
+ await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
256
+
257
+ if self.index_config:
258
+ version = await _get_version(cur, table="vector_migrations")
259
+ for v, migration in enumerate(
260
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
261
+ ):
262
+ sql = migration.sql
263
+ if migration.params:
264
+ params = {
265
+ k: v(self) if v is not None and callable(v) else v
266
+ for k, v in migration.params.items()
267
+ }
268
+ if "dims" in params:
269
+ try:
270
+ params["dims"] = int(params["dims"])
271
+ except Exception as e:
272
+ raise ValueError(
273
+ f"Invalid dims for vector index: {params['dims']}"
274
+ ) from e
275
+ if "vector_type" in params:
276
+ vt = str(params["vector_type"])
277
+ if vt not in ("vector", "halfvec"):
278
+ raise ValueError(
279
+ f"Invalid vector_type for pgvector: {vt}"
280
+ )
281
+ params["vector_type"] = vt
282
+ if "index_type" in params:
283
+ it = str(params["index_type"])
284
+ if it not in ("hnsw", "ivfflat"):
285
+ raise ValueError(
286
+ f"Invalid index_type for pgvector: {it}"
287
+ )
288
+ params["index_type"] = it
289
+ sql = sql % params
290
+ await cur.execute(sql)
291
+ await cur.execute(
292
+ "INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
293
+ )
294
+
295
+ async def sweep_ttl(self) -> int:
296
+ """Delete expired store items based on TTL.
297
+
298
+ Returns:
299
+ int: The number of deleted items.
300
+ """
301
+ async with self._cursor() as cur:
302
+ await cur.execute(
303
+ """
304
+ DELETE FROM store
305
+ WHERE expires_at IS NOT NULL AND expires_at < NOW()
306
+ """
307
+ )
308
+ deleted_count = cur.rowcount
309
+ return deleted_count
310
+
311
+ async def start_ttl_sweeper(
312
+ self, sweep_interval_minutes: int | None = None
313
+ ) -> asyncio.Task[None]:
314
+ """Periodically delete expired store items based on TTL.
315
+
316
+ Returns:
317
+ Task that can be awaited or cancelled.
318
+ """
319
+ if not self.ttl_config:
320
+ return asyncio.create_task(asyncio.sleep(0))
321
+
322
+ if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
323
+ return self._ttl_sweeper_task
324
+
325
+ self._ttl_stop_event.clear()
326
+
327
+ interval = float(
328
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
329
+ )
330
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
331
+
332
+ async def _sweep_loop() -> None:
333
+ while not self._ttl_stop_event.is_set():
334
+ try:
335
+ try:
336
+ await asyncio.wait_for(
337
+ self._ttl_stop_event.wait(),
338
+ timeout=interval * 60,
339
+ )
340
+ break
341
+ except asyncio.TimeoutError:
342
+ pass
343
+
344
+ expired_items = await self.sweep_ttl()
345
+ if expired_items > 0:
346
+ logger.info(f"Store swept {expired_items} expired items")
347
+ except asyncio.CancelledError:
348
+ break
349
+ except Exception as exc:
350
+ logger.exception("Store TTL sweep iteration failed", exc_info=exc)
351
+
352
+ task = asyncio.create_task(_sweep_loop())
353
+ task.set_name("ttl_sweeper")
354
+ self._ttl_sweeper_task = task
355
+ return task
356
+
357
+ async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
358
+ """Stop the TTL sweeper task if it's running.
359
+
360
+ Args:
361
+ timeout: Maximum time to wait for the task to stop, in seconds.
362
+ If `None`, wait indefinitely.
363
+
364
+ Returns:
365
+ bool: True if the task was successfully stopped or wasn't running,
366
+ False if the timeout was reached before the task stopped.
367
+ """
368
+ if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
369
+ return True
370
+
371
+ logger.info("Stopping TTL sweeper task")
372
+ self._ttl_stop_event.set()
373
+
374
+ if timeout is not None:
375
+ try:
376
+ await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
377
+ success = True
378
+ except asyncio.TimeoutError:
379
+ success = False
380
+ else:
381
+ await self._ttl_sweeper_task
382
+ success = True
383
+
384
+ if success:
385
+ self._ttl_sweeper_task = None
386
+ logger.info("TTL sweeper task stopped")
387
+ else:
388
+ logger.warning("Timed out waiting for TTL sweeper task to stop")
389
+
390
+ return success
391
+
392
+ async def __aenter__(self) -> AsyncPostgresStore:
393
+ return self
394
+
395
+ async def __aexit__(
396
+ self,
397
+ exc_type: type[BaseException] | None,
398
+ exc_val: BaseException | None,
399
+ exc_tb: TracebackType | None,
400
+ ) -> None:
401
+ # Ensure the TTL sweeper task is stopped when exiting the context
402
+ if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
403
+ # Set the event to signal the task to stop
404
+ self._ttl_stop_event.set()
405
+ # We don't wait for the task to complete here to avoid blocking
406
+ # The task will clean up itself gracefully
407
+
408
+ async def _execute_batch(
409
+ self,
410
+ grouped_ops: dict,
411
+ results: list[Result],
412
+ conn: AsyncConnection[DictRow] | None = None,
413
+ ) -> None:
414
+ # Keep `conn` for compatibility with subclasses overriding this private hook.
415
+ # All database I/O goes through `_cursor()`, which owns connection acquisition.
416
+ async with self._cursor(pipeline=True) as cur:
417
+ if GetOp in grouped_ops:
418
+ await self._batch_get_ops(
419
+ cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
420
+ results,
421
+ cur,
422
+ )
423
+
424
+ if SearchOp in grouped_ops:
425
+ await self._batch_search_ops(
426
+ cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
427
+ results,
428
+ cur,
429
+ )
430
+
431
+ if ListNamespacesOp in grouped_ops:
432
+ await self._batch_list_namespaces_ops(
433
+ cast(
434
+ Sequence[tuple[int, ListNamespacesOp]],
435
+ grouped_ops[ListNamespacesOp],
436
+ ),
437
+ results,
438
+ cur,
439
+ )
440
+
441
+ if PutOp in grouped_ops:
442
+ await self._batch_put_ops(
443
+ cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]),
444
+ cur,
445
+ )
446
+
447
+ async def _batch_get_ops(
448
+ self,
449
+ get_ops: Sequence[tuple[int, GetOp]],
450
+ results: list[Result],
451
+ cur: AsyncCursor[DictRow],
452
+ ) -> None:
453
+ for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
454
+ await cur.execute(query, params)
455
+ rows = cast(list[Row], await cur.fetchall())
456
+ key_to_row = {row["key"]: row for row in rows}
457
+ for idx, key in items:
458
+ row = key_to_row.get(key)
459
+ if row:
460
+ results[idx] = _row_to_item(
461
+ namespace, row, loader=self._deserializer
462
+ )
463
+ else:
464
+ results[idx] = None
465
+
466
+ async def _batch_put_ops(
467
+ self,
468
+ put_ops: Sequence[tuple[int, PutOp]],
469
+ cur: AsyncCursor[DictRow],
470
+ ) -> None:
471
+ queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
472
+ if embedding_request:
473
+ if self.embeddings is None:
474
+ # Should not get here since the embedding config is required
475
+ # to return an embedding_request above
476
+ raise ValueError(
477
+ "Embedding configuration is required for vector operations "
478
+ f"(for semantic search). "
479
+ f"Please provide an EmbeddingConfig when initializing the {self.__class__.__name__}."
480
+ )
481
+ query, txt_params = embedding_request
482
+ vectors = await self.embeddings.aembed_documents(
483
+ [param[-1] for param in txt_params]
484
+ )
485
+ queries.append(
486
+ (
487
+ query,
488
+ [
489
+ p
490
+ for (ns, k, pathname, _), vector in zip(
491
+ txt_params, vectors, strict=False
492
+ )
493
+ for p in (ns, k, pathname, vector)
494
+ ],
495
+ )
496
+ )
497
+
498
+ for query, params in queries:
499
+ await cur.execute(query, params)
500
+
501
+ async def _batch_search_ops(
502
+ self,
503
+ search_ops: Sequence[tuple[int, SearchOp]],
504
+ results: list[Result],
505
+ cur: AsyncCursor[DictRow],
506
+ ) -> None:
507
+ queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
508
+
509
+ if embedding_requests and self.embeddings:
510
+ vectors = await self.embeddings.aembed_documents(
511
+ [query for _, query in embedding_requests]
512
+ )
513
+ for (idx, _), vector in zip(embedding_requests, vectors, strict=False):
514
+ _paramslist = queries[idx][1]
515
+ for i in range(len(_paramslist)):
516
+ if _paramslist[i] is PLACEHOLDER:
517
+ _paramslist[i] = vector
518
+
519
+ for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
520
+ await cur.execute(query, params)
521
+ rows = cast(list[Row], await cur.fetchall())
522
+ items = [
523
+ _row_to_search_item(
524
+ _decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
525
+ )
526
+ for row in rows
527
+ ]
528
+ results[idx] = items
529
+
530
+ async def _batch_list_namespaces_ops(
531
+ self,
532
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
533
+ results: list[Result],
534
+ cur: AsyncCursor[DictRow],
535
+ ) -> None:
536
+ queries = self._get_batch_list_namespaces_queries(list_ops)
537
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
538
+ await cur.execute(query, params)
539
+ rows = cast(list[dict], await cur.fetchall())
540
+ namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
541
+ results[idx] = namespaces
542
+
543
+ @asynccontextmanager
544
+ async def _cursor(
545
+ self, *, pipeline: bool = False
546
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
547
+ """Create a database cursor as a context manager.
548
+
549
+ Args:
550
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
551
+ Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
552
+ If pipeline mode is not supported, will fall back to using transaction context manager.
553
+ """
554
+ is_pooled_conn = isinstance(self.conn, AsyncConnectionPool)
555
+ # With AsyncConnectionPool, each _cursor() call checks out its own connection.
556
+ # The pool does not hand out the same connection concurrently, so a shared lock
557
+ # across calls is unnecessary here.
558
+ lock = asyncio.Lock() if is_pooled_conn else self.lock
559
+ async with _ainternal.get_connection(self.conn) as conn:
560
+ if self.pipe:
561
+ # a connection in pipeline mode can be used concurrently
562
+ # in multiple threads/coroutines, but only one cursor can be
563
+ # used at a time
564
+ try:
565
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
566
+ yield cur
567
+ finally:
568
+ if pipeline:
569
+ await self.pipe.sync()
570
+ elif pipeline:
571
+ # a connection not in pipeline mode can only be used by one
572
+ # thread/coroutine at a time, so we acquire a lock
573
+ if self.supports_pipeline:
574
+ async with (
575
+ lock,
576
+ conn.pipeline(),
577
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
578
+ ):
579
+ yield cur
580
+ else:
581
+ async with (
582
+ lock,
583
+ conn.transaction(),
584
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
585
+ ):
586
+ yield cur
587
+ else:
588
+ async with (
589
+ lock,
590
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
591
+ ):
592
+ yield cur