diff --git a/docs/components/vectordbs/dbs/s3_vectors.mdx b/docs/components/vectordbs/dbs/s3_vectors.mdx index 3bf37e9dd..442971a26 100644 --- a/docs/components/vectordbs/dbs/s3_vectors.mdx +++ b/docs/components/vectordbs/dbs/s3_vectors.mdx @@ -9,15 +9,22 @@ description: "Use Amazon S3 Vectors as a cost-optimized vector storage service i S3 Vectors support requires additional dependencies. Install them with: -```bash + +```bash Python pip install boto3 ``` +```bash TypeScript +npm install @aws-sdk/client-s3vectors +``` + + ### Usage To use Amazon S3 Vectors with Mem0, you need to have an AWS account and the necessary IAM permissions (`s3vectors:*`). Ensure your environment is configured with AWS credentials (e.g., via `~/.aws/credentials` or environment variables). -```python + +```python Python import os from mem0 import Memory @@ -47,6 +54,36 @@ messages = [ m.add(messages, user_id="alice", metadata={"category": "movies"}) ``` +```typescript TypeScript +import { Memory } from 'mem0ai/oss'; + +// Ensure your AWS credentials are configured in your environment +// e.g., by setting AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_DEFAULT_REGION + +const config = { + vectorStore: { + provider: 's3_vectors', + config: { + vectorBucketName: 'my-mem0-vector-bucket', + collectionName: 'my-memories-index', + embeddingModelDims: 1536, + distanceMetric: 'cosine', + region: 'us-east-1', + }, + }, +}; + +const memory = new Memory(config); +const messages = [ + {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, + {"role": "assistant", "content": "How about a thriller movie? They can be quite engaging."}, + {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, + {"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."} +] +await memory.add(messages, { userId: "alice", metadata: { category: "movies" } }); +``` + + ### Config Here are the parameters available for configuring Amazon S3 Vectors: diff --git a/docs/components/vectordbs/overview.mdx b/docs/components/vectordbs/overview.mdx index 6b0b86f6b..cc00af3ef 100644 --- a/docs/components/vectordbs/overview.mdx +++ b/docs/components/vectordbs/overview.mdx @@ -10,7 +10,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize See the list of supported vector databases below. - The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, and an in-memory store. + The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, Amazon S3 Vectors, and an in-memory store. diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 4c44cbfbd..f69438052 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -107,6 +107,7 @@ }, "peerDependencies": { "@anthropic-ai/sdk": "^0.40.1", + "@aws-sdk/client-s3vectors": "3.967.0", "@azure/identity": "^4.0.0", "@azure/search-documents": "^12.0.0", "@cloudflare/workers-types": "^4.20250504.0", diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml index a2c094030..2e26095ad 100644 --- a/mem0-ts/pnpm-lock.yaml +++ b/mem0-ts/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@anthropic-ai/sdk': specifier: ^0.40.1 version: 0.40.1 + '@aws-sdk/client-s3vectors': + specifier: 3.967.0 + version: 3.967.0 '@azure/identity': specifier: ^4.0.0 version: 4.13.1 @@ -144,6 +147,131 @@ packages: '@anthropic-ai/sdk@0.40.1': resolution: {integrity: sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3vectors@3.967.0': + resolution: {integrity: sha512-DDmq8Jm/4IwliVEYVUbTub0RhcYJXVexRklhsIOdIWM42zf4ZMWPeeNvHJBSI+DMNbpqRN57TLCfH5tgiPdyVQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.967.0': + resolution: {integrity: sha512-7RgUwHcRMJtWme6kCHGUVT+Rn9GmNH+FHm34N9UgMXzUqQlzFMweE7T5E9O8nv3wIp7xFNB20ADaCw9Xdnox1Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.967.0': + resolution: {integrity: sha512-sJmuP7GrVmlbO6DpXkuf9Mbn6jGNNvy6PLawvaxVF150c8bpNk3w39rerRls6q1dot1dBFV2D29hBXMY1agNMg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.967.0': + resolution: {integrity: sha512-+XWw0+f/txeMbEVRtTFZhgSw1ymH1ffaVKkdMBSnw48rfSohJElKmitCqdihagRTZpzh7m8qI6tIQ5t3OUqugw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.967.0': + resolution: {integrity: sha512-0/GIAEv5pY5htg6IBMuYccBgzz3oS2DqHjHi396ziTrwlhbrCNX96AbNhQhzAx3LBZUk13sPfeapjyQ7G57Ekg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.967.0': + resolution: {integrity: sha512-U8dMpaM6Qf6+2Qvp1uG6OcWv1RlrZW7tQkpmzEVWH8HZTGrVHIXXju64NMtIOr7yOnNwd0CKcytuD1QG+phCwQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.967.0': + resolution: {integrity: sha512-kbvZsZL6CBlfnb71zuJdJmBUFZN5utNrcziZr/DZ2olEOkA9vlmizE8i9BUIbmS7ptjgvRnmcY1A966yfhiblw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.967.0': + resolution: {integrity: sha512-WuNbHs9rfKKSVok4+OBrZf0AHfzDgFYYMxN2G/q6ZfUmY4QmiPyxV5HkNFh1rqDxS9VV6kAZPo0EBmry10idSg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.967.0': + resolution: {integrity: sha512-sNCY5JDV0whsfsZ6c2+6eUwH33H7UhKbqvCPbEYlIIa8wkGjCtCyFI3zZIJHVcMKJJ3117vSUFHEkNA7g+8rtw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.967.0': + resolution: {integrity: sha512-0K6kITKNytFjk1UYabYUsTThgU6TQkyW6Wmt8S5zd1A/up7NSQGpp58Rpg9GIf4amQDQwb+p9FGG7emmV8FEeA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.967.0': + resolution: {integrity: sha512-Vkr7S2ec7q/v8i/MzkHcBEdqqfWz3lyb8FDjb+NjslEwdxC3f6XwADRZzWwV1pChfx6SbsvJXKfkcF/pKAelhA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.965.0': + resolution: {integrity: sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.965.0': + resolution: {integrity: sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.965.0': + resolution: {integrity: sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.967.0': + resolution: {integrity: sha512-2qzJzZj5u+cZiG7kz3XJPaTH4ssUY/aet1kwJsUTFKrWeHUf7mZZkDFfkXP5cOffgiOyR5ZkrmJoLKAde9hshg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.967.0': + resolution: {integrity: sha512-PYa7V8w0gaNux6Sz/Z7zrHmPloEE+EKpRxQIOG/D0askTr5Yd4oO2KGgcInf65uHK3f0Z9U4CTUGHZvQvABypA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.965.0': + resolution: {integrity: sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.967.0': + resolution: {integrity: sha512-Qnd/nJ0CgeUa7zQczgmdQm0vYUF7pD1G0C+dR1T7huHQHRIsgCWIsCV9wNKzOFluqtcr6YAeuTwvY0+l8XWxnA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.965.0': + resolution: {integrity: sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.965.0': + resolution: {integrity: sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.965.0': + resolution: {integrity: sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==} + + '@aws-sdk/util-user-agent-node@3.967.0': + resolution: {integrity: sha512-yUz6pCGxyG4+QaDg0dkdIBphjQp8A9rrbZa/+U3RJgRrW47hy64clFQUROzj5Poy1Ur8ICVXEUpBsSqRuYEU2g==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.965.0': + resolution: {integrity: sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -905,6 +1033,138 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/config-resolver@4.6.2': + resolution: {integrity: sha512-kUY1m60FASy0ijf+0vEuZ7v+WqFlCAyyhvLkO/A15wY0hK2BLEN9Wszf/Uc3PDJWlqnrwRf+lNXOmzyds3Eflw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.4.2': + resolution: {integrity: sha512-M/8PUgwzekn0GcMBjcrTXV4PGFIe1AKuMuHjkHNBfofCPQ9+9Nr35lCvRfQK9BwYHQM+LKiKj8GY0pXSwYm6qQ==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.4.2': + resolution: {integrity: sha512-GjmewKjtK4GQlWmKTIXABHTCW6z14kebGnW1P3CTvP9WWp0nmydkb4yhjkuIWlpXkAJfcAO3cEfRLeSATaEKSQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/middleware-content-length@4.4.2': + resolution: {integrity: sha512-yaZdk8sbKxDell/mN1OPwEwnZ0m7sTGkUK6OKlvWyCvPykAqojHEMjfLZlHdaMhIaJ2E4Jr2qMzLQHn9gp02DA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.6.2': + resolution: {integrity: sha512-uCYuVrw65beEpzQszGK7uiDK3Gk0HlzsZuN3JNjDM05/dwwEheg7gF4DKYWH8dpXK7c634GUfkH/vG5XuR9z2w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.7.2': + resolution: {integrity: sha512-QldB+7GcqKGVyjxTq5tpuHRFQKQJRnIETx8tdpEwYu5fHZR7z5+SM5eGd/TZqkUkV/6vUHgiiDVmmqjsobzPLg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.4.2': + resolution: {integrity: sha512-Nx6wuSMasSazyIC02cmmSF+jEGnBpVL6u/Ra9J+AoKQyX1Uwxqf96fSoD6G+D1SKqw51GVNC1V08GtJPe8Yhaw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.4.2': + resolution: {integrity: sha512-yWvlZxqgmk0ZiRd0QVzSAqvdkREeiw2boqT+DTbyi03ylx3LyYGs5hNvRjks5ay0cDotZ7H0AdG2tJDMy+IxgA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.5.2': + resolution: {integrity: sha512-kGnGhU7v/VSlKjcUHUNhMQ1eDiGKjDF6OlGlOWcilgsyR6Yyq4sDbEV6vs1RD8+xqj1OPpCYt817KAM44ymweQ==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.4.2': + resolution: {integrity: sha512-V74/UQ+fPPIqZHzeXIUd0H+k/MF+WpeO3I+5wrMCMgsllLt6uqSNbWnLhJs6pMeXZGu6ry+g1bpIdcHNeL+Oww==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.5.2': + resolution: {integrity: sha512-ZCQGk/UNS+YF5D+Jmhr8+PQX2PJKe4QzR6zuOR5sUinxO/ZiKNA7nyrbeO/Ds2hXLts3fZAtgLPKLJgvjCX9uQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.6.2': + resolution: {integrity: sha512-9Xti1yhj7Em9t4ZQ1E8YRA6wbhAqx1nfLu0XSAqv82HED43TMUvn3nOO2xBISxZc0hcOg7wvuHEypUy4L4XF2w==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.14.2': + resolution: {integrity: sha512-H3fp0F/IhG8a6qnoWHeGTH1qDPRR9VVZjcxM9NMXPB6nqzaqcoYIX8D4FFI1Ap2siGr+yvYgKqHC76+afBgF1A==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.4.2': + resolution: {integrity: sha512-vx7FUYJMRfQqdqPRLYwpisOuYU6e1PaabcPUTEc7xG6X0VEr4B6PnjZloHz67qva0pQ9/eNs3xPxvQ6w5gRGAw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.5.2': + resolution: {integrity: sha512-OQKnJft8J4KPJyL4K3w3XpOvstpZUbPdcs840dH+JTp2w+v8Lce8nxzUoeOiAIRIhnWFQd7FxAjkZ0OkpX6jzw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.4.2': + resolution: {integrity: sha512-PTdWf5ApVEJQvnCaAJJPFHkJcUUY5yImYEcfzPqFP8ytet2oI0P/2nCF2t0600MkAH/MC9ne8SvgcRKh8Ooqhw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.4.2': + resolution: {integrity: sha512-KaT3bRb0khPrjXsXzyAi47F5aKOqBLypDhhvVC7p65AoZCPPAQ4j8KB1oaK+TKf6k5XFgkAY4gMK1EFr7xh5Wg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-defaults-mode-browser@4.5.2': + resolution: {integrity: sha512-VexZx4D0vhOeXNLpmxgXK0s9CNV/jCSfjBDWS7YNjZ47K87rTzmgovr8s0CgOLASIkCAS4aAbTqIG4YzU6zduA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.4.2': + resolution: {integrity: sha512-nhmz4D2s2FGFTgWf8M2bQHCLKjJbCQBS91waDofexbptBESTExtpEgiaI0rWIP4MhAarCCdFapHdvlCoknBE6w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.6.2': + resolution: {integrity: sha512-K50/sbl8HRnBJt5+UYvIG8HCdEDNA9rhCmkWTDftzGMlJ4XlPgtqI1V6jTSi9tmK7MLfgw7CfGvzSKFSWOteOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.4.2': + resolution: {integrity: sha512-Xk039bCOxEZfasaHSHPj0QS8EY2npd0z3nxLkDvczg9ueF7eboiKw0HEa5pILLwjb+sjSlgzSjoCOVAM715TzA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.5.2': + resolution: {integrity: sha512-iinqHq7WnWWKQUCObyu4YBzXROJO8dHMQrM+pvut92rKgjhcLNHel8QVJNsIswjPLOEJffq6sYjOdq/ndOrIAA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.7.2': + resolution: {integrity: sha512-/JZLMlF/pzY7QIA5M70bKSq5Bz/JpGLHiLlttvrcZpfLyGSCR3BvIebbipmCvSsnLXnU2pGDsDgD513N1/GGVg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.4.2': + resolution: {integrity: sha512-N5CpfaL+/LPQU9PFdOT55ayUo5T0QypG4Almzd1/efJvoDypuT1shkgJk1+hhg/02scYluW6Q2JGnSHIPwCEGQ==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1082,6 +1342,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + apparatus@0.0.10: resolution: {integrity: sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==} engines: {node: '>=0.2.6'} @@ -1158,6 +1421,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} @@ -1503,6 +1769,10 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2741,6 +3011,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3080,6 +3353,384 @@ snapshots: transitivePeerDependencies: - encoding + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3vectors@3.967.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.967.0 + '@aws-sdk/credential-provider-node': 3.967.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.967.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.967.0 + '@smithy/config-resolver': 4.6.2 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/hash-node': 4.4.2 + '@smithy/invalid-dependency': 4.4.2 + '@smithy/middleware-content-length': 4.4.2 + '@smithy/middleware-endpoint': 4.6.2 + '@smithy/middleware-retry': 4.7.2 + '@smithy/middleware-serde': 4.4.2 + '@smithy/middleware-stack': 4.4.2 + '@smithy/node-config-provider': 4.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/smithy-client': 4.14.2 + '@smithy/types': 4.15.0 + '@smithy/url-parser': 4.4.2 + '@smithy/util-base64': 4.5.2 + '@smithy/util-body-length-browser': 4.4.2 + '@smithy/util-body-length-node': 4.4.2 + '@smithy/util-defaults-mode-browser': 4.5.2 + '@smithy/util-defaults-mode-node': 4.4.2 + '@smithy/util-endpoints': 3.6.2 + '@smithy/util-middleware': 4.4.2 + '@smithy/util-retry': 4.5.2 + '@smithy/util-utf8': 4.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.967.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.967.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.967.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.967.0 + '@smithy/config-resolver': 4.6.2 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/hash-node': 4.4.2 + '@smithy/invalid-dependency': 4.4.2 + '@smithy/middleware-content-length': 4.4.2 + '@smithy/middleware-endpoint': 4.6.2 + '@smithy/middleware-retry': 4.7.2 + '@smithy/middleware-serde': 4.4.2 + '@smithy/middleware-stack': 4.4.2 + '@smithy/node-config-provider': 4.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/smithy-client': 4.14.2 + '@smithy/types': 4.15.0 + '@smithy/url-parser': 4.4.2 + '@smithy/util-base64': 4.5.2 + '@smithy/util-body-length-browser': 4.4.2 + '@smithy/util-body-length-node': 4.4.2 + '@smithy/util-defaults-mode-browser': 4.5.2 + '@smithy/util-defaults-mode-node': 4.4.2 + '@smithy/util-endpoints': 3.6.2 + '@smithy/util-middleware': 4.4.2 + '@smithy/util-retry': 4.5.2 + '@smithy/util-utf8': 4.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.967.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@aws-sdk/xml-builder': 3.965.0 + '@smithy/core': 3.26.0 + '@smithy/node-config-provider': 4.5.2 + '@smithy/property-provider': 4.4.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/signature-v4': 5.5.2 + '@smithy/smithy-client': 4.14.2 + '@smithy/types': 4.15.0 + '@smithy/util-base64': 4.5.2 + '@smithy/util-middleware': 4.4.2 + '@smithy/util-utf8': 4.4.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/property-provider': 4.4.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/smithy-client': 4.14.2 + '@smithy/types': 4.15.0 + '@smithy/util-stream': 4.7.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/credential-provider-env': 3.967.0 + '@aws-sdk/credential-provider-http': 3.967.0 + '@aws-sdk/credential-provider-login': 3.967.0 + '@aws-sdk/credential-provider-process': 3.967.0 + '@aws-sdk/credential-provider-sso': 3.967.0 + '@aws-sdk/credential-provider-web-identity': 3.967.0 + '@aws-sdk/nested-clients': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/nested-clients': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.967.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.967.0 + '@aws-sdk/credential-provider-http': 3.967.0 + '@aws-sdk/credential-provider-ini': 3.967.0 + '@aws-sdk/credential-provider-process': 3.967.0 + '@aws-sdk/credential-provider-sso': 3.967.0 + '@aws-sdk/credential-provider-web-identity': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.967.0': + dependencies: + '@aws-sdk/client-sso': 3.967.0 + '@aws-sdk/core': 3.967.0 + '@aws-sdk/token-providers': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/nested-clients': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/protocol-http': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@smithy/core': 3.26.0 + '@smithy/protocol-http': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.967.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.967.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.967.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.967.0 + '@smithy/config-resolver': 4.6.2 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/hash-node': 4.4.2 + '@smithy/invalid-dependency': 4.4.2 + '@smithy/middleware-content-length': 4.4.2 + '@smithy/middleware-endpoint': 4.6.2 + '@smithy/middleware-retry': 4.7.2 + '@smithy/middleware-serde': 4.4.2 + '@smithy/middleware-stack': 4.4.2 + '@smithy/node-config-provider': 4.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/protocol-http': 5.5.2 + '@smithy/smithy-client': 4.14.2 + '@smithy/types': 4.15.0 + '@smithy/url-parser': 4.4.2 + '@smithy/util-base64': 4.5.2 + '@smithy/util-body-length-browser': 4.4.2 + '@smithy/util-body-length-node': 4.4.2 + '@smithy/util-defaults-mode-browser': 4.5.2 + '@smithy/util-defaults-mode-node': 4.4.2 + '@smithy/util-endpoints': 3.6.2 + '@smithy/util-middleware': 4.4.2 + '@smithy/util-retry': 4.5.2 + '@smithy/util-utf8': 4.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/config-resolver': 4.6.2 + '@smithy/node-config-provider': 4.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.967.0': + dependencies: + '@aws-sdk/core': 3.967.0 + '@aws-sdk/nested-clients': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.4.2 + '@smithy/shared-ini-file-loader': 4.6.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.965.0': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.15.0 + '@smithy/url-parser': 4.4.2 + '@smithy/util-endpoints': 3.6.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.967.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.967.0 + '@aws-sdk/types': 3.965.0 + '@smithy/node-config-provider': 4.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.965.0': + dependencies: + '@smithy/types': 4.15.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -3883,6 +4534,175 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/config-resolver@4.6.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/core@3.26.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.6.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.7.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.5.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/shared-ini-file-loader@4.6.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.14.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.5.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.5.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.6.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-middleware@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.5.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.7.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@supabase/auth-js@2.108.1': @@ -4067,6 +4887,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + anynum@1.0.1: {} + apparatus@0.0.10: dependencies: sylvester: 0.0.21 @@ -4171,6 +4993,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bowser@2.14.1: {} + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 @@ -4513,6 +5337,10 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.4.1 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5937,6 +6765,10 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 diff --git a/mem0-ts/src/oss/src/index.ts b/mem0-ts/src/oss/src/index.ts index 141edcacf..290872800 100644 --- a/mem0-ts/src/oss/src/index.ts +++ b/mem0-ts/src/oss/src/index.ts @@ -28,4 +28,5 @@ export * from "./vector_stores/langchain"; export * from "./vector_stores/vectorize"; export * from "./vector_stores/azure_ai_search"; export * from "./vector_stores/pgvector"; +export * from "./vector_stores/s3_vectors"; export * from "./utils/factory"; diff --git a/mem0-ts/src/oss/src/utils/factory.ts b/mem0-ts/src/oss/src/utils/factory.ts index 09a2db2db..228a6c418 100644 --- a/mem0-ts/src/oss/src/utils/factory.ts +++ b/mem0-ts/src/oss/src/utils/factory.ts @@ -38,6 +38,7 @@ import { LangchainEmbedder } from "../embeddings/langchain"; import { LangchainVectorStore } from "../vector_stores/langchain"; import { AzureAISearch } from "../vector_stores/azure_ai_search"; import { PGVector } from "../vector_stores/pgvector"; +import { S3Vectors } from "../vector_stores/s3_vectors"; export class EmbedderFactory { static create(provider: string, config: EmbeddingConfig): Embedder { @@ -116,6 +117,9 @@ export class VectorStoreFactory { return new AzureAISearch(config as any); case "pgvector": return new PGVector(config as any); + case "s3-vectors": + case "s3_vectors": + return new S3Vectors(config as any); default: throw new Error(`Unsupported vector store provider: ${provider}`); } diff --git a/mem0-ts/src/oss/src/vector_stores/s3_vectors.ts b/mem0-ts/src/oss/src/vector_stores/s3_vectors.ts new file mode 100644 index 000000000..cc2edf6b5 --- /dev/null +++ b/mem0-ts/src/oss/src/vector_stores/s3_vectors.ts @@ -0,0 +1,841 @@ +import { + CreateIndexCommand, + CreateVectorBucketCommand, + DeleteIndexCommand, + DeleteVectorsCommand, + GetIndexCommand, + GetVectorBucketCommand, + GetVectorsCommand, + ListVectorsCommand, + PutVectorsCommand, + QueryVectorsCommand, + S3VectorsClient, + type DistanceMetric, + type GetOutputVector, + type ListOutputVector, + type QueryOutputVector, + type S3VectorsClientConfig, + type VectorData, +} from "@aws-sdk/client-s3vectors"; +import { VectorStore } from "./base"; +import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; + +const MIGRATION_INDEX_NAME = "memory_migrations"; +const MIGRATION_VECTOR_KEY = "mem0-user"; +const DEFAULT_PAGE_SIZE = 500; +const ALWAYS_FALSE_FILTER_KEY = "$alwaysFalse"; + +type S3Filter = Record; + +interface S3VectorsConfig extends VectorStoreConfig { + vectorBucketName: string; + collectionName: string; + embeddingModelDims?: number; + dimension?: number; + distanceMetric?: DistanceMetric | "cosine" | "euclidean"; + region?: string; + regionName?: string; + client?: S3VectorsClientLike; + clientConfig?: S3VectorsClientConfig; +} + +interface S3VectorsClientLike { + send(command: any): Promise; +} + +export class S3Vectors implements VectorStore { + private readonly client: S3VectorsClientLike; + private readonly vectorBucketName: string; + private readonly collectionName: string; + private readonly dimension: number; + private readonly distanceMetric: DistanceMetric | "cosine" | "euclidean"; + private _initPromise?: Promise; + private cachedUserId?: string; + + constructor(config: S3VectorsConfig) { + if (!config.vectorBucketName) { + throw new Error("vectorBucketName is required"); + } + if (!config.collectionName) { + throw new Error("collectionName is required"); + } + + const dimension = config.embeddingModelDims ?? config.dimension; + if (!dimension || dimension < 1) { + throw new Error("embeddingModelDims or dimension is required"); + } + + this.vectorBucketName = config.vectorBucketName; + this.collectionName = config.collectionName; + this.dimension = dimension; + this.distanceMetric = config.distanceMetric || "cosine"; + this.client = + config.client || + new S3VectorsClient({ + ...(config.clientConfig || {}), + ...(config.region || config.regionName + ? { region: config.region || config.regionName } + : {}), + }); + + void this.initialize().catch(console.error); + } + + async initialize(): Promise { + if (!this._initPromise) { + this._initPromise = this._doInitialize(); + } + return this._initPromise; + } + + private async _doInitialize(): Promise { + await this.ensureBucketExists(); + await this.ensureIndexExists( + this.collectionName, + this.dimension, + this.distanceMetric, + ); + } + + async insert( + vectors: number[][], + ids: string[], + payloads: Record[], + ): Promise { + await this.initialize(); + this.assertBatchDimensions(vectors, "Insert"); + + await this.client.send( + new PutVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + vectors: vectors.map((vector, index) => ({ + key: ids[index], + data: this.toVectorData(vector), + metadata: payloads[index] || {}, + })), + }), + ); + } + + async keywordSearch(): Promise { + return null; + } + + async search( + query: number[], + topK: number = 5, + filters?: SearchFilters, + ): Promise { + await this.initialize(); + this.assertVectorDimension(query, "Query"); + + const filter = this.convertFilters(filters); + if (filter && this.isAlwaysFalseFilter(filter)) { + return []; + } + const response = await this.client.send( + new QueryVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + queryVector: this.toVectorData(query), + topK, + filter, + returnMetadata: true, + returnDistance: true, + }), + ); + + const responseDistanceMetric = + response.distanceMetric || this.distanceMetric; + return (response.vectors || []) + .map((vector: QueryOutputVector) => + this.normalizeQueryVector(vector, responseDistanceMetric), + ) + .slice(0, topK); + } + + async get(vectorId: string): Promise { + await this.initialize(); + + try { + const response = await this.client.send( + new GetVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + keys: [vectorId], + returnMetadata: true, + }), + ); + + const vector = response.vectors?.[0]; + if (!vector) { + return null; + } + + return this.normalizeStoredVector(vector); + } catch (error) { + if (this.isNotFound(error)) { + return null; + } + throw error; + } + } + + async update( + vectorId: string, + vector: number[], + payload: Record, + ): Promise { + await this.initialize(); + + let nextVector = vector; + let nextPayload = payload || {}; + + if (vector.length === 0 || !payload) { + const existing = await this.fetchStoredVector(vectorId); + if (!existing) { + throw new Error(`Vector with ID ${vectorId} not found`); + } + nextVector = vector.length > 0 ? vector : existing.vector; + nextPayload = payload || existing.payload; + } + + this.assertVectorDimension(nextVector, "Vector"); + + await this.client.send( + new PutVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + vectors: [ + { + key: vectorId, + data: this.toVectorData(nextVector), + metadata: nextPayload, + }, + ], + }), + ); + } + + async delete(vectorId: string): Promise { + await this.initialize(); + + await this.client.send( + new DeleteVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + keys: [vectorId], + }), + ); + } + + async deleteCol(): Promise { + await this.initialize(); + + try { + await this.client.send( + new DeleteIndexCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + }), + ); + } catch (error) { + if (this.isNotFound(error)) { + return; + } + throw error; + } + } + + async list( + filters?: SearchFilters, + topK: number = 100, + ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); + + const filter = this.convertFilters(filters); + const results: VectorStoreResult[] = []; + let nextToken: string | undefined; + + // Stop paginating once we have topK matches. Both callers of list() + // discard the count, so scanning the whole index just to total every + // match is wasted round-trips (O(index size) on getAll/deleteAll). + // Return the page length like qdrant does. + do { + const response = await this.client.send( + new ListVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + maxResults: DEFAULT_PAGE_SIZE, + nextToken, + returnMetadata: true, + }), + ); + + for (const vector of response.vectors || []) { + if (results.length >= topK) { + break; + } + const normalized = this.normalizeListedVector(vector); + if (filter && !this.matchesFilter(normalized.payload, filter)) { + continue; + } + results.push(normalized); + } + + nextToken = response.nextToken; + } while (nextToken && results.length < topK); + + return [results, results.length]; + } + + async getUserId(): Promise { + await this.initialize(); + if (this.cachedUserId) { + return this.cachedUserId; + } + + await this.ensureMigrationIndex(); + + const response = await this.client.send( + new GetVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: MIGRATION_INDEX_NAME, + keys: [MIGRATION_VECTOR_KEY], + returnMetadata: true, + }), + ); + + const metadata = this.normalizeMetadata(response.vectors?.[0]?.metadata); + const userId = metadata.user_id; + if (typeof userId === "string" && userId.length > 0) { + this.cachedUserId = userId; + return userId; + } + + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + await this.setUserId(randomUserId); + return randomUserId; + } + + async setUserId(userId: string): Promise { + await this.initialize(); + await this.ensureMigrationIndex(); + + await this.client.send( + new PutVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: MIGRATION_INDEX_NAME, + vectors: [ + { + key: MIGRATION_VECTOR_KEY, + data: this.toVectorData([0]), + metadata: { user_id: userId }, + }, + ], + }), + ); + + this.cachedUserId = userId; + } + + private async ensureBucketExists(): Promise { + try { + await this.client.send( + new GetVectorBucketCommand({ + vectorBucketName: this.vectorBucketName, + }), + ); + } catch (error) { + if (!this.isNotFound(error)) { + throw error; + } + + try { + await this.client.send( + new CreateVectorBucketCommand({ + vectorBucketName: this.vectorBucketName, + }), + ); + } catch (createError) { + if (!this.isConflict(createError)) { + throw createError; + } + } + } + } + + private async ensureIndexExists( + indexName: string, + dimension: number, + distanceMetric: DistanceMetric | "cosine" | "euclidean", + ): Promise { + try { + await this.client.send( + new GetIndexCommand({ + vectorBucketName: this.vectorBucketName, + indexName, + }), + ); + } catch (error) { + if (!this.isNotFound(error)) { + throw error; + } + + try { + await this.client.send( + new CreateIndexCommand({ + vectorBucketName: this.vectorBucketName, + indexName, + dataType: "float32", + dimension, + distanceMetric, + }), + ); + } catch (createError) { + if (!this.isConflict(createError)) { + throw createError; + } + } + } + } + + private async ensureMigrationIndex(): Promise { + await this.ensureIndexExists(MIGRATION_INDEX_NAME, 1, "euclidean"); + } + + private async fetchStoredVector( + vectorId: string, + ): Promise<{ vector: number[]; payload: Record } | null> { + const response = await this.client.send( + new GetVectorsCommand({ + vectorBucketName: this.vectorBucketName, + indexName: this.collectionName, + keys: [vectorId], + returnData: true, + returnMetadata: true, + }), + ); + + const vector = response.vectors?.[0]; + const data = this.fromVectorData(vector?.data); + if (!vector || !data) { + return null; + } + + return { + vector: data, + payload: this.normalizeMetadata(vector.metadata), + }; + } + + private convertFilters(filters?: SearchFilters): S3Filter | undefined { + if (!filters || Object.keys(filters).length === 0) { + return undefined; + } + const normalized = this.convertFilterNode(filters); + return this.isNoOpFilter(normalized) ? undefined : normalized; + } + + private convertFilterNode(node: Record): S3Filter { + const clauses: S3Filter[] = []; + + for (const [key, value] of Object.entries(node)) { + if (key === "$and" || key === "$or") { + if (!Array.isArray(value) || value.length === 0) { + throw new Error(`${key} filter requires a non-empty list`); + } + const normalizedEntries = value.map((entry) => + this.convertFilterNode(entry), + ); + const entries = normalizedEntries.filter( + (entry) => !this.isNoOpFilter(entry), + ); + if (key === "$and") { + if (entries.some((entry) => this.isAlwaysFalseFilter(entry))) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + if (entries.length === 0) { + continue; + } + } else { + if (normalizedEntries.some((entry) => this.isNoOpFilter(entry))) { + continue; + } + const viableEntries = entries.filter( + (entry) => !this.isAlwaysFalseFilter(entry), + ); + if (viableEntries.length === 0) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + clauses.push( + viableEntries.length === 1 + ? viableEntries[0] + : { [key]: viableEntries }, + ); + continue; + } + clauses.push(entries.length === 1 ? entries[0] : { [key]: entries }); + continue; + } + + if (key === "$not") { + if (!Array.isArray(value) || value.length === 0) { + throw new Error("$not filter requires a non-empty list"); + } + + const normalizedEntries = value.map((entry) => + this.convertFilterNode(entry), + ); + if (normalizedEntries.some((entry) => this.isNoOpFilter(entry))) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + const negated = normalizedEntries.filter( + (entry) => !this.isAlwaysFalseFilter(entry), + ); + if (negated.length === 0) { + continue; + } + const negatedFilters = negated.map((entry) => this.negateFilter(entry)); + clauses.push( + negatedFilters.length === 1 + ? negatedFilters[0] + : { $and: negatedFilters }, + ); + continue; + } + + const fieldFilter = this.convertFieldFilter(key, value); + if (this.isAlwaysFalseFilter(fieldFilter)) { + return fieldFilter; + } + if (this.isNoOpFilter(fieldFilter)) { + continue; + } + clauses.push(fieldFilter); + } + + if (clauses.some((entry) => this.isAlwaysFalseFilter(entry))) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + if (clauses.length === 1) { + return clauses[0]; + } + if (clauses.length === 0) { + return {}; + } + + return { $and: clauses }; + } + + private convertFieldFilter(key: string, value: any): S3Filter { + if (value === "*") { + return { [key]: { $exists: true } }; + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + return { [key]: { $in: value } }; + } + + if (typeof value !== "object" || value === null) { + return { [key]: { $eq: value } }; + } + + const operators: Record = {}; + + for (const [operator, operand] of Object.entries(value)) { + switch (operator) { + case "eq": + operators.$eq = operand; + break; + case "ne": + operators.$ne = operand; + break; + case "gt": + operators.$gt = operand; + break; + case "gte": + operators.$gte = operand; + break; + case "lt": + operators.$lt = operand; + break; + case "lte": + operators.$lte = operand; + break; + case "in": + if (!Array.isArray(operand) || operand.length === 0) { + return { [ALWAYS_FALSE_FILTER_KEY]: true }; + } + operators.$in = operand; + break; + case "nin": + if (!Array.isArray(operand) || operand.length === 0) { + break; + } + operators.$nin = operand; + break; + case "contains": + case "icontains": + case "startsWith": + throw new Error( + `S3 Vectors does not support '${operator}' metadata filters.`, + ); + default: + throw new Error( + `Unsupported S3 Vectors filter operator: ${operator}`, + ); + } + } + + if (Object.keys(operators).length === 0) { + return {}; + } + return { [key]: operators }; + } + + private negateFilter(filter: S3Filter): S3Filter { + if (Array.isArray(filter.$and)) { + return { + $or: filter.$and.map((entry: S3Filter) => this.negateFilter(entry)), + }; + } + + if (Array.isArray(filter.$or)) { + return { + $and: filter.$or.map((entry: S3Filter) => this.negateFilter(entry)), + }; + } + + const entries = Object.entries(filter); + if (entries.length !== 1) { + throw new Error("S3 Vectors cannot negate this filter shape."); + } + + const [key, rawValue] = entries[0]; + if (key.startsWith("$")) { + throw new Error("S3 Vectors cannot negate this filter shape."); + } + + const value = + typeof rawValue === "object" && rawValue !== null + ? rawValue + : { $eq: rawValue }; + const negated: Record = {}; + + for (const [operator, operand] of Object.entries(value)) { + switch (operator) { + case "$eq": + negated.$ne = operand; + break; + case "$ne": + negated.$eq = operand; + break; + case "$gt": + negated.$lte = operand; + break; + case "$gte": + negated.$lt = operand; + break; + case "$lt": + negated.$gte = operand; + break; + case "$lte": + negated.$gt = operand; + break; + case "$in": + negated.$nin = operand; + break; + case "$nin": + negated.$in = operand; + break; + case "$exists": + negated.$exists = !operand; + break; + default: + throw new Error("S3 Vectors cannot negate this filter shape."); + } + } + + return { [key]: negated }; + } + + private matchesFilter( + metadata: Record, + filter: S3Filter, + ): boolean { + if (this.isAlwaysFalseFilter(filter)) { + return false; + } + if (Array.isArray(filter.$and)) { + return filter.$and.every((entry: S3Filter) => + this.matchesFilter(metadata, entry), + ); + } + + if (Array.isArray(filter.$or)) { + return filter.$or.some((entry: S3Filter) => + this.matchesFilter(metadata, entry), + ); + } + + return Object.entries(filter).every(([key, rawValue]) => + this.matchesFieldFilter(metadata[key], rawValue), + ); + } + + private matchesFieldFilter(metadataValue: any, rawValue: any): boolean { + const value = + typeof rawValue === "object" && rawValue !== null + ? rawValue + : { $eq: rawValue }; + + return Object.entries(value).every(([operator, operand]) => + this.evaluateOperator(metadataValue, operator, operand), + ); + } + + private evaluateOperator( + metadataValue: any, + operator: string, + operand: any, + ): boolean { + switch (operator) { + case "$eq": + if (Array.isArray(metadataValue)) { + return metadataValue.some((entry) => entry === operand); + } + return metadataValue === operand; + case "$ne": + if (Array.isArray(metadataValue)) { + return !metadataValue.some((entry) => entry === operand); + } + return metadataValue !== operand; + case "$gt": + return typeof metadataValue === "number" && metadataValue > operand; + case "$gte": + return typeof metadataValue === "number" && metadataValue >= operand; + case "$lt": + return typeof metadataValue === "number" && metadataValue < operand; + case "$lte": + return typeof metadataValue === "number" && metadataValue <= operand; + case "$in": + if (!Array.isArray(operand) || operand.length === 0) { + return false; + } + if (Array.isArray(metadataValue)) { + return metadataValue.some((entry) => operand.includes(entry)); + } + return operand.includes(metadataValue); + case "$nin": + if (!Array.isArray(operand) || operand.length === 0) { + return true; + } + if (Array.isArray(metadataValue)) { + return metadataValue.every((entry) => !operand.includes(entry)); + } + return !operand.includes(metadataValue); + case "$exists": + return operand + ? metadataValue !== undefined + : metadataValue === undefined; + default: + throw new Error(`Unsupported S3 Vectors filter operator: ${operator}`); + } + } + + private toVectorData(vector: number[]): VectorData { + return { float32: vector }; + } + + private fromVectorData(data?: VectorData): number[] | undefined { + if (!data || !("float32" in data) || !Array.isArray(data.float32)) { + return undefined; + } + return data.float32; + } + + private normalizeMetadata(metadata: any): Record { + if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) { + return { ...(metadata as Record) }; + } + return {}; + } + + private normalizeQueryVector( + vector: QueryOutputVector, + distanceMetric: DistanceMetric | "cosine" | "euclidean", + ): VectorStoreResult { + return { + id: String(vector.key), + payload: this.normalizeMetadata(vector.metadata), + score: this.normalizeScore(vector.distance, distanceMetric), + }; + } + + private normalizeStoredVector( + vector: GetOutputVector | ListOutputVector, + ): VectorStoreResult { + return { + id: String(vector.key), + payload: this.normalizeMetadata(vector.metadata), + }; + } + + private normalizeListedVector(vector: ListOutputVector): VectorStoreResult { + return this.normalizeStoredVector(vector); + } + + private normalizeScore( + distance?: number, + distanceMetric: DistanceMetric | "cosine" | "euclidean" = this + .distanceMetric, + ): number | undefined { + if (distance === undefined || distance === null) { + return undefined; + } + if (!Number.isFinite(distance)) { + return undefined; + } + if (distanceMetric === "euclidean") { + return 1 / (1 + distance); + } + return Math.max(0, Math.min(1, 1 - distance)); + } + + private assertVectorDimension(vector: number[], context: string): void { + if (vector.length !== this.dimension) { + throw new Error( + `${context} dimension mismatch. Expected ${this.dimension}, got ${vector.length}`, + ); + } + } + + private assertBatchDimensions(vectors: number[][], context: string): void { + for (const vector of vectors) { + this.assertVectorDimension(vector, context); + } + } + + private isNotFound(error: any): boolean { + return error?.name === "NotFoundException"; + } + + private isConflict(error: any): boolean { + return error?.name === "ConflictException"; + } + + private isAlwaysFalseFilter(filter: S3Filter): boolean { + return filter[ALWAYS_FALSE_FILTER_KEY] === true; + } + + private isNoOpFilter(filter: S3Filter): boolean { + return Object.keys(filter).length === 0; + } +} diff --git a/mem0-ts/src/oss/tests/factory.unit.test.ts b/mem0-ts/src/oss/tests/factory.unit.test.ts index c1651abe9..99a735e7c 100644 --- a/mem0-ts/src/oss/tests/factory.unit.test.ts +++ b/mem0-ts/src/oss/tests/factory.unit.test.ts @@ -138,6 +138,11 @@ jest.mock("../src/vector_stores/pgvector", () => ({ .fn() .mockImplementation((config) => ({ type: "pgvector", config })), })); +jest.mock("../src/vector_stores/s3_vectors", () => ({ + S3Vectors: jest + .fn() + .mockImplementation((config) => ({ type: "s3-vectors", config })), +})); jest.mock("../src/storage/SupabaseHistoryManager", () => ({ SupabaseHistoryManager: jest .fn() @@ -260,6 +265,8 @@ describe("VectorStoreFactory", () => { ["vectorize"], ["azure-ai-search"], ["pgvector"], + ["s3-vectors"], + ["s3_vectors"], ])("creates vector store for provider '%s'", (provider) => { expect(() => VectorStoreFactory.create(provider, dummyVSConfig), diff --git a/mem0-ts/src/oss/tests/tsup-externals.test.ts b/mem0-ts/src/oss/tests/tsup-externals.test.ts index 9206aa27d..0512a4ee2 100644 --- a/mem0-ts/src/oss/tests/tsup-externals.test.ts +++ b/mem0-ts/src/oss/tests/tsup-externals.test.ts @@ -14,7 +14,6 @@ describe("tsup.config.ts externals", () => { beforeAll(() => { const pkgPath = path.resolve(__dirname, "../../../package.json"); const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); - // Filter out @types/* packages — they are type-only and not bundled at runtime peerDeps = Object.keys(pkg.peerDependencies || {}).filter( (dep) => !dep.startsWith("@types/"), ); @@ -22,16 +21,16 @@ describe("tsup.config.ts externals", () => { const tsupConfigPath = path.resolve(__dirname, "../../../tsup.config.ts"); const tsupContent = fs.readFileSync(tsupConfigPath, "utf-8"); - - // Extract strings from the external array (supports double, single, and backtick quotes) const externalMatch = tsupContent.match( /const external\s*=\s*\[([\s\S]*?)\];/, ); + if (!externalMatch) { throw new Error("Could not find external array in tsup.config.ts"); } + const matches = externalMatch[1].match(/["'`]([^"'`]+)["'`]/g); - externalDeps = (matches || []).map((m) => m.replace(/["'`]/g, "")); + externalDeps = (matches || []).map((value) => value.replace(/["'`]/g, "")); }); it("should have every peerDependency in the external array", () => { diff --git a/mem0-ts/src/oss/tests/vector-stores-compat.test.ts b/mem0-ts/src/oss/tests/vector-stores-compat.test.ts index 81c9c7f34..5c8225c1f 100644 --- a/mem0-ts/src/oss/tests/vector-stores-compat.test.ts +++ b/mem0-ts/src/oss/tests/vector-stores-compat.test.ts @@ -720,7 +720,358 @@ describe("AzureAISearch – backward compat with mocked client", () => { }); // ─────────────────────────────────────────────────────────────────────────── -// 6. Vectorize — mock Cloudflare client, test idempotent init +// 6. S3 Vectors — mock AWS client, test interface + init +// ─────────────────────────────────────────────────────────────────────────── +describe("S3 Vectors – backward compat with mocked client", () => { + function createMockS3VectorsClient(options?: { + queryDistance?: number; + queryDistanceMetric?: "cosine" | "euclidean"; + }) { + const buckets = new Set(); + const indexes = new Map< + string, + { + dimension: number; + distanceMetric: string; + } + >(); + const vectors = new Map< + string, + { + key: string; + data: { float32: number[] }; + metadata: Record; + } + >(); + + const vectorMapKey = (indexName: string, key: string) => + `${indexName}:${key}`; + + return { + send: jest.fn().mockImplementation(async (command: any) => { + const name = command.constructor.name; + const input = command.input; + + switch (name) { + case "GetVectorBucketCommand": + if (!buckets.has(input.vectorBucketName)) { + const error: any = new Error("Bucket not found"); + error.name = "NotFoundException"; + throw error; + } + return {}; + case "CreateVectorBucketCommand": + if (buckets.has(input.vectorBucketName)) { + const error: any = new Error("Bucket exists"); + error.name = "ConflictException"; + throw error; + } + buckets.add(input.vectorBucketName); + return {}; + case "GetIndexCommand": + if (!indexes.has(input.indexName)) { + const error: any = new Error("Index not found"); + error.name = "NotFoundException"; + throw error; + } + return {}; + case "CreateIndexCommand": + if (indexes.has(input.indexName)) { + const error: any = new Error("Index exists"); + error.name = "ConflictException"; + throw error; + } + indexes.set(input.indexName, { + dimension: input.dimension, + distanceMetric: input.distanceMetric, + }); + return {}; + case "PutVectorsCommand": + for (const vector of input.vectors || []) { + vectors.set(vectorMapKey(input.indexName, vector.key), { + key: vector.key, + data: vector.data, + metadata: vector.metadata || {}, + }); + } + return {}; + case "QueryVectorsCommand": + return { + distanceMetric: options?.queryDistanceMetric ?? "cosine", + vectors: [ + { + key: "doc-1", + metadata: { user_id: "u1", topic: "alpha" }, + distance: options?.queryDistance ?? 0.25, + }, + ], + }; + case "GetVectorsCommand": + return { + vectors: (input.keys || []) + .map((key: string) => + vectors.get(vectorMapKey(input.indexName, key)), + ) + .filter(Boolean), + }; + case "DeleteVectorsCommand": + for (const key of input.keys || []) { + vectors.delete(vectorMapKey(input.indexName, key)); + } + return {}; + case "DeleteIndexCommand": + indexes.delete(input.indexName); + for (const key of Array.from(vectors.keys())) { + if (key.startsWith(`${input.indexName}:`)) { + vectors.delete(key); + } + } + return {}; + case "ListVectorsCommand": + return { + vectors: Array.from(vectors.values()) + .filter((entry) => + vectorMapKey(input.indexName, entry.key).startsWith( + `${input.indexName}:`, + ), + ) + .map((entry) => ({ + key: entry.key, + metadata: entry.metadata, + })), + }; + default: + throw new Error(`Unexpected S3Vectors command: ${name}`); + } + }), + }; + } + + function findCommandInput( + client: { send: jest.Mock }, + commandName: string, + ): Record | undefined { + const match = client.send.mock.calls.find( + ([command]) => command.constructor.name === commandName, + ); + return match?.[0]?.input; + } + + it("implements full VectorStore interface", () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const store = new S3Vectors({ + client: createMockS3VectorsClient(), + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + }); + expect(typeof store.insert).toBe("function"); + expect(typeof store.search).toBe("function"); + expect(typeof store.get).toBe("function"); + expect(typeof store.update).toBe("function"); + expect(typeof store.delete).toBe("function"); + expect(typeof store.deleteCol).toBe("function"); + expect(typeof store.list).toBe("function"); + expect(typeof store.getUserId).toBe("function"); + expect(typeof store.setUserId).toBe("function"); + expect(typeof store.initialize).toBe("function"); + }); + + it("initialize() is idempotent (same promise returned)", async () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const mockClient = createMockS3VectorsClient(); + const store = new S3Vectors({ + client: mockClient, + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + }); + + const p1 = store.initialize(); + const p2 = store.initialize(); + const p3 = store.initialize(); + + await Promise.all([p1, p2, p3]); + + expect( + mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "CreateVectorBucketCommand", + ), + ).toHaveLength(1); + expect( + mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "CreateIndexCommand", + ), + ).toHaveLength(1); + expect( + mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "GetVectorBucketCommand", + ), + ).toHaveLength(1); + expect( + mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "GetIndexCommand", + ), + ).toHaveLength(1); + }); + + it("shapes S3 write requests and normalizes search results", async () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const mockClient = createMockS3VectorsClient(); + const store = new S3Vectors({ + client: mockClient, + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + }); + + await store.initialize(); + await store.insert( + [[0.1, 0.2, 0.3]], + ["doc-1"], + [{ user_id: "u1", topic: "alpha" }], + ); + + const putInput = findCommandInput(mockClient, "PutVectorsCommand"); + expect(putInput).toMatchObject({ + vectorBucketName: "test-bucket", + indexName: "test-index", + vectors: [ + { + key: "doc-1", + data: { float32: [0.1, 0.2, 0.3] }, + metadata: { user_id: "u1", topic: "alpha" }, + }, + ], + }); + + const results = await store.search([0.1, 0.2, 0.3], 5, { user_id: "u1" }); + + const queryInput = findCommandInput(mockClient, "QueryVectorsCommand"); + expect(queryInput).toMatchObject({ + vectorBucketName: "test-bucket", + indexName: "test-index", + queryVector: { float32: [0.1, 0.2, 0.3] }, + topK: 5, + filter: { user_id: { $eq: "u1" } }, + returnMetadata: true, + returnDistance: true, + }); + expect(results).toEqual([ + { + id: "doc-1", + payload: { user_id: "u1", topic: "alpha" }, + score: 0.75, + }, + ]); + }); + + it("normalizes euclidean distances without collapsing scores to zero", async () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const store = new S3Vectors({ + client: createMockS3VectorsClient({ + queryDistance: 1.5, + queryDistanceMetric: "euclidean", + }), + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + distanceMetric: "cosine", + }); + + const [result] = await store.search([0.1, 0.2, 0.3], 1); + + expect(result).toEqual({ + id: "doc-1", + payload: { user_id: "u1", topic: "alpha" }, + score: 0.4, + }); + }); + + it("normalizes empty in and nin operands before search hits AWS", async () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const mockClient = createMockS3VectorsClient(); + const store = new S3Vectors({ + client: mockClient, + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + }); + + const baselineQueryCalls = mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "QueryVectorsCommand", + ).length; + const emptyInResults = await store.search([0.1, 0.2, 0.3], 5, { + topic: { in: [] }, + }); + expect(emptyInResults).toEqual([]); + expect( + mockClient.send.mock.calls.filter( + ([command]) => command.constructor.name === "QueryVectorsCommand", + ), + ).toHaveLength(baselineQueryCalls); + + await store.search([0.1, 0.2, 0.3], 5, { + topic: { nin: [] }, + }); + + const queryInput = findCommandInput(mockClient, "QueryVectorsCommand"); + expect(queryInput).toMatchObject({ + vectorBucketName: "test-bucket", + indexName: "test-index", + queryVector: { float32: [0.1, 0.2, 0.3] }, + topK: 5, + returnMetadata: true, + returnDistance: true, + }); + expect(queryInput.filter).toBeUndefined(); + }); + it("applies client-side list filters, including empty $nin operands", async () => { + const { S3Vectors } = require("../src/vector_stores/s3_vectors"); + const mockClient = createMockS3VectorsClient(); + const store = new S3Vectors({ + client: mockClient, + vectorBucketName: "test-bucket", + collectionName: "test-index", + embeddingModelDims: 3, + }); + + await store.initialize(); + await store.insert( + [ + [0.1, 0.2, 0.3], + [0.3, 0.2, 0.1], + ], + ["doc-1", "doc-2"], + [ + { user_id: "u1", topic: "alpha", tags: ["keep"] }, + { user_id: "u2", topic: "beta", tags: ["skip"] }, + ], + ); + + const [allRows, allCount] = await store.list({ topic: { nin: [] } }, 10); + expect(allCount).toBe(2); + expect(allRows.map((row) => row.id)).toEqual(["doc-1", "doc-2"]); + + const [filteredRows, filteredCount] = await store.list( + { + $and: [{ topic: { nin: ["beta"] } }, { tags: { in: ["keep"] } }], + }, + 10, + ); + + expect(filteredCount).toBe(1); + expect(filteredRows).toEqual([ + { + id: "doc-1", + payload: { user_id: "u1", topic: "alpha", tags: ["keep"] }, + }, + ]); + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +// 7. Vectorize — mock Cloudflare client, test idempotent init // ─────────────────────────────────────────────────────────────────────────── describe("Vectorize – backward compat with mocked client", () => { let VectorizeDB: any; diff --git a/mem0-ts/tsup.config.ts b/mem0-ts/tsup.config.ts index 76ea4e777..86cc7789a 100644 --- a/mem0-ts/tsup.config.ts +++ b/mem0-ts/tsup.config.ts @@ -4,6 +4,7 @@ import pkg from "./package.json"; const external = [ "openai", "@anthropic-ai/sdk", + "@aws-sdk/client-s3vectors", "groq-sdk", "uuid", "pg",