c56bef871b
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from haystack import component, default_from_dict, default_to_dict
|
|
from haystack.components.embedders.mock_utils import (
|
|
EmbeddingFn,
|
|
_coerce_embedding,
|
|
_deterministic_embedding,
|
|
_estimate_usage,
|
|
)
|
|
from haystack.utils import deserialize_callable, serialize_callable
|
|
|
|
|
|
@component
|
|
class MockTextEmbedder:
|
|
"""
|
|
A Text Embedder that returns deterministic embeddings without calling any API.
|
|
|
|
It is a drop-in replacement for real Text Embedders (such as `OpenAITextEmbedder`) in tests, smoke tests, and
|
|
quick prototypes. It implements the same interface (`run`, `run_async`, serialization) but never contacts an
|
|
external service, so it is fully deterministic and free to run.
|
|
|
|
The embedding is selected based on how the component is configured:
|
|
|
|
- **Deterministic (default)**: with no configuration, the embedding is derived from a hash of the input text.
|
|
The same text always yields the same embedding, and different texts yield different embeddings, so the mock
|
|
works in retrieval pipelines and is reproducible across runs and processes.
|
|
- **Fixed embedding**: pass an `embedding` vector. The same vector is returned for every input.
|
|
- **Dynamic embedding**: pass an `embedding_fn` callable that receives the (prepared) text and returns the
|
|
embedding. This is useful when the embedding should depend on the input in a custom way.
|
|
|
|
### Usage example
|
|
|
|
```python
|
|
from haystack.components.embedders import MockTextEmbedder
|
|
|
|
embedder = MockTextEmbedder(dimension=8)
|
|
result = embedder.run("I love pizza!")
|
|
print(result["embedding"]) # a deterministic list of 8 floats
|
|
```
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
embedding: list[float] | None = None,
|
|
*,
|
|
embedding_fn: EmbeddingFn | None = None,
|
|
dimension: int = 768,
|
|
model: str = "mock-model",
|
|
meta: dict[str, Any] | None = None,
|
|
prefix: str = "",
|
|
suffix: str = "",
|
|
) -> None:
|
|
"""
|
|
Creates an instance of MockTextEmbedder.
|
|
|
|
:param embedding: An optional fixed embedding returned for every input. Mutually exclusive with
|
|
`embedding_fn`. If neither is provided, a deterministic embedding is derived from the input text.
|
|
:param embedding_fn: An optional callable that receives the prepared text (after `prefix`/`suffix` are
|
|
applied) and returns the embedding as a list of floats. Mutually exclusive with `embedding`. To support
|
|
serialization, pass a named function (lambdas and nested functions cannot be serialized).
|
|
:param dimension: The number of dimensions of the deterministic embedding. Ignored when `embedding` or
|
|
`embedding_fn` is provided, since their length is determined by the value or callable.
|
|
:param model: The model name reported in the metadata. Purely cosmetic; no model is loaded.
|
|
:param meta: Additional metadata merged into the output `meta`.
|
|
:param prefix: A string to add at the beginning of the text before embedding.
|
|
:param suffix: A string to add at the end of the text before embedding.
|
|
:raises ValueError: If both `embedding` and `embedding_fn` are provided, if `dimension` is not positive, or
|
|
if `embedding` is an empty list.
|
|
:raises TypeError: If `embedding` is not a sequence of numbers.
|
|
"""
|
|
if embedding is not None and embedding_fn is not None:
|
|
raise ValueError("Pass either 'embedding' or 'embedding_fn', not both.")
|
|
if dimension <= 0:
|
|
raise ValueError("'dimension' must be a positive integer.")
|
|
|
|
self.embedding = _coerce_embedding(embedding, name="'embedding'") if embedding is not None else None
|
|
self.embedding_fn = embedding_fn
|
|
self.dimension = dimension
|
|
self.model = model
|
|
self.meta = meta or {}
|
|
self.prefix = prefix
|
|
self.suffix = suffix
|
|
self._is_warmed_up = False
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Serialize the component to a dictionary."""
|
|
embedding_fn = serialize_callable(self.embedding_fn) if self.embedding_fn is not None else None
|
|
return default_to_dict(
|
|
self,
|
|
embedding=self.embedding,
|
|
embedding_fn=embedding_fn,
|
|
dimension=self.dimension,
|
|
model=self.model,
|
|
meta=self.meta,
|
|
prefix=self.prefix,
|
|
suffix=self.suffix,
|
|
)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> MockTextEmbedder:
|
|
"""Deserialize the component from a dictionary."""
|
|
init_params = data.get("init_parameters", {})
|
|
embedding_fn = init_params.get("embedding_fn")
|
|
if embedding_fn:
|
|
init_params["embedding_fn"] = deserialize_callable(embedding_fn)
|
|
return default_from_dict(cls, data)
|
|
|
|
def warm_up(self) -> None:
|
|
"""No-op warm up, provided for interface compatibility with real Embedders."""
|
|
self._is_warmed_up = True
|
|
|
|
def _embed(self, text: str) -> list[float]:
|
|
"""Produce the embedding for the prepared text according to the configured mode."""
|
|
if self.embedding_fn is not None:
|
|
return _coerce_embedding(self.embedding_fn(text), name="the return value of 'embedding_fn'")
|
|
if self.embedding is not None:
|
|
return list(self.embedding)
|
|
return _deterministic_embedding(text, self.dimension)
|
|
|
|
@component.output_types(embedding=list[float], meta=dict[str, Any])
|
|
def run(self, text: str) -> dict[str, Any]:
|
|
"""
|
|
Return a deterministic embedding for the input text without calling any API.
|
|
|
|
:param text: The text to embed.
|
|
:returns: A dictionary with the following keys:
|
|
- `embedding`: The embedding of the input text.
|
|
- `meta`: Metadata about the (mock) model.
|
|
:raises TypeError: If `text` is not a string.
|
|
"""
|
|
self.warm_up()
|
|
|
|
if not isinstance(text, str):
|
|
raise TypeError(
|
|
"MockTextEmbedder expects a string as an input. "
|
|
"In case you want to embed a list of Documents, please use the MockDocumentEmbedder."
|
|
)
|
|
|
|
text_to_embed = self.prefix + text + self.suffix
|
|
meta: dict[str, Any] = {"model": self.model, "usage": _estimate_usage([text_to_embed])}
|
|
meta.update(self.meta)
|
|
return {"embedding": self._embed(text_to_embed), "meta": meta}
|
|
|
|
@component.output_types(embedding=list[float], meta=dict[str, Any])
|
|
async def run_async(self, text: str) -> dict[str, Any]:
|
|
"""
|
|
Asynchronously return a deterministic embedding for the input text without calling any API.
|
|
|
|
:param text: The text to embed.
|
|
:returns: A dictionary with the following keys:
|
|
- `embedding`: The embedding of the input text.
|
|
- `meta`: Metadata about the (mock) model.
|
|
:raises TypeError: If `text` is not a string.
|
|
"""
|
|
return self.run(text=text)
|