Skip to content

Commit 2dfa0e6

Browse files
authored
[vibe coding] feat: add mistral embedding function support (#153)
## Summary Fix #135. Integrate [Mistral](https://docs.morphllm.com/api-reference/endpoint/embedding) embedding function ## Solution Description Mistral test result: ``` uv run pytest tests/unit_tests/test_mistral_embedding_function.py -vv ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.11.13, pytest-9.0.2, pluggy-1.6.0 -- /pyseekdb/.venv/bin/python3 cachedir: .pytest_cache rootdir: /pyseekdb configfile: pyproject.toml plugins: anyio-4.12.1 collected 12 items tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_mistral_env PASSED [ 8%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_defaults PASSED [ 16%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_custom_api_key_env PASSED [ 25%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_custom_api_base PASSED [ 33%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_additional_kwargs PASSED [ 41%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_missing_api_key PASSED [ 50%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_dimension_property_for_known_model PASSED [ 58%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_generation_single_document PASSED [ 66%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_generation_multiple_documents PASSED [ 75%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_with_empty_input PASSED [ 83%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_dimension_of_function PASSED [ 91%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_persistence PASSED [100%] ================================================================================ 12 passed in 3.22s ================================================================================= <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for Mistral text embeddings: a new embedding provider is available and can be configured via environment variable and standard embedding settings; supports batch inputs and serialization. * **Tests** * Added comprehensive unit tests for the Mistral embedding provider (guarded for runtime API availability). <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 35491a2 commit 2dfa0e6

4 files changed

Lines changed: 345 additions & 0 deletions

File tree

src/pyseekdb/client/embedding_function.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ def _initialize(cls) -> None:
634634
CohereEmbeddingFunction,
635635
GoogleVertexEmbeddingFunction,
636636
JinaEmbeddingFunction,
637+
MistralEmbeddingFunction,
637638
MorphEmbeddingFunction,
638639
OllamaEmbeddingFunction,
639640
OpenAIEmbeddingFunction,
@@ -647,6 +648,7 @@ def _initialize(cls) -> None:
647648
cls._registry["sentence_transformer"] = SentenceTransformerEmbeddingFunction
648649
cls._registry["openai"] = OpenAIEmbeddingFunction
649650
cls._registry["qwen"] = QwenEmbeddingFunction
651+
cls._registry["mistral"] = MistralEmbeddingFunction
650652
cls._registry["morph"] = MorphEmbeddingFunction
651653
cls._registry["siliconflow"] = SiliconflowEmbeddingFunction
652654
cls._registry["tencent_hunyuan"] = TencentHunyuanEmbeddingFunction

src/pyseekdb/utils/embedding_functions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .google_vertex_embedding_function import GoogleVertexEmbeddingFunction
1111
from .jina_embedding_function import JinaEmbeddingFunction
1212
from .litellm_base_embedding_function import LiteLLMBaseEmbeddingFunction
13+
from .mistral_embedding_function import MistralEmbeddingFunction
1314
from .morph_embedding_function import MorphEmbeddingFunction
1415
from .ollama_embedding_function import OllamaEmbeddingFunction
1516
from .openai_base_embedding_function import OpenAIBaseEmbeddingFunction
@@ -28,6 +29,7 @@
2829
"GoogleVertexEmbeddingFunction",
2930
"JinaEmbeddingFunction",
3031
"LiteLLMBaseEmbeddingFunction",
32+
"MistralEmbeddingFunction",
3133
"MorphEmbeddingFunction",
3234
"OllamaEmbeddingFunction",
3335
"OpenAIBaseEmbeddingFunction",
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import warnings
2+
from typing import Any
3+
4+
from pyseekdb.client.embedding_function import Documents, Embeddings
5+
from pyseekdb.utils.embedding_functions.openai_base_embedding_function import (
6+
OpenAIBaseEmbeddingFunction,
7+
)
8+
9+
# Known Mistral embedding model dimensions
10+
# Source: https://docs.mistral.ai/capabilities/embeddings/text_embeddings
11+
_MISTRAL_MODEL_DIMENSIONS = {
12+
"mistral-embed": 1024,
13+
}
14+
15+
16+
class MistralEmbeddingFunction(OpenAIBaseEmbeddingFunction):
17+
"""
18+
A convenient embedding function for Mistral text embedding models.
19+
20+
This class provides a simplified interface to Mistral text embeddings using the
21+
OpenAI-compatible API.
22+
23+
Note: The embeddings API only accepts the model name and input texts.
24+
25+
For more information about Mistral embeddings, see:
26+
https://docs.mistral.ai/capabilities/embeddings/text_embeddings
27+
28+
Example:
29+
pip install pyseekdb openai
30+
31+
.. code-block:: python
32+
import pyseekdb
33+
from pyseekdb.utils.embedding_functions import MistralEmbeddingFunction
34+
35+
# Using Mistral text embedding model
36+
# Set MISTRAL_API_KEY environment variable first
37+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
38+
39+
# Using with additional parameters
40+
ef = MistralEmbeddingFunction(
41+
model_name="mistral-embed",
42+
timeout=30,
43+
max_retries=3
44+
)
45+
46+
db = pyseekdb.Client(path="./seekdb.db")
47+
collection = db.create_collection(name="my_collection", embedding_function=ef)
48+
# Add documents
49+
collection.add(ids=["1", "2"], documents=["Hello world", "How are you?"], metadatas=[{"id": 1}, {"id": 2}])
50+
# Query using semantic search
51+
results = collection.query("How are you?", n_results=1)
52+
print(results)
53+
54+
"""
55+
56+
def __init__(
57+
self,
58+
model_name: str = "mistral-embed",
59+
api_key_env: str | None = None,
60+
api_base: str | None = None,
61+
dimensions: int | None = None,
62+
**kwargs: Any,
63+
):
64+
"""Initialize MistralEmbeddingFunction.
65+
66+
Args:
67+
model_name (str, optional): Name of the Mistral embedding model.
68+
Defaults to "mistral-embed".
69+
api_key_env (str, optional): Name of the environment variable containing the Mistral API key.
70+
Defaults to "MISTRAL_API_KEY" if not provided.
71+
api_base (str, optional): Base URL for the Mistral API endpoint.
72+
Defaults to "https://api.mistral.ai/v1" if not provided.
73+
dimensions (int, optional): This parameter is not supported by the Mistral embeddings API.
74+
If provided, a warning will be issued and the parameter will be ignored.
75+
**kwargs: Additional arguments to pass to the OpenAI client.
76+
Common options include:
77+
- timeout: Request timeout in seconds
78+
- max_retries: Maximum number of retries
79+
- See https://github.com/openai/openai-python for more options
80+
"""
81+
if dimensions is not None:
82+
warnings.warn(
83+
"The dimensions parameter is not supported by Mistral embeddings. "
84+
"The provided dimensions parameter will be ignored.",
85+
UserWarning,
86+
stacklevel=2,
87+
)
88+
89+
super().__init__(
90+
model_name=model_name,
91+
api_key_env=api_key_env,
92+
api_base=api_base,
93+
dimensions=None,
94+
**kwargs,
95+
)
96+
97+
def _get_default_api_base(self) -> str:
98+
return "https://api.mistral.ai/v1"
99+
100+
def _get_default_api_key_env(self) -> str:
101+
return "MISTRAL_API_KEY"
102+
103+
def _get_model_dimensions(self) -> dict[str, int]:
104+
return _MISTRAL_MODEL_DIMENSIONS
105+
106+
@staticmethod
107+
def name() -> str:
108+
return "mistral"
109+
110+
def __call__(self, documents: Documents) -> Embeddings:
111+
"""Generate embeddings for the given documents using Mistral's input parameter."""
112+
if isinstance(documents, str):
113+
documents = [documents]
114+
115+
if not documents:
116+
return []
117+
118+
request_params = {
119+
"model": self.model_name,
120+
"input": documents,
121+
}
122+
123+
response = self._client.embeddings.create(**request_params)
124+
embeddings = [item.embedding for item in response.data]
125+
126+
if len(embeddings) != len(documents):
127+
raise ValueError(f"Expected {len(documents)} embeddings but got {len(embeddings)} from API")
128+
129+
return embeddings
130+
131+
def get_config(self) -> dict[str, Any]:
132+
return super().get_config()
133+
134+
@staticmethod
135+
def build_from_config(config: dict[str, Any]) -> "MistralEmbeddingFunction":
136+
model_name = config.get("model_name")
137+
if model_name is None:
138+
raise ValueError("Missing required field 'model_name' in configuration")
139+
140+
api_key_env = config.get("api_key_env")
141+
api_base = config.get("api_base")
142+
dimensions = config.get("dimensions")
143+
client_kwargs = config.get("client_kwargs", {})
144+
if not isinstance(client_kwargs, dict):
145+
raise TypeError(f"client_kwargs must be a dictionary, but got {client_kwargs}")
146+
147+
return MistralEmbeddingFunction(
148+
model_name=model_name,
149+
api_key_env=api_key_env,
150+
api_base=api_base,
151+
dimensions=dimensions,
152+
**client_kwargs,
153+
)
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
Unit tests for MistralEmbeddingFunction.
3+
4+
Tests Mistral embedding function initialization, embedding generation, and dimension detection.
5+
Uses real API calls - requires MISTRAL_API_KEY environment variable to be set.
6+
7+
To run this test manually:
8+
pytest tests/unit_tests/test_mistral_embedding_function.py -v -s
9+
# Or with environment variable:
10+
MISTRAL_API_KEY=your-key pytest tests/unit_tests/test_mistral_embedding_function.py -v -s
11+
"""
12+
13+
import importlib.util
14+
import os
15+
16+
import pytest
17+
18+
from pyseekdb.client.embedding_function import dimension_of
19+
from pyseekdb.utils.embedding_functions import MistralEmbeddingFunction
20+
21+
from .test_utils import env_guard
22+
23+
24+
def is_openai_available() -> bool:
25+
"""
26+
Check if openai is available for testing.
27+
28+
Returns:
29+
True if openai is available, False otherwise.
30+
"""
31+
return importlib.util.find_spec("openai") is not None
32+
33+
34+
# Skip this test by default - it requires external API access and API keys
35+
@pytest.mark.skipif(
36+
not os.environ.get("MISTRAL_API_KEY") or not is_openai_available(),
37+
reason="MISTRAL_API_KEY environment variable must be set",
38+
)
39+
class TestMistralEmbeddingFunction:
40+
"""Test MistralEmbeddingFunction - skipped by default, requires manual execution"""
41+
42+
def test_mistral_env(self):
43+
"""Test if openai package is installed and required environment variables are set."""
44+
if not is_openai_available():
45+
print("openai package is not installed")
46+
raise AssertionError("openai package is not installed")
47+
48+
if not os.environ.get("MISTRAL_API_KEY"):
49+
print("MISTRAL_API_KEY environment variable is not set")
50+
raise AssertionError("MISTRAL_API_KEY environment variable is not set")
51+
52+
def test_initialization_with_defaults(self):
53+
"""Test MistralEmbeddingFunction initialization with default values"""
54+
print("\nTesting MistralEmbeddingFunction initialization with defaults")
55+
56+
# Check if openai is available and env vars are set
57+
self.test_mistral_env()
58+
59+
ef = MistralEmbeddingFunction()
60+
61+
assert ef is not None
62+
assert ef.model_name == "mistral-embed"
63+
assert ef.api_key_env == "MISTRAL_API_KEY"
64+
assert ef.api_base == "https://api.mistral.ai/v1"
65+
assert ef._dimensions_param is None
66+
print(f" Model name: {ef.model_name}")
67+
print(f" API key env: {ef.api_key_env}")
68+
print(f" API base: {ef.api_base}")
69+
70+
def test_initialization_with_custom_api_key_env(self):
71+
"""Test MistralEmbeddingFunction initialization with custom API key env"""
72+
print("\nTesting MistralEmbeddingFunction initialization with custom API key env")
73+
74+
self.test_mistral_env()
75+
76+
custom_key_env = "CUSTOM_MISTRAL_KEY"
77+
if not os.environ.get(custom_key_env):
78+
os.environ[custom_key_env] = "your-custom-key"
79+
80+
ef = MistralEmbeddingFunction(model_name="mistral-embed", api_key_env=custom_key_env)
81+
assert ef.api_key_env == custom_key_env
82+
print(f" Custom API key env: {ef.api_key_env}")
83+
84+
def test_initialization_with_custom_api_base(self):
85+
"""Test MistralEmbeddingFunction initialization with custom API base"""
86+
print("\nTesting MistralEmbeddingFunction initialization with custom API base")
87+
88+
self.test_mistral_env()
89+
90+
custom_base = "https://api.mistral.ai/v1"
91+
ef = MistralEmbeddingFunction(model_name="mistral-embed", api_base=custom_base)
92+
assert ef.api_base == custom_base
93+
print(f" Custom API base: {ef.api_base}")
94+
95+
def test_initialization_with_additional_kwargs(self):
96+
"""Test MistralEmbeddingFunction initialization with additional kwargs"""
97+
print("\nTesting MistralEmbeddingFunction initialization with kwargs")
98+
99+
self.test_mistral_env()
100+
101+
ef = MistralEmbeddingFunction(model_name="mistral-embed", timeout=30, max_retries=3)
102+
assert ef._client_kwargs.get("timeout") == 30
103+
assert ef._client_kwargs.get("max_retries") == 3
104+
print(f" Client kwargs: {ef._client_kwargs}")
105+
106+
def test_initialization_with_missing_api_key(self):
107+
"""Test MistralEmbeddingFunction initialization with missing API key"""
108+
print("\nTesting MistralEmbeddingFunction initialization with missing API key")
109+
110+
with env_guard(MISTRAL_API_KEY=None):
111+
try:
112+
MistralEmbeddingFunction(model_name="mistral-embed")
113+
raise AssertionError("Expected ValueError for missing API key")
114+
except ValueError as e:
115+
print(f" Caught expected error: {e}")
116+
117+
def test_dimension_property_for_known_model(self):
118+
"""Test MistralEmbeddingFunction dimension property for known model"""
119+
print("\nTesting MistralEmbeddingFunction dimension property for known model")
120+
121+
self.test_mistral_env()
122+
123+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
124+
assert ef.dimension == 1024
125+
print(f" Model mistral-embed dimension: {ef.dimension}")
126+
127+
def test_embedding_generation_single_document(self):
128+
"""Test MistralEmbeddingFunction embedding generation (single document)"""
129+
print("\nTesting MistralEmbeddingFunction embedding generation (single document)")
130+
131+
self.test_mistral_env()
132+
133+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
134+
embeddings = ef("Hello world")
135+
assert len(embeddings) == 1
136+
assert len(embeddings[0]) > 0
137+
print(f" Embedding dimension: {len(embeddings[0])}")
138+
139+
def test_embedding_generation_multiple_documents(self):
140+
"""Test MistralEmbeddingFunction embedding generation (multiple documents)"""
141+
print("\nTesting MistralEmbeddingFunction embedding generation (multiple documents)")
142+
143+
self.test_mistral_env()
144+
145+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
146+
embeddings = ef(["Hello world", "How are you?"])
147+
assert len(embeddings) == 2
148+
assert len(embeddings[0]) > 0
149+
print(f" Embedding dimension: {len(embeddings[0])}")
150+
151+
def test_embedding_with_empty_input(self):
152+
"""Test MistralEmbeddingFunction with empty input"""
153+
print("\nTesting MistralEmbeddingFunction with empty input")
154+
155+
self.test_mistral_env()
156+
157+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
158+
embeddings = ef([])
159+
assert embeddings == []
160+
print(" Empty input returned empty embeddings")
161+
162+
def test_dimension_of_function(self):
163+
"""Test dimension_of function with MistralEmbeddingFunction"""
164+
print("\nTesting dimension_of function with MistralEmbeddingFunction")
165+
166+
self.test_mistral_env()
167+
168+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
169+
dim = dimension_of(ef)
170+
assert dim == 1024
171+
print(f" dimension_of returned: {dim}")
172+
173+
def test_persistence(self):
174+
"""Test persistence for MistralEmbeddingFunction"""
175+
print("\nTesting MistralEmbeddingFunction persistence")
176+
177+
self.test_mistral_env()
178+
179+
assert MistralEmbeddingFunction.name() == "mistral"
180+
181+
ef = MistralEmbeddingFunction(model_name="mistral-embed")
182+
config = ef.get_config()
183+
184+
restored_ef = MistralEmbeddingFunction.build_from_config(config)
185+
assert isinstance(restored_ef, MistralEmbeddingFunction)
186+
assert restored_ef.model_name == ef.model_name
187+
assert restored_ef.api_key_env == ef.api_key_env
188+
assert restored_ef.api_base == ef.api_base

0 commit comments

Comments
 (0)