Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a1225f8e9 | ||
|
|
1b51ad5f89 |
@@ -7,18 +7,6 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-09-25" description="v2.2.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** `add()` (`Memory` and `AsyncMemory`) no longer reports records the vector store rejected as successful `ADD` events. Only records that were actually inserted are written to history, entity-linked, and returned. If none of the extracted memories could be inserted, `add()` now raises `VectorStoreError` instead of returning memories that were never stored ([#7066](https://github.com/mem0ai/mem0/pull/7066))
|
||||
- **Core:** Restore the context-manager protocol on `Memory` (`with Memory() as m:`) and `AsyncMemory` (`async with AsyncMemory() as m:`), which closes the instance on exit ([#7354](https://github.com/mem0ai/mem0/pull/7354))
|
||||
- **LLMs:** The AWS Bedrock Anthropic path now returns the first Converse content block that carries text instead of always reading `content[0]`. Claude reasoning models can emit a `reasoningContent` block before the text block, which previously made the call fail with a `KeyError` ([#6369](https://github.com/mem0ai/mem0/pull/6369))
|
||||
- **Vector Stores:** Turbopuffer filters now apply every operator (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`). Only `gte` and `lte` were read before, so any other operator was silently dropped and the query returned unfiltered results. An unsupported operator now raises `ValueError` ([#6564](https://github.com/mem0ai/mem0/pull/6564))
|
||||
- **Vector Stores:** Turbopuffer `search()` scores now respect `distance_metric`. With `euclidean_squared`, the unbounded squared distance maps to `1 / (1 + distance)` instead of `1 - distance`, which went negative and inverted ranking for any distance above 1 ([#6559](https://github.com/mem0ai/mem0/pull/6559))
|
||||
- **Vector Stores:** S3 Vectors `search()` scores are now metric-aware. With `euclidean`, the distance maps to `1 / (1 + distance)` instead of `max(0, 1 - distance)`, which collapsed most scores to 0 ([#6547](https://github.com/mem0ai/mem0/pull/6547))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="v2.2.0">
|
||||
|
||||
**New Features:**
|
||||
@@ -1257,16 +1245,6 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-09-25" description="v3.3.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Config (OSS):** `ConfigManager` no longer injects OpenAI's default `baseURL` and `model` into non-OpenAI LLM providers. The defaults now apply only to `openai` and `openai_structured`, so a DeepSeek, xAI, or other provider config without an explicit `baseURL` or `model` falls back to that provider's own defaults and env vars (`DEEPSEEK_API_BASE`, `XAI_API_BASE`, ...) instead of pointing at OpenAI with an OpenAI model name ([#7350](https://github.com/mem0ai/mem0/pull/7350))
|
||||
- **Vector Stores:** Turbopuffer filters now apply every operator (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`). Only the range operators were read before, so `eq`, `ne`, `in`, and `nin` were silently dropped. A `"*"` value now matches anything instead of nothing, an array value is treated as `in`, and an unsupported operator throws ([#6578](https://github.com/mem0ai/mem0/pull/6578))
|
||||
- **Vector Stores:** Turbopuffer `search()` with `euclidean_squared` now maps the unbounded squared distance to `1 / (1 + distance)` instead of `1 - distance`, which went negative and inverted ranking for any distance above 1 ([#6580](https://github.com/mem0ai/mem0/pull/6580))
|
||||
- **Packaging:** `pg`, `@types/pg`, and `natural` are now optional peer dependencies, and `pg` / `@types/pg` accept caret ranges instead of the exact `8.11.3` / `8.11.0` pins, so installing `mem0ai` no longer pulls in `pg` or conflicts with an app's own `pg` version. The PGVector store now imports `pg` only when it is used, so install it alongside `mem0ai` (`npm install pg`) if you use that store. `@types/jest` moved from peer dependencies to dev dependencies ([#7450](https://github.com/mem0ai/mem0/pull/7450))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="v3.3.0">
|
||||
|
||||
**New Features:**
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"@qdrant/js-client-rest": "^1.18.0",
|
||||
"uuid@<11.1.1": ">=11.1.1",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0",
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0",
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0",
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0",
|
||||
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
|
||||
|
||||
Generated
+5
-5
@@ -13,7 +13,7 @@ overrides:
|
||||
'@qdrant/js-client-rest': ^1.18.0
|
||||
uuid@<11.1.1: '>=11.1.1'
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<7.29.1: '>=7.29.1 <8.0.0'
|
||||
undici@<7.29.0: '>=7.29.0 <8.0.0'
|
||||
axios@<1.18.0: '>=1.18.0 <2.0.0'
|
||||
postcss@<8.5.18: '>=8.5.18 <9.0.0'
|
||||
mongoose@>=9.0.0 <9.7.2: '>=9.7.2 <10.0.0'
|
||||
@@ -2036,8 +2036,8 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici@7.30.0:
|
||||
resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==}
|
||||
undici@7.29.0:
|
||||
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
@@ -2547,7 +2547,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 5.9.3
|
||||
undici: 7.30.0
|
||||
undici: 7.29.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -4121,7 +4121,7 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@7.30.0: {}
|
||||
undici@7.29.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ overrides:
|
||||
"@qdrant/js-client-rest": "^1.18.0"
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0"
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0"
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0"
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
|
||||
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
"form-data@<4.0.6": ">=4.0.6",
|
||||
"uuid@<11.1.1": ">=11.1.1",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0",
|
||||
"undici@>=8.0.0 <8.10.2": ">=8.10.2 <9.0.0",
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0",
|
||||
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0",
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0",
|
||||
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0",
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0",
|
||||
|
||||
+10
-10
@@ -8,8 +8,8 @@ overrides:
|
||||
form-data@<4.0.6: '>=4.0.6'
|
||||
uuid@<11.1.1: '>=11.1.1'
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<7.29.1: '>=7.29.1 <8.0.0'
|
||||
undici@>=8.0.0 <8.10.2: '>=8.10.2 <9.0.0'
|
||||
undici@<7.29.0: '>=7.29.0 <8.0.0'
|
||||
undici@>=8.0.0 <8.9.0: '>=8.9.0 <9.0.0'
|
||||
axios@<1.18.0: '>=1.18.0 <2.0.0'
|
||||
brace-expansion@>=3.0.0 <5.0.8: '>=5.0.8 <6.0.0'
|
||||
postcss@<8.5.18: '>=8.5.18 <9.0.0'
|
||||
@@ -2354,12 +2354,12 @@ packages:
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
undici@7.30.0:
|
||||
resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==}
|
||||
undici@7.29.0:
|
||||
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@8.11.2:
|
||||
resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
|
||||
undici@8.10.0:
|
||||
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
@@ -2942,7 +2942,7 @@ snapshots:
|
||||
minimatch: 10.2.5
|
||||
proper-lockfile: 4.1.2
|
||||
typebox: 1.1.38
|
||||
undici: 8.11.2
|
||||
undici: 8.10.0
|
||||
yaml: 2.9.0
|
||||
optionalDependencies:
|
||||
'@mariozechner/clipboard': 0.3.9
|
||||
@@ -3203,7 +3203,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 6.0.3
|
||||
undici: 7.30.0
|
||||
undici: 7.29.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -4895,9 +4895,9 @@ snapshots:
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
undici@7.30.0: {}
|
||||
undici@7.29.0: {}
|
||||
|
||||
undici@8.11.2: {}
|
||||
undici@8.10.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ overrides:
|
||||
"form-data@<4.0.6": ">=4.0.6"
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0"
|
||||
"undici@>=8.0.0 <8.10.2": ">=8.10.2 <9.0.0"
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0"
|
||||
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0"
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0"
|
||||
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0"
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.0",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -288,7 +288,7 @@
|
||||
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0",
|
||||
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0",
|
||||
"sharp@<0.35.0": ">=0.35.0 <0.36.0",
|
||||
"adm-zip@<0.6.1": ">=0.6.1 <0.7.0",
|
||||
"adm-zip@<0.6.0": ">=0.6.0 <0.7.0",
|
||||
"langsmith@<0.6.0": "^0.6.0",
|
||||
"minimatch@<3.1.3": "^3.1.3",
|
||||
"minimatch@>=5.0.0 <5.1.8": "^5.1.8",
|
||||
@@ -303,7 +303,7 @@
|
||||
"glob@>=10.2.0 <10.5.0": "^10.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.4",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0",
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.967.0",
|
||||
"@aws-sdk/client-neptune-graph": "3.966.0",
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0",
|
||||
@@ -315,7 +315,7 @@
|
||||
"thrift@<0.23.0": "^0.23.0",
|
||||
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0",
|
||||
"protobufjs@<7.6.5": ">=7.6.5 <8.0.0",
|
||||
"ip-address@<10.5.1": ">=10.5.1 <11.0.0",
|
||||
"ip-address@<10.3.1": ">=10.3.1 <11.0.0",
|
||||
"browserslist@<4.28.7": ">=4.28.7 <5.0.0",
|
||||
"mysql2@<3.23.1": ">=3.23.1 <4.0.0"
|
||||
}
|
||||
|
||||
Generated
+18
-18
@@ -12,7 +12,7 @@ overrides:
|
||||
js-yaml@<3.15.1: '>=3.15.1 <4.0.0'
|
||||
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5.0.0'
|
||||
sharp@<0.35.0: '>=0.35.0 <0.36.0'
|
||||
adm-zip@<0.6.1: '>=0.6.1 <0.7.0'
|
||||
adm-zip@<0.6.0: '>=0.6.0 <0.7.0'
|
||||
langsmith@<0.6.0: ^0.6.0
|
||||
minimatch@<3.1.3: ^3.1.3
|
||||
minimatch@>=5.0.0 <5.1.8: ^5.1.8
|
||||
@@ -27,7 +27,7 @@ overrides:
|
||||
glob@>=10.2.0 <10.5.0: ^10.5.0
|
||||
'@modelcontextprotocol/sdk': ^1.25.4
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<7.29.1: '>=7.29.1 <8.0.0'
|
||||
undici@<7.29.0: '>=7.29.0 <8.0.0'
|
||||
'@aws-sdk/client-bedrock-runtime': 3.967.0
|
||||
'@aws-sdk/client-neptune-graph': 3.966.0
|
||||
axios@<1.18.0: '>=1.18.0 <2.0.0'
|
||||
@@ -39,7 +39,7 @@ overrides:
|
||||
thrift@<0.23.0: ^0.23.0
|
||||
mongoose@>=9.0.0 <9.7.2: '>=9.7.2 <10.0.0'
|
||||
protobufjs@<7.6.5: '>=7.6.5 <8.0.0'
|
||||
ip-address@<10.5.1: '>=10.5.1 <11.0.0'
|
||||
ip-address@<10.3.1: '>=10.3.1 <11.0.0'
|
||||
browserslist@<4.28.7: '>=4.28.7 <5.0.0'
|
||||
mysql2@<3.23.1: '>=3.23.1 <4.0.0'
|
||||
|
||||
@@ -2009,8 +2009,8 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
adm-zip@0.6.1:
|
||||
resolution: {integrity: sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==}
|
||||
adm-zip@0.6.0:
|
||||
resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
afinn-165-financialmarketnews@3.0.0:
|
||||
@@ -3034,8 +3034,8 @@ packages:
|
||||
resolution: {integrity: sha512-4rTJX6Q5wTYEvxboXi8DsEiUo+OvqJGtLYOSGm37KpdRXsG5XJjbVtYKGJpPSWP+QT7rWscA4vsrdmzbEbenpw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
ip-address@10.7.2:
|
||||
resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==}
|
||||
ip-address@10.4.0:
|
||||
resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
@@ -4609,8 +4609,8 @@ packages:
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
undici@7.30.0:
|
||||
resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==}
|
||||
undici@7.29.0:
|
||||
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
update-browserslist-db@1.3.2:
|
||||
@@ -6138,7 +6138,7 @@ snapshots:
|
||||
ms: 2.1.3
|
||||
secure-json-parse: 4.1.0
|
||||
tslib: 2.8.1
|
||||
undici: 7.30.0
|
||||
undici: 7.29.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6706,7 +6706,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 5.5.4
|
||||
undici: 7.30.0
|
||||
undici: 7.29.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -7178,7 +7178,7 @@ snapshots:
|
||||
'@turbopuffer/turbopuffer@2.5.0':
|
||||
dependencies:
|
||||
pako: 2.2.0
|
||||
undici: 7.30.0
|
||||
undici: 7.29.0
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
@@ -7346,7 +7346,7 @@ snapshots:
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
adm-zip@0.6.1: {}
|
||||
adm-zip@0.6.0: {}
|
||||
|
||||
afinn-165-financialmarketnews@3.0.0: {}
|
||||
|
||||
@@ -7614,7 +7614,7 @@ snapshots:
|
||||
cassandra-driver@4.8.0:
|
||||
dependencies:
|
||||
'@types/node': 18.19.130
|
||||
adm-zip: 0.6.1
|
||||
adm-zip: 0.6.0
|
||||
long: 5.2.5
|
||||
|
||||
chalk-template@0.4.0:
|
||||
@@ -8435,7 +8435,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ip-address@10.7.2: {}
|
||||
ip-address@10.4.0: {}
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
@@ -9291,7 +9291,7 @@ snapshots:
|
||||
|
||||
onnxruntime-node@1.24.3:
|
||||
dependencies:
|
||||
adm-zip: 0.6.1
|
||||
adm-zip: 0.6.0
|
||||
global-agent: 3.0.0
|
||||
onnxruntime-common: 1.24.3
|
||||
|
||||
@@ -9845,7 +9845,7 @@ snapshots:
|
||||
|
||||
socks@2.8.9:
|
||||
dependencies:
|
||||
ip-address: 10.7.2
|
||||
ip-address: 10.4.0
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
source-map-support@0.5.13:
|
||||
@@ -10182,7 +10182,7 @@ snapshots:
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
undici@7.30.0: {}
|
||||
undici@7.29.0: {}
|
||||
|
||||
update-browserslist-db@1.3.2(browserslist@4.28.9):
|
||||
dependencies:
|
||||
|
||||
@@ -13,7 +13,7 @@ overrides:
|
||||
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0"
|
||||
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0"
|
||||
"sharp@<0.35.0": ">=0.35.0 <0.36.0"
|
||||
"adm-zip@<0.6.1": ">=0.6.1 <0.7.0"
|
||||
"adm-zip@<0.6.0": ">=0.6.0 <0.7.0"
|
||||
"langsmith@<0.6.0": "^0.6.0"
|
||||
"minimatch@<3.1.3": "^3.1.3"
|
||||
"minimatch@>=5.0.0 <5.1.8": "^5.1.8"
|
||||
@@ -27,7 +27,7 @@ overrides:
|
||||
"glob@>=10.2.0 <10.5.0": "^10.5.0"
|
||||
"@modelcontextprotocol/sdk": "^1.25.4"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<7.29.1": ">=7.29.1 <8.0.0"
|
||||
"undici@<7.29.0": ">=7.29.0 <8.0.0"
|
||||
"axios@<1.18.0": ">=1.18.0 <2.0.0"
|
||||
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
|
||||
"brace-expansion@>=2.0.0 <2.1.4": ">=2.1.4 <3.0.0"
|
||||
@@ -37,7 +37,7 @@ overrides:
|
||||
"thrift@<0.23.0": "^0.23.0"
|
||||
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
|
||||
"protobufjs@<7.6.5": ">=7.6.5 <8.0.0"
|
||||
"ip-address@<10.5.1": ">=10.5.1 <11.0.0"
|
||||
"ip-address@<10.3.1": ">=10.3.1 <11.0.0"
|
||||
"browserslist@<4.28.7": ">=4.28.7 <5.0.0"
|
||||
"mysql2@<3.23.1": ">=3.23.1 <4.0.0"
|
||||
|
||||
|
||||
@@ -247,54 +247,23 @@ export class TurbopufferDB implements VectorStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Maps mem0's universal filter operators to Turbopuffer's filter tokens.
|
||||
private static readonly OPERATOR_MAP: Record<string, string> = {
|
||||
eq: "Eq",
|
||||
ne: "NotEq",
|
||||
gt: "Gt",
|
||||
gte: "Gte",
|
||||
lt: "Lt",
|
||||
lte: "Lte",
|
||||
in: "In",
|
||||
nin: "NotIn",
|
||||
};
|
||||
|
||||
private convertFilters(filters?: SearchFilters): any {
|
||||
if (!filters || Object.keys(filters).length === 0) return null;
|
||||
|
||||
const conditions: any[] = [];
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
// "*" is a match-any wildcard: it must not constrain the query. The old
|
||||
// code turned it into `[key, "Eq", "*"]`, matching nothing.
|
||||
if (value === "*") {
|
||||
continue;
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
if ("gte" in value) conditions.push([key, "Gte", value.gte]);
|
||||
if ("lte" in value) conditions.push([key, "Lte", value.lte]);
|
||||
if ("gt" in value) conditions.push([key, "Gt", value.gt]);
|
||||
if ("lt" in value) conditions.push([key, "Lt", value.lt]);
|
||||
} else {
|
||||
conditions.push([key, "Eq", value]);
|
||||
}
|
||||
|
||||
// Array shorthand: { key: [a, b] } means "in".
|
||||
if (Array.isArray(value)) {
|
||||
conditions.push([key, "In", value]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
// Operator dict: every operator present must hold. Previously only
|
||||
// `gte` and `lte` were read, so `gt`/`lt`/`ne`/`eq`/`in`/`nin` were
|
||||
// silently dropped and the filter returned unfiltered results.
|
||||
for (const [op, operand] of Object.entries(value)) {
|
||||
const token = TurbopufferDB.OPERATOR_MAP[op];
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
`Unsupported Turbopuffer filter operator '${op}' for field '${key}'. ` +
|
||||
`Supported operators: ${Object.keys(TurbopufferDB.OPERATOR_MAP).join(", ")}.`,
|
||||
);
|
||||
}
|
||||
conditions.push([key, token, operand]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scalar shorthand: equality.
|
||||
conditions.push([key, "Eq", value]);
|
||||
}
|
||||
|
||||
if (conditions.length === 0) return null;
|
||||
@@ -305,18 +274,7 @@ export class TurbopufferDB implements VectorStore {
|
||||
private parseRows(rows: any[]): VectorStoreResult[] {
|
||||
return rows.map((row) => {
|
||||
const { id, $dist, vector, ...rest } = row;
|
||||
let score: number | undefined;
|
||||
if ($dist == null) {
|
||||
score = undefined;
|
||||
} else if (this.distanceMetric === "euclidean_squared") {
|
||||
// euclidean_squared $dist is an unbounded squared distance, so 1 - $dist
|
||||
// goes negative for any $dist > 1 and inverts ranking. Convert it to a
|
||||
// bounded higher-is-better score, mirroring the milvus/baidu stores.
|
||||
score = 1 / (1 + $dist);
|
||||
} else {
|
||||
// Cosine distance is in [0, 2]; 1 - $dist stays a meaningful similarity.
|
||||
score = 1 - $dist;
|
||||
}
|
||||
const score = $dist != null ? 1 - $dist : undefined;
|
||||
return { id: String(id), payload: rest, score };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
/**
|
||||
* Turbopuffer vector store — score conversion unit tests.
|
||||
*
|
||||
* Drives parseRows() through the public search() API with a mocked
|
||||
* @turbopuffer/turbopuffer peer, asserting the score returned per metric.
|
||||
*/
|
||||
|
||||
const mockQuery = jest.fn();
|
||||
|
||||
jest.mock("@turbopuffer/turbopuffer", () => ({
|
||||
__esModule: true,
|
||||
default: class {
|
||||
namespace() {
|
||||
return { query: mockQuery };
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import { TurbopufferDB } from "../src/vector_stores/turbopuffer";
|
||||
|
||||
function makeStore(distanceMetric?: string) {
|
||||
return new TurbopufferDB({
|
||||
apiKey: "test-key",
|
||||
collectionName: "mem0",
|
||||
...(distanceMetric ? { distanceMetric } : {}),
|
||||
} as any);
|
||||
}
|
||||
|
||||
async function scoreFor(
|
||||
distanceMetric: string | undefined,
|
||||
row: Record<string, any>,
|
||||
): Promise<number | undefined> {
|
||||
mockQuery.mockResolvedValueOnce({ rows: [row] });
|
||||
const results = await makeStore(distanceMetric).search([0.1, 0.2, 0.3], 5);
|
||||
return results[0].score;
|
||||
}
|
||||
|
||||
describe("TurbopufferDB score conversion", () => {
|
||||
it("keeps cosine distance as 1 - dist (default metric)", async () => {
|
||||
// cosine_distance is the default; a distance of 0.25 -> similarity 0.75.
|
||||
expect(await scoreFor(undefined, { id: "a", $dist: 0.25 })).toBeCloseTo(
|
||||
0.75,
|
||||
10,
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds an unbounded euclidean_squared distance to a 0..1 similarity", async () => {
|
||||
// $dist = 4.0 is a squared distance. 1 - 4.0 = -3.0 would invert ranking;
|
||||
// 1 / (1 + 4.0) = 0.2 keeps it higher-is-better and in range.
|
||||
const score = await scoreFor("euclidean_squared", { id: "a", $dist: 4.0 });
|
||||
expect(score).toBeCloseTo(0.2, 10);
|
||||
expect(score!).toBeGreaterThanOrEqual(0);
|
||||
expect(score!).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("ranks a nearer euclidean_squared hit above a farther one", async () => {
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{ id: "near", $dist: 1.0 },
|
||||
{ id: "far", $dist: 9.0 },
|
||||
],
|
||||
});
|
||||
const results = await makeStore("euclidean_squared").search(
|
||||
[0.1, 0.2, 0.3],
|
||||
5,
|
||||
);
|
||||
const near = results.find((r) => r.id === "near")!;
|
||||
const far = results.find((r) => r.id === "far")!;
|
||||
expect(near.score!).toBeGreaterThan(far.score!);
|
||||
});
|
||||
|
||||
it("preserves an undefined score when the row has no distance", async () => {
|
||||
expect(await scoreFor("euclidean_squared", { id: "a" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
/**
|
||||
* Turbopuffer vector store — filter translation unit tests.
|
||||
*
|
||||
* Drives the private convertFilters() through the public search() API with a
|
||||
* mocked @turbopuffer/turbopuffer peer, and asserts the filter tuple handed to
|
||||
* ns.query().
|
||||
*/
|
||||
|
||||
const mockQuery = jest.fn().mockResolvedValue({ rows: [] });
|
||||
|
||||
// createClient() does `new sdk.default({...})`, whose namespace() returns the
|
||||
// object search() calls query() on.
|
||||
jest.mock("@turbopuffer/turbopuffer", () => ({
|
||||
__esModule: true,
|
||||
default: class {
|
||||
namespace() {
|
||||
return { query: mockQuery };
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import { TurbopufferDB } from "../src/vector_stores/turbopuffer";
|
||||
|
||||
function makeStore() {
|
||||
return new TurbopufferDB({
|
||||
apiKey: "test-key",
|
||||
collectionName: "mem0",
|
||||
} as any);
|
||||
}
|
||||
|
||||
async function filterFor(filters: any): Promise<any> {
|
||||
mockQuery.mockClear();
|
||||
await makeStore().search([0.1, 0.2, 0.3], 5, filters);
|
||||
return mockQuery.mock.calls[0][0].filters;
|
||||
}
|
||||
|
||||
describe("TurbopufferDB convertFilters", () => {
|
||||
it("maps every operator, not just gte/lte", async () => {
|
||||
expect(await filterFor({ age: { gt: 18 } })).toEqual(["age", "Gt", 18]);
|
||||
expect(await filterFor({ age: { lt: 65 } })).toEqual(["age", "Lt", 65]);
|
||||
expect(await filterFor({ age: { ne: 40 } })).toEqual(["age", "NotEq", 40]);
|
||||
expect(await filterFor({ role: { eq: "admin" } })).toEqual([
|
||||
"role",
|
||||
"Eq",
|
||||
"admin",
|
||||
]);
|
||||
expect(await filterFor({ tier: { in: ["a", "b"] } })).toEqual([
|
||||
"tier",
|
||||
"In",
|
||||
["a", "b"],
|
||||
]);
|
||||
expect(await filterFor({ tier: { nin: ["x"] } })).toEqual([
|
||||
"tier",
|
||||
"NotIn",
|
||||
["x"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies every operator in a compound range (AND), not just the first", async () => {
|
||||
const filter = await filterFor({ age: { gt: 18, lt: 65 } });
|
||||
expect(filter[0]).toBe("And");
|
||||
expect(filter[1]).toEqual(
|
||||
expect.arrayContaining([
|
||||
["age", "Gt", 18],
|
||||
["age", "Lt", 65],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats a bare array value as an 'in' filter", async () => {
|
||||
expect(await filterFor({ tier: ["gold", "silver"] })).toEqual([
|
||||
"tier",
|
||||
"In",
|
||||
["gold", "silver"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps scalar equality working", async () => {
|
||||
expect(await filterFor({ user_id: "u1" })).toEqual(["user_id", "Eq", "u1"]);
|
||||
});
|
||||
|
||||
it("skips a '*' wildcard value instead of matching it literally", async () => {
|
||||
// Only the real agent_id clause survives; user_id: "*" contributes nothing.
|
||||
expect(await filterFor({ user_id: "*", agent_id: "a1" })).toEqual([
|
||||
"agent_id",
|
||||
"Eq",
|
||||
"a1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws on an unsupported operator rather than silently dropping it", async () => {
|
||||
await expect(
|
||||
makeStore().search([0.1, 0.2, 0.3], 5, { name: { startsWith: "a" } }),
|
||||
).rejects.toThrow(/Unsupported Turbopuffer filter operator 'startsWith'/);
|
||||
});
|
||||
});
|
||||
@@ -579,20 +579,11 @@ class AWSBedrockLLM(LLMBase):
|
||||
# Use converse API for Anthropic models
|
||||
response = self.client.converse(**converse_params)
|
||||
|
||||
# Parse Converse API response. Claude reasoning models can emit a
|
||||
# `reasoningContent` block before the `text` block, so iterate to
|
||||
# find the first block that carries text instead of indexing
|
||||
# content[0] (same approach as the MiniMax branch below).
|
||||
# Parse Converse API response
|
||||
if hasattr(response, 'output') and hasattr(response.output, 'message'):
|
||||
for block in response.output.message.content:
|
||||
if hasattr(block, 'text'):
|
||||
return block.text
|
||||
return ""
|
||||
return response.output.message.content[0].text
|
||||
elif 'output' in response and 'message' in response['output']:
|
||||
for block in response['output']['message']['content']:
|
||||
if 'text' in block:
|
||||
return block['text']
|
||||
return ""
|
||||
return response['output']['message']['content'][0]['text']
|
||||
else:
|
||||
return str(response)
|
||||
|
||||
|
||||
@@ -69,16 +69,6 @@ class S3Vectors(VectorStoreBase):
|
||||
else:
|
||||
raise
|
||||
|
||||
def _distance_to_score(self, raw_distance: Optional[float]) -> Optional[float]:
|
||||
if raw_distance is None:
|
||||
return None
|
||||
# Euclidean distance is unbounded, so 1 - distance would collapse most
|
||||
# scores to 0. Use a bounded monotonic map instead. Cosine distance is
|
||||
# in [0, 2], where 1 - distance is already a valid similarity.
|
||||
if self.distance_metric == "euclidean":
|
||||
return 1.0 / (1.0 + raw_distance)
|
||||
return max(0.0, 1.0 - raw_distance)
|
||||
|
||||
def _parse_output(self, vectors: List[Dict]) -> List[OutputData]:
|
||||
results = []
|
||||
for v in vectors:
|
||||
@@ -91,7 +81,7 @@ class S3Vectors(VectorStoreBase):
|
||||
logger.warning(f"Failed to parse metadata for key {v.get('key')}")
|
||||
payload = {}
|
||||
raw_distance = v.get("distance")
|
||||
score = self._distance_to_score(raw_distance)
|
||||
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
|
||||
results.append(OutputData(id=v.get("key"), score=score, payload=payload))
|
||||
return results
|
||||
|
||||
|
||||
@@ -122,14 +122,7 @@ class TurbopufferDB(VectorStoreBase):
|
||||
dist = row_dict.pop("$dist", None)
|
||||
row_dict.pop("vector", None)
|
||||
|
||||
if dist is None:
|
||||
score = None
|
||||
elif self.distance_metric == "euclidean_squared":
|
||||
# $dist is unbounded squared-L2 (lower = closer); map to a bounded
|
||||
# higher-is-better score, mirroring milvus/baidu. Cosine returns 1 - dist.
|
||||
score = 1.0 / (1.0 + dist)
|
||||
else:
|
||||
score = 1 - dist
|
||||
score = 1 - dist if dist is not None else None
|
||||
|
||||
results.append(OutputData(
|
||||
id=row_id,
|
||||
@@ -138,18 +131,6 @@ class TurbopufferDB(VectorStoreBase):
|
||||
))
|
||||
return results
|
||||
|
||||
# Maps mem0 filter operators to their Turbopuffer equivalents.
|
||||
OPERATOR_MAP = {
|
||||
"eq": "Eq",
|
||||
"ne": "NotEq",
|
||||
"gt": "Gt",
|
||||
"gte": "Gte",
|
||||
"lt": "Lt",
|
||||
"lte": "Lte",
|
||||
"in": "In",
|
||||
"nin": "NotIn",
|
||||
}
|
||||
|
||||
def _convert_filters(self, filters: Optional[Dict]):
|
||||
"""
|
||||
Convert mem0 filters to Turbopuffer filter format.
|
||||
@@ -162,14 +143,10 @@ class TurbopufferDB(VectorStoreBase):
|
||||
conditions = []
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, dict):
|
||||
for op, operand in value.items():
|
||||
tpuf_op = self.OPERATOR_MAP.get(op)
|
||||
if tpuf_op is None:
|
||||
raise ValueError(
|
||||
f"Unsupported filter operator '{op}' for field '{key}'. "
|
||||
f"Supported operators: {sorted(self.OPERATOR_MAP)}"
|
||||
)
|
||||
conditions.append((key, tpuf_op, operand))
|
||||
if "gte" in value:
|
||||
conditions.append((key, "Gte", value["gte"]))
|
||||
if "lte" in value:
|
||||
conditions.append((key, "Lte", value["lte"]))
|
||||
else:
|
||||
conditions.append((key, "Eq", value))
|
||||
|
||||
|
||||
+2
-10
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "2.2.1"
|
||||
version = "2.2.0"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "support@mem0.ai" }
|
||||
@@ -83,7 +83,7 @@ test = [
|
||||
dev = [
|
||||
"ruff==0.16.0",
|
||||
"isort>=5.13.2",
|
||||
"pre-commit>=3.5.0",
|
||||
"pytest>=8.2.2",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
@@ -111,7 +111,6 @@ features = [
|
||||
"vector-stores",
|
||||
"llms",
|
||||
"extras",
|
||||
"dev",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.dev_py_3_11]
|
||||
@@ -121,7 +120,6 @@ features = [
|
||||
"vector-stores",
|
||||
"llms",
|
||||
"extras",
|
||||
"dev",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.dev_py_3_12]
|
||||
@@ -131,12 +129,6 @@ features = [
|
||||
"vector-stores",
|
||||
"llms",
|
||||
"extras",
|
||||
"dev",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default]
|
||||
features = [
|
||||
"dev",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default.scripts]
|
||||
|
||||
@@ -506,51 +506,3 @@ class TestParseResponseLegacy:
|
||||
response = {"body": body}
|
||||
result = llm._parse_response(response, tools=None)
|
||||
assert result == "hello from ai21"
|
||||
|
||||
|
||||
class TestAnthropicConverseContentParsing:
|
||||
"""The Anthropic Converse branch must not assume content[0] is the text
|
||||
block: Claude reasoning models emit a reasoningContent block before the
|
||||
text block, and some stop conditions produce an empty content array. The
|
||||
parser iterates for the first block carrying text, like the MiniMax branch.
|
||||
"""
|
||||
|
||||
def test_text_after_reasoning_content_block(self, mock_boto3):
|
||||
mock_boto3.converse.return_value = {
|
||||
"output": {
|
||||
"message": {
|
||||
"content": [
|
||||
{"reasoningContent": {"reasoningText": {"text": "step by step..."}}},
|
||||
{"text": "final answer"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
|
||||
|
||||
assert llm.generate_response(MESSAGES) == "final answer"
|
||||
|
||||
def test_empty_content_returns_empty_string(self, mock_boto3):
|
||||
mock_boto3.converse.return_value = {"output": {"message": {"content": []}}}
|
||||
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
|
||||
|
||||
assert llm.generate_response(MESSAGES) == ""
|
||||
|
||||
def test_plain_text_content_still_returned(self, mock_boto3):
|
||||
mock_boto3.converse.return_value = _converse_response("plain answer")
|
||||
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
|
||||
|
||||
assert llm.generate_response(MESSAGES) == "plain answer"
|
||||
|
||||
def test_object_style_response_iterates_blocks(self, mock_boto3):
|
||||
# Defensive attr-style branch: object wrapper with a reasoning block first.
|
||||
from types import SimpleNamespace
|
||||
|
||||
reasoning_block = SimpleNamespace(reasoningContent={"reasoningText": {"text": "hmm"}})
|
||||
text_block = SimpleNamespace(text="object answer")
|
||||
mock_boto3.converse.return_value = SimpleNamespace(
|
||||
output=SimpleNamespace(message=SimpleNamespace(content=[reasoning_block, text_block]))
|
||||
)
|
||||
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
|
||||
|
||||
assert llm.generate_response(MESSAGES) == "object answer"
|
||||
|
||||
@@ -186,45 +186,6 @@ def test_search(mock_boto_client):
|
||||
assert results[0].score == pytest.approx(0.1)
|
||||
|
||||
|
||||
def test_search_score_cosine_uses_one_minus_distance(mock_boto_client):
|
||||
"""Cosine metric keeps the 1 - distance similarity mapping."""
|
||||
mock_boto_client.query_vectors.return_value = {"vectors": [{"key": "id1", "distance": 0.25, "metadata": {}}]}
|
||||
store = S3Vectors(
|
||||
vector_bucket_name=BUCKET_NAME,
|
||||
collection_name=INDEX_NAME,
|
||||
embedding_model_dims=EMBEDDING_DIMS,
|
||||
distance_metric="cosine",
|
||||
)
|
||||
|
||||
results = store.search(query="test", vectors=[0.1, 0.2], top_k=1)
|
||||
|
||||
assert results[0].score == pytest.approx(0.75)
|
||||
|
||||
|
||||
def test_search_score_euclidean_uses_bounded_map(mock_boto_client):
|
||||
"""Euclidean distance is unbounded; scores must not collapse to 0."""
|
||||
mock_boto_client.query_vectors.return_value = {
|
||||
"vectors": [
|
||||
{"key": "near", "distance": 0.5, "metadata": {}},
|
||||
{"key": "far", "distance": 4.0, "metadata": {}},
|
||||
]
|
||||
}
|
||||
store = S3Vectors(
|
||||
vector_bucket_name=BUCKET_NAME,
|
||||
collection_name=INDEX_NAME,
|
||||
embedding_model_dims=EMBEDDING_DIMS,
|
||||
distance_metric="euclidean",
|
||||
)
|
||||
|
||||
results = store.search(query="test", vectors=[0.1, 0.2], top_k=2)
|
||||
|
||||
# 1 / (1 + d): a distance > 1 would give a negative (clamped 0) score under
|
||||
# the old cosine-only formula; the bounded map keeps ranking intact.
|
||||
assert results[0].score == pytest.approx(1.0 / 1.5)
|
||||
assert results[1].score == pytest.approx(1.0 / 5.0)
|
||||
assert results[0].score > results[1].score > 0.0
|
||||
|
||||
|
||||
def test_get(mock_boto_client):
|
||||
"""Test retrieving a vector by ID."""
|
||||
mock_boto_client.get_vectors.return_value = {
|
||||
|
||||
@@ -232,35 +232,6 @@ class TestParseOutput:
|
||||
def test_parse_empty_rows(self, db):
|
||||
assert db._parse_output([]) == []
|
||||
|
||||
def test_parse_cosine_score_is_one_minus_dist(self, db):
|
||||
# Default cosine metric: score = 1 - dist, unchanged by the metric fix.
|
||||
results = db._parse_output([_make_row("id1", dist=0.25)])
|
||||
assert results[0].score == pytest.approx(0.75)
|
||||
|
||||
def test_parse_euclidean_squared_score_is_bounded(self, mock_client):
|
||||
# euclidean_squared $dist is unbounded (e.g. 4.0). 1 - dist would give -3.0,
|
||||
# violating the higher-is-better contract; map it to 1/(1+dist) instead.
|
||||
db = TurbopufferDB(
|
||||
collection_name="test_ns",
|
||||
embedding_model_dims=4,
|
||||
api_key="tpuf_test_key",
|
||||
region="gcp-us-central1",
|
||||
distance_metric="euclidean_squared",
|
||||
)
|
||||
results = db._parse_output([_make_row("id1", dist=4.0)])
|
||||
assert results[0].score == pytest.approx(0.2)
|
||||
assert 0.0 <= results[0].score <= 1.0
|
||||
|
||||
def test_parse_euclidean_squared_preserves_none(self, mock_client):
|
||||
db = TurbopufferDB(
|
||||
collection_name="test_ns",
|
||||
embedding_model_dims=4,
|
||||
api_key="tpuf_test_key",
|
||||
region="gcp-us-central1",
|
||||
distance_metric="euclidean_squared",
|
||||
)
|
||||
assert db._parse_output([_make_row("id1")])[0].score is None
|
||||
|
||||
|
||||
# ── _convert_filters ─────────────────────────────────────────────────
|
||||
|
||||
@@ -305,40 +276,6 @@ class TestConvertFilters:
|
||||
assert ("user_id", "Eq", "u1") in conditions
|
||||
assert ("score", "Gte", 0.5) in conditions
|
||||
|
||||
def test_gt_operator_not_dropped(self, db):
|
||||
"""Regression: {"gt": ...} was silently dropped, returning unfiltered results."""
|
||||
result = db._convert_filters({"age": {"gt": 18}})
|
||||
assert result == ("age", "Gt", 18)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"op,expected_token",
|
||||
[
|
||||
("eq", "Eq"),
|
||||
("ne", "NotEq"),
|
||||
("gt", "Gt"),
|
||||
("gte", "Gte"),
|
||||
("lt", "Lt"),
|
||||
("lte", "Lte"),
|
||||
("in", "In"),
|
||||
("nin", "NotIn"),
|
||||
],
|
||||
)
|
||||
def test_all_operators_mapped(self, db, op, expected_token):
|
||||
operand = [1, 2] if op in ("in", "nin") else 5
|
||||
result = db._convert_filters({"age": {op: operand}})
|
||||
assert result == ("age", expected_token, operand)
|
||||
|
||||
def test_multiple_operators_on_one_field(self, db):
|
||||
result = db._convert_filters({"age": {"gt": 18, "lt": 65}})
|
||||
assert result[0] == "And"
|
||||
conditions = result[1]
|
||||
assert ("age", "Gt", 18) in conditions
|
||||
assert ("age", "Lt", 65) in conditions
|
||||
|
||||
def test_unknown_operator_raises(self, db):
|
||||
with pytest.raises(ValueError, match="Unsupported filter operator"):
|
||||
db._convert_filters({"age": {"between": [1, 2]}})
|
||||
|
||||
|
||||
# ── search ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user