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
+ """In-memory dictionary-backed store with optional vector search.
2
+
3
+ !!! example "Examples"
4
+ Basic key-value storage:
5
+ ```python
6
+ from coco_graph.store.memory import InMemoryStore
7
+
8
+ store = InMemoryStore()
9
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
10
+ item = store.get(("users", "123"), "prefs")
11
+ ```
12
+
13
+ Vector search using Coco embeddings:
14
+ ```python
15
+ from coco.embeddings import init_embeddings
16
+ from coco_graph.store.memory import InMemoryStore
17
+
18
+ store = InMemoryStore(
19
+ index={
20
+ "dims": 1536,
21
+ "embed": init_embeddings("openai:text-embedding-3-small")
22
+ }
23
+ )
24
+
25
+ # Store documents
26
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
27
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
28
+
29
+ # Search by similarity
30
+ results = store.search(("docs",), query="python programming")
31
+ ```
32
+
33
+ Vector search using OpenAI SDK directly:
34
+ ```python
35
+ from openai import OpenAI
36
+ from coco_graph.store.memory import InMemoryStore
37
+
38
+ client = OpenAI()
39
+
40
+ def embed_texts(texts: list[str]) -> list[list[float]]:
41
+ response = client.embeddings.create(
42
+ model="text-embedding-3-small",
43
+ input=texts
44
+ )
45
+ return [e.embedding for e in response.data]
46
+
47
+ store = InMemoryStore(
48
+ index={
49
+ "dims": 1536,
50
+ "embed": embed_texts
51
+ }
52
+ )
53
+
54
+ # Store documents
55
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
56
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
57
+
58
+ # Search by similarity
59
+ results = store.search(("docs",), query="python programming")
60
+ ```
61
+
62
+ Async vector search using OpenAI SDK:
63
+ ```python
64
+ from openai import AsyncOpenAI
65
+ from coco_graph.store.memory import InMemoryStore
66
+
67
+ client = AsyncOpenAI()
68
+
69
+ async def aembed_texts(texts: list[str]) -> list[list[float]]:
70
+ response = await client.embeddings.create(
71
+ model="text-embedding-3-small",
72
+ input=texts
73
+ )
74
+ return [e.embedding for e in response.data]
75
+
76
+ store = InMemoryStore(
77
+ index={
78
+ "dims": 1536,
79
+ "embed": aembed_texts
80
+ }
81
+ )
82
+
83
+ # Store documents
84
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
85
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
86
+
87
+ # Search by similarity
88
+ results = await store.asearch(("docs",), query="python programming")
89
+ ```
90
+
91
+ Warning:
92
+ This store keeps all data in memory. Data is lost when the process exits.
93
+ For persistence, use a database-backed store like PostgresStore.
94
+
95
+ Tip:
96
+ For vector search, install numpy for better performance:
97
+ ```bash
98
+ pip install numpy
99
+ ```
100
+ """
101
+
102
+ from __future__ import annotations
103
+
104
+ import asyncio
105
+ import concurrent.futures as cf
106
+ import functools
107
+ import logging
108
+ from collections import defaultdict
109
+ from collections.abc import Iterable
110
+ from datetime import datetime, timezone
111
+ from importlib import util
112
+ from typing import Any
113
+
114
+ from coco_core.embeddings import Embeddings
115
+
116
+ from coco_graph.store.base import (
117
+ BaseStore,
118
+ GetOp,
119
+ IndexConfig,
120
+ Item,
121
+ ListNamespacesOp,
122
+ MatchCondition,
123
+ Op,
124
+ PutOp,
125
+ Result,
126
+ SearchItem,
127
+ SearchOp,
128
+ ensure_embeddings,
129
+ get_text_at_path,
130
+ tokenize_path,
131
+ )
132
+
133
+ logger = logging.getLogger(__name__)
134
+
135
+
136
+ class InMemoryStore(BaseStore):
137
+ """In-memory dictionary-backed store with optional vector search.
138
+
139
+ !!! example "Examples"
140
+ Basic key-value storage:
141
+ store = InMemoryStore()
142
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
143
+ item = store.get(("users", "123"), "prefs")
144
+
145
+ Vector search with embeddings:
146
+ from coco.embeddings import init_embeddings
147
+ store = InMemoryStore(index={
148
+ "dims": 1536,
149
+ "embed": init_embeddings("openai:text-embedding-3-small"),
150
+ "fields": ["text"],
151
+ })
152
+
153
+ # Store documents
154
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
155
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
156
+
157
+ # Search by similarity
158
+ results = store.search(("docs",), query="python programming")
159
+
160
+ Note:
161
+ Semantic search is disabled by default. You can enable it by providing an `index` configuration
162
+ when creating the store. Without this configuration, all `index` arguments passed to
163
+ `put` or `aput`will have no effect.
164
+
165
+ Warning:
166
+ This store keeps all data in memory. Data is lost when the process exits.
167
+ For persistence, use a database-backed store like PostgresStore.
168
+
169
+ Tip:
170
+ For vector search, install numpy for better performance:
171
+ ```bash
172
+ pip install numpy
173
+ ```
174
+ """
175
+
176
+ __slots__ = (
177
+ "_data",
178
+ "_vectors",
179
+ "index_config",
180
+ "embeddings",
181
+ )
182
+
183
+ def __init__(self, *, index: IndexConfig | None = None) -> None:
184
+ # Both _data and _vectors are wrapped in the In-memory API
185
+ # Do not change their names
186
+ self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)
187
+ # [ns][key][path]
188
+ self._vectors: dict[tuple[str, ...], dict[str, dict[str, list[float]]]] = (
189
+ defaultdict(lambda: defaultdict(dict))
190
+ )
191
+ self.index_config = index
192
+ if self.index_config:
193
+ self.index_config = self.index_config.copy()
194
+ self.embeddings: Embeddings | None = ensure_embeddings(
195
+ self.index_config.get("embed"),
196
+ )
197
+ self.index_config["__tokenized_fields"] = [
198
+ (p, tokenize_path(p)) if p != "$" else (p, p)
199
+ for p in (self.index_config.get("fields") or ["$"])
200
+ ]
201
+
202
+ else:
203
+ self.index_config = None
204
+ self.embeddings = None
205
+
206
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
207
+ # The batch/abatch methods are treated as internal.
208
+ # Users should access via put/search/get/list_namespaces/etc.
209
+ results, put_ops, search_ops = self._prepare_ops(ops)
210
+ if search_ops:
211
+ queryinmem_store = self._embed_search_queries(search_ops)
212
+ self._batch_search(search_ops, queryinmem_store, results)
213
+
214
+ to_embed = self._extract_texts(put_ops)
215
+ if to_embed and self.index_config and self.embeddings:
216
+ embeddings = self.embeddings.embed_documents(list(to_embed))
217
+ self._insertinmem_store(to_embed, embeddings)
218
+ self._apply_put_ops(put_ops)
219
+ return results
220
+
221
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
222
+ # The batch/abatch methods are treated as internal.
223
+ # Users should access via put/search/get/list_namespaces/etc.
224
+ results, put_ops, search_ops = self._prepare_ops(ops)
225
+ if search_ops:
226
+ queryinmem_store = await self._aembed_search_queries(search_ops)
227
+ self._batch_search(search_ops, queryinmem_store, results)
228
+
229
+ to_embed = self._extract_texts(put_ops)
230
+ if to_embed and self.index_config and self.embeddings:
231
+ embeddings = await self.embeddings.aembed_documents(list(to_embed))
232
+ self._insertinmem_store(to_embed, embeddings)
233
+ self._apply_put_ops(put_ops)
234
+ return results
235
+
236
+ # Helpers
237
+
238
+ def _filter_items(self, op: SearchOp) -> list[tuple[Item, list[list[float]]]]:
239
+ """Filter items by namespace and filter function, return items with their embeddings."""
240
+ namespace_prefix = op.namespace_prefix
241
+
242
+ def filter_func(item: Item) -> bool:
243
+ if not op.filter:
244
+ return True
245
+
246
+ return all(
247
+ _compare_values(item.value.get(key), filter_value)
248
+ for key, filter_value in op.filter.items()
249
+ )
250
+
251
+ filtered = []
252
+ for namespace in self._data:
253
+ if not (
254
+ namespace[: len(namespace_prefix)] == namespace_prefix
255
+ if len(namespace) >= len(namespace_prefix)
256
+ else False
257
+ ):
258
+ continue
259
+
260
+ for key, item in self._data[namespace].items():
261
+ if filter_func(item):
262
+ if op.query and (embeddings := self._vectors[namespace].get(key)):
263
+ filtered.append((item, list(embeddings.values())))
264
+ else:
265
+ filtered.append((item, []))
266
+ return filtered
267
+
268
+ def _embed_search_queries(
269
+ self,
270
+ search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
271
+ ) -> dict[str, list[float]]:
272
+ queryinmem_store = {}
273
+ if self.index_config and self.embeddings and search_ops:
274
+ queries = {op.query for (op, _) in search_ops.values() if op.query}
275
+
276
+ if queries:
277
+ with cf.ThreadPoolExecutor() as executor:
278
+ futures = {
279
+ q: executor.submit(self.embeddings.embed_query, q)
280
+ for q in list(queries)
281
+ }
282
+ for query, future in futures.items():
283
+ queryinmem_store[query] = future.result()
284
+
285
+ return queryinmem_store
286
+
287
+ async def _aembed_search_queries(
288
+ self,
289
+ search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
290
+ ) -> dict[str, list[float]]:
291
+ queryinmem_store = {}
292
+ if self.index_config and self.embeddings and search_ops:
293
+ queries = {op.query for (op, _) in search_ops.values() if op.query}
294
+
295
+ if queries:
296
+ coros = [self.embeddings.aembed_query(q) for q in list(queries)]
297
+ results = await asyncio.gather(*coros)
298
+ queryinmem_store = dict(zip(queries, results, strict=False))
299
+
300
+ return queryinmem_store
301
+
302
+ def _batch_search(
303
+ self,
304
+ ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
305
+ queryinmem_store: dict[str, list[float]],
306
+ results: list[Result],
307
+ ) -> None:
308
+ """Perform batch similarity search for multiple queries."""
309
+ for i, (op, candidates) in ops.items():
310
+ if not candidates:
311
+ results[i] = []
312
+ continue
313
+ if op.query and queryinmem_store:
314
+ query_embedding = queryinmem_store[op.query]
315
+ flat_items, flat_vectors = [], []
316
+ scoreless = []
317
+ for item, vectors in candidates:
318
+ for vector in vectors:
319
+ flat_items.append(item)
320
+ flat_vectors.append(vector)
321
+ if not vectors:
322
+ scoreless.append(item)
323
+
324
+ scores = _cosine_similarity(query_embedding, flat_vectors)
325
+ sorted_results = sorted(
326
+ zip(scores, flat_items, strict=False),
327
+ key=lambda x: x[0],
328
+ reverse=True,
329
+ )
330
+ # max pooling
331
+ seen: set[tuple[tuple[str, ...], str]] = set()
332
+ kept: list[tuple[float | None, Item]] = []
333
+ for score, item in sorted_results:
334
+ key = (item.namespace, item.key)
335
+ if key in seen:
336
+ continue
337
+ ix = len(seen)
338
+ seen.add(key)
339
+ if ix >= op.offset + op.limit:
340
+ break
341
+ if ix < op.offset:
342
+ continue
343
+
344
+ kept.append((score, item))
345
+ if scoreless and len(kept) < op.limit:
346
+ # Corner case: if we request more items than what we have embedded,
347
+ # fill the rest with non-scored items
348
+ kept.extend(
349
+ (None, item) for item in scoreless[: op.limit - len(kept)]
350
+ )
351
+
352
+ results[i] = [
353
+ SearchItem(
354
+ namespace=item.namespace,
355
+ key=item.key,
356
+ value=item.value,
357
+ created_at=item.created_at,
358
+ updated_at=item.updated_at,
359
+ score=float(score) if score is not None else None,
360
+ )
361
+ for score, item in kept
362
+ ]
363
+ else:
364
+ results[i] = [
365
+ SearchItem(
366
+ namespace=item.namespace,
367
+ key=item.key,
368
+ value=item.value,
369
+ created_at=item.created_at,
370
+ updated_at=item.updated_at,
371
+ )
372
+ for (item, _) in candidates[op.offset : op.offset + op.limit]
373
+ ]
374
+
375
+ def _prepare_ops(
376
+ self, ops: Iterable[Op]
377
+ ) -> tuple[
378
+ list[Result],
379
+ dict[tuple[tuple[str, ...], str], PutOp],
380
+ dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
381
+ ]:
382
+ results: list[Result] = []
383
+ put_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
384
+ search_ops: dict[
385
+ int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]
386
+ ] = {}
387
+ for i, op in enumerate(ops):
388
+ if isinstance(op, GetOp):
389
+ item = self._data[op.namespace].get(op.key)
390
+ results.append(item)
391
+ elif isinstance(op, SearchOp):
392
+ search_ops[i] = (op, self._filter_items(op))
393
+ results.append(None)
394
+ elif isinstance(op, ListNamespacesOp):
395
+ results.append(self._handle_list_namespaces(op))
396
+ elif isinstance(op, PutOp):
397
+ put_ops[(op.namespace, op.key)] = op
398
+ results.append(None)
399
+ else:
400
+ raise ValueError(f"Unknown operation type: {type(op)}")
401
+
402
+ return results, put_ops, search_ops
403
+
404
+ def _apply_put_ops(self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]) -> None:
405
+ for (namespace, key), op in put_ops.items():
406
+ if op.value is None:
407
+ self._data[namespace].pop(key, None)
408
+ self._vectors[namespace].pop(key, None)
409
+ else:
410
+ self._data[namespace][key] = Item(
411
+ value=dict(op.value),
412
+ key=key,
413
+ namespace=namespace,
414
+ created_at=datetime.now(timezone.utc),
415
+ updated_at=datetime.now(timezone.utc),
416
+ )
417
+
418
+ def _extract_texts(
419
+ self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]
420
+ ) -> dict[str, list[tuple[tuple[str, ...], str, str]]]:
421
+ if put_ops and self.index_config and self.embeddings:
422
+ to_embed = defaultdict(list)
423
+
424
+ for op in put_ops.values():
425
+ if op.value is not None and op.index is not False:
426
+ if op.index is None:
427
+ paths = self.index_config["__tokenized_fields"]
428
+ else:
429
+ paths = [(ix, tokenize_path(ix)) for ix in op.index]
430
+ for path, field in paths:
431
+ texts = get_text_at_path(op.value, field)
432
+ if texts:
433
+ if len(texts) > 1:
434
+ for i, text in enumerate(texts):
435
+ to_embed[text].append(
436
+ (op.namespace, op.key, f"{path}.{i}")
437
+ )
438
+
439
+ else:
440
+ to_embed[texts[0]].append((op.namespace, op.key, path))
441
+
442
+ return to_embed
443
+
444
+ return {}
445
+
446
+ def _insertinmem_store(
447
+ self,
448
+ to_embed: dict[str, list[tuple[tuple[str, ...], str, str]]],
449
+ embeddings: list[list[float]],
450
+ ) -> None:
451
+ indices = [index for indices in to_embed.values() for index in indices]
452
+ if len(indices) != len(embeddings):
453
+ raise ValueError(
454
+ f"Number of embeddings ({len(embeddings)}) does not"
455
+ f" match number of indices ({len(indices)})"
456
+ )
457
+ for embedding, (ns, key, path) in zip(embeddings, indices, strict=False):
458
+ self._vectors[ns][key][path] = embedding
459
+
460
+ def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
461
+ all_namespaces = list(
462
+ self._data.keys()
463
+ ) # Avoid collection size changing while iterating
464
+ namespaces = all_namespaces
465
+ if op.match_conditions:
466
+ namespaces = [
467
+ ns
468
+ for ns in namespaces
469
+ if all(_does_match(condition, ns) for condition in op.match_conditions)
470
+ ]
471
+
472
+ if op.max_depth is not None:
473
+ namespaces = sorted({ns[: op.max_depth] for ns in namespaces})
474
+ else:
475
+ namespaces = sorted(namespaces)
476
+ return namespaces[op.offset : op.offset + op.limit]
477
+
478
+
479
+ @functools.lru_cache(maxsize=1)
480
+ def _check_numpy() -> bool:
481
+ if bool(util.find_spec("numpy")):
482
+ return True
483
+ logger.warning(
484
+ "NumPy not found in the current Python environment. "
485
+ "The InMemoryStore will use a pure Python implementation for vector operations, "
486
+ "which may significantly impact performance, especially for large datasets or frequent searches. "
487
+ "For optimal speed and efficiency, consider installing NumPy: "
488
+ "pip install numpy"
489
+ )
490
+ return False
491
+
492
+
493
+ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
494
+ """
495
+ Compute cosine similarity between a vector X and a matrix Y.
496
+ Lazy import numpy for efficiency.
497
+ """
498
+ if not Y:
499
+ return []
500
+ if _check_numpy():
501
+ import numpy as np
502
+
503
+ X_arr = np.array(X) if not isinstance(X, np.ndarray) else X
504
+ Y_arr = np.array(Y) if not isinstance(Y, np.ndarray) else Y
505
+ X_norm = np.linalg.norm(X_arr)
506
+ Y_norm = np.linalg.norm(Y_arr, axis=1)
507
+
508
+ # Avoid division by zero
509
+ mask = Y_norm != 0
510
+ similarities = np.zeros_like(Y_norm)
511
+ similarities[mask] = np.dot(Y_arr[mask], X_arr) / (Y_norm[mask] * X_norm)
512
+ return similarities.tolist()
513
+
514
+ similarities = []
515
+ for y in Y:
516
+ dot_product = sum(a * b for a, b in zip(X, y, strict=False))
517
+ norm1 = sum(a * a for a in X) ** 0.5
518
+ norm2 = sum(a * a for a in y) ** 0.5
519
+ similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
520
+ similarities.append(similarity)
521
+
522
+ return similarities
523
+
524
+
525
+ def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
526
+ """Whether a namespace key matches a match condition."""
527
+ match_type = match_condition.match_type
528
+ path = match_condition.path
529
+
530
+ if len(key) < len(path):
531
+ return False
532
+
533
+ if match_type == "prefix":
534
+ for k_elem, p_elem in zip(key, path, strict=False):
535
+ if p_elem == "*":
536
+ continue # Wildcard matches any element
537
+ if k_elem != p_elem:
538
+ return False
539
+ return True
540
+ elif match_type == "suffix":
541
+ for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):
542
+ if p_elem == "*":
543
+ continue # Wildcard matches any element
544
+ if k_elem != p_elem:
545
+ return False
546
+ return True
547
+ else:
548
+ raise ValueError(f"Unsupported match type: {match_type}")
549
+
550
+
551
+ def _compare_values(item_value: Any, filter_value: Any) -> bool:
552
+ """Compare values in a JSONB-like way, handling nested objects."""
553
+ if isinstance(filter_value, dict):
554
+ if any(k.startswith("$") for k in filter_value):
555
+ return all(
556
+ _apply_operator(item_value, op_key, op_value)
557
+ for op_key, op_value in filter_value.items()
558
+ )
559
+ if not isinstance(item_value, dict):
560
+ return False
561
+ return all(
562
+ _compare_values(item_value.get(k), v) for k, v in filter_value.items()
563
+ )
564
+ elif isinstance(filter_value, (list, tuple)):
565
+ return (
566
+ isinstance(item_value, (list, tuple))
567
+ and len(item_value) == len(filter_value)
568
+ and all(
569
+ _compare_values(iv, fv)
570
+ for iv, fv in zip(item_value, filter_value, strict=False)
571
+ )
572
+ )
573
+ else:
574
+ return item_value == filter_value
575
+
576
+
577
+ def _apply_operator(value: Any, operator: str, op_value: Any) -> bool:
578
+ """Apply a comparison operator, matching PostgreSQL's JSONB behavior."""
579
+ if operator == "$eq":
580
+ return value == op_value
581
+ elif operator == "$gt":
582
+ return float(value) > float(op_value)
583
+ elif operator == "$gte":
584
+ return float(value) >= float(op_value)
585
+ elif operator == "$lt":
586
+ return float(value) < float(op_value)
587
+ elif operator == "$lte":
588
+ return float(value) <= float(op_value)
589
+ elif operator == "$ne":
590
+ return value != op_value
591
+ else:
592
+ raise ValueError(f"Unsupported operator: {operator}")
File without changes
@@ -0,0 +1,4 @@
1
+ from coco_graph.store.postgres.aio import AsyncPostgresStore
2
+ from coco_graph.store.postgres.base import PoolConfig, PostgresStore
3
+
4
+ __all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]