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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- 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
|