diff --git a/docs/components/vectordbs/dbs/baidu.mdx b/docs/components/vectordbs/dbs/baidu.mdx index 72a26a4ce..ff7428cbd 100644 --- a/docs/components/vectordbs/dbs/baidu.mdx +++ b/docs/components/vectordbs/dbs/baidu.mdx @@ -5,10 +5,22 @@ description: "Use Baidu Mochow as an enterprise vector database in Mem0 for high [Baidu VectorDB](https://cloud.baidu.com/doc/VDB/index.html) is an enterprise-level distributed vector database service developed by Baidu Intelligent Cloud. It is powered by Baidu's proprietary "Mochow" vector database kernel, providing high performance, availability, and security for vector search. +### Installation + + +```bash Python +pip install pymochow +``` + +```bash TypeScript +npm install @mochow/mochow-sdk-node +``` + + + ### Usage ```python -import os from mem0 import Memory config = { @@ -36,19 +48,63 @@ messages = [ m.add(messages, user_id="alice", metadata={"category": "movies"}) ``` +```typescript +import { Memory } from "mem0ai/oss"; + +const memory = new Memory({ + embedder: { + provider: "openai", + config: { + apiKey: process.env.OPENAI_API_KEY || "", + model: "text-embedding-3-small", + embeddingDims: 1536, + }, + }, + vectorStore: { + provider: "baidu", + config: { + endpoint: process.env.BAIDU_ENDPOINT || "", + account: process.env.BAIDU_ACCOUNT || "root", + apiKey: process.env.BAIDU_API_KEY || "", + databaseName: "mem0", + tableName: "mem0_table", + embeddingModelDims: 1536, + metricType: "COSINE", + }, + }, + llm: { + provider: "openai", + config: { + apiKey: process.env.OPENAI_API_KEY || "", + model: "gpt-5-mini", + }, + }, +}); +``` + ### Config Here are the parameters available for configuring Baidu VectorDB: -| Parameter | Description | Default Value | -| --- | --- | --- | -| `endpoint` | Endpoint URL for your Baidu VectorDB instance | Required | -| `account` | Baidu VectorDB account name | `root` | -| `api_key` | API key for accessing Baidu VectorDB | Required | -| `database_name` | Name of the database | `mem0` | -| `table_name` | Name of the table | `mem0` | -| `embedding_model_dims` | Dimensions of the embedding model | `1536` | -| `metric_type` | Distance metric for similarity search | `L2` | +| Parameter | Description | Default Value | +| ---------------------- | --------------------------------------------- | ------------- | +| `endpoint` | Endpoint URL for your Baidu VectorDB instance | Required | +| `account` | Baidu VectorDB account name | `root` | +| `api_key` | API key for accessing Baidu VectorDB | Required | +| `database_name` | Name of the database | `mem0` | +| `table_name` | Name of the table | `mem0` | +| `embedding_model_dims` | Dimensions of the embedding model | `1536` | +| `metric_type` | Distance metric for similarity search | `L2` | +| `client` | Prebuilt Mochow client (TypeScript SDK only) | `None` | + +For the TypeScript OSS SDK, use the camelCase equivalents: + +- `databaseName` +- `tableName` +- `embeddingModelDims` +- `metricType` + +For OSS TS usage, `endpoint`, `account`, `apiKey`, `databaseName`, `tableName`, and `embeddingModelDims` are required unless you inject a prebuilt client. `metricType` defaults to `L2`, matching the Python SDK. ### Distance Metrics @@ -66,3 +122,5 @@ The vector index is automatically configured with the following HNSW parameters: - `efconstruction`: 200 (size of the dynamic candidate list) - `auto_build`: true (automatically build index) - `auto_build_index_policy`: Incremental build with 10000 rows increment + +The TypeScript provider also creates a BM25 inverted index over a `textLemmatized` column so `keywordSearch()` runs against a real full-text index. Mem0 lemmatizes the query before it reaches the vector store, so only the lemmatized form of each memory is indexed. If you point `tableName` at a table created before this index existed, `keywordSearch()` returns `null` and search falls back to vector similarity alone; recreate the table to enable it. diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 206bd4f8d..60b6d9358 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -98,7 +98,8 @@ "ts-node": "^10.9.2", "tsup": "^8.3.0", "typescript": "5.5.4", - "iovalkey": "^0.3.3" + "iovalkey": "^0.3.3", + "@mochow/mochow-sdk-node": "^2.1.5" }, "dependencies": { "axios": "^1.16.0", @@ -109,6 +110,7 @@ "peerDependencies": { "@anthropic-ai/sdk": "^0.40.1", "@aws-sdk/client-s3vectors": "3.967.0", + "@mochow/mochow-sdk-node": "^2.1.5", "@azure/identity": "^4.0.0", "@azure/search-documents": "^12.0.0", "@cloudflare/workers-types": "^4.20250504.0", @@ -151,6 +153,9 @@ }, "@zilliz/milvus2-sdk-node": { "optional": true + }, + "@mochow/mochow-sdk-node": { + "optional": true } }, "engines": { diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml index ae4b656ca..e652cc7df 100644 --- a/mem0-ts/pnpm-lock.yaml +++ b/mem0-ts/pnpm-lock.yaml @@ -109,7 +109,7 @@ importers: version: 4.5.0 cohere-ai: specifier: ^7.17.0 || ^8.0.0 - version: 8.0.0(@aws-crypto/sha256-js@5.2.0)(@smithy/protocol-http@5.5.2)(@smithy/signature-v4@5.5.2) + version: 8.0.0(@aws-crypto/sha256-js@5.2.0)(@smithy/protocol-http@5.5.2)(@smithy/signature-v4@5.6.2) compromise: specifier: ^14.0.0 version: 14.15.1 @@ -153,6 +153,9 @@ importers: specifier: ^3.24.1 version: 3.25.76 devDependencies: + '@mochow/mochow-sdk-node': + specifier: ^2.1.5 + version: 2.1.5 '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 @@ -191,7 +194,7 @@ importers: version: 10.9.2(@types/node@22.19.21)(typescript@5.5.4) tsup: specifier: ^8.3.0 - version: 8.5.1(typescript@5.5.4) + version: 8.5.1(tsx@4.23.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1137,6 +1140,9 @@ packages: '@mistralai/mistralai@1.15.1': resolution: {integrity: sha512-fb995eiz3r0KsBGtRjFV+/iLbX+UpfalxpF+YitT3R6ukrPD4PN+FGwwmYcRFhNAzVzDUtTVxQYnjQWEnwV5nw==} + '@mochow/mochow-sdk-node@2.1.5': + resolution: {integrity: sha512-IYluqAf50wH51uMGDqRni1Mmuc8GCNDHibJf0x3YlJFQkTJkU+l/PvLsmrer9qV1gMj6wHA+LE0OkNevW7KsCw==} + '@mongodb-js/saslprep@1.4.11': resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==} @@ -4076,6 +4082,11 @@ packages: typescript: optional: true + tsx@4.23.0: + resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -5700,6 +5711,11 @@ snapshots: - bufferutil - utf-8-validate + '@mochow/mochow-sdk-node@2.1.5': + dependencies: + tsx: 4.23.0 + winston: 3.19.0 + '@mongodb-js/saslprep@1.4.11': dependencies: sparse-bitfield: 3.0.3 @@ -6666,7 +6682,7 @@ snapshots: co@4.6.0: {} - cohere-ai@8.0.0(@aws-crypto/sha256-js@5.2.0)(@smithy/protocol-http@5.5.2)(@smithy/signature-v4@5.5.2): + cohere-ai@8.0.0(@aws-crypto/sha256-js@5.2.0)(@smithy/protocol-http@5.5.2)(@smithy/signature-v4@5.6.2): dependencies: convict: 6.2.5 form-data: 4.0.6 @@ -6676,7 +6692,7 @@ snapshots: optionalDependencies: '@aws-crypto/sha256-js': 5.2.0 '@smithy/protocol-http': 5.5.2 - '@smithy/signature-v4': 5.5.2 + '@smithy/signature-v4': 5.6.2 collect-v8-coverage@1.0.3: {} @@ -8330,9 +8346,11 @@ snapshots: platform@1.3.6: {} - postcss-load-config@6.0.1: + postcss-load-config@6.0.1(tsx@4.23.0): dependencies: lilconfig: 3.1.3 + optionalDependencies: + tsx: 4.23.0 postgres-array@2.0.0: {} @@ -8895,7 +8913,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(typescript@5.5.4): + tsup@8.5.1(tsx@4.23.0)(typescript@5.5.4): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 @@ -8906,7 +8924,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(tsx@4.23.0) resolve-from: 5.0.0 rollup: 4.61.1 source-map: 0.7.6 @@ -8922,6 +8940,12 @@ snapshots: - tsx - yaml + tsx@4.23.0: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/mem0-ts/src/oss/src/index.ts b/mem0-ts/src/oss/src/index.ts index ceaf8009c..aca5d6ef6 100644 --- a/mem0-ts/src/oss/src/index.ts +++ b/mem0-ts/src/oss/src/index.ts @@ -25,6 +25,7 @@ export * from "./llms/litellm"; export * from "./llms/vllm"; export * from "./vector_stores/base"; export * from "./vector_stores/memory"; +export * from "./vector_stores/baidu"; export * from "./vector_stores/qdrant"; export * from "./vector_stores/redis"; export * from "./vector_stores/valkey"; diff --git a/mem0-ts/src/oss/src/utils/factory.ts b/mem0-ts/src/oss/src/utils/factory.ts index 9039ba668..213e7bb42 100644 --- a/mem0-ts/src/oss/src/utils/factory.ts +++ b/mem0-ts/src/oss/src/utils/factory.ts @@ -23,6 +23,7 @@ import { CrossEncoderReranker } from "../rerankers/cross_encoder"; import { Embedder } from "../embeddings/base"; import { LLM } from "../llms/base"; import { VectorStore } from "../vector_stores/base"; +import { BaiduDB } from "../vector_stores/baidu"; import { Qdrant } from "../vector_stores/qdrant"; import { ChromaDB } from "../vector_stores/chroma"; import { VectorizeDB } from "../vector_stores/vectorize"; @@ -143,6 +144,8 @@ export class VectorStoreFactory { switch (provider.toLowerCase()) { case "memory": return new MemoryVectorStore(config); + case "baidu": + return new BaiduDB(config as any); case "qdrant": return new Qdrant(config as any); case "chroma": diff --git a/mem0-ts/src/oss/src/vector_stores/baidu.ts b/mem0-ts/src/oss/src/vector_stores/baidu.ts new file mode 100644 index 000000000..32f9bda5f --- /dev/null +++ b/mem0-ts/src/oss/src/vector_stores/baidu.ts @@ -0,0 +1,613 @@ +import type { + AutoBuildIncrementPolicy, + CommonResponse, + DescTableResponse, + FieldType, + IndexSchema, + MochowClient, + QueryResponse, + SearchResponse, + SelectResponse, + TableSchema, +} from "@mochow/mochow-sdk-node"; +import { VectorStore } from "./base"; +import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; + +type MochowSdk = typeof import("@mochow/mochow-sdk-node"); + +export interface BaiduConfig extends VectorStoreConfig { + endpoint: string; + account: string; + apiKey: string; + databaseName: string; + tableName: string; + embeddingModelDims: number; + metricType?: "L2" | "IP" | "COSINE"; + client?: MochowClient; +} + +const VECTOR_INDEX = "vector_idx"; +const FILTERING_INDEX = "metadata_filtering_idx"; +// Named after the column it actually indexes, and deliberately not Python's "data_bm25_idx". +// This index holds Porter-stemmed text, but mem0/vector_stores/baidu.py's keyword_search() +// sends a raw, unstemmed query to that name. Sharing it would let Python find an index whose +// contents it cannot match properly, silently returning degraded hits instead of None. +const BM25_INDEX = "text_lemmatized_bm25_idx"; +const PROJECTIONS = ["id", "data", "metadata"]; +const TABLE_POLL_INTERVAL_MS = 2000; +const TABLE_POLL_ATTEMPTS = 60; + +// Mochow's server accepts JSON columns, but the Node SDK's FieldType enum predates them +// (pymochow 2.4.1 ships FieldType.JSON == "JSON"). The wire value is the bare string. +const JSON_FIELD_TYPE = "JSON" as unknown as FieldType; + +// Querying a primary key that isn't there answers with this code, not an empty row. The Node +// SDK's ServerErrCode stops at 100, but its siblings against the same server name it: +// pymochow ROW_KEY_NOT_FOUND = 101, mochow-sdk-go RowKeyNotFound = 101. +const ROW_KEY_NOT_FOUND = 101; + +const SAFE_FILTER_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +function escapeFilterString(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Mochow resolves with a {code, msg} envelope instead of rejecting, so every call site +// has to inspect the code. `tolerated` lets callers accept the idempotent outcomes +// (database/table already exists, table already dropped). +function check( + response: CommonResponse, + action: string, + ...tolerated: number[] +): number { + if (response.code !== 0 && !tolerated.includes(response.code)) { + throw new Error( + `Baidu Mochow ${action} failed (code ${response.code}): ${response.msg}`, + ); + } + return response.code; +} + +function lemmatizedText(payload: Record): string { + const data = typeof payload.data === "string" ? payload.data : ""; + return typeof payload.textLemmatized === "string" && + payload.textLemmatized.length > 0 + ? payload.textLemmatized + : data; +} + +function memoryData(payload: Record): string { + return typeof payload.data === "string" ? payload.data : ""; +} + +function metadataPayload(payload: Record): Record { + const { data: _data, textLemmatized: _textLemmatized, ...metadata } = payload; + return metadata; +} + +function resultPayload(row: Record): Record { + return { + ...(row.metadata || {}), + ...(typeof row.data === "string" ? { data: row.data } : {}), + }; +} + +export class BaiduDB implements VectorStore { + private client: MochowClient | null = null; + private sdk: MochowSdk | null = null; + private readonly endpoint: string; + private readonly account: string; + private readonly apiKey: string; + private readonly databaseName: string; + private readonly tableName: string; + private readonly embeddingModelDims: number; + private readonly metricType: "L2" | "IP" | "COSINE"; + // Fails closed: keyword search stays off until an inverted index is observed. + private supportsKeywordSearch = false; + private storeUserId = "anonymous-baidu-user"; + private _initPromise?: Promise; + + constructor(config: BaiduConfig) { + this.endpoint = config.endpoint; + this.account = config.account; + this.apiKey = config.apiKey; + this.databaseName = config.databaseName; + this.tableName = config.tableName; + this.embeddingModelDims = config.embeddingModelDims; + this.metricType = config.metricType || "L2"; + this.client = config.client || null; + + const requiredFields: Array< + readonly [string, string | number | undefined] + > = [ + ["databaseName", this.databaseName], + ["tableName", this.tableName], + ["embeddingModelDims", this.embeddingModelDims], + ]; + + if (!this.client) { + requiredFields.unshift( + ["endpoint", this.endpoint], + ["account", this.account], + ["apiKey", this.apiKey], + ); + } + + for (const [name, value] of requiredFields) { + if (value === undefined || value === null || value === "") { + throw new Error( + `Baidu vector store requires a non-empty '${name}' config value.`, + ); + } + } + + this.initialize().catch(console.error); + } + + private get ns(): { database: string; table: string } { + return { database: this.databaseName, table: this.tableName }; + } + + // Loaded dynamically: @mochow/mochow-sdk-node is an optional peer dependency, so a static + // value import would break `import { Memory } from "mem0ai/oss"` for everyone else. + private async loadSdk(): Promise { + if (!this.sdk) { + let module: MochowSdk & { default?: MochowSdk }; + try { + module = await import("@mochow/mochow-sdk-node"); + } catch { + throw new Error( + "The Baidu vector store requires the '@mochow/mochow-sdk-node' package. Install it with: npm install @mochow/mochow-sdk-node", + ); + } + this.sdk = module.default ?? module; + } + return this.sdk; + } + + private async ensureClient(): Promise { + if (!this.client) { + const sdk = await this.loadSdk(); + this.client = new sdk.MochowClient({ + endpoint: this.endpoint, + credential: { account: this.account, apiKey: this.apiKey }, + }); + } + return this.client; + } + + private async ready(): Promise<{ client: MochowClient; sdk: MochowSdk }> { + await this.initialize(); + return { client: await this.ensureClient(), sdk: await this.loadSdk() }; + } + + private buildSchema(sdk: MochowSdk): TableSchema { + const { + AutoBuildPolicyType, + FieldType, + IndexType, + InvertedIndexAnalyzer, + InvertedIndexFieldAttribute, + InvertedIndexParseMode, + MetricType, + } = sdk; + + // sdk.AutoBuildIncrement() stamps policyType "TIMING" (bug in 2.1.5), so build the + // increment policy by hand. + const autoBuildPolicy: AutoBuildIncrementPolicy = { + policyType: AutoBuildPolicyType.Increment, + rowCountIncrement: 10000, + }; + + const vectorIndex: IndexSchema = { + indexName: VECTOR_INDEX, + indexType: IndexType.HNSW, + field: "vector", + metricType: MetricType[this.metricType], + params: { M: 16, efConstruction: 200 }, + autoBuild: true, + autoBuildPolicy, + }; + + return { + fields: [ + { + fieldName: "id", + fieldType: FieldType.String, + primaryKey: true, + partitionKey: true, + autoIncrement: false, + notNull: true, + }, + { + fieldName: "data", + fieldType: FieldType.Text, + }, + { + fieldName: "vector", + fieldType: FieldType.FloatVector, + notNull: true, + dimension: this.embeddingModelDims, + }, + // Stored outside `metadata` because Mochow cannot build an inverted index on a + // field inside a JSON column. Memory.search() passes an already-lemmatized query, + // so only the lemmatized form is worth indexing. + { fieldName: "textLemmatized", fieldType: FieldType.Text }, + { fieldName: "metadata", fieldType: JSON_FIELD_TYPE }, + ], + indexes: [ + vectorIndex, + { + indexName: FILTERING_INDEX, + indexType: IndexType.FilteringIndex, + fields: ["metadata"], + }, + { + indexName: BM25_INDEX, + indexType: IndexType.InvertedIndex, + fields: ["textLemmatized"], + fieldAttributes: [InvertedIndexFieldAttribute.Analyzed], + params: { + analyzer: InvertedIndexAnalyzer.EnglishAnalyzer, + parseMode: InvertedIndexParseMode.FineMode, + }, + }, + ], + }; + } + + private buildFilter(filters: SearchFilters): string { + const conditions: string[] = []; + + for (const [key, value] of Object.entries(filters)) { + if (!SAFE_FILTER_KEY.test(key)) { + throw new Error(`Invalid filter key: ${key}`); + } + + if (typeof value === "string") { + conditions.push(`metadata["${key}"] = "${escapeFilterString(value)}"`); + continue; + } + + if (typeof value === "number" || typeof value === "boolean") { + conditions.push(`metadata["${key}"] = ${value}`); + continue; + } + + throw new Error( + `Filter value for ${key} must be str, int, float, or bool, got ${Array.isArray(value) ? "array" : typeof value}`, + ); + } + + return conditions.join(" AND "); + } + + private filterOf(filters?: SearchFilters): string | undefined { + return filters && Object.keys(filters).length > 0 + ? this.buildFilter(filters) + : undefined; + } + + private async pollTable( + client: MochowClient, + settled: (response: DescTableResponse) => boolean, + what: string, + ): Promise { + for (let attempt = 0; attempt < TABLE_POLL_ATTEMPTS; attempt++) { + if (settled(await client.descTable(this.databaseName, this.tableName))) { + return; + } + await sleep(TABLE_POLL_INTERVAL_MS); + } + + throw new Error( + `Baidu Mochow table '${this.tableName}' was not ${what} after ${(TABLE_POLL_ATTEMPTS * TABLE_POLL_INTERVAL_MS) / 1000}s.`, + ); + } + + private async ensureTable(): Promise { + const sdk = await this.loadSdk(); + const client = await this.ensureClient(); + const { ServerErrCode, TableState } = sdk; + + check( + await client.createDatabase(this.databaseName), + `createDatabase '${this.databaseName}'`, + ServerErrCode.DBAlreadyExist, + ); + + const created = check( + await client.createTable({ + ...this.ns, + description: "mem0 memories", + replication: 3, + partition: { partitionType: sdk.PartitionType.HASH, partitionNum: 1 }, + enableDynamicField: false, + schema: this.buildSchema(sdk), + }), + `createTable '${this.tableName}'`, + ServerErrCode.TableAlreadyExist, + ); + + // A table is CREATING until its indexes are built; writing to it before then fails. + let description: DescTableResponse | undefined; + await this.pollTable( + client, + (response) => { + check(response, `descTable '${this.tableName}'`); + description = response; + return response.table.state === TableState.Normal; + }, + "ready", + ); + + this.applySchema( + created === ServerErrCode.TableAlreadyExist, + description!.table.schema, + ); + } + + private applySchema(preexisting: boolean, schema: TableSchema): void { + if (!preexisting) { + this.supportsKeywordSearch = true; + return; + } + + const fields = schema?.fields ?? []; + const indexes = schema?.indexes ?? []; + const field = (name: string) => fields.find((f) => f.fieldName === name); + const typeOf = (name: string) => String(field(name)?.fieldType ?? ""); + const label = `${this.databaseName}.${this.tableName}`; + + if ( + typeOf("id") !== "STRING" || + !typeOf("data").startsWith("TEXT") || + typeOf("vector") !== "FLOAT_VECTOR" || + typeOf("metadata") !== "JSON" + ) { + throw new Error( + `Baidu Mochow table '${label}' exists but is missing the id/data/vector/metadata schema mem0 requires. Drop it, or point 'tableName' at an unused table.`, + ); + } + + const dimension = field("vector")?.dimension; + if (dimension !== undefined && dimension !== this.embeddingModelDims) { + throw new Error( + `Baidu Mochow table '${label}' stores ${dimension}-dimensional vectors, but 'embeddingModelDims' is ${this.embeddingModelDims}.`, + ); + } + + this.supportsKeywordSearch = + typeOf("textLemmatized").startsWith("TEXT") && + indexes.some((index) => index.indexName === BM25_INDEX); + + if (!this.supportsKeywordSearch) { + console.warn( + `Baidu Mochow table '${label}' has no '${BM25_INDEX}' inverted index. keywordSearch() will return null until the table is recreated.`, + ); + } + } + + async initialize(): Promise { + if (!this._initPromise) { + this._initPromise = this.ensureTable().catch((error) => { + this._initPromise = undefined; + throw error; + }); + } + + return this._initPromise; + } + + async insert( + vectors: number[][], + ids: string[], + payloads: Record[], + ): Promise { + const { client } = await this.ready(); + + if (vectors.length !== ids.length || vectors.length !== payloads.length) { + throw new Error( + `Baidu insert requires vectors, ids, and payloads of equal length (got ${vectors.length}/${ids.length}/${payloads.length}).`, + ); + } + + const rows = vectors.map((vector, index) => ({ + id: ids[index], + data: memoryData(payloads[index] || {}), + vector, + textLemmatized: lemmatizedText(payloads[index] || {}), + metadata: metadataPayload(payloads[index] || {}), + })); + + check(await client.upsert({ ...this.ns, rows }), "upsert"); + } + + async search( + query: number[], + topK = 5, + filters?: SearchFilters, + ): Promise { + const { client, sdk } = await this.ready(); + const filter = this.filterOf(filters); + + const request = new sdk.VectorTopkSearchRequest( + "vector", + new sdk.Vector(query), + topK, + ) + .Projections(PROJECTIONS) + .Config(new sdk.VectorSearchConfig().Ef(200)); + if (filter) { + request.Filter(filter); + } + + const response = (await client.vectorSearch({ + ...this.ns, + request, + })) as SearchResponse; + check(response, "vectorSearch"); + + return (response.rows ?? []).map((result) => ({ + id: String(result.row.id), + payload: resultPayload(result.row), + score: result.score, + })); + } + + async keywordSearch( + query: string, + topK = 5, + filters?: SearchFilters, + ): Promise { + const { client, sdk } = await this.ready(); + + if (!this.supportsKeywordSearch) { + return null; + } + + const filter = this.filterOf(filters); + const request = new sdk.BM25SearchRequest(BM25_INDEX, query) + .Projections(PROJECTIONS) + .Limit(topK); + if (filter) { + request.Filter(filter); + } + + const response = (await client.bm25Search({ + ...this.ns, + request, + })) as SearchResponse; + check(response, "bm25Search"); + + return (response.rows ?? []).map((result) => ({ + id: String(result.row.id), + payload: resultPayload(result.row), + score: result.score, + })); + } + + async get(vectorId: string): Promise { + const { client } = await this.ready(); + + const response: QueryResponse = await client.query({ + ...this.ns, + primaryKey: { id: vectorId }, + projections: PROJECTIONS, + }); + check(response, `query '${vectorId}'`, ROW_KEY_NOT_FOUND); + + if (!response.row || response.row.id === undefined) { + return null; + } + + return { + id: String(response.row.id), + payload: resultPayload(response.row), + }; + } + + async update( + vectorId: string, + vector: number[], + payload: Record, + ): Promise { + const { client } = await this.ready(); + + check( + await client.upsert({ + ...this.ns, + rows: [ + { + id: vectorId, + data: memoryData(payload), + vector, + textLemmatized: lemmatizedText(payload), + metadata: metadataPayload(payload), + }, + ], + }), + `upsert '${vectorId}'`, + ); + } + + async delete(vectorId: string): Promise { + const { client } = await this.ready(); + + check( + await client.delete({ ...this.ns, primaryKey: { id: vectorId } }), + `delete '${vectorId}'`, + ); + } + + async deleteCol(): Promise { + // The constructor starts initialize() without awaiting it. Let any in-flight run land + // first, otherwise it recreates the table after dropTable() and reset() is a no-op. + await this._initPromise?.catch(() => undefined); + this._initPromise = undefined; + this.supportsKeywordSearch = false; + + const sdk = await this.loadSdk(); + const client = await this.ensureClient(); + const { ServerErrCode } = sdk; + + const dropped = check( + await client.dropTable(this.databaseName, this.tableName), + `dropTable '${this.tableName}'`, + ServerErrCode.TableNotExist, + ); + if (dropped === ServerErrCode.TableNotExist) { + return; + } + + // Drops are asynchronous; recreating the table before it is gone fails. + await this.pollTable( + client, + (response) => + check( + response, + `descTable '${this.tableName}'`, + ServerErrCode.TableNotExist, + ) === ServerErrCode.TableNotExist, + "dropped", + ); + } + + async reset(): Promise { + await this.deleteCol(); + await this.initialize(); + } + + async list( + filters?: SearchFilters, + topK = 100, + ): Promise<[VectorStoreResult[], number]> { + const { client } = await this.ready(); + + const response: SelectResponse = await client.select({ + ...this.ns, + filter: this.filterOf(filters), + projections: PROJECTIONS, + limit: topK, + }); + check(response, "select"); + + const memories = (response.rows ?? []).map((row) => ({ + id: String(row.id), + payload: resultPayload(row), + })); + return [memories, memories.length]; + } + + async getUserId(): Promise { + return this.storeUserId; + } + + async setUserId(userId: string): Promise { + this.storeUserId = userId; + } +} diff --git a/mem0-ts/src/oss/tests/baidu.test.ts b/mem0-ts/src/oss/tests/baidu.test.ts new file mode 100644 index 000000000..912758ef9 --- /dev/null +++ b/mem0-ts/src/oss/tests/baidu.test.ts @@ -0,0 +1,617 @@ +import { + AutoBuildPolicyType, + FieldType, + IndexType, + InvertedIndexFieldAttribute, + MetricType, + PartitionType, + ServerErrCode, + TableState, +} from "@mochow/mochow-sdk-node"; +import { BaiduDB } from "../src/vector_stores/baidu"; + +// No jest.mock() here on purpose: the real SDK supplies the enums and the search request +// classes (which carry an internal `set` map the client reads, so they cannot be hand-rolled +// as plain literals). Only the network-facing MochowClient is faked, via the `client` config. + +const OK = { code: 0, msg: "" }; +const DIMS = 1536; + +const normalTable = (schema: unknown = { fields: [], indexes: [] }) => ({ + ...OK, + table: { state: TableState.Normal, schema }, +}); + +const CORE_FIELDS = [ + { fieldName: "id", fieldType: FieldType.String }, + { fieldName: "data", fieldType: FieldType.Text }, + { fieldName: "vector", fieldType: FieldType.FloatVector, dimension: DIMS }, + { fieldName: "metadata", fieldType: "JSON" }, +]; + +const BM25_FIELDS = [ + ...CORE_FIELDS, + { fieldName: "textLemmatized", fieldType: FieldType.Text }, +]; + +/** Records call order, so ordering regressions (deleteCol vs. in-flight init) are visible. */ +function fakeClient(overrides: Record any> = {}) { + const calls: string[] = []; + const track = + (name: string, impl: (...args: any[]) => any) => + (...args: any[]) => { + calls.push(name); + return impl(...args); + }; + + const client: any = { + calls, + createDatabase: jest.fn(track("createDatabase", async () => OK)), + createTable: jest.fn(track("createTable", async () => OK)), + dropTable: jest.fn(track("dropTable", async () => OK)), + descTable: jest.fn(track("descTable", async () => normalTable())), + upsert: jest.fn(async () => OK), + delete: jest.fn(async () => OK), + query: jest.fn(), + select: jest.fn(), + vectorSearch: jest.fn(), + bm25Search: jest.fn(), + }; + + for (const [name, impl] of Object.entries(overrides)) { + client[name] = jest.fn(track(name, impl)); + } + return client; +} + +const makeStore = (client: any, extra: Record = {}) => + new BaiduDB({ + endpoint: "http://127.0.0.1:5287", + account: "root", + apiKey: "test-key", + databaseName: "mem0_db", + tableName: "mem0", + embeddingModelDims: DIMS, + client, + ...extra, + } as any); + +/** Run the poll loop's setTimeout inline so tests never wait the real 2s interval. */ +const runTimersInline = () => + jest.spyOn(global, "setTimeout").mockImplementation(((fn: () => void) => { + fn(); + return 0; + }) as any); + +beforeEach(() => { + jest.spyOn(console, "warn").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); +}); + +afterEach(() => jest.restoreAllMocks()); + +describe("BaiduDB config", () => { + it("rejects a missing required field", () => { + expect(() => makeStore(fakeClient(), { tableName: "" })).toThrow( + /non-empty 'tableName'/, + ); + }); + + it("does not require endpoint credentials when a client is injected", () => { + expect(() => + makeStore(fakeClient(), { endpoint: "", account: "", apiKey: "" }), + ).not.toThrow(); + }); +}); + +describe("BaiduDB table provisioning", () => { + it("creates the table with the schema mem0 needs", async () => { + const client = fakeClient(); + await makeStore(client).initialize(); + + const spec = client.createTable.mock.calls[0][0]; + expect(client.createDatabase).toHaveBeenCalledWith("mem0_db"); + expect(spec.database).toBe("mem0_db"); + expect(spec.table).toBe("mem0"); + expect(spec.enableDynamicField).toBe(false); + // Mochow rejects a partition without partitionType. + expect(spec.partition).toEqual({ + partitionType: PartitionType.HASH, + partitionNum: 1, + }); + + const fields = spec.schema.fields.map((f: any) => [ + f.fieldName, + f.fieldType, + ]); + expect(fields).toEqual([ + ["id", FieldType.String], + ["data", FieldType.Text], + ["vector", FieldType.FloatVector], + ["textLemmatized", FieldType.Text], + ["metadata", "JSON"], + ]); + expect(spec.schema.fields[0]).toMatchObject({ + primaryKey: true, + partitionKey: true, + notNull: true, + }); + expect(spec.schema.fields[2].dimension).toBe(DIMS); + }); + + it("builds a vector index with a genuine row-count-increment auto-build policy", async () => { + const client = fakeClient(); + await makeStore(client).initialize(); + + const [vectorIndex] = client.createTable.mock.calls[0][0].schema.indexes; + expect(vectorIndex).toMatchObject({ + indexName: "vector_idx", + indexType: IndexType.HNSW, + field: "vector", + metricType: MetricType.L2, + params: { M: 16, efConstruction: 200 }, + autoBuild: true, + }); + // Regression guard: sdk.AutoBuildIncrement() stamps policyType "TIMING" in 2.1.5. + expect(vectorIndex.autoBuildPolicy).toEqual({ + policyType: AutoBuildPolicyType.Increment, + rowCountIncrement: 10000, + }); + expect(AutoBuildPolicyType.Increment).not.toBe(AutoBuildPolicyType.Timing); + }); + + it("declares the filtering and BM25 indexes with an indexType", async () => { + const client = fakeClient(); + await makeStore(client).initialize(); + + const [, filtering, bm25] = + client.createTable.mock.calls[0][0].schema.indexes; + expect(filtering).toEqual({ + indexName: "metadata_filtering_idx", + indexType: IndexType.FilteringIndex, + fields: ["metadata"], + }); + // Memory.search() hands keywordSearch() an already-lemmatized query, so raw `data` is + // not worth indexing — only the lemmatized column is. The index is therefore named for + // that column and must never be called "data_bm25_idx": that is the name Python's + // keyword_search() queries with a raw, unstemmed query, and it must keep missing (and so + // falling back to vector search) rather than half-matching this stemmed index. + expect(bm25).toMatchObject({ + indexName: "text_lemmatized_bm25_idx", + indexType: IndexType.InvertedIndex, + fields: ["textLemmatized"], + fieldAttributes: [InvertedIndexFieldAttribute.Analyzed], + }); + }); + + it("honours a configured metric type", async () => { + const client = fakeClient(); + await makeStore(client, { metricType: "COSINE" }).initialize(); + const [vectorIndex] = client.createTable.mock.calls[0][0].schema.indexes; + expect(vectorIndex.metricType).toBe(MetricType.COSINE); + }); + + it("tolerates an existing database and table", async () => { + const client = fakeClient({ + createDatabase: async () => ({ + code: ServerErrCode.DBAlreadyExist, + msg: "db exists", + }), + createTable: async () => ({ + code: ServerErrCode.TableAlreadyExist, + msg: "table exists", + }), + descTable: async () => normalTable({ fields: BM25_FIELDS, indexes: [] }), + }); + await expect(makeStore(client).initialize()).resolves.toBeUndefined(); + }); + + it("waits for a CREATING table to become NORMAL", async () => { + runTimersInline(); + const states = [ + TableState.Creating, + TableState.Creating, + TableState.Normal, + ]; + const client = fakeClient({ + descTable: async () => ({ + ...OK, + table: { state: states.shift(), schema: { fields: [], indexes: [] } }, + }), + }); + + await makeStore(client).initialize(); + expect(client.descTable).toHaveBeenCalledTimes(3); + }); + + it("surfaces a non-zero envelope as an error rather than succeeding", async () => { + const client = fakeClient({ + createTable: async () => ({ + code: ServerErrCode.InvalidTableSchema, + msg: "bad schema", + }), + }); + await expect(makeStore(client).initialize()).rejects.toThrow( + /createTable 'mem0' failed \(code 60\): bad schema/, + ); + }); + + it("rejects an existing table whose vector dimension disagrees", async () => { + const client = fakeClient({ + createTable: async () => ({ + code: ServerErrCode.TableAlreadyExist, + msg: "", + }), + descTable: async () => + normalTable({ + fields: [ + CORE_FIELDS[0], + CORE_FIELDS[1], + { + fieldName: "vector", + fieldType: FieldType.FloatVector, + dimension: 768, + }, + CORE_FIELDS[3], + ], + indexes: [], + }), + }); + await expect(makeStore(client).initialize()).rejects.toThrow( + /stores 768-dimensional vectors, but 'embeddingModelDims' is 1536/, + ); + }); + + it("rejects an existing table missing the core schema", async () => { + const client = fakeClient({ + createTable: async () => ({ + code: ServerErrCode.TableAlreadyExist, + msg: "", + }), + descTable: async () => + normalTable({ fields: [CORE_FIELDS[0]], indexes: [] }), + }); + await expect(makeStore(client).initialize()).rejects.toThrow( + /missing the id\/data\/vector\/metadata schema/, + ); + }); +}); + +describe("BaiduDB keyword search support detection", () => { + it("fails closed when an existing table has no inverted index", async () => { + const client = fakeClient({ + createTable: async () => ({ + code: ServerErrCode.TableAlreadyExist, + msg: "", + }), + descTable: async () => normalTable({ fields: CORE_FIELDS, indexes: [] }), + }); + const store = makeStore(client); + + await expect(store.keywordSearch("hello")).resolves.toBeNull(); + expect(client.bm25Search).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining("text_lemmatized_bm25_idx"), + ); + }); + + it("enables keyword search when the existing table carries the BM25 index", async () => { + const client = fakeClient({ + createTable: async () => ({ + code: ServerErrCode.TableAlreadyExist, + msg: "", + }), + descTable: async () => + normalTable({ + fields: BM25_FIELDS, + indexes: [{ indexName: "text_lemmatized_bm25_idx" }], + }), + }); + client.bm25Search.mockResolvedValue({ ...OK, rows: [] }); + + await expect(makeStore(client).keywordSearch("hello")).resolves.toEqual([]); + expect(client.bm25Search).toHaveBeenCalled(); + }); + + it("queries the inverted index with the caller's already-lemmatized text", async () => { + const client = fakeClient(); + client.bm25Search.mockResolvedValue({ + ...OK, + rows: [ + { row: { id: "m1", data: "loves pizza", metadata: {} }, score: 3.5 }, + ], + }); + + const results = await makeStore(client).keywordSearch("love pizza", 7, { + userId: "alice", + }); + expect(results).toEqual([ + { id: "m1", payload: { data: "loves pizza" }, score: 3.5 }, + ]); + + const { request, ...ns } = client.bm25Search.mock.calls[0][0]; + expect(ns).toEqual({ database: "mem0_db", table: "mem0" }); + expect(request.indexName).toBe("text_lemmatized_bm25_idx"); + expect(request.searchText).toBe("love pizza"); + expect(request.limit).toBe(7); + expect(request.filter).toBe('metadata["userId"] = "alice"'); + }); +}); + +describe("BaiduDB writes", () => { + it("upserts the whole batch in one call and mirrors textLemmatized out of the payload", async () => { + const client = fakeClient(); + + await makeStore(client).insert( + [ + [1, 2], + [3, 4], + ], + ["a", "b"], + [ + { data: "loves pizza", textLemmatized: "love pizza" }, + { data: "runs daily" }, + ], + ); + + expect(client.upsert).toHaveBeenCalledTimes(1); + expect(client.upsert.mock.calls[0][0]).toEqual({ + database: "mem0_db", + table: "mem0", + rows: [ + { + id: "a", + data: "loves pizza", + vector: [1, 2], + textLemmatized: "love pizza", + metadata: {}, + }, + // Falls back to `data` when the caller did not lemmatize. + { + id: "b", + data: "runs daily", + vector: [3, 4], + textLemmatized: "runs daily", + metadata: {}, + }, + ], + }); + }); + + it("refuses a ragged batch instead of silently truncating it", async () => { + await expect( + makeStore(fakeClient()).insert([[1]], ["a", "b"], [{}]), + ).rejects.toThrow(/equal length \(got 1\/2\/1\)/); + }); + + it("updates and deletes by primary key", async () => { + const client = fakeClient(); + const store = makeStore(client); + + await store.update("m1", [9], { data: "new" }); + expect(client.upsert.mock.calls[0][0].rows).toEqual([ + { + id: "m1", + data: "new", + vector: [9], + textLemmatized: "new", + metadata: {}, + }, + ]); + + await store.delete("m1"); + expect(client.delete).toHaveBeenCalledWith({ + database: "mem0_db", + table: "mem0", + primaryKey: { id: "m1" }, + }); + }); + + it("throws when the server rejects an upsert", async () => { + const client = fakeClient(); + client.upsert.mockResolvedValue({ code: 100, msg: "duplicate key" }); + + await expect(makeStore(client).insert([[1]], ["a"], [{}])).rejects.toThrow( + /upsert failed \(code 100\): duplicate key/, + ); + }); +}); + +describe("BaiduDB reads", () => { + it("maps vector search hits out of the nested row envelope", async () => { + const client = fakeClient(); + client.vectorSearch.mockResolvedValue({ + ...OK, + rows: [ + { + row: { id: "m1", data: "x", metadata: {} }, + distance: 0.2, + score: 0.8, + }, + ], + }); + + const results = await makeStore(client).search([1, 2, 3], 5, { + userId: "alice", + }); + expect(results).toEqual([{ id: "m1", payload: { data: "x" }, score: 0.8 }]); + + const { request } = client.vectorSearch.mock.calls[0][0]; + expect(request.vectorField).toBe("vector"); + expect(request.vector).toEqual({ vector: [1, 2, 3] }); + expect(request.limit).toBe(5); + expect(request.filter).toBe('metadata["userId"] = "alice"'); + expect(request.projections).toEqual(["id", "data", "metadata"]); + expect(request.config.params).toEqual({ ef: 200 }); + }); + + it("omits the filter when no filters are supplied", async () => { + const client = fakeClient(); + client.vectorSearch.mockResolvedValue({ ...OK, rows: [] }); + await makeStore(client).search([1], 5); + expect(client.vectorSearch.mock.calls[0][0].request.filter).toBeUndefined(); + }); + + it("escapes quotes and rejects unsafe filter keys and values", async () => { + const client = fakeClient(); + client.vectorSearch.mockResolvedValue({ ...OK, rows: [] }); + const store = makeStore(client); + + await store.search([1], 5, { userId: 'a"b', runId: 3, agentId: true }); + expect(client.vectorSearch.mock.calls[0][0].request.filter).toBe( + 'metadata["userId"] = "a\\"b" AND metadata["runId"] = 3 AND metadata["agentId"] = true', + ); + + await expect(store.search([1], 5, { "bad key": "x" })).rejects.toThrow( + /Invalid filter key/, + ); + await expect( + store.search([1], 5, { userId: ["a"] as any }), + ).rejects.toThrow(/must be str, int, float, or bool, got array/); + }); + + it("returns null for a missing id and throws on a real query failure", async () => { + const client = fakeClient(); + const store = makeStore(client); + + client.query.mockResolvedValue({ ...OK, row: {} }); + await expect(store.get("nope")).resolves.toBeNull(); + + client.query.mockResolvedValue({ + ...OK, + row: { id: "m1", data: "stored text", metadata: { a: 1 } }, + }); + await expect(store.get("m1")).resolves.toEqual({ + id: "m1", + payload: { a: 1, data: "stored text" }, + }); + + client.query.mockResolvedValue({ code: 2, msg: "invalid parameter" }); + await expect(store.get("m1")).rejects.toThrow( + /query 'm1' failed \(code 2\): invalid parameter/, + ); + }); + + // The server signals a missing primary key with code 101; pymochow and mochow-sdk-go both + // name it (ROW_KEY_NOT_FOUND / RowKeyNotFound). The Node SDK's ServerErrCode stops at 100, + // so it has to be spelled out. Memory.get()/update()/delete() all branch on a null here. + it("returns null when the server reports the row key is missing", async () => { + const client = fakeClient(); + const store = makeStore(client); + + client.query.mockResolvedValue({ code: 101, msg: "row key not found" }); + await expect(store.get("nope")).resolves.toBeNull(); + }); + + it("lists flat select rows and reports how many came back", async () => { + const client = fakeClient(); + client.select.mockResolvedValue({ + ...OK, + isTruncated: false, + nextMarker: "", + rows: [{ id: "m1", data: "x", metadata: {} }, { id: "m2" }], + }); + + await expect( + makeStore(client).list({ userId: "alice" }, 50), + ).resolves.toEqual([ + [ + { id: "m1", payload: { data: "x" } }, + { id: "m2", payload: {} }, + ], + 2, + ]); + expect(client.select).toHaveBeenCalledWith({ + database: "mem0_db", + table: "mem0", + filter: 'metadata["userId"] = "alice"', + projections: ["id", "data", "metadata"], + limit: 50, + }); + }); +}); + +describe("BaiduDB deleteCol", () => { + it("waits for the drop to land before returning", async () => { + runTimersInline(); + const client = fakeClient(); + const store = makeStore(client); + await store.initialize(); + + client.descTable + .mockResolvedValueOnce({ ...OK, table: { state: TableState.Deleting } }) + .mockResolvedValueOnce({ + code: ServerErrCode.TableNotExist, + msg: "gone", + }); + + await store.deleteCol(); + expect(client.dropTable).toHaveBeenCalledWith("mem0_db", "mem0"); + expect(client.descTable).toHaveBeenCalledTimes(3); // 1 from initialize + 2 polls + }); + + it("is a no-op when the table is already gone", async () => { + const client = fakeClient(); + const store = makeStore(client); + await store.initialize(); + + client.dropTable.mockResolvedValue({ + code: ServerErrCode.TableNotExist, + msg: "gone", + }); + await expect(store.deleteCol()).resolves.toBeUndefined(); + }); + + // Regression: deleteCol() used to run alongside the fire-and-forget initialize() the + // constructor starts, so the in-flight createTable landed *after* dropTable and the table + // survived reset(). + it("does not race the initialize() the constructor kicks off", async () => { + runTimersInline(); + let release: () => void = () => {}; + const gate = new Promise((resolve) => { + release = resolve; + }); + + // The table exists after the first init, is gone once dropTable lands, then exists again. + const descQueue: unknown[] = [ + normalTable(), + { code: ServerErrCode.TableNotExist, msg: "gone" }, + ]; + const client = fakeClient({ + createDatabase: async () => { + await gate; + return OK; + }, + descTable: async () => descQueue.shift() ?? normalTable(), + }); + + const store = makeStore(client); // initialize() is now in flight, parked on `gate` + const resetting = store.reset(); + release(); + await resetting; + + expect(client.calls).toEqual([ + "createDatabase", + "createTable", + "descTable", + "dropTable", + "descTable", + "createDatabase", + "createTable", + "descTable", + ]); + expect(client.calls.indexOf("dropTable")).toBeGreaterThan( + client.calls.indexOf("createTable"), + ); + expect(client.createTable).toHaveBeenCalledTimes(2); + }); +}); + +describe("BaiduDB user id", () => { + it("round-trips the store user id", async () => { + const store = makeStore(fakeClient()); + await expect(store.getUserId()).resolves.toBe("anonymous-baidu-user"); + await store.setUserId("alice"); + await expect(store.getUserId()).resolves.toBe("alice"); + }); +}); diff --git a/mem0-ts/src/oss/tests/factory.unit.test.ts b/mem0-ts/src/oss/tests/factory.unit.test.ts index af59c47c5..7397f9c93 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/qdrant", () => ({ .fn() .mockImplementation((config) => ({ type: "qdrant", config })), })); +jest.mock("../src/vector_stores/baidu", () => ({ + BaiduDB: jest + .fn() + .mockImplementation((config) => ({ type: "baidu", config })), +})); jest.mock("../src/vector_stores/redis", () => ({ RedisDB: jest .fn() @@ -320,6 +325,7 @@ describe("VectorStoreFactory", () => { }); test.each([ + ["baidu"], ["qdrant"], ["redis"], ["valkey"], @@ -335,9 +341,8 @@ describe("VectorStoreFactory", () => { ["s3_vectors"], ["weaviate"], ])("creates vector store for provider '%s'", (provider) => { - expect(() => - VectorStoreFactory.create(provider, dummyVSConfig), - ).not.toThrow(); + const result = VectorStoreFactory.create(provider, dummyVSConfig) as any; + expect(result.config).toBe(dummyVSConfig); }); test("throws for unsupported provider", () => { diff --git a/mem0-ts/tsup.config.ts b/mem0-ts/tsup.config.ts index 863339632..09626b07f 100644 --- a/mem0-ts/tsup.config.ts +++ b/mem0-ts/tsup.config.ts @@ -5,6 +5,7 @@ const external = [ "openai", "@anthropic-ai/sdk", "@aws-sdk/client-s3vectors", + "@mochow/mochow-sdk-node", "groq-sdk", "cohere-ai", "@huggingface/transformers",