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
File without changes
@@ -0,0 +1,4 @@
1
+ from coco_graph.store.sqlite.aio import AsyncSqliteStore
2
+ from coco_graph.store.sqlite.base import SqliteStore
3
+
4
+ __all__ = ["AsyncSqliteStore", "SqliteStore"]
@@ -0,0 +1,632 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections import defaultdict
6
+ from collections.abc import AsyncIterator, Callable, Iterable, Sequence
7
+ from contextlib import asynccontextmanager
8
+ from types import TracebackType
9
+ from typing import Any, cast
10
+
11
+ import aiosqlite
12
+ import orjson
13
+ import sqlite_vec # type: ignore[import-untyped]
14
+ from coco_graph.store.base import (
15
+ GetOp,
16
+ ListNamespacesOp,
17
+ Op,
18
+ PutOp,
19
+ Result,
20
+ SearchOp,
21
+ TTLConfig,
22
+ )
23
+ from coco_graph.store.base.batch import AsyncBatchedBaseStore
24
+
25
+ from coco_graph.store.sqlite.base import (
26
+ _PLACEHOLDER,
27
+ NS_MATCH_FUNCTION,
28
+ BaseSqliteStore,
29
+ SqliteIndexConfig,
30
+ _decode_ns_text,
31
+ _ensure_index_config,
32
+ _group_ops,
33
+ _namespace_match,
34
+ _row_to_item,
35
+ _row_to_search_item,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
42
+ """Asynchronous SQLite-backed store with optional vector search.
43
+
44
+ This class provides an asynchronous interface for storing and retrieving data
45
+ using a SQLite database with support for vector search capabilities.
46
+
47
+ Examples:
48
+ Basic setup and usage:
49
+ ```python
50
+ from coco_graph.store.sqlite import AsyncSqliteStore
51
+
52
+ async with AsyncSqliteStore.from_conn_string(":memory:") as store:
53
+ await store.setup() # Run migrations
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_openai import OpenAIEmbeddings
63
+ from coco_graph.store.sqlite import AsyncSqliteStore
64
+
65
+ async with AsyncSqliteStore.from_conn_string(
66
+ ":memory:",
67
+ index={
68
+ "dims": 1536,
69
+ "embed": OpenAIEmbeddings(),
70
+ "fields": ["text"] # specify which fields to embed
71
+ }
72
+ ) as store:
73
+ await store.setup() # Run migrations once
74
+
75
+ # Store documents
76
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
77
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
78
+ await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
79
+
80
+ # Search by similarity
81
+ results = await store.asearch(("docs",), query="programming guides", limit=2)
82
+ ```
83
+
84
+ Warning:
85
+ Make sure to call `setup()` before first use to create necessary tables and indexes.
86
+
87
+ Note:
88
+ This class requires the aiosqlite package. Install with `pip install aiosqlite`.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ conn: aiosqlite.Connection,
94
+ *,
95
+ deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
96
+ | None = None,
97
+ index: SqliteIndexConfig | None = None,
98
+ ttl: TTLConfig | None = None,
99
+ ):
100
+ """Initialize the async SQLite store.
101
+
102
+ Args:
103
+ conn: The SQLite database connection.
104
+ deserializer: Optional custom deserializer function for values.
105
+ index: Optional vector search configuration.
106
+ ttl: Optional time-to-live configuration.
107
+ """
108
+ super().__init__()
109
+ self._deserializer = deserializer
110
+ self.conn = conn
111
+ self.lock = asyncio.Lock()
112
+ self.loop = asyncio.get_running_loop()
113
+ self.is_setup = False
114
+ self.index_config = index
115
+ if self.index_config:
116
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
117
+ else:
118
+ self.embeddings = None
119
+ self.ttl_config = ttl
120
+ self._ttl_sweeper_task: asyncio.Task[None] | None = None
121
+ self._ttl_stop_event = asyncio.Event()
122
+
123
+ @classmethod
124
+ @asynccontextmanager
125
+ async def from_conn_string(
126
+ cls,
127
+ conn_string: str,
128
+ *,
129
+ index: SqliteIndexConfig | None = None,
130
+ ttl: TTLConfig | None = None,
131
+ ) -> AsyncIterator[AsyncSqliteStore]:
132
+ """Create a new AsyncSqliteStore instance from a connection string.
133
+
134
+ Args:
135
+ conn_string: The SQLite connection string.
136
+ index: Optional vector search configuration.
137
+ ttl: Optional time-to-live configuration.
138
+
139
+ Returns:
140
+ An AsyncSqliteStore instance wrapped in an async context manager.
141
+ """
142
+ async with aiosqlite.connect(conn_string, isolation_level=None) as conn:
143
+ yield cls(conn, index=index, ttl=ttl)
144
+
145
+ async def setup(self) -> None:
146
+ """Set up the store database.
147
+
148
+ This method creates the necessary tables in the SQLite database if they don't
149
+ already exist and runs database migrations. It should be called before first use.
150
+ """
151
+ async with self.lock:
152
+ if self.is_setup:
153
+ return
154
+
155
+ # list_namespaces needs segment-aware matching, which SQLite cannot
156
+ # express in LIKE or GLOB. Registered here rather than in __init__
157
+ # because aiosqlite's create_function is a coroutine.
158
+ await self.conn.create_function(
159
+ NS_MATCH_FUNCTION, 2, _namespace_match, deterministic=True
160
+ )
161
+
162
+ # Create migrations table if it doesn't exist
163
+ await self.conn.execute(
164
+ """
165
+ CREATE TABLE IF NOT EXISTS store_migrations (
166
+ v INTEGER PRIMARY KEY
167
+ )
168
+ """
169
+ )
170
+
171
+ # Check current migration version
172
+ async with self.conn.execute(
173
+ "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
174
+ ) as cur:
175
+ row = await cur.fetchone()
176
+ if row is None:
177
+ version = -1
178
+ else:
179
+ version = row[0]
180
+
181
+ # Apply migrations
182
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
183
+ await self.conn.executescript(sql)
184
+ await self.conn.execute(
185
+ "INSERT INTO store_migrations (v) VALUES (?)", (v,)
186
+ )
187
+
188
+ # Apply vector migrations if index config is provided
189
+ if self.index_config:
190
+ # Create vector migrations table if it doesn't exist
191
+ await self.conn.enable_load_extension(True)
192
+ await self.conn.load_extension(sqlite_vec.loadable_path())
193
+ await self.conn.enable_load_extension(False)
194
+ await self.conn.execute(
195
+ """
196
+ CREATE TABLE IF NOT EXISTS vector_migrations (
197
+ v INTEGER PRIMARY KEY
198
+ )
199
+ """
200
+ )
201
+
202
+ # Check current vector migration version
203
+ async with self.conn.execute(
204
+ "SELECT v FROM vector_migrations ORDER BY v DESC LIMIT 1"
205
+ ) as cur:
206
+ row = await cur.fetchone()
207
+ if row is None:
208
+ version = -1
209
+ else:
210
+ version = row[0]
211
+
212
+ # Apply vector migrations
213
+ for v, sql in enumerate(
214
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
215
+ ):
216
+ await self.conn.executescript(sql)
217
+ await self.conn.execute(
218
+ "INSERT INTO vector_migrations (v) VALUES (?)", (v,)
219
+ )
220
+
221
+ self.is_setup = True
222
+
223
+ @asynccontextmanager
224
+ async def _cursor(
225
+ self, *, transaction: bool = True
226
+ ) -> AsyncIterator[aiosqlite.Cursor]:
227
+ """Get a cursor for the SQLite database.
228
+
229
+ Args:
230
+ transaction: Whether to use a transaction for database operations.
231
+
232
+ Yields:
233
+ An SQLite cursor object.
234
+ """
235
+ if not self.is_setup:
236
+ await self.setup()
237
+ async with self.lock:
238
+ if transaction:
239
+ await self.conn.execute("BEGIN")
240
+
241
+ async with self.conn.cursor() as cur:
242
+ try:
243
+ yield cur
244
+ finally:
245
+ if transaction:
246
+ await self.conn.execute("COMMIT")
247
+
248
+ async def sweep_ttl(self) -> int:
249
+ """Delete expired store items based on TTL.
250
+
251
+ Returns:
252
+ int: The number of deleted items.
253
+ """
254
+ async with self._cursor() as cur:
255
+ await cur.execute(
256
+ """
257
+ DELETE FROM store
258
+ WHERE expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP
259
+ """
260
+ )
261
+ deleted_count = cur.rowcount
262
+ return deleted_count
263
+
264
+ async def start_ttl_sweeper(
265
+ self, sweep_interval_minutes: int | None = None
266
+ ) -> asyncio.Task[None]:
267
+ """Periodically delete expired store items based on TTL.
268
+
269
+ Returns:
270
+ Task that can be awaited or cancelled.
271
+ """
272
+ if not self.ttl_config:
273
+ return asyncio.create_task(asyncio.sleep(0))
274
+
275
+ if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
276
+ return self._ttl_sweeper_task
277
+
278
+ self._ttl_stop_event.clear()
279
+
280
+ interval = float(
281
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
282
+ )
283
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
284
+
285
+ async def _sweep_loop() -> None:
286
+ while not self._ttl_stop_event.is_set():
287
+ try:
288
+ try:
289
+ await asyncio.wait_for(
290
+ self._ttl_stop_event.wait(),
291
+ timeout=interval * 60,
292
+ )
293
+ break
294
+ except asyncio.TimeoutError:
295
+ pass
296
+
297
+ expired_items = await self.sweep_ttl()
298
+ if expired_items > 0:
299
+ logger.info(f"Store swept {expired_items} expired items")
300
+ except asyncio.CancelledError:
301
+ break
302
+ except Exception as exc:
303
+ logger.exception("Store TTL sweep iteration failed", exc_info=exc)
304
+
305
+ task = asyncio.create_task(_sweep_loop())
306
+ task.set_name("ttl_sweeper")
307
+ self._ttl_sweeper_task = task
308
+ return task
309
+
310
+ async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
311
+ """Stop the TTL sweeper task if it's running.
312
+
313
+ Args:
314
+ timeout: Maximum time to wait for the task to stop, in seconds.
315
+ If `None`, wait indefinitely.
316
+
317
+ Returns:
318
+ bool: True if the task was successfully stopped or wasn't running,
319
+ False if the timeout was reached before the task stopped.
320
+ """
321
+ if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
322
+ return True
323
+
324
+ logger.info("Stopping TTL sweeper task")
325
+ self._ttl_stop_event.set()
326
+
327
+ if timeout is not None:
328
+ try:
329
+ await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
330
+ success = True
331
+ except asyncio.TimeoutError:
332
+ success = False
333
+ else:
334
+ await self._ttl_sweeper_task
335
+ success = True
336
+
337
+ if success:
338
+ self._ttl_sweeper_task = None
339
+ logger.info("TTL sweeper task stopped")
340
+ else:
341
+ logger.warning("Timed out waiting for TTL sweeper task to stop")
342
+
343
+ return success
344
+
345
+ async def __aenter__(self) -> AsyncSqliteStore:
346
+ return self
347
+
348
+ async def __aexit__(
349
+ self,
350
+ exc_type: type[BaseException] | None,
351
+ exc_val: BaseException | None,
352
+ exc_tb: TracebackType | None,
353
+ ) -> None:
354
+ # Ensure the TTL sweeper task is stopped when exiting the context
355
+ if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
356
+ # Set the event to signal the task to stop
357
+ self._ttl_stop_event.set()
358
+ # We don't wait for the task to complete here to avoid blocking
359
+ # The task will clean up itself gracefully
360
+
361
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
362
+ """Execute a batch of operations asynchronously.
363
+
364
+ Args:
365
+ ops: Iterable of operations to execute.
366
+
367
+ Returns:
368
+ List of operation results.
369
+ """
370
+ grouped_ops, num_ops = _group_ops(ops)
371
+ results: list[Result] = [None] * num_ops
372
+
373
+ async with self._cursor(transaction=True) as cur:
374
+ if GetOp in grouped_ops:
375
+ await self._batch_get_ops(
376
+ cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results, cur
377
+ )
378
+
379
+ if SearchOp in grouped_ops:
380
+ await self._batch_search_ops(
381
+ cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
382
+ results,
383
+ cur,
384
+ )
385
+
386
+ if ListNamespacesOp in grouped_ops:
387
+ await self._batch_list_namespaces_ops(
388
+ cast(
389
+ Sequence[tuple[int, ListNamespacesOp]],
390
+ grouped_ops[ListNamespacesOp],
391
+ ),
392
+ results,
393
+ cur,
394
+ )
395
+
396
+ if PutOp in grouped_ops:
397
+ await self._batch_put_ops(
398
+ cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]), cur
399
+ )
400
+
401
+ return results
402
+
403
+ async def _batch_get_ops(
404
+ self,
405
+ get_ops: Sequence[tuple[int, GetOp]],
406
+ results: list[Result],
407
+ cur: aiosqlite.Cursor,
408
+ ) -> None:
409
+ """Process batch GET operations.
410
+
411
+ Args:
412
+ get_ops: Sequence of GET operations.
413
+ results: List to store results in.
414
+ cur: Database cursor.
415
+ """
416
+ # Group all queries by namespace to execute all operations for each namespace together
417
+ namespace_queries = defaultdict(list)
418
+ for prepared_query in self._get_batch_GET_ops_queries(get_ops):
419
+ namespace_queries[prepared_query.namespace].append(prepared_query)
420
+
421
+ # Process each namespace's operations
422
+ for namespace, queries in namespace_queries.items():
423
+ # Execute TTL refresh queries first
424
+ for query in queries:
425
+ if query.kind == "refresh":
426
+ try:
427
+ await cur.execute(query.query, query.params)
428
+ except Exception as e:
429
+ raise ValueError(
430
+ f"Error executing TTL refresh: \n{query.query}\n{query.params}\n{e}"
431
+ ) from e
432
+
433
+ # Then execute GET queries and process results
434
+ for query in queries:
435
+ if query.kind == "get":
436
+ try:
437
+ await cur.execute(query.query, query.params)
438
+ except Exception as e:
439
+ raise ValueError(
440
+ f"Error executing GET query: \n{query.query}\n{query.params}\n{e}"
441
+ ) from e
442
+
443
+ rows = await cur.fetchall()
444
+ key_to_row = {
445
+ row[0]: {
446
+ "key": row[0],
447
+ "value": row[1],
448
+ "created_at": row[2],
449
+ "updated_at": row[3],
450
+ "expires_at": row[4] if len(row) > 4 else None,
451
+ "ttl_minutes": row[5] if len(row) > 5 else None,
452
+ }
453
+ for row in rows
454
+ }
455
+
456
+ # Process results for this query
457
+ for idx, key in query.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: aiosqlite.Cursor,
470
+ ) -> None:
471
+ """Process batch PUT operations.
472
+
473
+ Args:
474
+ put_ops: Sequence of PUT operations.
475
+ cur: Database cursor.
476
+ """
477
+ queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
478
+ if embedding_request:
479
+ if self.embeddings is None:
480
+ # Should not get here since the embedding config is required
481
+ # to return an embedding_request above
482
+ raise ValueError(
483
+ "Embedding configuration is required for vector operations "
484
+ f"(for semantic search). "
485
+ f"Please provide an Embeddings when initializing the {self.__class__.__name__}."
486
+ )
487
+
488
+ query, txt_params = embedding_request
489
+ # Update the params to replace the raw text with the vectors
490
+ vectors = await self.embeddings.aembed_documents(
491
+ [param[-1] for param in txt_params]
492
+ )
493
+
494
+ # Convert vectors to SQLite-friendly format
495
+ vector_params = []
496
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
497
+ vector_params.extend(
498
+ [ns, k, pathname, sqlite_vec.serialize_float32(vector)]
499
+ )
500
+
501
+ queries.append((query, vector_params))
502
+
503
+ for query, params in queries:
504
+ await cur.execute(query, params)
505
+
506
+ async def _batch_search_ops(
507
+ self,
508
+ search_ops: Sequence[tuple[int, SearchOp]],
509
+ results: list[Result],
510
+ cur: aiosqlite.Cursor,
511
+ ) -> None:
512
+ """Process batch SEARCH operations.
513
+
514
+ Args:
515
+ search_ops: Sequence of SEARCH operations.
516
+ results: List to store results in.
517
+ cur: Database cursor.
518
+ """
519
+ prepared_queries, embedding_requests = self._prepare_batch_search_queries(
520
+ search_ops
521
+ )
522
+
523
+ # Setup dot_product function if it doesn't exist
524
+ if embedding_requests and self.embeddings:
525
+ vectors = await self.embeddings.aembed_documents(
526
+ [query for _, query in embedding_requests]
527
+ )
528
+
529
+ for (embed_req_idx, _), embedding in zip(
530
+ embedding_requests, vectors, strict=False
531
+ ):
532
+ # Find the corresponding query in prepared_queries
533
+ # The embed_req_idx is the original index in search_ops, which should map to prepared_queries
534
+ if embed_req_idx < len(prepared_queries):
535
+ _params_list: list = prepared_queries[embed_req_idx][1]
536
+ for i, param in enumerate(_params_list):
537
+ if param is _PLACEHOLDER:
538
+ _params_list[i] = sqlite_vec.serialize_float32(embedding)
539
+ else:
540
+ logger.warning(
541
+ f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
542
+ )
543
+
544
+ for (original_op_idx, _), (query, params, needs_refresh) in zip(
545
+ search_ops, prepared_queries, strict=False
546
+ ):
547
+ await cur.execute(query, params)
548
+ rows = await cur.fetchall()
549
+
550
+ if needs_refresh and rows and self.ttl_config:
551
+ keys_to_refresh = []
552
+ for row_data in rows:
553
+ # Assuming row_data[0] is prefix (text), row_data[1] is key (text)
554
+ # These are raw text values directly from the DB.
555
+ keys_to_refresh.append((row_data[0], row_data[1]))
556
+
557
+ if keys_to_refresh:
558
+ updates_by_prefix = defaultdict(list)
559
+ for prefix_text, key_text in keys_to_refresh:
560
+ updates_by_prefix[prefix_text].append(key_text)
561
+
562
+ for prefix_text, key_list in updates_by_prefix.items():
563
+ placeholders = ",".join(["?"] * len(key_list))
564
+ update_query = f"""
565
+ UPDATE store
566
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
567
+ WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
568
+ """
569
+ update_params = (prefix_text, *key_list)
570
+ try:
571
+ await cur.execute(update_query, update_params)
572
+ except Exception as e:
573
+ logger.error(
574
+ f"Error during TTL refresh update for search: {e}"
575
+ )
576
+
577
+ # Process rows into items
578
+ if "score" in query: # Vector search query
579
+ items = [
580
+ _row_to_search_item(
581
+ _decode_ns_text(row[0]), # prefix
582
+ {
583
+ "key": row[1], # key
584
+ "value": row[2], # value
585
+ "created_at": row[3],
586
+ "updated_at": row[4],
587
+ "expires_at": row[5] if len(row) > 5 else None,
588
+ "ttl_minutes": row[6] if len(row) > 6 else None,
589
+ "score": row[7] if len(row) > 7 else None,
590
+ },
591
+ loader=self._deserializer,
592
+ )
593
+ for row in rows
594
+ ]
595
+ else: # Regular search query
596
+ items = [
597
+ _row_to_search_item(
598
+ _decode_ns_text(row[0]), # prefix
599
+ {
600
+ "key": row[1], # key
601
+ "value": row[2], # value
602
+ "created_at": row[3],
603
+ "updated_at": row[4],
604
+ "expires_at": row[5] if len(row) > 5 else None,
605
+ "ttl_minutes": row[6] if len(row) > 6 else None,
606
+ },
607
+ loader=self._deserializer,
608
+ )
609
+ for row in rows
610
+ ]
611
+
612
+ results[original_op_idx] = items
613
+
614
+ async def _batch_list_namespaces_ops(
615
+ self,
616
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
617
+ results: list[Result],
618
+ cur: aiosqlite.Cursor,
619
+ ) -> None:
620
+ """Process batch LIST NAMESPACES operations.
621
+
622
+ Args:
623
+ list_ops: Sequence of LIST NAMESPACES operations.
624
+ results: List to store results in.
625
+ cur: Database cursor.
626
+ """
627
+ queries = self._get_batch_list_namespaces_queries(list_ops)
628
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
629
+ await cur.execute(query, params)
630
+
631
+ rows = await cur.fetchall()
632
+ results[idx] = [_decode_ns_text(row[0]) for row in rows]