diff --git a/docs/components/embedders/models/together.mdx b/docs/components/embedders/models/together.mdx index 5727d5f1d..b8990760b 100644 --- a/docs/components/embedders/models/together.mdx +++ b/docs/components/embedders/models/together.mdx @@ -1,15 +1,20 @@ --- title: Together -description: "Configure Together AI as an embedding provider in Mem0 with support for 768-dimensional embedding models." +description: "Configure Together AI as an embedding provider in Mem0 with support for 1024-dimensional embedding models." --- -To use Together embedding models, set the `TOGETHER_API_KEY` environment variable. You can obtain the Together API key from the [Together Platform](https://api.together.xyz/settings/api-keys). +To use Together embedding models, set the `TOGETHER_API_KEY` environment variable. You can obtain the Together API key from the [Together Platform](https://api.together.ai/settings/projects/~current/api-keys). ### Usage - The `embedding_model_dims` parameter for `vector_store` should be set to `768` for Together embedder. + The `embedding_model_dims` parameter for `vector_store` should be set to `1024` for Together embedder. -```python + +**Breaking default change.** The default Together embedding model is now `intfloat/multilingual-e5-large-instruct` (**1024-dim**), replacing the previous default `togethercomputer/m2-bert-80M-8k-retrieval` (**768-dim**). If you created a self-hosted vector store with the old default, its collection is 768-dim and will reject the new 1024-dim vectors **recreate/reindex the collection at 1024 dimensions** after upgrading. To defer the change, pin the previous values explicitly (`model="togethercomputer/m2-bert-80M-8k-retrieval"`, `embedding_dims=768`) note Together no longer lists this model among its recommended embeddings, so reindexing at 1024 is the durable path. + + + +```python Python import os from mem0 import Memory @@ -20,7 +25,7 @@ config = { "embedder": { "provider": "together", "config": { - "model": "togethercomputer/m2-bert-80M-8k-retrieval" + "model": "intfloat/multilingual-e5-large-instruct" } } } @@ -29,18 +34,50 @@ m = Memory.from_config(config) 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": "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."} ] m.add(messages, user_id="john") ``` +```typescript TypeScript +import { Memory } from 'mem0ai/oss'; + +const config = { + embedder: { + provider: 'together', + config: { + apiKey: process.env.TOGETHER_API_KEY || '', + model: 'intfloat/multilingual-e5-large-instruct', + embeddingDims: 1024, + }, + }, +}; + +const memory = new Memory(config); +await memory.add("I'm visiting Paris", { userId: "john" }); +``` + + + ### Config Here are the parameters available for configuring Together embedder: + + | Parameter | Description | Default Value | | --- | --- | --- | -| `model` | The name of the embedding model to use | `togethercomputer/m2-bert-80M-8k-retrieval` | -| `embedding_dims` | Dimensions of the embedding model | `768` | +| `model` | The name of the embedding model to use | `intfloat/multilingual-e5-large-instruct` | +| `embedding_dims` | Dimensions of the embedding model | `1024` | | `api_key` | The Together API key | `None` | + + +| Parameter | Description | Default Value | +| --- | --- | --- | +| `model` | The name of the embedding model to use | `intfloat/multilingual-e5-large-instruct` | +| `embeddingDims` | Dimensions of the embedding model for vector store configuration | `1024` | +| `apiKey` | The Together API key | `TOGETHER_API_KEY` | +| `baseURL` | Base URL for an OpenAI-compatible Together endpoint | `https://api.together.ai/v1` | + + diff --git a/docs/components/embedders/overview.mdx b/docs/components/embedders/overview.mdx index 511934baf..0136560b6 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**, and **Ollama**. + 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**. diff --git a/docs/components/llms/models/together.mdx b/docs/components/llms/models/together.mdx index 0584349e6..2f4148c06 100644 --- a/docs/components/llms/models/together.mdx +++ b/docs/components/llms/models/together.mdx @@ -3,7 +3,7 @@ title: Together description: "Configure Together AI as an LLM provider in Mem0 with API key setup and Mixtral model configuration." --- -To use Together LLM models, you have to set the `TOGETHER_API_KEY` environment variable. You can obtain the Together API key from their [Account settings page](https://api.together.xyz/settings/api-keys). +To use Together LLM models, you have to set the `TOGETHER_API_KEY` environment variable. You can obtain the Together API key from their [Account settings page](https://api.together.ai/settings/projects/~current/api-keys). ## Usage @@ -37,4 +37,4 @@ m.add(messages, user_id="alice", metadata={"category": "movies"}) ## Config -All available parameters for the `together` config are present in [Master List of All Params in Config](../config). \ No newline at end of file +All available parameters for the `together` config are present in [Master List of All Params in Config](../config). diff --git a/mem0-ts/src/oss/src/embeddings/together.ts b/mem0-ts/src/oss/src/embeddings/together.ts new file mode 100644 index 000000000..acd1d17e3 --- /dev/null +++ b/mem0-ts/src/oss/src/embeddings/together.ts @@ -0,0 +1,24 @@ +import { OpenAIEmbedder } from "./openai"; +import { EmbeddingConfig } from "../types"; + +const DEFAULT_BASE_URL = "https://api.together.ai/v1"; +const DEFAULT_MODEL = "intfloat/multilingual-e5-large-instruct"; + +export class TogetherEmbedder extends OpenAIEmbedder { + constructor(config: EmbeddingConfig) { + const openAICompatibleConfig = { ...config }; + delete openAICompatibleConfig.embeddingDims; + + const apiKey = config.apiKey || process.env.TOGETHER_API_KEY; + if (!apiKey) { + throw new Error("Together API key is required"); + } + + super({ + ...openAICompatibleConfig, + apiKey, + baseURL: config.baseURL || config.url || DEFAULT_BASE_URL, + model: config.model || DEFAULT_MODEL, + }); + } +} diff --git a/mem0-ts/src/oss/src/index.ts b/mem0-ts/src/oss/src/index.ts index b9940a17d..4ad579376 100644 --- a/mem0-ts/src/oss/src/index.ts +++ b/mem0-ts/src/oss/src/index.ts @@ -5,6 +5,7 @@ export * from "./embeddings/base"; export * from "./embeddings/openai"; export * from "./embeddings/ollama"; export * from "./embeddings/lmstudio"; +export * from "./embeddings/together"; export * from "./embeddings/google"; export * from "./embeddings/azure"; export * from "./embeddings/langchain"; diff --git a/mem0-ts/src/oss/src/utils/factory.ts b/mem0-ts/src/oss/src/utils/factory.ts index efe8289a4..9ca9c26ae 100644 --- a/mem0-ts/src/oss/src/utils/factory.ts +++ b/mem0-ts/src/oss/src/utils/factory.ts @@ -1,6 +1,7 @@ import { OpenAIEmbedder } from "../embeddings/openai"; import { OllamaEmbedder } from "../embeddings/ollama"; import { LMStudioEmbedder } from "../embeddings/lmstudio"; +import { TogetherEmbedder } from "../embeddings/together"; import { OpenAILLM } from "../llms/openai"; import { OpenAIStructuredLLM } from "../llms/openai_structured"; import { AnthropicLLM } from "../llms/anthropic"; @@ -51,6 +52,8 @@ export class EmbedderFactory { return new OllamaEmbedder(config); case "lmstudio": return new LMStudioEmbedder(config); + case "together": + return new TogetherEmbedder(config); case "google": case "gemini": return new GoogleEmbedder(config); diff --git a/mem0-ts/src/oss/tests/factory.unit.test.ts b/mem0-ts/src/oss/tests/factory.unit.test.ts index 351dae885..d5a05aeb3 100644 --- a/mem0-ts/src/oss/tests/factory.unit.test.ts +++ b/mem0-ts/src/oss/tests/factory.unit.test.ts @@ -35,6 +35,11 @@ jest.mock("../src/embeddings/lmstudio", () => ({ .fn() .mockImplementation((config) => ({ type: "lmstudio-embedder", config })), })); +jest.mock("../src/embeddings/together", () => ({ + TogetherEmbedder: jest + .fn() + .mockImplementation((config) => ({ type: "together-embedder", config })), +})); jest.mock("../src/llms/openai", () => ({ OpenAILLM: jest @@ -190,6 +195,7 @@ describe("EmbedderFactory", () => { ["azure_openai"], ["langchain"], ["lmstudio"], + ["together"], ])("creates embedder for provider '%s'", (provider) => { expect(() => EmbedderFactory.create(provider, dummyEmbedConfig), diff --git a/mem0-ts/src/oss/tests/together-embedder.test.ts b/mem0-ts/src/oss/tests/together-embedder.test.ts new file mode 100644 index 000000000..6c667ac29 --- /dev/null +++ b/mem0-ts/src/oss/tests/together-embedder.test.ts @@ -0,0 +1,119 @@ +/// + +const mockEmbeddingsCreate = jest.fn(); +const mockOpenAI = jest.fn().mockImplementation(() => ({ + embeddings: { create: mockEmbeddingsCreate }, +})); + +jest.mock("openai", () => ({ + __esModule: true, + default: mockOpenAI, +})); + +import { TogetherEmbedder } from "../src/embeddings/together"; + +const mockEmbedding = [0.1, 0.2, 0.3]; +const originalEnv = process.env; + +describe("TogetherEmbedder (unit)", () => { + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + delete process.env.TOGETHER_API_KEY; + mockOpenAI.mockClear(); + mockEmbeddingsCreate.mockReset(); + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ index: 0, embedding: mockEmbedding }], + }); + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it("uses Together defaults with an API key from config", async () => { + const embedder = new TogetherEmbedder({ apiKey: "test-key" }); + + await embedder.embed("hello"); + + expect(mockOpenAI).toHaveBeenCalledWith({ + apiKey: "test-key", + baseURL: "https://api.together.ai/v1", + }); + expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ + model: "intfloat/multilingual-e5-large-instruct", + input: "hello", + encoding_format: "float", + }); + }); + + it("uses TOGETHER_API_KEY when config apiKey is not provided", async () => { + process.env.TOGETHER_API_KEY = "env-key"; + + const embedder = new TogetherEmbedder({}); + + await embedder.embed("hello"); + + expect(mockOpenAI).toHaveBeenCalledWith({ + apiKey: "env-key", + baseURL: "https://api.together.ai/v1", + }); + }); + + it("supports custom model and baseURL without forwarding embeddingDims", async () => { + const embedder = new TogetherEmbedder({ + apiKey: "test-key", + model: "custom-together-embed", + baseURL: "https://proxy.example.com/v1", + embeddingDims: 512, + }); + + await embedder.embed("hello"); + + expect(mockOpenAI).toHaveBeenCalledWith({ + apiKey: "test-key", + baseURL: "https://proxy.example.com/v1", + }); + expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ + model: "custom-together-embed", + input: "hello", + encoding_format: "float", + }); + }); + + it("uses url as a baseURL fallback", async () => { + const embedder = new TogetherEmbedder({ + apiKey: "test-key", + url: "https://url-fallback.example.com/v1", + }); + + await embedder.embed("hello"); + + expect(mockOpenAI).toHaveBeenCalledWith({ + apiKey: "test-key", + baseURL: "https://url-fallback.example.com/v1", + }); + }); + + it("sorts batch embeddings by response index", async () => { + mockEmbeddingsCreate.mockResolvedValueOnce({ + data: [ + { index: 1, embedding: [0.3, 0.4] }, + { index: 0, embedding: [0.1, 0.2] }, + ], + }); + + const embedder = new TogetherEmbedder({ apiKey: "test-key" }); + + await expect(embedder.embedBatch(["first", "second"])).resolves.toEqual([ + [0.1, 0.2], + [0.3, 0.4], + ]); + }); + + it("throws when no API key is available", () => { + expect(() => new TogetherEmbedder({})).toThrow( + "Together API key is required", + ); + }); +}); diff --git a/mem0/embeddings/together.py b/mem0/embeddings/together.py index 253c40a4c..830747d98 100644 --- a/mem0/embeddings/together.py +++ b/mem0/embeddings/together.py @@ -11,10 +11,9 @@ class TogetherEmbedding(EmbeddingBase): def __init__(self, config: Optional[BaseEmbedderConfig] = None): super().__init__(config) - self.config.model = self.config.model or "togethercomputer/m2-bert-80M-8k-retrieval" + self.config.model = self.config.model or "intfloat/multilingual-e5-large-instruct" api_key = self.config.api_key or os.getenv("TOGETHER_API_KEY") - # TODO: check if this is correct - self.config.embedding_dims = self.config.embedding_dims or 768 + self.config.embedding_dims = self.config.embedding_dims or 1024 self.client = Together(api_key=api_key) def embed(self, text, memory_action: Optional[Literal["add", "search", "update"]] = None): diff --git a/tests/embeddings/test_together_embeddings.py b/tests/embeddings/test_together_embeddings.py index 86dbe7939..ede95ec12 100644 --- a/tests/embeddings/test_together_embeddings.py +++ b/tests/embeddings/test_together_embeddings.py @@ -5,6 +5,9 @@ import pytest from mem0.configs.embeddings.base import BaseEmbedderConfig from mem0.embeddings.together import TogetherEmbedding +DEFAULT_MODEL = "intfloat/multilingual-e5-large-instruct" +DEFAULT_EMBEDDING_DIMS = 1024 + @pytest.fixture def mock_together_client(): @@ -15,7 +18,7 @@ def mock_together_client(): def test_embed_text(mock_together_client): - config = BaseEmbedderConfig(model="togethercomputer/m2-bert-80M-8k-retrieval", embedding_dims=768) + config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS) embedder = TogetherEmbedding(config) mock_together_client.embeddings.create.return_value = Mock(data=[Mock(embedding=[0.1, 0.2, 0.3, 0.4, 0.5])]) @@ -23,14 +26,12 @@ def test_embed_text(mock_together_client): text = "Sample text to embed." embedding = embedder.embed(text) - mock_together_client.embeddings.create.assert_called_once_with( - model="togethercomputer/m2-bert-80M-8k-retrieval", input=text - ) + mock_together_client.embeddings.create.assert_called_once_with(model=DEFAULT_MODEL, input=text) assert embedding == [0.1, 0.2, 0.3, 0.4, 0.5] def test_embed_batch_single_call(mock_together_client): - config = BaseEmbedderConfig(model="togethercomputer/m2-bert-80M-8k-retrieval", embedding_dims=768) + config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS) embedder = TogetherEmbedding(config) mock_item0 = Mock(index=0, embedding=[0.1, 0.2, 0.3]) @@ -40,14 +41,12 @@ def test_embed_batch_single_call(mock_together_client): texts = ["First text.", "Second text."] embeddings = embedder.embed_batch(texts) - mock_together_client.embeddings.create.assert_called_once_with( - model="togethercomputer/m2-bert-80M-8k-retrieval", input=texts - ) + mock_together_client.embeddings.create.assert_called_once_with(model=DEFAULT_MODEL, input=texts) assert embeddings == [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]] def test_embed_batch_empty_list(mock_together_client): - config = BaseEmbedderConfig(model="togethercomputer/m2-bert-80M-8k-retrieval", embedding_dims=768) + config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS) embedder = TogetherEmbedding(config) result = embedder.embed_batch([]) @@ -57,7 +56,7 @@ def test_embed_batch_empty_list(mock_together_client): def test_embed_batch_count_mismatch_raises(mock_together_client): - config = BaseEmbedderConfig(model="togethercomputer/m2-bert-80M-8k-retrieval", embedding_dims=768) + config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS) embedder = TogetherEmbedding(config) mock_item0 = Mock(index=0, embedding=[0.1, 0.2, 0.3]) @@ -65,3 +64,24 @@ def test_embed_batch_count_mismatch_raises(mock_together_client): with pytest.raises(ValueError, match="returned 1 embeddings for 2 texts"): embedder.embed_batch(["first text", "second text"]) + + +def test_default_config_applies_together_defaults(mock_together_client): + embedder = TogetherEmbedding(BaseEmbedderConfig()) + + assert embedder.config.model == DEFAULT_MODEL + assert embedder.config.embedding_dims == DEFAULT_EMBEDDING_DIMS + + +def test_explicit_config_overrides_defaults(mock_together_client): + # The `config.x or default` wiring must honor user-provided values, not clobber them. + config = BaseEmbedderConfig(model="BAAI/bge-base-en-v1.5", embedding_dims=768) + embedder = TogetherEmbedding(config) + + assert embedder.config.model == "BAAI/bge-base-en-v1.5" + assert embedder.config.embedding_dims == 768 + + # ...and the chosen model actually reaches the Together API call. + mock_together_client.embeddings.create.return_value = Mock(data=[Mock(embedding=[0.0] * 768)]) + embedder.embed("hello") + mock_together_client.embeddings.create.assert_called_once_with(model="BAAI/bge-base-en-v1.5", input="hello")