diff --git a/docs/components/embedders/models/fastembed.mdx b/docs/components/embedders/models/fastembed.mdx index d1f6d3531..f363839ba 100644 --- a/docs/components/embedders/models/fastembed.mdx +++ b/docs/components/embedders/models/fastembed.mdx @@ -7,10 +7,18 @@ You can use FastEmbed to run embedding models locally in Mem0. FastEmbed is an O ### Installation -```bash +FastEmbed is an optional dependency, so install it alongside Mem0. + + +```bash Python pip install fastembed ``` +```bash TypeScript +npm install fastembed +``` + + ### Usage @@ -38,13 +46,66 @@ messages = [ ] m.add(messages, user_id="john") ``` + +```typescript TypeScript +import { Memory } from "mem0ai/oss"; + +// FastEmbed needs no API key. Leave the embedder config empty to use the +// default model (fast-bge-small-en-v1.5), or set `model` to one of the +// supported models listed below. +const memory = new Memory({ + embedder: { + provider: "fastembed", + config: { + model: "fast-bge-small-en-v1.5", + }, + }, + llm: { + provider: "openai", + config: { apiKey: process.env.OPENAI_API_KEY }, // For fact extraction + }, +}); + +const messages = [ + { role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" }, + { role: "assistant", content: "How about thriller movies? 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: "john" }); +``` + + **The Python and TypeScript SDKs default to different models.** Python defaults to `thenlper/gte-large` (1024 dimensions), while TypeScript defaults to `fast-bge-small-en-v1.5` (384 dimensions). The TypeScript package (`fastembed` on npm) ships a fixed set of ONNX models and does not include `thenlper/gte-large`. Because the two defaults produce vectors of different dimensions, do not point both SDKs at the same vector store collection unless you configure them to use the same model. + + +The TypeScript SDK supports these FastEmbed models. Pass the exact string as `model`: + +- `fast-bge-small-en-v1.5` (default) +- `fast-bge-small-en` +- `fast-bge-base-en` +- `fast-bge-base-en-v1.5` +- `fast-bge-small-zh-v1.5` +- `fast-all-MiniLM-L6-v2` +- `fast-multilingual-e5-large` + ### Config -Here are the parameters available for configuring FastEmbed embedder: +Here are the parameters available for configuring the FastEmbed embedder: + + | Parameter | Description | Default Value | | --- | --- | --- | | `model` | The name of the FastEmbed model to use | `thenlper/gte-large` | | `embedding_dims` | Dimensions of the embedding model (auto-derived from the model if not set) | `None` | + + +| Parameter | Description | Default Value | +| --- | --- | --- | +| `model` | The FastEmbed model to use (see the supported list above) | `fast-bge-small-en-v1.5` | + +The embedding dimension is detected automatically at startup, so you do not need to set it manually. + + diff --git a/docs/components/embedders/overview.mdx b/docs/components/embedders/overview.mdx index 0136560b6..ab473cde6 100644 --- a/docs/components/embedders/overview.mdx +++ b/docs/components/embedders/overview.mdx @@ -10,7 +10,7 @@ Mem0 offers support for various embedding models, allowing users to choose the o See the list of supported embedders below. - All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **Google AI**, **Langchain**, **LM Studio**, **Ollama**, and **Together**. + All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **FastEmbed**, **Google AI**, **Langchain**, **LM Studio**, **Ollama**, and **Together**. diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 2a25304e8..7e45904ac 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -122,6 +122,7 @@ "@types/pg": "8.11.0", "better-sqlite3": "^12.6.2", "cloudflare": "^4.2.0", + "fastembed": "^2.1.0", "groq-sdk": "0.3.0", "ollama": "^0.5.14", "pg": "8.11.3", diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml index 8b9c59b50..449cc2dc7 100644 --- a/mem0-ts/pnpm-lock.yaml +++ b/mem0-ts/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: compromise: specifier: ^14.0.0 version: 14.15.1 + fastembed: + specifier: ^2.1.0 + version: 2.1.0 groq-sdk: specifier: 0.3.0 version: 0.3.0 @@ -153,6 +156,27 @@ packages: '@anthropic-ai/sdk@0.40.1': resolution: {integrity: sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==} + '@anush008/tokenizers-darwin-universal@0.0.0': + resolution: {integrity: sha512-SACpWEooTjFX89dFKRVUhivMxxcZRtA3nJGVepdLyrwTkQ1TZQ8581B5JoXp0TcTMHfgnDaagifvVoBiFEdNCQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@anush008/tokenizers-linux-x64-gnu@0.0.0': + resolution: {integrity: sha512-TLjByOPWUEq51L3EJkS+slyH57HKJ7lAz/aBtEt7TIPq4QsE2owOPGovByOLIq1x5Wgh9b+a4q2JasrEFSDDhg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@anush008/tokenizers-win32-x64-msvc@0.0.0': + resolution: {integrity: sha512-/5kP0G96+Cr6947F0ZetXnmL31YCaN15dbNbh2NHg7TXXRwfqk95+JtPP5Q7v4jbR2xxAmuseBqB4H/V7zKWuw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@anush008/tokenizers@0.0.0': + resolution: {integrity: sha512-IQD9wkVReKAhsEAbDjh/0KrBGTEXelqZLpOBRDaIRvlzZ9sjmUP+gKbpvzyJnei2JHQiE8JAgj7YcNloINbGBw==} + engines: {node: '>= 10'} + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -677,6 +701,20 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@huggingface/blake3-jit@0.0.2': + resolution: {integrity: sha512-Bq7B5qabyjrJfhBsl85Jd2QBtf+HzRD7h7A9GfN2lzrrsABhOa5evVPgzoCTxR7Ub0QFj7YDK1YkYRWBU25+2w==} + + '@huggingface/hub@2.13.2': + resolution: {integrity: sha512-I1Pbn9UcBHCt0M/SbZkKzYr3JVnbJp4YIFv74eVRH2Y22yT9Jpm6DtfM74tcjNLbC7SyFZ433znUw0szdFd4Fg==} + engines: {node: '>=18'} + hasBin: true + + '@huggingface/tasks@0.21.21': + resolution: {integrity: sha512-6cHtkeMvgbudI6p4+gPP/+xvcAG2uumF+ntojCSNnHy5xcSzuHXCMJHzh+rlX0TSZ/QdDZ4uWkPLQikbphNyEw==} + + '@huggingface/xetchunk-wasm@0.1.0': + resolution: {integrity: sha512-wWpp2qwPgf9kv1KLJjcDUk/OrpDOsFoQ3Qpz0U5LGn20csoymBf8eneOv6wm/GzPBzlWac1OYiR0aa1vT6aM2Q==} + '@iovalkey/commands@0.1.0': resolution: {integrity: sha512-/B9W4qKSSITDii5nkBCHyPkIkAi+ealUtr1oqBJsLxjSRLka4pxun2VvMNSmcwgAMxgXtQfl0qRv7TE+udPJzg==} @@ -684,6 +722,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -1434,6 +1476,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1535,6 +1581,14 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1542,6 +1596,10 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1648,10 +1706,18 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1668,6 +1734,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1736,6 +1805,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -1749,6 +1821,10 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -1790,6 +1866,9 @@ packages: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true + fastembed@2.1.0: + resolution: {integrity: sha512-oQkpcRHBppJ3+a3w9dU0uytSY0N1cnEa/iVMc8AXEd+tvT529GekOEFhNviJy89R3lvQXF6cdIMTXHj1Gi00xQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1858,6 +1937,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1877,6 +1960,9 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} + gearhash-jit@1.0.2: + resolution: {integrity: sha512-UhzJL4KXSdqAKepy/tZwmi2Rcy0YMmtiC4DQS4SURCuIWdh8ECZtnXK2ePRMLigfB61hRKdLK/Vgg2bSw73izQ==} + generic-pool@3.9.0: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} engines: {node: '>= 4'} @@ -1921,6 +2007,14 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + google-auth-library@10.7.0: resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==} engines: {node: '>=18'} @@ -1956,6 +2050,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2267,6 +2364,9 @@ packages: resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2388,6 +2488,10 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2443,13 +2547,34 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -2569,6 +2694,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -2582,6 +2711,13 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onnxruntime-common@1.21.0: + resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==} + + onnxruntime-node@1.21.0: + resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==} + os: [win32, darwin, linux] + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -2807,6 +2943,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2906,6 +3046,10 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rollup@4.61.1: resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2925,6 +3069,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2934,6 +3081,10 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2999,6 +3150,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -3087,6 +3241,15 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tar@7.5.19: + resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + engines: {node: '>=18'} + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -3200,6 +3363,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -3353,6 +3520,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3391,6 +3562,21 @@ snapshots: transitivePeerDependencies: - encoding + '@anush008/tokenizers-darwin-universal@0.0.0': + optional: true + + '@anush008/tokenizers-linux-x64-gnu@0.0.0': + optional: true + + '@anush008/tokenizers-win32-x64-msvc@0.0.0': + optional: true + + '@anush008/tokenizers@0.0.0': + optionalDependencies: + '@anush008/tokenizers-darwin-universal': 0.0.0 + '@anush008/tokenizers-linux-x64-gnu': 0.0.0 + '@anush008/tokenizers-win32-x64-msvc': 0.0.0 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -4162,6 +4348,22 @@ snapshots: - supports-color - utf-8-validate + '@huggingface/blake3-jit@0.0.2': {} + + '@huggingface/hub@2.13.2': + dependencies: + '@huggingface/tasks': 0.21.21 + '@huggingface/xetchunk-wasm': 0.1.0 + optionalDependencies: + cli-progress: 3.12.0 + + '@huggingface/tasks@0.21.21': {} + + '@huggingface/xetchunk-wasm@0.1.0': + dependencies: + '@huggingface/blake3-jit': 0.0.2 + gearhash-jit: 1.0.2 + '@iovalkey/commands@0.1.0': {} '@isaacs/cliui@8.0.2': @@ -4173,6 +4375,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -5035,6 +5241,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + boolean@3.2.0: {} + bowser@2.14.1: {} brace-expansion@1.1.15: @@ -5134,10 +5342,19 @@ snapshots: chownr@1.1.4: {} + chownr@2.0.0: {} + + chownr@3.0.0: {} + ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + optional: true + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -5238,8 +5455,20 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delayed-stream@1.0.0: {} denque@2.1.0: {} @@ -5248,6 +5477,8 @@ snapshots: detect-newline@3.1.0: {} + detect-node@2.1.0: {} + diff-sequences@29.6.3: {} diff@4.0.4: {} @@ -5306,6 +5537,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es6-error@4.1.1: {} + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -5339,6 +5572,8 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + event-target-shim@5.0.1: {} eventemitter3@4.0.7: {} @@ -5385,6 +5620,14 @@ snapshots: dependencies: strnum: 2.4.1 + fastembed@2.1.0: + dependencies: + '@anush008/tokenizers': 0.0.0 + '@huggingface/hub': 2.13.2 + onnxruntime-node: 1.21.0 + progress: 2.0.3 + tar: 6.2.1 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5454,6 +5697,10 @@ snapshots: fs-constants@1.0.0: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5477,6 +5724,8 @@ snapshots: transitivePeerDependencies: - supports-color + gearhash-jit@1.0.2: {} + generic-pool@3.9.0: {} gensync@1.0.0-beta.2: {} @@ -5529,6 +5778,20 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.8.4 + serialize-error: 7.0.1 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + google-auth-library@10.7.0: dependencies: base64-js: 1.5.1 @@ -5575,6 +5838,10 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -6070,6 +6337,8 @@ snapshots: json-parse-even-better-errors@3.0.2: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonwebtoken@9.0.3: @@ -6165,6 +6434,10 @@ snapshots: dependencies: tmpl: 1.0.5 + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + math-intrinsics@1.1.0: {} md5@2.3.0: @@ -6210,10 +6483,27 @@ snapshots: minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -6344,6 +6634,8 @@ snapshots: object-assign@4.1.1: {} + object-keys@1.1.1: {} + obuf@1.1.2: {} ollama@0.5.18: @@ -6358,6 +6650,14 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onnxruntime-common@1.21.0: {} + + onnxruntime-node@1.21.0: + dependencies: + global-agent: 3.0.0 + onnxruntime-common: 1.21.0 + tar: 7.5.19 + open@10.2.0: dependencies: default-browser: 5.5.0 @@ -6574,6 +6874,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + progress@2.0.3: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -6688,6 +6990,15 @@ snapshots: dependencies: glob: 10.5.0 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + rollup@4.61.1: dependencies: '@types/estree': 1.0.9 @@ -6729,10 +7040,16 @@ snapshots: safe-stable-stringify@2.5.0: {} + semver-compare@1.0.0: {} + semver@6.3.1: {} semver@7.8.4: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6790,6 +7107,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.1.3: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -6882,6 +7201,23 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tar@7.5.19: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.6 @@ -6995,6 +7331,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.13.1: {} + type-fest@0.21.3: {} type-fest@3.13.1: {} @@ -7108,6 +7446,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/mem0-ts/src/oss/src/config/manager.ts b/mem0-ts/src/oss/src/config/manager.ts index 4e45b4b38..c72692583 100644 --- a/mem0-ts/src/oss/src/config/manager.ts +++ b/mem0-ts/src/oss/src/config/manager.ts @@ -3,16 +3,22 @@ import { DEFAULT_MEMORY_CONFIG } from "./defaults"; export class ConfigManager { static mergeConfig(userConfig: Partial = {}): MemoryConfig { + const embedderProvider = + userConfig.embedder?.provider || DEFAULT_MEMORY_CONFIG.embedder.provider; + const embedderProviderKey = embedderProvider.toLowerCase(); const mergedConfig = { version: userConfig.version || DEFAULT_MEMORY_CONFIG.version, embedder: { - provider: - userConfig.embedder?.provider || - DEFAULT_MEMORY_CONFIG.embedder.provider, + provider: embedderProvider, config: (() => { const defaultConf = DEFAULT_MEMORY_CONFIG.embedder.config; const userConf = userConfig.embedder?.config; - let finalModel: string | any = defaultConf.model; + // The default embedder model (OpenAI's text-embedding-3-small) only + // makes sense for API-based providers. FastEmbed has its own fixed + // model set and default, so leave the model unset here and let + // FastEmbedEmbedder fall back to its own default. + let finalModel: string | any = + embedderProviderKey === "fastembed" ? undefined : defaultConf.model; if (userConf?.model && typeof userConf.model === "object") { finalModel = userConf.model; diff --git a/mem0-ts/src/oss/src/embeddings/fastembed.ts b/mem0-ts/src/oss/src/embeddings/fastembed.ts new file mode 100644 index 000000000..6c298ef50 --- /dev/null +++ b/mem0-ts/src/oss/src/embeddings/fastembed.ts @@ -0,0 +1,75 @@ +import { EmbeddingModel, FlagEmbedding } from "fastembed"; +import { Embedder } from "./base"; +import { EmbeddingConfig } from "../types"; + +const DEFAULT_MODEL = EmbeddingModel.BGESmallENV15; +type FastEmbedModel = Exclude; + +// FastEmbed only ships a fixed set of ONNX models. Keep the list handy so we can +// reject unknown model names up front with a clear message instead of letting +// FlagEmbedding.init fail later with an opaque download error. +const SUPPORTED_MODELS = Object.values(EmbeddingModel).filter( + (model) => model !== EmbeddingModel.CUSTOM, +) as FastEmbedModel[]; + +export class FastEmbedEmbedder implements Embedder { + private modelName: FastEmbedModel; + private embeddingModel?: Promise; + + constructor(config: EmbeddingConfig) { + if (typeof config.model === "string" && config.model.length > 0) { + if (!SUPPORTED_MODELS.includes(config.model as FastEmbedModel)) { + throw new Error( + `Unsupported FastEmbed model "${config.model}". ` + + `Supported models: ${SUPPORTED_MODELS.join(", ")}.`, + ); + } + this.modelName = config.model as FastEmbedModel; + } else { + this.modelName = DEFAULT_MODEL; + } + } + + private getEmbeddingModel(): Promise { + if (!this.embeddingModel) { + this.embeddingModel = FlagEmbedding.init({ + model: this.modelName, + }).catch((error) => { + this.embeddingModel = undefined; + throw error; + }); + } + + return this.embeddingModel; + } + + private normalizeInput(text: string): string { + return text.replace(/\n/g, " "); + } + + async embed(text: string): Promise { + const normalizedText = this.normalizeInput(text); + const model = await this.getEmbeddingModel(); + + for await (const batch of model.embed([normalizedText])) { + const embedding = batch[0]; + if (embedding !== undefined) { + return embedding; + } + } + + throw new Error("FastEmbed embed() returned no embeddings"); + } + + async embedBatch(texts: string[]): Promise { + const normalizedTexts = texts.map((text) => this.normalizeInput(text)); + const model = await this.getEmbeddingModel(); + const embeddings: number[][] = []; + + for await (const batch of model.embed(normalizedTexts)) { + embeddings.push(...batch); + } + + return embeddings; + } +} diff --git a/mem0-ts/src/oss/src/index.ts b/mem0-ts/src/oss/src/index.ts index 4ad579376..c1d2a8995 100644 --- a/mem0-ts/src/oss/src/index.ts +++ b/mem0-ts/src/oss/src/index.ts @@ -9,6 +9,7 @@ export * from "./embeddings/together"; export * from "./embeddings/google"; export * from "./embeddings/azure"; export * from "./embeddings/langchain"; +export * from "./embeddings/fastembed"; export * from "./llms/base"; export * from "./llms/openai"; export * from "./llms/google"; diff --git a/mem0-ts/src/oss/src/utils/factory.ts b/mem0-ts/src/oss/src/utils/factory.ts index 076436527..cfd935838 100644 --- a/mem0-ts/src/oss/src/utils/factory.ts +++ b/mem0-ts/src/oss/src/utils/factory.ts @@ -36,6 +36,7 @@ import { GoogleEmbedder } from "../embeddings/google"; import { GoogleLLM } from "../llms/google"; import { AzureOpenAILLM } from "../llms/azure"; import { AzureOpenAIEmbedder } from "../embeddings/azure"; +import { FastEmbedEmbedder } from "../embeddings/fastembed"; import { LangchainLLM } from "../llms/langchain"; import { LangchainEmbedder } from "../embeddings/langchain"; import { LangchainVectorStore } from "../vector_stores/langchain"; @@ -60,6 +61,8 @@ export class EmbedderFactory { return new GoogleEmbedder(config); case "azure_openai": return new AzureOpenAIEmbedder(config); + case "fastembed": + return new FastEmbedEmbedder(config); case "langchain": return new LangchainEmbedder(config); default: diff --git a/mem0-ts/src/oss/tests/config-manager.test.ts b/mem0-ts/src/oss/tests/config-manager.test.ts index fa88d5bbc..8655055ae 100644 --- a/mem0-ts/src/oss/tests/config-manager.test.ts +++ b/mem0-ts/src/oss/tests/config-manager.test.ts @@ -298,6 +298,38 @@ describe("ConfigManager", () => { }); }); + describe("mergeConfig - FastEmbed defaults", () => { + const baseLlm = { provider: "openai", config: { apiKey: "k" } }; + + it("does not inject the OpenAI default embedder model into fastembed", () => { + const cfg = ConfigManager.mergeConfig({ + embedder: { + provider: "fastembed", + config: {}, + }, + vectorStore: { provider: "memory", config: {} }, + llm: baseLlm, + }); + + expect(cfg.embedder.provider).toBe("fastembed"); + expect(cfg.embedder.config.model).toBeUndefined(); + }); + + it("treats FastEmbed provider casing the same way as the factory", () => { + const cfg = ConfigManager.mergeConfig({ + embedder: { + provider: "FastEmbed", + config: {}, + }, + vectorStore: { provider: "memory", config: {} }, + llm: baseLlm, + }); + + expect(cfg.embedder.provider).toBe("FastEmbed"); + expect(cfg.embedder.config.model).toBeUndefined(); + }); + }); + describe("mergeConfig - LM Studio LLM config", () => { const baseEmbedder = { provider: "openai", config: { apiKey: "k" } }; @@ -603,6 +635,30 @@ describe("Memory – LM Studio end-to-end flow", () => { expect(result.results[0].memory).toBe("User likes hiking"); }); + it("preserves the FastEmbed provider default model through the Memory config path", async () => { + const mem = new MemoryClass({ + embedder: { + provider: "fastembed", + config: {}, + }, + vectorStore: { provider: "memory", config: { collectionName: "test" } }, + llm: { + provider: "openai", + config: { apiKey: "test-key" }, + }, + disableHistory: true, + }); + + await mem.getAll({ filters: { user_id: "u1" } }); + + expect(mockEmbedderFactory.create).toHaveBeenCalledWith( + "fastembed", + expect.objectContaining({ + model: undefined, + }), + ); + }); + it("add flow works with lmstudio LLM for fact extraction", async () => { mockLlm.generateResponse.mockResolvedValueOnce( '{"facts":["User loves sushi"]}', diff --git a/mem0-ts/src/oss/tests/factory.unit.test.ts b/mem0-ts/src/oss/tests/factory.unit.test.ts index d5a05aeb3..85f6858d6 100644 --- a/mem0-ts/src/oss/tests/factory.unit.test.ts +++ b/mem0-ts/src/oss/tests/factory.unit.test.ts @@ -20,6 +20,11 @@ jest.mock("../src/embeddings/google", () => ({ .fn() .mockImplementation((config) => ({ type: "google-embedder", config })), })); +jest.mock("../src/embeddings/fastembed", () => ({ + FastEmbedEmbedder: jest + .fn() + .mockImplementation((config) => ({ type: "fastembed-embedder", config })), +})); jest.mock("../src/embeddings/azure", () => ({ AzureOpenAIEmbedder: jest .fn() @@ -193,6 +198,7 @@ describe("EmbedderFactory", () => { ["google"], ["gemini"], ["azure_openai"], + ["fastembed"], ["langchain"], ["lmstudio"], ["together"], diff --git a/mem0-ts/src/oss/tests/fastembed-embedder.test.ts b/mem0-ts/src/oss/tests/fastembed-embedder.test.ts new file mode 100644 index 000000000..dd90a301f --- /dev/null +++ b/mem0-ts/src/oss/tests/fastembed-embedder.test.ts @@ -0,0 +1,123 @@ +/// +/** + * FastEmbed Embedder — unit tests (mocked FastEmbed client). + * Verifies async generator handling for embed and embedBatch. + */ + +const mockInit = jest.fn(); +const mockEmbed = jest.fn(); + +jest.mock("fastembed", () => { + return { + __esModule: true, + EmbeddingModel: { + BGESmallENV15: "fast-bge-small-en-v1.5", + }, + FlagEmbedding: { + init: mockInit, + }, + }; +}); + +import { FastEmbedEmbedder } from "../src/embeddings/fastembed"; + +const mockVectorBatch = [ + [0.1, 0.2], + [0.3, 0.4], +]; +const mockEmbedding = [0.1, 0.2, 0.3]; + +const createEmbeddingGenerator = async function* (batches: number[][][]) { + for (const batch of batches) { + yield batch; + } +}; + +describe("FastEmbedEmbedder (unit)", () => { + beforeEach(() => { + mockInit.mockReset(); + mockEmbed.mockReset(); + + mockInit.mockResolvedValue({ + embed: mockEmbed, + }); + }); + + it("embed() initializes lazily, normalizes newlines, and returns the first vector", async () => { + mockEmbed.mockReturnValue( + createEmbeddingGenerator([[mockEmbedding], mockVectorBatch]), + ); + const embedder = new FastEmbedEmbedder({ + model: "fast-bge-small-en-v1.5", + }); + + expect(mockInit).not.toHaveBeenCalled(); + + const result = await embedder.embed("Example\ntext"); + + expect(mockInit).toHaveBeenCalledTimes(1); + expect(mockInit).toHaveBeenCalledWith({ + model: "fast-bge-small-en-v1.5", + }); + expect(mockEmbed).toHaveBeenCalledTimes(1); + expect(mockEmbed.mock.calls[0][0]).toEqual(["Example text"]); + expect(result).toEqual(mockEmbedding); + }); + + it("embedBatch() consumes all async embedding batches and reuses the initialized model", async () => { + mockEmbed.mockReturnValueOnce(createEmbeddingGenerator([[mockEmbedding]])); + mockEmbed.mockReturnValueOnce( + createEmbeddingGenerator([[mockEmbedding], mockVectorBatch]), + ); + const embedder = new FastEmbedEmbedder({}); + + await embedder.embed("warmup"); + const result = await embedder.embedBatch([ + "first text", + "second text", + "third text", + ]); + + expect(mockEmbed).toHaveBeenCalledWith([ + "first text", + "second text", + "third text", + ]); + expect(mockInit).toHaveBeenCalledTimes(1); + expect(result).toEqual([mockEmbedding, ...mockVectorBatch]); + }); + + it("embed() throws when FastEmbed yields no embeddings", async () => { + mockEmbed.mockReturnValue(createEmbeddingGenerator([])); + const embedder = new FastEmbedEmbedder({}); + + await expect(embedder.embed("missing")).rejects.toThrow( + "FastEmbed embed() returned no embeddings", + ); + }); + + it("embed() retries initialization after a transient init failure", async () => { + mockInit.mockRejectedValueOnce(new Error("transient init failure")); + mockInit.mockResolvedValueOnce({ + embed: mockEmbed, + }); + mockEmbed.mockReturnValue(createEmbeddingGenerator([[mockEmbedding]])); + const embedder = new FastEmbedEmbedder({}); + + await expect(embedder.embed("first attempt")).rejects.toThrow( + "transient init failure", + ); + + const result = await embedder.embed("second attempt"); + + expect(mockInit).toHaveBeenCalledTimes(2); + expect(result).toEqual(mockEmbedding); + }); + + it("throws a clear error for an unsupported model instead of failing at init", () => { + expect( + () => new FastEmbedEmbedder({ model: "text-embedding-3-small" }), + ).toThrow(/Unsupported FastEmbed model "text-embedding-3-small"/); + expect(mockInit).not.toHaveBeenCalled(); + }); +}); diff --git a/mem0-ts/tsup.config.ts b/mem0-ts/tsup.config.ts index 47ced0207..11d5bf342 100644 --- a/mem0-ts/tsup.config.ts +++ b/mem0-ts/tsup.config.ts @@ -23,6 +23,7 @@ const external = [ "cloudflare", "@cloudflare/workers-types", "@langchain/core", + "fastembed", "compromise", "natural", ];