From 2358d021a91d2eb490ce0e4e7916529cf4142ccb Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Wed, 29 Nov 2023 15:30:24 +0100 Subject: [PATCH 1/3] remove the preview package --- haystack/preview/README.md | 34 - haystack/preview/__init__.py | 19 - haystack/preview/components/__init__.py | 0 haystack/preview/components/audio/__init__.py | 4 - .../preview/components/audio/whisper_local.py | 140 --- .../components/audio/whisper_remote.py | 141 --- .../preview/components/builders/__init__.py | 5 - .../components/builders/answer_builder.py | 142 --- .../builders/dynamic_prompt_builder.py | 331 ------- .../components/builders/prompt_builder.py | 41 - .../preview/components/caching/__init__.py | 3 - .../components/caching/url_cache_checker.py | 63 -- .../components/classifiers/__init__.py | 3 - .../document_language_classifier.py | 82 -- .../preview/components/converters/__init__.py | 15 - .../preview/components/converters/azure.py | 105 --- .../preview/components/converters/html.py | 94 -- .../preview/components/converters/markdown.py | 97 -- .../preview/components/converters/pypdf.py | 105 --- .../preview/components/converters/tika.py | 54 -- haystack/preview/components/converters/txt.py | 56 -- .../preview/components/embedders/__init__.py | 13 - .../components/embedders/backends/__init__.py | 0 .../backends/sentence_transformers_backend.py | 46 - .../embedders/openai_document_embedder.py | 176 ---- .../embedders/openai_text_embedder.py | 106 --- ...sentence_transformers_document_embedder.py | 145 --- .../sentence_transformers_text_embedder.py | 118 --- .../preview/components/fetchers/__init__.py | 3 - .../components/fetchers/link_content.py | 203 ---- .../preview/components/generators/__init__.py | 6 - .../components/generators/chat/__init__.py | 4 - .../generators/chat/hugging_face_tgi.py | 280 ------ .../components/generators/chat/openai.py | 287 ------ .../preview/components/generators/cohere.py | 159 ---- .../preview/components/generators/hf_utils.py | 57 -- .../generators/hugging_face_local.py | 236 ----- .../components/generators/hugging_face_tgi.py | 237 ----- .../preview/components/generators/openai.py | 290 ------ .../preview/components/generators/utils.py | 49 - .../components/preprocessors/__init__.py | 4 - .../preprocessors/document_cleaner.py | 229 ----- .../preprocessors/document_splitter.py | 91 -- .../preview/components/rankers/__init__.py | 4 - .../preview/components/rankers/meta_field.py | 180 ---- .../rankers/transformers_similarity.py | 134 --- .../preview/components/readers/__init__.py | 3 - .../preview/components/readers/extractive.py | 421 --------- .../preview/components/retrievers/__init__.py | 4 - .../retrievers/in_memory_bm25_retriever.py | 106 --- .../in_memory_embedding_retriever.py | 125 --- .../preview/components/routers/__init__.py | 7 - .../components/routers/conditional_router.py | 347 ------- .../components/routers/document_joiner.py | 153 ---- .../components/routers/file_type_router.py | 87 -- .../components/routers/metadata_router.py | 81 -- .../routers/text_language_router.py | 73 -- .../preview/components/samplers/__init__.py | 3 - haystack/preview/components/samplers/top_p.py | 127 --- .../preview/components/websearch/__init__.py | 4 - .../preview/components/websearch/searchapi.py | 140 --- .../components/websearch/serper_dev.py | 140 --- .../preview/components/writers/__init__.py | 3 - .../components/writers/document_writer.py | 68 -- haystack/preview/dataclasses/__init__.py | 17 - haystack/preview/dataclasses/answer.py | 25 - haystack/preview/dataclasses/byte_stream.py | 38 - haystack/preview/dataclasses/chat_message.py | 80 -- haystack/preview/dataclasses/document.py | 186 ---- .../preview/dataclasses/streaming_chunk.py | 17 - haystack/preview/document_stores/__init__.py | 14 - haystack/preview/document_stores/decorator.py | 39 - haystack/preview/document_stores/errors.py | 10 - .../document_stores/in_memory/__init__.py | 3 - .../in_memory/document_store.py | 328 ------- haystack/preview/document_stores/protocols.py | 142 --- haystack/preview/errors.py | 2 - haystack/preview/lazy_imports.py | 44 - haystack/preview/marshal/__init__.py | 4 - haystack/preview/marshal/protocol.py | 9 - haystack/preview/marshal/yaml.py | 11 - haystack/preview/pipeline.py | 99 -- haystack/preview/telemetry/__init__.py | 1 - haystack/preview/telemetry/_environment.py | 106 --- haystack/preview/telemetry/_telemetry.py | 171 ---- haystack/preview/testing/__init__.py | 0 haystack/preview/testing/document_store.py | 866 ------------------ haystack/preview/testing/factory.py | 119 --- haystack/preview/testing/test_utils.py | 34 - haystack/preview/utils/__init__.py | 3 - haystack/preview/utils/expit.py | 5 - haystack/preview/utils/filters.py | 305 ------ haystack/preview/utils/requests_utils.py | 94 -- haystack/preview/version.py | 13 - 94 files changed, 9268 deletions(-) delete mode 100644 haystack/preview/README.md delete mode 100644 haystack/preview/__init__.py delete mode 100644 haystack/preview/components/__init__.py delete mode 100644 haystack/preview/components/audio/__init__.py delete mode 100644 haystack/preview/components/audio/whisper_local.py delete mode 100644 haystack/preview/components/audio/whisper_remote.py delete mode 100644 haystack/preview/components/builders/__init__.py delete mode 100644 haystack/preview/components/builders/answer_builder.py delete mode 100644 haystack/preview/components/builders/dynamic_prompt_builder.py delete mode 100644 haystack/preview/components/builders/prompt_builder.py delete mode 100644 haystack/preview/components/caching/__init__.py delete mode 100644 haystack/preview/components/caching/url_cache_checker.py delete mode 100644 haystack/preview/components/classifiers/__init__.py delete mode 100644 haystack/preview/components/classifiers/document_language_classifier.py delete mode 100644 haystack/preview/components/converters/__init__.py delete mode 100644 haystack/preview/components/converters/azure.py delete mode 100644 haystack/preview/components/converters/html.py delete mode 100644 haystack/preview/components/converters/markdown.py delete mode 100644 haystack/preview/components/converters/pypdf.py delete mode 100644 haystack/preview/components/converters/tika.py delete mode 100644 haystack/preview/components/converters/txt.py delete mode 100644 haystack/preview/components/embedders/__init__.py delete mode 100644 haystack/preview/components/embedders/backends/__init__.py delete mode 100644 haystack/preview/components/embedders/backends/sentence_transformers_backend.py delete mode 100644 haystack/preview/components/embedders/openai_document_embedder.py delete mode 100644 haystack/preview/components/embedders/openai_text_embedder.py delete mode 100644 haystack/preview/components/embedders/sentence_transformers_document_embedder.py delete mode 100644 haystack/preview/components/embedders/sentence_transformers_text_embedder.py delete mode 100644 haystack/preview/components/fetchers/__init__.py delete mode 100644 haystack/preview/components/fetchers/link_content.py delete mode 100644 haystack/preview/components/generators/__init__.py delete mode 100644 haystack/preview/components/generators/chat/__init__.py delete mode 100644 haystack/preview/components/generators/chat/hugging_face_tgi.py delete mode 100644 haystack/preview/components/generators/chat/openai.py delete mode 100644 haystack/preview/components/generators/cohere.py delete mode 100644 haystack/preview/components/generators/hf_utils.py delete mode 100644 haystack/preview/components/generators/hugging_face_local.py delete mode 100644 haystack/preview/components/generators/hugging_face_tgi.py delete mode 100644 haystack/preview/components/generators/openai.py delete mode 100644 haystack/preview/components/generators/utils.py delete mode 100644 haystack/preview/components/preprocessors/__init__.py delete mode 100644 haystack/preview/components/preprocessors/document_cleaner.py delete mode 100644 haystack/preview/components/preprocessors/document_splitter.py delete mode 100644 haystack/preview/components/rankers/__init__.py delete mode 100644 haystack/preview/components/rankers/meta_field.py delete mode 100644 haystack/preview/components/rankers/transformers_similarity.py delete mode 100644 haystack/preview/components/readers/__init__.py delete mode 100644 haystack/preview/components/readers/extractive.py delete mode 100644 haystack/preview/components/retrievers/__init__.py delete mode 100644 haystack/preview/components/retrievers/in_memory_bm25_retriever.py delete mode 100644 haystack/preview/components/retrievers/in_memory_embedding_retriever.py delete mode 100644 haystack/preview/components/routers/__init__.py delete mode 100644 haystack/preview/components/routers/conditional_router.py delete mode 100644 haystack/preview/components/routers/document_joiner.py delete mode 100644 haystack/preview/components/routers/file_type_router.py delete mode 100644 haystack/preview/components/routers/metadata_router.py delete mode 100644 haystack/preview/components/routers/text_language_router.py delete mode 100644 haystack/preview/components/samplers/__init__.py delete mode 100644 haystack/preview/components/samplers/top_p.py delete mode 100644 haystack/preview/components/websearch/__init__.py delete mode 100644 haystack/preview/components/websearch/searchapi.py delete mode 100644 haystack/preview/components/websearch/serper_dev.py delete mode 100644 haystack/preview/components/writers/__init__.py delete mode 100644 haystack/preview/components/writers/document_writer.py delete mode 100644 haystack/preview/dataclasses/__init__.py delete mode 100644 haystack/preview/dataclasses/answer.py delete mode 100644 haystack/preview/dataclasses/byte_stream.py delete mode 100644 haystack/preview/dataclasses/chat_message.py delete mode 100644 haystack/preview/dataclasses/document.py delete mode 100644 haystack/preview/dataclasses/streaming_chunk.py delete mode 100644 haystack/preview/document_stores/__init__.py delete mode 100644 haystack/preview/document_stores/decorator.py delete mode 100644 haystack/preview/document_stores/errors.py delete mode 100644 haystack/preview/document_stores/in_memory/__init__.py delete mode 100644 haystack/preview/document_stores/in_memory/document_store.py delete mode 100644 haystack/preview/document_stores/protocols.py delete mode 100644 haystack/preview/errors.py delete mode 100644 haystack/preview/lazy_imports.py delete mode 100644 haystack/preview/marshal/__init__.py delete mode 100644 haystack/preview/marshal/protocol.py delete mode 100644 haystack/preview/marshal/yaml.py delete mode 100644 haystack/preview/pipeline.py delete mode 100644 haystack/preview/telemetry/__init__.py delete mode 100644 haystack/preview/telemetry/_environment.py delete mode 100644 haystack/preview/telemetry/_telemetry.py delete mode 100644 haystack/preview/testing/__init__.py delete mode 100644 haystack/preview/testing/document_store.py delete mode 100644 haystack/preview/testing/factory.py delete mode 100644 haystack/preview/testing/test_utils.py delete mode 100644 haystack/preview/utils/__init__.py delete mode 100644 haystack/preview/utils/expit.py delete mode 100644 haystack/preview/utils/filters.py delete mode 100644 haystack/preview/utils/requests_utils.py delete mode 100644 haystack/preview/version.py diff --git a/haystack/preview/README.md b/haystack/preview/README.md deleted file mode 100644 index 1dcd4c351b..0000000000 --- a/haystack/preview/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Haystack 2.0 - Preview Features - -[![PyPI - Version](https://img.shields.io/pypi/v/haystack-ai.svg)](https://pypi.org/project/haystack-ai) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/haystack-ai.svg)](https://pypi.org/project/haystack-ai) - -Since Haystack 1.15, we’ve been slowly introducing new components and features to Haystack in the background in preparation for Haystack 2.0. In this `preview` module, you can find what’s been implemented so far regarding Haystack 2.0. **Keep in mind that Haystack 2.0 is still a work in progress.** Read more about Haystack 2.0 in [Shaping Haystack 2.0](https://github.com/deepset-ai/haystack/discussions/5568). - -## 💾 Installation - -**Install `haystack-ai`** - -There is a separate PyPI package that only ships the code in `preview` module. You can install `haystack-ai` using pip: -```sh -pip install haystack-ai -``` -The `haystack-ai` package is built on the `main` branch, so it's highly unstable, but it's useful if you want to try the new features as soon as they are merged. - -**Install `farm-haystack`** - -As an alternative way, you can install `farm-haystack`: -```sh -pip install farm-haystack -``` -The `farm-haystack` package includes all new features of Haystack 2.0. Note that updates to this package occur less frequently compared to `haystack-ai`. So, you might not get the all latest Haystack 2.0 features immediately when using `farm-haystack`. - -## 🚗 Getting Started - -In our **end 2 end tests** you can find example code for the following pipelines: -- [RAG pipeline](https://github.com/deepset-ai/haystack/blob/main/e2e/preview/pipelines/test_rag_pipelines.py) -- [Extractive QA pipeline](https://github.com/deepset-ai/haystack/blob/main/e2e/preview/pipelines/test_extractive_qa_pipeline.py) -- more to come, check out the [folder](https://github.com/deepset-ai/haystack/blob/main/e2e/preview/) - -## 💙 Stay Updated -To learn how and when components will be migrated to the new major version, have a look at the [Migrate Components to Pipeline v2](https://github.com/deepset-ai/haystack/issues/5265) roadmap item, where we keep track of issues and PRs about Haystack 2.0. When you have questions, you can always contact us using the [Shaping Haystack 2.0](https://github.com/deepset-ai/haystack/discussions/5568) discussion or [Haystack Discord server](https://discord.com/channels/993534733298450452/1141683185458094211). diff --git a/haystack/preview/__init__.py b/haystack/preview/__init__.py deleted file mode 100644 index 4d467a8e0a..0000000000 --- a/haystack/preview/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from canals import component -from canals.serialization import default_from_dict, default_to_dict -from canals.errors import DeserializationError, ComponentError -from haystack.preview.pipeline import Pipeline -from haystack.preview.dataclasses import Document, Answer, GeneratedAnswer, ExtractedAnswer - - -__all__ = [ - "component", - "default_from_dict", - "default_to_dict", - "DeserializationError", - "ComponentError", - "Pipeline", - "Document", - "Answer", - "GeneratedAnswer", - "ExtractedAnswer", -] diff --git a/haystack/preview/components/__init__.py b/haystack/preview/components/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/haystack/preview/components/audio/__init__.py b/haystack/preview/components/audio/__init__.py deleted file mode 100644 index 3d3b07cd82..0000000000 --- a/haystack/preview/components/audio/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.audio.whisper_local import LocalWhisperTranscriber -from haystack.preview.components.audio.whisper_remote import RemoteWhisperTranscriber - -__all__ = ["LocalWhisperTranscriber", "RemoteWhisperTranscriber"] diff --git a/haystack/preview/components/audio/whisper_local.py b/haystack/preview/components/audio/whisper_local.py deleted file mode 100644 index f45f652df2..0000000000 --- a/haystack/preview/components/audio/whisper_local.py +++ /dev/null @@ -1,140 +0,0 @@ -from typing import List, Optional, Dict, Any, Union, BinaryIO, Literal, get_args, Sequence - -import logging -from pathlib import Path - -from haystack.preview import component, Document, default_to_dict, ComponentError -from haystack.preview.lazy_imports import LazyImport - -with LazyImport( - "Run 'pip install transformers[torch]' to install torch and " - "'pip install \"openai-whisper>=20231106\"' to install whisper." -) as whisper_import: - import torch - import whisper - - -logger = logging.getLogger(__name__) -WhisperLocalModel = Literal["tiny", "small", "medium", "large", "large-v2"] - - -@component -class LocalWhisperTranscriber: - """ - Transcribes audio files using OpenAI's Whisper's model on your local machine. - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) and the official Whisper - [github repo](https://github.com/openai/whisper). - """ - - def __init__( - self, - model_name_or_path: WhisperLocalModel = "large", - device: Optional[str] = None, - whisper_params: Optional[Dict[str, Any]] = None, - ): - """ - :param model_name_or_path: Name of the model to use. Set it to one of the following values: - :type model_name_or_path: Literal["tiny", "small", "medium", "large", "large-v2"] - :param device: Name of the torch device to use for inference. If None, CPU is used. - :type device: Optional[str] - """ - whisper_import.check() - if model_name_or_path not in get_args(WhisperLocalModel): - raise ValueError( - f"Model name '{model_name_or_path}' not recognized. Choose one among: " - f"{', '.join(get_args(WhisperLocalModel))}." - ) - self.model_name = model_name_or_path - self.whisper_params = whisper_params or {} - self.device = torch.device(device) if device else torch.device("cpu") - self._model = None - - def warm_up(self) -> None: - """ - Loads the model. - """ - if not self._model: - self._model = whisper.load_model(self.model_name, device=self.device) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, model_name_or_path=self.model_name, device=str(self.device), whisper_params=self.whisper_params - ) - - @component.output_types(documents=List[Document]) - def run(self, audio_files: List[Path], whisper_params: Optional[Dict[str, Any]] = None): - """ - Transcribe the audio files into a list of Documents, one for each input file. - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) and the official Whisper - [github repo](https://github.com/openai/whisper). - - :param audio_files: A list of paths or binary streams to transcribe. - :returns: A list of Documents, one for each file. The content of the document is the transcription text, - while the document's metadata contains all the other values returned by the Whisper model, such as the - alignment data. Another key called `audio_file` contains the path to the audio file used for the - transcription. - """ - if self._model is None: - raise ComponentError("The component was not warmed up. Run 'warm_up()' before calling 'run()'.") - - if whisper_params is None: - whisper_params = self.whisper_params - - documents = self.transcribe(audio_files, **whisper_params) - return {"documents": documents} - - def transcribe(self, audio_files: Sequence[Union[str, Path, BinaryIO]], **kwargs) -> List[Document]: - """ - Transcribe the audio files into a list of Documents, one for each input file. - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) and the official Whisper - [github repo](https://github.com/openai/whisper). - - :param audio_files: A list of paths or binary streams to transcribe. - :returns: A list of Documents, one for each file. The content of the document is the transcription text, - while the document's metadata contains all the other values returned by the Whisper model, such as the - alignment data. Another key called `audio_file` contains the path to the audio file used for the - transcription. - """ - transcriptions = self._raw_transcribe(audio_files=audio_files, **kwargs) - documents = [] - for audio, transcript in zip(audio_files, transcriptions): - content = transcript.pop("text") - if not isinstance(audio, (str, Path)): - audio = "<>" - doc = Document(content=content, meta={"audio_file": audio, **transcript}) - documents.append(doc) - return documents - - def _raw_transcribe(self, audio_files: Sequence[Union[str, Path, BinaryIO]], **kwargs) -> List[Dict[str, Any]]: - """ - Transcribe the given audio files. Returns the output of the model, a dictionary, for each input file. - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) and the official Whisper - [github repo](https://github.com/openai/whisper). - - :param audio_files: A list of paths or binary streams to transcribe. - :returns: A list of transcriptions. - """ - return_segments = kwargs.pop("return_segments", False) - transcriptions = [] - for audio_file in audio_files: - if isinstance(audio_file, (str, Path)): - audio_file = open(audio_file, "rb") - - # mypy compains that _model is not guaranteed to be not None. It is: check self.warm_up() - transcription = self._model.transcribe(audio_file.name, **kwargs) # type: ignore - if not return_segments: - transcription.pop("segments", None) - transcriptions.append(transcription) - - return transcriptions diff --git a/haystack/preview/components/audio/whisper_remote.py b/haystack/preview/components/audio/whisper_remote.py deleted file mode 100644 index 848a7c170f..0000000000 --- a/haystack/preview/components/audio/whisper_remote.py +++ /dev/null @@ -1,141 +0,0 @@ -import io -import logging -import os -from typing import Any, Dict, List, Optional, Union -from pathlib import Path - -import openai - -from haystack.preview import Document, component, default_from_dict, default_to_dict -from haystack.preview.dataclasses import ByteStream - -logger = logging.getLogger(__name__) - - -API_BASE_URL = "https://api.openai.com/v1" - - -@component -class RemoteWhisperTranscriber: - """ - Transcribes audio files using OpenAI's Whisper using OpenAI API. Requires an API key. See the - [OpenAI blog post](https://beta.openai.com/docs/api-reference/whisper for more details. - You can get one by signing up for an [OpenAI account](https://beta.openai.com/). - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "whisper-1", - organization: Optional[str] = None, - api_base_url: str = API_BASE_URL, - **kwargs, - ): - """ - Transcribes a list of audio files into a list of Documents. - - :param api_key: OpenAI API key. - :param model_name: Name of the model to use. It now accepts only `whisper-1`. - :param organization: The OpenAI-Organization ID, defaults to `None`. For more details, see OpenAI - [documentation](https://platform.openai.com/docs/api-reference/requesting-organization). - :param api_base: OpenAI base URL, defaults to `"https://api.openai.com/v1"`. - :param kwargs: Other parameters to use for the model. These parameters are all sent directly to the OpenAI - endpoint. See OpenAI [documentation](https://platform.openai.com/docs/api-reference/audio) for more details. - Some of the supported parameters: - - `language`: The language of the input audio. - Supplying the input language in ISO-639-1 format - will improve accuracy and latency. - - `prompt`: An optional text to guide the model's - style or continue a previous audio segment. - The prompt should match the audio language. - - `response_format`: The format of the transcript - output, in one of these options: json, text, srt, - verbose_json, or vtt. Defaults to "json". Currently only "json" is supported. - - `temperature`: The sampling temperature, between 0 - and 1. Higher values like 0.8 will make the output more - random, while lower values like 0.2 will make it more - focused and deterministic. If set to 0, the model will - use log probability to automatically increase the - temperature until certain thresholds are hit. - """ - - # if the user does not provide the API key, check if it is set in the module client - api_key = api_key or openai.api_key - if api_key is None: - try: - api_key = os.environ["OPENAI_API_KEY"] - except KeyError as e: - raise ValueError( - "RemoteWhisperTranscriber expects an OpenAI API key. " - "Set the OPENAI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - openai.api_key = api_key - - self.organization = organization - self.model_name = model_name - self.api_base_url = api_base_url - - # Only response_format = "json" is supported - whisper_params = kwargs - if whisper_params.get("response_format") != "json": - logger.warning( - "RemoteWhisperTranscriber only supports 'response_format: json'. This parameter will be overwritten." - ) - whisper_params["response_format"] = "json" - self.whisper_params = whisper_params - - if organization is not None: - openai.organization = organization - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - This method overrides the default serializer in order to - avoid leaking the `api_key` value passed to the constructor. - """ - return default_to_dict( - self, - model_name=self.model_name, - organization=self.organization, - api_base_url=self.api_base_url, - **self.whisper_params, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "RemoteWhisperTranscriber": - """ - Deserialize this component from a dictionary. - """ - return default_from_dict(cls, data) - - @component.output_types(documents=List[Document]) - def run(self, sources: List[Union[str, Path, ByteStream]]): - """ - Transcribe the audio files into a list of Documents, one for each input file. - - For the supported audio formats, languages, and other parameters, see the - [Whisper API documentation](https://platform.openai.com/docs/guides/speech-to-text) and the official Whisper - [github repo](https://github.com/openai/whisper). - - :param audio_files: a list of ByteStream objects to transcribe. - :returns: a list of Documents, one for each file. The content of the document is the transcription text. - """ - documents = [] - - for source in sources: - if not isinstance(source, ByteStream): - path = source - source = ByteStream.from_file_path(Path(source)) - source.metadata["file_path"] = path - - file = io.BytesIO(source.data) - file.name = str(source.metadata["file_path"]) if "file_path" in source.metadata else "__fallback__.wav" - - content = openai.Audio.transcribe(file=file, model=self.model_name, **self.whisper_params) - doc = Document(content=content["text"], meta=source.metadata) - documents.append(doc) - - return {"documents": documents} diff --git a/haystack/preview/components/builders/__init__.py b/haystack/preview/components/builders/__init__.py deleted file mode 100644 index 6f47bce0d7..0000000000 --- a/haystack/preview/components/builders/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from haystack.preview.components.builders.answer_builder import AnswerBuilder -from haystack.preview.components.builders.prompt_builder import PromptBuilder -from haystack.preview.components.builders.dynamic_prompt_builder import DynamicPromptBuilder - -__all__ = ["AnswerBuilder", "PromptBuilder", "DynamicPromptBuilder"] diff --git a/haystack/preview/components/builders/answer_builder.py b/haystack/preview/components/builders/answer_builder.py deleted file mode 100644 index 0f44a33bc3..0000000000 --- a/haystack/preview/components/builders/answer_builder.py +++ /dev/null @@ -1,142 +0,0 @@ -import logging -import re -from typing import List, Dict, Any, Optional - -from haystack.preview import component, GeneratedAnswer, Document - - -logger = logging.getLogger(__name__) - - -@component -class AnswerBuilder: - """ - A component to parse the output of a Generator to `Answer` objects using regular expressions. - """ - - def __init__(self, pattern: Optional[str] = None, reference_pattern: Optional[str] = None): - """ - :param pattern: The regular expression pattern to use to extract the answer text from the generator output. - If not specified, the whole string is used as the answer. The regular expression can have at - most one capture group. If a capture group is present, the text matched by the capture group - is used as the answer. If no capture group is present, the whole match is used as the answer. - Examples: - `[^\\n]+$` finds "this is an answer" in a string "this is an argument.\nthis is an answer". - `Answer: (.*)` finds "this is an answer" in a string "this is an argument. Answer: this is an answer". - Default: `None`. - :param reference_pattern: The regular expression pattern to use for parsing the document references. - We assume that references are specified as indices of the input documents and that - indices start at 1. - Example: `\\[(\\d+)\\]` finds "1" in a string "this is an answer[1]". - If not specified, no parsing is done, and all documents are referenced. - Default: `None`. - """ - if pattern: - AnswerBuilder._check_num_groups_in_regex(pattern) - - self.pattern = pattern - self.reference_pattern = reference_pattern - - @component.output_types(answers=List[GeneratedAnswer]) - def run( - self, - query: str, - replies: List[str], - metadata: Optional[List[Dict[str, Any]]] = None, - documents: Optional[List[Document]] = None, - pattern: Optional[str] = None, - reference_pattern: Optional[str] = None, - ): - """ - Parse the output of a Generator to `Answer` objects using regular expressions. - - :param query: The query used in the prompts for the Generator. A strings. - :param replies: The output of the Generator. A list of strings. - :param metadata: The metadata returned by the Generator. An optional list of dictionaries. If not specified, - the generated answer will contain no metadata. - :param documents: The documents used as input to the Generator. A list of `Document` objects. If - `documents` are specified, they are added to the `Answer` objects. - If both `documents` and `reference_pattern` are specified, the documents referenced in the - Generator output are extracted from the input documents and added to the `Answer` objects. - Default: `None`. - :param pattern: The regular expression pattern to use to extract the answer text from the generator output. - If not specified, the whole string is used as the answer. The regular expression can have at - most one capture group. If a capture group is present, the text matched by the capture group - is used as the answer. If no capture group is present, the whole match is used as the answer. - Examples: - `[^\\n]+$` finds "this is an answer" in a string "this is an argument.\nthis is an answer". - `Answer: (.*)` finds "this is an answer" in a string "this is an argument. Answer: this is an answer". - Default: `None`. - :param reference_pattern: The regular expression pattern to use for parsing the document references. - We assume that references are specified as indices of the input documents and that - indices start at 1. - Example: `\\[(\\d+)\\]` finds "1" in a string "this is an answer[1]". - If not specified, no parsing is done, and all documents are referenced. - Default: `None`. - """ - if not metadata: - metadata = [{}] * len(replies) - elif len(replies) != len(metadata): - raise ValueError(f"Number of replies ({len(replies)}), and metadata ({len(metadata)}) must match.") - - if pattern: - AnswerBuilder._check_num_groups_in_regex(pattern) - - pattern = pattern or self.pattern - reference_pattern = reference_pattern or self.reference_pattern - - all_answers = [] - for reply, meta in zip(replies, metadata): - referenced_docs = [] - if documents: - reference_idxs = [] - if reference_pattern: - reference_idxs = AnswerBuilder._extract_reference_idxs(reply, reference_pattern) - else: - reference_idxs = [doc_idx for doc_idx, _ in enumerate(documents)] - - for idx in reference_idxs: - try: - referenced_docs.append(documents[idx]) - except IndexError: - logger.warning("Document index '%s' referenced in Generator output is out of range. ", idx + 1) - - answer_string = AnswerBuilder._extract_answer_string(reply, pattern) - answer = GeneratedAnswer(data=answer_string, query=query, documents=referenced_docs, metadata=meta) - all_answers.append(answer) - - return {"answers": all_answers} - - @staticmethod - def _extract_answer_string(reply: str, pattern: Optional[str] = None) -> str: - """ - Extract the answer string from the generator output using the specified pattern. - If no pattern is specified, the whole string is used as the answer. - - :param replies: The output of the Generator. A string. - :param pattern: The regular expression pattern to use to extract the answer text from the generator output. - """ - if pattern is None: - return reply - - if match := re.search(pattern, reply): - # No capture group in pattern -> use the whole match as answer - if not match.lastindex: - return match.group(0) - # One capture group in pattern -> use the capture group as answer - return match.group(1) - return "" - - @staticmethod - def _extract_reference_idxs(reply: str, reference_pattern: str) -> List[int]: - document_idxs = re.findall(reference_pattern, reply) - return [int(idx) - 1 for idx in document_idxs] - - @staticmethod - def _check_num_groups_in_regex(pattern: str): - num_groups = re.compile(pattern).groups - if num_groups > 1: - raise ValueError( - f"Pattern '{pattern}' contains multiple capture groups. " - f"Please specify a pattern with at most one capture group." - ) diff --git a/haystack/preview/components/builders/dynamic_prompt_builder.py b/haystack/preview/components/builders/dynamic_prompt_builder.py deleted file mode 100644 index 36ba15f804..0000000000 --- a/haystack/preview/components/builders/dynamic_prompt_builder.py +++ /dev/null @@ -1,331 +0,0 @@ -import logging -from typing import Dict, Any, Optional, List, Union, Set - -from jinja2 import Template, meta - -from haystack.preview import component -from haystack.preview import default_to_dict -from haystack.preview.dataclasses.chat_message import ChatMessage, ChatRole - -logger = logging.getLogger(__name__) - - -@component -class DynamicPromptBuilder: - """ - DynamicPromptBuilder is designed to construct dynamic prompts by processing either a list of `ChatMessage` - instances or a string template. It integrates with Jinja2 templating for dynamic prompt generation. - - In the case of `ChatMessage` instances, DynamicPromptBuilder assumes the last user message in the list as a - template and renders it with resolved pipeline variables and any additional template variables provided. For a - string template, it applies the template variables directly to render the final prompt. This dual functionality - allows DynamicPromptBuilder to be versatile in handling different types of prompt sources, making it suitable for - both chat-based and non-chat-based prompt generation scenarios. - - You can provide additional template variables directly to the pipeline `run` method. They are then merged with the - variables resolved from the pipeline runtime. This allows for greater flexibility and customization of the - generated prompts based on runtime conditions and user inputs. - - The following example demonstrates how to use DynamicPromptBuilder to generate a chat prompt: - - ```python - from haystack.preview.components.builders import DynamicPromptBuilder - from haystack.preview.components.generators.chat import GPTChatGenerator - from haystack.preview.dataclasses import ChatMessage - from haystack.preview import Pipeline - - # no parameter init, we don't use any runtime template variables - prompt_builder = DynamicPromptBuilder() - llm = GPTChatGenerator(api_key="", model_name="gpt-3.5-turbo") - - pipe = Pipeline() - pipe.add_component("prompt_builder", prompt_builder) - pipe.add_component("llm", llm) - pipe.connect("prompt_builder.prompt", "llm.messages") - - location = "Berlin" - messages = [ChatMessage.from_system("Always respond in German even if some input data is in other languages."), - ChatMessage.from_user("Tell me about {{location}}")] - - - pipe.run(data={"prompt_builder": {"template_variables":{"location": location}, "prompt_source": messages}}) - - >> {'llm': {'replies': [ChatMessage(content='Berlin ist die Hauptstadt Deutschlands und die größte Stadt des Landes. - >> Es ist eine lebhafte Metropole, die für ihre Geschichte, Kultur und einzigartigen Sehenswürdigkeiten bekannt ist. - >> Berlin bietet eine vielfältige Kulturszene, beeindruckende architektonische Meisterwerke wie den Berliner Dom - >> und das Brandenburger Tor, sowie weltberühmte Museen wie das Pergamonmuseum. Die Stadt hat auch eine pulsierende - >> Clubszene und ist für ihr aufregendes Nachtleben berühmt. Berlin ist ein Schmelztiegel verschiedener Kulturen und - >> zieht jedes Jahr Millionen von Touristen an.', role=, name=None, - >> metadata={'model': 'gpt-3.5-turbo-0613', 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 32, - >> 'completion_tokens': 153, 'total_tokens': 185}})]}} - ``` - - The following example demonstrates how to use DynamicPromptBuilder to generate a chat prompt with resolution - of pipeline runtime variables (such as documents): - - ```python - from haystack.preview.components.builders import DynamicPromptBuilder - from haystack.preview.components.generators.chat import GPTChatGenerator - from haystack.preview.dataclasses import ChatMessage, Document - from haystack.preview import Pipeline, component - from typing import List - - # we'll use documents runtime variable in our template, so we need to specify it in the init - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"]) - llm = GPTChatGenerator(api_key="", model_name="gpt-3.5-turbo") - - - @component - class DocumentProducer: - - @component.output_types(documents=List[Document]) - def run(self, doc_input: str): - return {"documents": [Document(content=doc_input)]} - - - - pipe = Pipeline() - pipe.add_component("doc_producer", DocumentProducer()) - pipe.add_component("prompt_builder", prompt_builder) - pipe.add_component("llm", llm) - - # note here how prompt_builder.documents is received from doc_producer.documents - pipe.connect("doc_producer.documents", "prompt_builder.documents") - pipe.connect("prompt_builder.prompt", "llm.messages") - - messages = [ChatMessage.from_system("Be helpful assistant, but brief!"), - ChatMessage.from_user("Here is the document: {{documents[0].content}} Now, answer the - following: {{query}}")] - - - pipe.run(data={"doc_producer": {"doc_input": "Hello world, I'm Haystack!"}, - "prompt_builder": {"prompt_source": messages, - "template_variables":{"query": "who's making a greeting?"}}}) - - >> {'llm': {'replies': [ChatMessage(content='Haystack', role=, name=None, - >> metadata={'model': 'gpt-3.5-turbo-0613', 'index': 0, 'finish_reason': 'stop', 'usage': - >> {'prompt_tokens': 51, 'completion_tokens': 2, 'total_tokens': 53}})]}} - ``` - - Similarly to chat prompt generation, you can use DynamicPromptBuilder to generate non-chat-based prompts. - The following example demonstrates how to use DynamicPromptBuilder to generate a non-chat prompt: - - ```python - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=False) - llm = GPTGenerator(api_key="", model_name="gpt-3.5-turbo") - - - @component - class DocumentProducer: - - @component.output_types(documents=List[Document]) - def run(self, doc_input: str): - return {"documents": [Document(content=doc_input)]} - - - pipe = Pipeline() - pipe.add_component("doc_producer", DocumentProducer()) - pipe.add_component("prompt_builder", prompt_builder) - pipe.add_component("llm", llm) - pipe.connect("doc_producer.documents", "prompt_builder.documents") - pipe.connect("prompt_builder.prompt", "llm.prompt") - - template = "Here is the document: {{documents[0].content}} \n Answer: {{query}}" - pipe.run(data={"doc_producer": {"doc_input": "Hello world, I live in Berlin"}, - "prompt_builder": {"prompt_source": template, - "template_variables":{"query": "Where does the speaker live?"}}}) - - >> {'llm': {'replies': ['The speaker lives in Berlin.'], - >> 'metadata': [{'model': 'gpt-3.5-turbo-0613', - >> 'index': 0, - >> 'finish_reason': 'stop', - >> 'usage': {'prompt_tokens': 28, - >> 'completion_tokens': 6, - >> 'total_tokens': 34}}]}} - - """ - - def __init__(self, runtime_variables: Optional[List[str]] = None, chat_mode: Optional[bool] = True): - """ - Initializes DynamicPromptBuilder with the provided variable names. These variable names are used to resolve - variables and their values during pipeline runtime execution. For example, if `runtime_variables` contains - `documents` your instance of DynamicPromptBuilder will expect an input called `documents`. - The values associated with variables from the pipeline runtime are then injected into template placeholders - of either a ChatMessage or a string template that is provided to the `run` method. - See `run` method for more details. - - :param runtime_variables: A list of template variable names you can use in chat prompt construction. - :type runtime_variables: Optional[List[str]] - :param chat_mode: A boolean flag to indicate if the chat prompt is being built for a chat-based prompt - templating. Defaults to True. - :type chat_mode: Optional[bool] - """ - runtime_variables = runtime_variables or [] - - if not runtime_variables: - logger.warning( - "template_variables were not provided, DynamicPromptBuilder will not resolve any pipeline variables." - ) - # setup inputs - if chat_mode: - run_input_slots = {"prompt_source": List[ChatMessage], "template_variables": Optional[Dict[str, Any]]} - else: - run_input_slots = {"prompt_source": str, "template_variables": Optional[Dict[str, Any]]} - - kwargs_input_slots = {var: Optional[Any] for var in runtime_variables} - component.set_input_types(self, **run_input_slots, **kwargs_input_slots) - - # setup outputs - if chat_mode: - component.set_output_types(self, prompt=List[ChatMessage]) - else: - component.set_output_types(self, prompt=str) - - self.runtime_variables = runtime_variables - self.chat_mode = chat_mode - - def to_dict(self) -> Dict[str, Any]: - """ - Converts the `DynamicPromptBuilder` instance to a dictionary format, primarily for serialization purposes. - - :return: A dictionary representation of the `DynamicPromptBuilder` instance, including its template variables. - :rtype: Dict[str, Any] - """ - return default_to_dict(self, runtime_variables=self.runtime_variables, chat_mode=self.chat_mode) - - def run( - self, - prompt_source: Union[List[ChatMessage], str], - template_variables: Optional[Dict[str, Any]] = None, - **kwargs, - ): - """ - Executes the dynamic prompt building process. Depending on the provided type of `prompt_source`, this method - either processes a list of `ChatMessage` instances or a string template. In the case of `ChatMessage` instances, - the last user message is treated as a template and rendered with the resolved pipeline variables and any - additional template variables provided. For a string template, it directly applies the template variables to - render the final prompt. You can provide additional template variables directly to this method, that are then merged - with the variables resolved from the pipeline runtime. - - :param prompt_source: A list of `ChatMessage` instances or a string template. The list scenario assumes the last - user message as the template for the chat prompt, while the string scenario is used for non-chat-based prompts. - :type prompt_source: Union[List[ChatMessage], str] - - :param template_variables: An optional dictionary of template variables. Template variables provided at - initialization are required to resolve pipeline variables, and these are additional variables users can - provide directly to this method. - :type template_variables: Optional[Dict[str, Any]] - - :param kwargs: Additional keyword arguments, typically resolved from a pipeline, which are merged with the - provided template variables. - - :return: A dictionary containing the key "prompt", which holds either the updated list of `ChatMessage` - instances or the rendered string template, forming the complete dynamic prompt. - :rtype: Dict[str, Union[List[ChatMessage], str]] - """ - kwargs = kwargs or {} - template_variables = template_variables or {} - template_variables_combined = {**kwargs, **template_variables} - if not template_variables_combined: - raise ValueError( - "The DynamicPromptBuilder run method requires template variables, but none were provided. " - "Please provide an appropriate template variable to enable prompt generation." - ) - # some of these checks are superfluous because pipeline will check them as well but let's - # handle them anyway for better error messages and robustness - result: Union[List[ChatMessage], str] - if isinstance(prompt_source, str): - result = self._process_simple_template(prompt_source, template_variables_combined) - elif isinstance(prompt_source, list): - result = self._process_chat_messages(prompt_source, template_variables_combined) - else: - raise ValueError( - f"{self.__class__.__name__} was not provided with a list of ChatMessage(s) or a string template." - "Please check the parameters passed to its run method." - ) - return {"prompt": result} - - def _process_simple_template(self, prompt_source: str, template_variables: Dict[str, Any]) -> str: - """ - Renders the template from the provided string source with the provided template variables. - - :param prompt_source: A Jinja2 template as a string. - :type prompt_source: str - :param template_variables: A dictionary of template variables. - :type template_variables: Dict[str, Any] - :return: A string containing the rendered template. - :rtype: str - """ - template = self._validate_template(prompt_source, set(template_variables.keys())) - return template.render(template_variables) - - def _process_chat_messages(self, prompt_source: List[ChatMessage], template_variables: Dict[str, Any]): - """ - Processes a list of :class:`ChatMessage` instances to generate a chat prompt. - - It takes the last user message in the list, treats it as a template, and renders it with the provided - template variables. The resulting message replaces the last user message in the list, forming a complete, - templated chat prompt. - - :param prompt_source: A list of `ChatMessage` instances to be processed. The last message is expected - to be from a user and is treated as a template. - :type prompt_source: List[ChatMessage] - - :param template_variables: A dictionary of template variables used for rendering the last user message. - :type template_variables: Dict[str, Any] - - :return: A list of `ChatMessage` instances, where the last user message has been replaced with its - templated version. - :rtype: List[ChatMessage] - - :raises ValueError: If `chat_messages` is empty or contains elements that are not instances of - `ChatMessage`. - :raises ValueError: If the last message in `chat_messages` is not from a user. - """ - if not prompt_source: - raise ValueError( - f"The {self.__class__.__name__} requires a non-empty list of ChatMessage instances. " - f"Please provide a valid list of ChatMessage instances to render the prompt." - ) - if not all(isinstance(message, ChatMessage) for message in prompt_source): - raise ValueError( - f"The {self.__class__.__name__} expects a list containing only ChatMessage instances. " - f"The provided list contains other types. Please ensure that all elements in the list " - f"are ChatMessage instances." - ) - - last_message: ChatMessage = prompt_source[-1] - if last_message.is_from(ChatRole.USER): - template = self._validate_template(last_message.content, set(template_variables.keys())) - templated_user_message = ChatMessage.from_user(template.render(template_variables)) - return prompt_source[:-1] + [templated_user_message] - else: - logger.warning( - "DynamicPromptBuilder was not provided with a user message as the last message in " - "chat conversation, no templating will be applied." - ) - return prompt_source - - def _validate_template(self, template_text: str, provided_variables: Set[str]): - """ - Checks if all the required template variables are provided to the pipeline `run` method. - If all the required template variables are provided, returns a Jinja2 template object. - Otherwise, raises a ValueError. - - :param template_text: A Jinja2 template as a string. - :param provided_variables: A set of provided template variables. - :type provided_variables: Set[str] - :return: A Jinja2 template object if all the required template variables are provided. - :raises ValueError: If all the required template variables are not provided. - """ - template = Template(template_text) - ast = template.environment.parse(template_text) - required_template_variables = meta.find_undeclared_variables(ast) - filled_template_vars = required_template_variables.intersection(provided_variables) - if len(filled_template_vars) != len(required_template_variables): - raise ValueError( - f"The {self.__class__.__name__} requires specific template variables that are missing. " - f"Required variables: {required_template_variables}. Only the following variables were " - f"provided: {provided_variables}. Please provide all the required template variables." - ) - return template diff --git a/haystack/preview/components/builders/prompt_builder.py b/haystack/preview/components/builders/prompt_builder.py deleted file mode 100644 index b6e0da6fb4..0000000000 --- a/haystack/preview/components/builders/prompt_builder.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Dict, Any - -from jinja2 import Template, meta - -from haystack.preview import component -from haystack.preview import default_to_dict - - -@component -class PromptBuilder: - """ - PromptBuilder is a component that renders a prompt from a template string using Jinja2 engine. - The template variables found in the template string are used as input types for the component and are all required. - - Usage: - ```python - template = "Translate the following context to {{ target_language }}. Context: {{ snippet }}; Translation:" - builder = PromptBuilder(template=template) - builder.run(target_language="spanish", snippet="I can't speak spanish.") - ``` - """ - - def __init__(self, template: str): - """ - Initialize the component with a template string. - - :param template: Jinja2 template string, e.g. "Summarize this document: {documents}\nSummary:" - :type template: str - """ - self._template_string = template - self.template = Template(template) - ast = self.template.environment.parse(template) - template_variables = meta.find_undeclared_variables(ast) - component.set_input_types(self, **{var: Any for var in template_variables}) - - def to_dict(self) -> Dict[str, Any]: - return default_to_dict(self, template=self._template_string) - - @component.output_types(prompt=str) - def run(self, **kwargs): - return {"prompt": self.template.render(kwargs)} diff --git a/haystack/preview/components/caching/__init__.py b/haystack/preview/components/caching/__init__.py deleted file mode 100644 index d2e8a69c1f..0000000000 --- a/haystack/preview/components/caching/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.caching.url_cache_checker import UrlCacheChecker - -__all__ = ["UrlCacheChecker"] diff --git a/haystack/preview/components/caching/url_cache_checker.py b/haystack/preview/components/caching/url_cache_checker.py deleted file mode 100644 index c3d87bcfcc..0000000000 --- a/haystack/preview/components/caching/url_cache_checker.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import List, Dict, Any - -from haystack.preview import component, Document, default_from_dict, default_to_dict, DeserializationError -from haystack.preview.document_stores import DocumentStore, document_store - - -@component -class UrlCacheChecker: - """ - A component checks for the presence of a document from a specific URL in the store. UrlCacheChecker can thus - implement caching functionality within web retrieval pipelines that use a Document Store. - """ - - def __init__(self, document_store: DocumentStore, url_field: str = "url"): - """ - Create a UrlCacheChecker component. - """ - self.document_store = document_store - self.url_field = url_field - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict(self, document_store=self.document_store.to_dict(), url_field=self.url_field) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "UrlCacheChecker": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - if "document_store" not in init_params: - raise DeserializationError("Missing 'document_store' in serialization data") - if "type" not in init_params["document_store"]: - raise DeserializationError("Missing 'type' in document store's serialization data") - if init_params["document_store"]["type"] not in document_store.registry: - raise DeserializationError(f"DocumentStore of type '{init_params['document_store']['type']}' not found.") - docstore_class = document_store.registry[init_params["document_store"]["type"]] - docstore = docstore_class.from_dict(init_params["document_store"]) - - data["init_parameters"]["document_store"] = docstore - return default_from_dict(cls, data) - - @component.output_types(hits=List[Document], misses=List[str]) - def run(self, urls: List[str]): - """ - Checks if any document coming from the given URL is already present in the store. If matching documents are - found, they are returned. If not, the URL is returned as a miss. - - :param urls: All the URLs the documents may be coming from to hit this cache. - """ - found_documents = [] - missing_urls = [] - - for url in urls: - filters = {self.url_field: url} - found = self.document_store.filter_documents(filters=filters) - if found: - found_documents.extend(found) - else: - missing_urls.append(url) - return {"hits": found_documents, "misses": missing_urls} diff --git a/haystack/preview/components/classifiers/__init__.py b/haystack/preview/components/classifiers/__init__.py deleted file mode 100644 index 6a4cfaee8d..0000000000 --- a/haystack/preview/components/classifiers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.classifiers.document_language_classifier import DocumentLanguageClassifier - -__all__ = ["DocumentLanguageClassifier"] diff --git a/haystack/preview/components/classifiers/document_language_classifier.py b/haystack/preview/components/classifiers/document_language_classifier.py deleted file mode 100644 index 5a04b4675b..0000000000 --- a/haystack/preview/components/classifiers/document_language_classifier.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging -from typing import List, Dict, Optional - -from haystack.preview import component, Document -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - -with LazyImport("Run 'pip install langdetect'") as langdetect_import: - import langdetect - - -@component -class DocumentLanguageClassifier: - """ - Classify the language of documents and add the detected language to their metadata. - A MetadataRouter can then route them onto different output connections depending on their language. - This is useful to route documents to different models in a pipeline depending on their language. - The set of supported languages can be specified. - For routing plain text using the same logic, use the related TextLanguageRouter component instead. - - Example usage within an indexing pipeline, storing in a Document Store - only documents written in English: - - ```python - document_store = InMemoryDocumentStore() - p = Pipeline() - p.add_component(instance=TextFileToDocument(), name="text_file_converter") - p.add_component(instance=DocumentLanguageClassifier(), name="language_classifier") - p.add_component(instance=MetadataRouter(rules={"en": {"language": {"$eq": "en"}}}), name="router") - p.add_component(instance=DocumentWriter(document_store=document_store), name="writer") - p.connect("text_file_converter.documents", "language_classifier.documents") - p.connect("language_classifier.documents", "router.documents") - p.connect("router.en", "writer.documents") - ``` - """ - - def __init__(self, languages: Optional[List[str]] = None): - """ - :param languages: A list of languages in ISO code, each corresponding to a different output connection - (see [langdetect` documentation](https://github.com/Mimino666/langdetect#languages)). - By default, only ["en"] is supported and Documents of any other language are routed to "unmatched". - """ - langdetect_import.check() - if not languages: - languages = ["en"] - self.languages = languages - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document]): - """ - Run the DocumentLanguageClassifier. This method classifies the documents' language and adds it to their metadata. - If a Document's text does not match any of the languages specified at initialization, the metadata value "unmatched" will be stored. - - :param documents: A list of documents to classify their language. - :return: List of Documents with an added metadata field called language. - """ - if not isinstance(documents, list) or documents and not isinstance(documents[0], Document): - raise TypeError( - "DocumentLanguageClassifier expects a list of Document as input. " - "In case you want to classify a text, please use the TextLanguageClassifier." - ) - - output: Dict[str, List[Document]] = {language: [] for language in self.languages} - output["unmatched"] = [] - - for document in documents: - detected_language = self.detect_language(document) - if detected_language in self.languages: - document.meta["language"] = detected_language - else: - document.meta["language"] = "unmatched" - - return {"documents": documents} - - def detect_language(self, document: Document) -> Optional[str]: - try: - language = langdetect.detect(document.content) - except langdetect.LangDetectException: - logger.warning("Langdetect cannot detect the language of Document with id: %s", document.id) - language = None - return language diff --git a/haystack/preview/components/converters/__init__.py b/haystack/preview/components/converters/__init__.py deleted file mode 100644 index 62367b2222..0000000000 --- a/haystack/preview/components/converters/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from haystack.preview.components.converters.txt import TextFileToDocument -from haystack.preview.components.converters.tika import TikaDocumentConverter -from haystack.preview.components.converters.azure import AzureOCRDocumentConverter -from haystack.preview.components.converters.pypdf import PyPDFToDocument -from haystack.preview.components.converters.html import HTMLToDocument -from haystack.preview.components.converters.markdown import MarkdownToDocument - -__all__ = [ - "TextFileToDocument", - "TikaDocumentConverter", - "AzureOCRDocumentConverter", - "PyPDFToDocument", - "HTMLToDocument", - "MarkdownToDocument", -] diff --git a/haystack/preview/components/converters/azure.py b/haystack/preview/components/converters/azure.py deleted file mode 100644 index 304078d7d2..0000000000 --- a/haystack/preview/components/converters/azure.py +++ /dev/null @@ -1,105 +0,0 @@ -from pathlib import Path -from typing import List, Union, Dict, Any, Optional -import os - -from haystack.preview.lazy_imports import LazyImport -from haystack.preview import component, Document, default_to_dict - - -with LazyImport(message="Run 'pip install \"azure-ai-formrecognizer>=3.2.0b2\"'") as azure_import: - from azure.ai.formrecognizer import DocumentAnalysisClient, AnalyzeResult - from azure.core.credentials import AzureKeyCredential - - -@component -class AzureOCRDocumentConverter: - """ - A component for converting files to Documents using Azure's Document Intelligence service. - Supported file formats are: PDF, JPEG, PNG, BMP, TIFF, DOCX, XLSX, PPTX, and HTML. - - In order to be able to use this component, you need an active Azure account - and a Document Intelligence or Cognitive Services resource. Please follow the steps described in the - [Azure documentation](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api) - to set up your resource. - """ - - def __init__(self, endpoint: str, api_key: Optional[str] = None, model_id: str = "prebuilt-read"): - """ - Create an AzureOCRDocumentConverter component. - - :param endpoint: The endpoint of your Azure resource. - :param api_key: The key of your Azure resource. It can be - explicitly provided or automatically read from the - environment variable AZURE_AI_API_KEY (recommended). - :param model_id: The model ID of the model you want to use. Please refer to [Azure documentation](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/choose-model-feature) - for a list of available models. Default: `"prebuilt-read"`. - """ - azure_import.check() - - if api_key is None: - try: - api_key = os.environ["AZURE_AI_API_KEY"] - except KeyError as e: - raise ValueError( - "AzureOCRDocumentConverter expects an Azure Credential key. " - "Set the AZURE_AI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - - self.api_key = api_key - self.document_analysis_client = DocumentAnalysisClient( - endpoint=endpoint, credential=AzureKeyCredential(api_key) - ) - self.endpoint = endpoint - self.model_id = model_id - - @component.output_types(documents=List[Document], azure=List[Dict]) - def run(self, paths: List[Union[str, Path]]): - """ - Convert files to Documents using Azure's Document Intelligence service. - - This component creates two outputs: `documents` and `raw_azure_response`. The `documents` output contains - a list of Documents that were created from the files. The `raw_azure_response` output contains a list of - the raw responses from Azure's Document Intelligence service. - - :param paths: Paths to the files to convert. - """ - documents = [] - azure_output = [] - for path in paths: - path = Path(path) - with open(path, "rb") as file: - poller = self.document_analysis_client.begin_analyze_document(model_id=self.model_id, document=file) - result = poller.result() - azure_output.append(result.to_dict()) - - file_suffix = path.suffix - document = AzureOCRDocumentConverter._convert_azure_result_to_document(result, file_suffix) - documents.append(document) - - return {"documents": documents, "raw_azure_response": azure_output} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict(self, endpoint=self.endpoint, model_id=self.model_id) - - @staticmethod - def _convert_azure_result_to_document(result: "AnalyzeResult", file_suffix: str) -> Document: - """ - Convert the result of Azure OCR to a Haystack text Document. - """ - if file_suffix == ".pdf": - text = "" - for page in result.pages: - lines = page.lines if page.lines else [] - for line in lines: - text += f"{line.content}\n" - - text += "\f" - else: - text = result.content - - document = Document(content=text) - - return document diff --git a/haystack/preview/components/converters/html.py b/haystack/preview/components/converters/html.py deleted file mode 100644 index 8b68119b38..0000000000 --- a/haystack/preview/components/converters/html.py +++ /dev/null @@ -1,94 +0,0 @@ -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -from haystack.preview import Document, component -from haystack.preview.dataclasses import ByteStream -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - -with LazyImport("Run 'pip install boilerpy3'") as boilerpy3_import: - from boilerpy3 import extractors - - -@component -class HTMLToDocument: - """ - Converts an HTML file to a Document. - - Usage example: - ```python - from haystack.preview.components.converters.html import HTMLToDocument - - converter = HTMLToDocument() - results = converter.run(sources=["sample.html"]) - documents = results["documents"] - print(documents[0].content) - # 'This is a text from the HTML file.' - ``` - - """ - - def __init__(self): - """ - Initializes the HTMLToDocument component. - """ - boilerpy3_import.check() - - @component.output_types(documents=List[Document]) - def run(self, sources: List[Union[str, Path, ByteStream]], meta: Optional[List[Dict[str, Any]]] = None): - """ - Converts a list of HTML files to Documents. - - :param sources: List of HTML file paths or ByteStream objects. - :param meta: Optional list of metadata to attach to the Documents. - The length of the list must match the number of sources. Defaults to `None`. - :return: List of converted Documents. - """ - - documents = [] - - # Create metadata placeholders if not provided - if meta: - if len(sources) != len(meta): - raise ValueError("The length of the metadata list must match the number of sources.") - else: - meta = [{}] * len(sources) - - extractor = extractors.ArticleExtractor(raise_on_failure=False) - - for source, metadata in zip(sources, meta): - try: - file_content, extracted_meta = self._extract_content(source) - except Exception as e: - logger.warning("Could not read %s. Skipping it. Error: %s", source, e) - continue - try: - text = extractor.get_content(file_content) - except Exception as conversion_e: # Consider specifying the expected exception type(s) here - logger.warning("Failed to extract text from %s. Skipping it. Error: %s", source, conversion_e) - continue - - # Merge metadata received from ByteStream with supplied metadata - if extracted_meta: - # Supplied metadata overwrites metadata from ByteStream for overlapping keys. - metadata = {**extracted_meta, **metadata} - document = Document(content=text, meta=metadata) - documents.append(document) - - return {"documents": documents} - - def _extract_content(self, source: Union[str, Path, ByteStream]) -> tuple: - """ - Extracts content from the given data source - :param source: The data source to extract content from. - :return: The extracted content and metadata. - """ - if isinstance(source, (str, Path)): - with open(source) as text_file: - return (text_file.read(), None) - if isinstance(source, ByteStream): - return (source.data.decode("utf-8"), source.metadata) - - raise ValueError(f"Unsupported source type: {type(source)}") diff --git a/haystack/preview/components/converters/markdown.py b/haystack/preview/components/converters/markdown.py deleted file mode 100644 index 145b78a910..0000000000 --- a/haystack/preview/components/converters/markdown.py +++ /dev/null @@ -1,97 +0,0 @@ -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -from tqdm import tqdm - -from haystack.preview import Document, component -from haystack.preview.dataclasses import ByteStream -from haystack.preview.lazy_imports import LazyImport - -with LazyImport("Run 'pip install markdown-it-py mdit_plain'") as markdown_conversion_imports: - from markdown_it import MarkdownIt - from mdit_plain.renderer import RendererPlain - - -logger = logging.getLogger(__name__) - - -@component -class MarkdownToDocument: - """ - Converts a Markdown file into a text Document. - - Usage example: - ```python - from haystack.preview.components.converters.markdown import MarkdownToDocument - - converter = MarkdownToDocument() - results = converter.run(sources=["sample.md"]) - documents = results["documents"] - print(documents[0].content) - # 'This is a text from the markdown file.' - ``` - """ - - def __init__(self, table_to_single_line: bool = False, progress_bar: bool = True): - """ - :param table_to_single_line: Convert contents of the table into a single line. Defaults to False. - :param progress_bar: Show a progress bar for the conversion. Defaults to True. - """ - markdown_conversion_imports.check() - - self.table_to_single_line = table_to_single_line - self.progress_bar = progress_bar - - @component.output_types(documents=List[Document]) - def run(self, sources: List[Union[str, Path, ByteStream]], meta: Optional[List[Dict[str, Any]]] = None): - """ - Reads text from a markdown file and executes optional preprocessing steps. - - :param sources: A list of markdown data sources (file paths or binary objects) - :param meta: Optional list of metadata to attach to the Documents. - The length of the list must match the number of paths. Defaults to `None`. - """ - parser = MarkdownIt(renderer_cls=RendererPlain) - if self.table_to_single_line: - parser.enable("table") - - documents = [] - if meta is None: - meta = [{}] * len(sources) - - for source, metadata in tqdm( - zip(sources, meta), - total=len(sources), - desc="Converting markdown files to Documents", - disable=not self.progress_bar, - ): - try: - file_content = self._extract_content(source) - except Exception as e: - logger.warning("Could not read %s. Skipping it. Error: %s", source, e) - continue - try: - text = parser.render(file_content) - except Exception as conversion_e: # Consider specifying the expected exception type(s) here - logger.warning("Failed to extract text from %s. Skipping it. Error: %s", source, conversion_e) - continue - - document = Document(content=text, meta=metadata) - documents.append(document) - - return {"documents": documents} - - def _extract_content(self, source: Union[str, Path, ByteStream]) -> str: - """ - Extracts content from the given data source. - :param source: The data source to extract content from. - :return: The extracted content. - """ - if isinstance(source, (str, Path)): - with open(source) as text_file: - return text_file.read() - if isinstance(source, ByteStream): - return source.data.decode("utf-8") - - raise ValueError(f"Unsupported source type: {type(source)}") diff --git a/haystack/preview/components/converters/pypdf.py b/haystack/preview/components/converters/pypdf.py deleted file mode 100644 index ede7da3816..0000000000 --- a/haystack/preview/components/converters/pypdf.py +++ /dev/null @@ -1,105 +0,0 @@ -import io -import logging -from typing import List, Union, Protocol, Dict -from pathlib import Path - -from haystack.preview.dataclasses import ByteStream -from haystack.preview.lazy_imports import LazyImport -from haystack.preview import Document, component, default_to_dict - -with LazyImport("Run 'pip install pypdf'") as pypdf_import: - from pypdf import PdfReader - - -logger = logging.getLogger(__name__) - - -class PyPDFConverter(Protocol): - """ - A protocol that defines a converter which takes a PdfReader object and converts it into a Document object. - """ - - def convert(self, reader: "PdfReader") -> Document: - ... - - -class DefaultConverter: - """ - The default converter class that extracts text from a PdfReader object's pages and returns a Document. - """ - - def convert(self, reader: "PdfReader") -> Document: - """Extract text from the PDF and return a Document object with the text content.""" - text = "".join(page.extract_text() for page in reader.pages if page.extract_text()) - return Document(content=text) - - -# This registry is used to store converters names and instances. -# It can be used to register custom converters. -CONVERTERS_REGISTRY: Dict[str, PyPDFConverter] = {"default": DefaultConverter()} - - -@component -class PyPDFToDocument: - """ - Converts PDF files to Document objects. - It uses a converter that follows the PyPDFConverter protocol to perform the conversion. - A default text extraction converter is used if no custom converter is provided. - """ - - def __init__(self, converter_name: str = "default"): - """ - Initializes the PyPDFToDocument component with an optional custom converter. - :param converter_name: A converter name that is registered in the CONVERTERS_REGISTRY. - Defaults to 'default'. - """ - pypdf_import.check() - - try: - converter = CONVERTERS_REGISTRY[converter_name] - except KeyError: - msg = ( - f"Invalid converter_name: {converter_name}.\n Available converters: {list(CONVERTERS_REGISTRY.keys())}" - ) - raise ValueError(msg) from KeyError - self.converter_name = converter_name - self._converter: PyPDFConverter = converter - - def to_dict(self): - # do not serialize the _converter instance - return default_to_dict(self, converter_name=self.converter_name) - - @component.output_types(documents=List[Document]) - def run(self, sources: List[Union[str, Path, ByteStream]]): - """ - Converts a list of PDF sources into Document objects using the configured converter. - - :param sources: A list of PDF data sources, which can be file paths or ByteStream objects. - :return: A dictionary containing a list of Document objects under the 'documents' key. - """ - documents = [] - for source in sources: - try: - pdf_reader = self._get_pdf_reader(source) - document = self._converter.convert(pdf_reader) - except Exception as e: - logger.warning("Could not read %s and convert it to Document, skipping. %s", source, e) - continue - documents.append(document) - - return {"documents": documents} - - def _get_pdf_reader(self, source: Union[str, Path, ByteStream]) -> "PdfReader": - """ - Creates a PdfReader object from a given source, which can be a file path or a ByteStream object. - - :param source: The source of the PDF data. - :return: A PdfReader instance initialized with the PDF data from the source. - :raises ValueError: If the source type is not supported. - """ - if isinstance(source, (str, Path)): - return PdfReader(str(source)) - elif isinstance(source, ByteStream): - return PdfReader(io.BytesIO(source.data)) - else: - raise ValueError(f"Unsupported source type: {type(source)}") diff --git a/haystack/preview/components/converters/tika.py b/haystack/preview/components/converters/tika.py deleted file mode 100644 index 89f7c217eb..0000000000 --- a/haystack/preview/components/converters/tika.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging -from pathlib import Path -from typing import List, Union - -from haystack.preview.lazy_imports import LazyImport -from haystack.preview import component, Document - - -with LazyImport("Run 'pip install tika'") as tika_import: - from tika import parser as tika_parser - -logger = logging.getLogger(__name__) - - -@component -class TikaDocumentConverter: - """ - A component for converting files of different types (pdf, docx, html, etc.) to Documents. - This component uses [Apache Tika](https://tika.apache.org/) for parsing the files and, therefore, - requires a running Tika server. - """ - - def __init__(self, tika_url: str = "http://localhost:9998/tika"): - """ - Create a TikaDocumentConverter component. - - :param tika_url: URL of the Tika server. Default: `"http://localhost:9998/tika"` - """ - tika_import.check() - self.tika_url = tika_url - - @component.output_types(documents=List[Document]) - def run(self, paths: List[Union[str, Path]]): - """ - Convert files to Documents. - - :param paths: A list of paths to the files to convert. - """ - - documents = [] - for path in paths: - path = Path(path) - try: - parsed_file = tika_parser.from_file(path.as_posix(), self.tika_url) - extracted_text = parsed_file["content"] - if not extracted_text: - logger.warning("Skipping file at '%s' as Tika was not able to extract any content.", str(path)) - continue - document = Document(content=extracted_text) - documents.append(document) - except Exception as e: - logger.error("Could not convert file at '%s' to Document. Error: %s", str(path), e) - - return {"documents": documents} diff --git a/haystack/preview/components/converters/txt.py b/haystack/preview/components/converters/txt.py deleted file mode 100644 index 2e63f72861..0000000000 --- a/haystack/preview/components/converters/txt.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -from pathlib import Path -from typing import List, Union - -from haystack.preview import Document, component -from haystack.preview.dataclasses import ByteStream - - -logger = logging.getLogger(__name__) - - -@component -class TextFileToDocument: - """ - A component for converting a text file to a Document. - """ - - def __init__(self, encoding: str = "utf-8"): - """ - Create a TextFileToDocument component. - - :param encoding: The default encoding of the text files. Default: `"utf-8"`. - Note that if the encoding is specified in the metadata of a ByteStream, - it will override this default. - """ - self.encoding = encoding - - @component.output_types(documents=List[Document]) - def run(self, sources: List[Union[str, Path, ByteStream]]): - """ - Convert text files to Documents. - - :param streams: A list of paths to text files or ByteStream objects. - Note that if an encoding is specified in the metadata of a ByteStream, - it will override the component's default. - :return: A dictionary containing the converted documents. - """ - documents = [] - for source in sources: - if isinstance(source, (Path, str)): - try: - path = source - source = ByteStream.from_file_path(Path(source)) - source.metadata["file_path"] = str(path) - except Exception as e: - logger.warning("Could not convert file %s. Skipping it. Error message: %s", source, e) - continue - try: - encoding = source.metadata.get("encoding", self.encoding) - document = Document(content=source.data.decode(encoding)) - document.meta = source.metadata - documents.append(document) - except Exception as e: - logger.warning("Could not convert file %s. Skipping it. Error message: %s", source, e) - - return {"documents": documents} diff --git a/haystack/preview/components/embedders/__init__.py b/haystack/preview/components/embedders/__init__.py deleted file mode 100644 index a0840d7e0a..0000000000 --- a/haystack/preview/components/embedders/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from haystack.preview.components.embedders.sentence_transformers_text_embedder import SentenceTransformersTextEmbedder -from haystack.preview.components.embedders.sentence_transformers_document_embedder import ( - SentenceTransformersDocumentEmbedder, -) -from haystack.preview.components.embedders.openai_document_embedder import OpenAIDocumentEmbedder -from haystack.preview.components.embedders.openai_text_embedder import OpenAITextEmbedder - -__all__ = [ - "SentenceTransformersTextEmbedder", - "SentenceTransformersDocumentEmbedder", - "OpenAITextEmbedder", - "OpenAIDocumentEmbedder", -] diff --git a/haystack/preview/components/embedders/backends/__init__.py b/haystack/preview/components/embedders/backends/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/haystack/preview/components/embedders/backends/sentence_transformers_backend.py b/haystack/preview/components/embedders/backends/sentence_transformers_backend.py deleted file mode 100644 index 8883c235a4..0000000000 --- a/haystack/preview/components/embedders/backends/sentence_transformers_backend.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import List, Optional, Union, Dict - -from haystack.preview.lazy_imports import LazyImport - -with LazyImport(message="Run 'pip install \"sentence-transformers>=2.2.0\"'") as sentence_transformers_import: - from sentence_transformers import SentenceTransformer - - -class _SentenceTransformersEmbeddingBackendFactory: - """ - Factory class to create instances of Sentence Transformers embedding backends. - """ - - _instances: Dict[str, "_SentenceTransformersEmbeddingBackend"] = {} - - @staticmethod - def get_embedding_backend( - model_name_or_path: str, device: Optional[str] = None, use_auth_token: Union[bool, str, None] = None - ): - embedding_backend_id = f"{model_name_or_path}{device}{use_auth_token}" - - if embedding_backend_id in _SentenceTransformersEmbeddingBackendFactory._instances: - return _SentenceTransformersEmbeddingBackendFactory._instances[embedding_backend_id] - embedding_backend = _SentenceTransformersEmbeddingBackend( - model_name_or_path=model_name_or_path, device=device, use_auth_token=use_auth_token - ) - _SentenceTransformersEmbeddingBackendFactory._instances[embedding_backend_id] = embedding_backend - return embedding_backend - - -class _SentenceTransformersEmbeddingBackend: - """ - Class to manage Sentence Transformers embeddings. - """ - - def __init__( - self, model_name_or_path: str, device: Optional[str] = None, use_auth_token: Union[bool, str, None] = None - ): - sentence_transformers_import.check() - self.model = SentenceTransformer( - model_name_or_path=model_name_or_path, device=device, use_auth_token=use_auth_token - ) - - def embed(self, data: List[str], **kwargs) -> List[List[float]]: - embeddings = self.model.encode(data, **kwargs).tolist() - return embeddings diff --git a/haystack/preview/components/embedders/openai_document_embedder.py b/haystack/preview/components/embedders/openai_document_embedder.py deleted file mode 100644 index 1a2f1e1f18..0000000000 --- a/haystack/preview/components/embedders/openai_document_embedder.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import List, Optional, Dict, Any, Tuple -import os - -import openai -from tqdm import tqdm - - -from haystack.preview import component, Document, default_to_dict - - -@component -class OpenAIDocumentEmbedder: - """ - A component for computing Document embeddings using OpenAI models. - The embedding of each Document is stored in the `embedding` field of the Document. - - Usage example: - ```python - from haystack.preview import Document - from haystack.preview.components.embedders import OpenAIDocumentEmbedder - - doc = Document(text="I love pizza!") - - document_embedder = OpenAIDocumentEmbedder() - - result = document_embedder.run([doc]) - print(result['documents'][0].embedding) - - # [0.017020374536514282, -0.023255806416273117, ...] - ``` - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "text-embedding-ada-002", - organization: Optional[str] = None, - prefix: str = "", - suffix: str = "", - batch_size: int = 32, - progress_bar: bool = True, - metadata_fields_to_embed: Optional[List[str]] = None, - embedding_separator: str = "\n", - ): - """ - Create a OpenAIDocumentEmbedder component. - :param api_key: The OpenAI API key. It can be explicitly provided or automatically read from the - environment variable OPENAI_API_KEY (recommended). - :param model_name: The name of the model to use. - :param api_base_url: The OpenAI API Base url, defaults to `https://api.openai.com/v1`. - :param organization: The OpenAI-Organization ID, defaults to `None`. For more details, see OpenAI - [documentation](https://platform.openai.com/docs/api-reference/requesting-organization). - :param prefix: A string to add to the beginning of each text. - :param suffix: A string to add to the end of each text. - :param batch_size: Number of Documents to encode at once. - :param progress_bar: Whether to show a progress bar or not. Can be helpful to disable in production deployments - to keep the logs clean. - :param metadata_fields_to_embed: List of meta fields that should be embedded along with the Document text. - :param embedding_separator: Separator used to concatenate the meta fields to the Document text. - """ - # if the user does not provide the API key, check if it is set in the module client - api_key = api_key or openai.api_key - if api_key is None: - try: - api_key = os.environ["OPENAI_API_KEY"] - except KeyError as e: - raise ValueError( - "OpenAIDocumentEmbedder expects an OpenAI API key. " - "Set the OPENAI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - - self.model_name = model_name - self.organization = organization - self.prefix = prefix - self.suffix = suffix - self.batch_size = batch_size - self.progress_bar = progress_bar - self.metadata_fields_to_embed = metadata_fields_to_embed or [] - self.embedding_separator = embedding_separator - - openai.api_key = api_key - if organization is not None: - openai.organization = organization - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name} - - def to_dict(self) -> Dict[str, Any]: - """ - This method overrides the default serializer in order to avoid leaking the `api_key` value passed - to the constructor. - """ - return default_to_dict( - self, - model_name=self.model_name, - organization=self.organization, - prefix=self.prefix, - suffix=self.suffix, - batch_size=self.batch_size, - progress_bar=self.progress_bar, - metadata_fields_to_embed=self.metadata_fields_to_embed, - embedding_separator=self.embedding_separator, - ) - - def _prepare_texts_to_embed(self, documents: List[Document]) -> List[str]: - """ - Prepare the texts to embed by concatenating the Document text with the metadata fields to embed. - """ - texts_to_embed = [] - for doc in documents: - meta_values_to_embed = [ - str(doc.meta[key]) - for key in self.metadata_fields_to_embed - if key in doc.meta and doc.meta[key] is not None - ] - - text_to_embed = ( - self.prefix + self.embedding_separator.join(meta_values_to_embed + [doc.content or ""]) + self.suffix - ) - - # copied from OpenAI embedding_utils (https://github.com/openai/openai-python/blob/main/openai/embeddings_utils.py) - # replace newlines, which can negatively affect performance. - text_to_embed = text_to_embed.replace("\n", " ") - texts_to_embed.append(text_to_embed) - return texts_to_embed - - def _embed_batch(self, texts_to_embed: List[str], batch_size: int) -> Tuple[List[List[float]], Dict[str, Any]]: - """ - Embed a list of texts in batches. - """ - - all_embeddings = [] - metadata = {} - for i in tqdm( - range(0, len(texts_to_embed), batch_size), disable=not self.progress_bar, desc="Calculating embeddings" - ): - batch = texts_to_embed[i : i + batch_size] - response = openai.Embedding.create(model=self.model_name, input=batch) - embeddings = [el["embedding"] for el in response.data] - all_embeddings.extend(embeddings) - - if "model" not in metadata: - metadata["model"] = response.model - if "usage" not in metadata: - metadata["usage"] = dict(response.usage.items()) - else: - metadata["usage"]["prompt_tokens"] += response.usage.prompt_tokens - metadata["usage"]["total_tokens"] += response.usage.total_tokens - - return all_embeddings, metadata - - @component.output_types(documents=List[Document], metadata=Dict[str, Any]) - def run(self, documents: List[Document]): - """ - Embed a list of Documents. - The embedding of each Document is stored in the `embedding` field of the Document. - - :param documents: A list of Documents to embed. - """ - if not isinstance(documents, list) or documents and not isinstance(documents[0], Document): - raise TypeError( - "OpenAIDocumentEmbedder expects a list of Documents as input." - "In case you want to embed a string, please use the OpenAITextEmbedder." - ) - - texts_to_embed = self._prepare_texts_to_embed(documents=documents) - - embeddings, metadata = self._embed_batch(texts_to_embed=texts_to_embed, batch_size=self.batch_size) - - for doc, emb in zip(documents, embeddings): - doc.embedding = emb - - return {"documents": documents, "metadata": metadata} diff --git a/haystack/preview/components/embedders/openai_text_embedder.py b/haystack/preview/components/embedders/openai_text_embedder.py deleted file mode 100644 index 7be6065a83..0000000000 --- a/haystack/preview/components/embedders/openai_text_embedder.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import List, Optional, Dict, Any -import os - -import openai - -from haystack.preview import component, default_to_dict - - -@component -class OpenAITextEmbedder: - """ - A component for embedding strings using OpenAI models. - - Usage example: - ```python - from haystack.preview.components.embedders import OpenAITextEmbedder - - text_to_embed = "I love pizza!" - - text_embedder = OpenAITextEmbedder() - - print(text_embedder.run(text_to_embed)) - - # {'embedding': [0.017020374536514282, -0.023255806416273117, ...], - # 'metadata': {'model': 'text-embedding-ada-002-v2', - # 'usage': {'prompt_tokens': 4, 'total_tokens': 4}}} - ``` - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "text-embedding-ada-002", - organization: Optional[str] = None, - prefix: str = "", - suffix: str = "", - ): - """ - Create an OpenAITextEmbedder component. - - :param api_key: The OpenAI API key. It can be explicitly provided or automatically read from the - environment variable OPENAI_API_KEY (recommended). - :param model_name: The name of the OpenAI model to use. For more details on the available models, - see [OpenAI documentation](https://platform.openai.com/docs/guides/embeddings/embedding-models). - :param organization: The OpenAI-Organization ID, defaults to `None`. For more details, - see [OpenAI documentation](https://platform.openai.com/docs/api-reference/requesting-organization). - :param prefix: A string to add to the beginning of each text. - :param suffix: A string to add to the end of each text. - """ - # if the user does not provide the API key, check if it is set in the module client - api_key = api_key or openai.api_key - if api_key is None: - try: - api_key = os.environ["OPENAI_API_KEY"] - except KeyError as e: - raise ValueError( - "OpenAITextEmbedder expects an OpenAI API key. " - "Set the OPENAI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - - self.model_name = model_name - self.organization = organization - self.prefix = prefix - self.suffix = suffix - - openai.api_key = api_key - if organization is not None: - openai.organization = organization - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name} - - def to_dict(self) -> Dict[str, Any]: - """ - This method overrides the default serializer in order to avoid leaking the `api_key` value passed - to the constructor. - """ - - return default_to_dict( - self, model_name=self.model_name, organization=self.organization, prefix=self.prefix, suffix=self.suffix - ) - - @component.output_types(embedding=List[float], metadata=Dict[str, Any]) - def run(self, text: str): - """Embed a string.""" - if not isinstance(text, str): - raise TypeError( - "OpenAITextEmbedder expects a string as an input." - "In case you want to embed a list of Documents, please use the OpenAIDocumentEmbedder." - ) - - text_to_embed = self.prefix + text + self.suffix - - # copied from OpenAI embedding_utils (https://github.com/openai/openai-python/blob/main/openai/embeddings_utils.py) - # replace newlines, which can negatively affect performance. - text_to_embed = text_to_embed.replace("\n", " ") - - response = openai.Embedding.create(model=self.model_name, input=text_to_embed) - - metadata = {"model": response.model, "usage": dict(response.usage.items())} - embedding = response.data[0]["embedding"] - - return {"embedding": embedding, "metadata": metadata} diff --git a/haystack/preview/components/embedders/sentence_transformers_document_embedder.py b/haystack/preview/components/embedders/sentence_transformers_document_embedder.py deleted file mode 100644 index 7b7a5ca183..0000000000 --- a/haystack/preview/components/embedders/sentence_transformers_document_embedder.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import List, Optional, Union, Dict, Any - -from haystack.preview import component, Document, default_to_dict -from haystack.preview.components.embedders.backends.sentence_transformers_backend import ( - _SentenceTransformersEmbeddingBackendFactory, -) - - -@component -class SentenceTransformersDocumentEmbedder: - """ - A component for computing Document embeddings using Sentence Transformers models. - The embedding of each Document is stored in the `embedding` field of the Document. - - Usage example: - ```python - from haystack.preview import Document - from haystack.preview.components.embedders import SentenceTransformersDocumentEmbedder - doc = Document(text="I love pizza!") - doc_embedder = SentenceTransformersDocumentEmbedder() - doc_embedder.warm_up() - - result = doc_embedder.run([doc]) - print(result['documents'][0].embedding) - - # [-0.07804739475250244, 0.1498992145061493, ...] - ``` - """ - - def __init__( - self, - model_name_or_path: str = "sentence-transformers/all-mpnet-base-v2", - device: Optional[str] = None, - token: Union[bool, str, None] = None, - prefix: str = "", - suffix: str = "", - batch_size: int = 32, - progress_bar: bool = True, - normalize_embeddings: bool = False, - metadata_fields_to_embed: Optional[List[str]] = None, - embedding_separator: str = "\n", - ): - """ - Create a SentenceTransformersDocumentEmbedder component. - - :param model_name_or_path: Local path or name of the model in Hugging Face's model hub, - such as ``'sentence-transformers/all-mpnet-base-v2'``. - :param device: Device (like 'cuda' / 'cpu') that should be used for computation. - Defaults to CPU. - :param token: The API token used to download private models from Hugging Face. - If this parameter is set to `True`, then the token generated when running - `transformers-cli login` (stored in ~/.huggingface) will be used. - :param prefix: A string to add to the beginning of each Document text before embedding. - Can be used to prepend the text with an instruction, as required by some embedding models, - such as E5 and bge. - :param suffix: A string to add to the end of each Document text before embedding. - :param batch_size: Number of strings to encode at once. - :param progress_bar: If true, displays progress bar during embedding. - :param normalize_embeddings: If set to true, returned vectors will have length 1. - :param metadata_fields_to_embed: List of meta fields that should be embedded along with the Document content. - :param embedding_separator: Separator used to concatenate the meta fields to the Document content. - """ - - self.model_name_or_path = model_name_or_path - # TODO: remove device parameter and use Haystack's device management once migrated - self.device = device or "cpu" - self.token = token - self.prefix = prefix - self.suffix = suffix - self.batch_size = batch_size - self.progress_bar = progress_bar - self.normalize_embeddings = normalize_embeddings - self.metadata_fields_to_embed = metadata_fields_to_embed or [] - self.embedding_separator = embedding_separator - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name_or_path} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, - model_name_or_path=self.model_name_or_path, - device=self.device, - token=self.token if not isinstance(self.token, str) else None, # don't serialize valid tokens - prefix=self.prefix, - suffix=self.suffix, - batch_size=self.batch_size, - progress_bar=self.progress_bar, - normalize_embeddings=self.normalize_embeddings, - metadata_fields_to_embed=self.metadata_fields_to_embed, - embedding_separator=self.embedding_separator, - ) - - def warm_up(self): - """ - Load the embedding backend. - """ - if not hasattr(self, "embedding_backend"): - self.embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend( - model_name_or_path=self.model_name_or_path, device=self.device, use_auth_token=self.token - ) - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document]): - """ - Embed a list of Documents. - The embedding of each Document is stored in the `embedding` field of the Document. - """ - if not isinstance(documents, list) or documents and not isinstance(documents[0], Document): - raise TypeError( - "SentenceTransformersDocumentEmbedder expects a list of Documents as input." - "In case you want to embed a list of strings, please use the SentenceTransformersTextEmbedder." - ) - if not hasattr(self, "embedding_backend"): - raise RuntimeError("The embedding model has not been loaded. Please call warm_up() before running.") - - # TODO: once non textual Documents are properly supported, we should also prepare them for embedding here - - texts_to_embed = [] - for doc in documents: - meta_values_to_embed = [ - str(doc.meta[key]) for key in self.metadata_fields_to_embed if key in doc.meta and doc.meta[key] - ] - text_to_embed = ( - self.prefix + self.embedding_separator.join(meta_values_to_embed + [doc.content or ""]) + self.suffix - ) - texts_to_embed.append(text_to_embed) - - embeddings = self.embedding_backend.embed( - texts_to_embed, - batch_size=self.batch_size, - show_progress_bar=self.progress_bar, - normalize_embeddings=self.normalize_embeddings, - ) - - for doc, emb in zip(documents, embeddings): - doc.embedding = emb - - return {"documents": documents} diff --git a/haystack/preview/components/embedders/sentence_transformers_text_embedder.py b/haystack/preview/components/embedders/sentence_transformers_text_embedder.py deleted file mode 100644 index badb3997ee..0000000000 --- a/haystack/preview/components/embedders/sentence_transformers_text_embedder.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import List, Optional, Union, Dict, Any - -from haystack.preview import component, default_to_dict -from haystack.preview.components.embedders.backends.sentence_transformers_backend import ( - _SentenceTransformersEmbeddingBackendFactory, -) - - -@component -class SentenceTransformersTextEmbedder: - """ - A component for embedding strings using Sentence Transformers models. - - Usage example: - ```python - from haystack.preview.components.embedders import SentenceTransformersTextEmbedder - - text_to_embed = "I love pizza!" - - text_embedder = SentenceTransformersTextEmbedder() - text_embedder.warm_up() - - print(text_embedder.run(text_to_embed)) - - # {'embedding': [-0.07804739475250244, 0.1498992145061493,, ...]} - ``` - """ - - def __init__( - self, - model_name_or_path: str = "sentence-transformers/all-mpnet-base-v2", - device: Optional[str] = None, - token: Union[bool, str, None] = None, - prefix: str = "", - suffix: str = "", - batch_size: int = 32, - progress_bar: bool = True, - normalize_embeddings: bool = False, - ): - """ - Create a SentenceTransformersTextEmbedder component. - - :param model_name_or_path: Local path or name of the model in Hugging Face's model hub, - such as ``'sentence-transformers/all-mpnet-base-v2'``. - :param device: Device (like 'cuda' / 'cpu') that should be used for computation. - Defaults to CPU. - :param token: The API token used to download private models from Hugging Face. - If this parameter is set to `True`, then the token generated when running - `transformers-cli login` (stored in ~/.huggingface) will be used. - :param prefix: A string to add to the beginning of each Document text before embedding. - Can be used to prepend the text with an instruction, as required by some embedding models, - such as E5 and bge. - :param suffix: A string to add to the end of each text. - :param batch_size: Number of strings to encode at once. - :param progress_bar: If true, displays progress bar during embedding. - :param normalize_embeddings: If set to true, returned vectors will have length 1. - """ - - self.model_name_or_path = model_name_or_path - # TODO: remove device parameter and use Haystack's device management once migrated - self.device = device or "cpu" - self.token = token - self.prefix = prefix - self.suffix = suffix - self.batch_size = batch_size - self.progress_bar = progress_bar - self.normalize_embeddings = normalize_embeddings - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name_or_path} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, - model_name_or_path=self.model_name_or_path, - device=self.device, - token=self.token if not isinstance(self.token, str) else None, # don't serialize valid tokens - prefix=self.prefix, - suffix=self.suffix, - batch_size=self.batch_size, - progress_bar=self.progress_bar, - normalize_embeddings=self.normalize_embeddings, - ) - - def warm_up(self): - """ - Load the embedding backend. - """ - if not hasattr(self, "embedding_backend"): - self.embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend( - model_name_or_path=self.model_name_or_path, device=self.device, use_auth_token=self.token - ) - - @component.output_types(embedding=List[float]) - def run(self, text: str): - """Embed a string.""" - if not isinstance(text, str): - raise TypeError( - "SentenceTransformersTextEmbedder expects a string as input." - "In case you want to embed a list of Documents, please use the SentenceTransformersDocumentEmbedder." - ) - if not hasattr(self, "embedding_backend"): - raise RuntimeError("The embedding model has not been loaded. Please call warm_up() before running.") - - text_to_embed = self.prefix + text + self.suffix - embedding = self.embedding_backend.embed( - [text_to_embed], - batch_size=self.batch_size, - show_progress_bar=self.progress_bar, - normalize_embeddings=self.normalize_embeddings, - )[0] - return {"embedding": embedding} diff --git a/haystack/preview/components/fetchers/__init__.py b/haystack/preview/components/fetchers/__init__.py deleted file mode 100644 index f0580d369a..0000000000 --- a/haystack/preview/components/fetchers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.fetchers.link_content import LinkContentFetcher - -__all__ = ["LinkContentFetcher"] diff --git a/haystack/preview/components/fetchers/link_content.py b/haystack/preview/components/fetchers/link_content.py deleted file mode 100644 index 664f716291..0000000000 --- a/haystack/preview/components/fetchers/link_content.py +++ /dev/null @@ -1,203 +0,0 @@ -import logging -from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor -from typing import Callable, Dict, List, Optional, Tuple - -import requests -from requests import Response -from requests.exceptions import HTTPError -from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt, wait_exponential - -from haystack.preview import component -from haystack.preview.dataclasses import ByteStream -from haystack.preview.version import __version__ - -logger = logging.getLogger(__name__) - - -DEFAULT_USER_AGENT = f"haystack/LinkContentFetcher/{__version__}" - -REQUEST_HEADERS = { - "accept": "*/*", - "User-Agent": DEFAULT_USER_AGENT, - "Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7", - "referer": "https://www.google.com/", -} - - -def text_content_handler(response: Response) -> ByteStream: - """ - :param response: Response object from the request. - :return: The extracted text. - """ - return ByteStream.from_string(response.text) - - -def binary_content_handler(response: Response) -> ByteStream: - """ - :param response: Response object from the request. - :return: The extracted binary file-like object. - """ - return ByteStream(data=response.content) - - -@component -class LinkContentFetcher: - """ - LinkContentFetcher is a component for fetching and extracting content from URLs. It supports handling various - content types, retries on failures, and automatic user-agent rotation for failed web requests. - """ - - def __init__( - self, - raise_on_failure: bool = True, - user_agents: Optional[List[str]] = None, - retry_attempts: int = 2, - timeout: int = 3, - ): - """ - Initializes a LinkContentFetcher instance. - - :param raise_on_failure: If True, raises an exception if it fails to fetch a single URL. - For multiple URLs, it logs errors and returns the content it successfully fetched. Default is True. - :param user_agents: A list of user agents for fetching content. If None, a default user agent is used. - :param retry_attempts: Specifies how many times you want it to retry to fetch the URL's content. Default is 2. - :param timeout: Timeout in seconds for the request. Default is 3. - """ - self.raise_on_failure = raise_on_failure - self.user_agents = user_agents or [DEFAULT_USER_AGENT] - self.current_user_agent_idx: int = 0 - self.retry_attempts = retry_attempts - self.timeout = timeout - - # register default content handlers that extract data from the response - self.handlers: Dict[str, Callable[[Response], ByteStream]] = defaultdict(lambda: text_content_handler) - self.handlers["text/html"] = text_content_handler - self.handlers["text/plain"] = text_content_handler - self.handlers["application/pdf"] = binary_content_handler - self.handlers["application/octet-stream"] = binary_content_handler - - @retry( - reraise=True, - stop=stop_after_attempt(self.retry_attempts), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=(retry_if_exception_type((HTTPError, requests.RequestException))), - # This method is invoked only after failed requests (exception raised) - after=self._switch_user_agent, - ) - def get_response(url): - # we need to copy because we modify the headers - headers = REQUEST_HEADERS.copy() - headers["User-Agent"] = self.user_agents[self.current_user_agent_idx] - response = requests.get(url, headers=headers, timeout=timeout or 3) - response.raise_for_status() - return response - - self._get_response: Callable = get_response - - @component.output_types(streams=List[ByteStream]) - def run(self, urls: List[str]): - """ - Fetches content from a list of URLs and returns a list of extracted content streams. - Each content stream is a ByteStream object containing the extracted content as binary data. - Each ByteStream object in the returned list corresponds to the contents of a single URL. - The content type of each stream is stored in the metadata of the ByteStream object under - the key "content_type". The URL of the fetched content is stored under the key "url". - - :param urls: A list of URLs to fetch content from. - :return: A lists of ByteStream objects representing the extracted content. - - :raises: If the provided list of URLs contains only a single URL, and `raise_on_failure` is set to True, - an exception will be raised in case of an error during content retrieval. In all other scenarios, any - retrieval errors are logged, and a list of successfully retrieved ByteStream objects is returned. - """ - streams: List[ByteStream] = [] - if not urls: - return {"streams": streams} - - # don't use multithreading if there's only one URL - if len(urls) == 1: - stream_metadata, stream = self.fetch(urls[0]) - stream.metadata.update(stream_metadata) - streams.append(stream) - else: - with ThreadPoolExecutor() as executor: - results = executor.map(self._fetch_with_exception_suppression, urls) - - for stream_metadata, stream in results: # type: ignore - if stream_metadata is not None and stream is not None: - stream.metadata.update(stream_metadata) - streams.append(stream) - - return {"streams": streams} - - def fetch(self, url: str) -> Tuple[Dict[str, str], ByteStream]: - """ - Fetches content from a URL and returns it as a ByteStream. - - :param url: The URL to fetch content from. - :return: A tuple containing the ByteStream metadata dict and the corresponding ByteStream. - ByteStream metadata contains the URL and the content type of the fetched content. - The content type is a string indicating the type of content fetched (for example, "text/html", "application/pdf"). - The ByteStream object contains the fetched content as binary data. - - :raises: If an error occurs during content retrieval and `raise_on_failure` is set to True, this method will - raise an exception. Otherwise, all fetching errors are logged, and an empty ByteStream is returned. - - """ - content_type: str = "text/html" - stream: ByteStream = ByteStream(data=b"") - try: - response = self._get_response(url) - content_type = self._get_content_type(response) - handler: Callable = self.handlers[content_type] - stream = handler(response) - except Exception as e: - if self.raise_on_failure: - raise e - # less verbose log as this is expected to happen often (requests failing, blocked, etc.) - logger.debug("Couldn't retrieve content from %s because %s", url, str(e)) - - finally: - self.current_user_agent_idx = 0 - - return {"content_type": content_type, "url": url}, stream - - def _fetch_with_exception_suppression(self, url: str) -> Tuple[Optional[Dict[str, str]], Optional[ByteStream]]: - """ - Fetches content from a URL and returns it as a ByteStream. - - If `raise_on_failure` is set to True, this method will wrap the fetch() method and catch any exceptions. - Otherwise, it will simply call the fetch() method. - :param url: The URL to fetch content from. - :return: A tuple containing the ByteStream metadata dict and the corresponding ByteStream. - - """ - if self.raise_on_failure: - try: - return self.fetch(url) - except Exception as e: - logger.warning("Error fetching %s: %s", url, str(e)) - return {"content_type": "Unknown", "url": url}, None - else: - return self.fetch(url) - - def _get_content_type(self, response: Response): - """ - Get the content type of the response. - - :param response: The response object. - :return: The content type of the response. - """ - content_type = response.headers.get("Content-Type", "") - return content_type.split(";")[0] - - def _switch_user_agent(self, retry_state: RetryCallState) -> None: - """ - Switches the User-Agent for this LinkContentRetriever to the next one in the list of user agents. - Used by tenacity to retry the requests with a different user agent. - - :param retry_state: The retry state (unused, required by tenacity). - """ - self.current_user_agent_idx = (self.current_user_agent_idx + 1) % len(self.user_agents) - logger.debug("Switched user agent to %s", self.user_agents[self.current_user_agent_idx]) diff --git a/haystack/preview/components/generators/__init__.py b/haystack/preview/components/generators/__init__.py deleted file mode 100644 index 037ca7b7a5..0000000000 --- a/haystack/preview/components/generators/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from haystack.preview.components.generators.cohere import CohereGenerator -from haystack.preview.components.generators.hugging_face_local import HuggingFaceLocalGenerator -from haystack.preview.components.generators.hugging_face_tgi import HuggingFaceTGIGenerator -from haystack.preview.components.generators.openai import GPTGenerator - -__all__ = ["HuggingFaceLocalGenerator", "HuggingFaceTGIGenerator", "GPTGenerator", "CohereGenerator"] diff --git a/haystack/preview/components/generators/chat/__init__.py b/haystack/preview/components/generators/chat/__init__.py deleted file mode 100644 index 2126529d83..0000000000 --- a/haystack/preview/components/generators/chat/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.generators.chat.hugging_face_tgi import HuggingFaceTGIChatGenerator -from haystack.preview.components.generators.chat.openai import GPTChatGenerator - -__all__ = ["HuggingFaceTGIChatGenerator", "GPTChatGenerator"] diff --git a/haystack/preview/components/generators/chat/hugging_face_tgi.py b/haystack/preview/components/generators/chat/hugging_face_tgi.py deleted file mode 100644 index 4d4062ef24..0000000000 --- a/haystack/preview/components/generators/chat/hugging_face_tgi.py +++ /dev/null @@ -1,280 +0,0 @@ -import logging -from dataclasses import asdict -from typing import Any, Dict, List, Optional, Iterable, Callable -from urllib.parse import urlparse - -from haystack.preview import component, default_to_dict, default_from_dict -from haystack.preview.components.generators.utils import serialize_callback_handler, deserialize_callback_handler -from haystack.preview.dataclasses import ChatMessage, StreamingChunk -from haystack.preview.components.generators.hf_utils import check_valid_model, check_generation_params -from haystack.preview.lazy_imports import LazyImport - -with LazyImport(message="Run 'pip install transformers'") as transformers_import: - from huggingface_hub import InferenceClient - from huggingface_hub.inference._text_generation import TextGenerationStreamResponse, TextGenerationResponse, Token - from transformers import AutoTokenizer - -logger = logging.getLogger(__name__) - - -class HuggingFaceTGIChatGenerator: - """ - Enables text generation using HuggingFace Hub hosted chat-based LLMs. This component is designed to seamlessly - inference chat-based models deployed on the Text Generation Inference (TGI) backend. - - You can use this component for chat LLMs hosted on Hugging Face inference endpoints, the rate-limited - Inference API tier: - - ```python - from haystack.preview.components.generators.chat import HuggingFaceTGIChatGenerator - from haystack.preview.dataclasses import ChatMessage - - messages = [ChatMessage.from_system("\nYou are a helpful, respectful and honest assistant"), - ChatMessage.from_user("What's Natural Language Processing?")] - - - client = HuggingFaceTGIChatGenerator(model="meta-llama/Llama-2-70b-chat-hf", token="") - client.warm_up() - response = client.run(messages, generation_kwargs={"max_new_tokens": 120}) - print(response) - ``` - - For chat LLMs hosted on paid https://huggingface.co/inference-endpoints endpoint and/or your own custom TGI - endpoint, you'll need to provide the URL of the endpoint as well as a valid token: - - ```python - from haystack.preview.components.generators.chat import HuggingFaceTGIChatGenerator - from haystack.preview.dataclasses import ChatMessage - - messages = [ChatMessage.from_system("\nYou are a helpful, respectful and honest assistant"), - ChatMessage.from_user("What's Natural Language Processing?")] - - client = HuggingFaceTGIChatGenerator(model="meta-llama/Llama-2-70b-chat-hf", - url="", - token="") - client.warm_up() - response = client.run(messages, generation_kwargs={"max_new_tokens": 120}) - print(response) - ``` - - Key Features and Compatibility: - - **Primary Compatibility**: Designed to work seamlessly with any chat-based model deployed using the TGI - framework. For more information on TGI, visit https://github.com/huggingface/text-generation-inference. - - **Hugging Face Inference Endpoints**: Supports inference of TGI chat LLMs deployed on Hugging Face - inference endpoints. For more details, refer to https://huggingface.co/inference-endpoints. - - **Inference API Support**: Supports inference of TGI chat LLMs hosted on the rate-limited Inference - API tier. Learn more about the Inference API at https://huggingface.co/inference-api. - Discover available chat models using the following command: - ``` - wget -qO- https://api-inference.huggingface.co/framework/text-generation-inference | grep chat - ``` - and simply use the model ID as the model parameter for this component. You'll also need to provide a valid - Hugging Face API token as the token parameter. - - **Custom TGI Endpoints**: Supports inference of TGI chat LLMs deployed on custom TGI endpoints. Anyone can - deploy their own TGI endpoint using the TGI framework. For more details, refer - to https://huggingface.co/inference-endpoints. - - Input and Output Format: - - **ChatMessage Format**: This component uses the ChatMessage format to structure both input and output, - ensuring coherent and contextually relevant responses in chat-based text generation scenarios. Details on the - ChatMessage format can be found at https://github.com/openai/openai-python/blob/main/chatml.md. - - """ - - def __init__( - self, - model: str = "meta-llama/Llama-2-13b-chat-hf", - url: Optional[str] = None, - token: Optional[str] = None, - chat_template: Optional[str] = None, - generation_kwargs: Optional[Dict[str, Any]] = None, - stop_words: Optional[List[str]] = None, - streaming_callback: Optional[Callable[[StreamingChunk], None]] = None, - ): - """ - Initialize the HuggingFaceTGIChatGenerator instance. - - :param model: A string representing the model path or URL. Default is "meta-llama/Llama-2-13b-chat-hf". - :param url: An optional string representing the URL of the TGI endpoint. - :param chat_template: This optional parameter allows you to specify a Jinja template for formatting chat - messages. While high-quality and well-supported chat models typically include their own chat templates - accessible through their tokenizer, there are models that do not offer this feature. For such scenarios, - or if you wish to use a custom template instead of the model's default, you can use this parameter to - set your preferred chat template. - :param token: The Hugging Face token for HTTP bearer authorization. - You can find your HF token at https://huggingface.co/settings/tokens. - :param generation_kwargs: A dictionary containing keyword arguments to customize text generation. - Some examples: `max_new_tokens`, `temperature`, `top_k`, `top_p`,... - See Hugging Face's [documentation](https://huggingface.co/docs/huggingface_hub/v0.18.0.rc0/en/package_reference/inference_client#huggingface_hub.inference._text_generation.TextGenerationParameters) - for more information. - :param stop_words: An optional list of strings representing the stop words. - :param streaming_callback: An optional callable for handling streaming responses. - """ - transformers_import.check() - - if url: - r = urlparse(url) - is_valid_url = all([r.scheme in ["http", "https"], r.netloc]) - if not is_valid_url: - raise ValueError(f"Invalid TGI endpoint URL provided: {url}") - - check_valid_model(model, token) - - # handle generation kwargs setup - generation_kwargs = generation_kwargs.copy() if generation_kwargs else {} - check_generation_params(generation_kwargs, ["n"]) - generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", []) - generation_kwargs["stop_sequences"].extend(stop_words or []) - - self.model = model - self.url = url - self.chat_template = chat_template - self.token = token - self.generation_kwargs = generation_kwargs - self.client = InferenceClient(url or model, token=token) - self.streaming_callback = streaming_callback - self.tokenizer = None - - def warm_up(self) -> None: - """ - Load the tokenizer. - """ - self.tokenizer = AutoTokenizer.from_pretrained(self.model, token=self.token) - # mypy can't infer that chat_template attribute exists on the object returned by AutoTokenizer.from_pretrained - chat_template = getattr(self.tokenizer, "chat_template", None) - if not chat_template and not self.chat_template: - logger.warning( - "The model '%s' doesn't have a default chat_template, and no chat_template was supplied during " - "this component's initialization. It’s possible that the model doesn't support ChatML inference " - "format, potentially leading to unexpected behavior.", - self.model, - ) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - - :return: A dictionary containing the serialized component. - """ - callback_name = serialize_callback_handler(self.streaming_callback) if self.streaming_callback else None - return default_to_dict( - self, - model=self.model, - url=self.url, - chat_template=self.chat_template, - token=self.token if not isinstance(self.token, str) else None, # don't serialize valid tokens - generation_kwargs=self.generation_kwargs, - streaming_callback=callback_name, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTGIChatGenerator": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - serialized_callback_handler = init_params.get("streaming_callback") - if serialized_callback_handler: - data["init_parameters"]["streaming_callback"] = deserialize_callback_handler(serialized_callback_handler) - return default_from_dict(cls, data) - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - # Don't send URL as it is sensitive information - return {"model": self.model} - - @component.output_types(replies=List[ChatMessage]) - def run(self, messages: List[ChatMessage], generation_kwargs: Optional[Dict[str, Any]] = None): - """ - Invoke the text generation inference based on the provided messages and generation parameters. - - :param messages: A list of ChatMessage instances representing the input messages. - :param generation_kwargs: Additional keyword arguments for text generation. - :return: A list containing the generated responses as ChatMessage instances. - """ - - # check generation kwargs given as parameters to override the default ones - additional_params = ["n", "stop_words"] - check_generation_params(generation_kwargs, additional_params) - - # update generation kwargs by merging with the default ones - generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})} - num_responses = generation_kwargs.pop("n", 1) - - # merge stop_words and stop_sequences into a single list - generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", []) - generation_kwargs["stop_sequences"].extend(generation_kwargs.pop("stop_words", [])) - - if self.tokenizer is None: - raise RuntimeError("Please call warm_up() before running LLM inference.") - - # apply either model's chat template or the user-provided one - prepared_prompt: str = self.tokenizer.apply_chat_template( - conversation=messages, chat_template=self.chat_template, tokenize=False - ) - prompt_token_count: int = len(self.tokenizer.encode(prepared_prompt, add_special_tokens=False)) - - if self.streaming_callback: - if num_responses > 1: - raise ValueError("Cannot stream multiple responses, please set n=1.") - - return self._run_streaming(prepared_prompt, prompt_token_count, generation_kwargs) - - return self._run_non_streaming(prepared_prompt, prompt_token_count, num_responses, generation_kwargs) - - def _run_streaming( - self, prepared_prompt: str, prompt_token_count: int, generation_kwargs: Dict[str, Any] - ) -> Dict[str, List[ChatMessage]]: - res: Iterable[TextGenerationStreamResponse] = self.client.text_generation( - prepared_prompt, stream=True, details=True, **generation_kwargs - ) - chunk = None - # pylint: disable=not-an-iterable - for chunk in res: - token: Token = chunk.token - if token.special: - continue - chunk_metadata = {**asdict(token), **(asdict(chunk.details) if chunk.details else {})} - stream_chunk = StreamingChunk(token.text, chunk_metadata) - self.streaming_callback(stream_chunk) # type: ignore # streaming_callback is not None (verified in the run method) - - message = ChatMessage.from_assistant(chunk.generated_text) - message.metadata.update( - { - "finish_reason": chunk.details.finish_reason.value, - "index": 0, - "model": self.client.model, - "usage": { - "completion_tokens": chunk.details.generated_tokens, - "prompt_tokens": prompt_token_count, - "total_tokens": prompt_token_count + chunk.details.generated_tokens, - }, - } - ) - return {"replies": [message]} - - def _run_non_streaming( - self, prepared_prompt: str, prompt_token_count: int, num_responses: int, generation_kwargs: Dict[str, Any] - ) -> Dict[str, List[ChatMessage]]: - chat_messages: List[ChatMessage] = [] - for _i in range(num_responses): - tgr: TextGenerationResponse = self.client.text_generation( - prepared_prompt, details=True, **generation_kwargs - ) - message = ChatMessage.from_assistant(tgr.generated_text) - message.metadata.update( - { - "finish_reason": tgr.details.finish_reason.value, - "index": _i, - "model": self.client.model, - "usage": { - "completion_tokens": len(tgr.details.tokens), - "prompt_tokens": prompt_token_count, - "total_tokens": prompt_token_count + len(tgr.details.tokens), - }, - } - ) - chat_messages.append(message) - return {"replies": chat_messages} diff --git a/haystack/preview/components/generators/chat/openai.py b/haystack/preview/components/generators/chat/openai.py deleted file mode 100644 index 0b219378ee..0000000000 --- a/haystack/preview/components/generators/chat/openai.py +++ /dev/null @@ -1,287 +0,0 @@ -import dataclasses -import logging -import os -from typing import Optional, List, Callable, Dict, Any - -import openai -from openai.openai_object import OpenAIObject - -from haystack.preview import component, default_from_dict, default_to_dict -from haystack.preview.components.generators.utils import serialize_callback_handler, deserialize_callback_handler -from haystack.preview.dataclasses import StreamingChunk, ChatMessage - -logger = logging.getLogger(__name__) - - -API_BASE_URL = "https://api.openai.com/v1" - - -@component -class GPTChatGenerator: - """ - Enables text generation using OpenAI's large language models (LLMs). It supports gpt-4 and gpt-3.5-turbo - family of models accessed through the chat completions API endpoint. - - Users can pass any text generation parameters valid for the `openai.ChatCompletion.create` method - directly to this component via the `**generation_kwargs` parameter in __init__ or the `**generation_kwargs` - parameter in `run` method. - - For more details on the parameters supported by the OpenAI API, refer to the OpenAI - [documentation](https://platform.openai.com/docs/api-reference/chat). - - ```python - from haystack.preview.components.generators.chat import GPTChatGenerator - from haystack.preview.dataclasses import ChatMessage - - messages = [ChatMessage.from_user("What's Natural Language Processing?")] - - client = GPTChatGenerator() - response = client.run(messages) - print(response) - - >>{'replies': [ChatMessage(content='Natural Language Processing (NLP) is a branch of artificial intelligence - >>that focuses on enabling computers to understand, interpret, and generate human language in a way that is - >>meaningful and useful.', role=, name=None, - >>metadata={'model': 'gpt-3.5-turbo-0613', 'index': 0, 'finish_reason': 'stop', - >>'usage': {'prompt_tokens': 15, 'completion_tokens': 36, 'total_tokens': 51}})]} - - ``` - - Key Features and Compatibility: - - **Primary Compatibility**: Designed to work seamlessly with the OpenAI API Chat Completion endpoint - and gpt-4 and gpt-3.5-turbo family of models. - - **Streaming Support**: Supports streaming responses from the OpenAI API Chat Completion endpoint. - - **Customizability**: Supports all parameters supported by the OpenAI API Chat Completion endpoint. - - Input and Output Format: - - **ChatMessage Format**: This component uses the ChatMessage format for structuring both input and output, - ensuring coherent and contextually relevant responses in chat-based text generation scenarios. Details on the - ChatMessage format can be found at: https://github.com/openai/openai-python/blob/main/chatml.md. - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "gpt-3.5-turbo", - streaming_callback: Optional[Callable[[StreamingChunk], None]] = None, - api_base_url: str = API_BASE_URL, - generation_kwargs: Optional[Dict[str, Any]] = None, - ): - """ - Creates an instance of ChatGPTGenerator. Unless specified otherwise in the `model_name`, this is for OpenAI's - GPT-3.5 model. - - :param api_key: The OpenAI API key. It can be explicitly provided or automatically read from the - environment variable OPENAI_API_KEY (recommended). - :param model_name: The name of the model to use. - :param streaming_callback: A callback function that is called when a new token is received from the stream. - The callback function accepts StreamingChunk as an argument. - :param api_base_url: The OpenAI API Base url, defaults to `https://api.openai.com/v1`. - :param generation_kwargs: Other parameters to use for the model. These parameters are all sent directly to - the OpenAI endpoint. See OpenAI [documentation](https://platform.openai.com/docs/api-reference/chat) for - more details. - Some of the supported parameters: - - `max_tokens`: The maximum number of tokens the output text can have. - - `temperature`: What sampling temperature to use. Higher values mean the model will take more risks. - Try 0.9 for more creative applications and 0 (argmax sampling) for ones with a well-defined answer. - - `top_p`: An alternative to sampling with temperature, called nucleus sampling, where the model - considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens - comprising the top 10% probability mass are considered. - - `n`: How many completions to generate for each prompt. For example, if the LLM gets 3 prompts and n is 2, - it will generate two completions for each of the three prompts, ending up with 6 completions in total. - - `stop`: One or more sequences after which the LLM should stop generating tokens. - - `presence_penalty`: What penalty to apply if a token is already present at all. Bigger values mean - the model will be less likely to repeat the same token in the text. - - `frequency_penalty`: What penalty to apply if a token has already been generated in the text. - Bigger values mean the model will be less likely to repeat the same token in the text. - - `logit_bias`: Add a logit bias to specific tokens. The keys of the dictionary are tokens, and the - values are the bias to add to that token. - """ - # if the user does not provide the API key, check if it is set in the module client - api_key = api_key or openai.api_key - if api_key is None: - try: - api_key = os.environ["OPENAI_API_KEY"] - except KeyError as e: - raise ValueError( - "GPTChatGenerator expects an OpenAI API key. " - "Set the OPENAI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - openai.api_key = api_key - - self.model_name = model_name - self.generation_kwargs = generation_kwargs or {} - self.streaming_callback = streaming_callback - - self.api_base_url = api_base_url - openai.api_base = api_base_url - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - :return: The serialized component as a dictionary. - """ - callback_name = serialize_callback_handler(self.streaming_callback) if self.streaming_callback else None - return default_to_dict( - self, - model_name=self.model_name, - streaming_callback=callback_name, - api_base_url=self.api_base_url, - generation_kwargs=self.generation_kwargs, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "GPTChatGenerator": - """ - Deserialize this component from a dictionary. - :param data: The dictionary representation of this component. - :return: The deserialized component instance. - """ - init_params = data.get("init_parameters", {}) - serialized_callback_handler = init_params.get("streaming_callback") - if serialized_callback_handler: - data["init_parameters"]["streaming_callback"] = deserialize_callback_handler(serialized_callback_handler) - return default_from_dict(cls, data) - - @component.output_types(replies=List[ChatMessage]) - def run(self, messages: List[ChatMessage], generation_kwargs: Optional[Dict[str, Any]] = None): - """ - Invoke the text generation inference based on the provided messages and generation parameters. - - :param messages: A list of ChatMessage instances representing the input messages. - :param generation_kwargs: Additional keyword arguments for text generation. These parameters will - potentially override the parameters passed in the __init__ method. - For more details on the parameters supported by the OpenAI API, refer to the - OpenAI [documentation](https://platform.openai.com/docs/api-reference/chat/create). - :return: A list containing the generated responses as ChatMessage instances. - """ - - # update generation kwargs by merging with the generation kwargs passed to the run method - generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})} - - # adapt ChatMessage(s) to the format expected by the OpenAI API - openai_formatted_messages = self._convert_to_openai_format(messages) - - completion = openai.ChatCompletion.create( - model=self.model_name, - messages=openai_formatted_messages, - stream=self.streaming_callback is not None, - **generation_kwargs, - ) - - completions: List[ChatMessage] - if self.streaming_callback: - num_responses = generation_kwargs.pop("n", 1) - if num_responses > 1: - raise ValueError("Cannot stream multiple responses, please set n=1.") - chunks: List[StreamingChunk] = [] - chunk = None - for chunk in completion: - if chunk.choices: - chunk_delta: StreamingChunk = self._build_chunk(chunk, chunk.choices[0]) - chunks.append(chunk_delta) - self.streaming_callback(chunk_delta) # invoke callback with the chunk_delta - completions = [self._connect_chunks(chunk, chunks)] - else: - completions = [self._build_message(completion, choice) for choice in completion.choices] - - # before returning, do post-processing of the completions - for message in completions: - self._check_finish_reason(message) - - return {"replies": completions} - - def _convert_to_openai_format(self, messages: List[ChatMessage]) -> List[Dict[str, Any]]: - """ - Converts the list of ChatMessage to the list of messages in the format expected by the OpenAI API. - :param messages: The list of ChatMessage. - :return: The list of messages in the format expected by the OpenAI API. - """ - openai_chat_message_format = {"role", "content", "name"} - openai_formatted_messages = [] - for m in messages: - message_dict = dataclasses.asdict(m) - filtered_message = {k: v for k, v in message_dict.items() if k in openai_chat_message_format and v} - openai_formatted_messages.append(filtered_message) - return openai_formatted_messages - - def _connect_chunks(self, chunk: OpenAIObject, chunks: List[StreamingChunk]) -> ChatMessage: - """ - Connects the streaming chunks into a single ChatMessage. - :param chunk: The last chunk returned by the OpenAI API. - :param chunks: The list of all chunks returned by the OpenAI API. - """ - complete_response = ChatMessage.from_assistant("".join([chunk.content for chunk in chunks])) - complete_response.metadata.update( - { - "model": chunk.model, - "index": 0, - "finish_reason": chunk.choices[0].finish_reason, - "usage": {}, # we don't have usage data for streaming responses - } - ) - return complete_response - - def _build_message(self, completion: OpenAIObject, choice: OpenAIObject) -> ChatMessage: - """ - Converts the non-streaming response from the OpenAI API to a ChatMessage. - :param completion: The completion returned by the OpenAI API. - :param choice: The choice returned by the OpenAI API. - :return: The ChatMessage. - """ - message: OpenAIObject = choice.message - # message.content is str but message.function_call is OpenAIObject but JSON in fact, convert to str - content = str(message.function_call) if choice.finish_reason == "function_call" else message.content - chat_message = ChatMessage.from_assistant(content) - chat_message.metadata.update( - { - "model": completion.model, - "index": choice.index, - "finish_reason": choice.finish_reason, - "usage": dict(completion.usage.items()), - } - ) - return chat_message - - def _build_chunk(self, chunk: OpenAIObject, choice: OpenAIObject) -> StreamingChunk: - """ - Converts the streaming response chunk from the OpenAI API to a StreamingChunk. - :param chunk: The chunk returned by the OpenAI API. - :param choice: The choice returned by the OpenAI API. - :return: The StreamingChunk. - """ - has_content = bool(hasattr(choice.delta, "content") and choice.delta.content) - if has_content: - content = choice.delta.content - elif hasattr(choice.delta, "function_call"): - content = choice.delta.function_call - else: - content = "" - chunk_message = StreamingChunk(content) - chunk_message.metadata.update( - {"model": chunk.model, "index": choice.index, "finish_reason": choice.finish_reason} - ) - return chunk_message - - def _check_finish_reason(self, message: ChatMessage) -> None: - """ - Check the `finish_reason` returned with the OpenAI completions. - If the `finish_reason` is `length` or `content_filter`, log a warning. - :param message: The message returned by the LLM. - """ - if message.metadata["finish_reason"] == "length": - logger.warning( - "The completion for index %s has been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions.", - message.metadata["index"], - ) - if message.metadata["finish_reason"] == "content_filter": - logger.warning( - "The completion for index %s has been truncated due to the content filter.", message.metadata["index"] - ) diff --git a/haystack/preview/components/generators/cohere.py b/haystack/preview/components/generators/cohere.py deleted file mode 100644 index ee7106a5b1..0000000000 --- a/haystack/preview/components/generators/cohere.py +++ /dev/null @@ -1,159 +0,0 @@ -import logging -import os -import sys -from typing import Any, Callable, Dict, List, Optional - -from haystack.preview.lazy_imports import LazyImport -from haystack.preview import DeserializationError, component, default_from_dict, default_to_dict - -with LazyImport(message="Run 'pip install cohere'") as cohere_import: - from cohere import Client, COHERE_API_URL - -logger = logging.getLogger(__name__) - - -@component -class CohereGenerator: - """LLM Generator compatible with Cohere's generate endpoint. - - Queries the LLM using Cohere's API. Invocations are made using 'cohere' package. - See [Cohere API](https://docs.cohere.com/reference/generate) for more details. - - Example usage: - - ```python - from haystack.preview.generators import CohereGenerator - generator = CohereGenerator(api_key="test-api-key") - generator.run(prompt="What's the capital of France?") - ``` - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "command", - streaming_callback: Optional[Callable] = None, - api_base_url: Optional[str] = None, - **kwargs, - ): - """ - Instantiates a `CohereGenerator` component. - :param api_key: The API key for the Cohere API. If not set, it will be read from the COHERE_API_KEY env var. - :param model_name: The name of the model to use. Available models are: [command, command-light, command-nightly, command-nightly-light]. Defaults to "command". - :param streaming_callback: A callback function to be called with the streaming response. Defaults to None. - :param api_base_url: The base URL of the Cohere API. Defaults to "https://api.cohere.ai". - :param kwargs: Additional model parameters. These will be used during generation. Refer to https://docs.cohere.com/reference/generate for more details. - Some of the parameters are: - - 'max_tokens': The maximum number of tokens to be generated. Defaults to 1024. - - 'truncate': One of NONE|START|END to specify how the API will handle inputs longer than the maximum token length. Defaults to END. - - 'temperature': A non-negative float that tunes the degree of randomness in generation. Lower temperatures mean less random generations. - - 'preset': Identifier of a custom preset. A preset is a combination of parameters, such as prompt, temperature etc. You can create presets in the playground. - - 'end_sequences': The generated text will be cut at the beginning of the earliest occurrence of an end sequence. The sequence will be excluded from the text. - - 'stop_sequences': The generated text will be cut at the end of the earliest occurrence of a stop sequence. The sequence will be included the text. - - 'k': Defaults to 0, min value of 0.01, max value of 0.99. - - 'p': Ensures that only the most likely tokens, with total probability mass of `p`, are considered for generation at each step. If both `k` and `p` are enabled, `p` acts after `k`. - - 'frequency_penalty': Used to reduce repetitiveness of generated tokens. The higher the value, the stronger a penalty is applied to previously present tokens, - proportional to how many times they have already appeared in the prompt or prior generation.' - - 'presence_penalty': Defaults to 0.0, min value of 0.0, max value of 1.0. Can be used to reduce repetitiveness of generated tokens. - Similar to `frequency_penalty`, except that this penalty is applied equally to all tokens that have already appeared, regardless of their exact frequencies. - - 'return_likelihoods': One of GENERATION|ALL|NONE to specify how and if the token likelihoods are returned with the response. Defaults to NONE. - - 'logit_bias': Used to prevent the model from generating unwanted tokens or to incentivize it to include desired tokens. - The format is {token_id: bias} where bias is a float between -10 and 10. - - """ - cohere_import.check() - - if not api_key: - api_key = os.environ.get("COHERE_API_KEY") - if not api_key: - raise ValueError( - "CohereGenerator needs an API key to run. Either provide it as init parameter or set the env var COHERE_API_KEY." - ) - - if not api_base_url: - api_base_url = COHERE_API_URL - - self.api_key = api_key - self.model_name = model_name - self.streaming_callback = streaming_callback - self.api_base_url = api_base_url - self.model_parameters = kwargs - self.client = Client(api_key=self.api_key, api_url=self.api_base_url) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - if self.streaming_callback: - module = self.streaming_callback.__module__ - if module == "builtins": - callback_name = self.streaming_callback.__name__ - else: - callback_name = f"{module}.{self.streaming_callback.__name__}" - else: - callback_name = None - - return default_to_dict( - self, - model_name=self.model_name, - streaming_callback=callback_name, - api_base_url=self.api_base_url, - **self.model_parameters, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "CohereGenerator": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - streaming_callback = None - if "streaming_callback" in init_params and init_params["streaming_callback"]: - parts = init_params["streaming_callback"].split(".") - module_name = ".".join(parts[:-1]) - function_name = parts[-1] - module = sys.modules.get(module_name, None) - if not module: - raise DeserializationError(f"Could not locate the module of the streaming callback: {module_name}") - streaming_callback = getattr(module, function_name, None) - if not streaming_callback: - raise DeserializationError(f"Could not locate the streaming callback: {function_name}") - data["init_parameters"]["streaming_callback"] = streaming_callback - return default_from_dict(cls, data) - - @component.output_types(replies=List[str], metadata=List[Dict[str, Any]]) - def run(self, prompt: str): - """ - Queries the LLM with the prompts to produce replies. - :param prompt: The prompt to be sent to the generative model. - """ - response = self.client.generate( - model=self.model_name, prompt=prompt, stream=self.streaming_callback is not None, **self.model_parameters - ) - if self.streaming_callback: - metadata_dict: Dict[str, Any] = {} - for chunk in response: - self.streaming_callback(chunk) - metadata_dict["index"] = chunk.index - replies = response.texts - metadata_dict["finish_reason"] = response.finish_reason - metadata = [metadata_dict] - self._check_truncated_answers(metadata) - return {"replies": replies, "metadata": metadata} - - metadata = [{"finish_reason": resp.finish_reason} for resp in response] - replies = [resp.text for resp in response] - self._check_truncated_answers(metadata) - return {"replies": replies, "metadata": metadata} - - def _check_truncated_answers(self, metadata: List[Dict[str, Any]]): - """ - Check the `finish_reason` returned with the Cohere response. - If the `finish_reason` is `MAX_TOKEN`, log a warning to the user. - :param metadata: The metadata returned by the Cohere API. - """ - if metadata[0]["finish_reason"] == "MAX_TOKENS": - logger.warning( - "Responses have been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions." - ) diff --git a/haystack/preview/components/generators/hf_utils.py b/haystack/preview/components/generators/hf_utils.py deleted file mode 100644 index 9eca92ae5a..0000000000 --- a/haystack/preview/components/generators/hf_utils.py +++ /dev/null @@ -1,57 +0,0 @@ -import inspect -from typing import Any, Dict, List, Optional - -from haystack.preview.lazy_imports import LazyImport - -with LazyImport(message="Run 'pip install transformers'") as transformers_import: - from huggingface_hub import InferenceClient, HfApi - from huggingface_hub.utils import RepositoryNotFoundError - - -def check_generation_params(kwargs: Optional[Dict[str, Any]], additional_accepted_params: Optional[List[str]] = None): - """ - Check the provided generation parameters for validity. - - :param kwargs: A dictionary containing the generation parameters. - :param additional_accepted_params: An optional list of strings representing additional accepted parameters. - :raises ValueError: If any unknown text generation parameters are provided. - """ - transformers_import.check() - - if kwargs: - accepted_params = { - param - for param in inspect.signature(InferenceClient.text_generation).parameters.keys() - if param not in ["self", "prompt"] - } - if additional_accepted_params: - accepted_params.update(additional_accepted_params) - unknown_params = set(kwargs.keys()) - accepted_params - if unknown_params: - raise ValueError( - f"Unknown text generation parameters: {unknown_params}. The valid parameters are: {accepted_params}." - ) - - -def check_valid_model(model_id: str, token: Optional[str]) -> None: - """ - Check if the provided model ID corresponds to a valid model on HuggingFace Hub. - Also check if the model is a text generation model. - - :param model_id: A string representing the HuggingFace model ID. - :param token: An optional string representing the authentication token. - :raises ValueError: If the model is not found or is not a text generation model. - """ - transformers_import.check() - - api = HfApi() - try: - model_info = api.model_info(model_id, token=token) - except RepositoryNotFoundError as e: - raise ValueError( - f"Model {model_id} not found on HuggingFace Hub. Please provide a valid HuggingFace model_id." - ) from e - - allowed_model = model_info.pipeline_tag in ["text-generation", "text2text-generation"] - if not allowed_model: - raise ValueError(f"Model {model_id} is not a text generation model. Please provide a text generation model.") diff --git a/haystack/preview/components/generators/hugging_face_local.py b/haystack/preview/components/generators/hugging_face_local.py deleted file mode 100644 index 91dc639588..0000000000 --- a/haystack/preview/components/generators/hugging_face_local.py +++ /dev/null @@ -1,236 +0,0 @@ -import logging -from typing import Any, Dict, List, Literal, Optional, Union -from copy import deepcopy - -from haystack.preview import component, default_to_dict -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - -SUPPORTED_TASKS = ["text-generation", "text2text-generation"] - -with LazyImport(message="Run 'pip install transformers[torch]'") as torch_and_transformers_import: - import torch - from huggingface_hub import model_info - from transformers import ( - pipeline, - StoppingCriteriaList, - StoppingCriteria, - PreTrainedTokenizer, - PreTrainedTokenizerFast, - ) - - class StopWordsCriteria(StoppingCriteria): - """ - Stops text generation if any one of the stop words is generated. - - Note: When a stop word is encountered, the generation of new text is stopped. - However, if the stop word is in the prompt itself, it can stop generating new text - prematurely after the first token. This is particularly important for LLMs designed - for dialogue generation. For these models, like for example mosaicml/mpt-7b-chat, - the output includes both the new text and the original prompt. Therefore, it's important - to make sure your prompt has no stop words. - """ - - def __init__( - self, - tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], - stop_words: List[str], - device: Union[str, torch.device] = "cpu", - ): - super().__init__() - encoded_stop_words = tokenizer(stop_words, add_special_tokens=False, padding=True, return_tensors="pt") - self.stop_ids = encoded_stop_words.input_ids.to(device) - - def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool: - for stop_id in self.stop_ids: - found_stop_word = self.is_stop_word_found(input_ids, stop_id) - if found_stop_word: - return True - return False - - def is_stop_word_found(self, generated_text_ids: torch.Tensor, stop_id: torch.Tensor) -> bool: - generated_text_ids = generated_text_ids[-1] - len_generated_text_ids = generated_text_ids.size(0) - len_stop_id = stop_id.size(0) - result = all(generated_text_ids[len_generated_text_ids - len_stop_id :].eq(stop_id)) - return result - - -@component -class HuggingFaceLocalGenerator: - """ - Generator based on a Hugging Face model. - This component provides an interface to generate text using a Hugging Face model that runs locally. - - Usage example: - ```python - from haystack.preview.components.generators import HuggingFaceLocalGenerator - - generator = HuggingFaceLocalGenerator(model="google/flan-t5-large", - task="text2text-generation", - generation_kwargs={ - "max_new_tokens": 100, - "temperature": 0.9, - }) - - print(generator.run("Who is the best American actor?")) - # {'replies': ['John Cusack']} - ``` - """ - - def __init__( - self, - model_name_or_path: str = "google/flan-t5-base", - task: Optional[Literal["text-generation", "text2text-generation"]] = None, - device: Optional[str] = None, - token: Optional[Union[str, bool]] = None, - generation_kwargs: Optional[Dict[str, Any]] = None, - huggingface_pipeline_kwargs: Optional[Dict[str, Any]] = None, - stop_words: Optional[List[str]] = None, - ): - """ - :param model_name_or_path: The name or path of a Hugging Face model for text generation, - for example, "google/flan-t5-large". - If the model is also specified in the `huggingface_pipeline_kwargs`, this parameter will be ignored. - :param task: The task for the Hugging Face pipeline. - Possible values are "text-generation" and "text2text-generation". - Generally, decoder-only models like GPT support "text-generation", - while encoder-decoder models like T5 support "text2text-generation". - If the task is also specified in the `huggingface_pipeline_kwargs`, this parameter will be ignored. - If not specified, the component will attempt to infer the task from the model name, - calling the Hugging Face Hub API. - :param device: The device on which the model is loaded. (e.g., "cpu", "cuda:0"). - If `device` or `device_map` is specified in the `huggingface_pipeline_kwargs`, - this parameter will be ignored. - :param token: The token to use as HTTP bearer authorization for remote files. - If True, will use the token generated when running huggingface-cli login (stored in ~/.huggingface). - If the token is also specified in the `huggingface_pipeline_kwargs`, this parameter will be ignored. - :param generation_kwargs: A dictionary containing keyword arguments to customize text generation. - Some examples: `max_length`, `max_new_tokens`, `temperature`, `top_k`, `top_p`,... - See Hugging Face's documentation for more information: - - https://huggingface.co/docs/transformers/main/en/generation_strategies#customize-text-generation - - https://huggingface.co/docs/transformers/main/en/main_classes/text_generation#transformers.GenerationConfig - :param huggingface_pipeline_kwargs: Dictionary containing keyword arguments used to initialize the - Hugging Face pipeline for text generation. - These keyword arguments provide fine-grained control over the Hugging Face pipeline. - In case of duplication, these kwargs override `model_name_or_path`, `task`, `device`, and `token` init parameters. - See Hugging Face's [documentation](https://huggingface.co/docs/transformers/en/main_classes/pipelines#transformers.pipeline.task) - for more information on the available kwargs. - In this dictionary, you can also include `model_kwargs` to specify the kwargs - for model initialization: - https://huggingface.co/docs/transformers/en/main_classes/model#transformers.PreTrainedModel.from_pretrained - :param stop_words: A list of stop words. If any one of the stop words is generated, the generation is stopped. - If you provide this parameter, you should not specify the `stopping_criteria` in `generation_kwargs`. - For some chat models, the output includes both the new text and the original prompt. - In these cases, it's important to make sure your prompt has no stop words. - """ - torch_and_transformers_import.check() - - huggingface_pipeline_kwargs = huggingface_pipeline_kwargs or {} - generation_kwargs = generation_kwargs or {} - - # check if the huggingface_pipeline_kwargs contain the essential parameters - # otherwise, populate them with values from other init parameters - huggingface_pipeline_kwargs.setdefault("model", model_name_or_path) - huggingface_pipeline_kwargs.setdefault("token", token) - if ( - device is not None - and "device" not in huggingface_pipeline_kwargs - and "device_map" not in huggingface_pipeline_kwargs - ): - huggingface_pipeline_kwargs["device"] = device - - # task identification and validation - if task is None: - if "task" in huggingface_pipeline_kwargs: - task = huggingface_pipeline_kwargs["task"] - elif isinstance(huggingface_pipeline_kwargs["model"], str): - task = model_info( - huggingface_pipeline_kwargs["model"], token=huggingface_pipeline_kwargs["token"] - ).pipeline_tag - - if task not in SUPPORTED_TASKS: - raise ValueError( - f"Task '{task}' is not supported. " f"The supported tasks are: {', '.join(SUPPORTED_TASKS)}." - ) - huggingface_pipeline_kwargs["task"] = task - - # if not specified, set return_full_text to False for text-generation - # only generated text is returned (excluding prompt) - if task == "text-generation": - generation_kwargs.setdefault("return_full_text", False) - - if stop_words and "stopping_criteria" in generation_kwargs: - raise ValueError( - "Found both the `stop_words` init parameter and the `stopping_criteria` key in `generation_kwargs`. " - "Please specify only one of them." - ) - - self.huggingface_pipeline_kwargs = huggingface_pipeline_kwargs - self.generation_kwargs = generation_kwargs - self.stop_words = stop_words - self.pipeline = None - self.stopping_criteria_list = None - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - if isinstance(self.huggingface_pipeline_kwargs["model"], str): - return {"model": self.huggingface_pipeline_kwargs["model"]} - return {"model": f"[object of type {type(self.huggingface_pipeline_kwargs['model'])}]"} - - def warm_up(self): - if self.pipeline is None: - self.pipeline = pipeline(**self.huggingface_pipeline_kwargs) - - if self.stop_words and self.stopping_criteria_list is None: - stop_words_criteria = StopWordsCriteria( - tokenizer=self.pipeline.tokenizer, stop_words=self.stop_words, device=self.pipeline.device - ) - self.stopping_criteria_list = StoppingCriteriaList([stop_words_criteria]) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - pipeline_kwargs_to_serialize = deepcopy(self.huggingface_pipeline_kwargs) - - # we don't want to serialize valid tokens - if isinstance(pipeline_kwargs_to_serialize["token"], str): - pipeline_kwargs_to_serialize["token"] = None - - return default_to_dict( - self, - huggingface_pipeline_kwargs=pipeline_kwargs_to_serialize, - generation_kwargs=self.generation_kwargs, - stop_words=self.stop_words, - ) - - @component.output_types(replies=List[str]) - def run(self, prompt: str, generation_kwargs: Optional[Dict[str, Any]] = None): - """ - Run the text generation model on the given prompt. - - :param prompt: A string representing the prompt. - :param generation_kwargs: Additional keyword arguments for text generation. - :return: A dictionary containing the generated replies. - """ - if self.pipeline is None: - raise RuntimeError("The generation model has not been loaded. Please call warm_up() before running.") - - if not prompt: - return {"replies": []} - - # merge generation kwargs from init method with those from run method - updated_generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})} - - output = self.pipeline(prompt, stopping_criteria=self.stopping_criteria_list, **updated_generation_kwargs) - replies = [o["generated_text"] for o in output if "generated_text" in o] - - if self.stop_words: - # the output of the pipeline includes the stop word - replies = [reply.replace(stop_word, "").rstrip() for reply in replies for stop_word in self.stop_words] - - return {"replies": replies} diff --git a/haystack/preview/components/generators/hugging_face_tgi.py b/haystack/preview/components/generators/hugging_face_tgi.py deleted file mode 100644 index 71dc64acd7..0000000000 --- a/haystack/preview/components/generators/hugging_face_tgi.py +++ /dev/null @@ -1,237 +0,0 @@ -import logging -from dataclasses import asdict -from typing import Any, Dict, List, Optional, Iterable, Callable -from urllib.parse import urlparse - -from haystack.preview import component, default_to_dict, default_from_dict -from haystack.preview.components.generators.utils import serialize_callback_handler, deserialize_callback_handler -from haystack.preview.dataclasses import StreamingChunk -from haystack.preview.components.generators.hf_utils import check_generation_params, check_valid_model -from haystack.preview.lazy_imports import LazyImport - -with LazyImport(message="Run 'pip install transformers'") as transformers_import: - from huggingface_hub import InferenceClient - from huggingface_hub.inference._text_generation import TextGenerationStreamResponse, TextGenerationResponse, Token - from transformers import AutoTokenizer - - -logger = logging.getLogger(__name__) - - -@component -class HuggingFaceTGIGenerator: - """ - Enables text generation using HuggingFace Hub hosted non-chat LLMs. This component is designed to seamlessly - inference models deployed on the Text Generation Inference (TGI) backend. - - You can use this component for LLMs hosted on Hugging Face inference endpoints, the rate-limited - Inference API tier: - - ```python - from haystack.preview.components.generators import HuggingFaceTGIGenerator - client = HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1", token="") - client.warm_up() - response = client.run("What's Natural Language Processing?", max_new_tokens=120) - print(response) - ``` - - Or for LLMs hosted on paid https://huggingface.co/inference-endpoints endpoint, and/or your own custom TGI endpoint. - In these two cases, you'll need to provide the URL of the endpoint as well as a valid token: - - ```python - from haystack.preview.components.generators import HuggingFaceTGIGenerator - client = HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1", - url="", - token="") - client.warm_up() - response = client.run("What's Natural Language Processing?", max_new_tokens=120) - print(response) - ``` - - - Key Features and Compatibility: - - **Primary Compatibility**: Designed to work seamlessly with any non-chat model deployed using the TGI - framework. For more information on TGI, visit https://github.com/huggingface/text-generation-inference. - - **Hugging Face Inference Endpoints**: Supports inference of TGI chat LLMs deployed on Hugging Face - inference endpoints. For more details refer to https://huggingface.co/inference-endpoints. - - **Inference API Support**: Supports inference of TGI LLMs hosted on the rate-limited Inference - API tier. Learn more about the Inference API at: https://huggingface.co/inference-api - Discover available LLMs using the following command: - ``` - wget -qO- https://api-inference.huggingface.co/framework/text-generation-inference - ``` - And simply use the model ID as the model parameter for this component. You'll also need to provide a valid - Hugging Face API token as the token parameter. - - **Custom TGI Endpoints**: Supports inference of LLMs deployed on custom TGI endpoints. Anyone can - deploy their own TGI endpoint using the TGI framework. For more details refer - to https://huggingface.co/inference-endpoints. - Input and Output Format: - - **String Format**: This component uses the str format for structuring both input and output, - ensuring coherent and contextually relevant responses in text generation scenarios. - """ - - def __init__( - self, - model: str = "mistralai/Mistral-7B-v0.1", - url: Optional[str] = None, - token: Optional[str] = None, - generation_kwargs: Optional[Dict[str, Any]] = None, - stop_words: Optional[List[str]] = None, - streaming_callback: Optional[Callable[[StreamingChunk], None]] = None, - ): - """ - Initialize the HuggingFaceTGIGenerator instance. - - :param model: A string representing the model id on HF Hub. Default is "mistralai/Mistral-7B-v0.1". - :param url: An optional string representing the URL of the TGI endpoint. - :param token: The HuggingFace token to use as HTTP bearer authorization - You can find your HF token at https://huggingface.co/settings/tokens - :param generation_kwargs: A dictionary containing keyword arguments to customize text generation. - Some examples: `max_new_tokens`, `temperature`, `top_k`, `top_p`,... - See Hugging Face's documentation for more information at: - https://huggingface.co/docs/huggingface_hub/v0.18.0.rc0/en/package_reference/inference_client#huggingface_hub.inference._text_generation.TextGenerationParameters - :param stop_words: An optional list of strings representing the stop words. - :param streaming_callback: An optional callable for handling streaming responses. - """ - transformers_import.check() - - if url: - r = urlparse(url) - is_valid_url = all([r.scheme in ["http", "https"], r.netloc]) - if not is_valid_url: - raise ValueError(f"Invalid TGI endpoint URL provided: {url}") - - check_valid_model(model, token) - - # handle generation kwargs setup - generation_kwargs = generation_kwargs.copy() if generation_kwargs else {} - check_generation_params(generation_kwargs, ["n"]) - generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", []) - generation_kwargs["stop_sequences"].extend(stop_words or []) - - self.model = model - self.url = url - self.token = token - self.generation_kwargs = generation_kwargs - self.client = InferenceClient(url or model, token=token) - self.streaming_callback = streaming_callback - self.tokenizer = None - - def warm_up(self) -> None: - """ - Load the tokenizer - """ - self.tokenizer = AutoTokenizer.from_pretrained(self.model, token=self.token) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - - :return: A dictionary containing the serialized component. - """ - callback_name = serialize_callback_handler(self.streaming_callback) if self.streaming_callback else None - return default_to_dict( - self, - model=self.model, - url=self.url, - token=self.token if not isinstance(self.token, str) else None, # don't serialize valid tokens - generation_kwargs=self.generation_kwargs, - streaming_callback=callback_name, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTGIGenerator": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - serialized_callback_handler = init_params.get("streaming_callback") - if serialized_callback_handler: - data["init_parameters"]["streaming_callback"] = deserialize_callback_handler(serialized_callback_handler) - return default_from_dict(cls, data) - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - # Don't send URL as it is sensitive information - return {"model": self.model} - - @component.output_types(replies=List[str], metadata=List[Dict[str, Any]]) - def run(self, prompt: str, generation_kwargs: Optional[Dict[str, Any]] = None): - """ - Invoke the text generation inference for the given prompt and generation parameters. - - :param prompt: A string representing the prompt. - :param generation_kwargs: Additional keyword arguments for text generation. - :return: A dictionary containing the generated replies and metadata. Both are lists of length n. - Replies are strings and metadata are dictionaries. - """ - # check generation kwargs given as parameters to override the default ones - additional_params = ["n", "stop_words"] - check_generation_params(generation_kwargs, additional_params) - - # update generation kwargs by merging with the default ones - generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})} - num_responses = generation_kwargs.pop("n", 1) - generation_kwargs.setdefault("stop_sequences", []).extend(generation_kwargs.pop("stop_words", [])) - - if self.tokenizer is None: - raise RuntimeError("Please call warm_up() before running LLM inference.") - - prompt_token_count = len(self.tokenizer.encode(prompt, add_special_tokens=False)) - - if self.streaming_callback: - if num_responses > 1: - raise ValueError("Cannot stream multiple responses, please set n=1.") - - return self._run_streaming(prompt, prompt_token_count, generation_kwargs) - - return self._run_non_streaming(prompt, prompt_token_count, num_responses, generation_kwargs) - - def _run_streaming(self, prompt: str, prompt_token_count: int, generation_kwargs: Dict[str, Any]): - res_chunk: Iterable[TextGenerationStreamResponse] = self.client.text_generation( - prompt, details=True, stream=True, **generation_kwargs - ) - chunks: List[StreamingChunk] = [] - # pylint: disable=not-an-iterable - for chunk in res_chunk: - token: Token = chunk.token - if token.special: - continue - chunk_metadata = {**asdict(token), **(asdict(chunk.details) if chunk.details else {})} - stream_chunk = StreamingChunk(token.text, chunk_metadata) - chunks.append(stream_chunk) - self.streaming_callback(stream_chunk) # type: ignore # streaming_callback is not None (verified in the run method) - metadata = { - "finish_reason": chunks[-1].metadata.get("finish_reason", None), - "model": self.client.model, - "usage": { - "completion_tokens": chunks[-1].metadata.get("generated_tokens", 0), - "prompt_tokens": prompt_token_count, - "total_tokens": prompt_token_count + chunks[-1].metadata.get("generated_tokens", 0), - }, - } - return {"replies": ["".join([chunk.content for chunk in chunks])], "metadata": [metadata]} - - def _run_non_streaming( - self, prompt: str, prompt_token_count: int, num_responses: int, generation_kwargs: Dict[str, Any] - ): - responses: List[str] = [] - all_metadata: List[Dict[str, Any]] = [] - for _i in range(num_responses): - tgr: TextGenerationResponse = self.client.text_generation(prompt, details=True, **generation_kwargs) - all_metadata.append( - { - "model": self.client.model, - "index": _i, - "finish_reason": tgr.details.finish_reason.value, - "usage": { - "completion_tokens": len(tgr.details.tokens), - "prompt_tokens": prompt_token_count, - "total_tokens": prompt_token_count + len(tgr.details.tokens), - }, - } - ) - responses.append(tgr.generated_text) - return {"replies": responses, "metadata": all_metadata} diff --git a/haystack/preview/components/generators/openai.py b/haystack/preview/components/generators/openai.py deleted file mode 100644 index 34316637c1..0000000000 --- a/haystack/preview/components/generators/openai.py +++ /dev/null @@ -1,290 +0,0 @@ -import dataclasses -import logging -import os -from typing import Optional, List, Callable, Dict, Any - -import openai -from openai.openai_object import OpenAIObject - -from haystack.preview import component, default_from_dict, default_to_dict -from haystack.preview.components.generators.utils import serialize_callback_handler, deserialize_callback_handler -from haystack.preview.dataclasses import StreamingChunk, ChatMessage - -logger = logging.getLogger(__name__) - - -API_BASE_URL = "https://api.openai.com/v1" - - -@component -class GPTGenerator: - """ - Enables text generation using OpenAI's large language models (LLMs). It supports gpt-4 and gpt-3.5-turbo - family of models. - - Users can pass any text generation parameters valid for the `openai.ChatCompletion.create` method - directly to this component via the `**generation_kwargs` parameter in __init__ or the `**generation_kwargs` - parameter in `run` method. - - For more details on the parameters supported by the OpenAI API, refer to the OpenAI - [documentation](https://platform.openai.com/docs/api-reference/chat). - - ```python - from haystack.preview.components.generators import GPTGenerator - client = GPTGenerator() - response = client.run("What's Natural Language Processing? Be brief.") - print(response) - - >> {'replies': ['Natural Language Processing (NLP) is a branch of artificial intelligence that focuses on - >> the interaction between computers and human language. It involves enabling computers to understand, interpret, - >> and respond to natural human language in a way that is both meaningful and useful.'], 'metadata': [{'model': - >> 'gpt-3.5-turbo-0613', 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 16, - >> 'completion_tokens': 49, 'total_tokens': 65}}]} - ``` - - Key Features and Compatibility: - - **Primary Compatibility**: Designed to work seamlessly with gpt-4, gpt-3.5-turbo family of models. - - **Streaming Support**: Supports streaming responses from the OpenAI API. - - **Customizability**: Supports all parameters supported by the OpenAI API. - - Input and Output Format: - - **String Format**: This component uses the strings for both input and output. - """ - - def __init__( - self, - api_key: Optional[str] = None, - model_name: str = "gpt-3.5-turbo", - streaming_callback: Optional[Callable[[StreamingChunk], None]] = None, - api_base_url: str = API_BASE_URL, - system_prompt: Optional[str] = None, - generation_kwargs: Optional[Dict[str, Any]] = None, - ): - """ - Creates an instance of GPTGenerator. Unless specified otherwise in the `model_name`, this is for OpenAI's - GPT-3.5 model. - - :param api_key: The OpenAI API key. It can be explicitly provided or automatically read from the - environment variable OPENAI_API_KEY (recommended). - :param model_name: The name of the model to use. - :param streaming_callback: A callback function that is called when a new token is received from the stream. - The callback function accepts StreamingChunk as an argument. - :param api_base_url: The OpenAI API Base url, defaults to `https://api.openai.com/v1`. - :param system_prompt: The system prompt to use for text generation. If not provided, the system prompt is - omitted, and the default system prompt of the model is used. - :param generation_kwargs: Other parameters to use for the model. These parameters are all sent directly to - the OpenAI endpoint. See OpenAI [documentation](https://platform.openai.com/docs/api-reference/chat) for - more details. - Some of the supported parameters: - - `max_tokens`: The maximum number of tokens the output text can have. - - `temperature`: What sampling temperature to use. Higher values mean the model will take more risks. - Try 0.9 for more creative applications and 0 (argmax sampling) for ones with a well-defined answer. - - `top_p`: An alternative to sampling with temperature, called nucleus sampling, where the model - considers the results of the tokens with top_p probability mass. So, 0.1 means only the tokens - comprising the top 10% probability mass are considered. - - `n`: How many completions to generate for each prompt. For example, if the LLM gets 3 prompts and n is 2, - it will generate two completions for each of the three prompts, ending up with 6 completions in total. - - `stop`: One or more sequences after which the LLM should stop generating tokens. - - `presence_penalty`: What penalty to apply if a token is already present at all. Bigger values mean - the model will be less likely to repeat the same token in the text. - - `frequency_penalty`: What penalty to apply if a token has already been generated in the text. - Bigger values mean the model will be less likely to repeat the same token in the text. - - `logit_bias`: Add a logit bias to specific tokens. The keys of the dictionary are tokens, and the - values are the bias to add to that token. - """ - # if the user does not provide the API key, check if it is set in the module client - api_key = api_key or openai.api_key - if api_key is None: - try: - api_key = os.environ["OPENAI_API_KEY"] - except KeyError as e: - raise ValueError( - "GPTGenerator expects an OpenAI API key. " - "Set the OPENAI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - openai.api_key = api_key - - self.model_name = model_name - self.generation_kwargs = generation_kwargs or {} - self.system_prompt = system_prompt - self.streaming_callback = streaming_callback - - self.api_base_url = api_base_url - openai.api_base = api_base_url - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - :return: The serialized component as a dictionary. - """ - callback_name = serialize_callback_handler(self.streaming_callback) if self.streaming_callback else None - return default_to_dict( - self, - model_name=self.model_name, - streaming_callback=callback_name, - api_base_url=self.api_base_url, - generation_kwargs=self.generation_kwargs, - system_prompt=self.system_prompt, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "GPTGenerator": - """ - Deserialize this component from a dictionary. - :param data: The dictionary representation of this component. - :return: The deserialized component instance. - """ - init_params = data.get("init_parameters", {}) - serialized_callback_handler = init_params.get("streaming_callback") - if serialized_callback_handler: - data["init_parameters"]["streaming_callback"] = deserialize_callback_handler(serialized_callback_handler) - return default_from_dict(cls, data) - - @component.output_types(replies=List[str], metadata=List[Dict[str, Any]]) - def run(self, prompt: str, generation_kwargs: Optional[Dict[str, Any]] = None): - """ - Invoke the text generation inference based on the provided messages and generation parameters. - - :param prompt: The string prompt to use for text generation. - :param generation_kwargs: Additional keyword arguments for text generation. These parameters will - potentially override the parameters passed in the __init__ method. - For more details on the parameters supported by the OpenAI API, refer to the - OpenAI [documentation](https://platform.openai.com/docs/api-reference/chat/create). - :return: A list of strings containing the generated responses and a list of dictionaries containing the metadata - for each response. - """ - message = ChatMessage.from_user(prompt) - if self.system_prompt: - messages = [ChatMessage.from_system(self.system_prompt), message] - else: - messages = [message] - - # update generation kwargs by merging with the generation kwargs passed to the run method - generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})} - - # adapt ChatMessage(s) to the format expected by the OpenAI API - openai_formatted_messages = self._convert_to_openai_format(messages) - - completion = openai.ChatCompletion.create( - model=self.model_name, - messages=openai_formatted_messages, - stream=self.streaming_callback is not None, - **generation_kwargs, - ) - - completions: List[ChatMessage] - if self.streaming_callback: - num_responses = generation_kwargs.pop("n", 1) - if num_responses > 1: - raise ValueError("Cannot stream multiple responses, please set n=1.") - chunks: List[StreamingChunk] = [] - chunk = None - for chunk in completion: - if chunk.choices: - chunk_delta: StreamingChunk = self._build_chunk(chunk, chunk.choices[0]) - chunks.append(chunk_delta) - self.streaming_callback(chunk_delta) # invoke callback with the chunk_delta - completions = [self._connect_chunks(chunk, chunks)] - else: - completions = [self._build_message(completion, choice) for choice in completion.choices] - - # before returning, do post-processing of the completions - for completion in completions: - self._check_finish_reason(completion) - - return { - "replies": [message.content for message in completions], - "metadata": [message.metadata for message in completions], - } - - def _convert_to_openai_format(self, messages: List[ChatMessage]) -> List[Dict[str, Any]]: - """ - Converts the list of ChatMessage to the list of messages in the format expected by the OpenAI API. - :param messages: The list of ChatMessage. - :return: The list of messages in the format expected by the OpenAI API. - """ - openai_chat_message_format = {"role", "content", "name"} - openai_formatted_messages = [] - for m in messages: - message_dict = dataclasses.asdict(m) - filtered_message = {k: v for k, v in message_dict.items() if k in openai_chat_message_format and v} - openai_formatted_messages.append(filtered_message) - return openai_formatted_messages - - def _connect_chunks(self, chunk: OpenAIObject, chunks: List[StreamingChunk]) -> ChatMessage: - """ - Connects the streaming chunks into a single ChatMessage. - """ - complete_response = ChatMessage.from_assistant("".join([chunk.content for chunk in chunks])) - complete_response.metadata.update( - { - "model": chunk.model, - "index": 0, - "finish_reason": chunk.choices[0].finish_reason, - "usage": {}, # we don't have usage data for streaming responses - } - ) - return complete_response - - def _build_message(self, completion: OpenAIObject, choice: OpenAIObject) -> ChatMessage: - """ - Converts the response from the OpenAI API to a ChatMessage. - :param completion: The completion returned by the OpenAI API. - :param choice: The choice returned by the OpenAI API. - :return: The ChatMessage. - """ - message: OpenAIObject = choice.message - content = dict(message.function_call) if choice.finish_reason == "function_call" else message.content - chat_message = ChatMessage.from_assistant(content) - chat_message.metadata.update( - { - "model": completion.model, - "index": choice.index, - "finish_reason": choice.finish_reason, - "usage": dict(completion.usage.items()), - } - ) - return chat_message - - def _build_chunk(self, chunk: OpenAIObject, choice: OpenAIObject) -> StreamingChunk: - """ - Converts the response from the OpenAI API to a StreamingChunk. - :param chunk: The chunk returned by the OpenAI API. - :param choice: The choice returned by the OpenAI API. - :return: The StreamingChunk. - """ - has_content = bool(hasattr(choice.delta, "content") and choice.delta.content) - if has_content: - content = choice.delta.content - elif hasattr(choice.delta, "function_call"): - content = str(choice.delta.function_call) - else: - content = "" - chunk_message = StreamingChunk(content) - chunk_message.metadata.update( - {"model": chunk.model, "index": choice.index, "finish_reason": choice.finish_reason} - ) - return chunk_message - - def _check_finish_reason(self, message: ChatMessage) -> None: - """ - Check the `finish_reason` returned with the OpenAI completions. - If the `finish_reason` is `length`, log a warning to the user. - :param message: The message returned by the LLM. - """ - if message.metadata["finish_reason"] == "length": - logger.warning( - "The completion for index %s has been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions.", - message.metadata["index"], - ) - if message.metadata["finish_reason"] == "content_filter": - logger.warning( - "The completion for index %s has been truncated due to the content filter.", message.metadata["index"] - ) diff --git a/haystack/preview/components/generators/utils.py b/haystack/preview/components/generators/utils.py deleted file mode 100644 index 397009e4e0..0000000000 --- a/haystack/preview/components/generators/utils.py +++ /dev/null @@ -1,49 +0,0 @@ -import inspect -import sys -from typing import Optional, Callable - -from haystack.preview import DeserializationError -from haystack.preview.dataclasses import StreamingChunk - - -def default_streaming_callback(chunk: StreamingChunk) -> None: - """ - Default callback function for streaming responses. - Prints the tokens of the first completion to stdout as soon as they are received - """ - print(chunk.content, flush=True, end="") - - -def serialize_callback_handler(streaming_callback: Callable[[StreamingChunk], None]) -> str: - """ - Serializes the streaming callback handler. - :param streaming_callback: The streaming callback handler function - :return: The full path of the streaming callback handler function - """ - module = inspect.getmodule(streaming_callback) - - # Get the full package path of the function - if module is not None: - full_path = f"{module.__name__}.{streaming_callback.__name__}" - else: - full_path = streaming_callback.__name__ - return full_path - - -def deserialize_callback_handler(callback_name: str) -> Optional[Callable[[StreamingChunk], None]]: - """ - Deserializes the streaming callback handler. - :param callback_name: The full path of the streaming callback handler function - :return: The streaming callback handler function - :raises DeserializationError: If the streaming callback handler function cannot be found - """ - parts = callback_name.split(".") - module_name = ".".join(parts[:-1]) - function_name = parts[-1] - module = sys.modules.get(module_name, None) - if not module: - raise DeserializationError(f"Could not locate the module of the streaming callback: {module_name}") - streaming_callback = getattr(module, function_name, None) - if not streaming_callback: - raise DeserializationError(f"Could not locate the streaming callback: {function_name}") - return streaming_callback diff --git a/haystack/preview/components/preprocessors/__init__.py b/haystack/preview/components/preprocessors/__init__.py deleted file mode 100644 index 2045621e22..0000000000 --- a/haystack/preview/components/preprocessors/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.preprocessors.document_cleaner import DocumentCleaner -from haystack.preview.components.preprocessors.document_splitter import DocumentSplitter - -__all__ = ["DocumentSplitter", "DocumentCleaner"] diff --git a/haystack/preview/components/preprocessors/document_cleaner.py b/haystack/preview/components/preprocessors/document_cleaner.py deleted file mode 100644 index 370173baca..0000000000 --- a/haystack/preview/components/preprocessors/document_cleaner.py +++ /dev/null @@ -1,229 +0,0 @@ -import logging -import re -from copy import deepcopy -from functools import partial, reduce -from itertools import chain -from typing import Generator, List, Optional, Set - -from haystack.preview import Document, component - -logger = logging.getLogger(__name__) - - -@component -class DocumentCleaner: - """ - Makes text documents more readable by removing extra whitespaces, empty lines, specified substrings, regexes, page headers and footers (in this order). - This is useful for preparing the documents for further processing by LLMs. - - Example usage in an indexing pipeline: - - ```python - document_store = InMemoryDocumentStore() - p = Pipeline() - p.add_component(instance=TextFileToDocument(), name="text_file_converter") - p.add_component(instance=DocumentCleaner(), name="cleaner") - p.add_component(instance=TextDocumentSplitter(split_by="sentence", split_length=1), name="splitter") - p.add_component(instance=DocumentWriter(document_store=document_store), name="writer") - p.connect("text_file_converter.documents", "cleaner.documents") - p.connect("cleaner.documents", "splitter.documents") - p.connect("splitter.documents", "writer.documents") - ``` - """ - - def __init__( - self, - remove_empty_lines: bool = True, - remove_extra_whitespaces: bool = True, - remove_repeated_substrings: bool = False, - remove_substrings: Optional[List[str]] = None, - remove_regex: Optional[str] = None, - ): - """ - :param remove_empty_lines: Whether to remove empty lines. - :param remove_extra_whitespaces: Whether to remove extra whitespaces. - :param remove_repeated_substrings: Whether to remove repeated substrings (headers/footers) from pages. - Pages in the text need to be separated by form feed character "\f", - which is supported by TextFileToDocument and AzureOCRDocumentConverter. - :param remove_substrings: List of substrings to remove from the text. - :param remove_regex: Regex to match and replace substrings by "". - """ - - self.remove_empty_lines = remove_empty_lines - self.remove_extra_whitespaces = remove_extra_whitespaces - self.remove_repeated_substrings = remove_repeated_substrings - self.remove_substrings = remove_substrings - self.remove_regex = remove_regex - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document]): - """ - Run the DocumentCleaner on the given list of documents. - The documents' metadata remain unchanged. - """ - if not isinstance(documents, list) or documents and not isinstance(documents[0], Document): - raise TypeError("DocumentCleaner expects a List of Documents as input.") - - cleaned_docs = [] - for doc in documents: - if doc.content is None: - logger.warning( - "DocumentCleaner only cleans text documents but document.content for document ID %s is None.", - doc.id, - ) - cleaned_docs.append(doc) - continue - text = doc.content - - if self.remove_extra_whitespaces: - text = self._remove_extra_whitespaces(text) - if self.remove_empty_lines: - text = self._remove_empty_lines(text) - if self.remove_substrings: - text = self._remove_substrings(text, self.remove_substrings) - if self.remove_regex: - text = self._remove_regex(text, self.remove_regex) - if self.remove_repeated_substrings: - text = self._remove_repeated_substrings(text) - - cleaned_docs.append(Document(content=text, meta=deepcopy(doc.meta))) - - return {"documents": cleaned_docs} - - def _remove_empty_lines(self, text: str) -> str: - """ - Remove empty lines and lines that contain nothing but whitespaces from text. - :param text: Text to clean. - :param return: The text without empty lines. - """ - lines = text.split("\n") - non_empty_lines = filter(lambda line: line.strip() != "", lines) - return "\n".join(non_empty_lines) - - def _remove_extra_whitespaces(self, text: str) -> str: - """ - Remove extra whitespaces from text. - :param text: Text to clean. - :param return: The text without extra whitespaces. - """ - return re.sub(r"\s\s+", " ", text).strip() - - def _remove_regex(self, text: str, regex: str) -> str: - """ - Remove substrings that match the specified regex from the text. - :param text: Text to clean. - :param regex: Regex to match and replace substrings by "". - :param return: The text without any substrings that match the regex. - """ - return re.sub(regex, "", text).strip() - - def _remove_substrings(self, text: str, substrings: List[str]) -> str: - """ - Remove all specified substrings from the text. - :param text: Text to clean. - :param substrings: Substrings to remove. - :return: The text without the specified substrings. - """ - for substring in substrings: - text = text.replace(substring, "") - return text - - def _remove_repeated_substrings(self, text: str) -> str: - """ - Remove any substrings from the text that occur repeatedly on every page. For example headers or footers. - Pages in the text need to be separated by form feed character "\f". - :param text: Text to clean. - :return: The text without the repeated substrings. - """ - return self._find_and_remove_header_footer( - text, n_chars=300, n_first_pages_to_ignore=1, n_last_pages_to_ignore=1 - ) - - def _find_and_remove_header_footer( - self, text: str, n_chars: int, n_first_pages_to_ignore: int, n_last_pages_to_ignore: int - ) -> str: - """ - Heuristic to find footers and headers across different pages by searching for the longest common string. - Pages in the text need to be separated by form feed character "\f". - For headers, we only search in the first n_chars characters (for footer: last n_chars). - Note: This heuristic uses exact matches and therefore works well for footers like "Copyright 2019 by XXX", - but won't detect "Page 3 of 4" or similar. - - :param n_chars: The number of first/last characters where the header/footer shall be searched in. - :param n_first_pages_to_ignore: The number of first pages to ignore (e.g. TOCs often don't contain footer/header). - :param n_last_pages_to_ignore: The number of last pages to ignore. - :return: The text without the found headers and footers. - """ - - pages = text.split("\f") - - # header - start_of_pages = [p[:n_chars] for p in pages[n_first_pages_to_ignore:-n_last_pages_to_ignore]] - found_header = self._find_longest_common_ngram(start_of_pages) - if found_header: - pages = [page.replace(found_header, "") for page in pages] - - # footer - end_of_pages = [p[-n_chars:] for p in pages[n_first_pages_to_ignore:-n_last_pages_to_ignore]] - found_footer = self._find_longest_common_ngram(end_of_pages) - if found_footer: - pages = [page.replace(found_footer, "") for page in pages] - - logger.debug("Removed header '%s' and footer '%s' in document", found_header, found_footer) - text = "\f".join(pages) - return text - - def _ngram(self, seq: str, n: int) -> Generator[str, None, None]: - """ - Return all ngrams of length n from a text sequence. Each ngram consists of n words split by whitespace. - :param seq: The sequence to generate ngrams from. - :param n: The length of the ngrams to generate. - :return: A Generator generating all ngrams of length n from the given sequence. - """ - - # In order to maintain the original whitespace, but still consider \n and \t for n-gram tokenization, - # we add a space here and remove it after creation of the ngrams again (see below) - seq = seq.replace("\n", " \n") - seq = seq.replace("\t", " \t") - - words = seq.split(" ") - ngrams = ( - " ".join(words[i : i + n]).replace(" \n", "\n").replace(" \t", "\t") for i in range(0, len(words) - n + 1) - ) - - return ngrams - - def _allngram(self, seq: str, min_ngram: int, max_ngram: int) -> Set[str]: - """ - Generates all possible ngrams from a given sequence of text. - Considering all ngram lengths between the minimum and maximum length. - - :param seq: The sequence to generate ngrams from. - :param min_ngram: The minimum length of ngram to consider. - :param max_ngram: The maximum length of ngram to consider. - :return: A set of all ngrams from the given sequence. - """ - lengths = range(min_ngram, max_ngram) if max_ngram else range(min_ngram, len(seq)) - ngrams = map(partial(self._ngram, seq), lengths) - res = set(chain.from_iterable(ngrams)) - return res - - def _find_longest_common_ngram(self, sequences: List[str], min_ngram: int = 3, max_ngram: int = 30) -> str: - """ - Find the longest common ngram across a list of text sequences (e.g. start of pages). - Considering all ngram lengths between the minimum and maximum length. Helpful for finding footers, headers etc. - Empty sequences are ignored. - - :param sequences: The list of strings that shall be searched for common n_grams. - :param max_ngram: The maximum length of ngram to consider. - :param min_ngram: The minimum length of ngram to consider. - :return: The longest ngram that all sequences have in common. - """ - sequences = [s for s in sequences if s] # filter empty sequences - if not sequences: - return "" - seqs_ngrams = map(partial(self._allngram, min_ngram=min_ngram, max_ngram=max_ngram), sequences) - intersection = reduce(set.intersection, seqs_ngrams) - - longest = max(intersection, key=len, default="") - return longest if longest.strip() else "" diff --git a/haystack/preview/components/preprocessors/document_splitter.py b/haystack/preview/components/preprocessors/document_splitter.py deleted file mode 100644 index ecb8a3f11f..0000000000 --- a/haystack/preview/components/preprocessors/document_splitter.py +++ /dev/null @@ -1,91 +0,0 @@ -from copy import deepcopy -from typing import List, Literal - -from more_itertools import windowed - -from haystack.preview import component, Document - - -@component -class DocumentSplitter: - """ - Splits a list of text documents into a list of text documents with shorter texts. - This is useful for splitting documents with long texts that otherwise would not fit into the maximum text length of language models. - """ - - def __init__( - self, split_by: Literal["word", "sentence", "passage"] = "word", split_length: int = 200, split_overlap: int = 0 - ): - """ - :param split_by: The unit by which the document should be split. Choose from "word" for splitting by " ", - "sentence" for splitting by ".", or "passage" for splitting by "\n\n". - :param split_length: The maximum number of units in each split. - :param split_overlap: The number of units that each split should overlap. - """ - - self.split_by = split_by - if split_by not in ["word", "sentence", "passage"]: - raise ValueError("split_by must be one of 'word', 'sentence' or 'passage'.") - if split_length <= 0: - raise ValueError("split_length must be greater than 0.") - self.split_length = split_length - if split_overlap < 0: - raise ValueError("split_overlap must be greater than or equal to 0.") - self.split_overlap = split_overlap - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document]): - """ - Splits the documents by split_by after split_length units with an overlap of split_overlap units. - Returns a list of documents with the split texts. - A metadata field "source_id" is added to each document to keep track of the original document that was split. - Other metadata are copied from the original document. - :param documents: The documents to split. - :return: A list of documents with the split texts. - """ - - if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)): - raise TypeError("DocumentSplitter expects a List of Documents as input.") - - split_docs = [] - for doc in documents: - if doc.content is None: - raise ValueError( - f"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None." - ) - units = self._split_into_units(doc.content, self.split_by) - text_splits = self._concatenate_units(units, self.split_length, self.split_overlap) - metadata = deepcopy(doc.meta) - metadata["source_id"] = doc.id - split_docs += [Document(content=txt, meta=metadata) for txt in text_splits] - return {"documents": split_docs} - - def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage"]) -> List[str]: - if split_by == "passage": - split_at = "\n\n" - elif split_by == "sentence": - split_at = "." - elif split_by == "word": - split_at = " " - else: - raise NotImplementedError( - "DocumentSplitter only supports 'passage', 'sentence' or 'word' split_by options." - ) - units = text.split(split_at) - # Add the delimiter back to all units except the last one - for i in range(len(units) - 1): - units[i] += split_at - return units - - def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]: - """ - Concatenates the elements into parts of split_length units. - """ - text_splits = [] - segments = windowed(elements, n=split_length, step=split_length - split_overlap) - for seg in segments: - current_units = [unit for unit in seg if unit is not None] - txt = "".join(current_units) - if len(txt) > 0: - text_splits.append(txt) - return text_splits diff --git a/haystack/preview/components/rankers/__init__.py b/haystack/preview/components/rankers/__init__.py deleted file mode 100644 index f188f01225..0000000000 --- a/haystack/preview/components/rankers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.rankers.meta_field import MetaFieldRanker -from haystack.preview.components.rankers.transformers_similarity import TransformersSimilarityRanker - -__all__ = ["MetaFieldRanker", "TransformersSimilarityRanker"] diff --git a/haystack/preview/components/rankers/meta_field.py b/haystack/preview/components/rankers/meta_field.py deleted file mode 100644 index 7c7e4a73cc..0000000000 --- a/haystack/preview/components/rankers/meta_field.py +++ /dev/null @@ -1,180 +0,0 @@ -import logging -import warnings -from collections import defaultdict -from typing import List, Dict, Any, Optional, Literal - -from haystack.preview import ComponentError, Document, component, default_to_dict - -logger = logging.getLogger(__name__) - - -@component -class MetaFieldRanker: - """ - Ranks Documents based on the value of their specific metadata field. The ranking is done in a descending order. - - Usage example: - ``` - from haystack.preview import Document - from haystack.preview.components.rankers import MetaFieldRanker - - ranker = MetaFieldRanker(metadata_field="rating") - docs = [ - Document(text="Paris", metadata={"rating": 1.3}), - Document(text="Berlin", metadata={"rating": 0.7}), - Document(text="Barcelona", metadata={"rating": 2.1}), - ] - - output = ranker.run(documents=docs) - docs = output["documents"] - assert docs[0].text == "Barcelona" - """ - - def __init__( - self, - metadata_field: str, - weight: float = 1.0, - top_k: Optional[int] = None, - ranking_mode: Literal["reciprocal_rank_fusion", "linear_score"] = "reciprocal_rank_fusion", - ): - """ - Creates an instance of MetaFieldRanker. - - :param metadata_field: The name of the metadata field to rank by. - :param weight: In range [0,1]. - 0 disables ranking by a metadata field. - 0.5 content and metadata fields have the same impact for the ranking. - 1 means ranking by a metadata field only. The highest value comes first. - :param top_k: The maximum number of Documents you want the Ranker to return per query. - :param ranking_mode: The mode used to combine the Retriever's and Ranker's scores. - Possible values are 'reciprocal_rank_fusion' (default) and 'linear_score'. - Use the 'score' mode only with Retrievers or Rankers that return a score in range [0,1]. - """ - - self.metadata_field = metadata_field - self.weight = weight - self.top_k = top_k - self.ranking_mode = ranking_mode - - if self.weight < 0 or self.weight > 1: - raise ValueError( - """ - Parameter must be in range [0,1] but is currently set to '{}'.\n - '0' disables sorting by a metadata field, '0.5' assigns equal weight to the previous relevance scores and the metadata field, and '1' ranks by the metadata field only.\n - Change the parameter to a value in range 0 to 1 when initializing the MetaFieldRanker. - """.format( - self.weight - ) - ) - - if self.ranking_mode not in ["reciprocal_rank_fusion", "linear_score"]: - raise ValueError( - """ - The value of parameter must be 'reciprocal_rank_fusion' or 'linear_score', but is currently set to '{}'. \n - Change the value to 'reciprocal_rank_fusion' or 'linear_score' when initializing the MetaFieldRanker. - """.format( - self.ranking_mode - ) - ) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize object to a dictionary. - """ - return default_to_dict( - self, - metadata_field=self.metadata_field, - weight=self.weight, - top_k=self.top_k, - ranking_mode=self.ranking_mode, - ) - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document], top_k: Optional[int] = None): - """ - Use this method to rank a list of Documents based on the selected metadata field by: - 1. Sorting the Documents by the metadata field in descending order. - 2. Merging the scores from the metadata field with the scores from the previous component according to the strategy and weight provided. - 3. Returning the top-k documents. - - :param documents: Documents to be ranked. - :param top_k: (optional) The number of Documents you want the Ranker to return. If not provided, the Ranker returns all Documents it received. - """ - if not documents: - return {"documents": []} - - if top_k is None: - top_k = self.top_k - elif top_k <= 0: - raise ValueError(f"top_k must be > 0, but got {top_k}") - - try: - sorted_by_metadata = sorted(documents, key=lambda doc: doc.meta[self.metadata_field], reverse=True) - except KeyError: - raise ComponentError( - """ - The parameter is currently set to '{}' but the Documents {} don't have this metadata key.\n - Double-check the names of the metadata fields in your documents \n - and set to the name of the field that contains the metadata you want to use for ranking. - """.format( - self.metadata_field, ",".join([doc.id for doc in documents if self.metadata_field not in doc.meta]) - ) - ) - - if self.weight > 0: - sorted_documents = self._merge_scores(documents, sorted_by_metadata) - return {"documents": sorted_documents[:top_k]} - else: - return {"documents": sorted_by_metadata[:top_k]} - - def _merge_scores(self, documents: List[Document], sorted_documents: List[Document]) -> List[Document]: - """ - Merge scores for Documents sorted both by their content and by their metadata field. - """ - scores_map: Dict = defaultdict(int) - - if self.ranking_mode == "reciprocal_rank_fusion": - for i, (doc, sorted_doc) in enumerate(zip(documents, sorted_documents)): - scores_map[doc.id] += self._calculate_rrf(rank=i) * (1 - self.weight) - scores_map[sorted_doc.id] += self._calculate_rrf(rank=i) * self.weight - elif self.ranking_mode == "linear_score": - for i, (doc, sorted_doc) in enumerate(zip(documents, sorted_documents)): - score = float(0) - if doc.score is None: - warnings.warn("The score wasn't provided; defaulting to 0.") - elif doc.score < 0 or doc.score > 1: - warnings.warn( - "The score {} for Document {} is outside the [0,1] range; defaulting to 0".format( - doc.score, doc.id - ) - ) - else: - score = doc.score - - scores_map[doc.id] += score * (1 - self.weight) - scores_map[sorted_doc.id] += self._calc_linear_score(rank=i, amount=len(sorted_documents)) * self.weight - - for doc in documents: - doc.score = scores_map[doc.id] - - new_sorted_documents = sorted(documents, key=lambda doc: doc.score if doc.score else -1, reverse=True) - return new_sorted_documents - - @staticmethod - def _calculate_rrf(rank: int, k: int = 61) -> float: - """ - Calculates the reciprocal rank fusion. The constant K is set to 61 (60 was suggested by the original paper, - plus 1 as python lists are 0-based and the [paper](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) used 1-based ranking). - """ - return 1 / (k + rank) - - @staticmethod - def _calc_linear_score(rank: int, amount: int) -> float: - """ - Calculate the metadata field score as a linear score between the greatest and the lowest score in the list. - This linear scaling is useful for: - - Reducing the effect of outliers - - Creating scores that are meaningfully distributed in the range [0,1], - similar to scores coming from a Retriever or Ranker. - """ - return (amount - rank) / amount diff --git a/haystack/preview/components/rankers/transformers_similarity.py b/haystack/preview/components/rankers/transformers_similarity.py deleted file mode 100644 index 0c4176a7a1..0000000000 --- a/haystack/preview/components/rankers/transformers_similarity.py +++ /dev/null @@ -1,134 +0,0 @@ -import logging -from pathlib import Path -from typing import List, Union, Dict, Any, Optional - -from haystack.preview import ComponentError, Document, component, default_to_dict -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - - -with LazyImport(message="Run 'pip install transformers[torch,sentencepiece]'") as torch_and_transformers_import: - import torch - from transformers import AutoModelForSequenceClassification, AutoTokenizer - - -@component -class TransformersSimilarityRanker: - """ - Ranks Documents based on their similarity to the query. - It uses a pre-trained cross-encoder model (from the Hugging Face Hub) to embed the query and the Documents. - - Usage example: - ``` - from haystack.preview import Document - from haystack.preview.components.rankers import TransformersSimilarityRanker - - ranker = TransformersSimilarityRanker() - docs = [Document(content="Paris"), Document(content="Berlin")] - query = "City in Germany" - output = ranker.run(query=query, documents=docs) - docs = output["documents"] - assert len(docs) == 2 - assert docs[0].content == "Berlin" - ``` - """ - - def __init__( - self, - model_name_or_path: Union[str, Path] = "cross-encoder/ms-marco-MiniLM-L-6-v2", - device: str = "cpu", - token: Union[bool, str, None] = None, - top_k: int = 10, - ): - """ - Creates an instance of TransformersSimilarityRanker. - - :param model_name_or_path: The name or path of a pre-trained cross-encoder model - from the Hugging Face Hub. - :param device: The torch device (for example, cuda:0, cpu, mps) to which you want to limit model inference. - :param token: The API token used to download private models from Hugging Face. - If this parameter is set to `True`, the token generated when running - `transformers-cli login` (stored in ~/.huggingface) is used. - :param top_k: The maximum number of Documents to return per query. - """ - torch_and_transformers_import.check() - - self.model_name_or_path = model_name_or_path - if top_k <= 0: - raise ValueError(f"top_k must be > 0, but got {top_k}") - self.top_k = top_k - self.device = device - self.token = token - self.model = None - self.tokenizer = None - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": str(self.model_name_or_path)} - - def warm_up(self): - """ - Warm up the model and tokenizer used for scoring the Documents. - """ - if self.model_name_or_path and not self.model: - self.model = AutoModelForSequenceClassification.from_pretrained(self.model_name_or_path, token=self.token) - self.model = self.model.to(self.device) - self.model.eval() - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name_or_path, token=self.token) - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, - device=self.device, - model_name_or_path=self.model_name_or_path, - token=self.token if not isinstance(self.token, str) else None, # don't serialize valid tokens - top_k=self.top_k, - ) - - @component.output_types(documents=List[Document]) - def run(self, query: str, documents: List[Document], top_k: Optional[int] = None): - """ - Returns a list of Documents ranked by their similarity to the given query. - - :param query: Query string. - :param documents: List of Documents. - :param top_k: The maximum number of Documents you want the Ranker to return. - :return: List of Documents sorted by their similarity to the query with the most similar Documents appearing first. - """ - if not documents: - return {"documents": []} - - if top_k is None: - top_k = self.top_k - - elif top_k <= 0: - raise ValueError(f"top_k must be > 0, but got {top_k}") - - # If a model path is provided but the model isn't loaded - if self.model_name_or_path and not self.model: - raise ComponentError( - f"The component {self.__class__.__name__} wasn't warmed up. Run 'warm_up()' before calling 'run()'." - ) - - query_doc_pairs = [[query, doc.content] for doc in documents] - features = self.tokenizer( - query_doc_pairs, padding=True, truncation=True, return_tensors="pt" - ).to( # type: ignore - self.device - ) - with torch.inference_mode(): - similarity_scores = self.model(**features).logits.squeeze() # type: ignore - - _, sorted_indices = torch.sort(similarity_scores, descending=True) - ranked_docs = [] - for sorted_index_tensor in sorted_indices: - i = sorted_index_tensor.item() - documents[i].score = similarity_scores[i].item() - ranked_docs.append(documents[i]) - return {"documents": ranked_docs[:top_k]} diff --git a/haystack/preview/components/readers/__init__.py b/haystack/preview/components/readers/__init__.py deleted file mode 100644 index e48da38979..0000000000 --- a/haystack/preview/components/readers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.readers.extractive import ExtractiveReader - -__all__ = ["ExtractiveReader"] diff --git a/haystack/preview/components/readers/extractive.py b/haystack/preview/components/readers/extractive.py deleted file mode 100644 index 5c763eef94..0000000000 --- a/haystack/preview/components/readers/extractive.py +++ /dev/null @@ -1,421 +0,0 @@ -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union -import math -import warnings -import logging -import os - -from haystack.preview import component, default_to_dict, ComponentError, Document, ExtractedAnswer -from haystack.preview.lazy_imports import LazyImport - -with LazyImport("Run 'pip install transformers[torch,sentencepiece]'") as torch_and_transformers_import: - from transformers import AutoModelForQuestionAnswering, AutoTokenizer - from tokenizers import Encoding - import torch - - -logger = logging.getLogger(__name__) - - -@component -class ExtractiveReader: - """ - A component that locates and extract answers to a given query from Documents. It's used for performing extractive - QA. The Reader assigns a probability score to every possible answer span independently of other answer spans. - This fixes a common issue of other implementations which make comparisons across documents harder by normalizing - each document's answers independently. - - Example usage: - ```python - p = Pipeline() - p.add_component(instance=InMemoryBM25Retriever(document_store=InMemoryDocumentStore()), name="retriever") - p.add_component(instance=ExtractiveReader(), name="reader") - p.connect("retriever", "reader") - question = "Who lives in Berlin?" - p.run({"retriever": {"query": question}, "reader": {"query": question}}) - ``` - """ - - def __init__( - self, - model_name_or_path: Union[Path, str] = "deepset/roberta-base-squad2-distilled", - device: Optional[str] = None, - token: Union[bool, str, None] = None, - top_k: int = 20, - confidence_threshold: Optional[float] = None, - max_seq_length: int = 384, - stride: int = 128, - max_batch_size: Optional[int] = None, - answers_per_seq: Optional[int] = None, - no_answer: bool = True, - calibration_factor: float = 0.1, - model_kwargs: Optional[Dict[str, Any]] = None, - ) -> None: - """ - Creates an ExtractiveReader - :param model_name_or_path: A Hugging Face transformers question answering model. - Can either be a path to a folder containing the model files or an identifier for the Hugging Face hub. - Default: `'deepset/roberta-base-squad2-distilled'` - :param device: Pytorch device string. Uses GPU by default, if available. - :param token: The API token used to download private models from Hugging Face. - If this parameter is set to `True`, then the token generated when running - `transformers-cli login` (stored in ~/.huggingface) is used. - :param top_k: Number of answers to return per query. - It is required even if confidence_threshold is set. Defaults to 20. - An additional answer with no text is returned if no_answer is set to True (default). - :param confidence_threshold: Returns only answers with the probability score above this threshold. - :param max_seq_length: Maximum number of tokens. - If a sequence exceeds it, the sequence is split. - Default: 384 - :param stride: Number of tokens that overlap when sequence is split because it exceeds max_seq_length. - Default: 128 - :param max_batch_size: Maximum number of samples that are fed through the model at the same time. - :param answers_per_seq: Number of answer candidates to consider per sequence. - This is relevant when a Document was split into multiple sequences because of max_seq_length. - :param no_answer: Whether to return no answer scores. - :param calibration_factor: Factor used for calibrating probability scores. - :param model_kwargs: Additional keyword arguments passed to `AutoModelForQuestionAnswering.from_pretrained` - when loading the model specified in `model_name_or_path`. For details on what kwargs you can pass, - see the model's documentation. - """ - torch_and_transformers_import.check() - self.model_name_or_path = str(model_name_or_path) - self.model = None - self.device = device - self.token = token - self.max_seq_length = max_seq_length - self.top_k = top_k - self.confidence_threshold = confidence_threshold - self.stride = stride - self.max_batch_size = max_batch_size - self.answers_per_seq = answers_per_seq - self.no_answer = no_answer - self.calibration_factor = calibration_factor - self.model_kwargs = model_kwargs or {} - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"model": self.model_name_or_path} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, - model_name_or_path=self.model_name_or_path, - device=self.device, - token=self.token if not isinstance(self.token, str) else None, - max_seq_length=self.max_seq_length, - top_k=self.top_k, - confidence_threshold=self.confidence_threshold, - stride=self.stride, - max_batch_size=self.max_batch_size, - answers_per_seq=self.answers_per_seq, - no_answer=self.no_answer, - calibration_factor=self.calibration_factor, - model_kwargs=self.model_kwargs, - ) - - def warm_up(self): - """ - Loads model and tokenizer - """ - if self.model is None: - if torch.cuda.is_available(): - self.device = self.device or "cuda:0" - elif ( - hasattr(torch.backends, "mps") - and torch.backends.mps.is_available() - and os.getenv("HAYSTACK_MPS_ENABLED", "true") != "false" - ): - self.device = self.device or "mps:0" - else: - self.device = self.device or "cpu:0" - - self.model = AutoModelForQuestionAnswering.from_pretrained( - self.model_name_or_path, token=self.token, **self.model_kwargs - ).to(self.device) - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name_or_path, token=self.token) - - def _flatten_documents( - self, queries: List[str], documents: List[List[Document]] - ) -> Tuple[List[str], List[Document], List[int]]: - """ - Flattens queries and Documents so all query-document pairs are arranged along one batch axis. - """ - flattened_queries = [query for documents_, query in zip(documents, queries) for _ in documents_] - flattened_documents = [document for documents_ in documents for document in documents_] - query_ids = [i for i, documents_ in enumerate(documents) for _ in documents_] - return flattened_queries, flattened_documents, query_ids - - def _preprocess( - self, queries: List[str], documents: List[Document], max_seq_length: int, query_ids: List[int], stride: int - ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, List[Encoding], List[int], List[int]]: - """ - Split and tokenize Documents and preserve structures by returning mappings to query and Document IDs. - """ - texts = [] - document_ids = [] - for i, doc in enumerate(documents): - if doc.content is None: - warnings.warn( - f"Document with id {doc.id} was passed to ExtractiveReader. The Document doesn't " - f"contain any text and it will be ignored." - ) - continue - texts.append(doc.content) - document_ids.append(i) - encodings_pt = self.tokenizer( - queries, - [document.content for document in documents], - padding=True, - truncation=True, - max_length=max_seq_length, - return_tensors="pt", - return_overflowing_tokens=True, - stride=stride, - ) - - input_ids = encodings_pt.input_ids.to(self.device) - attention_mask = encodings_pt.attention_mask.to(self.device) - - query_ids = [query_ids[index] for index in encodings_pt.overflow_to_sample_mapping] - document_ids = [document_ids[sample_id] for sample_id in encodings_pt.overflow_to_sample_mapping] - - encodings = encodings_pt.encodings - sequence_ids = torch.tensor( - [[id_ if id_ is not None else -1 for id_ in encoding.sequence_ids] for encoding in encodings] - ).to(self.device) - - return input_ids, attention_mask, sequence_ids, encodings, query_ids, document_ids - - def _postprocess( - self, - start: torch.Tensor, - end: torch.Tensor, - sequence_ids: torch.Tensor, - attention_mask: torch.Tensor, - answers_per_seq: int, - encodings: List[Encoding], - ) -> Tuple[List[List[int]], List[List[int]], torch.Tensor]: - """ - Turn start and end logits into probability scores for each answer span. Unlike most other - implementations, it doesn't normalize the scores to make them easier to compare across different - splits. Returns the top k answer spans. - """ - mask = sequence_ids == 1 - mask = torch.logical_and(mask, attention_mask == 1) - start = torch.where(mask, start, -torch.inf) - end = torch.where(mask, end, -torch.inf) - start = start.unsqueeze(-1) - end = end.unsqueeze(-2) - - logits = start + end # shape: (batch_size, seq_length (start), seq_length (end)) - mask = torch.ones(logits.shape[-2:], dtype=torch.bool, device=self.device) - mask = torch.triu(mask) # End shouldn't be before start - masked_logits = torch.where(mask, logits, -torch.inf) - probabilities = torch.sigmoid(masked_logits * self.calibration_factor) - - flat_probabilities = probabilities.flatten(-2, -1) # necessary for topk - candidates = torch.topk(flat_probabilities, answers_per_seq) - seq_length = logits.shape[-1] - start_candidates = candidates.indices // seq_length # Recover indices from flattening - end_candidates = candidates.indices % seq_length - start_candidates = start_candidates.cpu() - end_candidates = end_candidates.cpu() - - start_candidates_tokens_to_chars = [ - [encoding.token_to_chars(start) for start in candidates] - for candidates, encoding in zip(start_candidates, encodings) - ] - if missing_start_tokens := [ - (batch, index) - for batch, token_to_chars in enumerate(start_candidates_tokens_to_chars) - for index, pair in enumerate(token_to_chars) - if pair is None - ]: - logger.warning("Some start tokens could not be found in the context: %s", missing_start_tokens) - start_candidates_char_indices = [ - [token_to_chars[0] if token_to_chars else None for token_to_chars in candidates] - for candidates in start_candidates_tokens_to_chars - ] - - end_candidates_tokens_to_chars = [ - [encoding.token_to_chars(end) for end in candidates] - for candidates, encoding in zip(end_candidates, encodings) - ] - if missing_end_tokens := [ - (batch, index) - for batch, token_to_chars in enumerate(end_candidates_tokens_to_chars) - for index, pair in enumerate(token_to_chars) - if pair is None - ]: - logger.warning("Some end tokens could not be found in the context: %s", missing_end_tokens) - end_candidates_char_indices = [ - [token_to_chars[1] if token_to_chars else None for token_to_chars in candidates] - for candidates in end_candidates_tokens_to_chars - ] - - probabilities = candidates.values.cpu() - - return start_candidates_char_indices, end_candidates_char_indices, probabilities - - def _nest_answers( - self, - start: List[List[int]], - end: List[List[int]], - probabilities: torch.Tensor, - flattened_documents: List[Document], - queries: List[str], - answers_per_seq: int, - top_k: Optional[int], - confidence_threshold: Optional[float], - query_ids: List[int], - document_ids: List[int], - no_answer: bool, - ) -> List[List[ExtractedAnswer]]: - """ - Reconstructs the nested structure that existed before flattening. Also computes a no answer probability. - This probability is different from most other implementations because it does not consider the no answer - logit introduced with SQuAD 2. Instead, it just computes the probability that the answer does not exist - in the top k or top p. - """ - flat_answers_without_queries = [] - for document_id, start_candidates_, end_candidates_, probabilities_ in zip( - document_ids, start, end, probabilities - ): - for start_, end_, probability in zip(start_candidates_, end_candidates_, probabilities_): - doc = flattened_documents[document_id] - # doc.content cannot be None, because those documents are filtered when preprocessing. - # However, mypy doesn't know that. - flat_answers_without_queries.append( - { - "data": doc.content[start_:end_], # type: ignore - "document": doc, - "probability": probability.item(), - "start": start_, - "end": end_, - "metadata": {}, - } - ) - i = 0 - nested_answers = [] - for query_id in range(query_ids[-1] + 1): - current_answers = [] - while i < len(flat_answers_without_queries) and query_ids[i // answers_per_seq] == query_id: - answer = flat_answers_without_queries[i] - answer["query"] = queries[query_id] - current_answers.append(ExtractedAnswer(**answer)) - i += 1 - current_answers = sorted(current_answers, key=lambda answer: answer.probability, reverse=True) - current_answers = current_answers[:top_k] - if no_answer: - no_answer_probability = math.prod(1 - answer.probability for answer in current_answers) - answer_ = ExtractedAnswer( - data=None, query=queries[query_id], metadata={}, document=None, probability=no_answer_probability - ) - current_answers.append(answer_) - current_answers = sorted(current_answers, key=lambda answer: answer.probability, reverse=True) - if confidence_threshold is not None: - current_answers = [answer for answer in current_answers if answer.probability >= confidence_threshold] - nested_answers.append(current_answers) - - return nested_answers - - @component.output_types(answers=List[ExtractedAnswer]) - def run( - self, - query: str, - documents: List[Document], - top_k: Optional[int] = None, - confidence_threshold: Optional[float] = None, - max_seq_length: Optional[int] = None, - stride: Optional[int] = None, - max_batch_size: Optional[int] = None, - answers_per_seq: Optional[int] = None, - no_answer: Optional[bool] = None, - ): - """ - Locates and extracts answers from the given Documents using the given query. - - :param query: Query string. - :param documents: List of Documents in which you want to search for an answer to the query. - :param top_k: The maximum number of answers to return. - An additional answer is returned if no_answer is set to True (default). - :param confidence_threshold: - :return: List of ExtractedAnswers sorted by (desc.) answer score. - :param confidence_threshold: Returns only answers with the probability score above this threshold. - :param max_seq_length: Maximum number of tokens. - If a sequence exceeds it, the sequence is split. - Default: 384 - :param stride: Number of tokens that overlap when sequence is split because it exceeds max_seq_length. - Default: 128 - :param max_batch_size: Maximum number of samples that are fed through the model at the same time. - :param answers_per_seq: Number of answer candidates to consider per sequence. - This is relevant when a Document was split into multiple sequences because of max_seq_length. - :param no_answer: Whether to return no answer scores. - """ - queries = [query] # Temporary solution until we have decided what batching should look like in v2 - nested_documents = [documents] - if self.model is None: - raise ComponentError("The component was not warmed up. Run 'warm_up()' before calling 'run()'.") - - top_k = top_k or self.top_k - confidence_threshold = confidence_threshold or self.confidence_threshold - max_seq_length = max_seq_length or self.max_seq_length - stride = stride or self.stride - max_batch_size = max_batch_size or self.max_batch_size - answers_per_seq = answers_per_seq or self.answers_per_seq or top_k or 20 - no_answer = no_answer if no_answer is not None else self.no_answer - - flattened_queries, flattened_documents, query_ids = self._flatten_documents(queries, nested_documents) - input_ids, attention_mask, sequence_ids, encodings, query_ids, document_ids = self._preprocess( - flattened_queries, flattened_documents, max_seq_length, query_ids, stride - ) - - num_batches = math.ceil(input_ids.shape[0] / max_batch_size) if max_batch_size else 1 - batch_size = max_batch_size or input_ids.shape[0] - - start_logits_list = [] - end_logits_list = [] - - for i in range(num_batches): - start_index = i * batch_size - end_index = start_index + batch_size - cur_input_ids = input_ids[start_index:end_index] - cur_attention_mask = attention_mask[start_index:end_index] - - output = self.model(input_ids=cur_input_ids, attention_mask=cur_attention_mask) - cur_start_logits = output.start_logits - cur_end_logits = output.end_logits - if num_batches != 1: - cur_start_logits = cur_start_logits.cpu() - cur_end_logits = cur_end_logits.cpu() - start_logits_list.append(cur_start_logits) - end_logits_list.append(cur_end_logits) - - start_logits = torch.cat(start_logits_list) - end_logits = torch.cat(end_logits_list) - - start, end, probabilities = self._postprocess( - start_logits, end_logits, sequence_ids, attention_mask, answers_per_seq, encodings - ) - - answers = self._nest_answers( - start, - end, - probabilities, - flattened_documents, - queries, - answers_per_seq, - top_k, - confidence_threshold, - query_ids, - document_ids, - no_answer, - ) - - return {"answers": answers[0]} # same temporary batching fix as above diff --git a/haystack/preview/components/retrievers/__init__.py b/haystack/preview/components/retrievers/__init__.py deleted file mode 100644 index 92e534d58b..0000000000 --- a/haystack/preview/components/retrievers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.retrievers.in_memory_bm25_retriever import InMemoryBM25Retriever -from haystack.preview.components.retrievers.in_memory_embedding_retriever import InMemoryEmbeddingRetriever - -__all__ = ["InMemoryBM25Retriever", "InMemoryEmbeddingRetriever"] diff --git a/haystack/preview/components/retrievers/in_memory_bm25_retriever.py b/haystack/preview/components/retrievers/in_memory_bm25_retriever.py deleted file mode 100644 index a3f7826123..0000000000 --- a/haystack/preview/components/retrievers/in_memory_bm25_retriever.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Dict, List, Any, Optional - -from haystack.preview import component, Document, default_to_dict, default_from_dict, DeserializationError -from haystack.preview.document_stores import InMemoryDocumentStore, document_store - - -@component -class InMemoryBM25Retriever: - """ - Uses the BM25 algorithm to retrieve documents from the InMemoryDocumentStore. - - Needs to be connected to the InMemoryDocumentStore to run. - """ - - def __init__( - self, - document_store: InMemoryDocumentStore, - filters: Optional[Dict[str, Any]] = None, - top_k: int = 10, - scale_score: bool = False, - ): - """ - Create the InMemoryBM25Retriever component. - - :param document_store: An instance of InMemoryDocumentStore. - :param filters: A dictionary with filters to narrow down the search space. Defaults to `None`. - :param top_k: The maximum number of documents to retrieve. Defaults to `10`. - :param scale_score: Scales the BM25 score to a unit interval in the range of 0 to 1, where 1 means extremely relevant. If set to `False`, uses raw similarity scores. - Defaults to `False`. - - :raises ValueError: If the specified `top_k` is not > 0. - """ - if not isinstance(document_store, InMemoryDocumentStore): - raise ValueError("document_store must be an instance of InMemoryDocumentStore") - - self.document_store = document_store - - if top_k <= 0: - raise ValueError(f"top_k must be greater than 0. Currently, the top_k is {top_k}") - - self.filters = filters - self.top_k = top_k - self.scale_score = scale_score - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"document_store": type(self.document_store).__name__} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - docstore = self.document_store.to_dict() - return default_to_dict( - self, document_store=docstore, filters=self.filters, top_k=self.top_k, scale_score=self.scale_score - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "InMemoryBM25Retriever": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - if "document_store" not in init_params: - raise DeserializationError("Missing 'document_store' in serialization data") - if "type" not in init_params["document_store"]: - raise DeserializationError("Missing 'type' in document store's serialization data") - if init_params["document_store"]["type"] not in document_store.registry: - raise DeserializationError(f"DocumentStore type '{init_params['document_store']['type']}' not found") - - docstore_class = document_store.registry[init_params["document_store"]["type"]] - docstore = docstore_class.from_dict(init_params["document_store"]) - data["init_parameters"]["document_store"] = docstore - return default_from_dict(cls, data) - - @component.output_types(documents=List[Document]) - def run( - self, - query: str, - filters: Optional[Dict[str, Any]] = None, - top_k: Optional[int] = None, - scale_score: Optional[bool] = None, - ): - """ - Run the InMemoryBM25Retriever on the given input data. - - :param query: The query string for the Retriever. - :param filters: A dictionary with filters to narrow down the search space. - :param top_k: The maximum number of documents to return. - :param scale_score: Scales the BM25 score to a unit interval in the range of 0 to 1, where 1 means extremely relevant. If set to `False`, uses raw similarity scores. - If not specified, the value provided at initialization is used. - :return: The retrieved documents. - - :raises ValueError: If the specified DocumentStore is not found or is not a InMemoryDocumentStore instance. - """ - if filters is None: - filters = self.filters - if top_k is None: - top_k = self.top_k - if scale_score is None: - scale_score = self.scale_score - - docs = self.document_store.bm25_retrieval(query=query, filters=filters, top_k=top_k, scale_score=scale_score) - return {"documents": docs} diff --git a/haystack/preview/components/retrievers/in_memory_embedding_retriever.py b/haystack/preview/components/retrievers/in_memory_embedding_retriever.py deleted file mode 100644 index dad86fdc58..0000000000 --- a/haystack/preview/components/retrievers/in_memory_embedding_retriever.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import Dict, List, Any, Optional - -from haystack.preview import component, Document, default_to_dict, default_from_dict, DeserializationError -from haystack.preview.document_stores import InMemoryDocumentStore, document_store - - -@component -class InMemoryEmbeddingRetriever: - """ - Uses a vector similarity metric to retrieve documents from the InMemoryDocumentStore. - - Needs to be connected to the InMemoryDocumentStore to run. - """ - - def __init__( - self, - document_store: InMemoryDocumentStore, - filters: Optional[Dict[str, Any]] = None, - top_k: int = 10, - scale_score: bool = False, - return_embedding: bool = False, - ): - """ - Create the InMemoryEmbeddingRetriever component. - - :param document_store: An instance of InMemoryDocumentStore. - :param filters: A dictionary with filters to narrow down the search space. Defaults to `None`. - :param top_k: The maximum number of documents to retrieve. Defaults to `10`. - :param scale_score: Scales the BM25 score to a unit interval in the range of 0 to 1, where 1 means extremely relevant. If set to `False`, uses raw similarity scores. - Defaults to `False`. - :param return_embedding: Whether to return the embedding of the retrieved Documents. Default is `False`. - - :raises ValueError: If the specified top_k is not > 0. - """ - if not isinstance(document_store, InMemoryDocumentStore): - raise ValueError("document_store must be an instance of InMemoryDocumentStore") - - self.document_store = document_store - - if top_k <= 0: - raise ValueError(f"top_k must be greater than 0. Currently, top_k is {top_k}") - - self.filters = filters - self.top_k = top_k - self.scale_score = scale_score - self.return_embedding = return_embedding - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"document_store": type(self.document_store).__name__} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - docstore = self.document_store.to_dict() - return default_to_dict( - self, - document_store=docstore, - filters=self.filters, - top_k=self.top_k, - scale_score=self.scale_score, - return_embedding=self.return_embedding, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "InMemoryEmbeddingRetriever": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - if "document_store" not in init_params: - raise DeserializationError("Missing 'document_store' in serialization data") - if "type" not in init_params["document_store"]: - raise DeserializationError("Missing 'type' in document store's serialization data") - if init_params["document_store"]["type"] not in document_store.registry: - raise DeserializationError(f"DocumentStore type '{init_params['document_store']['type']}' not found") - - docstore_class = document_store.registry[init_params["document_store"]["type"]] - docstore = docstore_class.from_dict(init_params["document_store"]) - data["init_parameters"]["document_store"] = docstore - return default_from_dict(cls, data) - - @component.output_types(documents=List[Document]) - def run( - self, - query_embedding: List[float], - filters: Optional[Dict[str, Any]] = None, - top_k: Optional[int] = None, - scale_score: Optional[bool] = None, - return_embedding: Optional[bool] = None, - ): - """ - Run the InMemoryEmbeddingRetriever on the given input data. - - :param query_embedding: Embedding of the query. - :param filters: A dictionary with filters to narrow down the search space. - :param top_k: The maximum number of documents to return. - :param scale_score: Scales the similarity score to a unit interval in the range of 0 to 1, where 1 means extremely relevant. If set to `False`, uses raw similarity scores. - If not specified, the value provided at initialization is used. - :param return_embedding: Whether to return the embedding of the retrieved Documents. - :return: The retrieved documents. - - :raises ValueError: If the specified DocumentStore is not found or is not an InMemoryDocumentStore instance. - """ - if filters is None: - filters = self.filters - if top_k is None: - top_k = self.top_k - if scale_score is None: - scale_score = self.scale_score - if return_embedding is None: - return_embedding = self.return_embedding - - docs = self.document_store.embedding_retrieval( - query_embedding=query_embedding, - filters=filters, - top_k=top_k, - scale_score=scale_score, - return_embedding=return_embedding, - ) - - return {"documents": docs} diff --git a/haystack/preview/components/routers/__init__.py b/haystack/preview/components/routers/__init__.py deleted file mode 100644 index 2da95625fc..0000000000 --- a/haystack/preview/components/routers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from haystack.preview.components.routers.document_joiner import DocumentJoiner -from haystack.preview.components.routers.file_type_router import FileTypeRouter -from haystack.preview.components.routers.metadata_router import MetadataRouter -from haystack.preview.components.routers.conditional_router import ConditionalRouter -from haystack.preview.components.routers.text_language_router import TextLanguageRouter - -__all__ = ["DocumentJoiner", "FileTypeRouter", "MetadataRouter", "TextLanguageRouter", "ConditionalRouter"] diff --git a/haystack/preview/components/routers/conditional_router.py b/haystack/preview/components/routers/conditional_router.py deleted file mode 100644 index af96d96ff3..0000000000 --- a/haystack/preview/components/routers/conditional_router.py +++ /dev/null @@ -1,347 +0,0 @@ -import importlib -import inspect -import logging -import sys -from typing import List, Dict, Any, Set, get_origin - -from jinja2 import meta, Environment, TemplateSyntaxError -from jinja2.nativetypes import NativeEnvironment - -from haystack.preview import component, default_from_dict, default_to_dict, DeserializationError - -logger = logging.getLogger(__name__) - - -class NoRouteSelectedException(Exception): - """Exception raised when no route is selected in ConditionalRouter.""" - - -class RouteConditionException(Exception): - """Exception raised when there is an error parsing or evaluating the condition expression in ConditionalRouter.""" - - -def serialize_type(target: Any) -> str: - """ - Serializes a type or an instance to its string representation, including the module name. - - This function handles types, instances of types, and special typing objects. - It assumes that non-typing objects will have a '__name__' attribute and raises - an error if a type cannot be serialized. - - :param target: The object to serialize, can be an instance or a type. - :type target: Any - :return: The string representation of the type. - :raises ValueError: If the type cannot be serialized. - """ - # If the target is a string and contains a dot, treat it as an already serialized type - if isinstance(target, str) and "." in target: - return target - - # Determine if the target is a type or an instance of a typing object - is_type_or_typing = isinstance(target, type) or bool(get_origin(target)) - type_obj = target if is_type_or_typing else type(target) - module = inspect.getmodule(type_obj) - type_obj_repr = repr(type_obj) - - if type_obj_repr.startswith("typing."): - # e.g., typing.List[int] -> List[int], we'll add the module below - type_name = type_obj_repr.split(".", 1)[1] - elif hasattr(type_obj, "__name__"): - type_name = type_obj.__name__ - else: - # If type cannot be serialized, raise an error - raise ValueError(f"Could not serialize type: {type_obj_repr}") - - # Construct the full path with module name if available - if module and hasattr(module, "__name__"): - if module.__name__ == "builtins": - # omit the module name for builtins, it just clutters the output - # e.g. instead of 'builtins.str', we'll just return 'str' - full_path = type_name - else: - full_path = f"{module.__name__}.{type_name}" - else: - full_path = type_name - - return full_path - - -def deserialize_type(type_str: str) -> Any: - """ - Deserializes a type given its full import path as a string, including nested generic types. - - This function will dynamically import the module if it's not already imported - and then retrieve the type object from it. It also handles nested generic types like 'typing.List[typing.Dict[int, str]]'. - - :param type_str: The string representation of the type's full import path. - :return: The deserialized type object. - :raises DeserializationError: If the type cannot be deserialized due to missing module or type. - """ - - def parse_generic_args(args_str): - args = [] - bracket_count = 0 - current_arg = "" - - for char in args_str: - if char == "[": - bracket_count += 1 - elif char == "]": - bracket_count -= 1 - - if char == "," and bracket_count == 0: - args.append(current_arg.strip()) - current_arg = "" - else: - current_arg += char - - if current_arg: - args.append(current_arg.strip()) - - return args - - if "[" in type_str and type_str.endswith("]"): - # Handle generics - main_type_str, generics_str = type_str.split("[", 1) - generics_str = generics_str[:-1] - - main_type = deserialize_type(main_type_str) - generic_args = tuple(deserialize_type(arg) for arg in parse_generic_args(generics_str)) - - # Reconstruct - return main_type[generic_args] - - else: - # Handle non-generics - parts = type_str.split(".") - module_name = ".".join(parts[:-1]) or "builtins" - type_name = parts[-1] - - module = sys.modules.get(module_name) - if not module: - try: - module = importlib.import_module(module_name) - except ImportError as e: - raise DeserializationError(f"Could not import the module: {module_name}") from e - - deserialized_type = getattr(module, type_name, None) - if not deserialized_type: - raise DeserializationError(f"Could not locate the type: {type_name} in the module: {module_name}") - - return deserialized_type - - -@component -class ConditionalRouter: - """ - ConditionalRouter in Haystack 2.x pipelines is designed to manage data routing based on specific conditions. - This is achieved by defining a list named 'routes'. Each element in this list is a dictionary representing a - single route. - - A route dictionary comprises four key elements: - - 'condition': A Jinja2 string expression that determines if the route is selected. - - 'output': A Jinja2 expression defining the route's output value. - - 'output_type': The type of the output data (e.g., str, List[int]). - - 'output_name': The name under which the `output` value of the route is published. This name is used to connect - the router to other components in the pipeline. - - Here's an example: - - ```python - from haystack.preview.components.routers import ConditionalRouter - - routes = [ - { - "condition": "{{streams|length > 2}}", - "output": "{{streams}}", - "output_name": "enough_streams", - "output_type": List[int], - }, - { - "condition": "{{streams|length <= 2}}", - "output": "{{streams}}", - "output_name": "insufficient_streams", - "output_type": List[int], - }, - ] - router = ConditionalRouter(routes) - # When 'streams' has more than 2 items, 'enough_streams' output will activate, emitting the list [1, 2, 3] - kwargs = {"streams": [1, 2, 3], "query": "Haystack"} - result = router.run(**kwargs) - assert result == {"enough_streams": [1, 2, 3]} - ``` - - In this example, we configure two routes. The first route sends the 'streams' value to 'enough_streams' if the - stream count exceeds two. Conversely, the second route directs 'streams' to 'insufficient_streams' when there - are two or fewer streams. - - In the pipeline setup, the router is connected to other components using the output names. For example, the - 'enough_streams' output might be connected to another component that processes the streams, while the - 'insufficient_streams' output might be connected to a component that fetches more streams, and so on. - - Here is a pseudocode example of a pipeline that uses the ConditionalRouter and routes fetched ByteStreams to - different components depending on the number of streams fetched: - - ``` - from typing import List - from haystack import Pipeline - from haystack.preview.dataclasses import ByteStream - from haystack.preview.components.routers import ConditionalRouter - - routes = [ - { - "condition": "{{streams|length > 2}}", - "output": "{{streams}}", - "output_name": "enough_streams", - "output_type": List[ByteStream], - }, - { - "condition": "{{streams|length <= 2}}", - "output": "{{streams}}", - "output_name": "insufficient_streams", - "output_type": List[ByteStream], - }, - ] - - pipe = Pipeline() - pipe.add_component("router", router) - ... - pipe.connect("router.enough_streams", "some_component_a.streams") - pipe.connect("router.insufficient_streams", "some_component_b.streams_or_some_other_input") - ... - ``` - """ - - def __init__(self, routes: List[Dict]): - """ - Initializes the ConditionalRouter with a list of routes detailing the conditions for routing. - - :param routes: A list of dictionaries, each defining a route with a boolean condition expression - ('condition'), an output value ('output'), the output type ('output_type') and - ('output_name') that defines the output name for the variable defined in 'output'. - """ - self._validate_routes(routes) - self.routes: List[dict] = routes - - # Create a Jinja native environment to inspect variables in the condition templates - env = NativeEnvironment() - - # Inspect the routes to determine input and output types. - input_types: Set[str] = set() # let's just store the name, type will always be Any - output_types: Dict[str, str] = {} - - for route in routes: - # extract inputs - route_input_names = self._extract_variables(env, [route["output"], route["condition"]]) - input_types.update(route_input_names) - - # extract outputs - output_types.update({route["output_name"]: route["output_type"]}) - - component.set_input_types(self, **{var: Any for var in input_types}) - component.set_output_types(self, **output_types) - - def to_dict(self) -> Dict[str, Any]: - for route in self.routes: - # output_type needs to be serialized to a string - route["output_type"] = serialize_type(route["output_type"]) - - return default_to_dict(self, routes=self.routes) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ConditionalRouter": - init_params = data.get("init_parameters", {}) - routes = init_params.get("routes") - for route in routes: - # output_type needs to be deserialized from a string to a type - route["output_type"] = deserialize_type(route["output_type"]) - return default_from_dict(cls, data) - - def run(self, **kwargs): - """ - Executes the routing logic by evaluating the specified boolean condition expressions - for each route in the order they are listed. The method directs the flow - of data to the output specified in the first route, whose expression - evaluates to True. If no route's expression evaluates to True, an exception - is raised. - - :param kwargs: A dictionary containing the pipeline variables, which should - include all variables used in the "condition" templates. - - :return: A dictionary containing the output and the corresponding result, - based on the first route whose expression evaluates to True. - - :raises NoRouteSelectedException: If no route's expression evaluates to True. - """ - # Create a Jinja native environment to evaluate the condition templates as Python expressions - env = NativeEnvironment() - - for route in self.routes: - try: - t = env.from_string(route["condition"]) - if t.render(**kwargs): - # We now evaluate the `output` expression to determine the route output - t_output = env.from_string(route["output"]) - output = t_output.render(**kwargs) - # and return the output as a dictionary under the output_name key - return {route["output_name"]: output} - except Exception as e: - raise RouteConditionException(f"Error evaluating condition for route '{route}': {e}") from e - - raise NoRouteSelectedException(f"No route fired. Routes: {self.routes}") - - def _validate_routes(self, routes: List[Dict]): - """ - Validates a list of routes. - - :param routes: A list of routes. - :type routes: List[Dict] - """ - env = NativeEnvironment() - for route in routes: - try: - keys = set(route.keys()) - except AttributeError: - raise ValueError(f"Route must be a dictionary, got: {route}") - - mandatory_fields = {"condition", "output", "output_type", "output_name"} - has_all_mandatory_fields = mandatory_fields.issubset(keys) - if not has_all_mandatory_fields: - raise ValueError( - f"Route must contain 'condition', 'output', 'output_type' and 'output_name' fields: {route}" - ) - for field in ["condition", "output"]: - if not self._validate_template(env, route[field]): - raise ValueError(f"Invalid template for field '{field}': {route[field]}") - - def _extract_variables(self, env: NativeEnvironment, templates: List[str]) -> Set[str]: - """ - Extracts all variables from a list of Jinja template strings. - - :param env: A Jinja environment. - :type env: Environment - :param templates: A list of Jinja template strings. - :type templates: List[str] - :return: A set of variable names. - """ - variables = set() - for template in templates: - ast = env.parse(template) - variables.update(meta.find_undeclared_variables(ast)) - return variables - - def _validate_template(self, env: Environment, template_text: str): - """ - Validates a template string by parsing it with Jinja. - - :param env: A Jinja environment. - :type env: Environment - :param template_text: A Jinja template string. - :type template_text: str - :return: True if the template is valid, False otherwise. - """ - try: - env.parse(template_text) - return True - except TemplateSyntaxError: - return False diff --git a/haystack/preview/components/routers/document_joiner.py b/haystack/preview/components/routers/document_joiner.py deleted file mode 100644 index 96b9b989b2..0000000000 --- a/haystack/preview/components/routers/document_joiner.py +++ /dev/null @@ -1,153 +0,0 @@ -import itertools -import logging -from collections import defaultdict -from math import inf -from typing import List, Optional -from canals.component.types import Variadic - -from haystack.preview import component, Document - - -logger = logging.getLogger(__name__) - - -@component -class DocumentJoiner: - """ - A component that joins input lists of Documents from multiple connections and outputs them as one list. - - The component allows multiple join modes: - * concatenate: Combine Documents from multiple components. Discards duplicate Documents. - Documents get their scores from the last component in the pipeline that assigns scores. - This join mode doesn't influence Document scores. - * merge: Merge scores of duplicate Documents coming from multiple components. - Optionally, you can assign a weight to the scores and set the top_k limit for this join mode. - You can also use this join mode to rerank retrieved Documents. - * reciprocal_rank_fusion: Combine Documents into a single list based on their ranking received from multiple components. - - Example usage in a hybrid retrieval pipeline: - ```python - document_store = InMemoryDocumentStore() - p = Pipeline() - p.add_component(instance=InMemoryBM25Retriever(document_store=document_store), name="bm25_retriever") - p.add_component( - instance=SentenceTransformersTextEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="text_embedder", - ) - p.add_component(instance=InMemoryEmbeddingRetriever(document_store=document_store), name="embedding_retriever") - p.add_component(instance=DocumentJoiner(), name="joiner") - p.connect("bm25_retriever", "joiner") - p.connect("embedding_retriever", "joiner") - p.connect("text_embedder", "embedding_retriever") - query = "What is the capital of France?" - p.run(data={"bm25_retriever": {"query": query}, - "text_embedder": {"text": query}}) - ``` - """ - - def __init__( - self, - join_mode: str = "concatenate", - weights: Optional[List[float]] = None, - top_k: Optional[int] = None, - sort_by_score: bool = True, - ): - """ - Initialize the DocumentJoiner. - - :param join_mode: Specifies the join mode to use. Available modes: `concatenate` to combine Documents from multiple Retrievers, `merge` to aggregate the scores of - individual Documents, `reciprocal_rank_fusion` to apply rank-based scoring. - :param weights: A component-wise list (the length of the list must be equal to the number of input components) of weights for - adjusting Document scores when using the `merge` join_mode. By default, equal weight is given - to each Retriever score. This param is not compatible with the `concatenate` join_mode. - :param top_k: The maximum number of Documents to be returned as output. By default, returns all Documents. - :param sort_by_score: Whether the output list of Documents should be sorted by Document scores in descending order. - By default, the output is sorted. - Documents without score are handled as if their score was -infinity. - """ - if join_mode not in ["concatenate", "merge", "reciprocal_rank_fusion"]: - raise ValueError(f"DocumentJoiner component does not support '{join_mode}' join_mode.") - self.join_mode = join_mode - self.weights = [float(i) / sum(weights) for i in weights] if weights else None - self.top_k = top_k - self.sort_by_score = sort_by_score - - @component.output_types(documents=List[Document]) - def run(self, documents: Variadic[List[Document]]): - """ - Run the DocumentJoiner. This method joins the input lists of Documents into one output list based on the join_mode specified during initialization. - - :param documents: An arbitrary number of lists of Documents to join. - """ - output_documents = [] - if self.join_mode == "concatenate": - output_documents = self._concatenate(documents) - elif self.join_mode == "merge": - output_documents = self._merge(documents) - elif self.join_mode == "reciprocal_rank_fusion": - output_documents = self._reciprocal_rank_fusion(documents) - - if self.sort_by_score: - output_documents = sorted( - output_documents, key=lambda doc: doc.score if doc.score is not None else -inf, reverse=True - ) - if any(doc.score is None for doc in output_documents): - logger.info( - "Some of the Documents DocumentJoiner got have score=None. It was configured to sort Documents by " - "score, so those with score=None were sorted as if they had a score of -infinity." - ) - - if self.top_k: - output_documents = output_documents[: self.top_k] - return {"documents": output_documents} - - def _concatenate(self, document_lists): - """ - Concatenate multiple lists of Documents and return only the Document with the highest score for duplicate Documents. - """ - output = [] - docs_per_id = defaultdict(list) - for doc in itertools.chain.from_iterable(document_lists): - docs_per_id[doc.id].append(doc) - for docs in docs_per_id.values(): - doc_with_best_score = max(docs, key=lambda doc: doc.score if doc.score else -inf) - output.append(doc_with_best_score) - return output - - def _merge(self, document_lists): - """ - Merge multiple lists of Documents and calculate a weighted sum of the scores of duplicate Documents. - """ - scores_map = defaultdict(int) - documents_map = {} - weights = self.weights if self.weights else [1 / len(document_lists)] * len(document_lists) - - for documents, weight in zip(document_lists, weights): - for doc in documents: - scores_map[doc.id] += (doc.score if doc.score else 0) * weight - documents_map[doc.id] = doc - - for doc in documents_map.values(): - doc.score = scores_map[doc.id] - - return documents_map.values() - - def _reciprocal_rank_fusion(self, document_lists): - """ - Merge multiple lists of Documents and assign scores based on reciprocal rank fusion. - The constant k is set to 61 (60 was suggested by the original paper, - plus 1 as python lists are 0-based and the paper used 1-based ranking). - """ - k = 61 - - scores_map = defaultdict(int) - documents_map = {} - for documents in document_lists: - for rank, doc in enumerate(documents): - scores_map[doc.id] += 1 / (k + rank) - documents_map[doc.id] = doc - - for doc in documents_map.values(): - doc.score = scores_map[doc.id] - - return documents_map.values() diff --git a/haystack/preview/components/routers/file_type_router.py b/haystack/preview/components/routers/file_type_router.py deleted file mode 100644 index e644129706..0000000000 --- a/haystack/preview/components/routers/file_type_router.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging -import mimetypes -from collections import defaultdict -from pathlib import Path -from typing import List, Union, Optional, Dict - -from haystack.preview import component -from haystack.preview.dataclasses import ByteStream - -logger = logging.getLogger(__name__) - - -@component -class FileTypeRouter: - """ - FileTypeRouter takes a list of data sources (file paths or byte streams) and groups them by their corresponding - MIME types. For file paths, MIME types are inferred from their extensions, while for byte streams, MIME types - are determined from the provided metadata. - - The set of MIME types to consider is specified during the initialization of the component. - - This component is invaluable when categorizing a large collection of files or data streams by their MIME - types and routing them to different components for further processing. - """ - - def __init__(self, mime_types: List[str]): - """ - Initialize the FileTypeRouter. - - :param mime_types: A list of file mime types to consider when routing - files (e.g. ["text/plain", "audio/x-wav", "image/jpeg"]). - """ - if not mime_types: - raise ValueError("The list of mime types cannot be empty.") - - for mime_type in mime_types: - if not self.is_valid_mime_type_format(mime_type): - raise ValueError( - f"Unknown mime type: '{mime_type}'. Ensure you passed a list of strings in the 'mime_types' parameter" - ) - - component.set_output_types(self, unclassified=List[Path], **{mime_type: List[Path] for mime_type in mime_types}) - self.mime_types = mime_types - - def run(self, sources: List[Union[str, Path, ByteStream]]) -> Dict[str, List[Union[ByteStream, Path]]]: - """ - Categorizes the provided data sources by their MIME types. - - :param sources: A list of file paths or byte streams to categorize. - :return: A dictionary where keys are MIME types and values are lists of data sources. - """ - - mime_types = defaultdict(list) - for source in sources: - if isinstance(source, str): - source = Path(source) - - if isinstance(source, Path): - mime_type = self.get_mime_type(source) - elif isinstance(source, ByteStream): - mime_type = source.metadata.get("content_type") - else: - raise ValueError(f"Unsupported data source type: {type(source)}") - - if mime_type in self.mime_types: - mime_types[mime_type].append(source) - else: - mime_types["unclassified"].append(source) - - return mime_types - - def get_mime_type(self, path: Path) -> Optional[str]: - """ - Get the MIME type of the provided file path. - - :param path: The file path to get the MIME type for. - :return: The MIME type of the provided file path, or None if the MIME type cannot be determined. - """ - return mimetypes.guess_type(path.as_posix())[0] - - def is_valid_mime_type_format(self, mime_type: str) -> bool: - """ - Check if the provided MIME type is in valid format - :param mime_type: The MIME type to check. - :return: True if the provided MIME type is a valid MIME type format, False otherwise. - """ - return mime_type in mimetypes.types_map.values() diff --git a/haystack/preview/components/routers/metadata_router.py b/haystack/preview/components/routers/metadata_router.py deleted file mode 100644 index f83b1e5542..0000000000 --- a/haystack/preview/components/routers/metadata_router.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Dict, List - -from haystack.preview import component, Document -from haystack.preview.utils.filters import document_matches_filter, convert - - -@component -class MetadataRouter: - """ - A component that routes documents to different connections based on the content of their fields. - """ - - def __init__(self, rules: Dict[str, Dict]): - """ - Initialize the MetadataRouter. - - :param rules: A dictionary of rules that specify which edge to route a document to based on its metadata. - The keys of the dictionary are the names of the output connections, and the values are dictionaries that - follow the format of filtering expressions in Haystack. For example: - ```python - { - "edge_1": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-01-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2023-04-01"}, - ], - }, - "edge_2": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-04-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2023-07-01"}, - ], - }, - "edge_3": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-07-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2023-10-01"}, - ], - }, - "edge_4": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-10-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2024-01-01"}, - ], - }, - } - ``` - """ - self.rules = rules - component.set_output_types(self, unmatched=List[Document], **{edge: List[Document] for edge in rules}) - - def run(self, documents: List[Document]): - """ - Run the MetadataRouter. This method routes the documents to different edges based on their fields content and - the rules specified during initialization. If a document does not match any of the rules, it is routed to - a connection named "unmatched". - - :param documents: A list of documents to route to different edges. - """ - unmatched_documents = [] - output: Dict[str, List[Document]] = {edge: [] for edge in self.rules} - - for document in documents: - cur_document_matched = False - for edge, rule in self.rules.items(): - if "operator" not in rule: - # Must be a legacy filter, convert it - rule = convert(rule) - if document_matches_filter(rule, document): - output[edge].append(document) - cur_document_matched = True - - if not cur_document_matched: - unmatched_documents.append(document) - - output["unmatched"] = unmatched_documents - return output diff --git a/haystack/preview/components/routers/text_language_router.py b/haystack/preview/components/routers/text_language_router.py deleted file mode 100644 index 4e8ffd0167..0000000000 --- a/haystack/preview/components/routers/text_language_router.py +++ /dev/null @@ -1,73 +0,0 @@ -import logging -from typing import List, Dict, Optional - -from haystack.preview import component -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - -with LazyImport("Run 'pip install langdetect'") as langdetect_import: - import langdetect - - -@component -class TextLanguageRouter: - """ - Routes a text input onto one of different output connections depending on its language. - This is useful for routing queries to different models in a pipeline depending on their language. - The set of supported languages can be specified. - For routing Documents based on their language use the related DocumentLanguageClassifier component to first - classify the documents and then the MetaDataRouter to route them. - - Example usage in a retrieval pipeline that passes only English language queries to the retriever: - - ```python - document_store = InMemoryDocumentStore() - p = Pipeline() - p.add_component(instance=TextLanguageRouter(), name="text_language_router") - p.add_component(instance=InMemoryBM25Retriever(document_store=document_store), name="retriever") - p.connect("text_language_router.en", "retriever.query") - p.run({"text_language_router": {"text": "What's your query?"}}) - ``` - """ - - def __init__(self, languages: Optional[List[str]] = None): - """ - :param languages: A list of languages in ISO code, each corresponding to a different output connection (see [langdetect` documentation](https://github.com/Mimino666/langdetect#languages)). By default, only ["en"] is supported and texts of any other language are routed to "unmatched". - """ - langdetect_import.check() - if not languages: - languages = ["en"] - self.languages = languages - component.set_output_types(self, unmatched=str, **{language: str for language in languages}) - - def run(self, text: str) -> Dict[str, str]: - """ - Run the TextLanguageRouter. This method routes the text one of different edges based on its language. - If the text does not match any of the languages specified at initialization, it is routed to - a connection named "unmatched". - - :param text: A str to route to one of different edges. - """ - if not isinstance(text, str): - raise TypeError( - "TextLanguageRouter expects a str as input. In case you want to classify a document, please use the DocumentLanguageClassifier and MetaDataRouter." - ) - - output: Dict[str, str] = {} - - detected_language = self.detect_language(text) - if detected_language in self.languages: - output[detected_language] = text - else: - output["unmatched"] = text - - return output - - def detect_language(self, text: str) -> Optional[str]: - try: - language = langdetect.detect(text) - except langdetect.LangDetectException: - logger.warning("Langdetect cannot detect the language of text: %s", text) - language = None - return language diff --git a/haystack/preview/components/samplers/__init__.py b/haystack/preview/components/samplers/__init__.py deleted file mode 100644 index cab0e878e8..0000000000 --- a/haystack/preview/components/samplers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.samplers.top_p import TopPSampler - -__all__ = ["TopPSampler"] diff --git a/haystack/preview/components/samplers/top_p.py b/haystack/preview/components/samplers/top_p.py deleted file mode 100644 index c3740cbbaa..0000000000 --- a/haystack/preview/components/samplers/top_p.py +++ /dev/null @@ -1,127 +0,0 @@ -import logging -from typing import List, Optional - -from haystack.preview import ComponentError, Document, component -from haystack.preview.lazy_imports import LazyImport - -logger = logging.getLogger(__name__) - - -with LazyImport(message="Run 'pip install \"torch>=1.13\"'") as torch_import: - import torch - - -@component -class TopPSampler: - """ - Filters documents using top-p (nucleus) sampling based on their similarity scores' cumulative probability. - - Usage example: - - ```python - from haystack.preview import Document - from haystack.preview.components.samplers import TopPSampler - - sampler = TopPSampler(top_p=0.95) - docs = [ - Document(text="Berlin", metadata={"similarity_score": -10.6}), - Document(text="Belgrade", metadata={"similarity_score": -8.9}), - Document(text="Sarajevo", metadata={"similarity_score": -4.6}), - ] - output = sampler.run(documents=docs) - docs = output["documents"] - assert len(docs) == 1 - assert docs[0].content == "Sarajevo" - ``` - """ - - def __init__(self, top_p: float = 1.0, score_field: Optional[str] = None): - """ - Creates an instance of TopPSampler. - - :param top_p: Cumulative probability threshold (usually between 0.9 and 0.99). - :param score_field: Field name in a document's metadata containing the scores. Defaults to the Document score - if not provided. - """ - torch_import.check() - - self.top_p = top_p - self.score_field = score_field - - @component.output_types(documents=List[Document]) - def run(self, documents: List[Document], top_p: Optional[float] = None): - """ - Filter documents based on their similarity scores using top-p sampling. - - :param documents: List of Documents to filter. - :param top_p: Cumulative probability threshold. Defaults to the value set during initialization or 1.0 - if not set. - :return: List of filtered Documents. - """ - if not documents: - return {"documents": []} - - top_p = top_p or self.top_p or 1.0 # default to 1.0 if both are None - - if not 0 <= top_p <= 1: - raise ComponentError(f"top_p must be between 0 and 1. Got {top_p}.") - - similarity_scores = torch.tensor(self._collect_scores(documents), dtype=torch.float32) - - # Apply softmax normalization to the similarity scores - probs = torch.nn.functional.softmax(similarity_scores, dim=-1) - - # Sort the probabilities and calculate their cumulative sum - sorted_probs, sorted_indices = torch.sort(probs, descending=True) - cumulative_probs = torch.cumsum(sorted_probs, dim=-1) - - # Check if the cumulative probabilities are close to top_p with a 1e-6 tolerance - close_to_top_p = torch.isclose(cumulative_probs, torch.tensor(top_p, device=cumulative_probs.device), atol=1e-6) - - # Combine the close_to_top_p with original condition using logical OR - condition = (cumulative_probs <= top_p) | close_to_top_p - - # Find the indices with cumulative probabilities that exceed top_p - top_p_indices = torch.where(torch.BoolTensor(condition))[0] - - # Map the selected indices back to their original indices - original_indices = sorted_indices[top_p_indices] - selected_docs = [documents[i.item()] for i in original_indices] - - # If low p resulted in no documents being selected, then - # return at least one document - if not selected_docs: - logger.warning( - "Top-p sampling with p=%s resulted in no documents being selected. " - "Returning the document with the highest similarity score.", - top_p, - ) - highest_prob_indices = torch.argsort(probs, descending=True) - selected_docs = [documents[int(highest_prob_indices[0].item())]] - - return {"documents": selected_docs} - - def _collect_scores(self, documents: List[Document]) -> List[float]: - """ - Collect the scores from the documents' metadata. - :param documents: List of Documents. - :return: List of scores. - """ - if self.score_field: - missing_scores_docs = [d for d in documents if self.score_field not in d.meta] - if missing_scores_docs: - missing_scores_docs_ids = [d.id for d in missing_scores_docs if d.id] - raise ComponentError( - f"Score field '{self.score_field}' not found in metadata of documents " - f"with IDs: {missing_scores_docs_ids}." - f"Make sure that all documents have a score field '{self.score_field}' in their metadata." - ) - return [d.meta[self.score_field] for d in documents] - else: - missing_scores_docs = [d for d in documents if d.score is None] - if missing_scores_docs: - missing_scores_docs_ids = [d.id for d in missing_scores_docs if d.id] - raise ComponentError( - f"Ensure all documents have a valid score value. These docs {missing_scores_docs_ids} don't." - ) - return [d.score for d in documents] # type: ignore ## because Document score is Optional diff --git a/haystack/preview/components/websearch/__init__.py b/haystack/preview/components/websearch/__init__.py deleted file mode 100644 index ce20e77857..0000000000 --- a/haystack/preview/components/websearch/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.components.websearch.serper_dev import SerperDevWebSearch -from haystack.preview.components.websearch.searchapi import SearchApiWebSearch - -__all__ = ["SerperDevWebSearch", "SearchApiWebSearch"] diff --git a/haystack/preview/components/websearch/searchapi.py b/haystack/preview/components/websearch/searchapi.py deleted file mode 100644 index fafe8bd95a..0000000000 --- a/haystack/preview/components/websearch/searchapi.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -import os -import logging -from typing import Dict, List, Optional, Any - -import requests - -from haystack.preview import Document, component, default_to_dict, ComponentError - -logger = logging.getLogger(__name__) - - -SEARCHAPI_BASE_URL = "https://www.searchapi.io/api/v1/search" - - -class SearchApiError(ComponentError): - ... - - -@component -class SearchApiWebSearch: - """ - Search engine using SearchApi API. Given a query, it returns a list of URLs that are the most relevant. - - See the [SearchApi website](https://www.searchapi.io/) for more details. - """ - - def __init__( - self, - api_key: Optional[str] = None, - top_k: Optional[int] = 10, - allowed_domains: Optional[List[str]] = None, - search_params: Optional[Dict[str, Any]] = None, - ): - """ - :param api_key: API key for the SearchApi API. It can be - explicitly provided or automatically read from the - environment variable SEARCHAPI_API_KEY (recommended). - :param top_k: Number of documents to return. - :param allowed_domains: List of domains to limit the search to. - :param search_params: Additional parameters passed to the SearchApi API. - For example, you can set 'num' to 100 to increase the number of search results. - See the [SearchApi website](https://www.searchapi.io/) for more details. - """ - if api_key is None: - try: - api_key = os.environ["SEARCHAPI_API_KEY"] - except KeyError as e: - raise ValueError( - "SearchApiWebSearch expects an API key. " - "Set the SEARCHAPI_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - self.api_key = api_key - self.top_k = top_k - self.allowed_domains = allowed_domains - self.search_params = search_params or {} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, top_k=self.top_k, allowed_domains=self.allowed_domains, search_params=self.search_params - ) - - @component.output_types(documents=List[Document], links=List[str]) - def run(self, query: str): - """ - Search the SearchApi API for the given query and return the results as a list of Documents and a list of links. - - :param query: Query string. - """ - query_prepend = "OR ".join(f"site:{domain} " for domain in self.allowed_domains) if self.allowed_domains else "" - - payload = json.dumps({"q": query_prepend + " " + query, **self.search_params}) - headers = {"Authorization": f"Bearer {self.api_key}", "X-SearchApi-Source": "Haystack"} - - try: - response = requests.get(SEARCHAPI_BASE_URL, headers=headers, params=payload, timeout=90) - response.raise_for_status() # Will raise an HTTPError for bad responses - except requests.Timeout: - raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") - - except requests.RequestException as e: - raise SearchApiError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e - - # Request succeeded - json_result = response.json() - - # organic results are the main results from the search engine - organic_results = [] - if "organic_results" in json_result: - for result in json_result["organic_results"]: - organic_results.append( - Document.from_dict({"title": result["title"], "content": result["snippet"], "link": result["link"]}) - ) - - # answer box has a direct answer to the query - answer_box = [] - if "answer_box" in json_result: - answer_box = [ - Document.from_dict( - { - "title": json_result["answer_box"].get("title", ""), - "content": json_result["answer_box"].get("answer", ""), - "link": json_result["answer_box"].get("link", ""), - } - ) - ] - - knowledge_graph = [] - if "knowledge_graph" in json_result: - knowledge_graph = [ - Document.from_dict( - { - "title": json_result["knowledge_graph"].get("title", ""), - "content": json_result["knowledge_graph"].get("description", ""), - } - ) - ] - - related_questions = [] - if "related_questions" in json_result: - for result in json_result["related_questions"]: - related_questions.append( - Document.from_dict( - { - "title": result["question"], - "content": result["answer"] if result.get("answer") else result.get("answer_highlight", ""), - "link": result.get("source", {}).get("link", ""), - } - ) - ) - - documents = answer_box + knowledge_graph + organic_results + related_questions - - links = [result["link"] for result in json_result["organic_results"]] - - logger.debug("SearchApi returned %s documents for the query '%s'", len(documents), query) - return {"documents": documents[: self.top_k], "links": links[: self.top_k]} diff --git a/haystack/preview/components/websearch/serper_dev.py b/haystack/preview/components/websearch/serper_dev.py deleted file mode 100644 index 8b98d3ebc9..0000000000 --- a/haystack/preview/components/websearch/serper_dev.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -import os -import logging -from typing import Dict, List, Optional, Any - -import requests - -from haystack.preview import Document, component, default_to_dict, ComponentError - -logger = logging.getLogger(__name__) - - -SERPERDEV_BASE_URL = "https://google.serper.dev/search" - - -class SerperDevError(ComponentError): - ... - - -@component -class SerperDevWebSearch: - """ - Search engine using SerperDev API. Given a query, it returns a list of URLs that are the most relevant. - - See the [Serper Dev website](https://serper.dev/) for more details. - """ - - def __init__( - self, - api_key: Optional[str] = None, - top_k: Optional[int] = 10, - allowed_domains: Optional[List[str]] = None, - search_params: Optional[Dict[str, Any]] = None, - ): - """ - :param api_key: API key for the SerperDev API. It can be - explicitly provided or automatically read from the - environment variable SERPERDEV_API_KEY (recommended). - :param top_k: Number of documents to return. - :param allowed_domains: List of domains to limit the search to. - :param search_params: Additional parameters passed to the SerperDev API. - For example, you can set 'num' to 20 to increase the number of search results. - See the [Serper Dev website](https://serper.dev/) for more details. - """ - if api_key is None: - try: - api_key = os.environ["SERPERDEV_API_KEY"] - except KeyError as e: - raise ValueError( - "SerperDevWebSearch expects an API key. " - "Set the SERPERDEV_API_KEY environment variable (recommended) or pass it explicitly." - ) from e - raise ValueError("API key for SerperDev API must be set.") - self.api_key = api_key - self.top_k = top_k - self.allowed_domains = allowed_domains - self.search_params = search_params or {} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict( - self, top_k=self.top_k, allowed_domains=self.allowed_domains, search_params=self.search_params - ) - - @component.output_types(documents=List[Document], links=List[str]) - def run(self, query: str): - """ - Search the SerperDev API for the given query and return the results as a list of Documents and a list of links. - - :param query: Query string. - """ - query_prepend = "OR ".join(f"site:{domain} " for domain in self.allowed_domains) if self.allowed_domains else "" - - payload = json.dumps( - {"q": query_prepend + query, "gl": "us", "hl": "en", "autocorrect": True, **self.search_params} - ) - headers = {"X-API-KEY": self.api_key, "Content-Type": "application/json"} - - try: - response = requests.post(SERPERDEV_BASE_URL, headers=headers, data=payload, timeout=30) - response.raise_for_status() # Will raise an HTTPError for bad responses - except requests.Timeout: - raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") - - except requests.RequestException as e: - raise SerperDevError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e - - # If we reached this point, it means the request was successful and we can proceed - json_result = response.json() - - # we get the snippet from the json result and put it in the content field of the document - organic = [ - Document(meta={k: v for k, v in d.items() if k != "snippet"}, content=d["snippet"]) - for d in json_result["organic"] - ] - - # answer box is what search engine shows as a direct answer to the query - answer_box = [] - if "answerBox" in json_result: - answer_dict = json_result["answerBox"] - highlighted_answers = answer_dict.get("snippetHighlighted") - answer_box_content = None - # Check if highlighted_answers is a list and has at least one element - if isinstance(highlighted_answers, list) and len(highlighted_answers) > 0: - answer_box_content = highlighted_answers[0] - elif isinstance(highlighted_answers, str): - answer_box_content = highlighted_answers - if not answer_box_content: - for key in ["snippet", "answer", "title"]: - if key in answer_dict: - answer_box_content = answer_dict[key] - break - if answer_box_content: - answer_box = [ - Document( - content=answer_box_content, - meta={"title": answer_dict.get("title", ""), "link": answer_dict.get("link", "")}, - ) - ] - - # these are related questions that search engine shows - people_also_ask = [] - if "peopleAlsoAsk" in json_result: - for result in json_result["peopleAlsoAsk"]: - title = result.get("title", "") - people_also_ask.append( - Document( - content=result["snippet"] if result.get("snippet") else title, - meta={"title": title, "link": result.get("link", None)}, - ) - ) - - documents = answer_box + organic + people_also_ask - - links = [result["link"] for result in json_result["organic"]] - - logger.debug("Serper Dev returned %s documents for the query '%s'", len(documents), query) - return {"documents": documents[: self.top_k], "links": links[: self.top_k]} diff --git a/haystack/preview/components/writers/__init__.py b/haystack/preview/components/writers/__init__.py deleted file mode 100644 index 8328148352..0000000000 --- a/haystack/preview/components/writers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.components.writers.document_writer import DocumentWriter - -__all__ = ["DocumentWriter"] diff --git a/haystack/preview/components/writers/document_writer.py b/haystack/preview/components/writers/document_writer.py deleted file mode 100644 index 2ce45afde5..0000000000 --- a/haystack/preview/components/writers/document_writer.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import List, Optional, Dict, Any - -from haystack.preview import component, Document, default_from_dict, default_to_dict, DeserializationError -from haystack.preview.document_stores import DocumentStore, DuplicatePolicy, document_store - - -@component -class DocumentWriter: - """ - A component for writing documents to a DocumentStore. - """ - - def __init__(self, document_store: DocumentStore, policy: DuplicatePolicy = DuplicatePolicy.FAIL): - """ - Create a DocumentWriter component. - - :param policy: The policy to use when encountering duplicate documents (default is DuplicatePolicy.FAIL). - """ - self.document_store = document_store - self.policy = policy - - def _get_telemetry_data(self) -> Dict[str, Any]: - """ - Data that is sent to Posthog for usage analytics. - """ - return {"document_store": type(self.document_store).__name__} - - def to_dict(self) -> Dict[str, Any]: - """ - Serialize this component to a dictionary. - """ - return default_to_dict(self, document_store=self.document_store.to_dict(), policy=self.policy.name) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "DocumentWriter": - """ - Deserialize this component from a dictionary. - """ - init_params = data.get("init_parameters", {}) - if "document_store" not in init_params: - raise DeserializationError("Missing 'document_store' in serialization data") - if "type" not in init_params["document_store"]: - raise DeserializationError("Missing 'type' in document store's serialization data") - if init_params["document_store"]["type"] not in document_store.registry: - raise DeserializationError(f"DocumentStore of type '{init_params['document_store']['type']}' not found.") - docstore_class = document_store.registry[init_params["document_store"]["type"]] - docstore = docstore_class.from_dict(init_params["document_store"]) - - data["init_parameters"]["document_store"] = docstore - data["init_parameters"]["policy"] = DuplicatePolicy[data["init_parameters"]["policy"]] - return default_from_dict(cls, data) - - @component.output_types(documents_written=int) - def run(self, documents: List[Document], policy: Optional[DuplicatePolicy] = None): - """ - Run DocumentWriter on the given input data. - - :param documents: A list of documents to write to the store. - :param policy: The policy to use when encountering duplicate documents. - :return: Number of documents written - - :raises ValueError: If the specified document store is not found. - """ - if policy is None: - policy = self.policy - - documents_written = self.document_store.write_documents(documents=documents, policy=policy) - return {"documents_written": documents_written} diff --git a/haystack/preview/dataclasses/__init__.py b/haystack/preview/dataclasses/__init__.py deleted file mode 100644 index f27204bc4f..0000000000 --- a/haystack/preview/dataclasses/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from haystack.preview.dataclasses.document import Document -from haystack.preview.dataclasses.answer import ExtractedAnswer, GeneratedAnswer, Answer -from haystack.preview.dataclasses.byte_stream import ByteStream -from haystack.preview.dataclasses.chat_message import ChatMessage -from haystack.preview.dataclasses.chat_message import ChatRole -from haystack.preview.dataclasses.streaming_chunk import StreamingChunk - -__all__ = [ - "Document", - "ExtractedAnswer", - "GeneratedAnswer", - "Answer", - "ByteStream", - "ChatMessage", - "ChatRole", - "StreamingChunk", -] diff --git a/haystack/preview/dataclasses/answer.py b/haystack/preview/dataclasses/answer.py deleted file mode 100644 index ed3c1ae0c1..0000000000 --- a/haystack/preview/dataclasses/answer.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Any, Dict, List, Optional -from dataclasses import dataclass -from haystack.preview.dataclasses.document import Document - - -@dataclass(frozen=True) -class Answer: - data: Any - query: str - metadata: Dict[str, Any] - - -@dataclass(frozen=True) -class ExtractedAnswer(Answer): - data: Optional[str] - document: Optional[Document] - probability: float - start: Optional[int] = None - end: Optional[int] = None - - -@dataclass(frozen=True) -class GeneratedAnswer(Answer): - data: str - documents: List[Document] diff --git a/haystack/preview/dataclasses/byte_stream.py b/haystack/preview/dataclasses/byte_stream.py deleted file mode 100644 index dd84e1c26b..0000000000 --- a/haystack/preview/dataclasses/byte_stream.py +++ /dev/null @@ -1,38 +0,0 @@ -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional, Dict, Any - - -@dataclass(frozen=True) -class ByteStream: - """ - Base data class representing a binary object in the Haystack API. - """ - - data: bytes - metadata: Dict[str, Any] = field(default_factory=dict, hash=False) - mime_type: Optional[str] = field(default=None) - - def to_file(self, destination_path: Path): - with open(destination_path, "wb") as fd: - fd.write(self.data) - - @classmethod - def from_file_path(cls, filepath: Path, mime_type: Optional[str] = None) -> "ByteStream": - """ - Create a ByteStream from the contents read from a file. - - :param filepath: A valid path to a file. - """ - with open(filepath, "rb") as fd: - return cls(data=fd.read(), mime_type=mime_type) - - @classmethod - def from_string(cls, text: str, encoding: str = "utf-8", mime_type: Optional[str] = None) -> "ByteStream": - """ - Create a ByteStream encoding a string. - - :param text: The string to encode - :param encoding: The encoding used to convert the string into bytes - """ - return cls(data=text.encode(encoding), mime_type=mime_type) diff --git a/haystack/preview/dataclasses/chat_message.py b/haystack/preview/dataclasses/chat_message.py deleted file mode 100644 index 08c61d6cfc..0000000000 --- a/haystack/preview/dataclasses/chat_message.py +++ /dev/null @@ -1,80 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import Dict, Any, Optional - - -class ChatRole(str, Enum): - """Enumeration representing the roles within a chat.""" - - ASSISTANT = "assistant" - USER = "user" - SYSTEM = "system" - FUNCTION = "function" - - -@dataclass -class ChatMessage: - """ - Represents a message in a LLM chat conversation. - - :param content: The text content of the message. - :param role: The role of the entity sending the message. - :param name: The name of the function being called (only applicable for role FUNCTION). - :param metadata: Additional metadata associated with the message. - """ - - content: str - role: ChatRole - name: Optional[str] - metadata: Dict[str, Any] = field(default_factory=dict, hash=False) - - def is_from(self, role: ChatRole) -> bool: - """ - Check if the message is from a specific role. - - :param role: The role to check against. - :return: True if the message is from the specified role, False otherwise. - """ - return self.role == role - - @classmethod - def from_assistant(cls, content: str, metadata: Optional[Dict[str, Any]] = None) -> "ChatMessage": - """ - Create a message from the assistant. - - :param content: The text content of the message. - :param metadata: Additional metadata associated with the message. - :return: A new ChatMessage instance. - """ - return cls(content, ChatRole.ASSISTANT, None, metadata or {}) - - @classmethod - def from_user(cls, content: str) -> "ChatMessage": - """ - Create a message from the user. - - :param content: The text content of the message. - :return: A new ChatMessage instance. - """ - return cls(content, ChatRole.USER, None) - - @classmethod - def from_system(cls, content: str) -> "ChatMessage": - """ - Create a message from the system. - - :param content: The text content of the message. - :return: A new ChatMessage instance. - """ - return cls(content, ChatRole.SYSTEM, None) - - @classmethod - def from_function(cls, content: str, name: str) -> "ChatMessage": - """ - Create a message from a function call. - - :param content: The text content of the message. - :param name: The name of the function being called. - :return: A new ChatMessage instance. - """ - return cls(content, ChatRole.FUNCTION, name) diff --git a/haystack/preview/dataclasses/document.py b/haystack/preview/dataclasses/document.py deleted file mode 100644 index 168951edc0..0000000000 --- a/haystack/preview/dataclasses/document.py +++ /dev/null @@ -1,186 +0,0 @@ -import io -import hashlib -import logging -from dataclasses import asdict, dataclass, field, fields -from typing import Any, Dict, List, Optional - -import numpy -import pandas - -from haystack.preview.dataclasses.byte_stream import ByteStream - -logger = logging.getLogger(__name__) - - -class _BackwardCompatible(type): - """ - Metaclass that handles Document backward compatibility. - """ - - def __call__(cls, *args, **kwargs): - """ - Called before Document.__init__, will remap legacy fields to new ones. - Also handles building a Document from a flattened dictionary. - """ - # Move `content` to new fields depending on the type - content = kwargs.get("content") - if isinstance(content, pandas.DataFrame): - kwargs["dataframe"] = content - del kwargs["content"] - - # Not used anymore - if "content_type" in kwargs: - del kwargs["content_type"] - - # Embedding were stored as NumPy arrays in 1.x, so we convert it to the new type - if isinstance(embedding := kwargs.get("embedding"), numpy.ndarray): - kwargs["embedding"] = embedding.tolist() - - # id_hash_keys is not used anymore - if "id_hash_keys" in kwargs: - del kwargs["id_hash_keys"] - - return super().__call__(*args, **kwargs) - - -@dataclass -class Document(metaclass=_BackwardCompatible): - """ - Base data class containing some data to be queried. - Can contain text snippets, tables, and file paths to images or audios. - Documents can be sorted by score and saved to/from dictionary and JSON. - - :param id: Unique identifier for the document. When not set, it's generated based on the Document fields' values. - :param content: Text of the document, if the document contains text. - :param dataframe: Pandas dataframe with the document's content, if the document contains tabular data. - :param blob: Binary data associated with the document, if the document has any binary data associated with it. - :param meta: Additional custom metadata for the document. Must be JSON-serializable. - :param score: Score of the document. Used for ranking, usually assigned by retrievers. - :param embedding: Vector representation of the document. - """ - - id: str = field(default="") - content: Optional[str] = field(default=None) - dataframe: Optional[pandas.DataFrame] = field(default=None) - blob: Optional[ByteStream] = field(default=None) - meta: Dict[str, Any] = field(default_factory=dict) - score: Optional[float] = field(default=None) - embedding: Optional[List[float]] = field(default=None) - - def __repr__(self): - fields = [] - if self.content is not None: - fields.append( - f"content: '{self.content}'" if len(self.content) < 100 else f"content: '{self.content[:100]}...'" - ) - if self.dataframe is not None: - fields.append(f"dataframe: {self.dataframe.shape}") - if self.blob is not None: - fields.append(f"blob: {len(self.blob.data)} bytes") - if len(self.meta) > 0: - fields.append(f"meta: {self.meta}") - if self.score is not None: - fields.append(f"score: {self.score}") - if self.embedding is not None: - fields.append(f"embedding: vector of size {len(self.embedding)}") - fields_str = ", ".join(fields) - return f"{self.__class__.__name__}(id={self.id}, {fields_str})" - - def __eq__(self, other): - """ - Compares Documents for equality. - Two Documents are considered equals if their dictionary representation is identical. - """ - if type(self) != type(other): - return False - return self.to_dict() == other.to_dict() - - def __post_init__(self): - """ - Generate the ID based on the init parameters. - """ - # Generate an id only if not explicitly set - self.id = self.id or self._create_id() - - def _create_id(self): - """ - Creates a hash of the given content that acts as the document's ID. - """ - text = self.content or None - dataframe = self.dataframe.to_json() if self.dataframe is not None else None - blob = self.blob.data if self.blob is not None else None - mime_type = self.blob.mime_type if self.blob is not None else None - meta = self.meta or {} - embedding = self.embedding if self.embedding is not None else None - data = f"{text}{dataframe}{blob}{mime_type}{meta}{embedding}" - return hashlib.sha256(data.encode("utf-8")).hexdigest() - - def to_dict(self, flatten=True) -> Dict[str, Any]: - """ - Converts Document into a dictionary. - `dataframe` and `blob` fields are converted to JSON-serializable types. - - :param flatten: Whether to flatten `meta` field or not. Defaults to `True` to be backward-compatible with Haystack 1.x. - """ - data = asdict(self) - if (dataframe := data.get("dataframe")) is not None: - data["dataframe"] = dataframe.to_json() - if (blob := data.get("blob")) is not None: - data["blob"] = {"data": list(blob["data"]), "mime_type": blob["mime_type"]} - - if flatten: - meta = data.pop("meta") - return {**data, **meta} - - return data - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "Document": - """ - Creates a new Document object from a dictionary. - `dataframe` and `blob` fields are converted to their original types. - """ - if (dataframe := data.get("dataframe")) is not None: - data["dataframe"] = pandas.read_json(io.StringIO(dataframe)) - if blob := data.get("blob"): - data["blob"] = ByteStream(data=bytes(blob["data"]), mime_type=blob["mime_type"]) - # Store metadata for a moment while we try un-flattening allegedly flatten metadata. - # We don't expect both a `meta=` keyword and flatten metadata keys so we'll raise a - # ValueError later if this is the case. - meta = data.pop("meta", {}) - # Unflatten metadata if it was flattened. We assume any keyword argument that's not - # a document field is a metadata key. We treat legacy fields as document fields - # for backward compatibility. - flatten_meta = {} - legacy_fields = ["content_type", "id_hash_keys"] - document_fields = legacy_fields + [f.name for f in fields(cls)] - for key in list(data.keys()): - if key not in document_fields: - flatten_meta[key] = data.pop(key) - - # We don't support passing both flatten keys and the `meta` keyword parameter - if meta and flatten_meta: - raise ValueError( - "You can pass either the 'meta' parameter or flattened metadata keys as keyword arguments, " - "but currently you're passing both. Pass either the 'meta' parameter or flattened metadata keys." - ) - - # Finally put back all the metadata - return cls(**data, meta={**meta, **flatten_meta}) - - @property - def content_type(self): - """ - Returns the type of the content for the document. - This is necessary to keep backward compatibility with 1.x. - A ValueError will be raised if both `text` and `dataframe` fields are set - or both are missing. - """ - if self.content is not None and self.dataframe is not None: - raise ValueError("Both text and dataframe are set.") - - if self.content is not None: - return "text" - elif self.dataframe is not None: - return "table" - raise ValueError("Neither text nor dataframe is set.") diff --git a/haystack/preview/dataclasses/streaming_chunk.py b/haystack/preview/dataclasses/streaming_chunk.py deleted file mode 100644 index 1245560431..0000000000 --- a/haystack/preview/dataclasses/streaming_chunk.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass, field -from typing import Dict, Any - - -@dataclass -class StreamingChunk: - """ - The StreamingChunk class encapsulates a segment of streamed content along with - associated metadata. This structure facilitates the handling and processing of - streamed data in a systematic manner. - - :param content: The content of the message chunk as a string. - :param metadata: A dictionary containing metadata related to the message chunk. - """ - - content: str - metadata: Dict[str, Any] = field(default_factory=dict, hash=False) diff --git a/haystack/preview/document_stores/__init__.py b/haystack/preview/document_stores/__init__.py deleted file mode 100644 index 632c1f448f..0000000000 --- a/haystack/preview/document_stores/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from haystack.preview.document_stores.protocols import DocumentStore, DuplicatePolicy -from haystack.preview.document_stores.in_memory.document_store import InMemoryDocumentStore -from haystack.preview.document_stores.errors import DocumentStoreError, DuplicateDocumentError, MissingDocumentError -from haystack.preview.document_stores.decorator import document_store - -__all__ = [ - "DocumentStore", - "DuplicatePolicy", - "InMemoryDocumentStore", - "DocumentStoreError", - "DuplicateDocumentError", - "MissingDocumentError", - "document_store", -] diff --git a/haystack/preview/document_stores/decorator.py b/haystack/preview/document_stores/decorator.py deleted file mode 100644 index c82ccf91d9..0000000000 --- a/haystack/preview/document_stores/decorator.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - - -class _DocumentStore: - """ - Marks a class as an Haystack _DocumentStore. - All classes decorated with @document_store will be registered here and can be used in Haystack Pipelines. - """ - - def __init__(self): - self.registry = {} - - def _decorate(self, cls): - cls.__haystack_document_store__ = True - - classname = f"{cls.__module__}.{cls.__name__}" - if classname in self.registry: - logger.error( - "DocumentStore %s is already registered. Previous imported from '%s', new imported from '%s'", - classname, - self.registry[classname], - cls, - ) - - self.registry[classname] = cls - logger.debug("Registered DocumentStore %s", cls) - - return cls - - def __call__(self, cls=None): - if cls: - return self._decorate(cls) - - return self._decorate - - -document_store = _DocumentStore() diff --git a/haystack/preview/document_stores/errors.py b/haystack/preview/document_stores/errors.py deleted file mode 100644 index c345b04e50..0000000000 --- a/haystack/preview/document_stores/errors.py +++ /dev/null @@ -1,10 +0,0 @@ -class DocumentStoreError(Exception): - pass - - -class DuplicateDocumentError(DocumentStoreError): - pass - - -class MissingDocumentError(DocumentStoreError): - pass diff --git a/haystack/preview/document_stores/in_memory/__init__.py b/haystack/preview/document_stores/in_memory/__init__.py deleted file mode 100644 index 5b3644431a..0000000000 --- a/haystack/preview/document_stores/in_memory/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.document_stores.in_memory.document_store import InMemoryDocumentStore - -__all__ = ["InMemoryDocumentStore"] diff --git a/haystack/preview/document_stores/in_memory/document_store.py b/haystack/preview/document_stores/in_memory/document_store.py deleted file mode 100644 index f00c4199bd..0000000000 --- a/haystack/preview/document_stores/in_memory/document_store.py +++ /dev/null @@ -1,328 +0,0 @@ -import re -from typing import Literal, Any, Dict, List, Optional, Iterable - -import logging - -import numpy as np -import rank_bm25 -from tqdm.auto import tqdm - -from haystack.preview import default_from_dict, default_to_dict -from haystack.preview.document_stores.decorator import document_store -from haystack.preview.dataclasses import Document -from haystack.preview.document_stores.protocols import DuplicatePolicy -from haystack.preview.utils.filters import document_matches_filter, convert -from haystack.preview.document_stores.errors import DuplicateDocumentError, DocumentStoreError -from haystack.preview.utils import expit - -logger = logging.getLogger(__name__) - -# document scores are essentially unbounded and will be scaled to values between 0 and 1 if scale_score is set to -# True (default). Scaling uses the expit function (inverse of the logit function) after applying a scaling factor -# (e.g., BM25_SCALING_FACTOR for the bm25_retrieval method). -# Larger scaling factor decreases scaled scores. For example, an input of 10 is scaled to 0.99 with BM25_SCALING_FACTOR=2 -# but to 0.78 with BM25_SCALING_FACTOR=8 (default). The defaults were chosen empirically. Increase the default if most -# unscaled scores are larger than expected (>30) and otherwise would incorrectly all be mapped to scores ~1. -BM25_SCALING_FACTOR = 8 -DOT_PRODUCT_SCALING_FACTOR = 100 - - -@document_store -class InMemoryDocumentStore: - """ - Stores data in-memory. It's ephemeral and cannot be saved to disk. - """ - - def __init__( - self, - bm25_tokenization_regex: str = r"(?u)\b\w\w+\b", - bm25_algorithm: Literal["BM25Okapi", "BM25L", "BM25Plus"] = "BM25Okapi", - bm25_parameters: Optional[Dict] = None, - embedding_similarity_function: Literal["dot_product", "cosine"] = "dot_product", - ): - """ - Initializes the DocumentStore. - - :param bm25_tokenization_regex: The regular expression used to tokenize the text for BM25 retrieval. - :param bm25_algorithm: The BM25 algorithm to use. One of "BM25Okapi", "BM25L", or "BM25Plus". - :param bm25_parameters: Parameters for BM25 implementation in a dictionary format. - For example: {'k1':1.5, 'b':0.75, 'epsilon':0.25} - You can learn more about these parameters by visiting https://github.com/dorianbrown/rank_bm25. - By default, no parameters are set. - :param embedding_similarity_function: The similarity function used to compare Documents embeddings. - One of "dot_product" (default) or "cosine". - To choose the most appropriate function, look for information about your embedding model. - """ - self.storage: Dict[str, Document] = {} - self._bm25_tokenization_regex = bm25_tokenization_regex - self.tokenizer = re.compile(bm25_tokenization_regex).findall - algorithm_class = getattr(rank_bm25, bm25_algorithm) - if algorithm_class is None: - raise ValueError(f"BM25 algorithm '{bm25_algorithm}' not found.") - self.bm25_algorithm = algorithm_class - self.bm25_parameters = bm25_parameters or {} - self.embedding_similarity_function = embedding_similarity_function - - def to_dict(self) -> Dict[str, Any]: - """ - Serializes this store to a dictionary. - """ - return default_to_dict( - self, - bm25_tokenization_regex=self._bm25_tokenization_regex, - bm25_algorithm=self.bm25_algorithm.__name__, - bm25_parameters=self.bm25_parameters, - embedding_similarity_function=self.embedding_similarity_function, - ) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "InMemoryDocumentStore": - """ - Deserializes the store from a dictionary. - """ - return default_from_dict(cls, data) - - def count_documents(self) -> int: - """ - Returns the number of how many documents are present in the DocumentStore. - """ - return len(self.storage.keys()) - - def filter_documents(self, filters: Optional[Dict[str, Any]] = None) -> List[Document]: - """ - Returns the documents that match the filters provided. - - For a detailed specification of the filters, refer to the DocumentStore.filter_documents() protocol documentation. - - :param filters: The filters to apply to the document list. - :return: A list of Documents that match the given filters. - """ - if filters: - if "operator" not in filters: - filters = convert(filters) - return [doc for doc in self.storage.values() if document_matches_filter(filters=filters, document=doc)] - return list(self.storage.values()) - - def write_documents(self, documents: List[Document], policy: DuplicatePolicy = DuplicatePolicy.FAIL) -> int: - """ - Writes (or overwrites) documents into the DocumentStore. - - :param documents: A list of documents. - :param policy: Documents with the same ID count as duplicates. When duplicates are met, - the DocumentStore can: - - skip: keep the existing document and ignore the new one. - - overwrite: remove the old document and write the new one. - - fail: an error is raised. - :raises DuplicateError: Exception trigger on duplicate document if `policy=DuplicatePolicy.FAIL` - :return: None - """ - if ( - not isinstance(documents, Iterable) - or isinstance(documents, str) - or any(not isinstance(doc, Document) for doc in documents) - ): - raise ValueError("Please provide a list of Documents.") - - written_documents = len(documents) - for document in documents: - if policy != DuplicatePolicy.OVERWRITE and document.id in self.storage.keys(): - if policy == DuplicatePolicy.FAIL: - raise DuplicateDocumentError(f"ID '{document.id}' already exists.") - if policy == DuplicatePolicy.SKIP: - logger.warning("ID '%s' already exists", document.id) - written_documents -= 1 - continue - self.storage[document.id] = document - return written_documents - - def delete_documents(self, document_ids: List[str]) -> None: - """ - Deletes all documents with matching document_ids from the DocumentStore. - :param object_ids: The object_ids to delete. - """ - for doc_id in document_ids: - if doc_id not in self.storage.keys(): - continue - del self.storage[doc_id] - - def bm25_retrieval( - self, query: str, filters: Optional[Dict[str, Any]] = None, top_k: int = 10, scale_score: bool = False - ) -> List[Document]: - """ - Retrieves documents that are most relevant to the query using BM25 algorithm. - - :param query: The query string. - :param filters: A dictionary with filters to narrow down the search space. - :param top_k: The number of top documents to retrieve. Default is 10. - :param scale_score: Whether to scale the scores of the retrieved documents. Default is False. - :return: A list of the top_k documents most relevant to the query. - """ - if not query: - raise ValueError("Query should be a non-empty string") - - content_type_filter = { - "operator": "OR", - "conditions": [ - {"field": "content", "operator": "!=", "value": None}, - {"field": "dataframe", "operator": "!=", "value": None}, - ], - } - if filters: - if "operator" not in filters: - filters = convert(filters) - filters = {"operator": "AND", "conditions": [content_type_filter, filters]} - else: - filters = content_type_filter - all_documents = self.filter_documents(filters=filters) - - # Lowercase all documents - lower_case_documents = [] - for doc in all_documents: - if doc.content is None and doc.dataframe is None: - logger.info("Document '%s' has no text or dataframe content. Skipping it.", doc.id) - else: - if doc.content is not None: - lower_case_documents.append(doc.content.lower()) - if doc.dataframe is not None: - logger.warning( - "Document '%s' has both text and dataframe content. " - "Using text content and skipping dataframe content.", - doc.id, - ) - continue - if doc.dataframe is not None: - str_content = doc.dataframe.astype(str) - csv_content = str_content.to_csv(index=False) - lower_case_documents.append(csv_content.lower()) - - # Tokenize the entire content of the DocumentStore - tokenized_corpus = [ - self.tokenizer(doc) for doc in tqdm(lower_case_documents, unit=" docs", desc="Ranking by BM25...") - ] - if len(tokenized_corpus) == 0: - logger.info("No documents found for BM25 retrieval. Returning empty list.") - return [] - - # initialize BM25 - bm25_scorer = self.bm25_algorithm(tokenized_corpus, **self.bm25_parameters) - # tokenize query - tokenized_query = self.tokenizer(query.lower()) - # get scores for the query against the corpus - docs_scores = bm25_scorer.get_scores(tokenized_query) - if scale_score: - docs_scores = [expit(float(score / BM25_SCALING_FACTOR)) for score in docs_scores] - # get the last top_k indexes and reverse them - top_docs_positions = np.argsort(docs_scores)[-top_k:][::-1] - - # Create documents with the BM25 score to return them - return_documents = [] - for i in top_docs_positions: - doc = all_documents[i] - doc_fields = doc.to_dict() - doc_fields["score"] = docs_scores[i] - return_document = Document.from_dict(doc_fields) - return_documents.append(return_document) - return return_documents - - def embedding_retrieval( - self, - query_embedding: List[float], - filters: Optional[Dict[str, Any]] = None, - top_k: int = 10, - scale_score: bool = False, - return_embedding: bool = False, - ) -> List[Document]: - """ - Retrieves documents that are most similar to the query embedding using a vector similarity metric. - - :param query_embedding: Embedding of the query. - :param filters: A dictionary with filters to narrow down the search space. - :param top_k: The number of top documents to retrieve. Default is 10. - :param scale_score: Whether to scale the scores of the retrieved Documents. Default is False. - :param return_embedding: Whether to return the embedding of the retrieved Documents. Default is False. - :return: A list of the top_k documents most relevant to the query. - """ - if len(query_embedding) == 0 or not isinstance(query_embedding[0], float): - raise ValueError("query_embedding should be a non-empty list of floats.") - - filters = filters or {} - all_documents = self.filter_documents(filters=filters) - - documents_with_embeddings = [doc for doc in all_documents if doc.embedding is not None] - if len(documents_with_embeddings) == 0: - logger.warning( - "No Documents found with embeddings. Returning empty list. " - "To generate embeddings, use a DocumentEmbedder." - ) - return [] - elif len(documents_with_embeddings) < len(all_documents): - logger.info( - "Skipping some Documents that don't have an embedding. " - "To generate embeddings, use a DocumentEmbedder." - ) - - scores = self._compute_query_embedding_similarity_scores( - embedding=query_embedding, documents=documents_with_embeddings, scale_score=scale_score - ) - - # create Documents with the similarity score for the top k results - top_documents = [] - for doc, score in sorted(zip(documents_with_embeddings, scores), key=lambda x: x[1], reverse=True)[:top_k]: - doc_fields = doc.to_dict() - doc_fields["score"] = score - if return_embedding is False: - doc_fields["embedding"] = None - top_documents.append(Document.from_dict(doc_fields)) - - return top_documents - - def _compute_query_embedding_similarity_scores( - self, embedding: List[float], documents: List[Document], scale_score: bool = False - ) -> List[float]: - """ - Computes the similarity scores between the query embedding and the embeddings of the documents. - - :param embedding: Embedding of the query. - :param documents: A list of Documents. - :param scale_score: Whether to scale the scores of the Documents. Default is False. - :return: A list of scores. - """ - - query_embedding = np.array(embedding) - if query_embedding.ndim == 1: - query_embedding = np.expand_dims(a=query_embedding, axis=0) - - try: - document_embeddings = np.array([doc.embedding for doc in documents]) - except ValueError as e: - if "inhomogeneous shape" in str(e): - raise DocumentStoreError( - "The embedding size of all Documents should be the same. " - "Please make sure that the Documents have been embedded with the same model." - ) from e - raise e - if document_embeddings.ndim == 1: - document_embeddings = np.expand_dims(a=document_embeddings, axis=0) - - if self.embedding_similarity_function == "cosine": - # cosine similarity is a normed dot product - query_embedding /= np.linalg.norm(x=query_embedding, axis=1, keepdims=True) - document_embeddings /= np.linalg.norm(x=document_embeddings, axis=1, keepdims=True) - - try: - scores = np.dot(a=query_embedding, b=document_embeddings.T)[0].tolist() - except ValueError as e: - if "shapes" in str(e) and "not aligned" in str(e): - raise DocumentStoreError( - "The embedding size of the query should be the same as the embedding size of the Documents. " - "Please make sure that the query has been embedded with the same model as the Documents." - ) from e - raise e - - if scale_score: - if self.embedding_similarity_function == "dot_product": - scores = [expit(float(score / DOT_PRODUCT_SCALING_FACTOR)) for score in scores] - elif self.embedding_similarity_function == "cosine": - scores = [(score + 1) / 2 for score in scores] - - return scores diff --git a/haystack/preview/document_stores/protocols.py b/haystack/preview/document_stores/protocols.py deleted file mode 100644 index 6a27f19551..0000000000 --- a/haystack/preview/document_stores/protocols.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import Protocol, Optional, Dict, Any, List -import logging -from enum import Enum - -from haystack.preview.dataclasses import Document - - -# Ellipsis are needed for the type checker, it's safe to disable module-wide -# pylint: disable=unnecessary-ellipsis - -logger = logging.getLogger(__name__) - - -class DuplicatePolicy(Enum): - SKIP = "skip" - OVERWRITE = "overwrite" - FAIL = "fail" - - -class DocumentStore(Protocol): - """ - Stores Documents to be used by the components of a Pipeline. - - Classes implementing this protocol often store the documents permanently and allow specialized components to - perform retrieval on them, either by embedding, by keyword, hybrid, and so on, depending on the backend used. - - In order to retrieve documents, consider using a Retriever that supports the DocumentStore implementation that - you're using. - """ - - def to_dict(self) -> Dict[str, Any]: - """ - Serializes this store to a dictionary. - """ - ... - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "DocumentStore": - """ - Deserializes the store from a dictionary. - """ - ... - - def count_documents(self) -> int: - """ - Returns the number of documents stored. - """ - ... - - def filter_documents(self, filters: Optional[Dict[str, Any]] = None) -> List[Document]: - """ - Returns the documents that match the filters provided. - - Filters are defined as nested dictionaries that can be of two types: - - Comparison - - Logic - - Comparison dictionaries must contain the keys: - - - `field` - - `operator` - - `value` - - Logic dictionaries must contain the keys: - - - `operator` - - `conditions` - - The `conditions` key must be a list of dictionaries, either of type Comparison or Logic. - - The `operator` value in Comparison dictionaries must be one of: - - - `==` - - `!=` - - `>` - - `>=` - - `<` - - `<=` - - `in` - - `not in` - - The `operator` values in Logic dictionaries must be one of: - - - `NOT` - - `OR` - - `AND` - - - A simple filter: - ```python - filters = {"field": "meta.type", "operator": "==", "value": "article"} - ``` - - A more complex filter: - ```python - filters = { - "operator": "AND", - "conditions": [ - {"field": "meta.type", "operator": "==", "value": "article"}, - {"field": "meta.date", "operator": ">=", "value": 1420066800}, - {"field": "meta.date", "operator": "<", "value": 1609455600}, - {"field": "meta.rating", "operator": ">=", "value": 3}, - { - "operator": "OR", - "conditions": [ - {"field": "meta.genre", "operator": "in", "value": ["economy", "politics"]}, - {"field": "meta.publisher", "operator": "==", "value": "nytimes"}, - ], - }, - ], - } - - :param filters: the filters to apply to the document list. - :return: a list of Documents that match the given filters. - """ - ... - - def write_documents(self, documents: List[Document], policy: DuplicatePolicy = DuplicatePolicy.FAIL) -> int: - """ - Writes (or overwrites) documents into the DocumentStore. - - :param documents: a list of documents. - :param policy: documents with the same ID count as duplicates. When duplicates are met, - the DocumentStore can: - - skip: keep the existing document and ignore the new one. - - overwrite: remove the old document and write the new one. - - fail: an error is raised - :raises DuplicateError: Exception trigger on duplicate document if `policy=DuplicatePolicy.FAIL` - :return: The number of documents that was written. - If DuplicatePolicy.OVERWRITE is used, this number is always equal to the number of documents in input. - If DuplicatePolicy.SKIP is used, this number can be lower than the number of documents in the input list. - """ - ... - - def delete_documents(self, document_ids: List[str]) -> None: - """ - Deletes all documents with a matching document_ids from the DocumentStore. - Fails with `MissingDocumentError` if no document with this id is present in the DocumentStore. - - :param object_ids: the object_ids to delete - """ - ... diff --git a/haystack/preview/errors.py b/haystack/preview/errors.py deleted file mode 100644 index c7a6c47d6d..0000000000 --- a/haystack/preview/errors.py +++ /dev/null @@ -1,2 +0,0 @@ -class FilterError(Exception): - pass diff --git a/haystack/preview/lazy_imports.py b/haystack/preview/lazy_imports.py deleted file mode 100644 index 5f474beef9..0000000000 --- a/haystack/preview/lazy_imports.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional, Type -from types import TracebackType -from lazy_imports.try_import import _DeferredImportExceptionContextManager - - -DEFAULT_IMPORT_ERROR_MSG = "Try 'pip install {}'" - - -class LazyImport(_DeferredImportExceptionContextManager): - """ - Wrapper on top of lazy_import's _DeferredImportExceptionContextManager that adds the possibility to customize the - error messages. - """ - - def __init__(self, message: str = DEFAULT_IMPORT_ERROR_MSG) -> None: - super().__init__() - self.import_error_msg = message - - def __exit__( - self, exc_type: Optional[Type[Exception]], exc_value: Optional[Exception], traceback: Optional[TracebackType] - ) -> Optional[bool]: - """Exit the context manager. - - Args: - exc_type: - Raised exception type. :obj:`None` if nothing is raised. - exc_value: - Raised exception object. :obj:`None` if nothing is raised. - traceback: - Associated traceback. :obj:`None` if nothing is raised. - - Returns: - :obj:`None` if nothing is deferred, otherwise :obj:`True`. - :obj:`True` will suppress any exceptions avoiding them from propagating. - - """ - if isinstance(exc_value, ImportError): - message = ( - f"Failed to import '{exc_value.name}'. {self.import_error_msg.format(exc_value.name)}. " - f"Original error: {exc_value}" - ) - self._deferred = (exc_value, message) - return True - return None diff --git a/haystack/preview/marshal/__init__.py b/haystack/preview/marshal/__init__.py deleted file mode 100644 index f737be0574..0000000000 --- a/haystack/preview/marshal/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from haystack.preview.marshal.protocol import Marshaller -from haystack.preview.marshal.yaml import YamlMarshaller - -__all__ = ["Marshaller", "YamlMarshaller"] diff --git a/haystack/preview/marshal/protocol.py b/haystack/preview/marshal/protocol.py deleted file mode 100644 index 06663b7534..0000000000 --- a/haystack/preview/marshal/protocol.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Protocol, Dict, Any, Union - - -class Marshaller(Protocol): - def marshal(self, dict_: Dict[str, Any]) -> str: - ... - - def unmarshal(self, data_: Union[str, bytes, bytearray]) -> Dict[str, Any]: - ... diff --git a/haystack/preview/marshal/yaml.py b/haystack/preview/marshal/yaml.py deleted file mode 100644 index 5fca27fb6f..0000000000 --- a/haystack/preview/marshal/yaml.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Dict, Any, Union - -import yaml - - -class YamlMarshaller: - def marshal(self, dict_: Dict[str, Any]) -> str: - return yaml.dump(dict_) - - def unmarshal(self, data_: Union[str, bytes, bytearray]) -> Dict[str, Any]: - return yaml.safe_load(data_) diff --git a/haystack/preview/pipeline.py b/haystack/preview/pipeline.py deleted file mode 100644 index 275277295e..0000000000 --- a/haystack/preview/pipeline.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Any, Dict, Optional, Union, TextIO -from pathlib import Path -import datetime -import logging -import canals - -from haystack.preview.telemetry import pipeline_running -from haystack.preview.marshal import Marshaller, YamlMarshaller - - -DEFAULT_MARSHALLER = YamlMarshaller() -logger = logging.getLogger(__name__) - - -class Pipeline(canals.Pipeline): - def __init__( - self, - metadata: Optional[Dict[str, Any]] = None, - max_loops_allowed: int = 100, - debug_path: Union[Path, str] = Path(".haystack_debug/"), - ): - """ - Creates the Pipeline. - - Args: - metadata: arbitrary dictionary to store metadata about this pipeline. Make sure all the values contained in - this dictionary can be serialized and deserialized if you wish to save this pipeline to file with - `save_pipelines()/load_pipelines()`. - max_loops_allowed: how many times the pipeline can run the same node before throwing an exception. - debug_path: when debug is enabled in `run()`, where to save the debug data. - """ - self._telemetry_runs = 0 - self._last_telemetry_sent: Optional[datetime.datetime] = None - super().__init__(metadata=metadata, max_loops_allowed=max_loops_allowed, debug_path=debug_path) - - def run(self, data: Dict[str, Any], debug: bool = False) -> Dict[str, Any]: - """ - Runs the pipeline. - - :params data: the inputs to give to the input components of the Pipeline. - :params debug: whether to collect and return debug information. - - :returns: A dictionary with the outputs of the output components of the Pipeline. - - :raises PipelineRuntimeError: if the any of the components fail or return unexpected output. - """ - pipeline_running(self) - return super().run(data=data, debug=debug) - - def dumps(self, marshaller: Marshaller = DEFAULT_MARSHALLER) -> str: - """ - Returns the string representation of this pipeline according to the - format dictated by the `Marshaller` in use. - - :params marshaller: The Marshaller used to create the string representation. Defaults to - `YamlMarshaller` - - :returns: A string representing the pipeline. - """ - return marshaller.marshal(self.to_dict()) - - def dump(self, fp: TextIO, marshaller: Marshaller = DEFAULT_MARSHALLER): - """ - Writes the string representation of this pipeline to the file-like object - passed in the `fp` argument. - - :params fp: A file-like object ready to be written to. - :params marshaller: The Marshaller used to create the string representation. Defaults to - `YamlMarshaller`. - """ - fp.write(marshaller.marshal(self.to_dict())) - - @classmethod - def loads(cls, data: Union[str, bytes, bytearray], marshaller: Marshaller = DEFAULT_MARSHALLER) -> "Pipeline": - """ - Creates a `Pipeline` object from the string representation passed in the `data` argument. - - :params data: The string representation of the pipeline, can be `str`, `bytes` or `bytearray`. - :params marshaller: the Marshaller used to create the string representation. Defaults to - `YamlMarshaller` - - :returns: A `Pipeline` object. - """ - return cls.from_dict(marshaller.unmarshal(data)) - - @classmethod - def load(cls, fp: TextIO, marshaller: Marshaller = DEFAULT_MARSHALLER) -> "Pipeline": - """ - Creates a `Pipeline` object from the string representation read from the file-like - object passed in the `fp` argument. - - :params data: The string representation of the pipeline, can be `str`, `bytes` or `bytearray`. - :params fp: A file-like object ready to be read from. - :params marshaller: the Marshaller used to create the string representation. Defaults to - `YamlMarshaller` - - :returns: A `Pipeline` object. - """ - return cls.from_dict(marshaller.unmarshal(fp.read())) diff --git a/haystack/preview/telemetry/__init__.py b/haystack/preview/telemetry/__init__.py deleted file mode 100644 index be32ab8102..0000000000 --- a/haystack/preview/telemetry/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from haystack.preview.telemetry._telemetry import pipeline_running, tutorial_running diff --git a/haystack/preview/telemetry/_environment.py b/haystack/preview/telemetry/_environment.py deleted file mode 100644 index c450c19320..0000000000 --- a/haystack/preview/telemetry/_environment.py +++ /dev/null @@ -1,106 +0,0 @@ -# pylint: disable=global-statement -import logging -import os -import platform -import sys -from typing import Optional, Dict, Any - -from haystack.preview.version import __version__ - -logger = logging.getLogger(__name__) - - -# This value cannot change during the lifetime of the process -_IS_DOCKER_CACHE = None - - -def _in_podman() -> bool: - """ - Podman run would create the file /run/.containernv, see: - https://github.com/containers/podman/blob/main/docs/source/markdown/podman-run.1.md.in#L31 - """ - return os.path.exists("/run/.containerenv") - - -def _has_dockerenv() -> bool: - """ - This might not work anymore at some point (even if it's been a while now), see: - https://github.com/moby/moby/issues/18355#issuecomment-220484748 - """ - return os.path.exists("/.dockerenv") - - -def _has_docker_cgroup_v1() -> bool: - """ - This only works with cgroups v1 - """ - path = "/proc/self/cgroup" # 'self' should be always symlinked to the actual PID - return os.path.isfile(path) and any("docker" in line for line in open(path)) - - -def _has_docker_cgroup_v2() -> bool: - """ - cgroups v2 version, inspired from - https://github.com/jenkinsci/docker-workflow-plugin/blob/master/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java - """ - path = "/proc/self/mountinfo" # 'self' should be always symlinked to the actual PID - return os.path.isfile(path) and any("/docker/containers/" in line for line in open(path)) - - -def _is_containerized() -> Optional[bool]: - """ - This code is based on the popular 'is-docker' package for node.js - """ - global _IS_DOCKER_CACHE - - if _IS_DOCKER_CACHE is None: - _IS_DOCKER_CACHE = _in_podman() or _has_dockerenv() or _has_docker_cgroup_v1() or _has_docker_cgroup_v2() - - return _IS_DOCKER_CACHE - - -def collect_system_specs() -> Dict[str, Any]: - """ - Collects meta data about the setup that is used with Haystack, such as: - operating system, python version, Haystack version, transformers version, - pytorch version, number of GPUs, execution environment. - - These values are highly unlikely to change during the runtime of the pipeline, - so they're collected only once. - """ - specs = { - "libraries.haystack": __version__, - "os.containerized": _is_containerized(), - "os.version": platform.release(), - "os.family": platform.system(), - "os.machine": platform.machine(), - "python.version": platform.python_version(), - "hardware.cpus": os.cpu_count(), - "hardware.gpus": 0, - "libraries.transformers": False, - "libraries.torch": False, - "libraries.cuda": False, - "libraries.pytest": sys.modules["pytest"].__version__ if "pytest" in sys.modules.keys() else False, - "libraries.ipython": sys.modules["ipython"].__version__ if "ipython" in sys.modules.keys() else False, - "libraries.colab": sys.modules["google.colab"].__version__ if "google.colab" in sys.modules.keys() else False, - } - - # Try to find out transformer's version - try: - import transformers - - specs["libraries.transformers"] = transformers.__version__ - except ImportError: - pass - - # Try to find out torch's version and info on potential GPU(s) - try: - import torch - - specs["libraries.torch"] = torch.__version__ - if torch.cuda.is_available(): - specs["libraries.cuda"] = torch.version.cuda - specs["libraries.gpus"] = torch.cuda.device_count() - except ImportError: - pass - return specs diff --git a/haystack/preview/telemetry/_telemetry.py b/haystack/preview/telemetry/_telemetry.py deleted file mode 100644 index 24a0e9d9db..0000000000 --- a/haystack/preview/telemetry/_telemetry.py +++ /dev/null @@ -1,171 +0,0 @@ -from typing import Any, Dict, Optional, TYPE_CHECKING, List, Tuple -import os -from pathlib import Path -from collections import defaultdict -import datetime -import logging -import uuid -import yaml -import posthog - -from haystack.preview.telemetry._environment import collect_system_specs - -if TYPE_CHECKING: - from haystack.preview.pipeline import Pipeline - - -HAYSTACK_TELEMETRY_ENABLED = "HAYSTACK_TELEMETRY_ENABLED" -CONFIG_PATH = Path("~/.haystack/config.yaml").expanduser() - -#: Telemetry sends at most one event every number of seconds specified in this constant -MIN_SECONDS_BETWEEN_EVENTS = 60 - - -logger = logging.getLogger(__name__) - - -class Telemetry: - """ - Haystack reports anonymous usage statistics to support continuous software improvements for all its users. - - You can opt-out of sharing usage statistics by manually setting the environment - variable `HAYSTACK_TELEMETRY_ENABLED` as described for different operating systems on the - [documentation page](https://docs.haystack.deepset.ai/docs/telemetry#how-can-i-opt-out). - - Check out the documentation for more details: [Telemetry](https://docs.haystack.deepset.ai/docs/telemetry). - """ - - def __init__(self): - """ - Initializes the telemetry. Loads the user_id from the config file, - or creates a new id and saves it if the file is not found. - - It also collects system information which cannot change across the lifecycle - of the process (for example `is_containerized()`). - """ - - # disable posthog logging - for module_name in ["posthog", "backoff"]: - logging.getLogger(module_name).setLevel(logging.CRITICAL) - # Prevent module from sending errors to stderr when an exception is encountered during an emit() call - logging.getLogger(module_name).addHandler(logging.NullHandler()) - logging.getLogger(module_name).propagate = False - - self.user_id = None - - if CONFIG_PATH.exists(): - # Load the config file - try: - with open(CONFIG_PATH, "r", encoding="utf-8") as config_file: - config = yaml.safe_load(config_file) - if "user_id" in config: - self.user_id = config["user_id"] - except Exception as e: - logger.debug("Telemetry could not read the config file %s", CONFIG_PATH, exc_info=e) - else: - # Create the config file - logger.info( - "Haystack sends anonymous usage data to understand the actual usage and steer dev efforts " - "towards features that are most meaningful to users. You can opt-out at anytime by manually " - "setting the environment variable HAYSTACK_TELEMETRY_ENABLED as described for different " - "operating systems in the [documentation page](https://docs.haystack.deepset.ai/docs/telemetry#how-can-i-opt-out). " - "More information at [Telemetry](https://docs.haystack.deepset.ai/docs/telemetry)." - ) - CONFIG_PATH.parents[0].mkdir(parents=True, exist_ok=True) - self.user_id = str(uuid.uuid4()) - try: - with open(CONFIG_PATH, "w") as outfile: - yaml.dump({"user_id": self.user_id}, outfile, default_flow_style=False) - except Exception as e: - logger.debug("Telemetry could not write config file to %s", CONFIG_PATH, exc_info=e) - - self.event_properties = collect_system_specs() - - def send_event(self, event_name: str, event_properties: Optional[Dict[str, Any]] = None): - """ - Sends a telemetry event. - - :param event_name: The name of the event to show in PostHog. - :param event_properties: Additional event metadata. These are merged with the - system metadata collected in __init__, so take care not to overwrite them. - """ - event_properties = event_properties or {} - try: - posthog.capture( - distinct_id=self.user_id, event=event_name, properties={**self.event_properties, **event_properties} - ) - except Exception as e: - logger.debug("Telemetry couldn't make a POST request to PostHog.", exc_info=e) - - -def send_telemetry(func): - """ - Decorator that sends the output of the wrapped function to PostHog. - The wrapped function is actually called only if telemetry is enabled. - """ - - # FIXME? Somehow, functools.wraps makes `telemetry` out of scope. Let's take care of it later. - def send_telemetry_wrapper(*args, **kwargs): - try: - if telemetry: - output = func(*args, **kwargs) - if output: - telemetry.send_event(*output) - except Exception as e: - # Never let telemetry break things - logger.debug("There was an issue sending a telemetry event", exc_info=e) - - return send_telemetry_wrapper - - -@send_telemetry -def pipeline_running(pipeline: "Pipeline") -> Optional[Tuple[str, Dict[str, Any]]]: - """ - Collects name, type and the content of the _telemetry_data attribute, if present, for each component in the - pipeline and sends such data to Posthog. - - :param pipeline: the pipeline that is running. - """ - pipeline._telemetry_runs += 1 - if ( - pipeline._last_telemetry_sent - and (datetime.datetime.now() - pipeline._last_telemetry_sent).seconds < MIN_SECONDS_BETWEEN_EVENTS - ): - return None - - pipeline._last_telemetry_sent = datetime.datetime.now() - - # Collect info about components - pipeline_description = pipeline.to_dict() - components: Dict[str, List[Dict[str, Any]]] = defaultdict(list) - for component_name, component in pipeline_description["components"].items(): - instance = pipeline.get_component(component_name) - if hasattr(instance, "_get_telemetry_data"): - telemetry_data = getattr(instance, "_get_telemetry_data")() - try: - components[component["type"]].append({"name": component_name, **telemetry_data}) - except TypeError: - components[component["type"]].append({"name": component_name}) - else: - components[component["type"]].append({"name": component_name}) - - # Data sent to Posthog - return "Pipeline run (2.x)", { - "pipeline_id": str(id(pipeline)), - "runs": pipeline._telemetry_runs, - "components": components, - } - - -@send_telemetry -def tutorial_running(tutorial_id: str) -> Tuple[str, Dict[str, Any]]: - """ - Send a telemetry event for a tutorial, if telemetry is enabled. - :param tutorial_id: identifier of the tutorial - """ - return "Tutorial", {"tutorial.id": tutorial_id} - - -telemetry = None -if os.getenv("HAYSTACK_TELEMETRY_ENABLED", "true").lower() in ("true", "1"): - telemetry = Telemetry() diff --git a/haystack/preview/testing/__init__.py b/haystack/preview/testing/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/haystack/preview/testing/document_store.py b/haystack/preview/testing/document_store.py deleted file mode 100644 index bb8c1f15fb..0000000000 --- a/haystack/preview/testing/document_store.py +++ /dev/null @@ -1,866 +0,0 @@ -# pylint: disable=too-many-public-methods -from typing import List -import random - -import pytest -import pandas as pd - -from haystack.preview.dataclasses import Document -from haystack.preview.document_stores import DocumentStore, DuplicatePolicy -from haystack.preview.document_stores.errors import DuplicateDocumentError -from haystack.preview.errors import FilterError - - -def _random_embeddings(n): - return [random.random() for _ in range(n)] - - -# These are random embedding that are used to test filters. -# We declare them here as they're used both in the `filterable_docs` fixture -# and the body of several `filter_documents` tests. -TEST_EMBEDDING_1 = _random_embeddings(768) -TEST_EMBEDDING_2 = _random_embeddings(768) - - -class CountDocumentsTest: - """ - Utility class to test a Document Store `count_documents` method. - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(CountDocumentsTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_count_empty(self, document_store: DocumentStore): - assert document_store.count_documents() == 0 - - @pytest.mark.unit - def test_count_not_empty(self, document_store: DocumentStore): - document_store.write_documents( - [Document(content="test doc 1"), Document(content="test doc 2"), Document(content="test doc 3")] - ) - assert document_store.count_documents() == 3 - - -class WriteDocumentsTest: - """ - Utility class to test a Document Store `write_documents` method. - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - The Document Store `filter_documents` method must be at least partly implemented to return all stored Documents - for this tests to work correctly. - Example usage: - - ```python - class MyDocumentStoreTest(WriteDocumentsTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_write_documents(self, document_store: DocumentStore): - """ - Test write_documents() normal behaviour. - """ - doc = Document(content="test doc") - assert document_store.write_documents([doc]) == 1 - assert document_store.filter_documents() == [doc] - - @pytest.mark.unit - def test_write_documents_duplicate_fail(self, document_store: DocumentStore): - """ - Test write_documents() fails when trying to write Document with same id - using DuplicatePolicy.FAIL. - """ - doc = Document(content="test doc") - assert document_store.write_documents([doc]) == 1 - with pytest.raises(DuplicateDocumentError): - document_store.write_documents(documents=[doc], policy=DuplicatePolicy.FAIL) - assert document_store.filter_documents() == [doc] - - @pytest.mark.unit - def test_write_documents_duplicate_skip(self, document_store: DocumentStore): - """ - Test write_documents() skips Document when trying to write one with same id - using DuplicatePolicy.SKIP. - """ - doc = Document(content="test doc") - assert document_store.write_documents([doc]) == 1 - assert document_store.write_documents(documents=[doc], policy=DuplicatePolicy.SKIP) == 0 - - @pytest.mark.unit - def test_write_documents_duplicate_overwrite(self, document_store: DocumentStore): - """ - Test write_documents() overwrites stored Document when trying to write one with same id - using DuplicatePolicy.OVERWRITE. - """ - doc1 = Document(id="1", content="test doc 1") - doc2 = Document(id="1", content="test doc 2") - - assert document_store.write_documents([doc2]) == 1 - assert document_store.filter_documents() == [doc2] - assert document_store.write_documents(documents=[doc1], policy=DuplicatePolicy.OVERWRITE) == 1 - assert document_store.filter_documents() == [doc1] - - @pytest.mark.unit - def test_write_documents_invalid_input(self, document_store: DocumentStore): - """ - Test write_documents() fails when providing unexpected input. - """ - with pytest.raises(ValueError): - document_store.write_documents(["not a document for sure"]) # type: ignore - with pytest.raises(ValueError): - document_store.write_documents("not a list actually") # type: ignore - - -class DeleteDocumentsTest: - """ - Utility class to test a Document Store `delete_documents` method. - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - The Document Store `write_documents` and `count_documents` methods must be implemented for this tests to work correctly. - Example usage: - - ```python - class MyDocumentStoreTest(DeleteDocumentsTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_delete_documents(self, document_store: DocumentStore): - """ - Test delete_documents() normal behaviour. - """ - doc = Document(content="test doc") - document_store.write_documents([doc]) - assert document_store.count_documents() == 1 - - document_store.delete_documents([doc.id]) - assert document_store.count_documents() == 0 - - @pytest.mark.unit - def test_delete_documents_empty_document_store(self, document_store: DocumentStore): - """ - Test delete_documents() doesn't fail when called using an empty Document Store. - """ - document_store.delete_documents(["non_existing_id"]) - - @pytest.mark.unit - def test_delete_documents_non_existing_document(self, document_store: DocumentStore): - """ - Test delete_documents() doesn't delete any Document when called with non existing id. - """ - doc = Document(content="test doc") - document_store.write_documents([doc]) - assert document_store.count_documents() == 1 - - document_store.delete_documents(["non_existing_id"]) - - # No Document has been deleted - assert document_store.count_documents() == 1 - - -class FilterableDocsFixtureMixin: - """ - Mixin class that adds a filterable_docs() fixture to a test class. - """ - - @pytest.fixture - def filterable_docs(self) -> List[Document]: - documents = [] - for i in range(3): - documents.append( - Document( - content=f"A Foo Document {i}", - meta={"name": f"name_{i}", "page": "100", "chapter": "intro", "number": 2}, - embedding=_random_embeddings(768), - ) - ) - documents.append( - Document( - content=f"A Bar Document {i}", - meta={"name": f"name_{i}", "page": "123", "chapter": "abstract", "number": -2}, - embedding=_random_embeddings(768), - ) - ) - documents.append( - Document( - content=f"A Foobar Document {i}", - meta={"name": f"name_{i}", "page": "90", "chapter": "conclusion", "number": -10}, - embedding=_random_embeddings(768), - ) - ) - documents.append( - Document( - content=f"Document {i} without embedding", - meta={"name": f"name_{i}", "no_embedding": True, "chapter": "conclusion"}, - ) - ) - documents.append(Document(dataframe=pd.DataFrame([i]), meta={"name": f"table_doc_{i}"})) - documents.append( - Document(content=f"Doc {i} with zeros emb", meta={"name": "zeros_doc"}, embedding=TEST_EMBEDDING_1) - ) - documents.append( - Document(content=f"Doc {i} with ones emb", meta={"name": "ones_doc"}, embedding=TEST_EMBEDDING_2) - ) - return documents - - -class LegacyFilterDocumentsInvalidFiltersTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using invalid legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsInvalidFiltersTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_incorrect_filter_type(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(ValueError): - document_store.filter_documents(filters="something odd") # type: ignore - - @pytest.mark.unit - def test_incorrect_filter_nesting(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"number": {"page": "100"}}) - - @pytest.mark.unit - def test_deeper_incorrect_filter_nesting(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"number": {"page": {"chapter": "intro"}}}) - - -class LegacyFilterDocumentsEqualTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using implicit and explicit '$eq' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsEqualTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_filter_document_content(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"content": "A Foo Document 1"}) - assert result == [doc for doc in filterable_docs if doc.content == "A Foo Document 1"] - - @pytest.mark.unit - def test_filter_simple_metadata_value(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": "100"}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") == "100"] - - @pytest.mark.unit - def test_filter_document_dataframe(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"dataframe": pd.DataFrame([1])}) - assert result == [ - doc for doc in filterable_docs if doc.dataframe is not None and doc.dataframe.equals(pd.DataFrame([1])) - ] - - @pytest.mark.unit - def test_eq_filter_explicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": {"$eq": "100"}}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") == "100"] - - @pytest.mark.unit - def test_eq_filter_implicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": "100"}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") == "100"] - - @pytest.mark.unit - def test_eq_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"dataframe": pd.DataFrame([1])}) - assert result == [ - doc - for doc in filterable_docs - if isinstance(doc.dataframe, pd.DataFrame) and doc.dataframe.equals(pd.DataFrame([1])) - ] - - @pytest.mark.unit - def test_eq_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - embedding = [0.0] * 768 - result = document_store.filter_documents(filters={"embedding": embedding}) - assert result == [doc for doc in filterable_docs if embedding == doc.embedding] - - -class LegacyFilterDocumentsNotEqualTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$ne' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsNotEqualTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_ne_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": {"$ne": "100"}}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") != "100"] - - @pytest.mark.unit - def test_ne_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"dataframe": {"$ne": pd.DataFrame([1])}}) - assert result == [ - doc - for doc in filterable_docs - if not isinstance(doc.dataframe, pd.DataFrame) or not doc.dataframe.equals(pd.DataFrame([1])) - ] - - @pytest.mark.unit - def test_ne_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"embedding": {"$ne": TEST_EMBEDDING_1}}) - assert result == [doc for doc in filterable_docs if doc.embedding != TEST_EMBEDDING_1] - - -class LegacyFilterDocumentsInTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using implicit and explicit '$in' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsInTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_filter_simple_list_single_element(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": ["100"]}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") == "100"] - - @pytest.mark.unit - def test_filter_simple_list_one_value(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": ["100"]}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") in ["100"]] - - @pytest.mark.unit - def test_filter_simple_list(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": ["100", "123"]}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") in ["100", "123"]] - - @pytest.mark.unit - def test_incorrect_filter_name(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"non_existing_meta_field": ["whatever"]}) - assert len(result) == 0 - - @pytest.mark.unit - def test_incorrect_filter_value(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": ["nope"]}) - assert len(result) == 0 - - @pytest.mark.unit - def test_in_filter_explicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": {"$in": ["100", "123", "n.a."]}}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") in ["100", "123"]] - - @pytest.mark.unit - def test_in_filter_implicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": ["100", "123", "n.a."]}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") in ["100", "123"]] - - @pytest.mark.unit - def test_in_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"dataframe": {"$in": [pd.DataFrame([1]), pd.DataFrame([2])]}}) - assert result == [ - doc - for doc in filterable_docs - if isinstance(doc.dataframe, pd.DataFrame) - and (doc.dataframe.equals(pd.DataFrame([1])) or doc.dataframe.equals(pd.DataFrame([2]))) - ] - - @pytest.mark.unit - def test_in_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - embedding_zero = [0.0] * 768 - embedding_one = [1.0] * 768 - result = document_store.filter_documents(filters={"embedding": {"$in": [embedding_zero, embedding_one]}}) - assert result == [ - doc for doc in filterable_docs if (embedding_zero == doc.embedding or embedding_one == doc.embedding) - ] - - -class LegacyFilterDocumentsNotInTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$nin' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsNotInTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_nin_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents( - filters={"dataframe": {"$nin": [pd.DataFrame([1]), pd.DataFrame([0])]}} - ) - assert result == [ - doc - for doc in filterable_docs - if not isinstance(doc.dataframe, pd.DataFrame) - or (not doc.dataframe.equals(pd.DataFrame([1])) and not doc.dataframe.equals(pd.DataFrame([0]))) - ] - - @pytest.mark.unit - def test_nin_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"embedding": {"$nin": [TEST_EMBEDDING_1, TEST_EMBEDDING_2]}}) - assert result == [doc for doc in filterable_docs if doc.embedding not in [TEST_EMBEDDING_1, TEST_EMBEDDING_2]] - - @pytest.mark.unit - def test_nin_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"page": {"$nin": ["100", "123", "n.a."]}}) - assert result == [doc for doc in filterable_docs if doc.meta.get("page") not in ["100", "123"]] - - -class LegacyFilterDocumentsGreaterThanTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$gt' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsGreaterThanTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_gt_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$gt": 0.0}}) - assert result == [doc for doc in filterable_docs if "number" in doc.meta and doc.meta["number"] > 0] - - @pytest.mark.unit - def test_gt_filter_non_numeric(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"page": {"$gt": "100"}}) - - @pytest.mark.unit - def test_gt_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"dataframe": {"$gt": pd.DataFrame([[1, 2, 3], [-1, -2, -3]])}}) - - @pytest.mark.unit - def test_gt_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"embedding": {"$gt": TEST_EMBEDDING_1}}) - - -class LegacyFilterDocumentsGreaterThanEqualTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$gte' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsGreaterThanEqualTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_gte_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$gte": -2}}) - assert result == [doc for doc in filterable_docs if "number" in doc.meta and doc.meta["number"] >= -2] - - @pytest.mark.unit - def test_gte_filter_non_numeric(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"page": {"$gte": "100"}}) - - @pytest.mark.unit - def test_gte_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"dataframe": {"$gte": pd.DataFrame([[1, 2, 3], [-1, -2, -3]])}}) - - @pytest.mark.unit - def test_gte_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"embedding": {"$gte": TEST_EMBEDDING_1}}) - - -class LegacyFilterDocumentsLessThanTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$lt' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsLessThanTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_lt_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$lt": 0.0}}) - assert result == [ - doc for doc in filterable_docs if doc.meta.get("number") is not None and doc.meta["number"] < 0 - ] - - @pytest.mark.unit - def test_lt_filter_non_numeric(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"page": {"$lt": "100"}}) - - @pytest.mark.unit - def test_lt_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"dataframe": {"$lt": pd.DataFrame([[1, 2, 3], [-1, -2, -3]])}}) - - @pytest.mark.unit - def test_lt_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"embedding": {"$lt": TEST_EMBEDDING_2}}) - - -class LegacyFilterDocumentsLessThanEqualTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using explicit '$lte' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsLessThanEqualTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_lte_filter(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$lte": 2.0}}) - assert result == [ - doc for doc in filterable_docs if doc.meta.get("number") is not None and doc.meta["number"] <= 2.0 - ] - - @pytest.mark.unit - def test_lte_filter_non_numeric(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"page": {"$lte": "100"}}) - - @pytest.mark.unit - def test_lte_filter_table(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"dataframe": {"$lte": pd.DataFrame([[1, 2, 3], [-1, -2, -3]])}}) - - @pytest.mark.unit - def test_lte_filter_embedding(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - with pytest.raises(FilterError): - document_store.filter_documents(filters={"embedding": {"$lte": TEST_EMBEDDING_1}}) - - -class LegacyFilterDocumentsSimpleLogicalTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using logical '$and', '$or' and '$not' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsSimpleLogicalTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_filter_simple_or(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters = {"$or": {"name": {"$in": ["name_0", "name_1"]}, "number": {"$lt": 1.0}}} - result = document_store.filter_documents(filters=filters) - assert result == [ - doc - for doc in filterable_docs - if (doc.meta.get("number") is not None and doc.meta["number"] < 1) - or doc.meta.get("name") in ["name_0", "name_1"] - ] - - @pytest.mark.unit - def test_filter_simple_implicit_and_with_multi_key_dict( - self, document_store: DocumentStore, filterable_docs: List[Document] - ): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$lte": 2.0, "$gte": 0.0}}) - assert result == [ - doc - for doc in filterable_docs - if "number" in doc.meta and doc.meta["number"] >= 0.0 and doc.meta["number"] <= 2.0 - ] - - @pytest.mark.unit - def test_filter_simple_explicit_and_with_list(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$and": [{"$lte": 2}, {"$gte": 0}]}}) - assert result == [ - doc - for doc in filterable_docs - if "number" in doc.meta and doc.meta["number"] <= 2.0 and doc.meta["number"] >= 0.0 - ] - - @pytest.mark.unit - def test_filter_simple_implicit_and(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - result = document_store.filter_documents(filters={"number": {"$lte": 2.0, "$gte": 0}}) - assert result == [ - doc - for doc in filterable_docs - if "number" in doc.meta and doc.meta["number"] <= 2.0 and doc.meta["number"] >= 0.0 - ] - - -class LegacyFilterDocumentsNestedLogicalTest(FilterableDocsFixtureMixin): - """ - Utility class to test a Document Store `filter_documents` method using multiple nested logical '$and', '$or' and '$not' legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsNestedLogicalTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_filter_nested_implicit_and(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters_simplified = {"number": {"$lte": 2, "$gte": 0}, "name": ["name_0", "name_1"]} - result = document_store.filter_documents(filters=filters_simplified) - assert result == [ - doc - for doc in filterable_docs - if ( - "number" in doc.meta - and doc.meta["number"] <= 2 - and doc.meta["number"] >= 0 - and doc.meta.get("name") in ["name_0", "name_1"] - ) - ] - - @pytest.mark.unit - def test_filter_nested_or(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters = {"$or": {"name": {"$or": [{"$eq": "name_0"}, {"$eq": "name_1"}]}, "number": {"$lt": 1.0}}} - result = document_store.filter_documents(filters=filters) - assert result == [ - doc - for doc in filterable_docs - if ( - doc.meta.get("name") in ["name_0", "name_1"] - or (doc.meta.get("number") is not None and doc.meta["number"] < 1) - ) - ] - - @pytest.mark.unit - def test_filter_nested_and_or_explicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters_simplified = { - "$and": {"page": {"$eq": "123"}, "$or": {"name": {"$in": ["name_0", "name_1"]}, "number": {"$lt": 1.0}}} - } - result = document_store.filter_documents(filters=filters_simplified) - assert result == [ - doc - for doc in filterable_docs - if ( - doc.meta.get("page") in ["123"] - and (doc.meta.get("name") in ["name_0", "name_1"] or ("number" in doc.meta and doc.meta["number"] < 1)) - ) - ] - - @pytest.mark.unit - def test_filter_nested_and_or_implicit(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters_simplified = { - "page": {"$eq": "123"}, - "$or": {"name": {"$in": ["name_0", "name_1"]}, "number": {"$lt": 1.0}}, - } - result = document_store.filter_documents(filters=filters_simplified) - assert result == [ - doc - for doc in filterable_docs - if ( - doc.meta.get("page") in ["123"] - and (doc.meta.get("name") in ["name_0", "name_1"] or ("number" in doc.meta and doc.meta["number"] < 1)) - ) - ] - - @pytest.mark.unit - def test_filter_nested_or_and(self, document_store: DocumentStore, filterable_docs: List[Document]): - document_store.write_documents(filterable_docs) - filters_simplified = { - "$or": { - "number": {"$lt": 1}, - "$and": {"name": {"$in": ["name_0", "name_1"]}, "$not": {"chapter": {"$eq": "intro"}}}, - } - } - result = document_store.filter_documents(filters=filters_simplified) - assert result == [ - doc - for doc in filterable_docs - if ( - (doc.meta.get("number") is not None and doc.meta["number"] < 1) - or (doc.meta.get("name") in ["name_0", "name_1"] and (doc.meta.get("chapter") != "intro")) - ) - ] - - @pytest.mark.unit - def test_filter_nested_multiple_identical_operators_same_level( - self, document_store: DocumentStore, filterable_docs: List[Document] - ): - document_store.write_documents(filterable_docs) - filters = { - "$or": [ - {"$and": {"name": {"$in": ["name_0", "name_1"]}, "page": "100"}}, - {"$and": {"chapter": {"$in": ["intro", "abstract"]}, "page": "123"}}, - ] - } - result = document_store.filter_documents(filters=filters) - assert result == [ - doc - for doc in filterable_docs - if ( - (doc.meta.get("name") in ["name_0", "name_1"] and doc.meta.get("page") == "100") - or (doc.meta.get("chapter") in ["intro", "abstract"] and doc.meta.get("page") == "123") - ) - ] - - -class LegacyFilterDocumentsTest( # pylint: disable=too-many-ancestors - LegacyFilterDocumentsInvalidFiltersTest, - LegacyFilterDocumentsEqualTest, - LegacyFilterDocumentsNotEqualTest, - LegacyFilterDocumentsInTest, - LegacyFilterDocumentsNotInTest, - LegacyFilterDocumentsGreaterThanTest, - LegacyFilterDocumentsGreaterThanEqualTest, - LegacyFilterDocumentsLessThanTest, - LegacyFilterDocumentsLessThanEqualTest, - LegacyFilterDocumentsSimpleLogicalTest, - LegacyFilterDocumentsNestedLogicalTest, -): - """ - Utility class to test a Document Store `filter_documents` method using different types of legacy filters - - To use it create a custom test class and override the `document_store` fixture to return your Document Store. - Example usage: - - ```python - class MyDocumentStoreTest(LegacyFilterDocumentsTest): - @pytest.fixture - def document_store(self): - return MyDocumentStore() - ``` - """ - - @pytest.mark.unit - def test_no_filter_empty(self, document_store: DocumentStore): - assert document_store.filter_documents() == [] - assert document_store.filter_documents(filters={}) == [] - - @pytest.mark.unit - def test_no_filter_not_empty(self, document_store: DocumentStore): - docs = [Document(content="test doc")] - document_store.write_documents(docs) - assert document_store.filter_documents() == docs - assert document_store.filter_documents(filters={}) == docs - - -class DocumentStoreBaseTests( - CountDocumentsTest, WriteDocumentsTest, DeleteDocumentsTest, LegacyFilterDocumentsTest -): # pylint: disable=too-many-ancestors - @pytest.fixture - def document_store(self) -> DocumentStore: - raise NotImplementedError() diff --git a/haystack/preview/testing/factory.py b/haystack/preview/testing/factory.py deleted file mode 100644 index d36392bfa0..0000000000 --- a/haystack/preview/testing/factory.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Any, Dict, Optional, Tuple, Type, List, Union - -from haystack.preview import default_to_dict, default_from_dict -from haystack.preview.dataclasses import Document -from haystack.preview.document_stores import document_store, DocumentStore, DuplicatePolicy - - -def document_store_class( - name: str, - documents: Optional[List[Document]] = None, - documents_count: Optional[int] = None, - bases: Optional[Tuple[type, ...]] = None, - extra_fields: Optional[Dict[str, Any]] = None, -) -> Type[DocumentStore]: - """ - Utility function to create a DocumentStore class with the given name and list of documents. - - If `documents` is set but `documents_count` is not, `documents_count` will be the length - of `documents`. - If both are set explicitly they don't influence each other. - - `write_documents()` and `delete_documents()` are no-op. - You can override them using `extra_fields`. - - ### Usage - - Create a DocumentStore class that returns no documents: - ```python - MyFakeStore = document_store_class("MyFakeComponent") - document_store = MyFakeStore() - assert document_store.documents_count() == 0 - assert document_store.filter_documents() == [] - ``` - - Create a DocumentStore class that returns a single document: - ```python - doc = Document(id="fake_id", text="Fake content") - MyFakeStore = document_store_class("MyFakeComponent", documents=[doc]) - document_store = MyFakeStore() - assert document_store.documents_count() == 1 - assert document_store.filter_documents() == [doc] - ``` - - Create a DocumentStore class that returns no document but returns a custom count: - ```python - MyFakeStore = document_store_class("MyFakeComponent", documents_count=100) - document_store = MyFakeStore() - assert document_store.documents_count() == 100 - assert document_store.filter_documents() == [] - ``` - - Create a DocumentStore class that returns a document and a custom count: - ```python - doc = Document(id="fake_id", text="Fake content") - MyFakeStore = document_store_class("MyFakeComponent", documents=[doc], documents_count=100) - document_store = MyFakeStore() - assert document_store.documents_count() == 100 - assert document_store.filter_documents() == [doc] - ``` - - Create a DocumentStore class with a custom base class: - ```python - MyFakeStore = document_store_class( - "MyFakeStore", - bases=(MyBaseClass,) - ) - document_store = MyFakeStore() - assert isinstance(store, MyBaseClass) - ``` - - Create a DocumentStore class with an extra field `my_field`: - ```python - MyFakeStore = document_store_class( - "MyFakeStore", - extra_fields={"my_field": 10} - ) - document_store = MyFakeStore() - assert document_store.my_field == 10 - ``` - """ - if documents is not None and documents_count is None: - documents_count = len(documents) - elif documents_count is None: - documents_count = 0 - - def count_documents(self) -> Union[int, None]: - return documents_count - - def filter_documents(self, filters: Optional[Dict[str, Any]] = None) -> List[Document]: - if documents is not None: - return documents - return [] - - def write_documents(self, documents: List[Document], policy: DuplicatePolicy = DuplicatePolicy.FAIL) -> None: - return - - def delete_documents(self, document_ids: List[str]) -> None: - return - - def to_dict(self) -> Dict[str, Any]: - return default_to_dict(self) - - fields = { - "count_documents": count_documents, - "filter_documents": filter_documents, - "write_documents": write_documents, - "delete_documents": delete_documents, - "to_dict": to_dict, - "from_dict": classmethod(default_from_dict), - } - - if extra_fields is not None: - fields = {**fields, **extra_fields} - - if bases is None: - bases = (object,) - - cls = type(name, bases, fields) - return document_store(cls) diff --git a/haystack/preview/testing/test_utils.py b/haystack/preview/testing/test_utils.py deleted file mode 100644 index 596feb7001..0000000000 --- a/haystack/preview/testing/test_utils.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import random -import logging -import numpy as np - - -logger = logging.getLogger(__name__) - - -def set_all_seeds(seed: int, deterministic_cudnn: bool = False) -> None: - """ - Setting multiple seeds to make runs reproducible. - - Important: Enabling `deterministic_cudnn` gives you full reproducibility with CUDA, - but might slow down your training (see https://pytorch.org/docs/stable/notes/randomness.html#cudnn) ! - - :param seed:number to use as seed - :param deterministic_cudnn: Enable for full reproducibility when using CUDA. Caution: might slow down training. - """ - random.seed(seed) - np.random.seed(seed) - os.environ["PYTHONHASHSEED"] = str(seed) - - try: - import torch - - torch.manual_seed(seed) - torch.cuda.manual_seed_all(seed) - if deterministic_cudnn: - torch.backends.cudnn.deterministic = True - torch.backends.cudnn.benchmark = False - - except (ImportError, ModuleNotFoundError) as exc: - logger.info("Could not set PyTorch seed because torch is not installed. Exception: %s", exc) diff --git a/haystack/preview/utils/__init__.py b/haystack/preview/utils/__init__.py deleted file mode 100644 index a84ea468e2..0000000000 --- a/haystack/preview/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from haystack.preview.utils.expit import expit -from haystack.preview.utils.requests_utils import request_with_retry -from haystack.preview.utils.filters import document_matches_filter diff --git a/haystack/preview/utils/expit.py b/haystack/preview/utils/expit.py deleted file mode 100644 index 0aaaa563cc..0000000000 --- a/haystack/preview/utils/expit.py +++ /dev/null @@ -1,5 +0,0 @@ -import numpy as np - - -def expit(x: float) -> float: - return 1 / (1 + np.exp(-x)) diff --git a/haystack/preview/utils/filters.py b/haystack/preview/utils/filters.py deleted file mode 100644 index 35475c15db..0000000000 --- a/haystack/preview/utils/filters.py +++ /dev/null @@ -1,305 +0,0 @@ -from typing import List, Any, Union, Dict -from dataclasses import fields -from datetime import datetime - -import pandas as pd - -from haystack.preview.dataclasses import Document -from haystack.preview.errors import FilterError - - -def document_matches_filter(filters: Dict[str, Any], document: Document) -> bool: - """ - Return whether `filters` match the Document. - For a detailed specification of the filters, refer to the DocumentStore.filter_documents() protocol documentation. - """ - if "field" in filters: - return _comparison_condition(filters, document) - return _logic_condition(filters, document) - - -def _and(document: Document, conditions: List[Dict[str, Any]]) -> bool: - return all(_comparison_condition(condition, document) for condition in conditions) - - -def _or(document: Document, conditions: List[Dict[str, Any]]) -> bool: - return any(_comparison_condition(condition, document) for condition in conditions) - - -def _not(document: Document, conditions: List[Dict[str, Any]]) -> bool: - return not _and(document, conditions) - - -LOGICAL_OPERATORS = {"NOT": _not, "OR": _or, "AND": _and} - - -def _equal(document_value: Any, filter_value: Any) -> bool: - if isinstance(document_value, pd.DataFrame): - document_value = document_value.to_json() - - if isinstance(filter_value, pd.DataFrame): - filter_value = filter_value.to_json() - - return document_value == filter_value - - -def _not_equal(document_value: Any, filter_value: Any) -> bool: - return not _equal(document_value=document_value, filter_value=filter_value) - - -def _greater_than(document_value: Any, filter_value: Any) -> bool: - if document_value is None or filter_value is None: - # We can't compare None values reliably using operators '>', '>=', '<', '<=' - return False - - if isinstance(document_value, str) or isinstance(filter_value, str): - try: - document_value = datetime.fromisoformat(document_value) - filter_value = datetime.fromisoformat(filter_value) - except (ValueError, TypeError) as exc: - msg = ( - "Can't compare strings using operators '>', '>=', '<', '<='. " - "Strings are only comparable if they are ISO formatted dates." - ) - raise FilterError(msg) from exc - if type(filter_value) in [list, pd.DataFrame]: - msg = f"Filter value can't be of type {type(filter_value)} using operators '>', '>=', '<', '<='" - raise FilterError(msg) - return document_value > filter_value - - -def _greater_than_equal(document_value: Any, filter_value: Any) -> bool: - if document_value is None or filter_value is None: - # We can't compare None values reliably using operators '>', '>=', '<', '<=' - return False - - return _equal(document_value=document_value, filter_value=filter_value) or _greater_than( - document_value=document_value, filter_value=filter_value - ) - - -def _less_than(document_value: Any, filter_value: Any) -> bool: - if document_value is None or filter_value is None: - # We can't compare None values reliably using operators '>', '>=', '<', '<=' - return False - - return not _greater_than_equal(document_value=document_value, filter_value=filter_value) - - -def _less_than_equal(document_value: Any, filter_value: Any) -> bool: - if document_value is None or filter_value is None: - # We can't compare None values reliably using operators '>', '>=', '<', '<=' - return False - - return not _greater_than(document_value=document_value, filter_value=filter_value) - - -def _in(document_value: Any, filter_value: Any) -> bool: - if not isinstance(filter_value, list): - msg = ( - f"Filter value must be a `list` when using operator 'in' or 'not in', received type '{type(filter_value)}'" - ) - raise FilterError(msg) - return any(_equal(e, document_value) for e in filter_value) - - -def _not_in(document_value: Any, filter_value: Any) -> bool: - return not _in(document_value=document_value, filter_value=filter_value) - - -COMPARISON_OPERATORS = { - "==": _equal, - "!=": _not_equal, - ">": _greater_than, - ">=": _greater_than_equal, - "<": _less_than, - "<=": _less_than_equal, - "in": _in, - "not in": _not_in, -} - - -def _logic_condition(condition: Dict[str, Any], document: Document) -> bool: - if "operator" not in condition: - msg = f"'operator' key missing in {condition}" - raise FilterError(msg) - if "conditions" not in condition: - msg = f"'conditions' key missing in {condition}" - raise FilterError(msg) - operator: str = condition["operator"] - conditions: List[Dict[str, Any]] = condition["conditions"] - return LOGICAL_OPERATORS[operator](document, conditions) - - -def _comparison_condition(condition: Dict[str, Any], document: Document) -> bool: - if "field" not in condition: - # 'field' key is only found in comparison dictionaries. - # We assume this is a logic dictionary since it's not present. - return _logic_condition(condition, document) - field: str = condition["field"] - - if "operator" not in condition: - msg = f"'operator' key missing in {condition}" - raise FilterError(msg) - if "value" not in condition: - msg = f"'value' key missing in {condition}" - raise FilterError(msg) - - if "." in field: - # Handles fields formatted like so: - # 'meta.person.name' - parts = field.split(".") - document_value = getattr(document, parts[0]) - for part in parts[1:]: - if part not in document_value: - # If a field is not found we treat it as None - document_value = None - break - document_value = document_value[part] - elif field not in [f.name for f in fields(document)]: - # Converted legacy filters don't add the `meta.` prefix, so we assume - # that all filter fields that are not actual fields in Document are converted - # filters. - # - # We handle this to avoid breaking compatibility with converted legacy filters. - # This will be removed as soon as we stop supporting legacy filters. - document_value = document.meta.get(field) - else: - document_value = getattr(document, field) - operator: str = condition["operator"] - filter_value: Any = condition["value"] - return COMPARISON_OPERATORS[operator](filter_value=filter_value, document_value=document_value) - - -def convert(filters: Dict[str, Any]) -> Dict[str, Any]: - """ - Convert a filter declared using the legacy style into the new style. - This is mostly meant to ease migration from Haystack 1.x to 2.x for developers - of Document Stores and Components that use filters. - - This function doesn't verify if `filters` are declared using the legacy style. - - Example usage: - ```python - legacy_filter = { - "$and": { - "type": {"$eq": "article"}, - "date": {"$gte": "2015-01-01", "$lt": "2021-01-01"}, - "rating": {"$gte": 3}, - "$or": {"genre": {"$in": ["economy", "politics"]}, "publisher": {"$eq": "nytimes"}}, - } - } - assert convert(legacy_filter) == { - "operator": "AND", - "conditions": [ - {"field": "type", "operator": "==", "value": "article"}, - {"field": "date", "operator": ">=", "value": "2015-01-01"}, - {"field": "date", "operator": "<", "value": "2021-01-01"}, - {"field": "rating", "operator": ">=", "value": 3}, - { - "operator": "OR", - "conditions": [ - {"field": "genre", "operator": "in", "value": ["economy", "politics"]}, - {"field": "publisher", "operator": "==", "value": "nytimes"}, - ], - }, - ], - } - ``` - """ - if not isinstance(filters, dict): - msg = f"Can't convert filters from type '{type(filters)}'" - raise ValueError(msg) - - converted = _internal_convert(filters) - if "conditions" not in converted: - # This is done to handle a corner case when filter is really simple like so: - # {"text": "A Foo Document 1"} - # The root '$and' operator is implicit so the conversion doesn't handle - # it and it must be added explicitly like so. - # This only happens for simple filters like the one above. - return {"operator": "AND", "conditions": [converted]} - return converted - - -def _internal_convert(filters: Union[List[Any], Dict[str, Any]], previous_key=None) -> Any: - """ - Recursively convert filters from legacy to new style. - """ - conditions = [] - - if isinstance(filters, list) and (result := _handle_list(filters, previous_key)) is not None: - return result - - if not isinstance(filters, dict): - return _handle_non_dict(filters, previous_key) - - for key, value in filters.items(): - if ( - previous_key is not None - and previous_key not in ALL_LEGACY_OPERATORS_MAPPING - and key not in ALL_LEGACY_OPERATORS_MAPPING - ): - msg = f"This filter ({filters}) seems to be malformed." - raise FilterError(msg) - if key not in ALL_LEGACY_OPERATORS_MAPPING: - converted = _internal_convert(value, previous_key=key) - if isinstance(converted, list): - conditions.extend(converted) - else: - conditions.append(converted) - elif key in LEGACY_LOGICAL_OPERATORS_MAPPING: - if previous_key not in ALL_LEGACY_OPERATORS_MAPPING and isinstance(value, list): - converted = [_internal_convert({previous_key: v}) for v in value] - conditions.append({"operator": ALL_LEGACY_OPERATORS_MAPPING[key], "conditions": converted}) - else: - converted = _internal_convert(value, previous_key=key) - if key == "$not" and type(converted) not in [dict, list]: - # This handles a corner when '$not' is used like this: - # '{"page": {"$not": 102}}' - # Without this check we would miss the implicit '$eq' - converted = {"field": previous_key, "operator": "==", "value": value} - if not isinstance(converted, list): - converted = [converted] - conditions.append({"operator": ALL_LEGACY_OPERATORS_MAPPING[key], "conditions": converted}) - elif key in LEGACY_COMPARISON_OPERATORS_MAPPING: - conditions.append({"field": previous_key, "operator": ALL_LEGACY_OPERATORS_MAPPING[key], "value": value}) - - if len(conditions) == 1: - return conditions[0] - - if previous_key is None: - return {"operator": "AND", "conditions": conditions} - - return conditions - - -def _handle_list(filters, previous_key): - if previous_key in LEGACY_LOGICAL_OPERATORS_MAPPING: - return [_internal_convert(f) for f in filters] - elif previous_key not in LEGACY_COMPARISON_OPERATORS_MAPPING: - return {"field": previous_key, "operator": "in", "value": filters} - return None - - -def _handle_non_dict(filters, previous_key): - if previous_key not in ALL_LEGACY_OPERATORS_MAPPING: - return {"field": previous_key, "operator": "==", "value": filters} - return filters - - -# Operator mappings from legacy style to new one -LEGACY_LOGICAL_OPERATORS_MAPPING = {"$and": "AND", "$or": "OR", "$not": "NOT"} - -LEGACY_COMPARISON_OPERATORS_MAPPING = { - "$eq": "==", - "$ne": "!=", - "$gt": ">", - "$gte": ">=", - "$lt": "<", - "$lte": "<=", - "$in": "in", - "$nin": "not in", -} - -ALL_LEGACY_OPERATORS_MAPPING = {**LEGACY_LOGICAL_OPERATORS_MAPPING, **LEGACY_COMPARISON_OPERATORS_MAPPING} diff --git a/haystack/preview/utils/requests_utils.py b/haystack/preview/utils/requests_utils.py deleted file mode 100644 index 245d7737fb..0000000000 --- a/haystack/preview/utils/requests_utils.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import Optional, List - -import logging - -from tenacity import retry, wait_exponential, retry_if_exception_type, stop_after_attempt, before_log, after_log -import requests - -logger = logging.getLogger(__file__) - - -def request_with_retry( - attempts: int = 3, status_codes_to_retry: Optional[List[int]] = None, **kwargs -) -> requests.Response: - """ - request_with_retry is a simple wrapper function that executes an HTTP request - with a configurable exponential backoff retry on failures. - - All kwargs will be passed to ``requests.request``, so it accepts the same arguments. - - Example Usage: - -------------- - - # Sending an HTTP request with default retry configs - res = request_with_retry(method="GET", url="https://example.com") - - # Sending an HTTP request with custom number of attempts - res = request_with_retry(method="GET", url="https://example.com", attempts=10) - - # Sending an HTTP request with custom HTTP codes to retry - res = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=[408, 503]) - - # Sending an HTTP request with custom timeout in seconds - res = request_with_retry(method="GET", url="https://example.com", timeout=5) - - # Sending an HTTP request with custom authorization handling - class CustomAuth(requests.auth.AuthBase): - def __call__(self, r): - r.headers["authorization"] = "Basic " - return r - - res = request_with_retry(method="GET", url="https://example.com", auth=CustomAuth()) - - # All of the above combined - res = request_with_retry( - method="GET", - url="https://example.com", - auth=CustomAuth(), - attempts=10, - status_codes_to_retry=[408, 503], - timeout=5 - ) - - # Sending a POST request - res = request_with_retry(method="POST", url="https://example.com", data={"key": "value"}, attempts=10) - - # Retry all 5xx status codes - res = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=list(range(500, 600))) - - :param attempts: Maximum number of attempts to retry the request, defaults to 3 - :param status_codes_to_retry: List of HTTP status codes that will trigger a retry, defaults to [408, 418, 429, 503]: - - `408: Request Timeout` - - `418` - - `429: Too Many Requests` - - `503: Service Unavailable` - :param **kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - """ - - if status_codes_to_retry is None: - status_codes_to_retry = [408, 418, 429, 503] - - @retry( - reraise=True, - wait=wait_exponential(), - retry=retry_if_exception_type((requests.HTTPError, TimeoutError)), - stop=stop_after_attempt(attempts), - before=before_log(logger, logging.DEBUG), - after=after_log(logger, logging.DEBUG), - ) - def run(): - timeout = kwargs.pop("timeout", 10) - res = requests.request(**kwargs, timeout=timeout) - - if res.status_code in status_codes_to_retry: - # We raise only for the status codes that must trigger a retry - res.raise_for_status() - - return res - - res = run() - # We raise here too in case the request failed with a status code that - # won't trigger a retry, this way the call will still cause an explicit exception - res.raise_for_status() - return res diff --git a/haystack/preview/version.py b/haystack/preview/version.py deleted file mode 100644 index 23a3060671..0000000000 --- a/haystack/preview/version.py +++ /dev/null @@ -1,13 +0,0 @@ -from importlib import metadata - -# haystack.preview is distributed as a separate package called `haystack-ai`. -# We want to keep all preview dependencies separate from the current Haystack version, -# so imports in haystack.preview must only import from haystack.preview. -# Since we need to access __version__ in haystack.preview without importing from -# haystack we must set it here too. -# When installing `haystack-ai` we want to use that package version though -# as `farm-haystack` might not be installed and cause this to fail. -try: - __version__ = str(metadata.version("haystack-ai")) -except metadata.PackageNotFoundError: - __version__ = str(metadata.version("farm-haystack")) From 301b473ef877ba42edf3548e194cf433c6ea6dce Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Wed, 29 Nov 2023 15:40:59 +0100 Subject: [PATCH 2/3] more cleanup --- .github/labeler.yml | 5 - .github/utils/pydoc-markdown.sh | 8 - .github/workflows/ci_metrics.yml | 2 - .github/workflows/e2e.yml | 4 +- .github/workflows/e2e_preview.yml | 42 - .github/workflows/examples_tests.yml | 3 +- .github/workflows/linting.yml | 3 - .github/workflows/linting_preview.yml | 78 - .github/workflows/linting_skipper.yml | 3 - .github/workflows/preview_imports.yml | 55 - .github/workflows/pypi_release_preview.yml | 23 - .github/workflows/readme_sync.yml | 10 - .github/workflows/snippets_tests.yml | 81 - .github/workflows/tests.yml | 3 - .github/workflows/tests_preview.yml | 318 ---- .github/workflows/tests_preview_skipper.yml | 21 - .github/workflows/tests_skipper.yml | 3 - conftest.py | 3 - docs/pydoc/config-preview/builder.yml | 26 - docs/pydoc/config-preview/caching.yml | 26 - docs/pydoc/config-preview/classifier.yml | 26 - docs/pydoc/config-preview/converter.yml | 26 - docs/pydoc/config-preview/document_store.yml | 26 - docs/pydoc/config-preview/embedder.yml | 26 - docs/pydoc/config-preview/fetcher.yml | 26 - docs/pydoc/config-preview/generator.yml | 26 - docs/pydoc/config-preview/pipeline.yml | 26 - docs/pydoc/config-preview/preprocessor.yml | 26 - docs/pydoc/config-preview/ranker.yml | 26 - docs/pydoc/config-preview/reader.yml | 26 - docs/pydoc/config-preview/retriever.yml | 26 - docs/pydoc/config-preview/router.yml | 26 - docs/pydoc/config-preview/sampler.yml | 26 - docs/pydoc/config-preview/websearch.yml | 26 - docs/pydoc/config-preview/whisper.yml | 26 - docs/pydoc/config-preview/writer.yml | 26 - docs/pydoc/renderers.py | 13 - e2e/conftest.py | 5 - e2e/preview/conftest.py | 11 - .../pipelines/test_dense_doc_search.py | 84 - .../pipelines/test_extractive_qa_pipeline.py | 67 - .../test_hybrid_doc_search_pipeline.py | 57 - .../pipelines/test_preprocessing_pipeline.py | 89 - e2e/preview/pipelines/test_rag_pipelines.py | 159 -- e2e/preview/samples/doc_1.txt | 1 - e2e/preview/samples/sample_pdf_1.pdf | Bin 44524 -> 0 bytes .../in_memory_bm25_documentsearch.py | 28 - .../preview/retrievers/in_memory_bm25_rag.py | 53 - pyproject.toml | 20 +- releasenotes/config.yaml | 6 +- test/conftest.py | 5 - test/preview/components/audio/__init__.py | 0 .../components/audio/test_whisper_local.py | 170 -- .../components/audio/test_whisper_remote.py | 254 --- test/preview/components/builders/__init__.py | 0 .../builders/test_answer_builder.py | 154 -- .../builders/test_dynamic_prompt_builder.py | 154 -- .../builders/test_prompt_builder.py | 40 - .../caching/test_url_cache_checker.py | 95 - .../test_document_language_classifier.py | 52 - .../preview/components/converters/__init__.py | 0 .../test_azure_ocr_doc_converter.py | 88 - .../converters/test_html_to_document.py | 156 -- .../converters/test_markdown_to_document.py | 93 - .../converters/test_pypdf_to_document.py | 77 - .../converters/test_textfile_to_document.py | 69 - .../converters/test_tika_doc_converter.py | 75 - test/preview/components/embedders/__init__.py | 0 .../test_openai_document_embedder.py | 288 --- .../embedders/test_openai_text_embedder.py | 118 -- ...sentence_transformers_document_embedder.py | 210 --- ...sentence_transformers_embedding_backend.py | 42 - ...est_sentence_transformers_text_embedder.py | 151 -- test/preview/components/fetchers/__init__.py | 0 .../fetchers/test_link_content_fetcher.py | 196 -- .../components/generators/chat/__init__.py | 0 .../components/generators/chat/conftest.py | 11 - .../generators/chat/test_hugging_face_tgi.py | 317 ---- .../components/generators/chat/test_openai.py | 330 ---- .../preview/components/generators/conftest.py | 21 - .../generators/test_cohere_generators.py | 172 -- .../components/generators/test_hf_utils.py | 50 - .../test_hugging_face_local_generator.py | 349 ---- .../generators/test_hugging_face_tgi.py | 295 --- .../components/generators/test_openai.py | 343 ---- .../components/generators/test_utils.py | 37 - .../components/preprocessors/__init__.py | 0 .../preprocessors/test_document_cleaner.py | 139 -- .../preprocessors/test_document_splitter.py | 142 -- .../components/rankers/test_metafield.py | 122 -- .../rankers/test_transformers_similarity.py | 102 - .../components/readers/test_extractive.py | 415 ----- .../preview/components/retrievers/__init__.py | 0 .../test_in_memory_bm25_retriever.py | 185 -- .../test_in_memory_embedding_retriever.py | 169 -- test/preview/components/routers/__init__.py | 0 .../routers/test_conditional_router.py | 324 ---- .../routers/test_document_joiner.py | 140 -- .../components/routers/test_file_router.py | 139 -- .../routers/test_metadata_router.py | 35 - .../routers/test_text_language_router.py | 52 - test/preview/components/samplers/__init__.py | 0 .../preview/components/samplers/test_top_p.py | 89 - test/preview/components/websearch/__init__.py | 0 .../components/websearch/test_searchapi.py | 445 ----- .../components/websearch/test_serperdev.py | 182 -- test/preview/components/writers/__init__.py | 0 .../writers/test_document_writer.py | 106 -- test/preview/conftest.py | 23 - test/preview/dataclasses/test_byte_stream.py | 43 - test/preview/dataclasses/test_chat_message.py | 70 - test/preview/dataclasses/test_document.py | 309 ---- .../dataclasses/test_streaming_chunk.py | 32 - .../preview/document_stores/test_in_memory.py | 399 ---- test/preview/test_files/audio/answer.wav | Bin 29228 -> 0 bytes .../the context for this answer is here.wav | Bin 99884 -> 0 bytes .../this is the content of the document.wav | Bin 89644 -> 0 bytes test/preview/test_files/docx/sample_docx.docx | Bin 13232 -> 0 bytes .../test_files/html/what_is_haystack.html | 1634 ----------------- test/preview/test_files/images/apple.jpg | Bin 69286 -> 0 bytes .../test_files/images/haystack-logo.png | Bin 30437 -> 0 bytes test/preview/test_files/markdown/sample.md | 65 - test/preview/test_files/pdf/react_paper.pdf | Bin 538934 -> 0 bytes test/preview/test_files/pdf/sample_pdf_1.pdf | Bin 44524 -> 0 bytes test/preview/test_files/pdf/sample_pdf_2.pdf | Bin 26093 -> 0 bytes test/preview/test_files/txt/doc_1.txt | 2 - test/preview/test_files/txt/doc_2.txt | 3 - test/preview/test_files/txt/doc_3.txt | 11 - .../test_files/yaml/test_pipeline.yaml | 14 - test/preview/test_pipeline.py | 62 - test/preview/test_telemetry.py | 54 - test/preview/testing/test_factory.py | 70 - test/preview/utils/test_filters.py | 725 -------- 133 files changed, 5 insertions(+), 12435 deletions(-) delete mode 100644 .github/workflows/e2e_preview.yml delete mode 100644 .github/workflows/linting_preview.yml delete mode 100644 .github/workflows/preview_imports.yml delete mode 100644 .github/workflows/pypi_release_preview.yml delete mode 100644 .github/workflows/snippets_tests.yml delete mode 100644 .github/workflows/tests_preview.yml delete mode 100644 .github/workflows/tests_preview_skipper.yml delete mode 100644 docs/pydoc/config-preview/builder.yml delete mode 100644 docs/pydoc/config-preview/caching.yml delete mode 100644 docs/pydoc/config-preview/classifier.yml delete mode 100644 docs/pydoc/config-preview/converter.yml delete mode 100644 docs/pydoc/config-preview/document_store.yml delete mode 100644 docs/pydoc/config-preview/embedder.yml delete mode 100644 docs/pydoc/config-preview/fetcher.yml delete mode 100644 docs/pydoc/config-preview/generator.yml delete mode 100644 docs/pydoc/config-preview/pipeline.yml delete mode 100644 docs/pydoc/config-preview/preprocessor.yml delete mode 100644 docs/pydoc/config-preview/ranker.yml delete mode 100644 docs/pydoc/config-preview/reader.yml delete mode 100644 docs/pydoc/config-preview/retriever.yml delete mode 100644 docs/pydoc/config-preview/router.yml delete mode 100644 docs/pydoc/config-preview/sampler.yml delete mode 100644 docs/pydoc/config-preview/websearch.yml delete mode 100644 docs/pydoc/config-preview/whisper.yml delete mode 100644 docs/pydoc/config-preview/writer.yml delete mode 100644 e2e/preview/conftest.py delete mode 100644 e2e/preview/pipelines/test_dense_doc_search.py delete mode 100644 e2e/preview/pipelines/test_extractive_qa_pipeline.py delete mode 100644 e2e/preview/pipelines/test_hybrid_doc_search_pipeline.py delete mode 100644 e2e/preview/pipelines/test_preprocessing_pipeline.py delete mode 100644 e2e/preview/pipelines/test_rag_pipelines.py delete mode 100644 e2e/preview/samples/doc_1.txt delete mode 100644 e2e/preview/samples/sample_pdf_1.pdf delete mode 100644 examples/preview/retrievers/in_memory_bm25_documentsearch.py delete mode 100644 examples/preview/retrievers/in_memory_bm25_rag.py delete mode 100644 test/preview/components/audio/__init__.py delete mode 100644 test/preview/components/audio/test_whisper_local.py delete mode 100644 test/preview/components/audio/test_whisper_remote.py delete mode 100644 test/preview/components/builders/__init__.py delete mode 100644 test/preview/components/builders/test_answer_builder.py delete mode 100644 test/preview/components/builders/test_dynamic_prompt_builder.py delete mode 100644 test/preview/components/builders/test_prompt_builder.py delete mode 100644 test/preview/components/caching/test_url_cache_checker.py delete mode 100644 test/preview/components/classifiers/test_document_language_classifier.py delete mode 100644 test/preview/components/converters/__init__.py delete mode 100644 test/preview/components/converters/test_azure_ocr_doc_converter.py delete mode 100644 test/preview/components/converters/test_html_to_document.py delete mode 100644 test/preview/components/converters/test_markdown_to_document.py delete mode 100644 test/preview/components/converters/test_pypdf_to_document.py delete mode 100644 test/preview/components/converters/test_textfile_to_document.py delete mode 100644 test/preview/components/converters/test_tika_doc_converter.py delete mode 100644 test/preview/components/embedders/__init__.py delete mode 100644 test/preview/components/embedders/test_openai_document_embedder.py delete mode 100644 test/preview/components/embedders/test_openai_text_embedder.py delete mode 100644 test/preview/components/embedders/test_sentence_transformers_document_embedder.py delete mode 100644 test/preview/components/embedders/test_sentence_transformers_embedding_backend.py delete mode 100644 test/preview/components/embedders/test_sentence_transformers_text_embedder.py delete mode 100644 test/preview/components/fetchers/__init__.py delete mode 100644 test/preview/components/fetchers/test_link_content_fetcher.py delete mode 100644 test/preview/components/generators/chat/__init__.py delete mode 100644 test/preview/components/generators/chat/conftest.py delete mode 100644 test/preview/components/generators/chat/test_hugging_face_tgi.py delete mode 100644 test/preview/components/generators/chat/test_openai.py delete mode 100644 test/preview/components/generators/conftest.py delete mode 100644 test/preview/components/generators/test_cohere_generators.py delete mode 100644 test/preview/components/generators/test_hf_utils.py delete mode 100644 test/preview/components/generators/test_hugging_face_local_generator.py delete mode 100644 test/preview/components/generators/test_hugging_face_tgi.py delete mode 100644 test/preview/components/generators/test_openai.py delete mode 100644 test/preview/components/generators/test_utils.py delete mode 100644 test/preview/components/preprocessors/__init__.py delete mode 100644 test/preview/components/preprocessors/test_document_cleaner.py delete mode 100644 test/preview/components/preprocessors/test_document_splitter.py delete mode 100644 test/preview/components/rankers/test_metafield.py delete mode 100644 test/preview/components/rankers/test_transformers_similarity.py delete mode 100644 test/preview/components/readers/test_extractive.py delete mode 100644 test/preview/components/retrievers/__init__.py delete mode 100644 test/preview/components/retrievers/test_in_memory_bm25_retriever.py delete mode 100644 test/preview/components/retrievers/test_in_memory_embedding_retriever.py delete mode 100644 test/preview/components/routers/__init__.py delete mode 100644 test/preview/components/routers/test_conditional_router.py delete mode 100644 test/preview/components/routers/test_document_joiner.py delete mode 100644 test/preview/components/routers/test_file_router.py delete mode 100644 test/preview/components/routers/test_metadata_router.py delete mode 100644 test/preview/components/routers/test_text_language_router.py delete mode 100644 test/preview/components/samplers/__init__.py delete mode 100644 test/preview/components/samplers/test_top_p.py delete mode 100644 test/preview/components/websearch/__init__.py delete mode 100644 test/preview/components/websearch/test_searchapi.py delete mode 100644 test/preview/components/websearch/test_serperdev.py delete mode 100644 test/preview/components/writers/__init__.py delete mode 100644 test/preview/components/writers/test_document_writer.py delete mode 100644 test/preview/conftest.py delete mode 100644 test/preview/dataclasses/test_byte_stream.py delete mode 100644 test/preview/dataclasses/test_chat_message.py delete mode 100644 test/preview/dataclasses/test_document.py delete mode 100644 test/preview/dataclasses/test_streaming_chunk.py delete mode 100644 test/preview/document_stores/test_in_memory.py delete mode 100644 test/preview/test_files/audio/answer.wav delete mode 100644 test/preview/test_files/audio/the context for this answer is here.wav delete mode 100644 test/preview/test_files/audio/this is the content of the document.wav delete mode 100644 test/preview/test_files/docx/sample_docx.docx delete mode 100644 test/preview/test_files/html/what_is_haystack.html delete mode 100644 test/preview/test_files/images/apple.jpg delete mode 100644 test/preview/test_files/images/haystack-logo.png delete mode 100644 test/preview/test_files/markdown/sample.md delete mode 100644 test/preview/test_files/pdf/react_paper.pdf delete mode 100644 test/preview/test_files/pdf/sample_pdf_1.pdf delete mode 100644 test/preview/test_files/pdf/sample_pdf_2.pdf delete mode 100644 test/preview/test_files/txt/doc_1.txt delete mode 100644 test/preview/test_files/txt/doc_2.txt delete mode 100644 test/preview/test_files/txt/doc_3.txt delete mode 100644 test/preview/test_files/yaml/test_pipeline.yaml delete mode 100644 test/preview/test_pipeline.py delete mode 100644 test/preview/test_telemetry.py delete mode 100644 test/preview/testing/test_factory.py delete mode 100644 test/preview/utils/test_filters.py diff --git a/.github/labeler.yml b/.github/labeler.yml index 03b320d9c4..5d926787cf 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -2,11 +2,6 @@ proposal: - proposals/text/* -# 2.x -2.x: -- haystack/preview/**/* -- test/preview/**/* - # Topics topic:tests: - test/**/* diff --git a/.github/utils/pydoc-markdown.sh b/.github/utils/pydoc-markdown.sh index 39bc6dd78e..4322da034e 100755 --- a/.github/utils/pydoc-markdown.sh +++ b/.github/utils/pydoc-markdown.sh @@ -9,11 +9,3 @@ for file in ../config/* ; do echo "Converting $file..." pydoc-markdown "$file" done -# render preview markdown docs -cd .. -rm -rf temp-preview && mkdir temp-preview -cd temp-preview -for file in ../config-preview/* ; do - echo "Converting $file..." - pydoc-markdown "$file" -done diff --git a/.github/workflows/ci_metrics.yml b/.github/workflows/ci_metrics.yml index 31a329e334..688f3b9e1b 100644 --- a/.github/workflows/ci_metrics.yml +++ b/.github/workflows/ci_metrics.yml @@ -4,10 +4,8 @@ on: workflow_run: workflows: - "end-to-end" - - "end-to-end (Preview)" - "Linting" - "Tests" - - "Tests (Preview)" - "REST API Tests" types: - completed diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index af42e93542..5512f571bd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,7 +13,6 @@ on: - ready_for_review paths: - "e2e/**/*.py" - - "!e2e/preview/**/*.py" # See e2e_preview.yml - ".github/workflows/e2e.yml" env: @@ -31,7 +30,6 @@ jobs: folder: - "document_search" - "pipelines" - - "preview" runs-on: ubuntu-latest @@ -59,7 +57,7 @@ jobs: run: docker run -d -p 8080:8080 --name haystack_test_weaviate --env AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED='true' --env PERSISTENCE_DATA_PATH='/var/lib/weaviate' --env ENABLE_EXPERIMENTAL_BM25='true' --env DISK_USE_READONLY_PERCENTAGE='95' semitechnologies/weaviate:1.17.2 - name: Install Haystack - run: pip install -e .[inference,elasticsearch7,faiss,weaviate,opensearch,dev,pdf,preview] langdetect + run: pip install -e .[inference,elasticsearch7,faiss,weaviate,opensearch,dev,pdf] langdetect # FIXME caching prevents PRs from running the e2e tests properly diff --git a/.github/workflows/e2e_preview.yml b/.github/workflows/e2e_preview.yml deleted file mode 100644 index 2e9763d280..0000000000 --- a/.github/workflows/e2e_preview.yml +++ /dev/null @@ -1,42 +0,0 @@ -# If you change this name also do it in ci_metrics.yml -name: end-to-end (Preview) - -on: - workflow_dispatch: # Activate this workflow manually - schedule: - - cron: "0 0 * * *" - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - paths: - - "e2e/preview/**/*.py" - - ".github/workflows/e2e_preview.yml" - -env: - PYTHON_VERSION: "3.8" - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - -jobs: - run: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt install ffmpeg # for local Whisper tests - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf tika 'azure-ai-formrecognizer>=3.2.0b2' - - - name: Run tests - run: pytest e2e/preview diff --git a/.github/workflows/examples_tests.yml b/.github/workflows/examples_tests.yml index c0a7f10887..2ec252b352 100644 --- a/.github/workflows/examples_tests.yml +++ b/.github/workflows/examples_tests.yml @@ -8,7 +8,6 @@ on: pull_request: paths: - "examples/**" - - "!examples/preview/**" types: - opened - reopened @@ -48,7 +47,7 @@ jobs: pip install .[inference,dev,elasticsearch,preprocessing,file-conversion] - name: Run - run: pytest examples/ --ignore examples/preview/ + run: pytest examples/ - name: Calculate alert data id: calculator diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 2a8a6d96e7..c4a4f850e0 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -6,9 +6,6 @@ on: paths: - "**.py" - "**/pyproject.toml" - - "!haystack/preview/**/*.py" - - "!test/preview/**/*.py" - - "!e2e/preview/**/*.py" env: PYTHON_VERSION: "3.8" diff --git a/.github/workflows/linting_preview.yml b/.github/workflows/linting_preview.yml deleted file mode 100644 index 1c7209d138..0000000000 --- a/.github/workflows/linting_preview.yml +++ /dev/null @@ -1,78 +0,0 @@ -# If you change this name also do it in linting-skipper.yml and ci_metrics.yml -name: Linting (Preview) - -on: - pull_request: - paths: - - "haystack/preview/**/*.py" - - "test/preview/**/*.py" - - "e2e/preview/**/*.py" - - "**/pyproject.toml" - -env: - PYTHON_VERSION: "3.8" - -jobs: - mypy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # With the default value of 1, there are corner cases where tj-actions/changed-files - # fails with a `no merge base` error - fetch-depth: 0 - - - name: Get changed files - id: files - uses: tj-actions/changed-files@v40 - with: - files: | - **/*.py - files_ignore: | - test/** - rest_api/test/** - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - - - name: Mypy - if: steps.files.outputs.any_changed == 'true' - run: | - mkdir .mypy_cache/ - mypy --install-types --non-interactive ${{ steps.files.outputs.all_changed_files }} --exclude=rest_api/build/ --exclude=rest_api/test/ - - pylint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # With the default value of 1, there are corner cases where tj-actions/changed-files - # fails with a `no merge base` error - fetch-depth: 0 - - - name: Get changed files - id: files - uses: tj-actions/changed-files@v40 - with: - files: | - haystack/preview/**/*.py - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Haystack - run: | - pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf markdown-it-py mdit_plain tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - pip install ./haystack-linter - - - name: Pylint - if: steps.files.outputs.any_changed == 'true' - run: | - pylint -ry -j 0 ${{ steps.files.outputs.all_changed_files }} diff --git a/.github/workflows/linting_skipper.yml b/.github/workflows/linting_skipper.yml index 0176579329..1430c845e6 100644 --- a/.github/workflows/linting_skipper.yml +++ b/.github/workflows/linting_skipper.yml @@ -6,9 +6,6 @@ on: paths-ignore: - "**.py" - "**/pyproject.toml" - - "!haystack/preview/**/*.py" - - "!test/preview/**/*.py" - - "!e2e/preview/**/*.py" jobs: mypy: diff --git a/.github/workflows/preview_imports.yml b/.github/workflows/preview_imports.yml deleted file mode 100644 index ba2d70f76d..0000000000 --- a/.github/workflows/preview_imports.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Verify preview imports only preview - -on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - paths: - - "haystack/preview/**.py" - -jobs: - verify-imports: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # With the default value of 1, there are corner cases where tj-actions/changed-files - # fails with a `no merge base` error - fetch-depth: 0 - - - name: Get changed files - id: files - uses: tj-actions/changed-files@v40 - with: - files: | - haystack/preview/**.py - - - name: Check imports - shell: python - run: | - import re - regex = r"^(from haystack|import haystack)(?!\.preview| import preview)(.*)" - - changed_files = "${{ steps.files.outputs.all_changed_files }}".split() - matches = {} - for path in changed_files: - with open(path, "r") as f: - file_matches = [] - for line in f.readlines(): - file_matches.extend(re.finditer(regex, line.strip())) - if file_matches: - matches[path] = file_matches - - for path, match in matches.items(): - print(f"Bad imports in file '{path}'") - for m in match: - print(m.group()) - print() - - if matches: - print("::error:: Imports in haystack.preview can only import from haystack.preview") - import sys; sys.exit(1) diff --git a/.github/workflows/pypi_release_preview.yml b/.github/workflows/pypi_release_preview.yml deleted file mode 100644 index 91e9b0f7e4..0000000000 --- a/.github/workflows/pypi_release_preview.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Trigger preview release - -on: - push: - branches: - - main - paths: - - "haystack/preview/**.py" - -jobs: - release-on-pypi: - runs-on: ubuntu-latest - - steps: - - name: Trigger preview release - env: - HAYSTACK_BOT_REPO_DISPATCH_PA_TOKEN: ${{ secrets.HAYSTACK_BOT_REPO_DISPATCH_PA_TOKEN }} - run: | - curl -L \ - -X POST \ - -H "Authorization: Bearer $HAYSTACK_BOT_REPO_DISPATCH_PA_TOKEN" \ - https://api.github.com/repos/deepset-ai/haystack-preview-package/dispatches \ - -d '{"event_type":"preview_release"}' diff --git a/.github/workflows/readme_sync.yml b/.github/workflows/readme_sync.yml index f13389a70d..f0e5d02836 100644 --- a/.github/workflows/readme_sync.yml +++ b/.github/workflows/readme_sync.yml @@ -53,16 +53,6 @@ jobs: with: rdme: docs ./docs/pydoc/temp --key="$README_API_KEY" --version=${{ steps.current-version.outputs.minor }}-unstable - - name: Sync preview docs with 2.0 - # Sync the preview docs to the `2.0` version on Readme - id: sync-main-preview - if: github.ref_name == 'main' && github.event_name == 'push' - uses: readmeio/rdme@8.3.1 - env: - README_API_KEY: ${{ secrets.README_API_KEY }} - with: - rdme: docs ./docs/pydoc/temp-preview --key="$README_API_KEY" --version=2.0 - - name: Sync docs with current release # Mutually exclusive with the previous one, this step is supposed to only run on version branches. # Sync the current Haystack version `X.Y.Z` with its corresponding Readme version `X.Y`. diff --git a/.github/workflows/snippets_tests.yml b/.github/workflows/snippets_tests.yml deleted file mode 100644 index c12ea60099..0000000000 --- a/.github/workflows/snippets_tests.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Test documentation snippets for Haystack 2.x - -on: - workflow_dispatch: # Activate this workflow manually - push: - branches: - - main - pull_request: - paths: - - examples/preview/** - types: - - opened - - reopened - - synchronize - - ready_for_review - -env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - PYTHON_VERSION: "3.8" - -jobs: - tests: - name: Snippets - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install snippets dependencies - run: | - pip install --upgrade pip - pip install ".[preview]" torch - - - name: Get changed files - id: files - uses: tj-actions/changed-files@v40 - with: - files: | - examples/preview/**.py - - - name: Run each snippet - run: | - CHANGED_FILES=${{ steps.files.outputs.all_changed_files }} - for file in $CHANGED_FILES; do - python "$file" - done - - - name: Calculate alert data - id: calculator - if: (success() || failure()) && github.ref_name == 'main' - shell: bash - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 307f3d2916..47cf50f16d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,9 +17,6 @@ on: paths: - "**.py" - "pyproject.toml" - - "!haystack/preview/**/*.py" # See tests_preview.yml - - "!test/preview/**/*.py" # See tests_preview.yml - - "!e2e/preview/**/*.py" # See e2e_preview.yml - "!.github/**/*.py" - "!rest_api/**/*.py" - "!docs/**/*.py" diff --git a/.github/workflows/tests_preview.yml b/.github/workflows/tests_preview.yml deleted file mode 100644 index f53ed289a7..0000000000 --- a/.github/workflows/tests_preview.yml +++ /dev/null @@ -1,318 +0,0 @@ -# If you change this name also do it in tests_preview_skipper.yml -name: Tests (Preview) - -on: - workflow_dispatch: # Activate this workflow manually - push: - branches: - - main - # release branches have the form v1.9.x - - "v[0-9].*[0-9].x" - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - paths: - - "haystack/preview/**/*.py" - - "test/preview/**/*.py" - -env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} - CORE_AZURE_CS_ENDPOINT: ${{ secrets.CORE_AZURE_CS_ENDPOINT }} - CORE_AZURE_CS_API_KEY: ${{ secrets.CORE_AZURE_CS_API_KEY }} - PYTHON_VERSION: "3.8" - -jobs: - black: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Black - run: | - pip install --upgrade pip - pip install .[formatting] - - - name: Check status - run: | - if ! black . --check; then - git status - echo "###################################################################################################" - echo "# " - echo "# CHECK FAILED! Black found issues with your code formatting." - echo "# " - echo "# Either:" - echo "# 1. Run Black locally before committing:" - echo "# " - echo "# pip install .[formatting]" - echo "# black ." - echo "# " - echo "# 2. Install the pre-commit hook:" - echo "# " - echo "# pre-commit install" - echo "# " - echo "# 3. See https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md for help." - echo "# " - echo "# If you have further problems, please open an issue: https://github.com/deepset-ai/haystack/issues" - echo "# " - echo "##################################################################################################" - exit 1 - fi - - - name: Calculate alert data - id: calculator - shell: bash - if: (success() || failure()) && github.ref_name == 'main' - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - unit-tests: - name: Unit / ${{ matrix.os }} - needs: black - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - windows-latest - - macos-latest - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf markdown-it-py mdit_plain tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - - - name: Run - run: pytest -m "not integration" test/preview - - - name: Calculate alert data - id: calculator - shell: bash - if: (success() || failure()) && github.ref_name == 'main' - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - integration-tests-linux: - name: Integration / ubuntu-latest - needs: unit-tests - runs-on: ubuntu-latest - services: - tika: - image: apache/tika:2.9.0.0 - ports: - - 9998:9998 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - sudo apt update - sudo apt install ffmpeg # for local Whisper tests - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf markdown-it-py mdit_plain tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - - - name: Run - run: pytest --maxfail=5 -m "integration" test/preview - - - name: Calculate alert data - id: calculator - shell: bash - if: (success() || failure()) && github.ref_name == 'main' - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - integration-tests-macos: - name: Integration / macos-latest - needs: unit-tests - runs-on: macos-latest-xl - env: - HAYSTACK_MPS_ENABLED: false - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - brew install ffmpeg # for local Whisper tests - brew install docker - colima start - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf markdown-it-py mdit_plain tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - - - name: Run Tika - run: docker run -d -p 9998:9998 apache/tika:2.9.0.0 - - - name: Run - run: pytest --maxfail=5 -m "integration" test/preview - - - name: Calculate alert data - id: calculator - shell: bash - if: (success() || failure()) && github.ref_name == 'main' - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - integration-tests-windows: - name: Integration / windows-latest - needs: unit-tests - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Haystack - run: pip install .[dev,preview,audio] langdetect transformers[torch,sentencepiece]==4.35.2 'sentence-transformers>=2.2.0' pypdf markdown-it-py mdit_plain tika 'azure-ai-formrecognizer>=3.2.0b2' cohere - - - name: Run - run: pytest --maxfail=5 -m "integration" test/preview -k 'not tika' - - - name: Calculate alert data - id: calculator - shell: bash - if: (success() || failure()) && github.ref_name == 'main' - run: | - if [ "${{ job.status }}" = "success" ]; then - echo "alert_type=success" >> "$GITHUB_OUTPUT"; - else - echo "alert_type=error" >> "$GITHUB_OUTPUT"; - fi - - - name: Send event to Datadog - if: (success() || failure()) && github.ref_name == 'main' - uses: masci/datadog@v1 - with: - api-key: ${{ secrets.CORE_DATADOG_API_KEY }} - api-url: https://api.datadoghq.eu - events: | - - title: "${{ github.workflow }} workflow" - text: "Job ${{ github.job }} in branch ${{ github.ref_name }}" - alert_type: "${{ steps.calculator.outputs.alert_type }}" - source_type_name: "Github" - host: ${{ github.repository_owner }} - tags: - - "project:${{ github.repository }}" - - "job:${{ github.job }}" - - "run_id:${{ github.run_id }}" - - "workflow:${{ github.workflow }}" - - "branch:${{ github.ref_name }}" - - "url:https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/tests_preview_skipper.yml b/.github/workflows/tests_preview_skipper.yml deleted file mode 100644 index 2f64eaae99..0000000000 --- a/.github/workflows/tests_preview_skipper.yml +++ /dev/null @@ -1,21 +0,0 @@ -# If you change this name also do it in tests_preview.yml -name: Tests (Preview) - -on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - paths-ignore: - - "haystack/preview/**/*.py" - - "test/preview/**/*.py" - -jobs: - catch-all: - name: Catch-all check - runs-on: ubuntu-latest - steps: - - name: Skip preview tests - run: echo "Skipped!" diff --git a/.github/workflows/tests_skipper.yml b/.github/workflows/tests_skipper.yml index 5ac396aebd..c4e4dbae09 100644 --- a/.github/workflows/tests_skipper.yml +++ b/.github/workflows/tests_skipper.yml @@ -11,9 +11,6 @@ on: paths-ignore: - "**.py" - "pyproject.toml" - - "!haystack/preview/**/*.py" # See tests_preview.yml - - "!test/preview/**/*.py" # See tests_preview.yml - - "!e2e/preview/**/*.py" # See e2e_preview.yml - "!.github/**/*.py" - "!rest_api/**/*.py" - "!docs/**/*.py" diff --git a/conftest.py b/conftest.py index 7d27f2659d..475c5dcc91 100644 --- a/conftest.py +++ b/conftest.py @@ -11,9 +11,6 @@ def pytest_addoption(parser): def pytest_generate_tests(metafunc): - # Ugly hack to avoid polluting preview tests this with unwanted fixtures - if "test/preview" in metafunc.module.__file__ or "test\\preview" in metafunc.module.__file__: - return # Get selected docstores from CLI arg document_store_type = metafunc.config.option.document_store_type selected_doc_stores = [item.strip() for item in document_store_type.split(",")] diff --git a/docs/pydoc/config-preview/builder.yml b/docs/pydoc/config-preview/builder.yml deleted file mode 100644 index 7ec4b36141..0000000000 --- a/docs/pydoc/config-preview/builder.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/builders] - modules: ["answer_builder", "prompt_builder", "dynamic_prompt_builder"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Extract the output of a Generator to an Answer format, and build prompts. - category_slug: haystack-classes - title: Builder API - slug: builder-api - order: 5 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: builder_api.md diff --git a/docs/pydoc/config-preview/caching.yml b/docs/pydoc/config-preview/caching.yml deleted file mode 100644 index 7f2bc0e852..0000000000 --- a/docs/pydoc/config-preview/caching.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/caching] - modules: ["url_cache_checker"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Checks if any document coming from the given URL is already present in the store. - category_slug: haystack-classes - title: UrlCacheChecker API - slug: caching-api - order: 160 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: caching_api.md diff --git a/docs/pydoc/config-preview/classifier.yml b/docs/pydoc/config-preview/classifier.yml deleted file mode 100644 index 8a68fa696d..0000000000 --- a/docs/pydoc/config-preview/classifier.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/classifiers] - modules: ["document_language_classifier"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Detects the language of the Documents and routes them appropriately. - category_slug: haystack-classes - title: Language Classifier API - slug: language-classifier-api - order: 10 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: language_classifier_api.md diff --git a/docs/pydoc/config-preview/converter.yml b/docs/pydoc/config-preview/converter.yml deleted file mode 100644 index b6f3aae0df..0000000000 --- a/docs/pydoc/config-preview/converter.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/converters] - modules: ["azure", "html", "markdown", "pypdf", "tika", "txt"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Extracts text from files in different formats and converts it into the unified Document format. - category_slug: haystack-classes - title: Converter API - slug: converter-api - order: 50 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: converter_api.md diff --git a/docs/pydoc/config-preview/document_store.yml b/docs/pydoc/config-preview/document_store.yml deleted file mode 100644 index 6c51c46290..0000000000 --- a/docs/pydoc/config-preview/document_store.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/document_stores/in_memory] - modules: ["document_store"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Stores your texts and meta data and provides them to the Retriever at query time. - category_slug: haystack-classes - title: DocumentStore API - slug: document-store-api - order: 20 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: document_store.md diff --git a/docs/pydoc/config-preview/embedder.yml b/docs/pydoc/config-preview/embedder.yml deleted file mode 100644 index 077d292dc2..0000000000 --- a/docs/pydoc/config-preview/embedder.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/embedders] - modules: ["openai_document_embedder", "openai_text_embedder", "sentence_transformers_document_embedder", "sentence_transformers_text_embedder"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Transforms queries into vectors to look for similar or relevant Documents. - category_slug: haystack-classes - title: Embedder API - slug: embedder-api - order: 40 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: embedder_api.md diff --git a/docs/pydoc/config-preview/fetcher.yml b/docs/pydoc/config-preview/fetcher.yml deleted file mode 100644 index 7d7a6ee4b9..0000000000 --- a/docs/pydoc/config-preview/fetcher.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/fetchers] - modules: ["link_content"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Fetches content from a list of URLs and returns a list of extracted content streams. - category_slug: haystack-classes - title: LinkContentFetcher API - slug: fetcher-api - order: 70 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: fetcher_api.md diff --git a/docs/pydoc/config-preview/generator.yml b/docs/pydoc/config-preview/generator.yml deleted file mode 100644 index dbed3a820a..0000000000 --- a/docs/pydoc/config-preview/generator.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/generators] - modules: ["hugging_face_local", "hugging_face_tgi", "openai", "chat/hugging_face_tgi", "chat/openai"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Enables text generation using LLMs. - category_slug: haystack-classes - title: Generator API - slug: generator-api - order: 60 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: generator_api.md diff --git a/docs/pydoc/config-preview/pipeline.yml b/docs/pydoc/config-preview/pipeline.yml deleted file mode 100644 index 2b61de5451..0000000000 --- a/docs/pydoc/config-preview/pipeline.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview] - modules: ["pipeline"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Arranges components and integrations in flow. - category_slug: haystack-classes - title: Pipelines API - slug: pipelines-api - order: 80 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: pipelines_api.md diff --git a/docs/pydoc/config-preview/preprocessor.yml b/docs/pydoc/config-preview/preprocessor.yml deleted file mode 100644 index d2acf19da4..0000000000 --- a/docs/pydoc/config-preview/preprocessor.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/preprocessors] - modules: ["document_cleaner", "document_splitter"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Normalizes white spaces, gets rid of headers and footers, cleans empty lines in your Documents, or splits them into smaller pieces. - category_slug: haystack-classes - title: PreProcessor API - slug: preprocessor-api - order: 90 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: preprocessor_api.md diff --git a/docs/pydoc/config-preview/ranker.yml b/docs/pydoc/config-preview/ranker.yml deleted file mode 100644 index d0fbe6d687..0000000000 --- a/docs/pydoc/config-preview/ranker.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/rankers] - modules: ["meta_field", "transformers_similarity"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Reorders a set of Documents based on their relevance to the query. - category_slug: haystack-classes - title: Ranker API - slug: ranker-api - order: 110 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: ranker_api.md diff --git a/docs/pydoc/config-preview/reader.yml b/docs/pydoc/config-preview/reader.yml deleted file mode 100644 index 59163e4422..0000000000 --- a/docs/pydoc/config-preview/reader.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/readers] - modules: ["extractive"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Takes a query and a set of Documents as input and returns ExtractedAnswers by selecting a text span within the Documents. - category_slug: haystack-classes - title: Reader API - slug: reader-api - order: 120 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: reader_api.md diff --git a/docs/pydoc/config-preview/retriever.yml b/docs/pydoc/config-preview/retriever.yml deleted file mode 100644 index 805753de56..0000000000 --- a/docs/pydoc/config-preview/retriever.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/retrievers] - modules: ["in_memory_bm25_retriever", "in_memory_embedding_retriever"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Sweeps through a Document Store and returns a set of candidate Documents that are relevant to the query. - category_slug: haystack-classes - title: Retriever API - slug: retriever-api - order: 130 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: retriever_api.md diff --git a/docs/pydoc/config-preview/router.yml b/docs/pydoc/config-preview/router.yml deleted file mode 100644 index 4f01b3a869..0000000000 --- a/docs/pydoc/config-preview/router.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/routers] - modules: ["document_joiner", "file_type_router", "metadata_router", "text_language_router"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Routes data to the right component based on its file type or metadata. - category_slug: haystack-classes - title: Router API - slug: router-api - order: 140 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: router_api.md diff --git a/docs/pydoc/config-preview/sampler.yml b/docs/pydoc/config-preview/sampler.yml deleted file mode 100644 index fe1072ab6d..0000000000 --- a/docs/pydoc/config-preview/sampler.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/samplers] - modules: ["top_p"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Filters documents based on their similarity scores using top-p sampling. - category_slug: haystack-classes - title: TopPSampler API - slug: sampler-api - order: 150 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: sampler_api.md diff --git a/docs/pydoc/config-preview/websearch.yml b/docs/pydoc/config-preview/websearch.yml deleted file mode 100644 index 6433940e04..0000000000 --- a/docs/pydoc/config-preview/websearch.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/websearch] - modules: ["serper_dev"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Web search engine for Haystack. - category_slug: haystack-classes - title: Websearch API - slug: websearch-api - order: 170 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: websearch_api.md diff --git a/docs/pydoc/config-preview/whisper.yml b/docs/pydoc/config-preview/whisper.yml deleted file mode 100644 index 1d47d80163..0000000000 --- a/docs/pydoc/config-preview/whisper.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/audio] - modules: ["whisper_local", "whisper_remote"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Transcribes audio files. - category_slug: haystack-classes - title: WhisperTranscriber API - slug: whisper-transcriber-api - order: 180 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: whisper_transcriber_api.md diff --git a/docs/pydoc/config-preview/writer.yml b/docs/pydoc/config-preview/writer.yml deleted file mode 100644 index 9ff0095369..0000000000 --- a/docs/pydoc/config-preview/writer.yml +++ /dev/null @@ -1,26 +0,0 @@ -loaders: - - type: loaders.CustomPythonLoader - search_path: [../../../haystack/preview/components/writers] - modules: ["document_writer"] - ignore_when_discovered: ["__init__"] -processors: - - type: filter - expression: - documented_only: true - do_not_filter_modules: false - skip_empty_modules: true - - type: smart - - type: crossref -renderer: - type: renderers.ReadmePreviewRenderer - excerpt: Writes Documents to a DocumentStore. - category_slug: haystack-classes - title: DocumentWriter API - slug: writer-api - order: 30 - markdown: - descriptive_class_title: false - descriptive_module_title: true - add_method_class_prefix: true - add_member_class_prefix: false - filename: writer_api.md diff --git a/docs/pydoc/renderers.py b/docs/pydoc/renderers.py index 6450f666cb..c74a60dc7c 100644 --- a/docs/pydoc/renderers.py +++ b/docs/pydoc/renderers.py @@ -133,16 +133,3 @@ def _frontmatter(self) -> str: slug=self.slug, order=self.order, ) - - -@dataclasses.dataclass -class ReadmePreviewRenderer(ReadmeRenderer): - """ - This custom Renderer behaves just like the ReadmeRenderer but renders docs with the hardcoded version 2.0 to generate correct category ids. - """ - - def _doc_version(self) -> str: - """ - Returns the hardcoded docs version 2.0. - """ - return "v2.0" diff --git a/e2e/conftest.py b/e2e/conftest.py index 41d02927e2..7308073ca3 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -25,11 +25,6 @@ def samples_path(): return Path(__file__).parent / "samples" -@pytest.fixture -def preview_samples_path(): - return Path(__file__).parent / "preview" / "test_files" - - @pytest.fixture def docs_all_formats(): return [ diff --git a/e2e/preview/conftest.py b/e2e/preview/conftest.py deleted file mode 100644 index 3ad8d8a746..0000000000 --- a/e2e/preview/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -import pytest -from haystack.preview.testing.test_utils import set_all_seeds - -set_all_seeds(0) - - -@pytest.fixture -def samples_path(): - return Path(__file__).parent / "samples" diff --git a/e2e/preview/pipelines/test_dense_doc_search.py b/e2e/preview/pipelines/test_dense_doc_search.py deleted file mode 100644 index ae2612893d..0000000000 --- a/e2e/preview/pipelines/test_dense_doc_search.py +++ /dev/null @@ -1,84 +0,0 @@ -import json -from pathlib import Path - -from haystack.preview import Pipeline -from haystack.preview.components.embedders import SentenceTransformersDocumentEmbedder, SentenceTransformersTextEmbedder -from haystack.preview.components.converters import PyPDFToDocument, TextFileToDocument -from haystack.preview.components.preprocessors import DocumentCleaner, DocumentSplitter -from haystack.preview.components.routers import FileTypeRouter, DocumentJoiner -from haystack.preview.components.writers import DocumentWriter -from haystack.preview.document_stores import InMemoryDocumentStore -from haystack.preview.components.retrievers import InMemoryEmbeddingRetriever - - -def test_dense_doc_search_pipeline(tmp_path, samples_path): - # Create the indexing pipeline - indexing_pipeline = Pipeline() - indexing_pipeline.add_component( - instance=FileTypeRouter(mime_types=["text/plain", "application/pdf"]), name="file_type_router" - ) - indexing_pipeline.add_component(instance=TextFileToDocument(), name="text_file_converter") - indexing_pipeline.add_component(instance=PyPDFToDocument(), name="pdf_file_converter") - indexing_pipeline.add_component(instance=DocumentJoiner(), name="joiner") - indexing_pipeline.add_component(instance=DocumentCleaner(), name="cleaner") - indexing_pipeline.add_component( - instance=DocumentSplitter(split_by="sentence", split_length=250, split_overlap=30), name="splitter" - ) - indexing_pipeline.add_component( - instance=SentenceTransformersDocumentEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="embedder", - ) - indexing_pipeline.add_component(instance=DocumentWriter(document_store=InMemoryDocumentStore()), name="writer") - - indexing_pipeline.connect("file_type_router.text/plain", "text_file_converter.sources") - indexing_pipeline.connect("file_type_router.application/pdf", "pdf_file_converter.sources") - indexing_pipeline.connect("text_file_converter.documents", "joiner.documents") - indexing_pipeline.connect("pdf_file_converter.documents", "joiner.documents") - indexing_pipeline.connect("joiner.documents", "cleaner.documents") - indexing_pipeline.connect("cleaner.documents", "splitter.documents") - indexing_pipeline.connect("splitter.documents", "embedder.documents") - indexing_pipeline.connect("embedder.documents", "writer.documents") - - # Draw the indexing pipeline - indexing_pipeline.draw(tmp_path / "test_dense_doc_search_indexing_pipeline.png") - - # Serialize the indexing pipeline to JSON - with open(tmp_path / "test_dense_doc_search_indexing_pipeline.json", "w") as f: - print(json.dumps(indexing_pipeline.to_dict(), indent=4)) - json.dump(indexing_pipeline.to_dict(), f) - - # Load the indexing pipeline back - with open(tmp_path / "test_dense_doc_search_indexing_pipeline.json", "r") as f: - indexing_pipeline = Pipeline.from_dict(json.load(f)) - - indexing_result = indexing_pipeline.run({"file_type_router": {"sources": samples_path.iterdir()}}) - filled_document_store = indexing_pipeline.get_component("writer").document_store - - assert indexing_result["writer"]["documents_written"] == 2 - assert filled_document_store.count_documents() == 2 - - # Create the querying pipeline - query_pipeline = Pipeline() - query_pipeline.add_component( - instance=SentenceTransformersTextEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="text_embedder", - ) - query_pipeline.add_component( - instance=InMemoryEmbeddingRetriever(document_store=filled_document_store, top_k=20), name="embedding_retriever" - ) - query_pipeline.connect("text_embedder", "embedding_retriever") - - querying_result = query_pipeline.run({"text_embedder": {"text": "Who lives in Rome?"}}) - assert querying_result["embedding_retriever"]["documents"][0].content == "My name is Giorgio and I live in Rome." - - # Draw the querying pipeline - query_pipeline.draw(tmp_path / "test_dense_doc_search_query_pipeline.png") - - # Serialize the querying pipeline to JSON - with open(tmp_path / "test_dense_doc_search_query_pipeline.json", "w") as f: - print(json.dumps(query_pipeline.to_dict(), indent=4)) - json.dump(query_pipeline.to_dict(), f) - - # Load the querying pipeline back - with open(tmp_path / "test_dense_doc_search_query_pipeline.json", "r") as f: - query_pipeline = Pipeline.from_dict(json.load(f)) diff --git a/e2e/preview/pipelines/test_extractive_qa_pipeline.py b/e2e/preview/pipelines/test_extractive_qa_pipeline.py deleted file mode 100644 index 5af133dd40..0000000000 --- a/e2e/preview/pipelines/test_extractive_qa_pipeline.py +++ /dev/null @@ -1,67 +0,0 @@ -import json - -from haystack.preview import Pipeline, Document -from haystack.preview.document_stores import InMemoryDocumentStore -from haystack.preview.components.retrievers import InMemoryBM25Retriever -from haystack.preview.components.readers import ExtractiveReader - - -def test_extractive_qa_pipeline(tmp_path): - # Create the pipeline - qa_pipeline = Pipeline() - qa_pipeline.add_component(instance=InMemoryBM25Retriever(document_store=InMemoryDocumentStore()), name="retriever") - qa_pipeline.add_component(instance=ExtractiveReader(model_name_or_path="deepset/tinyroberta-squad2"), name="reader") - qa_pipeline.connect("retriever", "reader") - - # Draw the pipeline - qa_pipeline.draw(tmp_path / "test_extractive_qa_pipeline.png") - - # Serialize the pipeline to JSON - with open(tmp_path / "test_bm25_rag_pipeline.json", "w") as f: - print(json.dumps(qa_pipeline.to_dict(), indent=4)) - json.dump(qa_pipeline.to_dict(), f) - - # Load the pipeline back - with open(tmp_path / "test_bm25_rag_pipeline.json", "r") as f: - qa_pipeline = Pipeline.from_dict(json.load(f)) - - # Populate the document store - documents = [ - Document(content="My name is Jean and I live in Paris."), - Document(content="My name is Mark and I live in Berlin."), - Document(content="My name is Giorgio and I live in Rome."), - ] - qa_pipeline.get_component("retriever").document_store.write_documents(documents) - - # Query and assert - questions = ["Who lives in Paris?", "Who lives in Berlin?", "Who lives in Rome?"] - answers_spywords = ["Jean", "Mark", "Giorgio"] - - for question, spyword, doc in zip(questions, answers_spywords, documents): - result = qa_pipeline.run({"retriever": {"query": question}, "reader": {"query": question}}) - - extracted_answers = result["reader"]["answers"] - - # we expect at least one real answer and no_answer - assert len(extracted_answers) > 1 - - # the best answer should contain the spyword - assert spyword in extracted_answers[0].data - - # no_answer - assert extracted_answers[-1].data is None - - # since these questions are easily answerable, the best answer should have higher probability than no_answer - assert extracted_answers[0].probability >= extracted_answers[-1].probability - - for answer in extracted_answers: - assert answer.query == question - - assert hasattr(answer, "probability") - assert hasattr(answer, "start") - assert hasattr(answer, "end") - - assert hasattr(answer, "document") - # the answer is extracted from the correct document - if answer.document is not None: - assert answer.document.id == doc.id diff --git a/e2e/preview/pipelines/test_hybrid_doc_search_pipeline.py b/e2e/preview/pipelines/test_hybrid_doc_search_pipeline.py deleted file mode 100644 index e85db341d8..0000000000 --- a/e2e/preview/pipelines/test_hybrid_doc_search_pipeline.py +++ /dev/null @@ -1,57 +0,0 @@ -import json - -from haystack.preview import Pipeline, Document -from haystack.preview.components.embedders import SentenceTransformersTextEmbedder -from haystack.preview.components.rankers import TransformersSimilarityRanker -from haystack.preview.components.routers.document_joiner import DocumentJoiner -from haystack.preview.document_stores import InMemoryDocumentStore -from haystack.preview.components.retrievers import InMemoryBM25Retriever, InMemoryEmbeddingRetriever - - -def test_hybrid_doc_search_pipeline(tmp_path): - # Create the pipeline - document_store = InMemoryDocumentStore() - hybrid_pipeline = Pipeline() - hybrid_pipeline.add_component(instance=InMemoryBM25Retriever(document_store=document_store), name="bm25_retriever") - hybrid_pipeline.add_component( - instance=SentenceTransformersTextEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="text_embedder", - ) - hybrid_pipeline.add_component( - instance=InMemoryEmbeddingRetriever(document_store=document_store), name="embedding_retriever" - ) - hybrid_pipeline.add_component(instance=DocumentJoiner(), name="joiner") - hybrid_pipeline.add_component(instance=TransformersSimilarityRanker(top_k=20), name="ranker") - - hybrid_pipeline.connect("bm25_retriever", "joiner") - hybrid_pipeline.connect("text_embedder", "embedding_retriever") - hybrid_pipeline.connect("embedding_retriever", "joiner") - hybrid_pipeline.connect("joiner", "ranker") - - # Draw the pipeline - hybrid_pipeline.draw(tmp_path / "test_hybrid_doc_search_pipeline.png") - - # Serialize the pipeline to JSON - with open(tmp_path / "test_hybrid_doc_search_pipeline.json", "w") as f: - print(json.dumps(hybrid_pipeline.to_dict(), indent=4)) - json.dump(hybrid_pipeline.to_dict(), f) - - # Load the pipeline back - with open(tmp_path / "test_hybrid_doc_search_pipeline.json", "r") as f: - hybrid_pipeline = Pipeline.from_dict(json.load(f)) - - # Populate the document store - documents = [ - Document(content="My name is Jean and I live in Paris."), - Document(content="My name is Mark and I live in Berlin."), - Document(content="My name is Mario and I live in the capital of Italy."), - Document(content="My name is Giorgio and I live in Rome."), - ] - hybrid_pipeline.get_component("bm25_retriever").document_store.write_documents(documents) - - query = "Who lives in Rome?" - result = hybrid_pipeline.run( - {"bm25_retriever": {"query": query}, "text_embedder": {"text": query}, "ranker": {"query": query}} - ) - assert result["ranker"]["documents"][0].content == "My name is Giorgio and I live in Rome." - assert result["ranker"]["documents"][1].content == "My name is Mario and I live in the capital of Italy." diff --git a/e2e/preview/pipelines/test_preprocessing_pipeline.py b/e2e/preview/pipelines/test_preprocessing_pipeline.py deleted file mode 100644 index 2f16f1d993..0000000000 --- a/e2e/preview/pipelines/test_preprocessing_pipeline.py +++ /dev/null @@ -1,89 +0,0 @@ -import json - -from haystack.preview import Pipeline -from haystack.preview.components.embedders import SentenceTransformersDocumentEmbedder -from haystack.preview.components.converters import TextFileToDocument -from haystack.preview.components.preprocessors import DocumentSplitter, DocumentCleaner -from haystack.preview.components.classifiers import DocumentLanguageClassifier -from haystack.preview.components.routers import FileTypeRouter, MetadataRouter -from haystack.preview.components.writers import DocumentWriter -from haystack.preview.document_stores import InMemoryDocumentStore - - -def test_preprocessing_pipeline(tmp_path): - # Create the pipeline and its components - document_store = InMemoryDocumentStore() - preprocessing_pipeline = Pipeline() - preprocessing_pipeline.add_component(instance=FileTypeRouter(mime_types=["text/plain"]), name="file_type_router") - preprocessing_pipeline.add_component(instance=TextFileToDocument(), name="text_file_converter") - preprocessing_pipeline.add_component(instance=DocumentLanguageClassifier(), name="language_classifier") - preprocessing_pipeline.add_component( - instance=MetadataRouter(rules={"en": {"field": "language", "operator": "==", "value": "en"}}), name="router" - ) - preprocessing_pipeline.add_component(instance=DocumentCleaner(), name="cleaner") - preprocessing_pipeline.add_component( - instance=DocumentSplitter(split_by="sentence", split_length=1), name="splitter" - ) - preprocessing_pipeline.add_component( - instance=SentenceTransformersDocumentEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="embedder", - ) - preprocessing_pipeline.add_component(instance=DocumentWriter(document_store=document_store), name="writer") - preprocessing_pipeline.connect("file_type_router.text/plain", "text_file_converter.sources") - preprocessing_pipeline.connect("text_file_converter.documents", "language_classifier.documents") - preprocessing_pipeline.connect("language_classifier.documents", "router.documents") - preprocessing_pipeline.connect("router.en", "cleaner.documents") - preprocessing_pipeline.connect("cleaner.documents", "splitter.documents") - preprocessing_pipeline.connect("splitter.documents", "embedder.documents") - preprocessing_pipeline.connect("embedder.documents", "writer.documents") - - # Draw the pipeline - preprocessing_pipeline.draw(tmp_path / "test_preprocessing_pipeline.png") - - # Serialize the pipeline to JSON - with open(tmp_path / "test_preprocessing_pipeline.json", "w") as f: - print(json.dumps(preprocessing_pipeline.to_dict(), indent=4)) - json.dump(preprocessing_pipeline.to_dict(), f) - - # Load the pipeline back - with open(tmp_path / "test_preprocessing_pipeline.json", "r") as f: - preprocessing_pipeline = Pipeline.from_dict(json.load(f)) - - # Write a txt file - with open(tmp_path / "test_file_english.txt", "w") as f: - f.write( - "This is an english sentence. There is more to it. It's a long text." - "Spans multiple lines." - "" - "Even contains empty lines. And extra whitespaces." - ) - - # Write a txt file - with open(tmp_path / "test_file_german.txt", "w") as f: - f.write("Ein deutscher Satz ohne Verb.") - - # Add two txt files and one non-txt file - paths = [ - tmp_path / "test_file_english.txt", - tmp_path / "test_file_german.txt", - tmp_path / "test_preprocessing_pipeline.json", - ] - - result = preprocessing_pipeline.run({"file_type_router": {"sources": paths}}) - - assert result["writer"]["documents_written"] == 6 - filled_document_store = preprocessing_pipeline.get_component("writer").document_store - assert filled_document_store.count_documents() == 6 - - # Check preprocessed texts - stored_documents = filled_document_store.filter_documents() - expected_texts = [ - "This is an english sentence.", - " There is more to it.", - " It's a long text.", - "Spans multiple lines.", - "Even contains empty lines.", - " And extra whitespaces.", - ] - assert expected_texts == [document.content for document in stored_documents] - assert all(document.meta["language"] == "en" for document in stored_documents) diff --git a/e2e/preview/pipelines/test_rag_pipelines.py b/e2e/preview/pipelines/test_rag_pipelines.py deleted file mode 100644 index d05c0fabea..0000000000 --- a/e2e/preview/pipelines/test_rag_pipelines.py +++ /dev/null @@ -1,159 +0,0 @@ -import os -import json -import pytest - -from haystack.preview import Pipeline, Document -from haystack.preview.document_stores import InMemoryDocumentStore -from haystack.preview.components.writers import DocumentWriter -from haystack.preview.components.retrievers import InMemoryBM25Retriever, InMemoryEmbeddingRetriever -from haystack.preview.components.embedders import SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder -from haystack.preview.components.generators import GPTGenerator -from haystack.preview.components.builders.answer_builder import AnswerBuilder -from haystack.preview.components.builders.prompt_builder import PromptBuilder - - -@pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", -) -def test_bm25_rag_pipeline(tmp_path): - # Create the RAG pipeline - prompt_template = """ - Given these documents, answer the question.\nDocuments: - {% for doc in documents %} - {{ doc.content }} - {% endfor %} - - \nQuestion: {{question}} - \nAnswer: - """ - rag_pipeline = Pipeline() - rag_pipeline.add_component(instance=InMemoryBM25Retriever(document_store=InMemoryDocumentStore()), name="retriever") - rag_pipeline.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder") - rag_pipeline.add_component(instance=GPTGenerator(api_key=os.environ.get("OPENAI_API_KEY")), name="llm") - rag_pipeline.add_component(instance=AnswerBuilder(), name="answer_builder") - rag_pipeline.connect("retriever", "prompt_builder.documents") - rag_pipeline.connect("prompt_builder", "llm") - rag_pipeline.connect("llm.replies", "answer_builder.replies") - rag_pipeline.connect("llm.metadata", "answer_builder.metadata") - rag_pipeline.connect("retriever", "answer_builder.documents") - - # Draw the pipeline - rag_pipeline.draw(tmp_path / "test_bm25_rag_pipeline.png") - - # Serialize the pipeline to JSON - with open(tmp_path / "test_bm25_rag_pipeline.json", "w") as f: - json.dump(rag_pipeline.to_dict(), f) - - # Load the pipeline back - with open(tmp_path / "test_bm25_rag_pipeline.json", "r") as f: - rag_pipeline = Pipeline.from_dict(json.load(f)) - - # Populate the document store - documents = [ - Document(content="My name is Jean and I live in Paris."), - Document(content="My name is Mark and I live in Berlin."), - Document(content="My name is Giorgio and I live in Rome."), - ] - rag_pipeline.get_component("retriever").document_store.write_documents(documents) - - # Query and assert - questions = ["Who lives in Paris?", "Who lives in Berlin?", "Who lives in Rome?"] - answers_spywords = ["Jean", "Mark", "Giorgio"] - - for question, spyword in zip(questions, answers_spywords): - result = rag_pipeline.run( - { - "retriever": {"query": question}, - "prompt_builder": {"question": question}, - "answer_builder": {"query": question}, - } - ) - - assert len(result["answer_builder"]["answers"]) == 1 - generated_answer = result["answer_builder"]["answers"][0] - assert spyword in generated_answer.data - assert generated_answer.query == question - assert hasattr(generated_answer, "documents") - assert hasattr(generated_answer, "metadata") - - -@pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", -) -def test_embedding_retrieval_rag_pipeline(tmp_path): - # Create the RAG pipeline - prompt_template = """ - Given these documents, answer the question.\nDocuments: - {% for doc in documents %} - {{ doc.content }} - {% endfor %} - - \nQuestion: {{question}} - \nAnswer: - """ - rag_pipeline = Pipeline() - rag_pipeline.add_component( - instance=SentenceTransformersTextEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="text_embedder", - ) - rag_pipeline.add_component( - instance=InMemoryEmbeddingRetriever(document_store=InMemoryDocumentStore()), name="retriever" - ) - rag_pipeline.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder") - rag_pipeline.add_component(instance=GPTGenerator(api_key=os.environ.get("OPENAI_API_KEY")), name="llm") - rag_pipeline.add_component(instance=AnswerBuilder(), name="answer_builder") - rag_pipeline.connect("text_embedder", "retriever") - rag_pipeline.connect("retriever", "prompt_builder.documents") - rag_pipeline.connect("prompt_builder", "llm") - rag_pipeline.connect("llm.replies", "answer_builder.replies") - rag_pipeline.connect("llm.metadata", "answer_builder.metadata") - rag_pipeline.connect("retriever", "answer_builder.documents") - - # Draw the pipeline - rag_pipeline.draw(tmp_path / "test_embedding_rag_pipeline.png") - - # Serialize the pipeline to JSON - with open(tmp_path / "test_embedding_rag_pipeline.json", "w") as f: - json.dump(rag_pipeline.to_dict(), f) - - # Load the pipeline back - with open(tmp_path / "test_embedding_rag_pipeline.json", "r") as f: - rag_pipeline = Pipeline.from_dict(json.load(f)) - - # Populate the document store - documents = [ - Document(content="My name is Jean and I live in Paris."), - Document(content="My name is Mark and I live in Berlin."), - Document(content="My name is Giorgio and I live in Rome."), - ] - document_store = rag_pipeline.get_component("retriever").document_store - indexing_pipeline = Pipeline() - indexing_pipeline.add_component( - instance=SentenceTransformersDocumentEmbedder(model_name_or_path="sentence-transformers/all-MiniLM-L6-v2"), - name="document_embedder", - ) - indexing_pipeline.add_component(instance=DocumentWriter(document_store=document_store), name="document_writer") - indexing_pipeline.connect("document_embedder", "document_writer") - indexing_pipeline.run({"document_embedder": {"documents": documents}}) - - # Query and assert - questions = ["Who lives in Paris?", "Who lives in Berlin?", "Who lives in Rome?"] - answers_spywords = ["Jean", "Mark", "Giorgio"] - - for question, spyword in zip(questions, answers_spywords): - result = rag_pipeline.run( - { - "text_embedder": {"text": question}, - "prompt_builder": {"question": question}, - "answer_builder": {"query": question}, - } - ) - - assert len(result["answer_builder"]["answers"]) == 1 - generated_answer = result["answer_builder"]["answers"][0] - assert spyword in generated_answer.data - assert generated_answer.query == question - assert hasattr(generated_answer, "documents") - assert hasattr(generated_answer, "metadata") diff --git a/e2e/preview/samples/doc_1.txt b/e2e/preview/samples/doc_1.txt deleted file mode 100644 index 1d3da15eb9..0000000000 --- a/e2e/preview/samples/doc_1.txt +++ /dev/null @@ -1 +0,0 @@ -My name is Giorgio and I live in Rome. diff --git a/e2e/preview/samples/sample_pdf_1.pdf b/e2e/preview/samples/sample_pdf_1.pdf deleted file mode 100644 index 87259b897f83b462f521276bf32d210ea008bcd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44524 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

1p2->@_y0X+Ac~V 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 LF=KrFr&;|4#k|z* diff --git a/examples/preview/retrievers/in_memory_bm25_documentsearch.py b/examples/preview/retrievers/in_memory_bm25_documentsearch.py deleted file mode 100644 index e153bbefa6..0000000000 --- a/examples/preview/retrievers/in_memory_bm25_documentsearch.py +++ /dev/null @@ -1,28 +0,0 @@ -from haystack.preview import Document -from haystack.preview.components.retrievers import InMemoryBM25Retriever -from haystack.preview.document_stores import InMemoryDocumentStore -from haystack.preview.pipeline import Pipeline - -# Create components and a query pipeline -document_store = InMemoryDocumentStore() -retriever = InMemoryBM25Retriever(document_store=document_store) - -pipeline = Pipeline() -pipeline.add_component(instance=retriever, name="retriever") - -# Add Documents -documents = [ - Document(content="There are over 7,000 languages spoken around the world today."), - Document( - content="Elephants have been observed to behave in a way that indicates a high level of self-awareness, such as recognizing themselves in mirrors." - ), - Document( - content="In certain parts of the world, like the Maldives, Puerto Rico, and San Diego, you can witness the phenomenon of bioluminescent waves." - ), -] -document_store.write_documents(documents) - -# Run the pipeline -result = pipeline.run(data={"retriever": {"query": "How many languages are there?"}}) - -print(result["retriever"]["documents"][0]) diff --git a/examples/preview/retrievers/in_memory_bm25_rag.py b/examples/preview/retrievers/in_memory_bm25_rag.py deleted file mode 100644 index ebb9ec5b00..0000000000 --- a/examples/preview/retrievers/in_memory_bm25_rag.py +++ /dev/null @@ -1,53 +0,0 @@ -import os - -from haystack.preview import Document -from haystack.preview import Pipeline -from haystack.preview.components.builders.answer_builder import AnswerBuilder -from haystack.preview.components.builders.prompt_builder import PromptBuilder -from haystack.preview.components.generators import GPTGenerator -from haystack.preview.components.retrievers import InMemoryBM25Retriever -from haystack.preview.document_stores import InMemoryDocumentStore - -# Create a RAG query pipeline -prompt_template = """ - Given these documents, answer the question.\nDocuments: - {% for doc in documents %} - {{ doc.content }} - {% endfor %} - - \nQuestion: {{question}} - \nAnswer: - """ - -rag_pipeline = Pipeline() -rag_pipeline.add_component(instance=InMemoryBM25Retriever(document_store=InMemoryDocumentStore()), name="retriever") -rag_pipeline.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder") -rag_pipeline.add_component(instance=GPTGenerator(api_key=os.environ.get("OPENAI_API_KEY")), name="llm") -rag_pipeline.add_component(instance=AnswerBuilder(), name="answer_builder") -rag_pipeline.connect("retriever", "prompt_builder.documents") -rag_pipeline.connect("prompt_builder", "llm") -rag_pipeline.connect("llm.replies", "answer_builder.replies") -rag_pipeline.connect("llm.metadata", "answer_builder.metadata") -rag_pipeline.connect("retriever", "answer_builder.documents") - -# Draw the pipeline -rag_pipeline.draw("./rag_pipeline.png") - -# Add Documents -documents = [ - Document(content="There are over 7,000 languages spoken around the world today."), - Document( - content="Elephants have been observed to behave in a way that indicates a high level of self-awareness, such as recognizing themselves in mirrors." - ), - Document( - content="In certain parts of the world, like the Maldives, Puerto Rico, and San Diego, you can witness the phenomenon of bioluminescent waves." - ), -] -rag_pipeline.get_component("retriever").document_store.write_documents(documents) - -# Run the pipeline -question = "How many languages are there?" -result = rag_pipeline.run( - {"retriever": {"query": question}, "prompt_builder": {"question": question}, "answer_builder": {"query": question}} -) -print(result["answer_builder"]["answers"][0]) diff --git a/pyproject.toml b/pyproject.toml index 8def0a45c3..1a1c0e94b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,21 +85,6 @@ dependencies = [ ] [project.optional-dependencies] -preview = [ - "canals==0.10.1", - "requests", - "pandas", - "rank_bm25", - "tqdm", - "tenacity", - "lazy-imports", - "posthog", # telemetry - - "Jinja2", - "openai<1.0.0", - "pyyaml", - "more-itertools", # DocumentSplitter -] inference = [ "transformers[torch,sentencepiece]==4.35.2", "sentence-transformers>=2.2.0", # See haystack/nodes/retriever/_embedding_encoder.py, _SentenceTransformersEmbeddingEncoder @@ -239,11 +224,11 @@ audio = [ ] all = [ - "farm-haystack[inference,docstores,crawler,preprocessing,file-conversion,pdf,ocr,metrics,aws,preview,audio]", + "farm-haystack[inference,docstores,crawler,preprocessing,file-conversion,pdf,ocr,metrics,aws,audio]", ] all-gpu = [ # beir is incompatible with faiss-gpu: https://github.com/beir-cellar/beir/issues/71 - "farm-haystack[inference,docstores-gpu,crawler,preprocessing,file-conversion,pdf,ocr,metrics,aws,preview,audio]", + "farm-haystack[inference,docstores-gpu,crawler,preprocessing,file-conversion,pdf,ocr,metrics,aws,audio]", ] [project.scripts] @@ -425,7 +410,6 @@ max-complexity = 28 [tool.ruff.per-file-ignores] "examples/basic_qa_pipeline.py" = ["C416"] -"haystack/preview/testing/document_store.py" = ["C416", "F821"] "haystack/telemetry.py" = ["F821"] [tool.ruff.pylint] diff --git a/releasenotes/config.yaml b/releasenotes/config.yaml index 4389c77c25..80a6b19769 100644 --- a/releasenotes/config.yaml +++ b/releasenotes/config.yaml @@ -36,10 +36,7 @@ template: | fixes: - | Add normal bug fixes here, or remove this section. - preview: - - | - Add changes to Haystack version 2, or remove this section. - Haystack version 2 can be found under haystack/preview. + sections: # The prelude section is implicitly included. - [upgrade, ⬆️ Upgrade Notes] @@ -49,4 +46,3 @@ sections: - [deprecations, ⚠️ Deprecation Notes] - [security, Security Notes] - [fixes, 🐛 Bug Fixes] - - [preview, 🩵 Haystack 2.0 preview] diff --git a/test/conftest.py b/test/conftest.py index 9e6fdb7426..9343e56b73 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -896,11 +896,6 @@ def sample_txt_file_paths_list(samples_path): return list((samples_path / "docs").glob("*.txt")) -@pytest.fixture -def preview_samples_path(): - return Path(__file__).parent / "preview" / "test_files" - - @pytest.fixture(autouse=True) def request_blocker(request: pytest.FixtureRequest, monkeypatch): """ diff --git a/test/preview/components/audio/__init__.py b/test/preview/components/audio/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/audio/test_whisper_local.py b/test/preview/components/audio/test_whisper_local.py deleted file mode 100644 index df23b12018..0000000000 --- a/test/preview/components/audio/test_whisper_local.py +++ /dev/null @@ -1,170 +0,0 @@ -import sys -from pathlib import Path -from unittest.mock import patch, MagicMock - -import pytest -import torch - -from haystack.preview.dataclasses import Document -from haystack.preview.components.audio import LocalWhisperTranscriber - - -SAMPLES_PATH = Path(__file__).parent.parent.parent / "test_files" - - -class TestLocalWhisperTranscriber: - @pytest.mark.unit - def test_init(self): - transcriber = LocalWhisperTranscriber( - model_name_or_path="large-v2" - ) # Doesn't matter if it's huge, the model is not loaded in init. - assert transcriber.model_name == "large-v2" - assert transcriber.device == torch.device("cpu") - assert transcriber._model is None - - @pytest.mark.unit - def test_init_wrong_model(self): - with pytest.raises(ValueError, match="Model name 'whisper-1' not recognized"): - LocalWhisperTranscriber(model_name_or_path="whisper-1") - - @pytest.mark.unit - def test_to_dict(self): - transcriber = LocalWhisperTranscriber() - data = transcriber.to_dict() - assert data == { - "type": "haystack.preview.components.audio.whisper_local.LocalWhisperTranscriber", - "init_parameters": {"model_name_or_path": "large", "device": "cpu", "whisper_params": {}}, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - transcriber = LocalWhisperTranscriber( - model_name_or_path="tiny", - device="cuda", - whisper_params={"return_segments": True, "temperature": [0.1, 0.6, 0.8]}, - ) - data = transcriber.to_dict() - assert data == { - "type": "haystack.preview.components.audio.whisper_local.LocalWhisperTranscriber", - "init_parameters": { - "model_name_or_path": "tiny", - "device": "cuda", - "whisper_params": {"return_segments": True, "temperature": [0.1, 0.6, 0.8]}, - }, - } - - @pytest.mark.unit - def test_warmup(self): - with patch("haystack.preview.components.audio.whisper_local.whisper") as mocked_whisper: - transcriber = LocalWhisperTranscriber(model_name_or_path="large-v2") - mocked_whisper.load_model.assert_not_called() - transcriber.warm_up() - mocked_whisper.load_model.assert_called_once_with("large-v2", device=torch.device(type="cpu")) - - @pytest.mark.unit - def test_warmup_doesnt_reload(self): - with patch("haystack.preview.components.audio.whisper_local.whisper") as mocked_whisper: - transcriber = LocalWhisperTranscriber(model_name_or_path="large-v2") - transcriber.warm_up() - transcriber.warm_up() - mocked_whisper.load_model.assert_called_once() - - @pytest.mark.unit - def test_run_with_path(self): - comp = LocalWhisperTranscriber(model_name_or_path="large-v2") - comp._model = MagicMock() - comp._model.transcribe.return_value = { - "text": "test transcription", - "other_metadata": ["other", "meta", "data"], - } - results = comp.run(audio_files=[SAMPLES_PATH / "audio" / "this is the content of the document.wav"]) - expected = Document( - content="test transcription", - meta={ - "audio_file": SAMPLES_PATH / "audio" / "this is the content of the document.wav", - "other_metadata": ["other", "meta", "data"], - }, - ) - assert results["documents"] == [expected] - - @pytest.mark.unit - def test_run_with_str(self): - comp = LocalWhisperTranscriber(model_name_or_path="large-v2") - comp._model = MagicMock() - comp._model.transcribe.return_value = { - "text": "test transcription", - "other_metadata": ["other", "meta", "data"], - } - results = comp.run( - audio_files=[str((SAMPLES_PATH / "audio" / "this is the content of the document.wav").absolute())] - ) - expected = Document( - content="test transcription", - meta={ - "audio_file": str((SAMPLES_PATH / "audio" / "this is the content of the document.wav").absolute()), - "other_metadata": ["other", "meta", "data"], - }, - ) - assert results["documents"] == [expected] - - @pytest.mark.unit - def test_transcribe(self): - comp = LocalWhisperTranscriber(model_name_or_path="large-v2") - comp._model = MagicMock() - comp._model.transcribe.return_value = { - "text": "test transcription", - "other_metadata": ["other", "meta", "data"], - } - results = comp.transcribe(audio_files=[SAMPLES_PATH / "audio" / "this is the content of the document.wav"]) - expected = Document( - content="test transcription", - meta={ - "audio_file": SAMPLES_PATH / "audio" / "this is the content of the document.wav", - "other_metadata": ["other", "meta", "data"], - }, - ) - assert results == [expected] - - @pytest.mark.unit - def test_transcribe_stream(self): - comp = LocalWhisperTranscriber(model_name_or_path="large-v2") - comp._model = MagicMock() - comp._model.transcribe.return_value = { - "text": "test transcription", - "other_metadata": ["other", "meta", "data"], - } - results = comp.transcribe( - audio_files=[open(SAMPLES_PATH / "audio" / "this is the content of the document.wav", "rb")] - ) - expected = Document( - content="test transcription", - meta={"audio_file": "<>", "other_metadata": ["other", "meta", "data"]}, - ) - assert results == [expected] - - @pytest.mark.integration - @pytest.mark.skipif(sys.platform in ["win32", "cygwin"], reason="ffmpeg not installed on Windows CI") - def test_whisper_local_transcriber(self, preview_samples_path): - comp = LocalWhisperTranscriber(model_name_or_path="medium", whisper_params={"language": "english"}) - comp.warm_up() - output = comp.run( - audio_files=[ - preview_samples_path / "audio" / "this is the content of the document.wav", - str((preview_samples_path / "audio" / "the context for this answer is here.wav").absolute()), - open(preview_samples_path / "audio" / "answer.wav", "rb"), - ] - ) - docs = output["documents"] - assert len(docs) == 3 - - assert docs[0].content.strip().lower() == "this is the content of the document." - assert preview_samples_path / "audio" / "this is the content of the document.wav" == docs[0].meta["audio_file"] - - assert docs[1].content.strip().lower() == "the context for this answer is here." - assert ( - str((preview_samples_path / "audio" / "the context for this answer is here.wav").absolute()) - == docs[1].meta["audio_file"] - ) - - assert docs[2].content.strip().lower() == "answer." - assert docs[2].meta["audio_file"] == "<>" diff --git a/test/preview/components/audio/test_whisper_remote.py b/test/preview/components/audio/test_whisper_remote.py deleted file mode 100644 index df6b8067f5..0000000000 --- a/test/preview/components/audio/test_whisper_remote.py +++ /dev/null @@ -1,254 +0,0 @@ -import os -from unittest.mock import patch -from pathlib import Path - -import openai -import pytest -from openai.util import convert_to_openai_object - -from haystack.preview.components.audio.whisper_remote import RemoteWhisperTranscriber -from haystack.preview.dataclasses import ByteStream - - -def mock_openai_response(response_format="json", **kwargs) -> openai.openai_object.OpenAIObject: - if response_format == "json": - dict_response = {"text": "test transcription"} - # Currently only "json" is supported. - else: - dict_response = {} - - return convert_to_openai_object(dict_response) - - -class TestRemoteWhisperTranscriber: - @pytest.mark.unit - def test_init_no_key(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - error_msg = "RemoteWhisperTranscriber expects an OpenAI API key." - with pytest.raises(ValueError, match=error_msg): - RemoteWhisperTranscriber(api_key=None) - - def test_init_key_env_var(self, monkeypatch): - openai.api_key = None - monkeypatch.setenv("OPENAI_API_KEY", "test_api_key") - RemoteWhisperTranscriber(api_key=None) - assert openai.api_key == "test_api_key" - - def test_init_key_module_env_and_global_var(self, monkeypatch): - monkeypatch.setenv("OPENAI_API_KEY", "test_api_key_2") - openai.api_key = "test_api_key_1" - RemoteWhisperTranscriber(api_key=None) - # The module global variable takes preference - assert openai.api_key == "test_api_key_1" - - @pytest.mark.unit - def test_init_default(self): - transcriber = RemoteWhisperTranscriber(api_key="test_api_key") - - assert openai.api_key == "test_api_key" - assert transcriber.model_name == "whisper-1" - assert transcriber.organization is None - assert transcriber.api_base_url == "https://api.openai.com/v1" - assert transcriber.whisper_params == {"response_format": "json"} - - @pytest.mark.unit - def test_init_custom_parameters(self): - transcriber = RemoteWhisperTranscriber( - api_key="test_api_key", - model_name="whisper-1", - organization="test-org", - api_base_url="test_api_url", - language="en", - prompt="test-prompt", - response_format="json", - temperature="0.5", - ) - - assert openai.api_key == "test_api_key" - assert transcriber.model_name == "whisper-1" - assert transcriber.organization == "test-org" - assert transcriber.api_base_url == "test_api_url" - assert transcriber.whisper_params == { - "language": "en", - "prompt": "test-prompt", - "response_format": "json", - "temperature": "0.5", - } - - @pytest.mark.unit - def test_to_dict_default_parameters(self): - transcriber = RemoteWhisperTranscriber(api_key="test_api_key") - data = transcriber.to_dict() - assert data == { - "type": "haystack.preview.components.audio.whisper_remote.RemoteWhisperTranscriber", - "init_parameters": { - "model_name": "whisper-1", - "api_base_url": "https://api.openai.com/v1", - "organization": None, - "response_format": "json", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - transcriber = RemoteWhisperTranscriber( - api_key="test_api_key", - model_name="whisper-1", - organization="test-org", - api_base_url="test_api_url", - language="en", - prompt="test-prompt", - response_format="json", - temperature="0.5", - ) - data = transcriber.to_dict() - assert data == { - "type": "haystack.preview.components.audio.whisper_remote.RemoteWhisperTranscriber", - "init_parameters": { - "model_name": "whisper-1", - "organization": "test-org", - "api_base_url": "test_api_url", - "language": "en", - "prompt": "test-prompt", - "response_format": "json", - "temperature": "0.5", - }, - } - - def test_from_dict_with_defualt_parameters(self, monkeypatch): - monkeypatch.setenv("OPENAI_API_KEY", "test_api_key") - - data = { - "type": "haystack.preview.components.audio.whisper_remote.RemoteWhisperTranscriber", - "init_parameters": { - "model_name": "whisper-1", - "api_base_url": "https://api.openai.com/v1", - "organization": None, - "response_format": "json", - }, - } - - transcriber = RemoteWhisperTranscriber.from_dict(data) - - assert openai.api_key == "test_api_key" - assert transcriber.model_name == "whisper-1" - assert transcriber.organization is None - assert transcriber.api_base_url == "https://api.openai.com/v1" - assert transcriber.whisper_params == {"response_format": "json"} - - def test_from_dict_with_custom_init_parameters(self, monkeypatch): - monkeypatch.setenv("OPENAI_API_KEY", "test_api_key") - - data = { - "type": "haystack.preview.components.audio.whisper_remote.RemoteWhisperTranscriber", - "init_parameters": { - "model_name": "whisper-1", - "organization": "test-org", - "api_base_url": "test_api_url", - "language": "en", - "prompt": "test-prompt", - "response_format": "json", - "temperature": "0.5", - }, - } - transcriber = RemoteWhisperTranscriber.from_dict(data) - - assert openai.api_key == "test_api_key" - assert transcriber.model_name == "whisper-1" - assert transcriber.organization == "test-org" - assert transcriber.api_base_url == "test_api_url" - assert transcriber.whisper_params == { - "language": "en", - "prompt": "test-prompt", - "response_format": "json", - "temperature": "0.5", - } - - def test_from_dict_with_defualt_parameters_no_env_var(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - - data = { - "type": "haystack.preview.components.audio.whisper_remote.RemoteWhisperTranscriber", - "init_parameters": { - "model_name": "whisper-1", - "api_base_url": "https://api.openai.com/v1", - "organization": None, - "response_format": "json", - }, - } - - with pytest.raises(ValueError, match="RemoteWhisperTranscriber expects an OpenAI API key."): - RemoteWhisperTranscriber.from_dict(data) - - @pytest.mark.unit - def test_run_str(self, preview_samples_path): - with patch("haystack.preview.components.audio.whisper_remote.openai.Audio") as openai_audio_patch: - model = "whisper-1" - file_path = str(preview_samples_path / "audio" / "this is the content of the document.wav") - openai_audio_patch.transcribe.side_effect = mock_openai_response - - transcriber = RemoteWhisperTranscriber(api_key="test_api_key", model_name=model, response_format="json") - result = transcriber.run(sources=[file_path]) - - assert result["documents"][0].content == "test transcription" - assert result["documents"][0].meta["file_path"] == file_path - - @pytest.mark.unit - def test_run_path(self, preview_samples_path): - with patch("haystack.preview.components.audio.whisper_remote.openai.Audio") as openai_audio_patch: - model = "whisper-1" - file_path = preview_samples_path / "audio" / "this is the content of the document.wav" - openai_audio_patch.transcribe.side_effect = mock_openai_response - - transcriber = RemoteWhisperTranscriber(api_key="test_api_key", model_name=model, response_format="json") - result = transcriber.run(sources=[file_path]) - - assert result["documents"][0].content == "test transcription" - assert result["documents"][0].meta["file_path"] == file_path - - @pytest.mark.unit - def test_run_bytestream(self, preview_samples_path): - with patch("haystack.preview.components.audio.whisper_remote.openai.Audio") as openai_audio_patch: - model = "whisper-1" - file_path = preview_samples_path / "audio" / "this is the content of the document.wav" - openai_audio_patch.transcribe.side_effect = mock_openai_response - - transcriber = RemoteWhisperTranscriber(api_key="test_api_key", model_name=model, response_format="json") - with open(file_path, "rb") as audio_stream: - byte_stream = audio_stream.read() - audio_file = ByteStream(byte_stream, metadata={"file_path": str(file_path.absolute())}) - - result = transcriber.run(sources=[audio_file]) - - assert result["documents"][0].content == "test transcription" - assert result["documents"][0].meta["file_path"] == str(file_path.absolute()) - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_whisper_remote_transcriber(self, preview_samples_path): - transcriber = RemoteWhisperTranscriber(api_key=os.environ.get("OPENAI_API_KEY")) - - paths = [ - preview_samples_path / "audio" / "this is the content of the document.wav", - str(preview_samples_path / "audio" / "the context for this answer is here.wav"), - ByteStream.from_file_path(preview_samples_path / "audio" / "answer.wav"), - ] - - output = transcriber.run(sources=paths) - - docs = output["documents"] - assert len(docs) == 3 - assert docs[0].content.strip().lower() == "this is the content of the document." - assert preview_samples_path / "audio" / "this is the content of the document.wav" == docs[0].meta["file_path"] - - assert docs[1].content.strip().lower() == "the context for this answer is here." - assert ( - str(preview_samples_path / "audio" / "the context for this answer is here.wav") == docs[1].meta["file_path"] - ) - - assert docs[2].content.strip().lower() == "answer." diff --git a/test/preview/components/builders/__init__.py b/test/preview/components/builders/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/builders/test_answer_builder.py b/test/preview/components/builders/test_answer_builder.py deleted file mode 100644 index 7ce6d56e7b..0000000000 --- a/test/preview/components/builders/test_answer_builder.py +++ /dev/null @@ -1,154 +0,0 @@ -import logging - -import pytest - -from haystack.preview import GeneratedAnswer, Document -from haystack.preview.components.builders.answer_builder import AnswerBuilder - - -class TestAnswerBuilder: - @pytest.mark.unit - def test_run_unmatching_input_len(self): - component = AnswerBuilder() - with pytest.raises(ValueError): - component.run(query="query", replies=["reply1"], metadata=[{"test": "meta"}, {"test": "meta2"}]) - - @pytest.mark.unit - def test_run_without_meta(self): - component = AnswerBuilder() - output = component.run(query="query", replies=["reply1"]) - answers = output["answers"] - assert answers[0].data == "reply1" - assert answers[0].metadata == {} - assert answers[0].query == "query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - @pytest.mark.unit - def test_run_meta_is_an_empty_list(self): - component = AnswerBuilder() - output = component.run(query="query", replies=["reply1"], metadata=[]) - answers = output["answers"] - assert answers[0].data == "reply1" - assert answers[0].metadata == {} - assert answers[0].query == "query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - def test_run_without_pattern(self): - component = AnswerBuilder() - output = component.run(query="test query", replies=["Answer: AnswerString"], metadata=[{}]) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "Answer: AnswerString" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - def test_run_with_pattern_with_capturing_group(self): - component = AnswerBuilder(pattern=r"Answer: (.*)") - output = component.run(query="test query", replies=["Answer: AnswerString"], metadata=[{}]) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "AnswerString" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - def test_run_with_pattern_without_capturing_group(self): - component = AnswerBuilder(pattern=r"'.*'") - output = component.run(query="test query", replies=["Answer: 'AnswerString'"], metadata=[{}]) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "'AnswerString'" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - def test_run_with_pattern_with_more_than_one_capturing_group(self): - with pytest.raises(ValueError, match="contains multiple capture groups"): - AnswerBuilder(pattern=r"Answer: (.*), (.*)") - - def test_run_with_pattern_set_at_runtime(self): - component = AnswerBuilder(pattern="unused pattern") - output = component.run( - query="test query", replies=["Answer: AnswerString"], metadata=[{}], pattern=r"Answer: (.*)" - ) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "AnswerString" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert answers[0].documents == [] - assert isinstance(answers[0], GeneratedAnswer) - - def test_run_with_documents_without_reference_pattern(self): - component = AnswerBuilder() - output = component.run( - query="test query", - replies=["Answer: AnswerString"], - metadata=[{}], - documents=[Document(content="test doc 1"), Document(content="test doc 2")], - ) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "Answer: AnswerString" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert len(answers[0].documents) == 2 - assert answers[0].documents[0].content == "test doc 1" - assert answers[0].documents[1].content == "test doc 2" - - def test_run_with_documents_with_reference_pattern(self): - component = AnswerBuilder(reference_pattern="\\[(\\d+)\\]") - output = component.run( - query="test query", - replies=["Answer: AnswerString[2]"], - metadata=[{}], - documents=[Document(content="test doc 1"), Document(content="test doc 2")], - ) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "Answer: AnswerString[2]" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert len(answers[0].documents) == 1 - assert answers[0].documents[0].content == "test doc 2" - - def test_run_with_documents_with_reference_pattern_and_no_match(self, caplog): - component = AnswerBuilder(reference_pattern="\\[(\\d+)\\]") - with caplog.at_level(logging.WARNING): - output = component.run( - query="test query", - replies=["Answer: AnswerString[3]"], - metadata=[{}], - documents=[Document(content="test doc 1"), Document(content="test doc 2")], - ) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "Answer: AnswerString[3]" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert len(answers[0].documents) == 0 - assert "Document index '3' referenced in Generator output is out of range." in caplog.text - - def test_run_with_reference_pattern_set_at_runtime(self): - component = AnswerBuilder(reference_pattern="unused pattern") - output = component.run( - query="test query", - replies=["Answer: AnswerString[2][3]"], - metadata=[{}], - documents=[Document(content="test doc 1"), Document(content="test doc 2"), Document(content="test doc 3")], - reference_pattern="\\[(\\d+)\\]", - ) - answers = output["answers"] - assert len(answers) == 1 - assert answers[0].data == "Answer: AnswerString[2][3]" - assert answers[0].metadata == {} - assert answers[0].query == "test query" - assert len(answers[0].documents) == 2 - assert answers[0].documents[0].content == "test doc 2" - assert answers[0].documents[1].content == "test doc 3" diff --git a/test/preview/components/builders/test_dynamic_prompt_builder.py b/test/preview/components/builders/test_dynamic_prompt_builder.py deleted file mode 100644 index 007040da1e..0000000000 --- a/test/preview/components/builders/test_dynamic_prompt_builder.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import List, Union - -import pytest -from jinja2 import TemplateSyntaxError - -from haystack.preview.components.builders.dynamic_prompt_builder import DynamicPromptBuilder -from haystack.preview.dataclasses import ChatMessage - - -class TestDynamicPromptBuilder: - def test_initialization_chat_on(self): - runtime_variables = ["var1", "var2", "var3"] - builder = DynamicPromptBuilder(runtime_variables, chat_mode=True) - assert builder.runtime_variables == runtime_variables - assert builder.chat_mode - - # regardless of the chat mode - # we have inputs that contain: prompt_source, template_variables + runtime_variables - expected_keys = set(runtime_variables + ["prompt_source", "template_variables"]) - assert set(builder.__canals_input__.keys()) == expected_keys - - # response is always prompt regardless of chat mode - assert set(builder.__canals_output__.keys()) == {"prompt"} - - # prompt_source is a list of ChatMessage or a string - assert builder.__canals_input__["prompt_source"].type == Union[List[ChatMessage], str] - - # output is always prompt, but the type is different depending on the chat mode - assert builder.__canals_output__["prompt"].type == List[ChatMessage] - - def test_initialization_chat_off(self): - runtime_variables = ["var1", "var2"] - builder = DynamicPromptBuilder(runtime_variables, False) - assert builder.runtime_variables == runtime_variables - assert not builder.chat_mode - - # regardless of the chat mode - # we have inputs that contain: prompt_source, template_variables + runtime_variables - expected_keys = set(runtime_variables + ["prompt_source", "template_variables"]) - assert set(builder.__canals_input__.keys()) == expected_keys - - # response is always prompt regardless of chat mode - assert set(builder.__canals_output__.keys()) == {"prompt"} - - # prompt_source is a list of ChatMessage or a string - assert builder.__canals_input__["prompt_source"].type == Union[List[ChatMessage], str] - - # output is always prompt, but the type is different depending on the chat mode - assert builder.__canals_output__["prompt"].type == str - - def test_to_dict_method_returns_expected_dictionary(self): - runtime_variables = ["var1", "var2", "var3"] - chat_mode = True - builder = DynamicPromptBuilder(runtime_variables, chat_mode) - expected_dict = { - "type": "haystack.preview.components.builders.dynamic_prompt_builder.DynamicPromptBuilder", - "init_parameters": {"runtime_variables": runtime_variables, "chat_mode": chat_mode}, - } - assert builder.to_dict() == expected_dict - - def test_processing_a_simple_template_with_provided_variables(self): - runtime_variables = ["var1", "var2", "var3"] - chat_mode = True - - builder = DynamicPromptBuilder(runtime_variables, chat_mode) - - template = "Hello, {{ name }}!" - template_variables = {"name": "John"} - expected_result = "Hello, John!" - - assert builder._process_simple_template(template, template_variables) == expected_result - - def test_processing_a_simple_template_with_invalid_template(self): - runtime_variables = ["var1", "var2", "var3"] - chat_mode = True - - builder = DynamicPromptBuilder(runtime_variables, chat_mode) - - template = "Hello, {{ name }!" - template_variables = {"name": "John"} - with pytest.raises(TemplateSyntaxError): - builder._process_simple_template(template, template_variables) - - def test_processing_a_simple_template_with_missing_variables(self): - runtime_variables = ["var1", "var2", "var3"] - - builder = DynamicPromptBuilder(runtime_variables, False) - - with pytest.raises(ValueError): - builder._process_simple_template("Hello, {{ name }}!", {}) - - def test_non_empty_chat_messages(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=True) - prompt_source = [ChatMessage.from_system(content="Hello"), ChatMessage.from_user(content="Hello, {{ who }}!")] - template_variables = {"who": "World"} - - result = prompt_builder._process_chat_messages(prompt_source, template_variables) - - assert result == [ChatMessage.from_system(content="Hello"), ChatMessage.from_user(content="Hello, World!")] - - def test_single_chat_message(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=True) - prompt_source = [ChatMessage.from_user(content="Hello, {{ who }}!")] - template_variables = {"who": "World"} - - result = prompt_builder._process_chat_messages(prompt_source, template_variables) - - assert result == [ChatMessage.from_user(content="Hello, World!")] - - def test_empty_chat_message_list(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=True) - - with pytest.raises(ValueError): - prompt_builder._process_chat_messages(prompt_source=[], template_variables={}) - - def test_chat_message_list_with_mixed_object_list(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=True) - - with pytest.raises(ValueError): - prompt_builder._process_chat_messages( - prompt_source=[ChatMessage.from_user("Hello"), "there world"], template_variables={} - ) - - def test_chat_message_list_with_missing_variables(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"], chat_mode=True) - prompt_source = [ChatMessage.from_user(content="Hello, {{ who }}!")] - - # Call the _process_chat_messages method and expect a ValueError - with pytest.raises(ValueError): - prompt_builder._process_chat_messages(prompt_source, template_variables={}) - - def test_missing_template_variables(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"]) - - # missing template variable city - with pytest.raises(ValueError): - prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name"}) - - # missing template variable name - with pytest.raises(ValueError): - prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"city"}) - - # completely unknown template variable - with pytest.raises(ValueError): - prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"age"}) - - def test_provided_template_variables(self): - prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"]) - - # both variables are provided - prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city"}) - - # provided variables are a superset of the required variables - prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city", "age"}) diff --git a/test/preview/components/builders/test_prompt_builder.py b/test/preview/components/builders/test_prompt_builder.py deleted file mode 100644 index e43e99bb92..0000000000 --- a/test/preview/components/builders/test_prompt_builder.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest - -from haystack.preview.components.builders.prompt_builder import PromptBuilder - - -@pytest.mark.unit -def test_init(): - builder = PromptBuilder(template="This is a {{ variable }}") - assert builder._template_string == "This is a {{ variable }}" - - -@pytest.mark.unit -def test_to_dict(): - builder = PromptBuilder(template="This is a {{ variable }}") - res = builder.to_dict() - assert res == { - "type": "haystack.preview.components.builders.prompt_builder.PromptBuilder", - "init_parameters": {"template": "This is a {{ variable }}"}, - } - - -@pytest.mark.unit -def test_run(): - builder = PromptBuilder(template="This is a {{ variable }}") - res = builder.run(variable="test") - assert res == {"prompt": "This is a test"} - - -@pytest.mark.unit -def test_run_without_input(): - builder = PromptBuilder(template="This is a template without input") - res = builder.run() - assert res == {"prompt": "This is a template without input"} - - -@pytest.mark.unit -def test_run_with_missing_input(): - builder = PromptBuilder(template="This is a {{ variable }}") - res = builder.run() - assert res == {"prompt": "This is a "} diff --git a/test/preview/components/caching/test_url_cache_checker.py b/test/preview/components/caching/test_url_cache_checker.py deleted file mode 100644 index 1a9487f045..0000000000 --- a/test/preview/components/caching/test_url_cache_checker.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest - -from haystack.preview import Document, DeserializationError -from haystack.preview.testing.factory import document_store_class -from haystack.preview.document_stores.in_memory import InMemoryDocumentStore -from haystack.preview.components.caching.url_cache_checker import UrlCacheChecker - - -class TestUrlCacheChecker: - @pytest.mark.unit - def test_to_dict(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - component = UrlCacheChecker(document_store=mocked_docstore_class()) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "url_field": "url", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - component = UrlCacheChecker(document_store=mocked_docstore_class(), url_field="my_url_field") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "url_field": "my_url_field", - }, - } - - @pytest.mark.unit - def test_from_dict(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - data = { - "type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "url_field": "my_url_field", - }, - } - component = UrlCacheChecker.from_dict(data) - assert isinstance(component.document_store, mocked_docstore_class) - assert component.url_field == "my_url_field" - - @pytest.mark.unit - def test_from_dict_without_docstore(self): - data = {"type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", "init_parameters": {}} - with pytest.raises(DeserializationError, match="Missing 'document_store' in serialization data"): - UrlCacheChecker.from_dict(data) - - @pytest.mark.unit - def test_from_dict_without_docstore_type(self): - data = { - "type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", - "init_parameters": {"document_store": {"init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="Missing 'type' in document store's serialization data"): - UrlCacheChecker.from_dict(data) - - @pytest.mark.unit - def test_from_dict_nonexisting_docstore(self): - data = { - "type": "haystack.preview.components.caching.url_cache_checker.UrlCacheChecker", - "init_parameters": {"document_store": {"type": "NonexistingDocumentStore", "init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="DocumentStore of type 'NonexistingDocumentStore' not found."): - UrlCacheChecker.from_dict(data) - - @pytest.mark.unit - def test_run(self): - docstore = InMemoryDocumentStore() - documents = [ - Document(content="doc1", meta={"url": "https://example.com/1"}), - Document(content="doc2", meta={"url": "https://example.com/2"}), - Document(content="doc3", meta={"url": "https://example.com/1"}), - Document(content="doc4", meta={"url": "https://example.com/2"}), - ] - docstore.write_documents(documents) - checker = UrlCacheChecker(docstore) - results = checker.run(urls=["https://example.com/1", "https://example.com/5"]) - assert results == {"hits": [documents[0], documents[2]], "misses": ["https://example.com/5"]} diff --git a/test/preview/components/classifiers/test_document_language_classifier.py b/test/preview/components/classifiers/test_document_language_classifier.py deleted file mode 100644 index 53214b3633..0000000000 --- a/test/preview/components/classifiers/test_document_language_classifier.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import pytest - -from haystack.preview import Document -from haystack.preview.components.classifiers import DocumentLanguageClassifier - - -class TestDocumentLanguageClassifier: - @pytest.mark.unit - def test_init(self): - component = DocumentLanguageClassifier() - assert component.languages == ["en"] - - @pytest.mark.unit - def test_non_document_input(self): - with pytest.raises(TypeError, match="DocumentLanguageClassifier expects a list of Document as input."): - classifier = DocumentLanguageClassifier() - classifier.run(documents="This is an english sentence.") - - @pytest.mark.unit - def test_single_document(self): - with pytest.raises(TypeError, match="DocumentLanguageClassifier expects a list of Document as input."): - classifier = DocumentLanguageClassifier() - classifier.run(documents=Document(content="This is an english sentence.")) - - @pytest.mark.unit - def test_empty_list(self): - classifier = DocumentLanguageClassifier() - result = classifier.run(documents=[]) - assert result == {"documents": []} - - @pytest.mark.unit - def test_detect_language(self): - classifier = DocumentLanguageClassifier() - detected_language = classifier.detect_language(Document(content="This is an english sentence.")) - assert detected_language == "en" - - @pytest.mark.unit - def test_classify_as_en_and_unmatched(self): - classifier = DocumentLanguageClassifier() - english_document = Document(content="This is an english sentence.") - german_document = Document(content="Ein deutscher Satz ohne Verb.") - result = classifier.run(documents=[english_document, german_document]) - assert result["documents"][0].meta["language"] == "en" - assert result["documents"][1].meta["language"] == "unmatched" - - @pytest.mark.unit - def test_warning_if_no_language_detected(self, caplog): - with caplog.at_level(logging.WARNING): - classifier = DocumentLanguageClassifier() - classifier.run(documents=[Document(content=".")]) - assert "Langdetect cannot detect the language of Document with id" in caplog.text diff --git a/test/preview/components/converters/__init__.py b/test/preview/components/converters/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/converters/test_azure_ocr_doc_converter.py b/test/preview/components/converters/test_azure_ocr_doc_converter.py deleted file mode 100644 index 83c5075c4b..0000000000 --- a/test/preview/components/converters/test_azure_ocr_doc_converter.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -from unittest.mock import patch, Mock - -import pytest - -from haystack.preview.components.converters.azure import AzureOCRDocumentConverter - - -class TestAzureOCRDocumentConverter: - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - monkeypatch.delenv("AZURE_AI_API_KEY", raising=False) - with pytest.raises(ValueError, match="AzureOCRDocumentConverter expects an Azure Credential key"): - AzureOCRDocumentConverter(endpoint="test_endpoint") - - @pytest.mark.unit - def test_to_dict(self): - component = AzureOCRDocumentConverter(endpoint="test_endpoint", api_key="test_credential_key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.converters.azure.AzureOCRDocumentConverter", - "init_parameters": {"endpoint": "test_endpoint", "model_id": "prebuilt-read"}, - } - - @pytest.mark.unit - def test_run(self, preview_samples_path): - with patch("haystack.preview.components.converters.azure.DocumentAnalysisClient") as mock_azure_client: - mock_result = Mock(pages=[Mock(lines=[Mock(content="mocked line 1"), Mock(content="mocked line 2")])]) - mock_result.to_dict.return_value = { - "api_version": "2023-02-28-preview", - "model_id": "prebuilt-read", - "content": "mocked line 1\nmocked line 2\n\f", - "pages": [{"lines": [{"content": "mocked line 1"}, {"content": "mocked line 2"}]}], - } - mock_azure_client.return_value.begin_analyze_document.return_value.result.return_value = mock_result - - component = AzureOCRDocumentConverter(endpoint="test_endpoint", api_key="test_credential_key") - output = component.run(paths=[preview_samples_path / "pdf" / "sample_pdf_1.pdf"]) - document = output["documents"][0] - assert document.content == "mocked line 1\nmocked line 2\n\f" - assert "raw_azure_response" in output - assert output["raw_azure_response"][0] == { - "api_version": "2023-02-28-preview", - "model_id": "prebuilt-read", - "content": "mocked line 1\nmocked line 2\n\f", - "pages": [{"lines": [{"content": "mocked line 1"}, {"content": "mocked line 2"}]}], - } - - @pytest.mark.integration - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_ENDPOINT", None), reason="Azure credentials not available") - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_API_KEY", None), reason="Azure credentials not available") - def test_run_with_pdf_file(self, preview_samples_path): - component = AzureOCRDocumentConverter( - endpoint=os.environ["CORE_AZURE_CS_ENDPOINT"], api_key=os.environ["CORE_AZURE_CS_API_KEY"] - ) - output = component.run(paths=[preview_samples_path / "pdf" / "sample_pdf_1.pdf"]) - documents = output["documents"] - assert len(documents) == 1 - assert "A sample PDF file" in documents[0].content - assert "Page 2 of Sample PDF" in documents[0].content - assert "Page 4 of Sample PDF" in documents[0].content - - @pytest.mark.integration - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_ENDPOINT", None), reason="Azure credentials not available") - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_API_KEY", None), reason="Azure credentials not available") - def test_with_image_file(self, preview_samples_path): - component = AzureOCRDocumentConverter( - endpoint=os.environ["CORE_AZURE_CS_ENDPOINT"], api_key=os.environ["CORE_AZURE_CS_API_KEY"] - ) - output = component.run(paths=[preview_samples_path / "images" / "haystack-logo.png"]) - documents = output["documents"] - assert len(documents) == 1 - assert "haystack" in documents[0].content - assert "by deepset" in documents[0].content - - @pytest.mark.integration - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_ENDPOINT", None), reason="Azure credentials not available") - @pytest.mark.skipif(not os.environ.get("CORE_AZURE_CS_API_KEY", None), reason="Azure credentials not available") - def test_run_with_docx_file(self, preview_samples_path): - component = AzureOCRDocumentConverter( - endpoint=os.environ["CORE_AZURE_CS_ENDPOINT"], api_key=os.environ["CORE_AZURE_CS_API_KEY"] - ) - output = component.run(paths=[preview_samples_path / "docx" / "sample_docx.docx"]) - documents = output["documents"] - assert len(documents) == 1 - assert "Sample Docx File" in documents[0].content - assert "Now we are in Page 2" in documents[0].content - assert "Page 3 was empty this is page 4" in documents[0].content diff --git a/test/preview/components/converters/test_html_to_document.py b/test/preview/components/converters/test_html_to_document.py deleted file mode 100644 index 4e182b279b..0000000000 --- a/test/preview/components/converters/test_html_to_document.py +++ /dev/null @@ -1,156 +0,0 @@ -import logging - -import pytest - -from haystack.preview.components.converters import HTMLToDocument -from haystack.preview.dataclasses import ByteStream - - -class TestHTMLToDocument: - @pytest.mark.unit - def test_run(self, preview_samples_path): - """ - Test if the component runs correctly. - """ - sources = [preview_samples_path / "html" / "what_is_haystack.html"] - converter = HTMLToDocument() - results = converter.run(sources=sources) - docs = results["documents"] - assert len(docs) == 1 - assert "Haystack" in docs[0].content - - @pytest.mark.unit - def test_run_doc_metadata(self, preview_samples_path): - """ - Test if the component runs correctly when metadata is supplied by the user. - """ - converter = HTMLToDocument() - sources = [preview_samples_path / "html" / "what_is_haystack.html"] - metadata = [{"file_name": "what_is_haystack.html"}] - results = converter.run(sources=sources, meta=metadata) - docs = results["documents"] - - assert len(docs) == 1 - assert "Haystack" in docs[0].content - assert docs[0].meta == {"file_name": "what_is_haystack.html"} - - @pytest.mark.unit - def test_incorrect_meta(self, preview_samples_path): - """ - Test if the component raises an error when incorrect metadata is supplied by the user. - """ - converter = HTMLToDocument() - sources = [preview_samples_path / "html" / "what_is_haystack.html"] - metadata = [{"file_name": "what_is_haystack.html"}, {"file_name": "haystack.html"}] - with pytest.raises(ValueError, match="The length of the metadata list must match the number of sources."): - converter.run(sources=sources, meta=metadata) - - @pytest.mark.unit - def test_run_bytestream_metadata(self, preview_samples_path): - """ - Test if the component runs correctly when metadata is read from the ByteStream object. - """ - converter = HTMLToDocument() - with open(preview_samples_path / "html" / "what_is_haystack.html", "rb") as file: - byte_stream = file.read() - stream = ByteStream(byte_stream, metadata={"content_type": "text/html", "url": "test_url"}) - - results = converter.run(sources=[stream]) - docs = results["documents"] - - assert len(docs) == 1 - assert "Haystack" in docs[0].content - assert docs[0].meta == {"content_type": "text/html", "url": "test_url"} - - @pytest.mark.unit - def test_run_bytestream_and_doc_metadata(self, preview_samples_path): - """ - Test if the component runs correctly when metadata is read from the ByteStream object and supplied by the user. - - There is no overlap between the metadata received. - """ - converter = HTMLToDocument() - with open(preview_samples_path / "html" / "what_is_haystack.html", "rb") as file: - byte_stream = file.read() - stream = ByteStream(byte_stream, metadata={"content_type": "text/html", "url": "test_url"}) - - metadata = [{"file_name": "what_is_haystack.html"}] - results = converter.run(sources=[stream], meta=metadata) - docs = results["documents"] - - assert len(docs) == 1 - assert "Haystack" in docs[0].content - assert docs[0].meta == {"file_name": "what_is_haystack.html", "content_type": "text/html", "url": "test_url"} - - @pytest.mark.unit - def test_run_bytestream_doc_overlapping_metadata(self, preview_samples_path): - """ - Test if the component runs correctly when metadata is read from the ByteStream object and supplied by the user. - - There is an overlap between the metadata received. - - The component should use the supplied metadata to overwrite the values if there is an overlap between the keys. - """ - converter = HTMLToDocument() - with open(preview_samples_path / "html" / "what_is_haystack.html", "rb") as file: - byte_stream = file.read() - # ByteStream has "url" present in metadata - stream = ByteStream(byte_stream, metadata={"content_type": "text/html", "url": "test_url_correct"}) - - # "url" supplied by the user overwrites value present in metadata - metadata = [{"file_name": "what_is_haystack.html", "url": "test_url_new"}] - results = converter.run(sources=[stream], meta=metadata) - docs = results["documents"] - - assert len(docs) == 1 - assert "Haystack" in docs[0].content - assert docs[0].meta == { - "file_name": "what_is_haystack.html", - "content_type": "text/html", - "url": "test_url_new", - } - - @pytest.mark.unit - def test_run_wrong_file_type(self, preview_samples_path, caplog): - """ - Test if the component runs correctly when an input file is not of the expected type. - """ - sources = [preview_samples_path / "audio" / "answer.wav"] - converter = HTMLToDocument() - with caplog.at_level(logging.WARNING): - results = converter.run(sources=sources) - assert "codec can't decode byte" in caplog.text - - assert results["documents"] == [] - - @pytest.mark.unit - def test_run_error_handling(self, caplog): - """ - Test if the component correctly handles errors. - """ - sources = ["non_existing_file.html"] - converter = HTMLToDocument() - with caplog.at_level(logging.WARNING): - results = converter.run(sources=sources) - assert "Could not read non_existing_file.html" in caplog.text - assert results["documents"] == [] - - @pytest.mark.unit - def test_mixed_sources_run(self, preview_samples_path): - """ - Test if the component runs correctly if the input is a mix of paths and ByteStreams. - """ - sources = [ - preview_samples_path / "html" / "what_is_haystack.html", - str((preview_samples_path / "html" / "what_is_haystack.html").absolute()), - ] - with open(preview_samples_path / "html" / "what_is_haystack.html", "rb") as f: - byte_stream = f.read() - sources.append(ByteStream(byte_stream)) - - converter = HTMLToDocument() - results = converter.run(sources=sources) - docs = results["documents"] - assert len(docs) == 3 - for doc in docs: - assert "Haystack" in doc.content diff --git a/test/preview/components/converters/test_markdown_to_document.py b/test/preview/components/converters/test_markdown_to_document.py deleted file mode 100644 index 3dc69429df..0000000000 --- a/test/preview/components/converters/test_markdown_to_document.py +++ /dev/null @@ -1,93 +0,0 @@ -import logging - -import pytest - -from haystack.preview.components.converters.markdown import MarkdownToDocument -from haystack.preview.dataclasses import ByteStream - - -class TestMarkdownToDocument: - @pytest.mark.unit - def test_init_params_default(self): - converter = MarkdownToDocument() - assert converter.table_to_single_line is False - assert converter.progress_bar is True - - @pytest.mark.unit - def test_init_params_custom(self): - converter = MarkdownToDocument(table_to_single_line=True, progress_bar=False) - assert converter.table_to_single_line is True - assert converter.progress_bar is False - - @pytest.mark.integration - def test_run(self, preview_samples_path): - converter = MarkdownToDocument() - sources = [preview_samples_path / "markdown" / "sample.md"] - results = converter.run(sources=sources) - docs = results["documents"] - - assert len(docs) == 1 - for doc in docs: - assert "What to build with Haystack" in doc.content - assert "# git clone https://github.com/deepset-ai/haystack.git" in doc.content - - @pytest.mark.integration - def test_run_metadata(self, preview_samples_path): - converter = MarkdownToDocument() - sources = [preview_samples_path / "markdown" / "sample.md"] - metadata = [{"file_name": "sample.md"}] - results = converter.run(sources=sources, meta=metadata) - docs = results["documents"] - - assert len(docs) == 1 - for doc in docs: - assert "What to build with Haystack" in doc.content - assert "# git clone https://github.com/deepset-ai/haystack.git" in doc.content - assert doc.meta == {"file_name": "sample.md"} - - @pytest.mark.integration - def test_run_wrong_file_type(self, preview_samples_path, caplog): - """ - Test if the component runs correctly when an input file is not of the expected type. - """ - sources = [preview_samples_path / "audio" / "answer.wav"] - converter = MarkdownToDocument() - with caplog.at_level(logging.WARNING): - output = converter.run(sources=sources) - assert "codec can't decode byte" in caplog.text - - docs = output["documents"] - assert not docs - - @pytest.mark.integration - def test_run_error_handling(self, caplog): - """ - Test if the component correctly handles errors. - """ - sources = ["non_existing_file.md"] - converter = MarkdownToDocument() - with caplog.at_level(logging.WARNING): - result = converter.run(sources=sources) - assert "Could not read non_existing_file.md" in caplog.text - assert not result["documents"] - - @pytest.mark.unit - def test_mixed_sources_run(self, preview_samples_path): - """ - Test if the component runs correctly if the input is a mix of strings, paths and ByteStreams. - """ - sources = [ - preview_samples_path / "markdown" / "sample.md", - str((preview_samples_path / "markdown" / "sample.md").absolute()), - ] - with open(preview_samples_path / "markdown" / "sample.md", "rb") as f: - byte_stream = f.read() - sources.append(ByteStream(byte_stream)) - - converter = MarkdownToDocument() - output = converter.run(sources=sources) - docs = output["documents"] - assert len(docs) == 3 - for doc in docs: - assert "What to build with Haystack" in doc.content - assert "# git clone https://github.com/deepset-ai/haystack.git" in doc.content diff --git a/test/preview/components/converters/test_pypdf_to_document.py b/test/preview/components/converters/test_pypdf_to_document.py deleted file mode 100644 index e7fb0202fb..0000000000 --- a/test/preview/components/converters/test_pypdf_to_document.py +++ /dev/null @@ -1,77 +0,0 @@ -import logging -import pytest -from pypdf import PdfReader - -from haystack.preview import Document -from haystack.preview.components.converters.pypdf import PyPDFToDocument, CONVERTERS_REGISTRY -from haystack.preview.dataclasses import ByteStream - - -class TestPyPDFToDocument: - def test_init(self): - component = PyPDFToDocument() - assert component.converter_name == "default" - assert hasattr(component, "_converter") - - def test_init_fail_nonexisting_converter(self): - with pytest.raises(ValueError): - PyPDFToDocument(converter_name="non_existing_converter") - - @pytest.mark.unit - def test_run(self, preview_samples_path): - """ - Test if the component runs correctly. - """ - paths = [preview_samples_path / "pdf" / "react_paper.pdf"] - converter = PyPDFToDocument() - output = converter.run(sources=paths) - docs = output["documents"] - assert len(docs) == 1 - assert "ReAct" in docs[0].content - - @pytest.mark.unit - def test_run_error_handling(self, preview_samples_path, caplog): - """ - Test if the component correctly handles errors. - """ - paths = ["non_existing_file.pdf"] - converter = PyPDFToDocument() - with caplog.at_level(logging.WARNING): - converter.run(sources=paths) - assert "Could not read non_existing_file.pdf" in caplog.text - - @pytest.mark.unit - def test_mixed_sources_run(self, preview_samples_path): - """ - Test if the component runs correctly when mixed sources are provided. - """ - paths = [preview_samples_path / "pdf" / "react_paper.pdf"] - with open(preview_samples_path / "pdf" / "react_paper.pdf", "rb") as f: - paths.append(ByteStream(f.read())) - - converter = PyPDFToDocument() - output = converter.run(sources=paths) - docs = output["documents"] - assert len(docs) == 2 - assert "ReAct" in docs[0].content - assert "ReAct" in docs[1].content - - @pytest.mark.unit - def test_custom_converter(self, preview_samples_path): - """ - Test if the component correctly handles custom converters. - """ - paths = [preview_samples_path / "pdf" / "react_paper.pdf"] - - class MyCustomConverter: - def convert(self, reader: PdfReader) -> Document: - return Document(content="I don't care about converting given pdfs, I always return this") - - CONVERTERS_REGISTRY["custom"] = MyCustomConverter() - - converter = PyPDFToDocument(converter_name="custom") - output = converter.run(sources=paths) - docs = output["documents"] - assert len(docs) == 1 - assert "ReAct" not in docs[0].content - assert "I don't care about converting given pdfs, I always return this" in docs[0].content diff --git a/test/preview/components/converters/test_textfile_to_document.py b/test/preview/components/converters/test_textfile_to_document.py deleted file mode 100644 index aafa77d1e2..0000000000 --- a/test/preview/components/converters/test_textfile_to_document.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -from unittest.mock import patch -from pathlib import Path - -import pytest - -from haystack.preview.dataclasses import ByteStream -from haystack.preview.components.converters.txt import TextFileToDocument - - -class TestTextfileToDocument: - @pytest.mark.unit - def test_run(self, preview_samples_path): - """ - Test if the component runs correctly. - """ - bytestream = ByteStream.from_file_path(preview_samples_path / "txt" / "doc_3.txt") - bytestream.metadata["file_path"] = str(preview_samples_path / "txt" / "doc_3.txt") - bytestream.metadata["key"] = "value" - files = [ - str(preview_samples_path / "txt" / "doc_1.txt"), - preview_samples_path / "txt" / "doc_2.txt", - bytestream, - ] - converter = TextFileToDocument() - output = converter.run(sources=files) - docs = output["documents"] - assert len(docs) == 3 - assert "Some text for testing." in docs[0].content - assert "This is a test line." in docs[1].content - assert "That's yet another file!" in docs[2].content - assert docs[0].meta["file_path"] == str(files[0]) - assert docs[1].meta["file_path"] == str(files[1]) - assert docs[2].meta == bytestream.metadata - - @pytest.mark.unit - def test_run_error_handling(self, preview_samples_path, caplog): - """ - Test if the component correctly handles errors. - """ - paths = [ - preview_samples_path / "txt" / "doc_1.txt", - "non_existing_file.txt", - preview_samples_path / "txt" / "doc_3.txt", - ] - converter = TextFileToDocument() - with caplog.at_level(logging.WARNING): - output = converter.run(sources=paths) - assert "non_existing_file.txt" in caplog.text - docs = output["documents"] - assert len(docs) == 2 - assert docs[0].meta["file_path"] == str(paths[0]) - assert docs[1].meta["file_path"] == str(paths[2]) - - @pytest.mark.unit - def test_encoding_override(self, preview_samples_path): - """ - Test if the encoding metadata field is used properly - """ - bytestream = ByteStream.from_file_path(preview_samples_path / "txt" / "doc_1.txt") - bytestream.metadata["key"] = "value" - - converter = TextFileToDocument(encoding="utf-16") - output = converter.run(sources=[bytestream]) - assert "Some text for testing." not in output["documents"][0].content - - bytestream.metadata["encoding"] = "utf-8" - output = converter.run(sources=[bytestream]) - assert "Some text for testing." in output["documents"][0].content diff --git a/test/preview/components/converters/test_tika_doc_converter.py b/test/preview/components/converters/test_tika_doc_converter.py deleted file mode 100644 index c346c4bf95..0000000000 --- a/test/preview/components/converters/test_tika_doc_converter.py +++ /dev/null @@ -1,75 +0,0 @@ -from unittest.mock import patch - -import pytest - -from haystack.preview.components.converters.tika import TikaDocumentConverter - - -class TestTikaDocumentConverter: - @pytest.mark.unit - def test_run(self): - component = TikaDocumentConverter() - with patch("haystack.preview.components.converters.tika.tika_parser.from_file") as mock_tika_parser: - mock_tika_parser.return_value = {"content": "Content of mock_file.pdf"} - documents = component.run(paths=["mock_file.pdf"])["documents"] - - assert len(documents) == 1 - assert documents[0].content == "Content of mock_file.pdf" - - @pytest.mark.unit - def test_run_logs_warning_if_content_empty(self, caplog): - component = TikaDocumentConverter() - with patch("haystack.preview.components.converters.tika.tika_parser.from_file") as mock_tika_parser: - mock_tika_parser.return_value = {"content": ""} - with caplog.at_level("WARNING"): - component.run(paths=["mock_file.pdf"]) - assert "Skipping file at 'mock_file.pdf' as Tika was not able to extract any content." in caplog.text - - @pytest.mark.unit - def test_run_logs_error(self, caplog): - component = TikaDocumentConverter() - with patch("haystack.preview.components.converters.tika.tika_parser.from_file") as mock_tika_parser: - mock_tika_parser.side_effect = Exception("Some error") - with caplog.at_level("ERROR"): - component.run(paths=["mock_file.pdf"]) - assert "Could not convert file at 'mock_file.pdf' to Document. Error: Some error" in caplog.text - - @pytest.mark.integration - def test_run_with_txt_files(self, preview_samples_path): - component = TikaDocumentConverter() - output = component.run( - paths=[preview_samples_path / "txt" / "doc_1.txt", preview_samples_path / "txt" / "doc_2.txt"] - ) - documents = output["documents"] - assert len(documents) == 2 - assert "Some text for testing.\nTwo lines in here." in documents[0].content - assert "This is a test line.\n123 456 789\n987 654 321" in documents[1].content - - @pytest.mark.integration - def test_run_with_pdf_file(self, preview_samples_path): - component = TikaDocumentConverter() - output = component.run( - paths=[preview_samples_path / "pdf" / "sample_pdf_1.pdf", preview_samples_path / "pdf" / "sample_pdf_2.pdf"] - ) - documents = output["documents"] - assert len(documents) == 2 - assert "A sample PDF file" in documents[0].content - assert "Page 2 of Sample PDF" in documents[0].content - assert "Page 4 of Sample PDF" in documents[0].content - assert "First Page" in documents[1].content - assert ( - "Wiki engines usually allow content to be written using a simplified markup language" - in documents[1].content - ) - assert "This section needs additional citations for verification." in documents[1].content - assert "This would make it easier for other users to find the article." in documents[1].content - - @pytest.mark.integration - def test_run_with_docx_file(self, preview_samples_path): - component = TikaDocumentConverter() - output = component.run(paths=[preview_samples_path / "docx" / "sample_docx.docx"]) - documents = output["documents"] - assert len(documents) == 1 - assert "Sample Docx File" in documents[0].content - assert "Now we are in Page 2" in documents[0].content - assert "Page 3 was empty this is page 4" in documents[0].content diff --git a/test/preview/components/embedders/__init__.py b/test/preview/components/embedders/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/embedders/test_openai_document_embedder.py b/test/preview/components/embedders/test_openai_document_embedder.py deleted file mode 100644 index 954846c480..0000000000 --- a/test/preview/components/embedders/test_openai_document_embedder.py +++ /dev/null @@ -1,288 +0,0 @@ -from unittest.mock import patch -from typing import List, cast - -import pytest -import numpy as np -import openai -from openai.util import convert_to_openai_object -from openai.openai_object import OpenAIObject - -from haystack.preview import Document -from haystack.preview.components.embedders.openai_document_embedder import OpenAIDocumentEmbedder - - -def mock_openai_response(input: List[str], model: str = "text-embedding-ada-002", **kwargs) -> OpenAIObject: - dict_response = { - "object": "list", - "data": [ - {"object": "embedding", "index": i, "embedding": np.random.rand(1536).tolist()} for i in range(len(input)) - ], - "model": model, - "usage": {"prompt_tokens": 4, "total_tokens": 4}, - } - - return cast(OpenAIObject, convert_to_openai_object(dict_response)) - - -class TestOpenAIDocumentEmbedder: - @pytest.mark.unit - def test_init_default(self, monkeypatch): - openai.api_key = None - monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key") - embedder = OpenAIDocumentEmbedder() - - assert openai.api_key == "fake-api-key" - - assert embedder.model_name == "text-embedding-ada-002" - assert embedder.organization is None - assert embedder.prefix == "" - assert embedder.suffix == "" - assert embedder.batch_size == 32 - assert embedder.progress_bar is True - assert embedder.metadata_fields_to_embed == [] - assert embedder.embedding_separator == "\n" - - @pytest.mark.unit - def test_init_with_parameters(self): - embedder = OpenAIDocumentEmbedder( - api_key="fake-api-key", - model_name="model", - organization="my-org", - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - metadata_fields_to_embed=["test_field"], - embedding_separator=" | ", - ) - assert openai.api_key == "fake-api-key" - assert openai.organization == "my-org" - - assert embedder.organization == "my-org" - assert embedder.model_name == "model" - assert embedder.prefix == "prefix" - assert embedder.suffix == "suffix" - assert embedder.batch_size == 64 - assert embedder.progress_bar is False - assert embedder.metadata_fields_to_embed == ["test_field"] - assert embedder.embedding_separator == " | " - - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - with pytest.raises(ValueError, match="OpenAIDocumentEmbedder expects an OpenAI API key"): - OpenAIDocumentEmbedder() - - @pytest.mark.unit - def test_to_dict(self): - component = OpenAIDocumentEmbedder(api_key="fake-api-key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.openai_document_embedder.OpenAIDocumentEmbedder", - "init_parameters": { - "model_name": "text-embedding-ada-002", - "organization": None, - "prefix": "", - "suffix": "", - "batch_size": 32, - "progress_bar": True, - "metadata_fields_to_embed": [], - "embedding_separator": "\n", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = OpenAIDocumentEmbedder( - api_key="fake-api-key", - model_name="model", - organization="my-org", - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - metadata_fields_to_embed=["test_field"], - embedding_separator=" | ", - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.openai_document_embedder.OpenAIDocumentEmbedder", - "init_parameters": { - "model_name": "model", - "organization": "my-org", - "prefix": "prefix", - "suffix": "suffix", - "batch_size": 64, - "progress_bar": False, - "metadata_fields_to_embed": ["test_field"], - "embedding_separator": " | ", - }, - } - - @pytest.mark.unit - def test_prepare_texts_to_embed_w_metadata(self): - documents = [ - Document(content=f"document number {i}:\ncontent", meta={"meta_field": f"meta_value {i}"}) for i in range(5) - ] - - embedder = OpenAIDocumentEmbedder( - api_key="fake-api-key", metadata_fields_to_embed=["meta_field"], embedding_separator=" | " - ) - - prepared_texts = embedder._prepare_texts_to_embed(documents) - - # note that newline is replaced by space - assert prepared_texts == [ - "meta_value 0 | document number 0: content", - "meta_value 1 | document number 1: content", - "meta_value 2 | document number 2: content", - "meta_value 3 | document number 3: content", - "meta_value 4 | document number 4: content", - ] - - @pytest.mark.unit - def test_prepare_texts_to_embed_w_suffix(self): - documents = [Document(content=f"document number {i}") for i in range(5)] - - embedder = OpenAIDocumentEmbedder(api_key="fake-api-key", prefix="my_prefix ", suffix=" my_suffix") - - prepared_texts = embedder._prepare_texts_to_embed(documents) - - assert prepared_texts == [ - "my_prefix document number 0 my_suffix", - "my_prefix document number 1 my_suffix", - "my_prefix document number 2 my_suffix", - "my_prefix document number 3 my_suffix", - "my_prefix document number 4 my_suffix", - ] - - @pytest.mark.unit - def test_embed_batch(self): - texts = ["text 1", "text 2", "text 3", "text 4", "text 5"] - - with patch( - "haystack.preview.components.embedders.openai_document_embedder.openai.Embedding" - ) as openai_embedding_patch: - openai_embedding_patch.create.side_effect = mock_openai_response - embedder = OpenAIDocumentEmbedder(api_key="fake-api-key", model_name="model") - - embeddings, metadata = embedder._embed_batch(texts_to_embed=texts, batch_size=2) - - assert openai_embedding_patch.create.call_count == 3 - - assert isinstance(embeddings, list) - assert len(embeddings) == len(texts) - for embedding in embeddings: - assert isinstance(embedding, list) - assert len(embedding) == 1536 - assert all(isinstance(x, float) for x in embedding) - - # openai.Embedding.create is called 3 times - assert metadata == {"model": "model", "usage": {"prompt_tokens": 3 * 4, "total_tokens": 3 * 4}} - - @pytest.mark.unit - def test_run(self): - docs = [ - Document(content="I love cheese", meta={"topic": "Cuisine"}), - Document(content="A transformer is a deep learning architecture", meta={"topic": "ML"}), - ] - - model = "text-similarity-ada-001" - with patch( - "haystack.preview.components.embedders.openai_document_embedder.openai.Embedding" - ) as openai_embedding_patch: - openai_embedding_patch.create.side_effect = mock_openai_response - embedder = OpenAIDocumentEmbedder( - api_key="fake-api-key", - model_name=model, - prefix="prefix ", - suffix=" suffix", - metadata_fields_to_embed=["topic"], - embedding_separator=" | ", - ) - - result = embedder.run(documents=docs) - - openai_embedding_patch.create.assert_called_once_with( - model=model, - input=[ - "prefix Cuisine | I love cheese suffix", - "prefix ML | A transformer is a deep learning architecture suffix", - ], - ) - documents_with_embeddings = result["documents"] - metadata = result["metadata"] - - assert isinstance(documents_with_embeddings, list) - assert len(documents_with_embeddings) == len(docs) - for doc in documents_with_embeddings: - assert isinstance(doc, Document) - assert isinstance(doc.embedding, list) - assert len(doc.embedding) == 1536 - assert all(isinstance(x, float) for x in doc.embedding) - assert metadata == {"model": model, "usage": {"prompt_tokens": 4, "total_tokens": 4}} - - @pytest.mark.unit - def test_run_custom_batch_size(self): - docs = [ - Document(content="I love cheese", meta={"topic": "Cuisine"}), - Document(content="A transformer is a deep learning architecture", meta={"topic": "ML"}), - ] - - model = "text-similarity-ada-001" - with patch( - "haystack.preview.components.embedders.openai_document_embedder.openai.Embedding" - ) as openai_embedding_patch: - openai_embedding_patch.create.side_effect = mock_openai_response - embedder = OpenAIDocumentEmbedder( - api_key="fake-api-key", - model_name=model, - prefix="prefix ", - suffix=" suffix", - metadata_fields_to_embed=["topic"], - embedding_separator=" | ", - batch_size=1, - ) - - result = embedder.run(documents=docs) - - assert openai_embedding_patch.create.call_count == 2 - - documents_with_embeddings = result["documents"] - metadata = result["metadata"] - - assert isinstance(documents_with_embeddings, list) - assert len(documents_with_embeddings) == len(docs) - for doc in documents_with_embeddings: - assert isinstance(doc, Document) - assert isinstance(doc.embedding, list) - assert len(doc.embedding) == 1536 - assert all(isinstance(x, float) for x in doc.embedding) - - # openai.Embedding.create is called 2 times - assert metadata == {"model": model, "usage": {"prompt_tokens": 2 * 4, "total_tokens": 2 * 4}} - - @pytest.mark.unit - def test_run_wrong_input_format(self): - embedder = OpenAIDocumentEmbedder(api_key="fake-api-key") - - # wrong formats - string_input = "text" - list_integers_input = [1, 2, 3] - - with pytest.raises(TypeError, match="OpenAIDocumentEmbedder expects a list of Documents as input"): - embedder.run(documents=string_input) - - with pytest.raises(TypeError, match="OpenAIDocumentEmbedder expects a list of Documents as input"): - embedder.run(documents=list_integers_input) - - @pytest.mark.unit - def test_run_on_empty_list(self): - embedder = OpenAIDocumentEmbedder(api_key="fake-api-key") - - empty_list_input = [] - result = embedder.run(documents=empty_list_input) - - assert result["documents"] is not None - assert not result["documents"] # empty list diff --git a/test/preview/components/embedders/test_openai_text_embedder.py b/test/preview/components/embedders/test_openai_text_embedder.py deleted file mode 100644 index 50be49ac5d..0000000000 --- a/test/preview/components/embedders/test_openai_text_embedder.py +++ /dev/null @@ -1,118 +0,0 @@ -from unittest.mock import patch -import pytest -import openai -from openai.util import convert_to_openai_object -import numpy as np - -from haystack.preview.components.embedders.openai_text_embedder import OpenAITextEmbedder - - -def mock_openai_response(model: str = "text-embedding-ada-002", **kwargs) -> openai.openai_object.OpenAIObject: - dict_response = { - "object": "list", - "data": [{"object": "embedding", "index": 0, "embedding": np.random.rand(1536).tolist()}], - "model": model, - "usage": {"prompt_tokens": 4, "total_tokens": 4}, - } - - return convert_to_openai_object(dict_response) - - -class TestOpenAITextEmbedder: - @pytest.mark.unit - def test_init_default(self, monkeypatch): - openai.api_key = None - monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key") - embedder = OpenAITextEmbedder() - - assert openai.api_key == "fake-api-key" - assert embedder.model_name == "text-embedding-ada-002" - assert embedder.organization is None - assert embedder.prefix == "" - assert embedder.suffix == "" - - @pytest.mark.unit - def test_init_with_parameters(self): - embedder = OpenAITextEmbedder( - api_key="fake-api-key", - model_name="model", - organization="fake-organization", - prefix="prefix", - suffix="suffix", - ) - assert openai.api_key == "fake-api-key" - assert embedder.model_name == "model" - assert embedder.organization == "fake-organization" - assert openai.organization == "fake-organization" - assert embedder.prefix == "prefix" - assert embedder.suffix == "suffix" - - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - with pytest.raises(ValueError, match="OpenAITextEmbedder expects an OpenAI API key"): - OpenAITextEmbedder() - - @pytest.mark.unit - def test_to_dict(self): - component = OpenAITextEmbedder(api_key="fake-api-key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.openai_text_embedder.OpenAITextEmbedder", - "init_parameters": { - "model_name": "text-embedding-ada-002", - "organization": None, - "prefix": "", - "suffix": "", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = OpenAITextEmbedder( - api_key="fake-api-key", - model_name="model", - organization="fake-organization", - prefix="prefix", - suffix="suffix", - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.openai_text_embedder.OpenAITextEmbedder", - "init_parameters": { - "model_name": "model", - "organization": "fake-organization", - "prefix": "prefix", - "suffix": "suffix", - }, - } - - @pytest.mark.unit - def test_run(self): - model = "text-similarity-ada-001" - - with patch( - "haystack.preview.components.embedders.openai_text_embedder.openai.Embedding" - ) as openai_embedding_patch: - openai_embedding_patch.create.side_effect = mock_openai_response - - embedder = OpenAITextEmbedder(api_key="fake-api-key", model_name=model, prefix="prefix ", suffix=" suffix") - result = embedder.run(text="The food was delicious") - - openai_embedding_patch.create.assert_called_once_with( - model=model, input="prefix The food was delicious suffix" - ) - - assert len(result["embedding"]) == 1536 - assert all(isinstance(x, float) for x in result["embedding"]) - assert result["metadata"] == {"model": model, "usage": {"prompt_tokens": 4, "total_tokens": 4}} - - @pytest.mark.unit - def test_run_wrong_input_format(self): - embedder = OpenAITextEmbedder(api_key="fake-api-key") - - list_integers_input = [1, 2, 3] - - with pytest.raises(TypeError, match="OpenAITextEmbedder expects a string as an input"): - embedder.run(text=list_integers_input) diff --git a/test/preview/components/embedders/test_sentence_transformers_document_embedder.py b/test/preview/components/embedders/test_sentence_transformers_document_embedder.py deleted file mode 100644 index 2f5e5e667f..0000000000 --- a/test/preview/components/embedders/test_sentence_transformers_document_embedder.py +++ /dev/null @@ -1,210 +0,0 @@ -from unittest.mock import patch, MagicMock -import pytest -import numpy as np - -from haystack.preview import Document -from haystack.preview.components.embedders.sentence_transformers_document_embedder import ( - SentenceTransformersDocumentEmbedder, -) - - -class TestSentenceTransformersDocumentEmbedder: - @pytest.mark.unit - def test_init_default(self): - embedder = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - assert embedder.model_name_or_path == "model" - assert embedder.device == "cpu" - assert embedder.token is None - assert embedder.prefix == "" - assert embedder.suffix == "" - assert embedder.batch_size == 32 - assert embedder.progress_bar is True - assert embedder.normalize_embeddings is False - assert embedder.metadata_fields_to_embed == [] - assert embedder.embedding_separator == "\n" - - @pytest.mark.unit - def test_init_with_parameters(self): - embedder = SentenceTransformersDocumentEmbedder( - model_name_or_path="model", - device="cuda", - token=True, - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - normalize_embeddings=True, - metadata_fields_to_embed=["test_field"], - embedding_separator=" | ", - ) - assert embedder.model_name_or_path == "model" - assert embedder.device == "cuda" - assert embedder.token is True - assert embedder.prefix == "prefix" - assert embedder.suffix == "suffix" - assert embedder.batch_size == 64 - assert embedder.progress_bar is False - assert embedder.normalize_embeddings is True - assert embedder.metadata_fields_to_embed == ["test_field"] - assert embedder.embedding_separator == " | " - - @pytest.mark.unit - def test_to_dict(self): - component = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.sentence_transformers_document_embedder.SentenceTransformersDocumentEmbedder", - "init_parameters": { - "model_name_or_path": "model", - "device": "cpu", - "token": None, - "prefix": "", - "suffix": "", - "batch_size": 32, - "progress_bar": True, - "normalize_embeddings": False, - "embedding_separator": "\n", - "metadata_fields_to_embed": [], - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = SentenceTransformersDocumentEmbedder( - model_name_or_path="model", - device="cuda", - token="the-token", - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - normalize_embeddings=True, - metadata_fields_to_embed=["meta_field"], - embedding_separator=" - ", - ) - data = component.to_dict() - - assert data == { - "type": "haystack.preview.components.embedders.sentence_transformers_document_embedder.SentenceTransformersDocumentEmbedder", - "init_parameters": { - "model_name_or_path": "model", - "device": "cuda", - "token": None, # the token is not serialized - "prefix": "prefix", - "suffix": "suffix", - "batch_size": 64, - "progress_bar": False, - "normalize_embeddings": True, - "embedding_separator": " - ", - "metadata_fields_to_embed": ["meta_field"], - }, - } - - @pytest.mark.unit - @patch( - "haystack.preview.components.embedders.sentence_transformers_document_embedder._SentenceTransformersEmbeddingBackendFactory" - ) - def test_warmup(self, mocked_factory): - embedder = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - mocked_factory.get_embedding_backend.assert_not_called() - embedder.warm_up() - mocked_factory.get_embedding_backend.assert_called_once_with( - model_name_or_path="model", device="cpu", use_auth_token=None - ) - - @pytest.mark.unit - @patch( - "haystack.preview.components.embedders.sentence_transformers_document_embedder._SentenceTransformersEmbeddingBackendFactory" - ) - def test_warmup_doesnt_reload(self, mocked_factory): - embedder = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - mocked_factory.get_embedding_backend.assert_not_called() - embedder.warm_up() - embedder.warm_up() - mocked_factory.get_embedding_backend.assert_called_once() - - @pytest.mark.unit - def test_run(self): - embedder = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - embedder.embedding_backend = MagicMock() - embedder.embedding_backend.embed = lambda x, **kwargs: np.random.rand(len(x), 16).tolist() - - documents = [Document(content=f"document number {i}") for i in range(5)] - - result = embedder.run(documents=documents) - - assert isinstance(result["documents"], list) - assert len(result["documents"]) == len(documents) - for doc in result["documents"]: - assert isinstance(doc, Document) - assert isinstance(doc.embedding, list) - assert isinstance(doc.embedding[0], float) - - @pytest.mark.unit - def test_run_wrong_input_format(self): - embedder = SentenceTransformersDocumentEmbedder(model_name_or_path="model") - - string_input = "text" - list_integers_input = [1, 2, 3] - - with pytest.raises( - TypeError, match="SentenceTransformersDocumentEmbedder expects a list of Documents as input" - ): - embedder.run(documents=string_input) - - with pytest.raises( - TypeError, match="SentenceTransformersDocumentEmbedder expects a list of Documents as input" - ): - embedder.run(documents=list_integers_input) - - @pytest.mark.unit - def test_embed_metadata(self): - embedder = SentenceTransformersDocumentEmbedder( - model_name_or_path="model", metadata_fields_to_embed=["meta_field"], embedding_separator="\n" - ) - embedder.embedding_backend = MagicMock() - - documents = [Document(content=f"document number {i}", meta={"meta_field": f"meta_value {i}"}) for i in range(5)] - - embedder.run(documents=documents) - - embedder.embedding_backend.embed.assert_called_once_with( - [ - "meta_value 0\ndocument number 0", - "meta_value 1\ndocument number 1", - "meta_value 2\ndocument number 2", - "meta_value 3\ndocument number 3", - "meta_value 4\ndocument number 4", - ], - batch_size=32, - show_progress_bar=True, - normalize_embeddings=False, - ) - - @pytest.mark.unit - def test_prefix_suffix(self): - embedder = SentenceTransformersDocumentEmbedder( - model_name_or_path="model", - prefix="my_prefix ", - suffix=" my_suffix", - metadata_fields_to_embed=["meta_field"], - embedding_separator="\n", - ) - embedder.embedding_backend = MagicMock() - - documents = [Document(content=f"document number {i}", meta={"meta_field": f"meta_value {i}"}) for i in range(5)] - - embedder.run(documents=documents) - - embedder.embedding_backend.embed.assert_called_once_with( - [ - "my_prefix meta_value 0\ndocument number 0 my_suffix", - "my_prefix meta_value 1\ndocument number 1 my_suffix", - "my_prefix meta_value 2\ndocument number 2 my_suffix", - "my_prefix meta_value 3\ndocument number 3 my_suffix", - "my_prefix meta_value 4\ndocument number 4 my_suffix", - ], - batch_size=32, - show_progress_bar=True, - normalize_embeddings=False, - ) diff --git a/test/preview/components/embedders/test_sentence_transformers_embedding_backend.py b/test/preview/components/embedders/test_sentence_transformers_embedding_backend.py deleted file mode 100644 index 4ac8c55869..0000000000 --- a/test/preview/components/embedders/test_sentence_transformers_embedding_backend.py +++ /dev/null @@ -1,42 +0,0 @@ -from unittest.mock import patch -import pytest -from haystack.preview.components.embedders.backends.sentence_transformers_backend import ( - _SentenceTransformersEmbeddingBackendFactory, -) - - -@pytest.mark.unit -@patch("haystack.preview.components.embedders.backends.sentence_transformers_backend.SentenceTransformer") -def test_factory_behavior(mock_sentence_transformer): - embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend( - model_name_or_path="my_model", device="cpu" - ) - same_embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend("my_model", "cpu") - another_embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend( - model_name_or_path="another_model", device="cpu" - ) - - assert same_embedding_backend is embedding_backend - assert another_embedding_backend is not embedding_backend - - -@pytest.mark.unit -@patch("haystack.preview.components.embedders.backends.sentence_transformers_backend.SentenceTransformer") -def test_model_initialization(mock_sentence_transformer): - _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend( - model_name_or_path="model", device="cpu", use_auth_token="my_token" - ) - mock_sentence_transformer.assert_called_once_with( - model_name_or_path="model", device="cpu", use_auth_token="my_token" - ) - - -@pytest.mark.unit -@patch("haystack.preview.components.embedders.backends.sentence_transformers_backend.SentenceTransformer") -def test_embedding_function_with_kwargs(mock_sentence_transformer): - embedding_backend = _SentenceTransformersEmbeddingBackendFactory.get_embedding_backend(model_name_or_path="model") - - data = ["sentence1", "sentence2"] - embedding_backend.embed(data=data, normalize_embeddings=True) - - embedding_backend.model.encode.assert_called_once_with(data, normalize_embeddings=True) diff --git a/test/preview/components/embedders/test_sentence_transformers_text_embedder.py b/test/preview/components/embedders/test_sentence_transformers_text_embedder.py deleted file mode 100644 index d93e576ac8..0000000000 --- a/test/preview/components/embedders/test_sentence_transformers_text_embedder.py +++ /dev/null @@ -1,151 +0,0 @@ -from unittest.mock import patch, MagicMock -import pytest - -import numpy as np - -from haystack.preview.components.embedders.sentence_transformers_text_embedder import SentenceTransformersTextEmbedder - - -class TestSentenceTransformersTextEmbedder: - @pytest.mark.unit - def test_init_default(self): - embedder = SentenceTransformersTextEmbedder(model_name_or_path="model") - assert embedder.model_name_or_path == "model" - assert embedder.device == "cpu" - assert embedder.token is None - assert embedder.prefix == "" - assert embedder.suffix == "" - assert embedder.batch_size == 32 - assert embedder.progress_bar is True - assert embedder.normalize_embeddings is False - - @pytest.mark.unit - def test_init_with_parameters(self): - embedder = SentenceTransformersTextEmbedder( - model_name_or_path="model", - device="cuda", - token=True, - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - normalize_embeddings=True, - ) - assert embedder.model_name_or_path == "model" - assert embedder.device == "cuda" - assert embedder.token is True - assert embedder.prefix == "prefix" - assert embedder.suffix == "suffix" - assert embedder.batch_size == 64 - assert embedder.progress_bar is False - assert embedder.normalize_embeddings is True - - @pytest.mark.unit - def test_to_dict(self): - component = SentenceTransformersTextEmbedder(model_name_or_path="model") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.sentence_transformers_text_embedder.SentenceTransformersTextEmbedder", - "init_parameters": { - "model_name_or_path": "model", - "device": "cpu", - "token": None, - "prefix": "", - "suffix": "", - "batch_size": 32, - "progress_bar": True, - "normalize_embeddings": False, - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = SentenceTransformersTextEmbedder( - model_name_or_path="model", - device="cuda", - token=True, - prefix="prefix", - suffix="suffix", - batch_size=64, - progress_bar=False, - normalize_embeddings=True, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.sentence_transformers_text_embedder.SentenceTransformersTextEmbedder", - "init_parameters": { - "model_name_or_path": "model", - "device": "cuda", - "token": True, - "prefix": "prefix", - "suffix": "suffix", - "batch_size": 64, - "progress_bar": False, - "normalize_embeddings": True, - }, - } - - @pytest.mark.unit - def test_to_dict_not_serialize_token(self): - component = SentenceTransformersTextEmbedder(model_name_or_path="model", token="awesome-token") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.embedders.sentence_transformers_text_embedder.SentenceTransformersTextEmbedder", - "init_parameters": { - "model_name_or_path": "model", - "device": "cpu", - "token": None, - "prefix": "", - "suffix": "", - "batch_size": 32, - "progress_bar": True, - "normalize_embeddings": False, - }, - } - - @pytest.mark.unit - @patch( - "haystack.preview.components.embedders.sentence_transformers_text_embedder._SentenceTransformersEmbeddingBackendFactory" - ) - def test_warmup(self, mocked_factory): - embedder = SentenceTransformersTextEmbedder(model_name_or_path="model") - mocked_factory.get_embedding_backend.assert_not_called() - embedder.warm_up() - mocked_factory.get_embedding_backend.assert_called_once_with( - model_name_or_path="model", device="cpu", use_auth_token=None - ) - - @pytest.mark.unit - @patch( - "haystack.preview.components.embedders.sentence_transformers_text_embedder._SentenceTransformersEmbeddingBackendFactory" - ) - def test_warmup_doesnt_reload(self, mocked_factory): - embedder = SentenceTransformersTextEmbedder(model_name_or_path="model") - mocked_factory.get_embedding_backend.assert_not_called() - embedder.warm_up() - embedder.warm_up() - mocked_factory.get_embedding_backend.assert_called_once() - - @pytest.mark.unit - def test_run(self): - embedder = SentenceTransformersTextEmbedder(model_name_or_path="model") - embedder.embedding_backend = MagicMock() - embedder.embedding_backend.embed = lambda x, **kwargs: np.random.rand(len(x), 16).tolist() - - text = "a nice text to embed" - - result = embedder.run(text=text) - embedding = result["embedding"] - - assert isinstance(embedding, list) - assert all(isinstance(el, float) for el in embedding) - - @pytest.mark.unit - def test_run_wrong_input_format(self): - embedder = SentenceTransformersTextEmbedder(model_name_or_path="model") - embedder.embedding_backend = MagicMock() - - list_integers_input = [1, 2, 3] - - with pytest.raises(TypeError, match="SentenceTransformersTextEmbedder expects a string as input"): - embedder.run(text=list_integers_input) diff --git a/test/preview/components/fetchers/__init__.py b/test/preview/components/fetchers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/fetchers/test_link_content_fetcher.py b/test/preview/components/fetchers/test_link_content_fetcher.py deleted file mode 100644 index 7d7d4f904d..0000000000 --- a/test/preview/components/fetchers/test_link_content_fetcher.py +++ /dev/null @@ -1,196 +0,0 @@ -from unittest.mock import patch, Mock - -import pytest -import requests - -from haystack.preview.components.fetchers.link_content import ( - LinkContentFetcher, - text_content_handler, - binary_content_handler, - DEFAULT_USER_AGENT, -) - -HTML_URL = "https://docs.haystack.deepset.ai/docs" -TEXT_URL = "https://raw.githubusercontent.com/deepset-ai/haystack/main/README.md" -PDF_URL = "https://raw.githubusercontent.com/deepset-ai/haystack/b5987a6d8d0714eb2f3011183ab40093d2e4a41a/e2e/samples/pipelines/sample_pdf_1.pdf" - - -@pytest.fixture -def mock_get_link_text_content(): - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = Mock( - status_code=200, text="Example test response", headers={"Content-Type": "text/plain"} - ) - yield mock_run - - -@pytest.fixture -def mock_get_link_content(test_files_path): - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = Mock( - status_code=200, - content=open(test_files_path / "pdf" / "sample_pdf_1.pdf", "rb").read(), - headers={"Content-Type": "application/pdf"}, - ) - yield mock_run - - -class TestLinkContentFetcher: - @pytest.mark.unit - def test_init(self): - fetcher = LinkContentFetcher() - assert fetcher.raise_on_failure is True - assert fetcher.user_agents == [DEFAULT_USER_AGENT] - assert fetcher.retry_attempts == 2 - assert fetcher.timeout == 3 - assert fetcher.handlers == { - "text/html": text_content_handler, - "text/plain": text_content_handler, - "application/pdf": binary_content_handler, - "application/octet-stream": binary_content_handler, - } - assert hasattr(fetcher, "_get_response") - - @pytest.mark.unit - def test_init_with_params(self): - fetcher = LinkContentFetcher(raise_on_failure=False, user_agents=["test"], retry_attempts=1, timeout=2) - assert fetcher.raise_on_failure is False - assert fetcher.user_agents == ["test"] - assert fetcher.retry_attempts == 1 - assert fetcher.timeout == 2 - - @pytest.mark.unit - def test_run_text(self): - correct_response = b"Example test response" - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = Mock( - status_code=200, text="Example test response", headers={"Content-Type": "text/plain"} - ) - fetcher = LinkContentFetcher() - streams = fetcher.run(urls=["https://www.example.com"])["streams"] - first_stream = streams[0] - assert first_stream.data == correct_response - assert first_stream.metadata["content_type"] == "text/plain" - - @pytest.mark.unit - def test_run_html(self): - correct_response = b"

Example test response

" - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = Mock( - status_code=200, text="

Example test response

", headers={"Content-Type": "text/html"} - ) - fetcher = LinkContentFetcher() - streams = fetcher.run(urls=["https://www.example.com"])["streams"] - first_stream = streams[0] - assert first_stream.data == correct_response - assert first_stream.metadata["content_type"] == "text/html" - - @pytest.mark.unit - def test_run_binary(self, test_files_path): - file_bytes = open(test_files_path / "pdf" / "sample_pdf_1.pdf", "rb").read() - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = Mock( - status_code=200, content=file_bytes, headers={"Content-Type": "application/pdf"} - ) - fetcher = LinkContentFetcher() - streams = fetcher.run(urls=["https://www.example.com"])["streams"] - first_stream = streams[0] - assert first_stream.data == file_bytes - assert first_stream.metadata["content_type"] == "application/pdf" - - @pytest.mark.unit - def test_run_bad_status_code(self): - empty_byte_stream = b"" - fetcher = LinkContentFetcher(raise_on_failure=False) - mock_response = Mock(status_code=403) - with patch("haystack.preview.components.fetchers.link_content.requests") as mock_run: - mock_run.get.return_value = mock_response - streams = fetcher.run(urls=["https://www.example.com"])["streams"] - - # empty byte stream is returned because raise_on_failure is False - assert len(streams) == 1 - first_stream = streams[0] - assert first_stream.data == empty_byte_stream - assert first_stream.metadata["content_type"] == "text/html" - - @pytest.mark.integration - def test_link_content_fetcher_html(self): - fetcher = LinkContentFetcher() - streams = fetcher.run([HTML_URL])["streams"] - first_stream = streams[0] - assert "Haystack" in first_stream.data.decode("utf-8") - assert first_stream.metadata["content_type"] == "text/html" - assert "url" in first_stream.metadata and first_stream.metadata["url"] == HTML_URL - - @pytest.mark.integration - def test_link_content_fetcher_text(self): - fetcher = LinkContentFetcher() - streams = fetcher.run([TEXT_URL])["streams"] - first_stream = streams[0] - assert "Haystack" in first_stream.data.decode("utf-8") - assert first_stream.metadata["content_type"] == "text/plain" - assert "url" in first_stream.metadata and first_stream.metadata["url"] == TEXT_URL - - @pytest.mark.integration - def test_link_content_fetcher_pdf(self): - fetcher = LinkContentFetcher() - streams = fetcher.run([PDF_URL])["streams"] - assert len(streams) == 1 - first_stream = streams[0] - assert first_stream.metadata["content_type"] in ("application/octet-stream", "application/pdf") - assert "url" in first_stream.metadata and first_stream.metadata["url"] == PDF_URL - - @pytest.mark.integration - def test_link_content_fetcher_multiple_different_content_types(self): - """ - This test is to ensure that the fetcher can handle a list of URLs that contain different content types. - """ - fetcher = LinkContentFetcher() - streams = fetcher.run([PDF_URL, HTML_URL])["streams"] - assert len(streams) == 2 - for stream in streams: - assert stream.metadata["content_type"] in ("text/html", "application/pdf", "application/octet-stream") - if stream.metadata["content_type"] == "text/html": - assert "Haystack" in stream.data.decode("utf-8") - elif stream.metadata["content_type"] == "application/pdf": - assert len(stream.data) > 0 - - @pytest.mark.integration - def test_link_content_fetcher_multiple_html_streams(self): - """ - This test is to ensure that the fetcher can handle a list of URLs that contain different content types, - and that we have two html streams. - """ - - fetcher = LinkContentFetcher() - streams = fetcher.run([PDF_URL, HTML_URL, "https://google.com"])["streams"] - assert len(streams) == 3 - for stream in streams: - assert stream.metadata["content_type"] in ("text/html", "application/pdf", "application/octet-stream") - if stream.metadata["content_type"] == "text/html": - assert "Haystack" in stream.data.decode("utf-8") or "Google" in stream.data.decode("utf-8") - elif stream.metadata["content_type"] == "application/pdf": - assert len(stream.data) > 0 - - @pytest.mark.integration - def test_mix_of_good_and_failed_requests(self): - """ - This test is to ensure that the fetcher can handle a list of URLs that contain URLs that fail to be fetched. - In such a case, the fetcher should return the content of the URLs that were successfully fetched and not raise - an exception. - """ - fetcher = LinkContentFetcher() - result = fetcher.run(["https://non_existent_website_dot.com/", "https://www.google.com/"]) - assert len(result["streams"]) == 1 - first_stream = result["streams"][0] - assert first_stream.metadata["content_type"] == "text/html" - - @pytest.mark.integration - def test_bad_request_exception_raised(self): - """ - This test is to ensure that the fetcher raises an exception when a single bad request is made and it is configured to - do so. - """ - fetcher = LinkContentFetcher() - with pytest.raises(requests.exceptions.ConnectionError): - fetcher.run(["https://non_existent_website_dot.com/"]) diff --git a/test/preview/components/generators/chat/__init__.py b/test/preview/components/generators/chat/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/generators/chat/conftest.py b/test/preview/components/generators/chat/conftest.py deleted file mode 100644 index 7a6e7a0fba..0000000000 --- a/test/preview/components/generators/chat/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - -from haystack.preview.dataclasses import ChatMessage - - -@pytest.fixture -def chat_messages(): - return [ - ChatMessage.from_system("You are a helpful assistant speaking A2 level of English"), - ChatMessage.from_user("Tell me about Berlin"), - ] diff --git a/test/preview/components/generators/chat/test_hugging_face_tgi.py b/test/preview/components/generators/chat/test_hugging_face_tgi.py deleted file mode 100644 index 35de294176..0000000000 --- a/test/preview/components/generators/chat/test_hugging_face_tgi.py +++ /dev/null @@ -1,317 +0,0 @@ -from unittest.mock import patch, MagicMock, Mock - -import pytest -from huggingface_hub.inference._text_generation import TextGenerationStreamResponse, Token, StreamDetails, FinishReason -from huggingface_hub.utils import RepositoryNotFoundError - -from haystack.preview.components.generators.chat import HuggingFaceTGIChatGenerator - -from haystack.preview.dataclasses import StreamingChunk, ChatMessage - - -@pytest.fixture -def mock_check_valid_model(): - with patch( - "haystack.preview.components.generators.chat.hugging_face_tgi.check_valid_model", MagicMock(return_value=None) - ) as mock: - yield mock - - -@pytest.fixture -def mock_text_generation(): - with patch("huggingface_hub.InferenceClient.text_generation", autospec=True) as mock_text_generation: - mock_response = Mock() - mock_response.generated_text = "I'm fine, thanks." - details = Mock() - details.finish_reason = MagicMock(field1="value") - details.tokens = [1, 2, 3] - mock_response.details = details - mock_text_generation.return_value = mock_response - yield mock_text_generation - - -# used to test serialization of streaming_callback -def streaming_callback_handler(x): - return x - - -class TestHuggingFaceTGIChatGenerator: - @pytest.mark.unit - def test_initialize_with_valid_model_and_generation_parameters(self, mock_check_valid_model, mock_auto_tokenizer): - model = "HuggingFaceH4/zephyr-7b-alpha" - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIChatGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - generator.warm_up() - - assert generator.generation_kwargs == {**generation_kwargs, **{"stop_sequences": ["stop"]}} - assert generator.tokenizer is not None - assert generator.client is not None - assert generator.streaming_callback == streaming_callback - - @pytest.mark.unit - def test_to_dict(self, mock_check_valid_model): - # Initialize the HuggingFaceTGIChatGenerator object with valid parameters - generator = HuggingFaceTGIChatGenerator( - model="NousResearch/Llama-2-7b-chat-hf", - token="token", - generation_kwargs={"n": 5}, - stop_words=["stop", "words"], - streaming_callback=lambda x: x, - ) - - # Call the to_dict method - result = generator.to_dict() - init_params = result["init_parameters"] - - # Assert that the init_params dictionary contains the expected keys and values - assert init_params["model"] == "NousResearch/Llama-2-7b-chat-hf" - assert init_params["token"] is None - assert init_params["generation_kwargs"] == {"n": 5, "stop_sequences": ["stop", "words"]} - - @pytest.mark.unit - def test_from_dict(self, mock_check_valid_model): - generator = HuggingFaceTGIChatGenerator( - model="NousResearch/Llama-2-7b-chat-hf", - generation_kwargs={"n": 5}, - stop_words=["stop", "words"], - streaming_callback=streaming_callback_handler, - ) - # Call the to_dict method - result = generator.to_dict() - - generator_2 = HuggingFaceTGIChatGenerator.from_dict(result) - assert generator_2.model == "NousResearch/Llama-2-7b-chat-hf" - assert generator_2.generation_kwargs == {"n": 5, "stop_sequences": ["stop", "words"]} - assert generator_2.streaming_callback is streaming_callback_handler - - @pytest.mark.unit - def test_warm_up(self, mock_check_valid_model, mock_auto_tokenizer): - generator = HuggingFaceTGIChatGenerator() - generator.warm_up() - - # Assert that the tokenizer is now initialized - assert generator.tokenizer is not None - - @pytest.mark.unit - def test_warm_up_no_chat_template(self, mock_check_valid_model, mock_auto_tokenizer, caplog): - generator = HuggingFaceTGIChatGenerator(model="meta-llama/Llama-2-13b-chat-hf") - - # Set chat_template to None for this specific test - mock_auto_tokenizer.chat_template = None - generator.warm_up() - - # warning message should be logged - assert "The model 'meta-llama/Llama-2-13b-chat-hf' doesn't have a default chat_template" in caplog.text - - @pytest.mark.unit - def test_custom_chat_template( - self, chat_messages, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation - ): - custom_chat_template = "Here goes some Jinja template" - - # mocked method to check if we called apply_chat_template with the custom template - mock_auto_tokenizer.apply_chat_template = MagicMock(return_value="some_value") - - generator = HuggingFaceTGIChatGenerator(chat_template=custom_chat_template) - generator.warm_up() - - assert generator.chat_template == custom_chat_template - - generator.run(messages=chat_messages) - assert mock_auto_tokenizer.apply_chat_template.call_count == 1 - - # and we indeed called apply_chat_template with the custom template - _, kwargs = mock_auto_tokenizer.apply_chat_template.call_args - assert kwargs["chat_template"] == custom_chat_template - - @pytest.mark.unit - def test_initialize_with_invalid_model_path_or_url(self, mock_check_valid_model): - model = "invalid_model" - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - mock_check_valid_model.side_effect = ValueError("Invalid model path or url") - - with pytest.raises(ValueError): - HuggingFaceTGIChatGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - - @pytest.mark.unit - def test_initialize_with_invalid_url(self, mock_check_valid_model): - with pytest.raises(ValueError): - HuggingFaceTGIChatGenerator(model="NousResearch/Llama-2-7b-chat-hf", url="invalid_url") - - @pytest.mark.unit - def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model): - # When custom TGI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id - mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id") - with pytest.raises(RepositoryNotFoundError): - HuggingFaceTGIChatGenerator(model="invalid_model_id", url="https://some_chat_model.com") - - @pytest.mark.unit - def test_generate_text_response_with_valid_prompt_and_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, chat_messages - ): - model = "meta-llama/Llama-2-13b-chat-hf" - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIChatGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - generator.warm_up() - - response = generator.run(messages=chat_messages) - - # check kwargs passed to text_generation - # note how n because it is not text generation parameter was not passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop"]} - - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_multiple_text_responses_with_valid_prompt_and_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, chat_messages - ): - model = "meta-llama/Llama-2-13b-chat-hf" - token = None - generation_kwargs = {"n": 3} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIChatGenerator( - model=model, - token=token, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - generator.warm_up() - - response = generator.run(chat_messages) - - # check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop"]} - - # note how n caused n replies to be generated - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 3 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_text_with_stop_words( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, chat_messages - ): - generator = HuggingFaceTGIChatGenerator() - generator.warm_up() - - stop_words = ["stop", "words"] - - # Generate text response with stop words - response = generator.run(chat_messages, generation_kwargs={"stop_words": stop_words}) - - # check kwargs passed to text_generation - # we translate stop_words to stop_sequences - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop", "words"]} - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_text_with_custom_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, chat_messages - ): - # Create an instance of HuggingFaceRemoteGenerator with no generation parameters - generator = HuggingFaceTGIChatGenerator() - generator.warm_up() - - # but then we pass them in run - generation_kwargs = {"temperature": 0.8, "max_new_tokens": 100} - response = generator.run(chat_messages, generation_kwargs=generation_kwargs) - - # again check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "max_new_tokens": 100, "stop_sequences": [], "temperature": 0.8} - - # Assert that the response contains the generated replies and the right response - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - assert response["replies"][0].content == "I'm fine, thanks." - - @pytest.mark.unit - def test_generate_text_with_streaming_callback( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, chat_messages - ): - streaming_call_count = 0 - - # Define the streaming callback function - def streaming_callback_fn(chunk: StreamingChunk): - nonlocal streaming_call_count - streaming_call_count += 1 - assert isinstance(chunk, StreamingChunk) - - # Create an instance of HuggingFaceRemoteGenerator - generator = HuggingFaceTGIChatGenerator(streaming_callback=streaming_callback_fn) - generator.warm_up() - - # Create a fake streamed response - # self needed here, don't remove - def mock_iter(self): - yield TextGenerationStreamResponse( - generated_text=None, token=Token(id=1, text="I'm fine, thanks.", logprob=0.0, special=False) - ) - yield TextGenerationStreamResponse( - generated_text=None, - token=Token(id=1, text="Ok bye", logprob=0.0, special=False), - details=StreamDetails(finish_reason=FinishReason.Length, generated_tokens=5), - ) - - mock_response = Mock(**{"__iter__": mock_iter}) - mock_text_generation.return_value = mock_response - - # Generate text response with streaming callback - response = generator.run(chat_messages) - - # check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": [], "stream": True} - - # Assert that the streaming callback was called twice - assert streaming_call_count == 2 - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] diff --git a/test/preview/components/generators/chat/test_openai.py b/test/preview/components/generators/chat/test_openai.py deleted file mode 100644 index 9535bc14d7..0000000000 --- a/test/preview/components/generators/chat/test_openai.py +++ /dev/null @@ -1,330 +0,0 @@ -import os -from unittest.mock import patch, Mock - -import openai -import pytest - -from haystack.preview.components.generators.chat import GPTChatGenerator -from haystack.preview.components.generators.utils import default_streaming_callback -from haystack.preview.dataclasses import ChatMessage, StreamingChunk - - -@pytest.fixture -def mock_chat_completion(): - """ - Mock the OpenAI API completion response and reuse it for tests - """ - with patch("openai.ChatCompletion.create", autospec=True) as mock_chat_completion_create: - # mimic the response from the OpenAI API - mock_choice = Mock() - mock_choice.index = 0 - mock_choice.finish_reason = "stop" - - mock_message = Mock() - mock_message.content = "I'm fine, thanks. How are you?" - mock_message.role = "user" - - mock_choice.message = mock_message - - mock_response = Mock() - mock_response.model = "gpt-3.5-turbo" - mock_response.usage = Mock() - mock_response.usage.items.return_value = [ - ("prompt_tokens", 57), - ("completion_tokens", 40), - ("total_tokens", 97), - ] - mock_response.choices = [mock_choice] - mock_chat_completion_create.return_value = mock_response - yield mock_chat_completion_create - - -def streaming_chunk(content: str): - """ - Mock chunks of streaming responses from the OpenAI API - """ - # mimic the chunk response from the OpenAI API - mock_choice = Mock() - mock_choice.index = 0 - mock_choice.delta.content = content - mock_choice.finish_reason = "stop" - - mock_response = Mock() - mock_response.choices = [mock_choice] - mock_response.model = "gpt-3.5-turbo" - mock_response.usage = Mock() - mock_response.usage.items.return_value = [("prompt_tokens", 57), ("completion_tokens", 40), ("total_tokens", 97)] - return mock_response - - -@pytest.fixture -def chat_messages(): - return [ - ChatMessage.from_system("You are a helpful assistant"), - ChatMessage.from_user("What's the capital of France"), - ] - - -class TestGPTChatGenerator: - @pytest.mark.unit - def test_init_default(self): - component = GPTChatGenerator(api_key="test-api-key") - assert openai.api_key == "test-api-key" - assert component.model_name == "gpt-3.5-turbo" - assert component.streaming_callback is None - assert component.api_base_url == "https://api.openai.com/v1" - assert openai.api_base == "https://api.openai.com/v1" - assert not component.generation_kwargs - - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - with pytest.raises(ValueError, match="GPTChatGenerator expects an OpenAI API key"): - GPTChatGenerator() - - @pytest.mark.unit - def test_init_with_parameters(self): - component = GPTChatGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=default_streaming_callback, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - assert openai.api_key == "test-api-key" - assert component.model_name == "gpt-4" - assert component.streaming_callback is default_streaming_callback - assert component.api_base_url == "test-base-url" - assert openai.api_base == "test-base-url" - assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"} - - @pytest.mark.unit - def test_to_dict_default(self): - component = GPTChatGenerator(api_key="test-api-key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.chat.openai.GPTChatGenerator", - "init_parameters": { - "model_name": "gpt-3.5-turbo", - "streaming_callback": None, - "api_base_url": "https://api.openai.com/v1", - "generation_kwargs": {}, - }, - } - - @pytest.mark.unit - def test_to_dict_with_parameters(self): - component = GPTChatGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=default_streaming_callback, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.chat.openai.GPTChatGenerator", - "init_parameters": { - "model_name": "gpt-4", - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - - @pytest.mark.unit - def test_to_dict_with_lambda_streaming_callback(self): - component = GPTChatGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=lambda x: x, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.chat.openai.GPTChatGenerator", - "init_parameters": { - "model_name": "gpt-4", - "api_base_url": "test-base-url", - "streaming_callback": "chat.test_openai.", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - - @pytest.mark.unit - def test_from_dict(self, monkeypatch): - monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key") - data = { - "type": "haystack.preview.components.generators.chat.openai.GPTChatGenerator", - "init_parameters": { - "model_name": "gpt-4", - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - component = GPTChatGenerator.from_dict(data) - assert component.model_name == "gpt-4" - assert component.streaming_callback is default_streaming_callback - assert component.api_base_url == "test-base-url" - assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"} - - @pytest.mark.unit - def test_from_dict_fail_wo_env_var(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - data = { - "type": "haystack.preview.components.generators.chat.openai.GPTChatGenerator", - "init_parameters": { - "model_name": "gpt-4", - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - with pytest.raises(ValueError, match="GPTChatGenerator expects an OpenAI API key"): - GPTChatGenerator.from_dict(data) - - @pytest.mark.unit - def test_run(self, chat_messages, mock_chat_completion): - component = GPTChatGenerator(api_key="test-api-key") - response = component.run(chat_messages) - - # check that the component returns the correct ChatMessage response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_run_with_params(self, chat_messages, mock_chat_completion): - component = GPTChatGenerator(api_key="test-api-key", generation_kwargs={"max_tokens": 10, "temperature": 0.5}) - response = component.run(chat_messages) - - # check that the component calls the OpenAI API with the correct parameters - _, kwargs = mock_chat_completion.call_args - assert kwargs["max_tokens"] == 10 - assert kwargs["temperature"] == 0.5 - - # check that the component returns the correct response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_run_streaming(self, chat_messages, mock_chat_completion): - streaming_call_count = 0 - - # Define the streaming callback function and assert that it is called with StreamingChunk objects - def streaming_callback_fn(chunk: StreamingChunk): - nonlocal streaming_call_count - streaming_call_count += 1 - assert isinstance(chunk, StreamingChunk) - - generator = GPTChatGenerator(api_key="test-api-key", streaming_callback=streaming_callback_fn) - - # Create a fake streamed response - # self needed here, don't remove - def mock_iter(self): - yield streaming_chunk("Hello") - yield streaming_chunk("How are you?") - - mock_response = Mock(**{"__iter__": mock_iter}) - mock_chat_completion.return_value = mock_response - - response = generator.run(chat_messages) - - # Assert that the streaming callback was called twice - assert streaming_call_count == 2 - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.unit - def test_check_abnormal_completions(self, caplog): - component = GPTChatGenerator(api_key="test-api-key") - messages = [ - ChatMessage.from_assistant( - "", metadata={"finish_reason": "content_filter" if i % 2 == 0 else "length", "index": i} - ) - for i, _ in enumerate(range(4)) - ] - - for m in messages: - component._check_finish_reason(m) - - # check truncation warning - message_template = ( - "The completion for index {index} has been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions." - ) - - for index in [1, 3]: - assert caplog.records[index].message == message_template.format(index=index) - - # check content filter warning - message_template = "The completion for index {index} has been truncated due to the content filter." - for index in [0, 2]: - assert caplog.records[index].message == message_template.format(index=index) - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run(self): - chat_messages = [ChatMessage.from_user("What's the capital of France")] - component = GPTChatGenerator(api_key=os.environ.get("OPENAI_API_KEY"), generation_kwargs={"n": 1}) - results = component.run(chat_messages) - assert len(results["replies"]) == 1 - message: ChatMessage = results["replies"][0] - assert "Paris" in message.content - assert "gpt-3.5" in message.metadata["model"] - assert message.metadata["finish_reason"] == "stop" - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_wrong_model(self, chat_messages): - component = GPTChatGenerator(model_name="something-obviously-wrong", api_key=os.environ.get("OPENAI_API_KEY")) - with pytest.raises(openai.InvalidRequestError, match="The model `something-obviously-wrong` does not exist"): - component.run(chat_messages) - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_streaming(self): - class Callback: - def __init__(self): - self.responses = "" - self.counter = 0 - - def __call__(self, chunk: StreamingChunk) -> None: - self.counter += 1 - self.responses += chunk.content if chunk.content else "" - - callback = Callback() - component = GPTChatGenerator(os.environ.get("OPENAI_API_KEY"), streaming_callback=callback) - results = component.run([ChatMessage.from_user("What's the capital of France?")]) - - assert len(results["replies"]) == 1 - message: ChatMessage = results["replies"][0] - assert "Paris" in message.content - - assert "gpt-3.5" in message.metadata["model"] - assert message.metadata["finish_reason"] == "stop" - - assert callback.counter > 1 - assert "Paris" in callback.responses diff --git a/test/preview/components/generators/conftest.py b/test/preview/components/generators/conftest.py deleted file mode 100644 index 435b36ea07..0000000000 --- a/test/preview/components/generators/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest.mock import patch, MagicMock - -import pytest - - -@pytest.fixture -def mock_auto_tokenizer(): - """ - In the original mock_auto_tokenizer fixture, we were mocking the transformers.AutoTokenizer.from_pretrained - method directly, but we were not providing a return value for this method. Therefore, when from_pretrained - was called within HuggingFaceTGIChatGenerator, it returned None because that's the default behavior of a - MagicMock object when a return value isn't specified. - - We will update the mock_auto_tokenizer fixture to return a MagicMock object when from_pretrained is called - in another PR. For now, we will use this fixture to mock the AutoTokenizer.from_pretrained method. - """ - - with patch("transformers.AutoTokenizer.from_pretrained", autospec=True) as mock_from_pretrained: - mock_tokenizer = MagicMock() - mock_from_pretrained.return_value = mock_tokenizer - yield mock_tokenizer diff --git a/test/preview/components/generators/test_cohere_generators.py b/test/preview/components/generators/test_cohere_generators.py deleted file mode 100644 index cd1f9cb2a4..0000000000 --- a/test/preview/components/generators/test_cohere_generators.py +++ /dev/null @@ -1,172 +0,0 @@ -import os - -import pytest -import cohere - -from haystack.preview.components.generators import CohereGenerator - - -def default_streaming_callback(chunk): - """ - Default callback function for streaming responses from Cohere API. - Prints the tokens of the first completion to stdout as soon as they are received and returns the chunk unchanged. - """ - print(chunk.text, flush=True, end="") - - -class TestGPTGenerator: - def test_init_default(self): - component = CohereGenerator(api_key="test-api-key") - assert component.api_key == "test-api-key" - assert component.model_name == "command" - assert component.streaming_callback is None - assert component.api_base_url == cohere.COHERE_API_URL - assert component.model_parameters == {} - - def test_init_with_parameters(self): - callback = lambda x: x - component = CohereGenerator( - api_key="test-api-key", - model_name="command-light", - max_tokens=10, - some_test_param="test-params", - streaming_callback=callback, - api_base_url="test-base-url", - ) - assert component.api_key == "test-api-key" - assert component.model_name == "command-light" - assert component.streaming_callback == callback - assert component.api_base_url == "test-base-url" - assert component.model_parameters == {"max_tokens": 10, "some_test_param": "test-params"} - - def test_to_dict_default(self): - component = CohereGenerator(api_key="test-api-key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.cohere.CohereGenerator", - "init_parameters": { - "model_name": "command", - "streaming_callback": None, - "api_base_url": cohere.COHERE_API_URL, - }, - } - - def test_to_dict_with_parameters(self): - component = CohereGenerator( - api_key="test-api-key", - model_name="command-light", - max_tokens=10, - some_test_param="test-params", - streaming_callback=default_streaming_callback, - api_base_url="test-base-url", - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.cohere.CohereGenerator", - "init_parameters": { - "model_name": "command-light", - "max_tokens": 10, - "some_test_param": "test-params", - "api_base_url": "test-base-url", - "streaming_callback": "test_cohere_generators.default_streaming_callback", - }, - } - - def test_to_dict_with_lambda_streaming_callback(self): - component = CohereGenerator( - api_key="test-api-key", - model_name="command", - max_tokens=10, - some_test_param="test-params", - streaming_callback=lambda x: x, - api_base_url="test-base-url", - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.cohere.CohereGenerator", - "init_parameters": { - "model_name": "command", - "streaming_callback": "test_cohere_generators.", - "api_base_url": "test-base-url", - "max_tokens": 10, - "some_test_param": "test-params", - }, - } - - def test_from_dict(self, monkeypatch): - monkeypatch.setenv("COHERE_API_KEY", "test-key") - data = { - "type": "haystack.preview.components.generators.cohere.CohereGenerator", - "init_parameters": { - "model_name": "command", - "max_tokens": 10, - "some_test_param": "test-params", - "api_base_url": "test-base-url", - "streaming_callback": "test_cohere_generators.default_streaming_callback", - }, - } - component = CohereGenerator.from_dict(data) - assert component.api_key == "test-key" - assert component.model_name == "command" - assert component.streaming_callback == default_streaming_callback - assert component.api_base_url == "test-base-url" - assert component.model_parameters == {"max_tokens": 10, "some_test_param": "test-params"} - - def test_check_truncated_answers(self, caplog): - component = CohereGenerator(api_key="test-api-key") - metadata = [{"finish_reason": "MAX_TOKENS"}] - component._check_truncated_answers(metadata) - assert caplog.records[0].message == ( - "Responses have been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions." - ) - - @pytest.mark.skipif( - not os.environ.get("COHERE_API_KEY", None), - reason="Export an env var called CO_API_KEY containing the Cohere API key to run this test.", - ) - @pytest.mark.integration - def test_cohere_generator_run(self): - component = CohereGenerator(api_key=os.environ.get("COHERE_API_KEY")) - results = component.run(prompt="What's the capital of France?") - assert len(results["replies"]) == 1 - assert "Paris" in results["replies"][0] - assert len(results["metadata"]) == 1 - assert results["metadata"][0]["finish_reason"] == "COMPLETE" - - @pytest.mark.skipif( - not os.environ.get("COHERE_API_KEY", None), - reason="Export an env var called COHERE_API_KEY containing the Cohere API key to run this test.", - ) - @pytest.mark.integration - def test_cohere_generator_run_wrong_model_name(self): - component = CohereGenerator(model_name="something-obviously-wrong", api_key=os.environ.get("COHERE_API_KEY")) - with pytest.raises( - cohere.CohereAPIError, - match="model not found, make sure the correct model ID was used and that you have access to the model.", - ): - component.run(prompt="What's the capital of France?") - - @pytest.mark.skipif( - not os.environ.get("COHERE_API_KEY", None), - reason="Export an env var called COHERE_API_KEY containing the Cohere API key to run this test.", - ) - @pytest.mark.integration - def test_cohere_generator_run_streaming(self): - class Callback: - def __init__(self): - self.responses = "" - - def __call__(self, chunk): - self.responses += chunk.text - return chunk - - callback = Callback() - component = CohereGenerator(os.environ.get("COHERE_API_KEY"), streaming_callback=callback) - results = component.run(prompt="What's the capital of France?") - - assert len(results["replies"]) == 1 - assert "Paris" in results["replies"][0] - assert len(results["metadata"]) == 1 - assert results["metadata"][0]["finish_reason"] == "COMPLETE" - assert callback.responses == results["replies"][0] diff --git a/test/preview/components/generators/test_hf_utils.py b/test/preview/components/generators/test_hf_utils.py deleted file mode 100644 index f69a099743..0000000000 --- a/test/preview/components/generators/test_hf_utils.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest - -from haystack.preview.components.generators.hf_utils import check_generation_params - - -@pytest.mark.unit -def test_empty_dictionary(): - # no exception raised - check_generation_params({}) - - -@pytest.mark.unit -def test_valid_generation_parameters(): - # these are valid parameters - kwargs = {"max_new_tokens": 100, "temperature": 0.8} - additional_accepted_params = None - check_generation_params(kwargs, additional_accepted_params) - - -@pytest.mark.unit -def test_invalid_generation_parameters(): - # these are invalid parameters - kwargs = {"invalid_param": "value"} - additional_accepted_params = None - with pytest.raises(ValueError): - check_generation_params(kwargs, additional_accepted_params) - - -@pytest.mark.unit -def test_additional_accepted_params_empty_list(): - kwargs = {"temperature": 0.8} - additional_accepted_params = [] - check_generation_params(kwargs, additional_accepted_params) - - -@pytest.mark.unit -def test_additional_accepted_params_known_parameter(): - # both are valid parameters - kwargs = {"temperature": 0.8} - additional_accepted_params = ["max_new_tokens"] - check_generation_params(kwargs, additional_accepted_params) - - -@pytest.mark.unit -def test_additional_accepted_params_unknown_parameter(): - kwargs = {"strange_param": "value"} - additional_accepted_params = ["strange_param"] - # Although strange_param is not generation param the check_generation_params - # does not raise exception because strange_param is passed as additional_accepted_params - check_generation_params(kwargs, additional_accepted_params) diff --git a/test/preview/components/generators/test_hugging_face_local_generator.py b/test/preview/components/generators/test_hugging_face_local_generator.py deleted file mode 100644 index 367a54c640..0000000000 --- a/test/preview/components/generators/test_hugging_face_local_generator.py +++ /dev/null @@ -1,349 +0,0 @@ -# pylint: disable=too-many-public-methods -from unittest.mock import patch, Mock - -import pytest -import torch - -from haystack.preview.components.generators.hugging_face_local import HuggingFaceLocalGenerator, StopWordsCriteria - - -class TestHuggingFaceLocalGenerator: - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.model_info") - def test_init_default(self, model_info_mock): - model_info_mock.return_value.pipeline_tag = "text2text-generation" - generator = HuggingFaceLocalGenerator() - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - } - assert generator.generation_kwargs == {} - assert generator.pipeline is None - - @pytest.mark.unit - def test_init_custom_token(self): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", token="test-token" - ) - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": "test-token", - } - - @pytest.mark.unit - def test_init_custom_device(self): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", device="cuda:0" - ) - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - "device": "cuda:0", - } - - @pytest.mark.unit - def test_init_task_parameter(self): - generator = HuggingFaceLocalGenerator(task="text2text-generation") - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - } - - @pytest.mark.unit - def test_init_task_in_huggingface_pipeline_kwargs(self): - generator = HuggingFaceLocalGenerator(huggingface_pipeline_kwargs={"task": "text2text-generation"}) - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - } - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.model_info") - def test_init_task_inferred_from_model_name(self, model_info_mock): - model_info_mock.return_value.pipeline_tag = "text2text-generation" - generator = HuggingFaceLocalGenerator(model_name_or_path="google/flan-t5-base") - - assert generator.huggingface_pipeline_kwargs == { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - } - - @pytest.mark.unit - def test_init_invalid_task(self): - with pytest.raises(ValueError, match="is not supported."): - HuggingFaceLocalGenerator(task="text-classification") - - @pytest.mark.unit - def test_init_huggingface_pipeline_kwargs_override_other_parameters(self): - """ - huggingface_pipeline_kwargs represent the main configuration of this component. - If they are provided, they should override other init parameters. - """ - - huggingface_pipeline_kwargs = { - "model": "gpt2", - "task": "text-generation", - "device": "cuda:0", - "token": "another-test-token", - } - - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", - task="text2text-generation", - device="cpu", - token="test-token", - huggingface_pipeline_kwargs=huggingface_pipeline_kwargs, - ) - - assert generator.huggingface_pipeline_kwargs == huggingface_pipeline_kwargs - - @pytest.mark.unit - def test_init_generation_kwargs(self): - generator = HuggingFaceLocalGenerator(task="text2text-generation", generation_kwargs={"max_new_tokens": 100}) - - assert generator.generation_kwargs == {"max_new_tokens": 100} - - @pytest.mark.unit - def test_init_set_return_full_text(self): - """ - if not specified, return_full_text is set to False for text-generation task - (only generated text is returned, excluding prompt) - """ - generator = HuggingFaceLocalGenerator(task="text-generation") - - assert generator.generation_kwargs == {"return_full_text": False} - - @pytest.mark.unit - def test_init_fails_with_both_stopwords_and_stoppingcriteria(self): - with pytest.raises( - ValueError, - match="Found both the `stop_words` init parameter and the `stopping_criteria` key in `generation_kwargs`", - ): - HuggingFaceLocalGenerator( - task="text2text-generation", - stop_words=["coca", "cola"], - generation_kwargs={"stopping_criteria": "fake-stopping-criteria"}, - ) - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.model_info") - def test_to_dict_default(self, model_info_mock): - model_info_mock.return_value.pipeline_tag = "text2text-generation" - - component = HuggingFaceLocalGenerator() - data = component.to_dict() - - assert data == { - "type": "haystack.preview.components.generators.hugging_face_local.HuggingFaceLocalGenerator", - "init_parameters": { - "huggingface_pipeline_kwargs": { - "model": "google/flan-t5-base", - "task": "text2text-generation", - "token": None, - }, - "generation_kwargs": {}, - "stop_words": None, - }, - } - - @pytest.mark.unit - def test_to_dict_with_parameters(self): - component = HuggingFaceLocalGenerator( - model_name_or_path="gpt2", - task="text-generation", - device="cuda:0", - token="test-token", - generation_kwargs={"max_new_tokens": 100}, - stop_words=["coca", "cola"], - ) - data = component.to_dict() - - assert data == { - "type": "haystack.preview.components.generators.hugging_face_local.HuggingFaceLocalGenerator", - "init_parameters": { - "huggingface_pipeline_kwargs": { - "model": "gpt2", - "task": "text-generation", - "token": None, # we don't want serialize valid tokens - "device": "cuda:0", - }, - "generation_kwargs": {"max_new_tokens": 100, "return_full_text": False}, - "stop_words": ["coca", "cola"], - }, - } - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.pipeline") - def test_warm_up(self, pipeline_mock): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", token="test-token" - ) - pipeline_mock.assert_not_called() - - generator.warm_up() - - pipeline_mock.assert_called_once_with( - model="google/flan-t5-base", task="text2text-generation", token="test-token" - ) - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.pipeline") - def test_warm_up_doesn_reload(self, pipeline_mock): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", token="test-token" - ) - - pipeline_mock.assert_not_called() - - generator.warm_up() - generator.warm_up() - - pipeline_mock.assert_called_once() - - @pytest.mark.unit - def test_run(self): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", - task="text2text-generation", - generation_kwargs={"max_new_tokens": 100}, - ) - - # create the pipeline object (simulating the warm_up) - generator.pipeline = Mock(return_value=[{"generated_text": "Rome"}]) - - results = generator.run(prompt="What's the capital of Italy?") - - generator.pipeline.assert_called_once_with( - "What's the capital of Italy?", max_new_tokens=100, stopping_criteria=None - ) - assert results == {"replies": ["Rome"]} - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.pipeline") - def test_run_empty_prompt(self, pipeline_mock): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", - task="text2text-generation", - generation_kwargs={"max_new_tokens": 100}, - ) - - generator.warm_up() - - results = generator.run(prompt="") - - assert results == {"replies": []} - - @pytest.mark.unit - def test_run_with_generation_kwargs(self): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", - task="text2text-generation", - generation_kwargs={"max_new_tokens": 100}, - ) - - # create the pipeline object (simulating the warm_up) - generator.pipeline = Mock(return_value=[{"generated_text": "Rome"}]) - - generator.run(prompt="irrelevant", generation_kwargs={"max_new_tokens": 200, "temperature": 0.5}) - - generator.pipeline.assert_called_once_with( - "irrelevant", max_new_tokens=200, temperature=0.5, stopping_criteria=None - ) - - @pytest.mark.unit - def test_run_fails_without_warm_up(self): - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", - task="text2text-generation", - generation_kwargs={"max_new_tokens": 100}, - ) - - with pytest.raises(RuntimeError, match="The generation model has not been loaded."): - generator.run(prompt="irrelevant") - - @pytest.mark.unit - def test_stop_words_criteria(self): - """ - Test that StopWordsCriteria will check stop word tokens in a continuous and sequential order - """ - # input ids for "unambiguously" - stop_words_id = torch.tensor([[73, 24621, 11937]]) - - # input ids for "This is ambiguously, but is unrelated." - input_ids1 = torch.tensor([[100, 19, 24621, 11937, 6, 68, 19, 73, 3897, 5]]) - # input ids for "This is unambiguously" - input_ids2 = torch.tensor([[100, 19, 73, 24621, 11937]]) - - # We used to implement stop words algorithm using the torch.isin function like this: - # `all(torch.isin(stop_words_id, input_ids1)[0])` - # However, this algorithm is not correct as it will return True for presence of "unambiguously" in input_ids1 - # and True for presence of "unambiguously" in input_ids2. This is because the algorithm will check - # if the stop word tokens are present in the input_ids, but it does not check if the stop word tokens are - # present in a continuous/sequential order. - - # In "This is ambiguously, but is unrelated." sentence the "un" token comes from "unrelated" and the - # "ambiguously" token comes from "ambiguously". The algorithm will return True for presence of - # "unambiguously" in input_ids1 which is not correct. - - stop_words_criteria = StopWordsCriteria(tokenizer=Mock(), stop_words=["mock data"]) - # because we are mocking the tokenizer, we need to set the stop words manually - stop_words_criteria.stop_ids = stop_words_id - - # this is the correct algorithm to check if the stop word tokens are present in a continuous and sequential order - # For the input_ids1, the stop word tokens are present BUT not in a continuous order - present_and_continuous = stop_words_criteria(input_ids1, scores=None) - assert not present_and_continuous - - # For the input_ids2, the stop word tokens are both present and in a continuous order - present_and_continuous = stop_words_criteria(input_ids2, scores=None) - assert present_and_continuous - - @pytest.mark.unit - @patch("haystack.preview.components.generators.hugging_face_local.pipeline") - @patch("haystack.preview.components.generators.hugging_face_local.StopWordsCriteria") - @patch("haystack.preview.components.generators.hugging_face_local.StoppingCriteriaList") - def test_warm_up_set_stopping_criteria_list( - self, pipeline_mock, stop_words_criteria_mock, stopping_criteria_list_mock - ): - """ - Test that warm_up method sets the `stopping_criteria_list` attribute - if `stop_words` is provided - """ - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", stop_words=["coca", "cola"] - ) - - generator.warm_up() - - stop_words_criteria_mock.assert_called_once() - stopping_criteria_list_mock.assert_called_once() - - assert hasattr(generator, "stopping_criteria_list") - - @pytest.mark.unit - def test_run_stop_words_removal(self): - """ - Test that stop words are removed from the generated text - (does not test stopping text generation) - """ - generator = HuggingFaceLocalGenerator( - model_name_or_path="google/flan-t5-base", task="text2text-generation", stop_words=["world"] - ) - - # create the pipeline object (simulating the warm_up) - generator.pipeline = Mock(return_value=[{"generated_text": "Hello world"}]) - - results = generator.run(prompt="irrelevant") - - assert results == {"replies": ["Hello"]} diff --git a/test/preview/components/generators/test_hugging_face_tgi.py b/test/preview/components/generators/test_hugging_face_tgi.py deleted file mode 100644 index 5fcbb304b1..0000000000 --- a/test/preview/components/generators/test_hugging_face_tgi.py +++ /dev/null @@ -1,295 +0,0 @@ -from unittest.mock import patch, MagicMock, Mock - -import pytest -from huggingface_hub.inference._text_generation import TextGenerationStreamResponse, Token, StreamDetails, FinishReason -from huggingface_hub.utils import RepositoryNotFoundError - -from haystack.preview.components.generators import HuggingFaceTGIGenerator -from haystack.preview.dataclasses import StreamingChunk - - -@pytest.fixture -def mock_check_valid_model(): - with patch( - "haystack.preview.components.generators.hugging_face_tgi.check_valid_model", MagicMock(return_value=None) - ) as mock: - yield mock - - -@pytest.fixture -def mock_text_generation(): - with patch("huggingface_hub.InferenceClient.text_generation", autospec=True) as mock_text_generation: - mock_response = Mock() - mock_response.generated_text = "I'm fine, thanks." - details = Mock() - details.finish_reason = MagicMock(field1="value") - details.tokens = [1, 2, 3] - mock_response.details = details - mock_text_generation.return_value = mock_response - yield mock_text_generation - - -# used to test serialization of streaming_callback -def streaming_callback_handler(x): - return x - - -class TestHuggingFaceTGIGenerator: - @pytest.mark.unit - def test_initialize_with_valid_model_and_generation_parameters(self, mock_check_valid_model): - model = "HuggingFaceH4/zephyr-7b-alpha" - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIGenerator( - model=model, - url=None, - token=None, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - - assert generator.model == model - assert generator.generation_kwargs == {**generation_kwargs, **{"stop_sequences": ["stop"]}} - assert generator.tokenizer is None - assert generator.client is not None - assert generator.streaming_callback == streaming_callback - - @pytest.mark.unit - def test_to_dict(self, mock_check_valid_model): - # Initialize the HuggingFaceRemoteGenerator object with valid parameters - generator = HuggingFaceTGIGenerator( - token="token", generation_kwargs={"n": 5}, stop_words=["stop", "words"], streaming_callback=lambda x: x - ) - - # Call the to_dict method - result = generator.to_dict() - init_params = result["init_parameters"] - - # Assert that the init_params dictionary contains the expected keys and values - assert init_params["model"] == "mistralai/Mistral-7B-v0.1" - assert not init_params["token"] - assert init_params["generation_kwargs"] == {"n": 5, "stop_sequences": ["stop", "words"]} - - @pytest.mark.unit - def test_from_dict(self, mock_check_valid_model): - generator = HuggingFaceTGIGenerator( - model="mistralai/Mistral-7B-v0.1", - generation_kwargs={"n": 5}, - stop_words=["stop", "words"], - streaming_callback=streaming_callback_handler, - ) - # Call the to_dict method - result = generator.to_dict() - - # now deserialize, call from_dict - generator_2 = HuggingFaceTGIGenerator.from_dict(result) - assert generator_2.model == "mistralai/Mistral-7B-v0.1" - assert generator_2.generation_kwargs == {"n": 5, "stop_sequences": ["stop", "words"]} - assert generator_2.streaming_callback is streaming_callback_handler - - @pytest.mark.unit - def test_initialize_with_invalid_url(self, mock_check_valid_model): - with pytest.raises(ValueError): - HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1", url="invalid_url") - - @pytest.mark.unit - def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model): - # When custom TGI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id - mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id") - with pytest.raises(RepositoryNotFoundError): - HuggingFaceTGIGenerator(model="invalid_model_id", url="https://some_chat_model.com") - - @pytest.mark.unit - def test_generate_text_response_with_valid_prompt_and_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation - ): - model = "mistralai/Mistral-7B-v0.1" - - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - generator.warm_up() - - prompt = "Hello, how are you?" - response = generator.run(prompt) - - # check kwargs passed to text_generation - # note how n was not passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop"]} - - assert isinstance(response, dict) - assert "replies" in response - assert "metadata" in response - assert isinstance(response["replies"], list) - assert isinstance(response["metadata"], list) - assert len(response["replies"]) == 1 - assert len(response["metadata"]) == 1 - assert [isinstance(reply, str) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_multiple_text_responses_with_valid_prompt_and_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation - ): - model = "mistralai/Mistral-7B-v0.1" - generation_kwargs = {"n": 3} - stop_words = ["stop"] - streaming_callback = None - - generator = HuggingFaceTGIGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - generator.warm_up() - - prompt = "Hello, how are you?" - response = generator.run(prompt) - - # check kwargs passed to text_generation - # note how n was not passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop"]} - - assert isinstance(response, dict) - assert "replies" in response - assert "metadata" in response - assert isinstance(response["replies"], list) - assert [isinstance(reply, str) for reply in response["replies"]] - - assert isinstance(response["metadata"], list) - assert len(response["replies"]) == 3 - assert len(response["metadata"]) == 3 - assert [isinstance(reply, dict) for reply in response["metadata"]] - - @pytest.mark.unit - def test_initialize_with_invalid_model(self, mock_check_valid_model): - model = "invalid_model" - generation_kwargs = {"n": 1} - stop_words = ["stop"] - streaming_callback = None - - mock_check_valid_model.side_effect = ValueError("Invalid model path or url") - - with pytest.raises(ValueError): - HuggingFaceTGIGenerator( - model=model, - generation_kwargs=generation_kwargs, - stop_words=stop_words, - streaming_callback=streaming_callback, - ) - - @pytest.mark.unit - def test_generate_text_with_stop_words(self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation): - generator = HuggingFaceTGIGenerator() - generator.warm_up() - - # Generate text response with stop words - response = generator.run("How are you?", generation_kwargs={"stop_words": ["stop", "words"]}) - - # check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": ["stop", "words"]} - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, str) for reply in response["replies"]] - - # Assert that the response contains the metadata - assert "metadata" in response - assert isinstance(response["metadata"], list) - assert len(response["metadata"]) > 0 - assert [isinstance(reply, dict) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_text_with_custom_generation_parameters( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation - ): - generator = HuggingFaceTGIGenerator() - generator.warm_up() - - generation_kwargs = {"temperature": 0.8, "max_new_tokens": 100} - response = generator.run("How are you?", generation_kwargs=generation_kwargs) - - # check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "max_new_tokens": 100, "stop_sequences": [], "temperature": 0.8} - - # Assert that the response contains the generated replies and the right response - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, str) for reply in response["replies"]] - assert response["replies"][0] == "I'm fine, thanks." - - # Assert that the response contains the metadata - assert "metadata" in response - assert isinstance(response["metadata"], list) - assert len(response["metadata"]) > 0 - assert [isinstance(reply, str) for reply in response["replies"]] - - @pytest.mark.unit - def test_generate_text_with_streaming_callback( - self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation - ): - streaming_call_count = 0 - - # Define the streaming callback function - def streaming_callback_fn(chunk: StreamingChunk): - nonlocal streaming_call_count - streaming_call_count += 1 - assert isinstance(chunk, StreamingChunk) - - # Create an instance of HuggingFaceRemoteGenerator - generator = HuggingFaceTGIGenerator(streaming_callback=streaming_callback_fn) - generator.warm_up() - - # Create a fake streamed response - # Don't remove self - def mock_iter(self): - yield TextGenerationStreamResponse( - generated_text=None, token=Token(id=1, text="I'm fine, thanks.", logprob=0.0, special=False) - ) - yield TextGenerationStreamResponse( - generated_text=None, - token=Token(id=1, text="Ok bye", logprob=0.0, special=False), - details=StreamDetails(finish_reason=FinishReason.Length, generated_tokens=5), - ) - - mock_response = Mock(**{"__iter__": mock_iter}) - mock_text_generation.return_value = mock_response - - # Generate text response with streaming callback - response = generator.run("prompt") - - # check kwargs passed to text_generation - _, kwargs = mock_text_generation.call_args - assert kwargs == {"details": True, "stop_sequences": [], "stream": True} - - # Assert that the streaming callback was called twice - assert streaming_call_count == 2 - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, str) for reply in response["replies"]] - - # Assert that the response contains the metadata - assert "metadata" in response - assert isinstance(response["metadata"], list) - assert len(response["metadata"]) > 0 - assert [isinstance(reply, dict) for reply in response["replies"]] diff --git a/test/preview/components/generators/test_openai.py b/test/preview/components/generators/test_openai.py deleted file mode 100644 index 94a862654d..0000000000 --- a/test/preview/components/generators/test_openai.py +++ /dev/null @@ -1,343 +0,0 @@ -import os -from typing import List -from unittest.mock import patch, Mock - -import openai -import pytest - -from haystack.preview.components.generators import GPTGenerator -from haystack.preview.components.generators.utils import default_streaming_callback -from haystack.preview.dataclasses import StreamingChunk, ChatMessage - - -@pytest.fixture -def mock_chat_completion(): - """ - Mock the OpenAI API completion response and reuse it for tests - """ - with patch("openai.ChatCompletion.create", autospec=True) as mock_chat_completion_create: - # mimic the response from the OpenAI API - mock_choice = Mock() - mock_choice.index = 0 - mock_choice.finish_reason = "stop" - - mock_message = Mock() - mock_message.content = "I'm fine, thanks. How are you?" - mock_message.role = "user" - - mock_choice.message = mock_message - - mock_response = Mock() - mock_response.model = "gpt-3.5-turbo" - mock_response.usage = Mock() - mock_response.usage.items.return_value = [ - ("prompt_tokens", 57), - ("completion_tokens", 40), - ("total_tokens", 97), - ] - mock_response.choices = [mock_choice] - mock_chat_completion_create.return_value = mock_response - yield mock_chat_completion_create - - -def streaming_chunk(content: str): - """ - Mock chunks of streaming responses from the OpenAI API - """ - # mimic the chunk response from the OpenAI API - mock_choice = Mock() - mock_choice.index = 0 - mock_choice.delta.content = content - mock_choice.finish_reason = "stop" - - mock_response = Mock() - mock_response.choices = [mock_choice] - mock_response.model = "gpt-3.5-turbo" - mock_response.usage = Mock() - mock_response.usage.items.return_value = [("prompt_tokens", 57), ("completion_tokens", 40), ("total_tokens", 97)] - return mock_response - - -class TestGPTGenerator: - @pytest.mark.unit - def test_init_default(self): - component = GPTGenerator(api_key="test-api-key") - assert openai.api_key == "test-api-key" - assert component.model_name == "gpt-3.5-turbo" - assert component.streaming_callback is None - assert component.api_base_url == "https://api.openai.com/v1" - assert openai.api_base == "https://api.openai.com/v1" - assert not component.generation_kwargs - - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - with pytest.raises(ValueError, match="GPTGenerator expects an OpenAI API key"): - GPTGenerator() - - @pytest.mark.unit - def test_init_with_parameters(self): - component = GPTGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=default_streaming_callback, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - assert openai.api_key == "test-api-key" - assert component.model_name == "gpt-4" - assert component.streaming_callback is default_streaming_callback - assert component.api_base_url == "test-base-url" - assert openai.api_base == "test-base-url" - assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"} - - @pytest.mark.unit - def test_to_dict_default(self): - component = GPTGenerator(api_key="test-api-key") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.openai.GPTGenerator", - "init_parameters": { - "model_name": "gpt-3.5-turbo", - "streaming_callback": None, - "system_prompt": None, - "api_base_url": "https://api.openai.com/v1", - "generation_kwargs": {}, - }, - } - - @pytest.mark.unit - def test_to_dict_with_parameters(self): - component = GPTGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=default_streaming_callback, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.openai.GPTGenerator", - "init_parameters": { - "model_name": "gpt-4", - "system_prompt": None, - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - - @pytest.mark.unit - def test_to_dict_with_lambda_streaming_callback(self): - component = GPTGenerator( - api_key="test-api-key", - model_name="gpt-4", - streaming_callback=lambda x: x, - api_base_url="test-base-url", - generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"}, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.generators.openai.GPTGenerator", - "init_parameters": { - "model_name": "gpt-4", - "system_prompt": None, - "api_base_url": "test-base-url", - "streaming_callback": "test_openai.", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - - @pytest.mark.unit - def test_from_dict(self, monkeypatch): - monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key") - data = { - "type": "haystack.preview.components.generators.openai.GPTGenerator", - "init_parameters": { - "model_name": "gpt-4", - "system_prompt": None, - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - component = GPTGenerator.from_dict(data) - assert component.model_name == "gpt-4" - assert component.streaming_callback is default_streaming_callback - assert component.api_base_url == "test-base-url" - assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"} - - @pytest.mark.unit - def test_from_dict_fail_wo_env_var(self, monkeypatch): - openai.api_key = None - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - data = { - "type": "haystack.preview.components.generators.openai.GPTGenerator", - "init_parameters": { - "model_name": "gpt-4", - "api_base_url": "test-base-url", - "streaming_callback": "haystack.preview.components.generators.utils.default_streaming_callback", - "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"}, - }, - } - with pytest.raises(ValueError, match="GPTGenerator expects an OpenAI API key"): - GPTGenerator.from_dict(data) - - @pytest.mark.unit - def test_run(self, mock_chat_completion): - component = GPTGenerator(api_key="test-api-key") - response = component.run("What's Natural Language Processing?") - - # check that the component returns the correct ChatMessage response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, str) for reply in response["replies"]] - - @pytest.mark.unit - def test_run_with_params(self, mock_chat_completion): - component = GPTGenerator(api_key="test-api-key", generation_kwargs={"max_tokens": 10, "temperature": 0.5}) - response = component.run("What's Natural Language Processing?") - - # check that the component calls the OpenAI API with the correct parameters - _, kwargs = mock_chat_completion.call_args - assert kwargs["max_tokens"] == 10 - assert kwargs["temperature"] == 0.5 - - # check that the component returns the correct response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, str) for reply in response["replies"]] - - @pytest.mark.unit - def test_run_streaming(self, mock_chat_completion): - streaming_call_count = 0 - - # Define the streaming callback function and assert that it is called with StreamingChunk objects - def streaming_callback_fn(chunk: StreamingChunk): - nonlocal streaming_call_count - streaming_call_count += 1 - assert isinstance(chunk, StreamingChunk) - - generator = GPTGenerator(api_key="test-api-key", streaming_callback=streaming_callback_fn) - - # Create a fake streamed response - # self needed here, don't remove - def mock_iter(self): - yield streaming_chunk("Hello") - yield streaming_chunk("How are you?") - - mock_response = Mock(**{"__iter__": mock_iter}) - mock_chat_completion.return_value = mock_response - - response = generator.run("Hello there") - - # Assert that the streaming callback was called twice - assert streaming_call_count == 2 - - # Assert that the response contains the generated replies - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) > 0 - assert [isinstance(reply, str) for reply in response["replies"]] - - @pytest.mark.unit - def test_check_abnormal_completions(self, caplog): - component = GPTGenerator(api_key="test-api-key") - - # underlying implementation uses ChatMessage objects so we have to use them here - messages: List[ChatMessage] = [] - for i, _ in enumerate(range(4)): - message = ChatMessage.from_assistant("Hello") - metadata = {"finish_reason": "content_filter" if i % 2 == 0 else "length", "index": i} - message.metadata.update(metadata) - messages.append(message) - - for m in messages: - component._check_finish_reason(m) - - # check truncation warning - message_template = ( - "The completion for index {index} has been truncated before reaching a natural stopping point. " - "Increase the max_tokens parameter to allow for longer completions." - ) - - for index in [1, 3]: - assert caplog.records[index].message == message_template.format(index=index) - - # check content filter warning - message_template = "The completion for index {index} has been truncated due to the content filter." - for index in [0, 2]: - assert caplog.records[index].message == message_template.format(index=index) - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run(self): - component = GPTGenerator(api_key=os.environ.get("OPENAI_API_KEY")) - results = component.run("What's the capital of France?") - assert len(results["replies"]) == 1 - assert len(results["metadata"]) == 1 - response: str = results["replies"][0] - assert "Paris" in response - - metadata = results["metadata"][0] - assert "gpt-3.5" in metadata["model"] - assert metadata["finish_reason"] == "stop" - - assert "usage" in metadata - assert "prompt_tokens" in metadata["usage"] and metadata["usage"]["prompt_tokens"] > 0 - assert "completion_tokens" in metadata["usage"] and metadata["usage"]["completion_tokens"] > 0 - assert "total_tokens" in metadata["usage"] and metadata["usage"]["total_tokens"] > 0 - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_wrong_model(self): - component = GPTGenerator(model_name="something-obviously-wrong", api_key=os.environ.get("OPENAI_API_KEY")) - with pytest.raises(openai.InvalidRequestError, match="The model `something-obviously-wrong` does not exist"): - component.run("Whatever") - - @pytest.mark.skipif( - not os.environ.get("OPENAI_API_KEY", None), - reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_streaming(self): - class Callback: - def __init__(self): - self.responses = "" - self.counter = 0 - - def __call__(self, chunk: StreamingChunk) -> None: - self.counter += 1 - self.responses += chunk.content if chunk.content else "" - - callback = Callback() - component = GPTGenerator(os.environ.get("OPENAI_API_KEY"), streaming_callback=callback) - results = component.run("What's the capital of France?") - - assert len(results["replies"]) == 1 - assert len(results["metadata"]) == 1 - response: str = results["replies"][0] - assert "Paris" in response - - metadata = results["metadata"][0] - - assert "gpt-3.5" in metadata["model"] - assert metadata["finish_reason"] == "stop" - - # unfortunately, the usage is not available for streaming calls - # we keep the key in the metadata for compatibility - assert "usage" in metadata and len(metadata["usage"]) == 0 - - assert callback.counter > 1 - assert "Paris" in callback.responses diff --git a/test/preview/components/generators/test_utils.py b/test/preview/components/generators/test_utils.py deleted file mode 100644 index 9339502990..0000000000 --- a/test/preview/components/generators/test_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from haystack.preview.components.generators.utils import default_streaming_callback -from haystack.preview.components.generators.utils import serialize_callback_handler, deserialize_callback_handler - - -# streaming callback needs to be on module level -def streaming_callback(chunk): - pass - - -@pytest.mark.unit -def test_callback_handler_serialization(): - result = serialize_callback_handler(streaming_callback) - assert result == "test_utils.streaming_callback" - - -@pytest.mark.unit -def test_callback_handler_serialization_non_local(): - result = serialize_callback_handler(default_streaming_callback) - assert result == "haystack.preview.components.generators.utils.default_streaming_callback" - - -@pytest.mark.unit -def test_callback_handler_deserialization(): - result = serialize_callback_handler(streaming_callback) - fn = deserialize_callback_handler(result) - - assert fn is streaming_callback - - -@pytest.mark.unit -def test_callback_handler_deserialization_non_local(): - result = serialize_callback_handler(default_streaming_callback) - fn = deserialize_callback_handler(result) - - assert fn is default_streaming_callback diff --git a/test/preview/components/preprocessors/__init__.py b/test/preview/components/preprocessors/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/preprocessors/test_document_cleaner.py b/test/preview/components/preprocessors/test_document_cleaner.py deleted file mode 100644 index 71f412f3f1..0000000000 --- a/test/preview/components/preprocessors/test_document_cleaner.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging - -import pytest - -from haystack.preview import Document -from haystack.preview.components.preprocessors import DocumentCleaner - - -class TestDocumentCleaner: - @pytest.mark.unit - def test_init(self): - cleaner = DocumentCleaner() - assert cleaner.remove_empty_lines is True - assert cleaner.remove_extra_whitespaces is True - assert cleaner.remove_repeated_substrings is False - assert cleaner.remove_substrings is None - assert cleaner.remove_regex is None - - @pytest.mark.unit - def test_non_text_document(self, caplog): - with caplog.at_level(logging.WARNING): - cleaner = DocumentCleaner() - cleaner.run(documents=[Document()]) - assert "DocumentCleaner only cleans text documents but document.content for document ID" in caplog.text - - @pytest.mark.unit - def test_single_document(self): - with pytest.raises(TypeError, match="DocumentCleaner expects a List of Documents as input."): - cleaner = DocumentCleaner() - cleaner.run(documents=Document()) - - @pytest.mark.unit - def test_empty_list(self): - cleaner = DocumentCleaner() - result = cleaner.run(documents=[]) - assert result == {"documents": []} - - @pytest.mark.unit - def test_remove_empty_lines(self): - cleaner = DocumentCleaner(remove_extra_whitespaces=False) - result = cleaner.run( - documents=[ - Document( - content="This is a text with some words. " - "" - "There is a second sentence. " - "" - "And there is a third sentence." - ) - ] - ) - assert len(result["documents"]) == 1 - assert ( - result["documents"][0].content - == "This is a text with some words. There is a second sentence. And there is a third sentence." - ) - - @pytest.mark.unit - def test_remove_whitespaces(self): - cleaner = DocumentCleaner(remove_empty_lines=False) - result = cleaner.run( - documents=[ - Document( - content=" This is a text with some words. " - "" - "There is a second sentence. " - "" - "And there is a third sentence. " - ) - ] - ) - assert len(result["documents"]) == 1 - assert result["documents"][0].content == ( - "This is a text with some words. " "" "There is a second sentence. " "" "And there is a third sentence." - ) - - @pytest.mark.unit - def test_remove_substrings(self): - cleaner = DocumentCleaner(remove_substrings=["This", "A", "words", "🪲"]) - result = cleaner.run(documents=[Document(content="This is a text with some words.🪲")]) - assert len(result["documents"]) == 1 - assert result["documents"][0].content == " is a text with some ." - - @pytest.mark.unit - def test_remove_regex(self): - cleaner = DocumentCleaner(remove_regex=r"\s\s+") - result = cleaner.run(documents=[Document(content="This is a text with some words.")]) - assert len(result["documents"]) == 1 - assert result["documents"][0].content == "This is a text with some words." - - @pytest.mark.unit - def test_remove_repeated_substrings(self): - cleaner = DocumentCleaner( - remove_empty_lines=False, remove_extra_whitespaces=False, remove_repeated_substrings=True - ) - - text = """First Page This is a header. - Page of - 2 - 4 - Lorem ipsum dolor sit amet - This is a footer number 1 - This is footer number 2 This is a header. - Page of - 3 - 4 - Sid ut perspiciatis unde - This is a footer number 1 - This is footer number 2 This is a header. - Page of - 4 - 4 - Sed do eiusmod tempor. - This is a footer number 1 - This is footer number 2""" - - expected_text = """First Page 2 - 4 - Lorem ipsum dolor sit amet 3 - 4 - Sid ut perspiciatis unde 4 - 4 - Sed do eiusmod tempor.""" - result = cleaner.run(documents=[Document(content=text)]) - assert result["documents"][0].content == expected_text - - @pytest.mark.unit - def test_copy_metadata(self): - cleaner = DocumentCleaner() - documents = [ - Document(content="Text. ", meta={"name": "doc 0"}), - Document(content="Text. ", meta={"name": "doc 1"}), - ] - result = cleaner.run(documents=documents) - assert len(result["documents"]) == 2 - assert result["documents"][0].id != result["documents"][1].id - for doc, cleaned_doc in zip(documents, result["documents"]): - assert doc.meta == cleaned_doc.meta - assert cleaned_doc.content == "Text." diff --git a/test/preview/components/preprocessors/test_document_splitter.py b/test/preview/components/preprocessors/test_document_splitter.py deleted file mode 100644 index 4e28d1b135..0000000000 --- a/test/preview/components/preprocessors/test_document_splitter.py +++ /dev/null @@ -1,142 +0,0 @@ -import pytest - -from haystack.preview import Document -from haystack.preview.components.preprocessors import DocumentSplitter - - -class TestDocumentSplitter: - @pytest.mark.unit - def test_non_text_document(self): - with pytest.raises( - ValueError, match="DocumentSplitter only works with text documents but document.content for document ID" - ): - splitter = DocumentSplitter() - splitter.run(documents=[Document()]) - - @pytest.mark.unit - def test_single_doc(self): - with pytest.raises(TypeError, match="DocumentSplitter expects a List of Documents as input."): - splitter = DocumentSplitter() - splitter.run(documents=Document()) - - @pytest.mark.unit - def test_empty_list(self): - splitter = DocumentSplitter() - res = splitter.run(documents=[]) - assert res == {"documents": []} - - @pytest.mark.unit - def test_unsupported_split_by(self): - with pytest.raises(ValueError, match="split_by must be one of 'word', 'sentence' or 'passage'."): - DocumentSplitter(split_by="unsupported") - - @pytest.mark.unit - def test_unsupported_split_length(self): - with pytest.raises(ValueError, match="split_length must be greater than 0."): - DocumentSplitter(split_length=0) - - @pytest.mark.unit - def test_unsupported_split_overlap(self): - with pytest.raises(ValueError, match="split_overlap must be greater than or equal to 0."): - DocumentSplitter(split_overlap=-1) - - @pytest.mark.unit - def test_split_by_word(self): - splitter = DocumentSplitter(split_by="word", split_length=10) - result = splitter.run( - documents=[ - Document( - content="This is a text with some words. There is a second sentence. And there is a third sentence." - ) - ] - ) - assert len(result["documents"]) == 2 - assert result["documents"][0].content == "This is a text with some words. There is a " - assert result["documents"][1].content == "second sentence. And there is a third sentence." - - @pytest.mark.unit - def test_split_by_word_multiple_input_docs(self): - splitter = DocumentSplitter(split_by="word", split_length=10) - result = splitter.run( - documents=[ - Document( - content="This is a text with some words. There is a second sentence. And there is a third sentence." - ), - Document( - content="This is a different text with some words. There is a second sentence. And there is a third sentence. And there is a fourth sentence." - ), - ] - ) - assert len(result["documents"]) == 5 - assert result["documents"][0].content == "This is a text with some words. There is a " - assert result["documents"][1].content == "second sentence. And there is a third sentence." - assert result["documents"][2].content == "This is a different text with some words. There is " - assert result["documents"][3].content == "a second sentence. And there is a third sentence. And " - assert result["documents"][4].content == "there is a fourth sentence." - - @pytest.mark.unit - def test_split_by_sentence(self): - splitter = DocumentSplitter(split_by="sentence", split_length=1) - result = splitter.run( - documents=[ - Document( - content="This is a text with some words. There is a second sentence. And there is a third sentence." - ) - ] - ) - assert len(result["documents"]) == 3 - assert result["documents"][0].content == "This is a text with some words." - assert result["documents"][1].content == " There is a second sentence." - assert result["documents"][2].content == " And there is a third sentence." - - @pytest.mark.unit - def test_split_by_passage(self): - splitter = DocumentSplitter(split_by="passage", split_length=1) - result = splitter.run( - documents=[ - Document( - content="This is a text with some words. There is a second sentence.\n\nAnd there is a third sentence.\n\n And another passage." - ) - ] - ) - assert len(result["documents"]) == 3 - assert result["documents"][0].content == "This is a text with some words. There is a second sentence.\n\n" - assert result["documents"][1].content == "And there is a third sentence.\n\n" - assert result["documents"][2].content == " And another passage." - - @pytest.mark.unit - def test_split_by_word_with_overlap(self): - splitter = DocumentSplitter(split_by="word", split_length=10, split_overlap=2) - result = splitter.run( - documents=[ - Document( - content="This is a text with some words. There is a second sentence. And there is a third sentence." - ) - ] - ) - assert len(result["documents"]) == 2 - assert result["documents"][0].content == "This is a text with some words. There is a " - assert result["documents"][1].content == "is a second sentence. And there is a third sentence." - - @pytest.mark.unit - def test_source_id_stored_in_metadata(self): - splitter = DocumentSplitter(split_by="word", split_length=10) - doc1 = Document(content="This is a text with some words.") - doc2 = Document(content="This is a different text with some words.") - result = splitter.run(documents=[doc1, doc2]) - assert result["documents"][0].meta["source_id"] == doc1.id - assert result["documents"][1].meta["source_id"] == doc2.id - - @pytest.mark.unit - def test_copy_metadata(self): - splitter = DocumentSplitter(split_by="word", split_length=10) - documents = [ - Document(content="Text.", meta={"name": "doc 0"}), - Document(content="Text.", meta={"name": "doc 1"}), - ] - result = splitter.run(documents=documents) - assert len(result["documents"]) == 2 - assert result["documents"][0].id != result["documents"][1].id - for doc, split_doc in zip(documents, result["documents"]): - assert doc.meta.items() <= split_doc.meta.items() - assert split_doc.content == "Text." diff --git a/test/preview/components/rankers/test_metafield.py b/test/preview/components/rankers/test_metafield.py deleted file mode 100644 index b6e762c6f8..0000000000 --- a/test/preview/components/rankers/test_metafield.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest - -from haystack.preview import Document, ComponentError -from haystack.preview.components.rankers.meta_field import MetaFieldRanker - - -class TestMetaFieldRanker: - @pytest.mark.unit - def test_to_dict(self): - component = MetaFieldRanker(metadata_field="rating") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.rankers.meta_field.MetaFieldRanker", - "init_parameters": { - "metadata_field": "rating", - "weight": 1.0, - "top_k": None, - "ranking_mode": "reciprocal_rank_fusion", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = MetaFieldRanker(metadata_field="rating", weight=0.5, top_k=5, ranking_mode="linear_score") - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.rankers.meta_field.MetaFieldRanker", - "init_parameters": {"metadata_field": "rating", "weight": 0.5, "top_k": 5, "ranking_mode": "linear_score"}, - } - - @pytest.mark.integration - @pytest.mark.parametrize("metafield_values, expected_first_value", [([1.3, 0.7, 2.1], 2.1), ([1, 5, 8], 8)]) - def test_run(self, metafield_values, expected_first_value): - """ - Test if the component ranks documents correctly. - """ - ranker = MetaFieldRanker(metadata_field="rating") - docs_before = [Document(content="abc", meta={"rating": value}) for value in metafield_values] - - output = ranker.run(documents=docs_before) - docs_after = output["documents"] - - assert len(docs_after) == 3 - assert docs_after[0].meta["rating"] == expected_first_value - - sorted_scores = sorted([doc.meta["rating"] for doc in docs_after], reverse=True) - assert [doc.meta["rating"] for doc in docs_after] == sorted_scores - - @pytest.mark.integration - def test_returns_empty_list_if_no_documents_are_provided(self): - ranker = MetaFieldRanker(metadata_field="rating") - output = ranker.run(documents=[]) - docs_after = output["documents"] - assert docs_after == [] - - @pytest.mark.integration - def test_raises_component_error_if_metadata_not_found(self): - ranker = MetaFieldRanker(metadata_field="rating") - docs_before = [Document(content="abc", meta={"wrong_field": 1.3})] - with pytest.raises(ComponentError): - ranker.run(documents=docs_before) - - @pytest.mark.integration - def test_raises_component_error_if_wrong_ranking_mode(self): - with pytest.raises(ValueError): - MetaFieldRanker(metadata_field="rating", ranking_mode="wrong_mode") - - @pytest.mark.integration - @pytest.mark.parametrize("score", [-1, 2, 1.3, 2.1]) - def test_raises_component_error_if_wrong_weight(self, score): - with pytest.raises(ValueError): - MetaFieldRanker(metadata_field="rating", weight=score) - - @pytest.mark.integration - def test_linear_score(self): - ranker = MetaFieldRanker(metadata_field="rating", ranking_mode="linear_score", weight=0.5) - docs_before = [ - Document(content="abc", meta={"rating": 1.3}, score=0.3), - Document(content="abc", meta={"rating": 0.7}, score=0.4), - Document(content="abc", meta={"rating": 2.1}, score=0.6), - ] - output = ranker.run(documents=docs_before) - docs_after = output["documents"] - assert docs_after[0].score == 0.8 - - @pytest.mark.integration - def test_reciprocal_rank_fusion(self): - ranker = MetaFieldRanker(metadata_field="rating", ranking_mode="reciprocal_rank_fusion", weight=0.5) - docs_before = [ - Document(content="abc", meta={"rating": 1.3}, score=0.3), - Document(content="abc", meta={"rating": 0.7}, score=0.4), - Document(content="abc", meta={"rating": 2.1}, score=0.6), - ] - output = ranker.run(documents=docs_before) - docs_after = output["documents"] - assert docs_after[0].score == 0.01626123744050767 - - @pytest.mark.integration - @pytest.mark.parametrize("score", [-1, 2, 1.3, 2.1]) - def test_linear_score_raises_warning_if_doc_wrong_score(self, score): - ranker = MetaFieldRanker(metadata_field="rating", ranking_mode="linear_score", weight=0.5) - docs_before = [ - Document(id=1, content="abc", meta={"rating": 1.3}, score=score), - Document(id=2, content="abc", meta={"rating": 0.7}, score=0.4), - Document(id=3, content="abc", meta={"rating": 2.1}, score=0.6), - ] - with pytest.warns( - UserWarning, match=rf"The score {score} for Document 1 is outside the \[0,1\] range; defaulting to 0" - ): - ranker.run(documents=docs_before) - - @pytest.mark.integration - def test_linear_score_raises_raises_warning_if_doc_without_score(self): - ranker = MetaFieldRanker(metadata_field="rating", ranking_mode="linear_score", weight=0.5) - docs_before = [ - Document(content="abc", meta={"rating": 1.3}), - Document(content="abc", meta={"rating": 0.7}), - Document(content="abc", meta={"rating": 2.1}), - ] - - with pytest.warns(UserWarning, match="The score wasn't provided; defaulting to 0."): - ranker.run(documents=docs_before) diff --git a/test/preview/components/rankers/test_transformers_similarity.py b/test/preview/components/rankers/test_transformers_similarity.py deleted file mode 100644 index 95c1d1aea7..0000000000 --- a/test/preview/components/rankers/test_transformers_similarity.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest - -from haystack.preview import Document, ComponentError -from haystack.preview.components.rankers.transformers_similarity import TransformersSimilarityRanker - - -class TestSimilarityRanker: - @pytest.mark.unit - def test_to_dict(self): - component = TransformersSimilarityRanker() - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.rankers.transformers_similarity.TransformersSimilarityRanker", - "init_parameters": { - "device": "cpu", - "top_k": 10, - "token": None, - "model_name_or_path": "cross-encoder/ms-marco-MiniLM-L-6-v2", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - component = TransformersSimilarityRanker( - model_name_or_path="my_model", device="cuda", token="my_token", top_k=5 - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.rankers.transformers_similarity.TransformersSimilarityRanker", - "init_parameters": { - "device": "cuda", - "model_name_or_path": "my_model", - "token": None, # we don't serialize valid tokens, - "top_k": 5, - }, - } - - @pytest.mark.integration - @pytest.mark.parametrize( - "query,docs_before_texts,expected_first_text", - [ - ("City in Bosnia and Herzegovina", ["Berlin", "Belgrade", "Sarajevo"], "Sarajevo"), - ("Machine learning", ["Python", "Bakery in Paris", "Tesla Giga Berlin"], "Python"), - ("Cubist movement", ["Nirvana", "Pablo Picasso", "Coffee"], "Pablo Picasso"), - ], - ) - def test_run(self, query, docs_before_texts, expected_first_text): - """ - Test if the component ranks documents correctly. - """ - ranker = TransformersSimilarityRanker(model_name_or_path="cross-encoder/ms-marco-MiniLM-L-6-v2") - ranker.warm_up() - docs_before = [Document(content=text) for text in docs_before_texts] - output = ranker.run(query=query, documents=docs_before) - docs_after = output["documents"] - - assert len(docs_after) == 3 - assert docs_after[0].content == expected_first_text - - sorted_scores = sorted([doc.score for doc in docs_after], reverse=True) - assert [doc.score for doc in docs_after] == sorted_scores - - # Returns an empty list if no documents are provided - @pytest.mark.integration - def test_returns_empty_list_if_no_documents_are_provided(self): - sampler = TransformersSimilarityRanker() - sampler.warm_up() - output = sampler.run(query="City in Germany", documents=[]) - assert not output["documents"] - - # Raises ComponentError if model is not warmed up - @pytest.mark.integration - def test_raises_component_error_if_model_not_warmed_up(self): - sampler = TransformersSimilarityRanker() - - with pytest.raises(ComponentError): - sampler.run(query="query", documents=[Document(content="document")]) - - @pytest.mark.integration - @pytest.mark.parametrize( - "query,docs_before_texts,expected_first_text", - [ - ("City in Bosnia and Herzegovina", ["Berlin", "Belgrade", "Sarajevo"], "Sarajevo"), - ("Machine learning", ["Python", "Bakery in Paris", "Tesla Giga Berlin"], "Python"), - ("Cubist movement", ["Nirvana", "Pablo Picasso", "Coffee"], "Pablo Picasso"), - ], - ) - def test_run_top_k(self, query, docs_before_texts, expected_first_text): - """ - Test if the component ranks documents correctly with a custom top_k. - """ - ranker = TransformersSimilarityRanker(model_name_or_path="cross-encoder/ms-marco-MiniLM-L-6-v2", top_k=2) - ranker.warm_up() - docs_before = [Document(content=text) for text in docs_before_texts] - output = ranker.run(query=query, documents=docs_before) - docs_after = output["documents"] - - assert len(docs_after) == 2 - assert docs_after[0].content == expected_first_text - - sorted_scores = sorted([doc.score for doc in docs_after], reverse=True) - assert [doc.score for doc in docs_after] == sorted_scores diff --git a/test/preview/components/readers/test_extractive.py b/test/preview/components/readers/test_extractive.py deleted file mode 100644 index 438922ae8d..0000000000 --- a/test/preview/components/readers/test_extractive.py +++ /dev/null @@ -1,415 +0,0 @@ -from math import ceil, exp -from typing import List -from unittest.mock import patch, Mock -import pytest - -import torch -from transformers import pipeline - -from haystack.preview.components.readers import ExtractiveReader -from haystack.preview import Document - - -@pytest.fixture -def mock_tokenizer(): - def mock_tokenize( - texts: List[str], - text_pairs: List[str], - padding: bool, - truncation: bool, - max_length: int, - return_tensors: str, - return_overflowing_tokens: bool, - stride: int, - ): - assert padding - assert truncation - assert return_tensors == "pt" - assert return_overflowing_tokens - - tokens = Mock() - - num_splits = [ceil(len(text + pair) / max_length) for text, pair in zip(texts, text_pairs)] - tokens.overflow_to_sample_mapping = [i for i, num in enumerate(num_splits) for _ in range(num)] - num_samples = sum(num_splits) - tokens.encodings = [Mock() for _ in range(num_samples)] - sequence_ids = [0] * 16 + [1] * 16 + [None] * (max_length - 32) - for encoding in tokens.encodings: - encoding.sequence_ids = sequence_ids - encoding.token_to_chars = lambda i: (i - 16, i - 15) - tokens.input_ids = torch.zeros(num_samples, max_length, dtype=torch.int) - attention_mask = torch.zeros(num_samples, max_length, dtype=torch.int) - attention_mask[:32] = 1 - tokens.attention_mask = attention_mask - return tokens - - with patch("haystack.preview.components.readers.extractive.AutoTokenizer.from_pretrained") as tokenizer: - tokenizer.return_value = mock_tokenize - yield tokenizer - - -@pytest.fixture() -def mock_reader(mock_tokenizer): - class MockModel(torch.nn.Module): - def to(self, device): - assert device == "cpu:0" - self.device_set = True - return self - - def forward(self, input_ids, attention_mask, *args, **kwargs): - assert input_ids.device == torch.device("cpu") - assert attention_mask.device == torch.device("cpu") - assert self.device_set - start = torch.zeros(input_ids.shape[:2]) - end = torch.zeros(input_ids.shape[:2]) - start[:, 27] = 1 - end[:, 31] = 1 - end[:, 32] = 1 - prediction = Mock() - prediction.start_logits = start - prediction.end_logits = end - return prediction - - with patch("haystack.preview.components.readers.extractive.AutoModelForQuestionAnswering.from_pretrained") as model: - model.return_value = MockModel() - reader = ExtractiveReader(model_name_or_path="mock-model", device="cpu:0") - reader.warm_up() - return reader - - -example_queries = ["Who is the chancellor of Germany?", "Who is the head of the department?"] -example_documents = [ - [ - Document(content="Angela Merkel was the chancellor of Germany."), - Document(content="Olaf Scholz is the chancellor of Germany"), - Document(content="Jerry is the head of the department."), - ] -] * 2 - - -@pytest.mark.unit -def test_to_dict(): - component = ExtractiveReader("my-model", token="secret-token", model_kwargs={"torch_dtype": "auto"}) - data = component.to_dict() - - assert data == { - "type": "haystack.preview.components.readers.extractive.ExtractiveReader", - "init_parameters": { - "model_name_or_path": "my-model", - "device": None, - "token": None, # don't serialize valid tokens - "top_k": 20, - "confidence_threshold": None, - "max_seq_length": 384, - "stride": 128, - "max_batch_size": None, - "answers_per_seq": None, - "no_answer": True, - "calibration_factor": 0.1, - "model_kwargs": {"torch_dtype": "auto"}, - }, - } - - -@pytest.mark.unit -def test_to_dict_empty_model_kwargs(): - component = ExtractiveReader("my-model", token="secret-token") - data = component.to_dict() - - assert data == { - "type": "haystack.preview.components.readers.extractive.ExtractiveReader", - "init_parameters": { - "model_name_or_path": "my-model", - "device": None, - "token": None, # don't serialize valid tokens - "top_k": 20, - "confidence_threshold": None, - "max_seq_length": 384, - "stride": 128, - "max_batch_size": None, - "answers_per_seq": None, - "no_answer": True, - "calibration_factor": 0.1, - "model_kwargs": {}, - }, - } - - -@pytest.mark.unit -def test_output(mock_reader: ExtractiveReader): - answers = mock_reader.run(example_queries[0], example_documents[0], top_k=3)[ - "answers" - ] # [0] Uncomment and remove first two indices when batching support is reintroduced - doc_ids = set() - no_answer_prob = 1 - for doc, answer in zip(example_documents[0], answers[:3]): - assert answer.start == 11 - assert answer.end == 16 - assert doc.content is not None - assert answer.data == doc.content[11:16] - assert answer.probability == pytest.approx(1 / (1 + exp(-2 * mock_reader.calibration_factor))) - no_answer_prob *= 1 - answer.probability - doc_ids.add(doc.id) - assert len(doc_ids) == 3 - assert answers[-1].probability == pytest.approx(no_answer_prob) - - -@pytest.mark.unit -def test_flatten_documents(mock_reader: ExtractiveReader): - queries, docs, query_ids = mock_reader._flatten_documents(example_queries, example_documents) - i = 0 - for j, query in enumerate(example_queries): - for doc in example_documents[j]: - assert queries[i] == query - assert docs[i] == doc - assert query_ids[i] == j - i += 1 - assert len(docs) == len(queries) == len(query_ids) == i - - -@pytest.mark.unit -def test_preprocess(mock_reader: ExtractiveReader): - _, _, seq_ids, _, query_ids, doc_ids = mock_reader._preprocess( - example_queries * 3, example_documents[0], 384, [1, 1, 1], 0 - ) - expected_seq_ids = torch.full((3, 384), -1, dtype=torch.int) - expected_seq_ids[:, :16] = 0 - expected_seq_ids[:, 16:32] = 1 - assert torch.equal(seq_ids, expected_seq_ids) - assert query_ids == [1, 1, 1] - assert doc_ids == [0, 1, 2] - - -def test_preprocess_splitting(mock_reader: ExtractiveReader): - _, _, seq_ids, _, query_ids, doc_ids = mock_reader._preprocess( - example_queries * 4, example_documents[0] + [Document(content="a" * 64)], 96, [1, 1, 1, 1], 0 - ) - assert seq_ids.shape[0] == 5 - assert query_ids == [1, 1, 1, 1, 1] - assert doc_ids == [0, 1, 2, 3, 3] - - -@pytest.mark.unit -def test_postprocess(mock_reader: ExtractiveReader): - start = torch.zeros((2, 8)) - start[0, 3] = 4 - start[0, 1] = 5 # test attention_mask - start[0, 4] = 3 - start[1, 2] = 1 - - end = torch.zeros((2, 8)) - end[0, 1] = 5 # test attention_mask - end[0, 2] = 4 # test that end can't be before start - end[0, 3] = 3 - end[0, 4] = 2 - end[1, :] = -10 - end[1, 4] = -1 - - sequence_ids = torch.ones((2, 8)) - attention_mask = torch.ones((2, 8)) - attention_mask[0, :2] = 0 - encoding = Mock() - encoding.token_to_chars = lambda i: (int(i), int(i) + 1) - - start_candidates, end_candidates, probs = mock_reader._postprocess( - start, end, sequence_ids, attention_mask, 3, [encoding, encoding] - ) - - assert len(start_candidates) == len(end_candidates) == len(probs) == 2 - assert len(start_candidates[0]) == len(end_candidates[0]) == len(probs[0]) == 3 - assert start_candidates[0][0] == 3 - assert end_candidates[0][0] == 4 - assert start_candidates[0][1] == 3 - assert end_candidates[0][1] == 5 - assert start_candidates[0][2] == 4 - assert end_candidates[0][2] == 5 - assert probs[0][0] == pytest.approx(1 / (1 + exp(-7 * mock_reader.calibration_factor))) - assert probs[0][1] == pytest.approx(1 / (1 + exp(-6 * mock_reader.calibration_factor))) - assert probs[0][2] == pytest.approx(1 / (1 + exp(-5 * mock_reader.calibration_factor))) - assert start_candidates[1][0] == 2 - assert end_candidates[1][0] == 5 - assert probs[1][0] == pytest.approx(1 / 2) - - -@pytest.mark.unit -def test_nest_answers(mock_reader: ExtractiveReader): - start = list(range(5)) - end = [i + 5 for i in start] - start = [start] * 6 # type: ignore - end = [end] * 6 # type: ignore - probabilities = torch.arange(5).unsqueeze(0) / 5 + torch.arange(6).unsqueeze(-1) / 25 - query_ids = [0] * 3 + [1] * 3 - document_ids = list(range(3)) * 2 - nested_answers = mock_reader._nest_answers( - start, end, probabilities, example_documents[0], example_queries, 5, 3, None, query_ids, document_ids, True # type: ignore - ) - expected_no_answers = [0.2 * 0.16 * 0.12, 0] - for query, answers, expected_no_answer, probabilities in zip( - example_queries, nested_answers, expected_no_answers, [probabilities[:3, -1], probabilities[3:, -1]] - ): - assert len(answers) == 4 - for doc, answer, probability in zip(example_documents[0], reversed(answers[:3]), probabilities): - assert answer.query == query - assert answer.document == doc - assert answer.probability == pytest.approx(probability) - no_answer = answers[-1] - assert no_answer.query == query - assert no_answer.document is None - assert no_answer.probability == pytest.approx(expected_no_answer) - - -@pytest.mark.unit -@patch("haystack.preview.components.readers.extractive.AutoTokenizer.from_pretrained") -@patch("haystack.preview.components.readers.extractive.AutoModelForQuestionAnswering.from_pretrained") -def test_warm_up_use_hf_token(mocked_automodel, mocked_autotokenizer): - reader = ExtractiveReader("deepset/roberta-base-squad2", token="fake-token") - reader.warm_up() - - mocked_automodel.assert_called_once_with("deepset/roberta-base-squad2", token="fake-token") - mocked_autotokenizer.assert_called_once_with("deepset/roberta-base-squad2", token="fake-token") - - -@pytest.mark.unit -def test_missing_token_to_chars_values(): - # See https://github.com/deepset-ai/haystack/issues/6098 - - def mock_tokenize( - texts: List[str], - text_pairs: List[str], - padding: bool, - truncation: bool, - max_length: int, - return_tensors: str, - return_overflowing_tokens: bool, - stride: int, - ): - assert padding - assert truncation - assert return_tensors == "pt" - assert return_overflowing_tokens - - tokens = Mock() - - num_splits = [ceil(len(text + pair) / max_length) for text, pair in zip(texts, text_pairs)] - tokens.overflow_to_sample_mapping = [i for i, num in enumerate(num_splits) for _ in range(num)] - num_samples = sum(num_splits) - tokens.encodings = [Mock() for _ in range(num_samples)] - sequence_ids = [0] * 16 + [1] * 16 + [None] * (max_length - 32) - for encoding in tokens.encodings: - encoding.sequence_ids = sequence_ids - encoding.token_to_chars = lambda i: None - tokens.input_ids = torch.zeros(num_samples, max_length, dtype=torch.int) - attention_mask = torch.zeros(num_samples, max_length, dtype=torch.int) - attention_mask[:32] = 1 - tokens.attention_mask = attention_mask - return tokens - - class MockModel(torch.nn.Module): - def to(self, device): - assert device == "cpu:0" - self.device_set = True - return self - - def forward(self, input_ids, attention_mask, *args, **kwargs): - assert input_ids.device == torch.device("cpu") - assert attention_mask.device == torch.device("cpu") - assert self.device_set - start = torch.zeros(input_ids.shape[:2]) - end = torch.zeros(input_ids.shape[:2]) - start[:, 27] = 1 - end[:, 31] = 1 - end[:, 32] = 1 - prediction = Mock() - prediction.start_logits = start - prediction.end_logits = end - return prediction - - with patch("haystack.preview.components.readers.extractive.AutoTokenizer.from_pretrained") as tokenizer, patch( - "haystack.preview.components.readers.extractive.AutoModelForQuestionAnswering.from_pretrained" - ) as model: - tokenizer.return_value = mock_tokenize - model.return_value = MockModel() - reader = ExtractiveReader(model_name_or_path="mock-model", device="cpu:0") - reader.warm_up() - - answers = reader.run(example_queries[0], example_documents[0], top_k=3)[ - "answers" - ] # [0] Uncomment and remove first two indices when batching support is reintroduced - for doc, answer in zip(example_documents[0], answers[:3]): - assert answer.start is None - assert answer.end is None - assert doc.content is not None - assert answer.data == doc.content - - -@pytest.mark.integration -def test_t5(): - reader = ExtractiveReader("TARUNBHATT/flan-t5-small-finetuned-squad") - reader.warm_up() - answers = reader.run(example_queries[0], example_documents[0], top_k=2)[ - "answers" - ] # remove indices when batching support is reintroduced - assert answers[0].data == "Angela Merkel" - assert answers[0].probability == pytest.approx(0.7764519453048706) - assert answers[1].data == "Olaf Scholz" - assert answers[1].probability == pytest.approx(0.7703777551651001) - assert answers[2].data is None - assert answers[2].probability == pytest.approx(0.051331606147570596) - # Uncomment assertions below when batching is reintroduced - # assert answers[0][2].probability == pytest.approx(0.051331606147570596) - # assert answers[1][0].data == "Jerry" - # assert answers[1][0].probability == pytest.approx(0.7413333654403687) - # assert answers[1][1].data == "Olaf Scholz" - # assert answers[1][1].probability == pytest.approx(0.7266613841056824) - # assert answers[1][2].data is None - # assert answers[1][2].probability == pytest.approx(0.0707035798685709) - - -@pytest.mark.integration -def test_roberta(): - reader = ExtractiveReader("deepset/tinyroberta-squad2") - reader.warm_up() - answers = reader.run(example_queries[0], example_documents[0], top_k=2)[ - "answers" - ] # remove indices when batching is reintroduced - assert answers[0].data == "Olaf Scholz" - assert answers[0].probability == pytest.approx(0.8614975214004517) - assert answers[1].data == "Angela Merkel" - assert answers[1].probability == pytest.approx(0.857952892780304) - assert answers[2].data is None - assert answers[2].probability == pytest.approx(0.019673851661650588) - # uncomment assertions below when there is batching in v2 - # assert answers[0][0].data == "Olaf Scholz" - # assert answers[0][0].probability == pytest.approx(0.8614975214004517) - # assert answers[0][1].data == "Angela Merkel" - # assert answers[0][1].probability == pytest.approx(0.857952892780304) - # assert answers[0][2].data is None - # assert answers[0][2].probability == pytest.approx(0.0196738764278237) - # assert answers[1][0].data == "Jerry" - # assert answers[1][0].probability == pytest.approx(0.7048940658569336) - # assert answers[1][1].data == "Olaf Scholz" - # assert answers[1][1].probability == pytest.approx(0.6604189872741699) - # assert answers[1][2].data is None - # assert answers[1][2].probability == pytest.approx(0.1002123719777046) - - -@pytest.mark.integration -def test_matches_hf_pipeline(): - reader = ExtractiveReader("deepset/tinyroberta-squad2", device="cpu") - reader.warm_up() - answers = reader.run(example_queries[0], [[example_documents[0][0]]][0], top_k=20, no_answer=False)[ - "answers" - ] # [0] Remove first two indices when batching support is reintroduced - pipe = pipeline("question-answering", model=reader.model, tokenizer=reader.tokenizer, align_to_words=False) - answers_hf = pipe( - question=example_queries[0], - context=example_documents[0][0].content, - max_answer_len=1_000, - handle_impossible_answer=False, - top_k=20, - ) # We need to disable HF postprocessing features to make the results comparable. This is related to https://github.com/huggingface/transformers/issues/26286 - assert len(answers) == len(answers_hf) == 20 - for answer, answer_hf in zip(answers, answers_hf): - assert answer.start == answer_hf["start"] - assert answer.end == answer_hf["end"] - assert answer.data == answer_hf["answer"] diff --git a/test/preview/components/retrievers/__init__.py b/test/preview/components/retrievers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/retrievers/test_in_memory_bm25_retriever.py b/test/preview/components/retrievers/test_in_memory_bm25_retriever.py deleted file mode 100644 index 2a84f3bace..0000000000 --- a/test/preview/components/retrievers/test_in_memory_bm25_retriever.py +++ /dev/null @@ -1,185 +0,0 @@ -from typing import Dict, Any - -import pytest - -from haystack.preview import Pipeline, DeserializationError -from haystack.preview.testing.factory import document_store_class -from haystack.preview.components.retrievers.in_memory_bm25_retriever import InMemoryBM25Retriever -from haystack.preview.dataclasses import Document -from haystack.preview.document_stores import InMemoryDocumentStore - - -@pytest.fixture() -def mock_docs(): - return [ - Document(content="Javascript is a popular programming language"), - Document(content="Java is a popular programming language"), - Document(content="Python is a popular programming language"), - Document(content="Ruby is a popular programming language"), - Document(content="PHP is a popular programming language"), - ] - - -class TestMemoryBM25Retriever: - @pytest.mark.unit - def test_init_default(self): - retriever = InMemoryBM25Retriever(InMemoryDocumentStore()) - assert retriever.filters is None - assert retriever.top_k == 10 - assert retriever.scale_score is False - - @pytest.mark.unit - def test_init_with_parameters(self): - retriever = InMemoryBM25Retriever( - InMemoryDocumentStore(), filters={"name": "test.txt"}, top_k=5, scale_score=True - ) - assert retriever.filters == {"name": "test.txt"} - assert retriever.top_k == 5 - assert retriever.scale_score - - @pytest.mark.unit - def test_init_with_invalid_top_k_parameter(self): - with pytest.raises(ValueError): - InMemoryBM25Retriever(InMemoryDocumentStore(), top_k=-2) - - @pytest.mark.unit - def test_to_dict(self): - MyFakeStore = document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - document_store = MyFakeStore() - document_store.to_dict = lambda: {"type": "MyFakeStore", "init_parameters": {}} - component = InMemoryBM25Retriever(document_store=document_store) - - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.retrievers.in_memory_bm25_retriever.InMemoryBM25Retriever", - "init_parameters": { - "document_store": {"type": "MyFakeStore", "init_parameters": {}}, - "filters": None, - "top_k": 10, - "scale_score": False, - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - MyFakeStore = document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - document_store = MyFakeStore() - document_store.to_dict = lambda: {"type": "MyFakeStore", "init_parameters": {}} - component = InMemoryBM25Retriever( - document_store=document_store, filters={"name": "test.txt"}, top_k=5, scale_score=True - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.retrievers.in_memory_bm25_retriever.InMemoryBM25Retriever", - "init_parameters": { - "document_store": {"type": "MyFakeStore", "init_parameters": {}}, - "filters": {"name": "test.txt"}, - "top_k": 5, - "scale_score": True, - }, - } - - @pytest.mark.unit - def test_from_dict(self): - document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - data = { - "type": "haystack.preview.components.retrievers.in_memory_bm25_retriever.InMemoryBM25Retriever", - "init_parameters": { - "document_store": {"type": "haystack.preview.testing.factory.MyFakeStore", "init_parameters": {}}, - "filters": {"name": "test.txt"}, - "top_k": 5, - }, - } - component = InMemoryBM25Retriever.from_dict(data) - assert isinstance(component.document_store, InMemoryDocumentStore) - assert component.filters == {"name": "test.txt"} - assert component.top_k == 5 - assert component.scale_score is False - - @pytest.mark.unit - def test_from_dict_without_docstore(self): - data = {"type": "InMemoryBM25Retriever", "init_parameters": {}} - with pytest.raises(DeserializationError, match="Missing 'document_store' in serialization data"): - InMemoryBM25Retriever.from_dict(data) - - @pytest.mark.unit - def test_from_dict_without_docstore_type(self): - data = {"type": "InMemoryBM25Retriever", "init_parameters": {"document_store": {"init_parameters": {}}}} - with pytest.raises(DeserializationError, match="Missing 'type' in document store's serialization data"): - InMemoryBM25Retriever.from_dict(data) - - @pytest.mark.unit - def test_from_dict_nonexisting_docstore(self): - data = { - "type": "haystack.preview.components.retrievers.in_memory_bm25_retriever.InMemoryBM25Retriever", - "init_parameters": {"document_store": {"type": "NonexistingDocstore", "init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="DocumentStore type 'NonexistingDocstore' not found"): - InMemoryBM25Retriever.from_dict(data) - - @pytest.mark.unit - def test_retriever_valid_run(self, mock_docs): - top_k = 5 - ds = InMemoryDocumentStore() - ds.write_documents(mock_docs) - - retriever = InMemoryBM25Retriever(ds, top_k=top_k) - result = retriever.run(query="PHP") - - assert "documents" in result - assert len(result["documents"]) == top_k - assert result["documents"][0].content == "PHP is a popular programming language" - - @pytest.mark.unit - def test_invalid_run_wrong_store_type(self): - SomeOtherDocumentStore = document_store_class("SomeOtherDocumentStore") - with pytest.raises(ValueError, match="document_store must be an instance of InMemoryDocumentStore"): - InMemoryBM25Retriever(SomeOtherDocumentStore()) - - @pytest.mark.integration - @pytest.mark.parametrize( - "query, query_result", - [ - ("Javascript", "Javascript is a popular programming language"), - ("Java", "Java is a popular programming language"), - ], - ) - def test_run_with_pipeline(self, mock_docs, query: str, query_result: str): - ds = InMemoryDocumentStore() - ds.write_documents(mock_docs) - retriever = InMemoryBM25Retriever(ds) - - pipeline = Pipeline() - pipeline.add_component("retriever", retriever) - result: Dict[str, Any] = pipeline.run(data={"retriever": {"query": query}}) - - assert result - assert "retriever" in result - results_docs = result["retriever"]["documents"] - assert results_docs - assert results_docs[0].content == query_result - - @pytest.mark.integration - @pytest.mark.parametrize( - "query, query_result, top_k", - [ - ("Javascript", "Javascript is a popular programming language", 1), - ("Java", "Java is a popular programming language", 2), - ("Ruby", "Ruby is a popular programming language", 3), - ], - ) - def test_run_with_pipeline_and_top_k(self, mock_docs, query: str, query_result: str, top_k: int): - ds = InMemoryDocumentStore() - ds.write_documents(mock_docs) - retriever = InMemoryBM25Retriever(ds) - - pipeline = Pipeline() - pipeline.add_component("retriever", retriever) - result: Dict[str, Any] = pipeline.run(data={"retriever": {"query": query, "top_k": top_k}}) - - assert result - assert "retriever" in result - results_docs = result["retriever"]["documents"] - assert results_docs - assert len(results_docs) == top_k - assert results_docs[0].content == query_result diff --git a/test/preview/components/retrievers/test_in_memory_embedding_retriever.py b/test/preview/components/retrievers/test_in_memory_embedding_retriever.py deleted file mode 100644 index 6c03e6d621..0000000000 --- a/test/preview/components/retrievers/test_in_memory_embedding_retriever.py +++ /dev/null @@ -1,169 +0,0 @@ -from typing import Dict, Any - -import pytest -import numpy as np - -from haystack.preview import Pipeline, DeserializationError -from haystack.preview.testing.factory import document_store_class -from haystack.preview.components.retrievers.in_memory_embedding_retriever import InMemoryEmbeddingRetriever -from haystack.preview.dataclasses import Document -from haystack.preview.document_stores import InMemoryDocumentStore - - -class TestMemoryEmbeddingRetriever: - @pytest.mark.unit - def test_init_default(self): - retriever = InMemoryEmbeddingRetriever(InMemoryDocumentStore()) - assert retriever.filters is None - assert retriever.top_k == 10 - assert retriever.scale_score is False - - @pytest.mark.unit - def test_init_with_parameters(self): - retriever = InMemoryEmbeddingRetriever( - InMemoryDocumentStore(), filters={"name": "test.txt"}, top_k=5, scale_score=True - ) - assert retriever.filters == {"name": "test.txt"} - assert retriever.top_k == 5 - assert retriever.scale_score - - @pytest.mark.unit - def test_init_with_invalid_top_k_parameter(self): - with pytest.raises(ValueError): - InMemoryEmbeddingRetriever(InMemoryDocumentStore(), top_k=-2) - - @pytest.mark.unit - def test_to_dict(self): - MyFakeStore = document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - document_store = MyFakeStore() - document_store.to_dict = lambda: {"type": "test_module.MyFakeStore", "init_parameters": {}} - component = InMemoryEmbeddingRetriever(document_store=document_store) - - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": { - "document_store": {"type": "test_module.MyFakeStore", "init_parameters": {}}, - "filters": None, - "top_k": 10, - "scale_score": False, - "return_embedding": False, - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - MyFakeStore = document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - document_store = MyFakeStore() - document_store.to_dict = lambda: {"type": "test_module.MyFakeStore", "init_parameters": {}} - component = InMemoryEmbeddingRetriever( - document_store=document_store, - filters={"name": "test.txt"}, - top_k=5, - scale_score=True, - return_embedding=True, - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": { - "document_store": {"type": "test_module.MyFakeStore", "init_parameters": {}}, - "filters": {"name": "test.txt"}, - "top_k": 5, - "scale_score": True, - "return_embedding": True, - }, - } - - @pytest.mark.unit - def test_from_dict(self): - document_store_class("MyFakeStore", bases=(InMemoryDocumentStore,)) - data = { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": { - "document_store": {"type": "haystack.preview.testing.factory.MyFakeStore", "init_parameters": {}}, - "filters": {"name": "test.txt"}, - "top_k": 5, - }, - } - component = InMemoryEmbeddingRetriever.from_dict(data) - assert isinstance(component.document_store, InMemoryDocumentStore) - assert component.filters == {"name": "test.txt"} - assert component.top_k == 5 - assert component.scale_score is False - - @pytest.mark.unit - def test_from_dict_without_docstore(self): - data = { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": {}, - } - with pytest.raises(DeserializationError, match="Missing 'document_store' in serialization data"): - InMemoryEmbeddingRetriever.from_dict(data) - - @pytest.mark.unit - def test_from_dict_without_docstore_type(self): - data = { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": {"document_store": {"init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="Missing 'type' in document store's serialization data"): - InMemoryEmbeddingRetriever.from_dict(data) - - @pytest.mark.unit - def test_from_dict_nonexisting_docstore(self): - data = { - "type": "haystack.preview.components.retrievers.in_memory_embedding_retriever.InMemoryEmbeddingRetriever", - "init_parameters": {"document_store": {"type": "NonexistingDocstore", "init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="DocumentStore type 'NonexistingDocstore' not found"): - InMemoryEmbeddingRetriever.from_dict(data) - - @pytest.mark.unit - def test_valid_run(self): - top_k = 3 - ds = InMemoryDocumentStore(embedding_similarity_function="cosine") - docs = [ - Document(content="my document", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="another document", embedding=[1.0, 1.0, 1.0, 1.0]), - Document(content="third document", embedding=[0.5, 0.7, 0.5, 0.7]), - ] - ds.write_documents(docs) - - retriever = InMemoryEmbeddingRetriever(ds, top_k=top_k) - result = retriever.run(query_embedding=[0.1, 0.1, 0.1, 0.1], return_embedding=True) - - assert "documents" in result - assert len(result["documents"]) == top_k - assert np.array_equal(result["documents"][0].embedding, [1.0, 1.0, 1.0, 1.0]) - - @pytest.mark.unit - def test_invalid_run_wrong_store_type(self): - SomeOtherDocumentStore = document_store_class("SomeOtherDocumentStore") - with pytest.raises(ValueError, match="document_store must be an instance of InMemoryDocumentStore"): - InMemoryEmbeddingRetriever(SomeOtherDocumentStore()) - - @pytest.mark.integration - def test_run_with_pipeline(self): - ds = InMemoryDocumentStore(embedding_similarity_function="cosine") - top_k = 2 - docs = [ - Document(content="my document", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="another document", embedding=[1.0, 1.0, 1.0, 1.0]), - Document(content="third document", embedding=[0.5, 0.7, 0.5, 0.7]), - ] - ds.write_documents(docs) - retriever = InMemoryEmbeddingRetriever(ds, top_k=top_k) - - pipeline = Pipeline() - pipeline.add_component("retriever", retriever) - result: Dict[str, Any] = pipeline.run( - data={"retriever": {"query_embedding": [0.1, 0.1, 0.1, 0.1], "return_embedding": True}} - ) - - assert result - assert "retriever" in result - results_docs = result["retriever"]["documents"] - assert results_docs - assert len(results_docs) == top_k - assert np.array_equal(results_docs[0].embedding, [1.0, 1.0, 1.0, 1.0]) diff --git a/test/preview/components/routers/__init__.py b/test/preview/components/routers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/routers/test_conditional_router.py b/test/preview/components/routers/test_conditional_router.py deleted file mode 100644 index 501fb530d1..0000000000 --- a/test/preview/components/routers/test_conditional_router.py +++ /dev/null @@ -1,324 +0,0 @@ -import copy -import typing -from typing import List, Dict -from unittest import mock - -import pytest - -from haystack.preview.components.routers import ConditionalRouter -from haystack.preview.components.routers.conditional_router import ( - NoRouteSelectedException, - serialize_type, - deserialize_type, -) -from haystack.preview.dataclasses import ChatMessage - - -class TestRouter: - @pytest.fixture - def routes(self): - return [ - {"condition": "{{streams|length < 2}}", "output": "{{query}}", "output_type": str, "output_name": "query"}, - { - "condition": "{{streams|length >= 2}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - ] - - @pytest.fixture - def router(self, routes): - return ConditionalRouter(routes) - - def test_missing_mandatory_fields(self): - """ - Router raises a ValueError if each route does not contain 'condition', 'output', and 'output_type' keys - """ - routes = [ - {"condition": "{{streams|length < 2}}", "output": "{{query}}"}, - {"condition": "{{streams|length < 2}}", "output_type": str}, - ] - with pytest.raises(ValueError): - ConditionalRouter(routes) - - def test_invalid_condition_field(self): - """ - ConditionalRouter init raises a ValueError if one of the routes contains invalid condition - """ - # invalid condition field - routes = [{"condition": "{{streams|length < 2", "output": "query", "output_type": str, "output_name": "test"}] - with pytest.raises(ValueError, match="Invalid template"): - ConditionalRouter(routes) - - def test_no_vars_in_output_route_but_with_output_name(self): - """ - Router can't accept a route with no variables used in the output field - """ - routes = [ - { - "condition": "{{streams|length > 2}}", - "output": "This is a constant", - "output_name": "enough_streams", - "output_type": str, - } - ] - router = ConditionalRouter(routes) - kwargs = {"streams": [1, 2, 3], "query": "Haystack"} - result = router.run(**kwargs) - assert result == {"enough_streams": "This is a constant"} - - def test_mandatory_and_optional_fields_with_extra_fields(self): - """ - Router accepts a list of routes with mandatory and optional fields but not if some new field is added - """ - - routes = [ - { - "condition": "{{streams|length < 2}}", - "output": "{{query}}", - "output_type": str, - "output_name": "test", - "bla": "bla", - }, - {"condition": "{{streams|length < 2}}", "output": "{{query}}", "output_type": str}, - ] - - with pytest.raises(ValueError): - ConditionalRouter(routes) - - def test_router_initialized(self, routes): - router = ConditionalRouter(routes) - - assert router.routes == routes - assert set(router.__canals_input__.keys()) == {"query", "streams"} - assert set(router.__canals_output__.keys()) == {"query", "streams"} - - def test_router_evaluate_condition_expressions(self, router): - # first route should be selected - kwargs = {"streams": [1, 2, 3], "query": "test"} - result = router.run(**kwargs) - assert result == {"streams": [1, 2, 3]} - - # second route should be selected - kwargs = {"streams": [1], "query": "test"} - result = router.run(**kwargs) - assert result == {"query": "test"} - - def test_router_evaluate_condition_expressions_using_output_slot(self): - routes = [ - { - "condition": "{{streams|length > 2}}", - "output": "{{streams}}", - "output_name": "enough_streams", - "output_type": List[int], - }, - { - "condition": "{{streams|length <= 2}}", - "output": "{{streams}}", - "output_name": "insufficient_streams", - "output_type": List[int], - }, - ] - router = ConditionalRouter(routes) - # enough_streams output slot will be selected with [1, 2, 3] list being outputted - kwargs = {"streams": [1, 2, 3], "query": "Haystack"} - result = router.run(**kwargs) - assert result == {"enough_streams": [1, 2, 3]} - - def test_complex_condition(self): - routes = [ - { - "condition": "{{messages[-1].metadata.finish_reason == 'function_call'}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - { - "condition": "{{True}}", - "output": "{{query}}", - "output_type": str, - "output_name": "query", - }, # catch-all condition - ] - router = ConditionalRouter(routes) - message = mock.MagicMock() - message.metadata.finish_reason = "function_call" - result = router.run(messages=[message], streams=[1, 2, 3], query="my query") - assert result == {"streams": [1, 2, 3]} - - def test_router_no_route(self, router): - # should raise an exception - router = ConditionalRouter( - [ - { - "condition": "{{streams|length < 2}}", - "output": "{{query}}", - "output_type": str, - "output_name": "query", - }, - { - "condition": "{{streams|length >= 5}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - ] - ) - - kwargs = {"streams": [1, 2, 3], "query": "test"} - with pytest.raises(NoRouteSelectedException): - router.run(**kwargs) - - def test_router_raises_value_error_if_route_not_dictionary(self): - """ - Router raises a ValueError if each route is not a dictionary - """ - routes = [ - {"condition": "{{streams|length < 2}}", "output": "{{query}}", "output_type": str, "output_name": "query"}, - ["{{streams|length >= 2}}", "streams", List[int]], - ] - - with pytest.raises(ValueError): - ConditionalRouter(routes) - - def test_router_raises_value_error_if_route_missing_keys(self): - """ - Router raises a ValueError if each route does not contain 'condition', 'output', and 'output_type' keys - """ - routes = [ - {"condition": "{{streams|length < 2}}", "output": "{{query}}"}, - {"condition": "{{streams|length < 2}}", "output_type": str}, - ] - - with pytest.raises(ValueError): - ConditionalRouter(routes) - - def test_output_type_serialization(self): - assert serialize_type(str) == "str" - assert serialize_type(List[int]) == "typing.List[int]" - assert serialize_type(List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]" - assert serialize_type(ChatMessage) == "haystack.preview.dataclasses.chat_message.ChatMessage" - assert serialize_type(typing.List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]" - assert serialize_type(List[ChatMessage]) == "typing.List[haystack.preview.dataclasses.chat_message.ChatMessage]" - assert ( - serialize_type(typing.Dict[int, ChatMessage]) - == "typing.Dict[int, haystack.preview.dataclasses.chat_message.ChatMessage]" - ) - assert serialize_type(int) == "int" - assert serialize_type(ChatMessage.from_user("ciao")) == "haystack.preview.dataclasses.chat_message.ChatMessage" - - def test_output_type_deserialization(self): - assert deserialize_type("str") == str - assert deserialize_type("typing.List[int]") == typing.List[int] - assert deserialize_type("typing.List[typing.Dict[str, int]]") == typing.List[Dict[str, int]] - assert deserialize_type("typing.Dict[str, int]") == Dict[str, int] - assert deserialize_type("typing.Dict[str, typing.List[int]]") == Dict[str, List[int]] - assert deserialize_type("typing.List[typing.Dict[str, typing.List[int]]]") == List[Dict[str, List[int]]] - assert ( - deserialize_type("typing.List[haystack.preview.dataclasses.chat_message.ChatMessage]") - == typing.List[ChatMessage] - ) - assert ( - deserialize_type("typing.Dict[int, haystack.preview.dataclasses.chat_message.ChatMessage]") - == typing.Dict[int, ChatMessage] - ) - assert deserialize_type("haystack.preview.dataclasses.chat_message.ChatMessage") == ChatMessage - assert deserialize_type("int") == int - - def test_router_de_serialization(self): - routes = [ - {"condition": "{{streams|length < 2}}", "output": "{{query}}", "output_type": str, "output_name": "query"}, - { - "condition": "{{streams|length >= 2}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - ] - router = ConditionalRouter(routes) - router_dict = router.to_dict() - - # assert that the router dict is correct, with all keys and values being strings - for route in router_dict["init_parameters"]["routes"]: - for key in route.keys(): - assert isinstance(key, str) - assert isinstance(route[key], str) - - new_router = ConditionalRouter.from_dict(router_dict) - assert router.routes == new_router.routes - - # now use both routers with the same input - kwargs = {"streams": [1, 2, 3], "query": "Haystack"} - result1 = router.run(**kwargs) - result2 = new_router.run(**kwargs) - - # check that the result is the same and correct - assert result1 == result2 and result1 == {"streams": [1, 2, 3]} - - def test_router_de_serialization_user_type(self): - routes = [ - { - "condition": "{{streams|length < 2}}", - "output": "{{message}}", - "output_type": ChatMessage, - "output_name": "message", - }, - { - "condition": "{{streams|length >= 2}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - ] - router = ConditionalRouter(routes) - router_dict = router.to_dict() - - # assert that the router dict is correct, with all keys and values being strings - for route in router_dict["init_parameters"]["routes"]: - for key in route.keys(): - assert isinstance(key, str) - assert isinstance(route[key], str) - - # check that the output_type is a string and a proper class name - assert ( - router_dict["init_parameters"]["routes"][0]["output_type"] - == "haystack.preview.dataclasses.chat_message.ChatMessage" - ) - - # deserialize the router - new_router = ConditionalRouter.from_dict(router_dict) - - # check that the output_type is the right class - assert new_router.routes[0]["output_type"] == ChatMessage - assert router.routes == new_router.routes - - # now use both routers to run the same message - message = ChatMessage.from_user("ciao") - kwargs = {"streams": [1], "message": message} - result1 = router.run(**kwargs) - result2 = new_router.run(**kwargs) - - # check that the result is the same and correct - assert result1 == result2 and result1["message"].content == message.content - - def test_router_serialization_idempotence(self): - routes = [ - { - "condition": "{{streams|length < 2}}", - "output": "{{message}}", - "output_type": ChatMessage, - "output_name": "message", - }, - { - "condition": "{{streams|length >= 2}}", - "output": "{{streams}}", - "output_type": List[int], - "output_name": "streams", - }, - ] - router = ConditionalRouter(routes) - # invoke to_dict twice and check that the result is the same - router_dict_first_invocation = copy.deepcopy(router.to_dict()) - router_dict_second_invocation = router.to_dict() - assert router_dict_first_invocation == router_dict_second_invocation diff --git a/test/preview/components/routers/test_document_joiner.py b/test/preview/components/routers/test_document_joiner.py deleted file mode 100644 index 9b4ab7bf2a..0000000000 --- a/test/preview/components/routers/test_document_joiner.py +++ /dev/null @@ -1,140 +0,0 @@ -import logging - -import pytest - -from haystack.preview import Document -from haystack.preview.components.routers.document_joiner import DocumentJoiner - - -class TestDocumentJoiner: - @pytest.mark.unit - def test_init(self): - joiner = DocumentJoiner() - assert joiner.join_mode == "concatenate" - assert joiner.weights is None - assert joiner.top_k is None - assert joiner.sort_by_score - - @pytest.mark.unit - def test_init_with_custom_parameters(self): - joiner = DocumentJoiner(join_mode="merge", weights=[0.4, 0.6], top_k=5, sort_by_score=False) - assert joiner.join_mode == "merge" - assert joiner.weights == [0.4, 0.6] - assert joiner.top_k == 5 - assert not joiner.sort_by_score - - @pytest.mark.unit - def test_empty_list(self): - joiner = DocumentJoiner() - result = joiner.run([]) - assert result == {"documents": []} - - @pytest.mark.unit - def test_list_of_empty_lists(self): - joiner = DocumentJoiner() - result = joiner.run([[], []]) - assert result == {"documents": []} - - @pytest.mark.unit - def test_list_with_one_empty_list(self): - joiner = DocumentJoiner() - documents = [Document(content="a"), Document(content="b"), Document(content="c")] - result = joiner.run([[], documents]) - assert result == {"documents": documents} - - @pytest.mark.unit - def test_unsupported_join_mode(self): - with pytest.raises(ValueError, match="DocumentJoiner component does not support 'unsupported_mode' join_mode."): - DocumentJoiner(join_mode="unsupported_mode") - - @pytest.mark.unit - def test_run_with_concatenate_join_mode_and_top_k(self): - joiner = DocumentJoiner(top_k=6) - documents_1 = [Document(content="a"), Document(content="b"), Document(content="c")] - documents_2 = [ - Document(content="d"), - Document(content="e"), - Document(content="f", meta={"key": "value"}), - Document(content="g"), - ] - output = joiner.run([documents_1, documents_2]) - assert len(output["documents"]) == 6 - assert sorted(documents_1 + documents_2[:-1], key=lambda d: d.id) == sorted( - output["documents"], key=lambda d: d.id - ) - - @pytest.mark.unit - def test_run_with_concatenate_join_mode_and_duplicate_documents(self): - joiner = DocumentJoiner() - documents_1 = [Document(content="a", score=0.3), Document(content="b"), Document(content="c")] - documents_2 = [ - Document(content="a", score=0.2), - Document(content="a"), - Document(content="f", meta={"key": "value"}), - ] - output = joiner.run([documents_1, documents_2]) - assert len(output["documents"]) == 4 - assert sorted(documents_1 + [documents_2[-1]], key=lambda d: d.id) == sorted( - output["documents"], key=lambda d: d.id - ) - - @pytest.mark.unit - def test_run_with_merge_join_mode(self): - joiner = DocumentJoiner(join_mode="merge", weights=[1.5, 0.5]) - documents_1 = [Document(content="a", score=1.0), Document(content="b", score=2.0)] - documents_2 = [ - Document(content="a", score=0.5), - Document(content="b", score=3.0), - Document(content="f", score=4.0, meta={"key": "value"}), - ] - output = joiner.run([documents_1, documents_2]) - assert len(output["documents"]) == 3 - expected_document_ids = [ - doc.id - for doc in [ - Document(content="a", score=1.25), - Document(content="b", score=2.25), - Document(content="f", score=4.0, meta={"key": "value"}), - ] - ] - assert all(doc.id in expected_document_ids for doc in output["documents"]) - - @pytest.mark.unit - def test_run_with_reciprocal_rank_fusion_join_mode(self): - joiner = DocumentJoiner(join_mode="reciprocal_rank_fusion") - documents_1 = [Document(content="a"), Document(content="b"), Document(content="c")] - documents_2 = [ - Document(content="b", score=1000.0), - Document(content="c"), - Document(content="a"), - Document(content="f", meta={"key": "value"}), - ] - output = joiner.run([documents_1, documents_2]) - assert len(output["documents"]) == 4 - expected_document_ids = [ - doc.id - for doc in [ - Document(content="b"), - Document(content="a"), - Document(content="c"), - Document(content="f", meta={"key": "value"}), - ] - ] - assert all(doc.id in expected_document_ids for doc in output["documents"]) - - @pytest.mark.unit - def test_sort_by_score_without_scores(self, caplog): - joiner = DocumentJoiner() - with caplog.at_level(logging.INFO): - documents = [Document(content="a"), Document(content="b", score=0.5)] - output = joiner.run([documents]) - assert "those with score=None were sorted as if they had a score of -infinity" in caplog.text - assert output["documents"] == documents[::-1] - - @pytest.mark.unit - def test_output_documents_not_sorted_by_score(self): - joiner = DocumentJoiner(sort_by_score=False) - documents_1 = [Document(content="a", score=0.1)] - documents_2 = [Document(content="d", score=0.2)] - output = joiner.run([documents_1, documents_2]) - assert output["documents"] == documents_1 + documents_2 diff --git a/test/preview/components/routers/test_file_router.py b/test/preview/components/routers/test_file_router.py deleted file mode 100644 index b513d95830..0000000000 --- a/test/preview/components/routers/test_file_router.py +++ /dev/null @@ -1,139 +0,0 @@ -import sys - -import pytest - -from haystack.preview.components.routers.file_type_router import FileTypeRouter -from haystack.preview.dataclasses import ByteStream - - -@pytest.mark.skipif( - sys.platform in ["win32", "cygwin"], - reason="Can't run on Windows Github CI, need access to registry to get mime types", -) -class TestFileTypeRouter: - @pytest.mark.unit - def test_run(self, preview_samples_path): - """ - Test if the component runs correctly in the simplest happy path. - """ - file_paths = [ - preview_samples_path / "txt" / "doc_1.txt", - preview_samples_path / "txt" / "doc_2.txt", - preview_samples_path / "audio" / "the context for this answer is here.wav", - preview_samples_path / "images" / "apple.jpg", - ] - - router = FileTypeRouter(mime_types=["text/plain", "audio/x-wav", "image/jpeg"]) - output = router.run(sources=file_paths) - assert output - assert len(output["text/plain"]) == 2 - assert len(output["audio/x-wav"]) == 1 - assert len(output["image/jpeg"]) == 1 - assert not output["unclassified"] - - @pytest.mark.unit - def test_run_with_bytestreams(self, preview_samples_path): - """ - Test if the component runs correctly with ByteStream inputs. - """ - file_paths = [ - preview_samples_path / "txt" / "doc_1.txt", - preview_samples_path / "txt" / "doc_2.txt", - preview_samples_path / "audio" / "the context for this answer is here.wav", - preview_samples_path / "images" / "apple.jpg", - ] - mime_types = ["text/plain", "text/plain", "audio/x-wav", "image/jpeg"] - # Convert file paths to ByteStream objects and set metadata - byte_streams = [] - for path, mime_type in zip(file_paths, mime_types): - stream = ByteStream(path.read_bytes()) - - stream.metadata["content_type"] = mime_type - - byte_streams.append(stream) - - # add unclassified ByteStream - bs = ByteStream(b"unclassified content") - bs.metadata["content_type"] = "unknown_type" - byte_streams.append(bs) - - router = FileTypeRouter(mime_types=["text/plain", "audio/x-wav", "image/jpeg"]) - output = router.run(sources=byte_streams) - assert output - assert len(output["text/plain"]) == 2 - assert len(output["audio/x-wav"]) == 1 - assert len(output["image/jpeg"]) == 1 - assert len(output.get("unclassified")) == 1 - - @pytest.mark.unit - def test_run_with_bytestreams_and_file_paths(self, preview_samples_path): - file_paths = [ - preview_samples_path / "txt" / "doc_1.txt", - preview_samples_path / "audio" / "the context for this answer is here.wav", - preview_samples_path / "txt" / "doc_2.txt", - preview_samples_path / "images" / "apple.jpg", - ] - mime_types = ["text/plain", "audio/x-wav", "text/plain", "image/jpeg"] - byte_stream_sources = [] - for path, mime_type in zip(file_paths, mime_types): - stream = ByteStream(path.read_bytes()) - stream.metadata["content_type"] = mime_type - byte_stream_sources.append(stream) - - mixed_sources = file_paths[:2] + byte_stream_sources[2:] - - router = FileTypeRouter(mime_types=["text/plain", "audio/x-wav", "image/jpeg"]) - output = router.run(sources=mixed_sources) - assert len(output["text/plain"]) == 2 - assert len(output["audio/x-wav"]) == 1 - assert len(output["image/jpeg"]) == 1 - - @pytest.mark.unit - def test_no_files(self): - """ - Test that the component runs correctly when no files are provided. - """ - router = FileTypeRouter(mime_types=["text/plain", "audio/x-wav", "image/jpeg"]) - output = router.run(sources=[]) - assert not output - - @pytest.mark.unit - def test_unlisted_extensions(self, preview_samples_path): - """ - Test that the component correctly handles files with non specified mime types. - """ - file_paths = [ - preview_samples_path / "txt" / "doc_1.txt", - preview_samples_path / "audio" / "ignored.mp3", - preview_samples_path / "audio" / "this is the content of the document.wav", - ] - router = FileTypeRouter(mime_types=["text/plain"]) - output = router.run(sources=file_paths) - assert len(output["text/plain"]) == 1 - assert "mp3" not in output - assert len(output["unclassified"]) == 2 - assert str(output["unclassified"][0]).endswith("ignored.mp3") - assert str(output["unclassified"][1]).endswith("this is the content of the document.wav") - - @pytest.mark.unit - def test_no_extension(self, preview_samples_path): - """ - Test that the component ignores files with no extension. - """ - file_paths = [ - preview_samples_path / "txt" / "doc_1.txt", - preview_samples_path / "txt" / "doc_2", - preview_samples_path / "txt" / "doc_2.txt", - ] - router = FileTypeRouter(mime_types=["text/plain"]) - output = router.run(sources=file_paths) - assert len(output["text/plain"]) == 2 - assert len(output["unclassified"]) == 1 - - @pytest.mark.unit - def test_unknown_mime_type(self): - """ - Test that the component handles files with unknown mime types. - """ - with pytest.raises(ValueError, match="Unknown mime type:"): - FileTypeRouter(mime_types=["type_invalid"]) diff --git a/test/preview/components/routers/test_metadata_router.py b/test/preview/components/routers/test_metadata_router.py deleted file mode 100644 index 5109f8db6c..0000000000 --- a/test/preview/components/routers/test_metadata_router.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from haystack.preview import Document -from haystack.preview.components.routers.metadata_router import MetadataRouter - - -class TestMetadataRouter: - @pytest.mark.unit - def test_run(self): - rules = { - "edge_1": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-01-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2023-04-01"}, - ], - }, - "edge_2": { - "operator": "AND", - "conditions": [ - {"field": "meta.created_at", "operator": ">=", "value": "2023-04-01"}, - {"field": "meta.created_at", "operator": "<", "value": "2023-07-01"}, - ], - }, - } - router = MetadataRouter(rules=rules) - documents = [ - Document(meta={"created_at": "2023-02-01"}), - Document(meta={"created_at": "2023-05-01"}), - Document(meta={"created_at": "2023-08-01"}), - ] - output = router.run(documents=documents) - assert output["edge_1"][0].meta["created_at"] == "2023-02-01" - assert output["edge_2"][0].meta["created_at"] == "2023-05-01" - assert output["unmatched"][0].meta["created_at"] == "2023-08-01" diff --git a/test/preview/components/routers/test_text_language_router.py b/test/preview/components/routers/test_text_language_router.py deleted file mode 100644 index c3e333ec2b..0000000000 --- a/test/preview/components/routers/test_text_language_router.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import pytest - -from haystack.preview import Document -from haystack.preview.components.routers import TextLanguageRouter - - -class TestTextLanguageRouter: - @pytest.mark.unit - def test_non_string_input(self): - with pytest.raises(TypeError, match="TextLanguageRouter expects a str as input."): - classifier = TextLanguageRouter() - classifier.run(text=Document(content="This is an english sentence.")) - - @pytest.mark.unit - def test_list_of_string(self): - with pytest.raises(TypeError, match="TextLanguageRouter expects a str as input."): - classifier = TextLanguageRouter() - classifier.run(text=["This is an english sentence."]) - - @pytest.mark.unit - def test_empty_string(self): - classifier = TextLanguageRouter() - result = classifier.run(text="") - assert result == {"unmatched": ""} - - @pytest.mark.unit - def test_detect_language(self): - classifier = TextLanguageRouter() - detected_language = classifier.detect_language("This is an english sentence.") - assert detected_language == "en" - - @pytest.mark.unit - def test_route_to_en(self): - classifier = TextLanguageRouter() - english_sentence = "This is an english sentence." - result = classifier.run(text=english_sentence) - assert result == {"en": english_sentence} - - @pytest.mark.unit - def test_route_to_unmatched(self): - classifier = TextLanguageRouter() - german_sentence = "Ein deutscher Satz ohne Verb." - result = classifier.run(text=german_sentence) - assert result == {"unmatched": german_sentence} - - @pytest.mark.unit - def test_warning_if_no_language_detected(self, caplog): - with caplog.at_level(logging.WARNING): - classifier = TextLanguageRouter() - classifier.run(text=".") - assert "Langdetect cannot detect the language of text: ." in caplog.text diff --git a/test/preview/components/samplers/__init__.py b/test/preview/components/samplers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/samplers/test_top_p.py b/test/preview/components/samplers/test_top_p.py deleted file mode 100644 index 74d0e269db..0000000000 --- a/test/preview/components/samplers/test_top_p.py +++ /dev/null @@ -1,89 +0,0 @@ -import random - -import pytest - -from haystack.preview import Document, ComponentError -from haystack.preview.components.samplers.top_p import TopPSampler - - -class TestTopPSampler: - @pytest.mark.unit - def test_run_scores_from_metadata(self): - """ - Test if the component runs correctly with scores already in the metadata. - """ - sampler = TopPSampler(top_p=0.95, score_field="similarity_score") - docs = [ - Document(content="Berlin", meta={"similarity_score": -10.6}), - Document(content="Belgrade", meta={"similarity_score": -8.9}), - Document(content="Sarajevo", meta={"similarity_score": -4.6}), - ] - output = sampler.run(documents=docs) - docs = output["documents"] - assert len(docs) == 1 - assert docs[0].content == "Sarajevo" - - @pytest.mark.unit - def test_run_scores(self): - """ - Test if the component runs correctly with scores in the Document score field. - """ - sampler = TopPSampler(top_p=0.99) - docs = [ - Document(content="Berlin", score=-10.6), - Document(content="Belgrade", score=-8.9), - Document(content="Sarajevo", score=-4.6), - ] - - random.shuffle(docs) - sorted_scores = sorted([doc.score for doc in docs], reverse=True) - - # top_p = 0.99 will get the top 1 document - output = sampler.run(documents=docs) - docs_filtered = output["documents"] - assert len(docs_filtered) == 1 - assert docs_filtered[0].content == "Sarajevo" - - assert [doc.score for doc in docs_filtered] == sorted_scores[:1] - - @pytest.mark.unit - def test_run_scores_top_p_1(self): - """ - Test if the component runs correctly top_p=1. - """ - sampler = TopPSampler(top_p=1.0) - docs = [ - Document(content="Berlin", score=-10.6), - Document(content="Belgrade", score=-8.9), - Document(content="Sarajevo", score=-4.6), - ] - - random.shuffle(docs) - output = sampler.run(documents=docs) - docs_filtered = output["documents"] - assert len(docs_filtered) == len(docs) - assert docs_filtered[0].content == "Sarajevo" - - assert [doc.score for doc in docs_filtered] == sorted([doc.score for doc in docs], reverse=True) - - # Returns an empty list if no documents are provided - @pytest.mark.unit - def test_returns_empty_list_if_no_documents_are_provided(self): - sampler = TopPSampler() - output = sampler.run(documents=[]) - assert output["documents"] == [] - - @pytest.mark.unit - def test_run_scores_no_metadata_present(self): - """ - Test if the component runs correctly with scores missing from the metadata yet being specified in the - score_field. - """ - sampler = TopPSampler(top_p=0.95, score_field="similarity_score") - docs = [ - Document(content="Berlin", score=-10.6), - Document(content="Belgrade", score=-8.9), - Document(content="Sarajevo", score=-4.6), - ] - with pytest.raises(ComponentError, match="Score field 'similarity_score' not found"): - sampler.run(documents=docs) diff --git a/test/preview/components/websearch/__init__.py b/test/preview/components/websearch/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/websearch/test_searchapi.py b/test/preview/components/websearch/test_searchapi.py deleted file mode 100644 index c1aef566b1..0000000000 --- a/test/preview/components/websearch/test_searchapi.py +++ /dev/null @@ -1,445 +0,0 @@ -import os -from unittest.mock import Mock, patch - -import pytest -from requests import Timeout, RequestException, HTTPError - -from haystack.preview import Document -from haystack.preview.components.websearch.searchapi import SearchApiError, SearchApiWebSearch - - -EXAMPLE_SEARCHAPI_RESPONSE = { - "search_metadata": { - "id": "search_Y16dWXw4JOrIwNjjvqoKNGlE", - "status": "Success", - "created_at": "2023-11-22T16:10:56Z", - "request_time_taken": 1.98, - "parsing_time_taken": 0.16, - "total_time_taken": 2.15, - "request_url": "https://www.google.com/search?q=Who+is+CEO+of+Microsoft%3F&oq=Who+is+CEO+of+Microsoft%3F&gl=us&hl=en&ie=UTF-8", - "html_url": "https://www.searchapi.io/api/v1/searches/search_Y16dWXw4JOrIwNjjvqoKNGlE.html", - "json_url": "https://www.searchapi.io/api/v1/searches/search_Y16dWXw4JOrIwNjjvqoKNGlE", - }, - "search_parameters": { - "engine": "google", - "q": "Who is CEO of Microsoft?", - "device": "desktop", - "google_domain": "google.com", - "hl": "en", - "gl": "us", - }, - "search_information": { - "query_displayed": "Who is CEO of Microsoft?", - "total_results": 429000000, - "time_taken_displayed": 0.48, - }, - "answer_box": { - "type": "organic_result", - "title": "Microsoft Corporation/CEO", - "answer": "Satya Nadella", - "answer_date": "Feb 4, 2014–", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Satya+Nadella&stick=H4sIAAAAAAAAAONgVuLSz9U3KDQxqMjKesRoyi3w8sc9YSmdSWtOXmNU4-IKzsgvd80rySypFJLgYoOy-KR4uJC08Sxi5Q1OLKlMVPBLTEnNyUkEALvb1RBWAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQzIcDKAB6BAgyEAE", - "snippet": "Microsoft CEO Satya Nadella speaks during the OpenAI DevDay event on November 06, 2023 in San Francisco, California.", - "date": "1 day ago", - "organic_result": { - "title": "Microsoft CEO Satya Nadella's response to the OpenAI board ...", - "link": "https://fortune.com/2023/11/21/microsoft-ceo-satya-nadella-openai-ceo-sam-altman-move-fast-fix-things/#:~:text=Microsoft%20CEO%20Satya%20Nadella%20speaks,2023%20in%20San%20Francisco%2C%20California.", - "source": "Fortune", - "domain": "fortune.com", - "displayed_link": "https://fortune.com › 2023/11/21 › microsoft-ceo-satya-...", - }, - "people_also_search_for": [ - { - "title": "Sundar Pichai", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Sundar+Pichai&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1HiArEs01OKzU20-AJSi4rz84IzU1LLEyuLF7HyBpfmpSQWKQRkJmckZgIAJfaYezgAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEAQ", - }, - { - "title": "Steve Ballmer", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Steve+Ballmer&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1ECs8yTssu0-AJSi4rz84IzU1LLEyuLF7HyBpeklqUqOCXm5OSmFgEA31ogfDYAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEAY", - }, - { - "title": "Anupama Nadella", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Anupama+Nadella&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1Hi1U_XNzRMMjPMzTHMMtHiC0gtKs7PC85MSS1PrCxexMrvmFdakJibqOCXmJKak5MIAEx0yhM9AAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEAg", - }, - { - "title": "Zain Nadella", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Zain+Nadella&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1Hi1U_XNzRMMjMyKCgsj9fiC0gtKs7PC85MSS1PrCxexMoTlZiZp-CXmJKak5MIANDRqOs6AAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEAo", - }, - { - "title": "Bill Gates", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Bill+Gates&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1ECswzN80q0-AJSi4rz84IzU1LLEyuLF7FyOWXm5Ci4J5akFgMAF5_u-TMAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEAw", - }, - { - "title": "Shantanu Narayen", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Shantanu+Narayen&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1HiArGMzC0ts5O0-AJSi4rz84IzU1LLEyuLF7EKBGck5pUk5pUq-CUWJVam5gEA2xdRszsAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEA4", - }, - { - "title": "Paul Allen", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Paul+Allen&stick=H4sIAAAAAAAAAONgFuLQz9U3MCkuM1ECs0xLsnO1-AJSi4rz84IzU1LLEyuLF7FyBSSW5ig45uSk5gEA_4-yKDMAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQxA16BAgnEBA", - }, - ], - }, - "knowledge_graph": { - "kgmid": "/m/0q40xjj", - "knowledge_graph_type": "People", - "title": "Satya Nadella", - "type": "CEO of Microsoft", - "description": "Satya Narayana Nadella is an Indian-American business executive. He is the executive chairman and CEO of Microsoft, succeeding Steve Ballmer in 2014 as CEO and John W. Thompson in 2021 as chairman.", - "source": {"name": "Wikipedia", "link": "https://en.wikipedia.org/wiki/Satya_Nadella"}, - "born": "August 19, 1967 (age 56 years), Hyderabad, India", - "born_links": [ - { - "text": "Hyderabad, India", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Hyderabad&si=ALGXSlZS0YT-iRe81F2cKC9lM9KWTK4y0m5Atx8g9YliNNw2meVELJr66A46Jmr2L7YaEMWXarsN12T-Vg9bXBeu7mCHCG-SpT-gWQmluIDs5SvdST1r6rBUhcAOclNosjy4RgkGlWnecyHsBen2Ttz-NbCqTmTwwPK9ro0lfOFPb0CUDvLAkTbBXx4xNX7WWUJ19n0EWeuA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECGUQAg", - } - ], - "awards": "Padma Bhushan, CNN-IBN Indian of the Year Global Indian", - "awards_links": [ - { - "text": "Padma Bhushan", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Padma+Bhushan&si=ALGXSlYh1-GEPndq7qMo--O-TPixQtNN4JMroSxgItz5kq0stCyOa5BGWGIYt20KbMd-zdQdvwREsU7qSkWcyv0yzHS195H46le5meMq90to5z-nIHo4evgG3koKwps5uC-gu8Huemxmq6P1usjVEj5YR9okGopoUaOxuuyZP-isnQAmC6otzjnjf1O9jMuQObZmAnl2HH7coBXCHbIx1QvAHw1KZOYyJKPnYhWaYgqfQo7yF5BOVVLXvtr_8FhnFIxxl7f_V2B6&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECF8QAg", - }, - { - "text": "CNN-IBN Indian of the Year Global Indian", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=CNN-IBN+Indian+of+the+Year+Global+Indian&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diF8YzT48GvLer52UWWyGCjf3yeWD9YQzPqUV-LEVPLmirdkrJ_7HPexciHWOKnyaMVi0vXdKPSwvc8pE4fD3qmgVyw7qAFoNmy-T-U6OlosYKKVbf9CZnaOonmPhLRRFHGEEmKVtb_0FdKkXeUE2RIDgUJ1n1LWZoTeporPHOj4JfKSJADc-hymzzDEb5-uW3KxQtTdv_GJNMOoleFxqH9cvObQvW0_NvpfHZcThW9b_9g1BXjLfozVqh6hjRTbb40p5vu5e9Oi4sNqxtACf4Xoys_QX5&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAXoECF8QAw", - }, - ], - "nominations": "CNN-IBN Indian of the Year Global Indian", - "nominations_links": [ - { - "text": "CNN-IBN Indian of the Year Global Indian", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=CNN-IBN+Indian+of+the+Year+Global+Indian&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diF8YzT48GvLer52UWWyGCjf3yeWD9YQzPqUV-LEVPLmirdkrJ_7HPexciHWOKnyaMVlh5LgokSYRM8a-Dib-kzfIaD6Uw_x_3lxo6j3NNKQbuBs4v4kkSCjL68joimLMo16eCX83PFrnvSsVKsgu6aFRoBYQt5p5NRofNfBXtVt2jzFVAWh23VsBHyyAxOuC2aQmgvKp-FGYymourIbHCdJ3rcx-Z&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECGIQAg", - } - ], - "books": "Hit Refresh: The Quest to Rediscover Microsoft's Soul and Imagine a Better Future for Everyone", - "books_links": [ - { - "text": "Hit Refresh: The Quest to Rediscover Microsoft's Soul and Imagine a Better Future for Everyone", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Hit+Refresh&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diFzM3kSV8cu0gYZuy4n6At7XJ8qKh8mnRaXfDbxUaZoS_kPW87tGFHpw6B9zAS2a52vwJDx-fkzytheyPXaMQENZSl3bwqC9Nz3bqn7-Pglqh0Bik5Ow9AdVr2XI8mdVktN4SkCIaPE4qQfjAurt8rjUVyQzu3OFQx04nfPH3Gv7vP8aKqg%3D%3D&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECGEQAg", - } - ], - "children": "Zain Nadella, Divya Nadella, Tara Nadella", - "children_links": [ - { - "text": "Zain Nadella", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Zain+Nadella&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diFxtguEvrMR1GmF2uy_-DLcVXwF5gosIuQudQPkad9bBUZxVKOG9PFdHXdEGQHrfXekG0E0x_raEKuDnHD6kk8_HfD3LZ57PWZ3Zyz0uhKPE15DfvA42IpAByWbms0fsgRw5IFCWwB5XMd3WM5U8KKsgeb_DmdoooQ_k3RrxO57jTcm5ZwgDlpBpGq0wj2Ksc2A65RQvA8NPJtpEqDcvEpJ4xWQ_tM_rHduCXRfsv9XFr84DzwA%3D%3D&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECGQQAg", - }, - { - "text": "Divya Nadella", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Divya+Nadella&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diFwYr_pFPi4_6apkHPz96V-E6wDawAGH_i6kZL7ZB-ETzV3LLESN1a8BgFguu3LOpz1qAQypmcVosQxCFWSJVexciDel34yrgWJmUu5bY2zzEmu1h95LQ35yUDkf6Mqcn-TiwyLu7OzGYkw6D9P4kNkS2D3gNPnRZb6vQJbqdayQg-wgn-LG2BmwR-RntneXFgSSZgotziGaY96UzeZ0zgRWYp6LAKlRqlTbeDeCbDDY2_VIWjQ%3D%3D&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAXoECGQQAw", - }, - { - "text": "Tara Nadella", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Tara+Nadella&si=ALGXSlZZLz93Q5j8HVkpXyxpTaoqXw8cocmoi-DFAGsSj5diF465A_RPTnaELE1D-l5XgaKmBEpoAyayrOAdoXqBSLZ8Qu5UB1hBz6xLN4I1DdUSzqN0G0e9_8lfDbD_Qnx2uLJL_3XUNJ3gPrjCNvCyYeR9a9wkCnMBLchfUhVji9EHiobO4WgdWkxKd44YXHxfMBIYEek8OfbdUx9tplETPYtu7X1HRtGzqp8lXsQ6Vacj-aT7K6Xw0psbP4NXwHRQ71MYjLS-A5_VpSnitGScPsP-1m41Kg%3D%3D&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAnoECGQQBA", - }, - ], - "education": "University of Wisconsin-Milwaukee (1990), MORE", - "education_links": [ - { - "text": "University of Wisconsin-Milwaukee", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=University+of+Wisconsin+Milwaukee&si=ALGXSlYh1-GEPndq7qMo--O-TPixQtNN4JMroSxgItz5kq0stDerMl6ZWDVLeoc_9LMxC6poOvWKyPlaxlQHHC7l9sV2e_sYZ2w92bas10emnFKqvF8PcMhCIIHCiTbdtg6nHIA-ihu0l0dNJtl3ZXuRejodvwikfjAsz-cGgFCLkxoi_eMM95SSZ77VXB0gP7fPTA6q__pIRK7T6ZfiSyM2xTbDt3YUvrWFmx5LBSJwRd2K1f0DK6sGaIa3ozdQOGvGXZkTOTLEG_a2ssbGBTX4MyU4cHmLsvW-Gfpq-makl3esSS7fQTc%3D&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQmxMoAHoECGAQAg", - }, - { - "text": "MORE", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=satya+nadella+education&stick=H4sIAAAAAAAAAOPgE-LSz9U3KDQxqMjK0pLOTrbSL0jNL8hJBVJFxfl5VqkppcmJJZn5eYtYxYsTSyoTFfISU1JzchIV4DIAcrWm-UUAAAA&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ44YBKAF6BAhgEAM", - }, - ], - "full_name": "Satya Narayana Nadella", - "profiles": [ - {"name": "LinkedIn", "link": "https://www.linkedin.com/in/satyanadella"}, - {"name": "Twitter", "link": "https://twitter.com/satyanadella"}, - ], - }, - "organic_results": [ - { - "position": 1, - "title": "Satya Nadella - Stories", - "link": "https://news.microsoft.com/exec/satya-nadella/", - "source": "Microsoft", - "domain": "news.microsoft.com", - "displayed_link": "https://news.microsoft.com › exec › satya-nadella", - "snippet": "Satya Nadella is Chairman and Chief Executive Officer of Microsoft. Before being named CEO in February 2014, Nadella held leadership roles in both ...", - "snippet_highlighted_words": ["Satya Nadella"], - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:jTiZ69Cck7EJ:https://news.microsoft.com/exec/satya-nadella/&hl=en&gl=us", - }, - { - "position": 2, - "title": "Satya Nadella", - "link": "https://en.wikipedia.org/wiki/Satya_Nadella", - "source": "Wikipedia", - "domain": "en.wikipedia.org", - "displayed_link": "https://en.wikipedia.org › wiki › Satya_Nadella", - "snippet": "Satya Narayana Nadella is an Indian-American business executive. He is the executive chairman and CEO of Microsoft, succeeding Steve Ballmer in 2014 as CEO ...", - "snippet_highlighted_words": ["Satya Narayana Nadella"], - "sitelinks": { - "inline": [ - { - "title": "Manipal Institute of Technology", - "link": "https://en.wikipedia.org/wiki/Manipal_Institute_of_Technology", - }, - { - "title": "University of Wisconsin", - "link": "https://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Milwaukee", - }, - {"title": "S. Somasegar", "link": "https://en.wikipedia.org/wiki/S._Somasegar"}, - ] - }, - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:Tgw93hG0PnoJ:https://en.wikipedia.org/wiki/Satya_Nadella&hl=en&gl=us", - }, - { - "position": 3, - "title": "Satya Nadella", - "link": "https://www.linkedin.com/in/satyanadella", - "source": "LinkedIn · Satya Nadella", - "domain": "www.linkedin.com", - "displayed_link": "10.5M+ followers", - "snippet": "As chairman and CEO of Microsoft, I define my mission and that of my company as empowering… | Learn more about Satya Nadella's work experience, education, ...", - "snippet_highlighted_words": ["Satya Nadella's"], - }, - { - "position": 4, - "title": "Who is Satya Nadella, Family, Salary, Education, Net Worth ...", - "link": "https://www.business-standard.com/about/who-is-satya-nadella", - "source": "Business Standard", - "domain": "www.business-standard.com", - "displayed_link": "https://www.business-standard.com › about › who-is-s...", - "snippet": "Satya Narayana Nadella is the chief executive officer (CEO) of Microsoft. Under him, Microsoft has more cloud computing revenue than Google, more subscribers ...", - "snippet_highlighted_words": ["Satya Narayana Nadella"], - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:yQ0bmLSmP8gJ:https://www.business-standard.com/about/who-is-satya-nadella&hl=en&gl=us", - }, - { - "position": 5, - "title": "Satya Nadella (@satyanadella) / X", - "link": "https://twitter.com/satyanadella", - "source": "Twitter · satyanadella", - "domain": "twitter.com", - "displayed_link": "3.1M+ followers", - "snippet": "Chairman and CEO of Microsoft Corporation.", - "snippet_highlighted_words": ["CEO of Microsoft"], - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:dEJiGKzwLfkJ:https://twitter.com/satyanadella&hl=en&gl=us", - }, - { - "position": 6, - "title": "Satya Nadella | Biography & Facts", - "link": "https://www.britannica.com/biography/Satya-Nadella", - "source": "Britannica", - "domain": "www.britannica.com", - "displayed_link": "https://www.britannica.com › biography › Satya-Nadella", - "snippet": "Satya Nadella (born August 19, 1967, Hyderabad, India) Indian-born business executive who was CEO of the computer software company Microsoft (2014– ).", - "snippet_highlighted_words": ["Satya Nadella"], - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:a0S8ke4I9qgJ:https://www.britannica.com/biography/Satya-Nadella&hl=en&gl=us", - }, - { - "position": 7, - "title": "Satya Nadella", - "link": "https://www.forbes.com/profile/satya-nadella/", - "source": "Forbes", - "domain": "www.forbes.com", - "displayed_link": "https://www.forbes.com › profile › satya-nadella", - "snippet": "Satya Nadella replaced billionaire Steve Ballmer as Microsoft CEO in 2014. Prior to that, Nadella was Microsoft EVP of the cloud and enterprise group.", - "snippet_highlighted_words": ["Satya Nadella"], - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:q_CXTYNnHSMJ:https://www.forbes.com/profile/satya-nadella/&hl=en&gl=us", - }, - { - "position": 8, - "title": "5 Facts You Didn't Know About Microsoft CEO Satya Nadella", - "link": "https://in.benzinga.com/content/35911756/5-facts-you-didnt-know-about-microsoft-ceo-satya-nadella", - "source": "Benzinga", - "domain": "in.benzinga.com", - "displayed_link": "https://in.benzinga.com › content › 5-facts-you-didnt-...", - "snippet": "Satya Nadella's journey at Microsoft underscores the importance of diverse experiences in shaping effective and empathetic leadership in the ...", - "snippet_highlighted_words": ["Satya Nadella's"], - "date": "8 hours ago", - "cached_page_link": "https://webcache.googleusercontent.com/search?q=cache:hCbtJUTgvEQJ:https://in.benzinga.com/content/35911756/5-facts-you-didnt-know-about-microsoft-ceo-satya-nadella&hl=en&gl=us", - }, - { - "position": 9, - "title": "Microsoft CEO Satya Nadella: Q&A - The Wall Street Journal", - "link": "https://www.wsj.com/video/microsoft-ceo-satya-nadella-qa/41D02815-935C-421D-8021-5E1BFD3DDE84", - "source": "Wall Street Journal", - "domain": "www.wsj.com", - "displayed_link": "https://www.wsj.com › video › microsoft-ceo-satya-nadel...", - "snippet": "Microsoft CEO Satya Nadella talks about his biggest accomplishment, how to make successful acquisitions and how the tech industry could improve its image ...", - "snippet_highlighted_words": ["Microsoft CEO"], - "video": {"source": "The Wall Street Journal", "channel": "The Wall Street Journal", "date": "Feb 1, 2019"}, - }, - ], - "related_questions": [ - { - "question": "Who is the real CEO of Microsoft?", - "answer": "Satya Nadella is Chairman and Chief Executive Officer of Microsoft.", - "answer_highlight": "Satya Nadella", - "source": { - "title": "Satya Nadella - Stories - Microsoft News", - "link": "https://news.microsoft.com/exec/satya-nadella/#:~:text=Satya%20Nadella%20is%20Chairman%20and%20Chief%20Executive%20Officer%20of%20Microsoft.", - "source": "Microsoft", - "domain": "news.microsoft.com", - "displayed_link": "https://news.microsoft.com › exec › satya-nadella", - }, - "search": { - "title": "Search for: Who is the real CEO of Microsoft?", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+the+real+CEO+of+Microsoft%3F&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQzmd6BAgeEAY", - }, - }, - { - "question": "Who is the CEO of Microsoft 2023?", - "answer": "Microsoft Corp. chief executive officer Satya Nadella signaled that he'd be open to Sam Altman going back to OpenAI, rather than joining his company as part of a surprise move announced over the weekend.", - "date": "2 days ago", - "source": { - "title": "Microsoft CEO Satya Nadella signals willingness to have Sam Altman ...", - "link": "https://economictimes.indiatimes.com/tech/technology/microsoft-ceo-satya-nadella-signals-willingness-to-have-sam-altman-rejoin-openai/articleshow/105370026.cms#:~:text=Microsoft%20Corp.%20chief%20executive%20officer,move%20announced%20over%20the%20weekend.", - "source": "indiatimes.com", - "domain": "economictimes.indiatimes.com", - "displayed_link": "https://economictimes.indiatimes.com › tech › articleshow", - }, - "search": { - "title": "Search for: Who is the CEO of Microsoft 2023?", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+the+CEO+of+Microsoft+2023%3F&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQzmd6BAgcEAY", - }, - }, - { - "question": "How many degrees does Satya Nadella have?", - "answer": "He earned a bachelor's degree in electrical engineering from Mangalore University, a master's degree in computer science from the University of Wisconsin – Milwaukee and a master's degree in business administration from the University of Chicago.", - "source": { - "title": "Satya Nadella - Institutional - BlackRock", - "link": "https://www.blackrock.com/institutions/en-zz/biographies/satya-nadella#:~:text=He%20earned%20a%20bachelor's%20degree,from%20the%20University%20of%20Chicago.", - "source": "blackrock.com", - "domain": "www.blackrock.com", - "displayed_link": "https://www.blackrock.com › en-zz › biographies › satya...", - }, - "search": { - "title": "Search for: How many degrees does Satya Nadella have?", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=How+many+degrees+does+Satya+Nadella+have%3F&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQzmd6BAgdEAY", - }, - }, - { - "question": "How old is Satya Nadella?", - "answer_highlight": "56 years (August 19, 1967)", - "entity": {"subject": "Satya Nadella", "attribute": "Age", "value": "56 years (August 19, 1967)"}, - "search": { - "title": "Search for: How old is Satya Nadella?", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=How+old+is+Satya+Nadella%3F&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQzmd6BAgREAY", - }, - }, - ], - "related_searches": [ - { - "query": "Who is ceo of microsoft wife", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+ceo+of+microsoft+wife&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhWEAE", - }, - { - "query": "Who is ceo of microsoft and microsoft", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+ceo+of+microsoft+and+microsoft&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhVEAE", - }, - { - "query": "Who is ceo of microsoft wikipedia", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+ceo+of+microsoft+wikipedia&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhUEAE", - }, - { - "query": "microsoft founder", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Microsoft+founder&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhSEAE", - }, - { - "query": "Who is ceo of microsoft 2020", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Who+is+ceo+of+microsoft+2020&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhTEAE", - }, - { - "query": "satya nadella net worth", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=Satya+Nadella+net+worth&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhREAE", - }, - { - "query": "ceo of microsoft salary", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=CEO+of+Microsoft+salary&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhQEAE", - }, - { - "query": "ceo of apple", - "link": "https://www.google.com/search?sca_esv=584620230&gl=us&hl=en&q=CEO+of+Apple&sa=X&ved=2ahUKEwi89re3_9eCAxU4IUQIHfHeB6MQ1QJ6BAhXEAE", - }, - ], -} - - -@pytest.fixture -def mock_searchapi_search_result(): - with patch("haystack.preview.components.websearch.searchapi.requests.get") as mock_get: - mock_get.return_value = Mock(status_code=200, json=lambda: EXAMPLE_SEARCHAPI_RESPONSE) - yield mock_get - - -class TestSearchApiSearchAPI: - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - monkeypatch.delenv("SEARCHAPI_API_KEY", raising=False) - with pytest.raises(ValueError, match="SearchApiWebSearch expects an API key"): - SearchApiWebSearch() - - @pytest.mark.unit - def test_to_dict(self): - component = SearchApiWebSearch( - api_key="api_key", top_k=10, allowed_domains=["testdomain.com"], search_params={"param": "test params"} - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.websearch.searchapi.SearchApiWebSearch", - "init_parameters": { - "top_k": 10, - "allowed_domains": ["testdomain.com"], - "search_params": {"param": "test params"}, - }, - } - - @pytest.mark.unit - @pytest.mark.parametrize("top_k", [1, 5, 7]) - def test_web_search_top_k(self, mock_searchapi_search_result, top_k: int): - ws = SearchApiWebSearch(api_key="api_key", top_k=top_k) - results = ws.run(query="Who is CEO of Microsoft?") - documents = results["documents"] - links = results["links"] - assert len(documents) == len(links) == top_k - assert all(isinstance(doc, Document) for doc in documents) - assert all(isinstance(link, str) for link in links) - assert all(link.startswith("http") for link in links) - - @pytest.mark.unit - @patch("requests.get") - def test_timeout_error(self, mock_get): - mock_get.side_effect = Timeout - ws = SearchApiWebSearch(api_key="api_key") - - with pytest.raises(TimeoutError): - ws.run(query="Who is CEO of Microsoft?") - - @pytest.mark.unit - @patch("requests.get") - def test_request_exception(self, mock_get): - mock_get.side_effect = RequestException - ws = SearchApiWebSearch(api_key="api_key") - - with pytest.raises(SearchApiError): - ws.run(query="Who is CEO of Microsoft?") - - @pytest.mark.unit - @patch("requests.get") - def test_bad_response_code(self, mock_get): - mock_response = mock_get.return_value - mock_response.status_code = 404 - mock_response.raise_for_status.side_effect = HTTPError - ws = SearchApiWebSearch(api_key="api_key") - - with pytest.raises(SearchApiError): - ws.run(query="Who is CEO of Microsoft?") - - @pytest.mark.skipif( - not os.environ.get("SEARCHAPI_API_KEY", None), - reason="Export an env var called SEARCHAPI_API_KEY containing the SearchApi API key to run this test.", - ) - @pytest.mark.integration - def test_web_search(self): - ws = SearchApiWebSearch(api_key=os.environ.get("SEARCHAPI_API_KEY", None), top_k=10) - results = ws.run(query="Who is CEO of Microsoft?") - documents = results["documents"] - links = results["links"] - assert len(documents) == len(links) == 10 - assert all(isinstance(doc, Document) for doc in results) - assert all(isinstance(link, str) for link in links) - assert all(link.startswith("http") for link in links) diff --git a/test/preview/components/websearch/test_serperdev.py b/test/preview/components/websearch/test_serperdev.py deleted file mode 100644 index 48d8619d21..0000000000 --- a/test/preview/components/websearch/test_serperdev.py +++ /dev/null @@ -1,182 +0,0 @@ -import os -from unittest.mock import Mock, patch - -import pytest -from requests import Timeout, RequestException, HTTPError - -from haystack.preview import Document -from haystack.preview.components.websearch.serper_dev import SerperDevWebSearch, SerperDevError - - -EXAMPLE_SERPERDEV_RESPONSE = { - "searchParameters": { - "q": "Who is the boyfriend of Olivia Wilde?", - "gl": "us", - "hl": "en", - "autocorrect": True, - "type": "search", - }, - "organic": [ - { - "title": "Olivia Wilde embraces Jason Sudeikis amid custody battle, Harry Styles split - Page Six", - "link": "https://pagesix.com/2023/01/29/olivia-wilde-hugs-it-out-with-jason-sudeikis-after-harry-styles-split/", - "snippet": "Looks like Olivia Wilde and Jason Sudeikis are starting 2023 on good terms. Amid their highly publicized custody battle – and the actress' ...", - "date": "Jan 29, 2023", - "position": 1, - }, - { - "title": "Olivia Wilde Is 'Quietly Dating' Again Following Harry Styles Split: 'He Makes Her Happy'", - "link": "https://www.yahoo.com/now/olivia-wilde-quietly-dating-again-183844364.html", - "snippet": "Olivia Wilde is “quietly dating again” following her November 2022 split from Harry Styles, a source exclusively tells Life & Style.", - "date": "Feb 10, 2023", - "position": 2, - }, - { - "title": "Olivia Wilde and Harry Styles' Relationship Timeline: The Way They Were - Us Weekly", - "link": "https://www.usmagazine.com/celebrity-news/pictures/olivia-wilde-and-harry-styles-relationship-timeline/", - "snippet": "Olivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.", - "date": "Mar 10, 2023", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSgTcalNFvptTbYBiDXX55s8yCGfn6F1qbed9DAN16LvynTr9GayK5SPmY&s", - "position": 3, - }, - { - "title": "Olivia Wilde Is 'Ready to Date Again' After Harry Styles Split - Us Weekly", - "link": "https://www.usmagazine.com/celebrity-news/news/olivia-wilde-is-ready-to-date-again-after-harry-styles-split/", - "snippet": "Ready for love! Olivia Wilde is officially back on the dating scene following her split from her ex-boyfriend, Harry Styles.", - "date": "Mar 1, 2023", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRCRAeRy5sVE631ZctzbzuOF70xkIOHaTvh2K7dYvdiVBwALiKrIjpscok&s", - "position": 4, - }, - { - "title": "Harry Styles and Olivia Wilde's Definitive Relationship Timeline - Harper's Bazaar", - "link": "https://www.harpersbazaar.com/celebrity/latest/a35172115/harry-styles-olivia-wilde-relationship-timeline/", - "snippet": "November 2020: News breaks about Olivia splitting from fiancé Jason Sudeikis. ... In mid-November, news breaks of Olivia Wilde's split from Jason ...", - "date": "Feb 23, 2023", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRRqw3fvZOIGHEepxCc7yFAWYsS_v_1H6X-4nxyFJxdfRuFQw_BrI6JVzI&s", - "position": 5, - }, - { - "title": "Harry Styles and Olivia Wilde's Relationship Timeline - People", - "link": "https://people.com/music/harry-styles-olivia-wilde-relationship-timeline/", - "snippet": "Harry Styles and Olivia Wilde first met on the set of Don't Worry Darling and stepped out as a couple in January 2021. Relive all their biggest relationship ...", - "position": 6, - }, - { - "title": "Jason Sudeikis and Olivia Wilde's Relationship Timeline - People", - "link": "https://people.com/movies/jason-sudeikis-olivia-wilde-relationship-timeline/", - "snippet": "Jason Sudeikis and Olivia Wilde ended their engagement of seven years in 2020. Here's a complete timeline of their relationship.", - "date": "Mar 24, 2023", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSleZoXusQyJJe2WMgIuck_cVaJ8AE0_hU2QxsXzYvKANi55UQlv82yAVI&s", - "position": 7, - }, - { - "title": "Olivia Wilde's anger at ex-boyfriend Harry Styles: She resents him and thinks he was using her | Marca", - "link": "https://www.marca.com/en/lifestyle/celebrities/2023/02/23/63f779a4e2704e8d988b4624.html", - "snippet": "The two started dating after Wilde split up with actor Jason Sudeikisin 2020. However, their relationship came to an end last November.", - "date": "Feb 23, 2023", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQBgJF2mSnIWCvPrqUqM4WTI9xPNWPyLvHuune85swpB1yE_G8cy_7KRh0&s", - "position": 8, - }, - { - "title": "Olivia Wilde's dating history: Who has the actress dated? | The US Sun", - "link": "https://www.the-sun.com/entertainment/5221040/olivia-wildes-dating-history/", - "snippet": "AMERICAN actress Olivia Wilde started dating Harry Styles in January 2021 after breaking off her engagement the year prior.", - "date": "Nov 19, 2022", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTpm8BToVFHJoH6yRggg0fLocLT9mt6lwsnRxFFDNdDGhDydzQiSKZ9__g&s", - "position": 9, - }, - ], - "relatedSearches": [ - {"query": "Harry Styles girlfriends in order"}, - {"query": "Harry Styles and Olivia Wilde engaged"}, - {"query": "Harry Styles and Olivia Wilde wedding"}, - {"query": "Who is Harry Styles married to"}, - {"query": "Jason Sudeikis Olivia Wilde relationship"}, - {"query": "Olivia Wilde and Jason Sudeikis kids"}, - {"query": "Olivia Wilde children"}, - {"query": "Harry Styles and Olivia Wilde age difference"}, - {"query": "Jason Sudeikis Olivia Wilde, Harry Styles"}, - ], -} - - -@pytest.fixture -def mock_serper_dev_search_result(): - with patch("haystack.preview.components.websearch.serper_dev.requests") as mock_run: - mock_run.post.return_value = Mock(status_code=200, json=lambda: EXAMPLE_SERPERDEV_RESPONSE) - yield mock_run - - -class TestSerperDevSearchAPI: - @pytest.mark.unit - def test_init_fail_wo_api_key(self, monkeypatch): - monkeypatch.delenv("SERPERDEV_API_KEY", raising=False) - with pytest.raises(ValueError, match="SerperDevWebSearch expects an API key"): - SerperDevWebSearch() - - @pytest.mark.unit - def test_to_dict(self): - component = SerperDevWebSearch( - api_key="test_key", top_k=10, allowed_domains=["test.com"], search_params={"param": "test"} - ) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.websearch.serper_dev.SerperDevWebSearch", - "init_parameters": {"top_k": 10, "allowed_domains": ["test.com"], "search_params": {"param": "test"}}, - } - - @pytest.mark.unit - @pytest.mark.parametrize("top_k", [1, 5, 7]) - def test_web_search_top_k(self, mock_serper_dev_search_result, top_k: int): - ws = SerperDevWebSearch(api_key="some_invalid_key", top_k=top_k) - results = ws.run(query="Who is the boyfriend of Olivia Wilde?") - documents = results["documents"] - links = results["links"] - assert len(documents) == len(links) == top_k - assert all(isinstance(doc, Document) for doc in documents) - assert all(isinstance(link, str) for link in links) - assert all(link.startswith("http") for link in links) - - @pytest.mark.unit - @patch("requests.post") - def test_timeout_error(self, mock_post): - mock_post.side_effect = Timeout - ws = SerperDevWebSearch(api_key="some_invalid_key") - - with pytest.raises(TimeoutError): - ws.run(query="Who is the boyfriend of Olivia Wilde?") - - @pytest.mark.unit - @patch("requests.post") - def test_request_exception(self, mock_post): - mock_post.side_effect = RequestException - ws = SerperDevWebSearch(api_key="some_invalid_key") - - with pytest.raises(SerperDevError): - ws.run(query="Who is the boyfriend of Olivia Wilde?") - - @pytest.mark.unit - @patch("requests.post") - def test_bad_response_code(self, mock_post): - mock_response = mock_post.return_value - mock_response.status_code = 404 - mock_response.raise_for_status.side_effect = HTTPError - ws = SerperDevWebSearch(api_key="some_invalid_key") - - with pytest.raises(SerperDevError): - ws.run(query="Who is the boyfriend of Olivia Wilde?") - - @pytest.mark.skipif( - not os.environ.get("SERPERDEV_API_KEY", None), - reason="Export an env var called SERPERDEV_API_KEY containing the SerperDev API key to run this test.", - ) - @pytest.mark.integration - def test_web_search(self): - ws = SerperDevWebSearch(api_key=os.environ.get("SERPERDEV_API_KEY", None), top_k=10) - results = ws.run(query="Who is the boyfriend of Olivia Wilde?") - documents = results["documents"] - links = results["documents"] - assert len(documents) == len(links) == 10 - assert all(isinstance(doc, Document) for doc in results) - assert all(isinstance(link, str) for link in links) - assert all(link.startswith("http") for link in links) diff --git a/test/preview/components/writers/__init__.py b/test/preview/components/writers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/preview/components/writers/test_document_writer.py b/test/preview/components/writers/test_document_writer.py deleted file mode 100644 index ed5b9a4119..0000000000 --- a/test/preview/components/writers/test_document_writer.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest - -from haystack.preview import Document, DeserializationError -from haystack.preview.testing.factory import document_store_class -from haystack.preview.components.writers.document_writer import DocumentWriter -from haystack.preview.document_stores import DuplicatePolicy -from haystack.preview.document_stores.in_memory import InMemoryDocumentStore - - -class TestDocumentWriter: - @pytest.mark.unit - def test_to_dict(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - component = DocumentWriter(document_store=mocked_docstore_class()) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.writers.document_writer.DocumentWriter", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "policy": "FAIL", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - component = DocumentWriter(document_store=mocked_docstore_class(), policy=DuplicatePolicy.SKIP) - data = component.to_dict() - assert data == { - "type": "haystack.preview.components.writers.document_writer.DocumentWriter", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "policy": "SKIP", - }, - } - - @pytest.mark.unit - def test_from_dict(self): - mocked_docstore_class = document_store_class("MockedDocumentStore") - data = { - "type": "haystack.preview.components.writers.document_writer.DocumentWriter", - "init_parameters": { - "document_store": { - "type": "haystack.preview.testing.factory.MockedDocumentStore", - "init_parameters": {}, - }, - "policy": "SKIP", - }, - } - component = DocumentWriter.from_dict(data) - assert isinstance(component.document_store, mocked_docstore_class) - assert component.policy == DuplicatePolicy.SKIP - - @pytest.mark.unit - def test_from_dict_without_docstore(self): - data = {"type": "DocumentWriter", "init_parameters": {}} - with pytest.raises(DeserializationError, match="Missing 'document_store' in serialization data"): - DocumentWriter.from_dict(data) - - @pytest.mark.unit - def test_from_dict_without_docstore_type(self): - data = {"type": "DocumentWriter", "init_parameters": {"document_store": {"init_parameters": {}}}} - with pytest.raises(DeserializationError, match="Missing 'type' in document store's serialization data"): - DocumentWriter.from_dict(data) - - @pytest.mark.unit - def test_from_dict_nonexisting_docstore(self): - data = { - "type": "DocumentWriter", - "init_parameters": {"document_store": {"type": "NonexistingDocumentStore", "init_parameters": {}}}, - } - with pytest.raises(DeserializationError, match="DocumentStore of type 'NonexistingDocumentStore' not found."): - DocumentWriter.from_dict(data) - - @pytest.mark.unit - def test_run(self): - document_store = InMemoryDocumentStore() - writer = DocumentWriter(document_store) - documents = [ - Document(content="This is the text of a document."), - Document(content="This is the text of another document."), - ] - - result = writer.run(documents=documents) - assert result["documents_written"] == 2 - - @pytest.mark.unit - def test_run_skip_policy(self): - document_store = InMemoryDocumentStore() - writer = DocumentWriter(document_store, policy=DuplicatePolicy.SKIP) - documents = [ - Document(content="This is the text of a document."), - Document(content="This is the text of another document."), - ] - - result = writer.run(documents=documents) - assert result["documents_written"] == 2 - - result = writer.run(documents=documents) - assert result["documents_written"] == 0 diff --git a/test/preview/conftest.py b/test/preview/conftest.py deleted file mode 100644 index 3a2f166120..0000000000 --- a/test/preview/conftest.py +++ /dev/null @@ -1,23 +0,0 @@ -from pathlib import Path -from unittest.mock import Mock -import pytest - -from haystack.preview.testing.test_utils import set_all_seeds - -set_all_seeds(0) - - -@pytest.fixture() -def mock_tokenizer(): - """ - Tokenizes the string by splitting on spaces. - """ - tokenizer = Mock() - tokenizer.encode = lambda text: text.split() - tokenizer.decode = lambda tokens: " ".join(tokens) - return tokenizer - - -@pytest.fixture() -def test_files_path(): - return Path(__file__).parent / "test_files" diff --git a/test/preview/dataclasses/test_byte_stream.py b/test/preview/dataclasses/test_byte_stream.py deleted file mode 100644 index 4c3f0e154c..0000000000 --- a/test/preview/dataclasses/test_byte_stream.py +++ /dev/null @@ -1,43 +0,0 @@ -import io - -from haystack.preview.dataclasses import ByteStream - -import pytest - - -@pytest.mark.unit -def test_from_file_path(tmp_path, request): - test_bytes = "Hello, world!\n".encode() - test_path = tmp_path / request.node.name - with open(test_path, "wb") as fd: - assert fd.write(test_bytes) - - b = ByteStream.from_file_path(test_path) - assert b.data == test_bytes - assert b.mime_type == None - - b = ByteStream.from_file_path(test_path, mime_type="text/plain") - assert b.data == test_bytes - assert b.mime_type == "text/plain" - - -@pytest.mark.unit -def test_from_string(): - test_string = "Hello, world!" - b = ByteStream.from_string(test_string) - assert b.data.decode() == test_string - assert b.mime_type == None - - b = ByteStream.from_string(test_string, mime_type="text/plain") - assert b.data.decode() == test_string - assert b.mime_type == "text/plain" - - -@pytest.mark.unit -def test_to_file(tmp_path, request): - test_str = "Hello, world!\n" - test_path = tmp_path / request.node.name - - ByteStream(test_str.encode()).to_file(test_path) - with open(test_path, "rb") as fd: - assert fd.read().decode() == test_str diff --git a/test/preview/dataclasses/test_chat_message.py b/test/preview/dataclasses/test_chat_message.py deleted file mode 100644 index 1c0ec71cf3..0000000000 --- a/test/preview/dataclasses/test_chat_message.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -from transformers import AutoTokenizer - -from haystack.preview.dataclasses import ChatMessage, ChatRole - - -@pytest.mark.unit -def test_from_assistant_with_valid_content(): - content = "Hello, how can I assist you?" - message = ChatMessage.from_assistant(content) - assert message.content == content - assert message.role == ChatRole.ASSISTANT - - -@pytest.mark.unit -def test_from_user_with_valid_content(): - content = "I have a question." - message = ChatMessage.from_user(content) - assert message.content == content - assert message.role == ChatRole.USER - - -@pytest.mark.unit -def test_from_system_with_valid_content(): - content = "System message." - message = ChatMessage.from_system(content) - assert message.content == content - assert message.role == ChatRole.SYSTEM - - -@pytest.mark.unit -def test_with_empty_content(): - message = ChatMessage.from_user("") - assert message.content == "" - - -@pytest.mark.unit -def test_from_function_with_empty_name(): - content = "Function call" - message = ChatMessage.from_function(content, "") - assert message.content == content - assert message.name == "" - - -@pytest.mark.integration -def test_apply_chat_templating_on_chat_message(): - messages = [ChatMessage.from_system("You are good assistant"), ChatMessage.from_user("I have a question")] - tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") - tokenized_messages = tokenizer.apply_chat_template(messages, tokenize=False) - assert tokenized_messages == "<|system|>\nYou are good assistant
\n<|user|>\nI have a question\n" - - -@pytest.mark.integration -def test_apply_custom_chat_templating_on_chat_message(): - anthropic_template = ( - "{%- for message in messages %}" - "{%- if message.role == 'user' %}\n\nHuman: {{ message.content.strip() }}" - "{%- elif message.role == 'assistant' %}\n\nAssistant: {{ message.content.strip() }}" - "{%- elif message.role == 'function' %}{{ raise('anthropic does not support function calls.') }}" - "{%- elif message.role == 'system' and loop.index == 1 %}{{ message.content }}" - "{%- else %}{{ raise('Invalid message role: ' + message.role) }}" - "{%- endif %}" - "{%- endfor %}" - "\n\nAssistant:" - ) - messages = [ChatMessage.from_system("You are good assistant"), ChatMessage.from_user("I have a question")] - # could be any tokenizer, let's use the one we already likely have in cache - tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") - tokenized_messages = tokenizer.apply_chat_template(messages, chat_template=anthropic_template, tokenize=False) - assert tokenized_messages == "You are good assistant\nHuman: I have a question\nAssistant:" diff --git a/test/preview/dataclasses/test_document.py b/test/preview/dataclasses/test_document.py deleted file mode 100644 index 593a3f449e..0000000000 --- a/test/preview/dataclasses/test_document.py +++ /dev/null @@ -1,309 +0,0 @@ -from pathlib import Path - -import pandas as pd -import pytest - -from haystack.preview import Document -from haystack.preview.dataclasses.byte_stream import ByteStream - - -@pytest.mark.unit -@pytest.mark.parametrize( - "doc,doc_str", - [ - (Document(content="test text"), "content: 'test text'"), - ( - Document(dataframe=pd.DataFrame([["John", 25], ["Martha", 34]], columns=["name", "age"])), - "dataframe: (2, 2)", - ), - (Document(blob=ByteStream(b"hello, test string")), "blob: 18 bytes"), - ( - Document( - content="test text", - dataframe=pd.DataFrame([["John", 25], ["Martha", 34]], columns=["name", "age"]), - blob=ByteStream(b"hello, test string"), - ), - "content: 'test text', dataframe: (2, 2), blob: 18 bytes", - ), - ], -) -def test_document_str(doc, doc_str): - assert f"Document(id={doc.id}, {doc_str})" == str(doc) - - -@pytest.mark.unit -def test_init(): - doc = Document() - assert doc.id == "d4675c57fcfe114db0b95f1da46eea3c5d6f5729c17d01fb5251ae19830a3455" - assert doc.content == None - assert doc.dataframe == None - assert doc.blob == None - assert doc.meta == {} - assert doc.score == None - assert doc.embedding == None - - -@pytest.mark.unit -def test_init_with_wrong_parameters(): - with pytest.raises(TypeError): - Document(text="") - - -@pytest.mark.unit -def test_init_with_parameters(): - blob_data = b"some bytes" - doc = Document( - content="test text", - dataframe=pd.DataFrame([0]), - blob=ByteStream(data=blob_data, mime_type="text/markdown"), - meta={"text": "test text"}, - score=0.812, - embedding=[0.1, 0.2, 0.3], - ) - assert doc.id == "ec92455f3f4576d40031163c89b1b4210b34ea1426ee0ff68ebed86cb7ba13f8" - assert doc.content == "test text" - assert doc.dataframe is not None - assert doc.dataframe.equals(pd.DataFrame([0])) - assert doc.blob.data == blob_data - assert doc.blob.mime_type == "text/markdown" - assert doc.meta == {"text": "test text"} - assert doc.score == 0.812 - assert doc.embedding == [0.1, 0.2, 0.3] - - -@pytest.mark.unit -def test_init_with_legacy_fields(): - doc = Document( - content="test text", content_type="text", id_hash_keys=["content"], score=0.812, embedding=[0.1, 0.2, 0.3] # type: ignore - ) - assert doc.id == "18fc2c114825872321cf5009827ca162f54d3be50ab9e9ffa027824b6ec223af" - assert doc.content == "test text" - assert doc.dataframe == None - assert doc.blob == None - assert doc.meta == {} - assert doc.score == 0.812 - assert doc.embedding == [0.1, 0.2, 0.3] - - -@pytest.mark.unit -def test_init_with_legacy_field(): - doc = Document( - content="test text", - content_type="text", # type: ignore - id_hash_keys=["content"], # type: ignore - score=0.812, - embedding=[0.1, 0.2, 0.3], - meta={"date": "10-10-2023", "type": "article"}, - ) - assert doc.id == "a2c0321b34430cc675294611e55529fceb56140ca3202f1c59a43a8cecac1f43" - assert doc.content == "test text" - assert doc.dataframe == None - assert doc.meta == {"date": "10-10-2023", "type": "article"} - assert doc.score == 0.812 - assert doc.embedding == [0.1, 0.2, 0.3] - - -@pytest.mark.unit -def test_basic_equality_type_mismatch(): - doc = Document(content="test text") - assert doc != "test text" - - -@pytest.mark.unit -def test_basic_equality_id(): - doc1 = Document(content="test text") - doc2 = Document(content="test text") - - assert doc1 == doc2 - - doc1.id = "1234" - doc2.id = "5678" - - assert doc1 != doc2 - - -@pytest.mark.unit -def test_to_dict(): - doc = Document() - assert doc.to_dict() == { - "id": doc._create_id(), - "content": None, - "dataframe": None, - "blob": None, - "score": None, - "embedding": None, - } - - -@pytest.mark.unit -def test_to_dict_without_flattening(): - doc = Document() - assert doc.to_dict(flatten=False) == { - "id": doc._create_id(), - "content": None, - "dataframe": None, - "blob": None, - "meta": {}, - "score": None, - "embedding": None, - } - - -@pytest.mark.unit -def test_to_dict_with_custom_parameters(): - doc = Document( - content="test text", - dataframe=pd.DataFrame([10, 20, 30]), - blob=ByteStream(b"some bytes", mime_type="application/pdf"), - meta={"some": "values", "test": 10}, - score=0.99, - embedding=[10.0, 10.0], - ) - - assert doc.to_dict() == { - "id": doc.id, - "content": "test text", - "dataframe": pd.DataFrame([10, 20, 30]).to_json(), - "blob": {"data": list(b"some bytes"), "mime_type": "application/pdf"}, - "some": "values", - "test": 10, - "score": 0.99, - "embedding": [10.0, 10.0], - } - - -@pytest.mark.unit -def test_to_dict_with_custom_parameters_without_flattening(): - doc = Document( - content="test text", - dataframe=pd.DataFrame([10, 20, 30]), - blob=ByteStream(b"some bytes", mime_type="application/pdf"), - meta={"some": "values", "test": 10}, - score=0.99, - embedding=[10.0, 10.0], - ) - - assert doc.to_dict(flatten=False) == { - "id": doc.id, - "content": "test text", - "dataframe": pd.DataFrame([10, 20, 30]).to_json(), - "blob": {"data": list(b"some bytes"), "mime_type": "application/pdf"}, - "meta": {"some": "values", "test": 10}, - "score": 0.99, - "embedding": [10, 10], - } - - -@pytest.mark.unit -def test_from_dict(): - assert Document.from_dict({}) == Document() - - -@pytest.mark.unit -def from_from_dict_with_parameters(): - blob_data = b"some bytes" - assert Document.from_dict( - { - "content": "test text", - "dataframe": pd.DataFrame([0]).to_json(), - "blob": {"data": list(blob_data), "mime_type": "text/markdown"}, - "meta": {"text": "test text"}, - "score": 0.812, - "embedding": [0.1, 0.2, 0.3], - } - ) == Document( - content="test text", - dataframe=pd.DataFrame([0]), - blob=ByteStream(blob_data, mime_type="text/markdown"), - meta={"text": "test text"}, - score=0.812, - embedding=[0.1, 0.2, 0.3], - ) - - -@pytest.mark.unit -def test_from_dict_with_legacy_fields(): - assert Document.from_dict( - { - "content": "test text", - "content_type": "text", - "id_hash_keys": ["content"], - "score": 0.812, - "embedding": [0.1, 0.2, 0.3], - } - ) == Document( - content="test text", content_type="text", id_hash_keys=["content"], score=0.812, embedding=[0.1, 0.2, 0.3] # type: ignore - ) - - -def test_from_dict_with_legacy_field_and_flat_meta(): - assert Document.from_dict( - { - "content": "test text", - "content_type": "text", - "id_hash_keys": ["content"], - "score": 0.812, - "embedding": [0.1, 0.2, 0.3], - "date": "10-10-2023", - "type": "article", - } - ) == Document( - content="test text", - content_type="text", # type: ignore - id_hash_keys=["content"], # type: ignore - score=0.812, - embedding=[0.1, 0.2, 0.3], - meta={"date": "10-10-2023", "type": "article"}, - ) - - -@pytest.mark.unit -def test_from_dict_with_flat_meta(): - blob_data = b"some bytes" - assert Document.from_dict( - { - "content": "test text", - "dataframe": pd.DataFrame([0]).to_json(), - "blob": {"data": list(blob_data), "mime_type": "text/markdown"}, - "score": 0.812, - "embedding": [0.1, 0.2, 0.3], - "date": "10-10-2023", - "type": "article", - } - ) == Document( - content="test text", - dataframe=pd.DataFrame([0]), - blob=ByteStream(blob_data, mime_type="text/markdown"), - score=0.812, - embedding=[0.1, 0.2, 0.3], - meta={"date": "10-10-2023", "type": "article"}, - ) - - -@pytest.mark.unit -def test_from_dict_with_flat_and_non_flat_meta(): - with pytest.raises(ValueError, match="Pass either the 'meta' parameter or flattened metadata keys"): - Document.from_dict( - { - "content": "test text", - "dataframe": pd.DataFrame([0]).to_json(), - "blob": {"data": list(b"some bytes"), "mime_type": "text/markdown"}, - "score": 0.812, - "meta": {"test": 10}, - "embedding": [0.1, 0.2, 0.3], - "date": "10-10-2023", - "type": "article", - } - ) - - -@pytest.mark.unit -def test_content_type(): - assert Document(content="text").content_type == "text" - assert Document(dataframe=pd.DataFrame([0])).content_type == "table" - - with pytest.raises(ValueError): - _ = Document().content_type - - with pytest.raises(ValueError): - _ = Document(content="text", dataframe=pd.DataFrame([0])).content_type diff --git a/test/preview/dataclasses/test_streaming_chunk.py b/test/preview/dataclasses/test_streaming_chunk.py deleted file mode 100644 index 1a4c99ccc3..0000000000 --- a/test/preview/dataclasses/test_streaming_chunk.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -from haystack.preview.dataclasses import StreamingChunk - - -@pytest.mark.unit -def test_create_chunk_with_content_and_metadata(): - chunk = StreamingChunk(content="Test content", metadata={"key": "value"}) - - assert chunk.content == "Test content" - assert chunk.metadata == {"key": "value"} - - -@pytest.mark.unit -def test_create_chunk_with_only_content(): - chunk = StreamingChunk(content="Test content") - - assert chunk.content == "Test content" - assert chunk.metadata == {} - - -@pytest.mark.unit -def test_access_content(): - chunk = StreamingChunk(content="Test content", metadata={"key": "value"}) - assert chunk.content == "Test content" - - -@pytest.mark.unit -def test_create_chunk_with_empty_content(): - chunk = StreamingChunk(content="") - assert chunk.content == "" - assert chunk.metadata == {} diff --git a/test/preview/document_stores/test_in_memory.py b/test/preview/document_stores/test_in_memory.py deleted file mode 100644 index ce37754338..0000000000 --- a/test/preview/document_stores/test_in_memory.py +++ /dev/null @@ -1,399 +0,0 @@ -import logging -from unittest.mock import patch - -import pandas as pd -import pytest - -from haystack.preview import Document -from haystack.preview.document_stores import InMemoryDocumentStore, DocumentStoreError, DuplicatePolicy - - -from haystack.preview.testing.document_store import DocumentStoreBaseTests - - -class TestMemoryDocumentStore(DocumentStoreBaseTests): # pylint: disable=R0904 - """ - Test InMemoryDocumentStore's specific features - """ - - @pytest.fixture - def document_store(self) -> InMemoryDocumentStore: - return InMemoryDocumentStore() - - @pytest.mark.unit - def test_to_dict(self): - store = InMemoryDocumentStore() - data = store.to_dict() - assert data == { - "type": "haystack.preview.document_stores.in_memory.document_store.InMemoryDocumentStore", - "init_parameters": { - "bm25_tokenization_regex": r"(?u)\b\w\w+\b", - "bm25_algorithm": "BM25Okapi", - "bm25_parameters": {}, - "embedding_similarity_function": "dot_product", - }, - } - - @pytest.mark.unit - def test_to_dict_with_custom_init_parameters(self): - store = InMemoryDocumentStore( - bm25_tokenization_regex="custom_regex", - bm25_algorithm="BM25Plus", - bm25_parameters={"key": "value"}, - embedding_similarity_function="cosine", - ) - data = store.to_dict() - assert data == { - "type": "haystack.preview.document_stores.in_memory.document_store.InMemoryDocumentStore", - "init_parameters": { - "bm25_tokenization_regex": "custom_regex", - "bm25_algorithm": "BM25Plus", - "bm25_parameters": {"key": "value"}, - "embedding_similarity_function": "cosine", - }, - } - - @pytest.mark.unit - @patch("haystack.preview.document_stores.in_memory.document_store.re") - def test_from_dict(self, mock_regex): - data = { - "type": "haystack.preview.document_stores.in_memory.document_store.InMemoryDocumentStore", - "init_parameters": { - "bm25_tokenization_regex": "custom_regex", - "bm25_algorithm": "BM25Plus", - "bm25_parameters": {"key": "value"}, - }, - } - store = InMemoryDocumentStore.from_dict(data) - mock_regex.compile.assert_called_with("custom_regex") - assert store.tokenizer - assert store.bm25_algorithm.__name__ == "BM25Plus" - assert store.bm25_parameters == {"key": "value"} - - @pytest.mark.unit - def test_written_documents_count(self, document_store: InMemoryDocumentStore): - # FIXME Remove after the document store base tests have been rewritten - documents = [Document(content=f"Hello world #{i}") for i in range(10)] - docs_written = document_store.write_documents(documents[0:2]) - assert docs_written == 2 - assert document_store.filter_documents() == documents[0:2] - - docs_written = document_store.write_documents(documents, DuplicatePolicy.SKIP) - assert docs_written == len(documents) - 2 - assert document_store.filter_documents() == documents - - @pytest.mark.unit - def test_bm25_retrieval(self, document_store: InMemoryDocumentStore): - document_store = InMemoryDocumentStore() - # Tests if the bm25_retrieval method returns the correct document based on the input query. - docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="What languages?", top_k=1) - assert len(results) == 1 - assert results[0].content == "Haystack supports multiple languages" - - @pytest.mark.unit - def test_bm25_retrieval_with_empty_document_store(self, document_store: InMemoryDocumentStore, caplog): - caplog.set_level(logging.INFO) - # Tests if the bm25_retrieval method correctly returns an empty list when there are no documents in the DocumentStore. - results = document_store.bm25_retrieval(query="How to test this?", top_k=2) - assert len(results) == 0 - assert "No documents found for BM25 retrieval. Returning empty list." in caplog.text - - @pytest.mark.unit - def test_bm25_retrieval_empty_query(self, document_store: InMemoryDocumentStore): - # Tests if the bm25_retrieval method returns a document when the query is an empty string. - docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")] - document_store.write_documents(docs) - with pytest.raises(ValueError, match="Query should be a non-empty string"): - document_store.bm25_retrieval(query="", top_k=1) - - @pytest.mark.unit - def test_bm25_retrieval_with_different_top_k(self, document_store: InMemoryDocumentStore): - # Tests if the bm25_retrieval method correctly changes the number of returned documents - # based on the top_k parameter. - docs = [ - Document(content="Hello world"), - Document(content="Haystack supports multiple languages"), - Document(content="Python is a popular programming language"), - ] - document_store.write_documents(docs) - - # top_k = 2 - results = document_store.bm25_retrieval(query="languages", top_k=2) - assert len(results) == 2 - - # top_k = 3 - results = document_store.bm25_retrieval(query="languages", top_k=3) - assert len(results) == 3 - - # Test two queries and make sure the results are different - @pytest.mark.unit - def test_bm25_retrieval_with_two_queries(self, document_store: InMemoryDocumentStore): - # Tests if the bm25_retrieval method returns different documents for different queries. - docs = [ - Document(content="Javascript is a popular programming language"), - Document(content="Java is a popular programming language"), - Document(content="Python is a popular programming language"), - Document(content="Ruby is a popular programming language"), - Document(content="PHP is a popular programming language"), - ] - document_store.write_documents(docs) - - results = document_store.bm25_retrieval(query="Java", top_k=1) - assert results[0].content == "Java is a popular programming language" - - results = document_store.bm25_retrieval(query="Python", top_k=1) - assert results[0].content == "Python is a popular programming language" - - # Test a query, add a new document and make sure results are appropriately updated - @pytest.mark.unit - def test_bm25_retrieval_with_updated_docs(self, document_store: InMemoryDocumentStore): - # Tests if the bm25_retrieval method correctly updates the retrieved documents when new - # documents are added to the DocumentStore. - docs = [Document(content="Hello world")] - document_store.write_documents(docs) - - results = document_store.bm25_retrieval(query="Python", top_k=1) - assert len(results) == 1 - - document_store.write_documents([Document(content="Python is a popular programming language")]) - results = document_store.bm25_retrieval(query="Python", top_k=1) - assert len(results) == 1 - assert results[0].content == "Python is a popular programming language" - - document_store.write_documents([Document(content="Java is a popular programming language")]) - results = document_store.bm25_retrieval(query="Python", top_k=1) - assert len(results) == 1 - assert results[0].content == "Python is a popular programming language" - - @pytest.mark.unit - def test_bm25_retrieval_with_scale_score(self, document_store: InMemoryDocumentStore): - docs = [Document(content="Python programming"), Document(content="Java programming")] - document_store.write_documents(docs) - - results1 = document_store.bm25_retrieval(query="Python", top_k=1, scale_score=True) - # Confirm that score is scaled between 0 and 1 - assert results1[0].score is not None - assert 0.0 <= results1[0].score <= 1.0 - - # Same query, different scale, scores differ when not scaled - results = document_store.bm25_retrieval(query="Python", top_k=1, scale_score=False) - assert results[0].score != results1[0].score - - @pytest.mark.unit - def test_bm25_retrieval_with_table_content(self, document_store: InMemoryDocumentStore): - # Tests if the bm25_retrieval method correctly returns a dataframe when the content_type is table. - table_content = pd.DataFrame({"language": ["Python", "Java"], "use": ["Data Science", "Web Development"]}) - docs = [Document(dataframe=table_content), Document(content="Gardening"), Document(content="Bird watching")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Java", top_k=1) - assert len(results) == 1 - - df = results[0].dataframe - assert isinstance(df, pd.DataFrame) - assert df.equals(table_content) - - @pytest.mark.unit - def test_bm25_retrieval_with_text_and_table_content(self, document_store: InMemoryDocumentStore, caplog): - table_content = pd.DataFrame({"language": ["Python", "Java"], "use": ["Data Science", "Web Development"]}) - document = Document(content="Gardening", dataframe=table_content) - docs = [ - document, - Document(content="Python"), - Document(content="Bird Watching"), - Document(content="Gardening"), - Document(content="Java"), - ] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Gardening", top_k=2) - assert document.id in [d.id for d in results] - assert "both text and dataframe content" in caplog.text - results = document_store.bm25_retrieval(query="Python", top_k=2) - assert document.id not in [d.id for d in results] - - @pytest.mark.unit - def test_bm25_retrieval_default_filter_for_text_and_dataframes(self, document_store: InMemoryDocumentStore): - docs = [Document(), Document(content="Gardening"), Document(content="Bird watching")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="doesn't matter, top_k is 10", top_k=10) - assert len(results) == 2 - - @pytest.mark.unit - def test_bm25_retrieval_with_filters(self, document_store: InMemoryDocumentStore): - selected_document = Document(content="Gardening", meta={"selected": True}) - docs = [Document(), selected_document, Document(content="Bird watching")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"selected": True}) - assert len(results) == 1 - assert results[0].id == selected_document.id - - @pytest.mark.unit - def test_bm25_retrieval_with_filters_keeps_default_filters(self, document_store: InMemoryDocumentStore): - docs = [Document(meta={"selected": True}), Document(content="Gardening"), Document(content="Bird watching")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"selected": True}) - assert len(results) == 0 - - @pytest.mark.unit - def test_bm25_retrieval_with_filters_on_text_or_dataframe(self, document_store: InMemoryDocumentStore): - document = Document(dataframe=pd.DataFrame({"language": ["Python", "Java"], "use": ["Data Science", "Web"]})) - docs = [Document(), Document(content="Gardening"), Document(content="Bird watching"), document] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"content": None}) - assert len(results) == 1 - assert results[0].id == document.id - - @pytest.mark.unit - def test_bm25_retrieval_with_documents_with_mixed_content(self, document_store: InMemoryDocumentStore): - double_document = Document(content="Gardening", embedding=[1.0, 2.0, 3.0]) - docs = [Document(embedding=[1.0, 2.0, 3.0]), double_document, Document(content="Bird watching")] - document_store.write_documents(docs) - results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"embedding": {"$not": None}}) - assert len(results) == 1 - assert results[0].id == double_document.id - - @pytest.mark.unit - def test_embedding_retrieval(self): - docstore = InMemoryDocumentStore(embedding_similarity_function="cosine") - # Tests if the embedding retrieval method returns the correct document based on the input query embedding. - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages", embedding=[1.0, 1.0, 1.0, 1.0]), - ] - docstore.write_documents(docs) - results = docstore.embedding_retrieval( - query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=1, filters={}, scale_score=False - ) - assert len(results) == 1 - assert results[0].content == "Haystack supports multiple languages" - - @pytest.mark.unit - def test_embedding_retrieval_invalid_query(self): - docstore = InMemoryDocumentStore() - with pytest.raises(ValueError, match="query_embedding should be a non-empty list of floats"): - docstore.embedding_retrieval(query_embedding=[]) - with pytest.raises(ValueError, match="query_embedding should be a non-empty list of floats"): - docstore.embedding_retrieval(query_embedding=["invalid", "list", "of", "strings"]) # type: ignore - - @pytest.mark.unit - def test_embedding_retrieval_no_embeddings(self, caplog): - caplog.set_level(logging.WARNING) - docstore = InMemoryDocumentStore() - docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")] - docstore.write_documents(docs) - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1]) - assert len(results) == 0 - assert "No Documents found with embeddings. Returning empty list." in caplog.text - - @pytest.mark.unit - def test_embedding_retrieval_some_documents_wo_embeddings(self, caplog): - caplog.set_level(logging.INFO) - docstore = InMemoryDocumentStore() - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages"), - ] - docstore.write_documents(docs) - docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1]) - assert "Skipping some Documents that don't have an embedding." in caplog.text - - @pytest.mark.unit - def test_embedding_retrieval_documents_different_embedding_sizes(self): - docstore = InMemoryDocumentStore() - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages", embedding=[1.0, 1.0]), - ] - docstore.write_documents(docs) - - with pytest.raises(DocumentStoreError, match="The embedding size of all Documents should be the same."): - docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1]) - - @pytest.mark.unit - def test_embedding_retrieval_query_documents_different_embedding_sizes(self): - docstore = InMemoryDocumentStore() - docs = [Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4])] - docstore.write_documents(docs) - - with pytest.raises( - DocumentStoreError, - match="The embedding size of the query should be the same as the embedding size of the Documents.", - ): - docstore.embedding_retrieval(query_embedding=[0.1, 0.1]) - - @pytest.mark.unit - def test_embedding_retrieval_with_different_top_k(self): - docstore = InMemoryDocumentStore() - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages", embedding=[1.0, 1.0, 1.0, 1.0]), - Document(content="Python is a popular programming language", embedding=[0.5, 0.5, 0.5, 0.5]), - ] - docstore.write_documents(docs) - - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=2) - assert len(results) == 2 - - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=3) - assert len(results) == 3 - - @pytest.mark.unit - def test_embedding_retrieval_with_scale_score(self): - docstore = InMemoryDocumentStore() - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages", embedding=[1.0, 1.0, 1.0, 1.0]), - Document(content="Python is a popular programming language", embedding=[0.5, 0.5, 0.5, 0.5]), - ] - docstore.write_documents(docs) - - results1 = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=1, scale_score=True) - # Confirm that score is scaled between 0 and 1 - assert results1[0].score is not None - assert 0.0 <= results1[0].score <= 1.0 - - # Same query, different scale, scores differ when not scaled - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=1, scale_score=False) - assert results[0].score != results1[0].score - - @pytest.mark.unit - def test_embedding_retrieval_return_embedding(self): - docstore = InMemoryDocumentStore(embedding_similarity_function="cosine") - docs = [ - Document(content="Hello world", embedding=[0.1, 0.2, 0.3, 0.4]), - Document(content="Haystack supports multiple languages", embedding=[1.0, 1.0, 1.0, 1.0]), - ] - docstore.write_documents(docs) - - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=1, return_embedding=False) - assert results[0].embedding is None - - results = docstore.embedding_retrieval(query_embedding=[0.1, 0.1, 0.1, 0.1], top_k=1, return_embedding=True) - assert results[0].embedding == [1.0, 1.0, 1.0, 1.0] - - @pytest.mark.unit - def test_compute_cosine_similarity_scores(self): - docstore = InMemoryDocumentStore(embedding_similarity_function="cosine") - docs = [ - Document(content="Document 1", embedding=[1.0, 0.0, 0.0, 0.0]), - Document(content="Document 2", embedding=[1.0, 1.0, 1.0, 1.0]), - ] - - scores = docstore._compute_query_embedding_similarity_scores( - embedding=[0.1, 0.1, 0.1, 0.1], documents=docs, scale_score=False - ) - assert scores == [0.5, 1.0] - - @pytest.mark.unit - def test_compute_dot_product_similarity_scores(self): - docstore = InMemoryDocumentStore(embedding_similarity_function="dot_product") - docs = [ - Document(content="Document 1", embedding=[1.0, 0.0, 0.0, 0.0]), - Document(content="Document 2", embedding=[1.0, 1.0, 1.0, 1.0]), - ] - - scores = docstore._compute_query_embedding_similarity_scores( - embedding=[0.1, 0.1, 0.1, 0.1], documents=docs, scale_score=False - ) - assert scores == [0.1, 0.4] diff --git a/test/preview/test_files/audio/answer.wav b/test/preview/test_files/audio/answer.wav deleted file mode 100644 index 874eab01fede4e6b7fe24270d70ff85a87f5caa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29228 zcmX6_1$-38+n$--J&PwKKyY{0qQ%|a3&q{twP}F zg;bOX_20w9-%MrUi{lvr|5Ch{@xN>S|MLvuBoQPWzl$OrAP9c?~zNl}wm#_H#L4Cr%-|-0Gy`*yf?+Tv(T_F}%N<-3;SlpW% zR}3c}tc>9fL%1XIzu!6jcWo#BnjVh~BsHF6;9dWFej5B7Mt-VG>IB{;AL&WDldR;F z`b%w7j5H>l$?v%GGxaZaB?hmKCjoT_&pn{7<2R{EZru5OJa>UQs)8yl$%X5lQ|r|l zby7W7@6~Jd3Oi(BJxA0?HAOv8-N*}apR^$BRXNoSYq>^>&}jM>d7(zDq3R!{;~lG$ z-$)@6NldIl$NkhNgUB#agQUY9)F9nRQ>^&Bny7lK)i|>vcz$}^#cb6=wO1Qd1Zha> zV9ndq1ho=-Rvv%PhWiSt47gGk(wt-_Pt{&L^AzqZBYyg#rl~S2o2sjJW2fKXFD+CJ z)l>~sE7TdR_oAAiN~$odU_YtADr!Zv;w*`j$0{n)!mKkpO2?4WsJE{dFTx;!rL zOTWyg4ywVV6g^I#(W~?%Jx|}zaOPrrsUQW(DD_j$lFemdnN_BjsbmS+M_!S6)l~ID zC6Z2LHF-!Bq4XVDL;fUXai%x1nzp#h*K(yCB74et^1Upq{!sm}4?Wa)oPdUXt4i{c zAL^M(AnnO^Tw?|-?;mwuEhh76Jyx6br@hE4m6iM=ljtD&0~RnwWmWI+-Y4WG`AQy? z73FjBOKg=xVb8@#4pNCMC((2=y-dH+)T|JTX8UL!`he^wTgh~s$x@Y8U6beKKDkhi zlD*|(`AL>ggVj3h@n_YXoFEqYOs3%so~Tt=UoX`TUt`rG?DHnz+B@TyB=Kn%RHw z@#2+Ss&1*bYCmi#n)D+xNgmRIwe%cI6_q$TVS7ZhbB!KpWOU^Y9}qFqJ&JAQe}txa z6O->Im++nPSMmMsec)Rg+-fzH8)+>2s5-0bY_s#QXHZx|SS@#Z;~E(tJUk z=@3pkowdOd_8@UhCaK?OGp(>=wQGwzw|k7UkrA%-p*iSq+Mm86eN=r>(C%Zt4E<#e zwFcM~_%psrRDxZuSM$|9xk+^8-1gaVd>CKE+wi^iR7)|oisx3$cSALX(P&pU2?o73H+it%gkwIvzOVetURIff%?Ju z<^V3_A~KVnWMlMBjz`XAt}(78M=?EtU8e)tzwCGRg1nM5`9o`z^~@@6AF+?{zeIO& zi|@b+_wZ!xxc&fygOFJd*W>|`&jd+da;NqPfS*_`6Nm;5fp^#AT(;9ck)?Hdue zXGWfv3(LQaR)TUM|`=5BMb<+tD4H>~aEQnRq#Ph3_bX?IpY zU+Ea+I_v7{++_6EHj+E=nofwB32KPEB1Z7Gb~C#H-zHAWwW=2)a9cG)9v9tsD~pA4 z1Pc0#`w#f11||iYg({d?EZdsN=gA(VIvcC?(b{XtOk;~^OL9QvS9AC`;dVAp)iPtl z^j%|Jp6qnuw?5jl>^NK5ujOreN-LsA>I00{&Ma=j^W2rqxla>v z1uwzth?z1YqJ3s@!v4*EY!4AxkqiIvmDM!u$%Lz9!ceLeW4>d)&bQ&XxZUHeftX{-0Al~>E^@;cm_LtEk88s0T3 zSCl{epsNIn=Q4OI@FZBm+RjUe-}znZH>)9kp=xM3j4ygG-DAviEOf@ZqMTi{@~SqU zz*8ePEKzM_E)mYt*uCvT{ENIw9?_-DMRUkc_AG0HHQRIsd;2b?^hycy`n<(_gMGvN zKLdZ5_w5bhfqD;XsEQN&oh8t(Dh5?SGU|govadCspLBLeGcJ4mY&+7njw<3Q5>`06 zc$x$03MFI4E$u-}cd+GRRgTkF~t{x~8r z;+JQIVS2uD5^+2kD{B+FJ^FG1kYQbw3_`|NZm%Fy9-EGGly%g$8GkybyJooax!M>z$U8fy{Tp8) zGf^j7NMqDM{)e5OpB9bO5_%ieR86A9TOP@KS^Yya@Wk6YSoU81?kXPHI56hp;1ev5bJ3+-!G0dhAceRfu0W}fZo?#F(PtP!~& zesW@;#5JkeuPTQ}o?UcePOHDQ*25_MmnX@w=P`&8^>@|QN>Z+u(xHT(Am$*%+TMN2J%LA5CMReR(zA}UAz z6*)GdockavV`m6_@lEol4Q>lD(+c@RRjj!}BC6-0d6>s|?R?~>VK+USTv_y1>M&o( zD~r!UO0Rq%7mBZT89tQn5!uOIs#qB|i8NC&G7s9Re*!hVj^zGHpMNe*^8cFXedufN zALB0-%w@e525q42(yD5HmY3~7)%sDLlbPf!p4rM7%4p?u)yR+~-{t(#ndisuh|t64 zMK?}sW=O~=#QGlRB}~vCwrc{M>$5& z!D5Q2A&)5^s`JNkHZN|c#g5*jo=(k#5YksqS7W1L%p|7-sES>hsaC>ft z_lasBdnaaG_&}|${Uwkpureq^2h0}c@X(cz2u-v0+qZZ{HJ{ehlMRn^qdVxy=c(lg zr}N}yzJXss`TGV6~9#bHuYQoRS)ZIpzD2>Kkh{z~{%qbU zDHVKs{d%Cd-|`;wKJ?}coHOt9#%ekVkq6`xy8gm+2|YtgliT8i=qtL4wS0ipm>!OH zW;&g_XAW=r_|(UvIz+#XKb2TH56k)BB5Ua03*hA6^w@@o9E-Nf#Ko-5p1XpKOxvI_P3M^ai# zF}Apucr@1sEj#Y13GK($YR$CKY#}Koqj?3Zr1{Z2jeah>_+;m@^V;p~b9PC)u~{-W z$Cu6*?wjVl=e^)HeVGGAgP(&3Lw&5DyfRtGMzLJ16P2VljYcJxMJ<#igl26sCusfS z&*pfQw_7%mdQ)^{cqpQ0{F?NevMfq}AhwEgVkrNQ)1MoEEt0hU=ZL&Q{m#ae34u};|^_c*9B%zj}%erni1szo~Ua%sI-d+T5CD@3k-6f7?CnMLdrvAcA~~ zUDC=JV!^(_#86&ydgy*&pg-9g^zQI)4-U4xe6IW{_n^Y7r931r{YjIEt$Y%ZO)NLB znYWD!aYeJ{%6TiptTY*8cSoH_RW8lyj5#xnP17UxU#Fih3H+U0@z=ZL9N!Cld;aC& z*Oy6OQ+5SLgjk?Y;5XBdJG4g5VxFwwb3CP-YqivDJ{!rp(i^Inq~a?dF6WWMWSE?9 zMTL68o))Ux#sYV&C#@qyuA$|eYp zpJlBH-3Vk4v<@5yt_r;gT?y_Eqz+6DED!cFPuNthm;K~taa@#>PvmaWm*%DGNPhKH zW)NGD7XB?QmPU@V|g&lv$VCZ(hAIO?g_verA z`M$UQnUp*%C9OY???g&BU#DOm{*F}BcQ|8R9UP1GM6He9-l%I#(<`!_qym`*!W~GqyUO=#6L@bxZb8Ey-G1jNL#+C@CNEhqjw<;fHv7e%yLtRMg6`n(;K`sXbS8Ac%wzv!Kes#bPGW}arP>4av(#f$ zHuqGVoMd-mB_dh6Hd)T3YZ6~0CV%AN=#*6768C00l;Lga{826=x4detvX)zJ|H~xt zJ?-~*KaMB${x!?HDka;mslPsXONVyx+2p$>^;YO6v$6f!7~`U&w4<_KU5nNtfhT2l zlrf63Qj+jSc6C8%4?UZsj{X<9FCOsQ!cwuUiMEM#C-HI`KaJY85C5C*uq#-F%r0gf zE3Li4&TcO-2Zr*6N}J=Yvesd$uJ=Gt;p?06*57z5*_%4V1jo@ z@|Yx^G&NZzKl#-!Ir(Svr2D^~`m32~c`6`z1=KH@tWxMRZK~1K7^vsh|I+(7%D6VT zK0DhRHQ6rYx@z=}w%n-T7^kPA6Oi{#@V4@-vdLWXNj4Pk?6=k)tD)tvmYHs|ty$Fa z*hB2{c3SI8C@$17wA&0>JJ9*|v>VwG_CHpb)y~{uwzg^l6>Eso8=|EAS8kS7TVirW#jm#4DHYQKP_4IKW7bZ4KwJ)rKW2?59IoW4X(%c)E z}-dvaA}T4#ER30_nV{ zE~_xoTji3gX?fSD$d57J$R}a%!Y+nyirgAKEbe^jL22!TPcd)9V%#I$TiwN7^>q&! zDyrKDLze=_{3(9!YnYNZ`JW{E>qp8cZ#Qp*uYo^4ct6zM8o-Z9sX76*zCaeSwfdiq zLr&_-@9OCCxQ06Yj%>~(=Wb^&hti|LDNSkTuTS7;=QV2Gazbe%-&p zKP?akUppAM?oZ|K?@tpvZ5n)(NF$5LA3!V8AXCQ6qUsM4M>Em(G$;KIjK&Mkd!W() z2VAZyqEAk5lyT4Ww0B*0%yblYUGVe^9~YSteL1Fa%&^Fi=ZD+xKIX~p+34JF+}EO6 zQ$)kB;+semlk7!iV(3z^Xs}{nhTrYq=5G?%97rGB9n2OQ7wR9njd;1AXGD#$1Xz6< z;P*r5dX`yVW8`!kGN$OiX;)cl?YeeWZ(#gsT+(N1YZznw=tpu(ZImm;ao(LzMV&g0 zZ?G#{>CL90R-ue$d214~>QFw9&*pFVbWvWukz9V3m*o;UTK)+X{jt0WBt5hIF7}Jp zqK@={vxo-o@BqkkT9OFNqn2LLSYb>wG8^@c1HeiiIybqJT+3a%ong-Hj?Rutj_%I8 zPR+I2DIIkX3ma?y($wS~kj3TVrTANX;VInz909RRX71_vsP>u zJI9h(te)3M=a}PI>4+a(!|IT(4b6UAbH*ofDlkfiEY2p-rZy&N&o7v5=W-sh`Y2K87unXJkt=X2#zG=7To3PS{Tyh^z z;qAnC(MVQ?UAz;8=DyOy}RyW(6y=OgEOXAS2%hsV*=SgbGARsd6YO~=#eB#+W$eleaO zvR_#lt=bIIYTD7ZfZ?CDlju1wWEW-IN>U zUYQv^#McT-o}@JJ*XmYh-|lJvMvA^*l?st2q!pOMhv0q+q66s(Zs9c9N~3^Xm!l=f zLRDG~0+%(P6r(L@W?Gp}rIBnY3$okVIlZs(!uVu7H|81Hfb4zHiP6NkZnSZna%^`D zcT{mC8yk&|Mh~N}QNTE_XVLosqhCThk{ESX+~GQJY~Qv<0K?p4?Z(Q^Sjkpq+hwP< zN5Gdh@C4BkIo1#}xr;ADcI#rVv^746-{X-Y1V5d}Tk(p#A@}nS;-lQAGJ|oujWssH z~^W1gp)={&lk{e~)V`ojDWccVbLw6#Njn5?E2FlP zYMX0m~Is8_5`&5q;7ux|uvu zBM?Q_B~^6z%10pf8=}A;i{IV79vOBdAH9iwrVOP8W}`8<}~3#G>)y zuE;7&qw?A%Bh(V0rAnm&v)q`RBI9Xq_Jy59HIk0D1wZ~w zh>NbFU#Lls(pcDOPB8z145NS2U!)=#tKP^OGDen?8)Q79>q|uY&T^?tM(wjx6~$?n zr>W=#ylN&`bE1l?@?e4A$%Zl{W&nxkF18@|Y?c+U^4xg#2VjE@;SYb%Y_tf?Pj8Wp zq!&3vVqpER!PY;=IbK(J$rz{uey4xYEA$S1f)2Ge&4?bS2Y&MgzgdNA+yoz9kpy95 zy;Wo6Azc|@pErR&?}oL0LDad26-FUuRv?eaPH^6L7>7Sz1}EB^%puF5K3ELB#8~iZ zbHzF?kT=u8uNChsw!tFOBgPFw1^q=$ATP;tvKKbJkE|u*NNM6j99{+ndjS*=yEXH`Jw6f4kd(*GcN-Tg%9@h4*OCZ zR@?;~Yd2hD7rBW$6QnR+JBd!Dv*}oB;Y{YKhjIqauRWscI=NNu#7Rt-gJlmHklTUp zm!~&rQ??46<`g=MT!3<8Hn8M^@WUiEKpj9_iUBY1o2-g!41y)h#uX03QX3;SXNIlM zA)iPRiJ&J?cdQ~ONoJf+1oaayNeBOYgi|uX+HX>ipa#efk2{9gH4}S311$0joLDy4 zWievP1K8P9M1tbDc21mWIK58#LgnB`j;N&e z$QJUBC?;+K1DVP%^UUB3Pr#E)$q}g4zRC{jA#@{WU_}Ag(suYjBX~kJ?Bsb^p9>Yl zN;y(?$Fo<+RdNz6p@|$TU(1fL^F^c#yzxFFP!0MUc#UavCS8b(GJ}qzWpFnkvIX7k z1!zweL2J?iNXsd(_)}2x9#xZ}VyKO5JOf&j8@PvmV9lSQ$w`kjyRkFZV8QcHt>(u) z5&9N;Rt?v_gZg;^>}4%H^fh@5PrV8TEQ8txADoMt=N~arSmLor_djpxA|J{7sFK}k zIdmf($Oe+07SkHhN@Oba;MyT_L#|T;>2WZ+$Dy<-L`SoMX6(n3pxYOR=7jLuVG^tH4^M_PzHlxt~va$GK=2o`! zkg|xQBZymuL*tV{0!m`auv^plhMHN@~hUeFQSYN3!8|_eixVGr`Sz2aoGMO zQ$!!0S+$~nqZjIj(;O{c%ih4?$H}y$s_}#@l+{=Wv8{^whZJOI7r@f;y_#^dFDGILmk(+An)Z8HJ)A<(NHUuRm*8ntEN_1Pwy|H<)itnydsNp zh??Xd!}4i^ptrEd@9dggkId1^vl-?lZK0N$Poovoc8&0Fe6;pO&t>J6+nu?zj{fy( zb+`+t2ytkxLUs{e!EqftQWjNNEhCe385*bjz&EO?h2lLsqII_0X<7A))?BF>|7d%l z(x|N0*YELyGOIC}HsmR!9{ns=(z&uS+oQ$#KhQ0%lfK1LcdZKKBTw}!qMp8vXCwoS zA~c&dgv>QE$!FRfHJ5h}ixHPoCWM_Pl>!lt<#rp|j5*CYEQVE(?}Egb&Ta-;vYAxc z!$>_bOItxQScUYaq@}oQ)ncjju7SP!ZezCZk#i5}7n-UksJlGQ@XPjAoO2QxWKA@_ zl0Sl)bF_ZTtcoZxhW*9X+MQ@FsG2&dYFNW@{>}BAj5LWh-8sa|9b4T0CUtXnceh9` z>%Q--<)^ZTYY@3;UNCmaFfq>kx6ehVL_Pof8(k5z&U?j5(I+5_CWPM8(_v-`G2;@3 z^7r3!Ms08}_ogRn^^AHPDI8kmEaYet@N*V%$$t5(ihG(-#otP%GqwSz2t(I97=6io z?v-QpI-#r9IcG`!mcIf+T2a&yf9g}sG^Bx}i?u@4*ZSG_wMovEDX~Ty_q|XtWabg{ zg-k`8smAm>c_}98No0jd!nU#&{(Z(=V`{KCo$c&zx2L1!UHIvAM7xFbB#9DEP0+?c z2YKqwXLc8xc)%4P^L=?EK8tPs;}J=6W1yMiE14)$sRKp}5pQqSi#xWOjqUXAKV@HW zo6aE3RY>b$U89R!9RkZ_Y{WgQL&}KQ^{V|Z+mqT!eH#16dDDI=W7I-J27l-)nJ2JS z=WLLfLuvX$tEU=Bmdj&SPe)qOA#f;Sj+G@a$dzQo`10_P(X;&lYoI4+o)?De4gYGd zcMK70&~fdPxwSp2DSJnIT33yQ>V=gvtcmy(=pOb#E8#!xE+Bf^+l_nbExhhD|7g{T z*b@|idy(hNdEUA)d#!uHa7TLiKx<-G6O)|DJQcX z&@N+}F(x^Kk=8xPIvuo)_r|XPW$oxJu>;x4E6&mRsD4VZi()vcr2e!3V(fM8H?qMz z;i#uo^fhB1t+GmM=I3|a&18i@GiOz0hE^KgDGj>mQTIOXx856tv~R$%-iJda=U)`< z;T?GkeX{t*mN>$KujoT(kHA>oDQcrx-(M-Vm3=hibo+Q}JJQ*YZzG$z%|c3ne@^_`Ha=EOWnMF&JvL7GcFpa*eY~aU3Ea~TZL{Bd*7HbnynCy4R9bX~ebJrGBB28jmt})Mb$4ggP9zy6 z<%dvOM>Xv!ua4TQvZqq06W`++E-sqw^l;5Gm$09xKXU1d`CWUTd!FqH&33<}BYgL@ z0qzt2c=DNjrito`HAYKs%(1SBB-dMW4a>^z*iH33=u;*bwPp5T4(BxeX6UV5)-{VP z^WOIKR1eL;Bwl+<*M+9g)p}5t5jjW|Q8VmlU@yNMKGj|wZ0juMDwpCkY8wZve&lz@ zYx^nU@nCDPc0~Ol#uBeCtd>A}?u+wki9D~G(gU&wKjc~;S|$g(2U=0;F)D@hsNJh* zjI^-MxjTVB8l=z1dF2j#)`-^E8izh_r^u|X={5W}-T6fS;5)}t?WzBacFs6vGv0#c z_0&qq%WP-b&^XcCmD7GNW1L?D=UHE4gnhvdIM4HZ{IoMdZV@IQ<7`b{2dJm3T`%N~ z&KsB+IuiB)ilTXEPJ~S+_Ha0PI#{XkCgMi&9u*PxDImo9u=QsCK&GfMWZ;jE(N(m)$rBwP^{Mtjm5PST-n6(eBp63; z8#jZg9eGGk%ar4c59+M-3{~@Nl15yyM!DXvu2(Ux^*1p}oVD;!Bk zh6=k~sCz;hmvOA1Y3PP4Yw(lpa8)!i1;c}5<3`y7lZQkHgK?yQE2FyonvP`EGQ8-sqn-#DWLA5N<3M|fedU|+$bOx8c!hqZa?seRs=1!&Dd7HwoTU-3=O zh1!8&bG{#)v79$Qr{k+R=?R+LP1wyU^MkfC-5$QJ63Hz`rPHQ{QnV;;m z?)oA*)WdO(E;W}h2(TLgnk+oQB=3(Y=|BsmCW*>W= zqq`koewkgoGs@DrDgDD=(9ynTuBQB*Z0$H>E|B?LBSY2oS*%>}i9T5$A2ii<=gYuw z>_rcIy77YMFx{}W-|UB~6n#faiju_P&{Yj z9^zz_90G>iFMQ}-o2zQtKj0txhkdhZ`6|21xL6=rq<3u5H-?s386%Zl!~f28l2qZ3 z#@m2u+T-uZH+{I&K+tc#`O+N*YVF z^!`Syv3t3lG4#`ula2GY*3LTih`VMBU@NDon>*xiJwJ(*{lz)QcdLY5*OSg%APVc- z#0}M1TV?OJ&O7_-i9u<-cJ*>LPufhH#CJ(~1<~=uuJRFtT`(6OyaB*j@&B3Zp9ZGJspn%339e$yUcER+~G1`sGj3*t*F_E)zG@x zy~P!bu5|GR*d+G_vzB$x#q2b)yS_;*uuss>+66n0n61^;Png%_DE6n`o%&&2Ul2{YzZ3LTn`Vau-jFS~ONWD5BI4AR<}0 zlbzRA_%j=AG?zLJytxc`_;jM8t=M|)fjS(T;-tFEOr`wJjbg7k+Ste9?Ba5k;p~gunt&k?9rX(;2Oo*vaEc=_xR72qR$5!M`}B)7hyAVX zv_dNA9L3vO_t+D?GIjcz7WHG>J2|i zlj&zMSKiW|%T*#3G?8buOjbWp-Kfga$d?w7H#R~2Af?1cS>G7P|Fv@%??_#6>reCz z`mNA;d$VJ$J~|X9_ZTP0G~R^1RJBPRT2Iu{mh(b#y8ek&v)hqOtPi-LzO<0qE-T0m zEZR{oSWT@oZ0J?$Qeib zyF;duuJ$SRjE=WMY!zK7s`CE&E7p^5x1X^4>@IrwpDF`AAhK)i=~iA;RM0$HxV&ON z1lD>*J1eW%7fC#;P0q+D)=Pw|T{N3=vM|+$$B|YH3QM)uG{qFx3$l{`ZV%B~I158N z6C|hfqI5mD>zCR#x>LQhuc>d)t$q{#{15c7P2xCfikEwMjcK zM?sa?O6x~fiIPCHCj* zddYWMEX^fW%IjngJtB($4Va86gN-s3`IkN>6U8r;j?ADj=!LFgGUc8q3g-BM46rQ3 zDSwH7p@!TqhN-{E4b_}pV1LTNVkpV3SBF~Y3ed*IYymAoy!Jn0wf3Cqz?1R<%WH>M z-64y4d2x+wCV6NB@)c?|ziOj(VKe1bu~z!jH|-0igM6Zj9Hq~sE95|+c(u^cxX|%S zWdenL3N&Chrfu4)mEuoYjW~fX9m33(9|(ITwseCj<`;oU8IU5FScCu_bVRp&6h*B5>ePP)o1Dh+J{ys)pNYONZjE+aOi zB5t)^l&1@*L0z<~cp-C>O|%d_2poQ$swIn%&$Kn&Po|-Fs0kKfDak^Q0q44fsk!N3 za|g)fl+sSB0g$w+m+l{>n$O)p8nr7_jCM6M_0Xa#av zw!r)Egg&7wUNs7nfE_R+H&Q04dthj~t4Uy>eyT!h2G+X{h`3ZQ5R4Z##K@Q6aS*V0wL}Uz3LULuNdyO0QU3} zc&mz7|4Q6ler3rA@XEh&tqSB2kotCrc^gy`c~7p$w1~(JfKfY1K5z-=pwox~-?#67 zGmRs`Tje6DRY_RRo;)=1P4|cI3-n9{M{W7#L?2Ujo>xH%K0v?@}_@FZAM@Ev% z@cL`u9gLhGuH>EQ!6Hl5vty5%G*st<+EWvuSiYw5eRx_TBU zx3+47DPt?y8oB{7V**xo8yrw`+|NEyT8xHra3$FJj=Yo@F5<*$@t8+}9T+11kauKS z8Lm=POU(Oj2TyyJlti4KhZr{qdo_eUBwfiOau+j>LE!eksSh%z*eni;9?)1k!OX;H z-rufg@3TAFE3C108hgC$;pO-)D25n1-dAEcxW9|456MEZBQkZMrL~i~W~?GLyis(_DQ zW;HN#S+A@#c3pdo9WORv>hvVEEX#0q39zmbbSG(xInCWPL2Crn%oHHAldx;;Sv{?g z)|i#1{b)3611_@}%|Sx2#b(fBX9sWpj34CH`8~TNSmO5lyuFOS;%;6AoXkERjS6ZK zpTcutMqz+R2Kzln7L?cV$d40Oau>80;}E+d!CReUOZ8_)eJDmsJAN4>441J=FKt8{ zQP9#w85z))tPqsS0=bY<9A zXSEq@$7@8oIby!J#tVV(c8J&fU+6;zig-~M^J+JR8`_i)VklVQ8zL7J<2LMfi>wOG z`W(3&d80EXY*xZM3(H@yvBRnu{hQU+#_J`G1;#<6gR#@7X5=!2G1fS(|1kC%)%4|h zDRd#nXjibgZOBw`IQi9Qs9FCM2e7++xyHBIN3quJc3*oYKgD0z&G}SO5>}fE+-o_o zDmB2>K0uZZ()IKuW`J5@HZ_rDW*J!}=z|n(0`})AeFVnzG}zmrV2A%A4n)ANP?~-a z0#m*3#V(Os-V)<5*}G9Z!sB0XPQU$6kunrI-TTmGG(jXk3od^h_(-YlBW9jdE^-1~ zUq;kXUyz>*k@n#HI-?eB1O;IUWTTbHjaQ)w7f|Iq$Ncy#u%xfa2-=J$(iEB#`q(El z1Ixk2f;YO%#^co5v2xI!Xsjj}%WPm6N7MXd9A4iVDjAPz3!iMGZ1|>2-G`oL4p{7U zqJ-Rz$1$)<9n~I0o0WL~@8IL6A+HPpgSk*GM8|Lr3}s&MTg{RG`XMf7BuB9iHxWbQ z5xqHht~kUi4><8&G9ZpZwK4+Ql`6soHO@dN{RW5w(0)jM3tIXYqN}`%sF00BvmRPG zeU#C~QOr3BI;c!8=`7&P;~eAM>u^JHa>D3he9{kVH`!cR;#zV~9hIlyt26ludzck% zwYG@0F|@#}ZQX%pyR5y`&dl?|%f7;ET_OYaupOdwfF7U|q39WhDb5%0)6rU(HUXNK z-Yf}~)kK<>rUP4-97 zoZSP|1o&hG#Ie7KNe0vAv@C5$>wybx0-bv;k_yaq92rYm(=*WJtVPyuNk)^}$nJwM zx&0Go`Y(R-MvVch`vVoqAh5OpH6Ky-3p6?l5iNBx8y!?U>Wv=o=In@~-7z(u1)ool z4tA3V@7)lrcUv&3f8hEd)sftR%{+sOAQdz~_pt-XU?UEo!tI0_=Kv<^s)18H1Pp3f|qWn ziX!Ge!5-M)siJ{ht)_D^Mb_iLDINA52>4ExTYJtLp@-WED?bB;!UP&e{{}jE>neEJ;Bx z)}4N&1y~LC1PY`!V2OKby;)lJlC@_o@R=HiOAU6R1X8z^dBQm2@fk$RYFs)IL%8 zc`}q9f8jF-#-ZZ4juiz}BQ+K^j)wbwfz|&ZTOvz85?aSBbxYHH#7G<~$O#n@z=F)kW&jF5g#*NlaFg5F;j zT0Ur_&aemcZ~7EEf%Z`7aFGKTcQO9RE(?A0Gs}efi`v_4FLa_D(-fyLB}7o;EWrH0 zX!4Czf|6?=+s-1jquNfbjdoVs1KrYF#t&jcoueIT5|kLjp*R}=70y*W6FncQt|zFrS3u=ZSzVPMkuR1) zyVFD##qQUTKSe`%4qj0c8Mr1q=ano6)kZti)}xRO0`TB?#N6`mezn|!%naEDN}E=)F?_T+tgjXJV*@hL51AEtxfG(+5vZXS!0*087c?1N(0s(HX=)yJ zrz{i$O<_v{TAK&*n%sd`{*BjKJO-vb=~r&dO{}HwT!V%|hmO zvnoCrA+w#x5AzC8nlYSg0#q||Xd+8yowW^^>o~2S)xYZwW3xU|uc9~oZ>9lA=uYTZ zOr>M?yEVLTpy(+|;6rUnLPht6M?h6rMKloWQTgTkpVDz4J||@r6h7IIoqExp@cZ5H z`*N_jX4HXd_PALJK||c=)*QZTeT3HapX{h~34#Z&T}m&|KnE?FrUtOwvvc zbq!4q-3iq)&zU`}Dwuxi2bJxAs^?8=B^Zl*Y%>(1e`&9^c>Q<1x4uokq;J<(gAK@~ zFVm=2n+>HC(WSdpSH$n*(4hAc)xqf3Lgw)ZDx&aj1kQ0j;!$QOlH>5XF(=WbULvtL z!=w0o7e!xzJNSthUK^U58c?UYfa~ok*@r1NClgDEqbp9Y{f4F%$V?5Av8vo*^z~raxg>-;vcXp~^i2RACd|Yc$TkCTi9l zKtQso=Qy!txVPnqxVI3q5^>sHRYzb7L!nk3g#L8}o_Rz4LTyqDeewvr!wTGUDfHG} z^wcZxxj}8P(yBP+vhefLxUWjUBf6p*nFV{ggM51rb@F$peJ`V4S&h#%>Vy0;6#aZ8 z>Zf6}J^M||qQ@JXjh6;>oHf!K{qzIc7o7YB*fOK%VX+k1Hc6J3GogA*!x!5%?0j|x zyE^p7ragc^;%zXI&=5M-QP9|}#JUgB1XLJBSXg! zG455#1^9HJB{-Wd_`gF?3rypjoxUX&%G#<6IZb1= zar!Ug2R`8<&K2WY=JYs+Ip!NjFb}m1I@_7702@YkkzdfdE*8Cb4Lgff%e)Zk6si#F z655Xm!|UcBmb7l$OL;X!QYUcWaijxg-2Y((wN082+Nd7-ZvBb=Q4i{$p~A1CAJbe~ z9`+k;NV=&J@*S!fDlYJ&d=(!KWpxWowC3g6cm`g9kAx~YJMOoLydc}*v%Z?cGn~+a z7ysY>DcJfK_|Ps?uQL(tYCuPR5Pnh$nQ1PvQGKX7L4l%E^`lEHi3;hBYzl4SI`s50 z=z*7O5&C$1K2NNXXf!j18EuUJYw14VZJ_%Wh7;#2!(nItt)yY zl>R+W>#0b)o;FHoNRf&LWtXhXP-fXHLWq0s8UOe1^t{gNu5-`%jqmt;Ki}_foZIlD z(OHe`ng1X&mf0ZIFM4Z4?MV%Ny6oZpc=hG_~SHpMqFv<_b0N46QceNxZP|PceN4zo>(9sxg$~Ej{39l zKjH`D=i-fE(zHZz)yFTB1<3*Gg>zLKi{KywSxRF&MVCaE#AasB$a*@vRL&haLvkWH zw`NbynwFYHIH zhb?}Ox&4gM&GmhsjCofgQx1BS^Wfj&2=}T5R#PY1>7-DbNcTu{d~gGFtATCamWre% z+uvCNzJH3>-s1d9=S1m5u|$K!WhC%5{GD&N_}v*#My5s=$9~A1n)P^gBKv}z3OPqb z{S{d)vfj=-5xXY#4c_yg$WEuCx;oh zQxB-YyagY|ME{bz&WxRKe&FKFCYi-mUEale{)|4Sb}(MO^BIh>FN+#2JNeRylpg9V z1@UceQhHPUWq5p;@8g|O-5>9icqXwiaZU0I`O;(XpqBdnOy|M(vWRMK=&FcAU5#mH zMW2r5#=eU6%-rJS)IYPX$~u8#Op7gy7T^Yt$r)c%<2>gK*7#&|8PTnYA60v{7d~4! zqHtW{p2D8-<8qCGEaNTJ;H%W8e}yQuBZKU#UEl_g%VaRaqc6Inl@{)VR zkP$Mh+vTmNQ|o1&<8j=j@O>?$-{rUWMg5|wrDEUBMR3bohe8FQUycr@14T{ES# zYGq|-O;wjX8T(JHRP5{M1JR1n>9Xt1nB~+|Ii41i>x^Ovof0>y#Js_-PO*b-svNtM zb;ZFD?_Oq4wdCksBkfhM&SJ)&K)*qlb@z;p?qX{y7hNreb$7yYCWJ4UT$^~?{@uEG z+Ck@3R?^?G_*o~C8slqs+kJO`Vx-=lm51bF-$PPQW~_?zj;+XSnmsyac#&xCCAoWw z^egg&>f@7H<1#15ekR$kBOSz(%iRRDO%I(_PbU@T7mO_ETF|zjSHVm+_e0@x@g8h$ zX>usco{>>KGFEM>mK#{Pu(&~DT;TRioJ?vV_BX~y`qKSKyWm&Td3O=%=ZtFj=p*)!kMp=Y(u?)iRBzaV zmAtP4-Cv|CCYyKT&WJq;h*BW^*s@@n5S_Um4*ZTsme~ zel6=3-BetLs{`!KD33uuFUMS|9`}f7@P5+h%^OW_ySB(5lDBe*O`H(8h z%XnCJ>Xh7Nf*gF7Si2@!GS$^NmRXoyhF4X!x49o8QEeZC`qCXd0#iS z(l6s$-@DM#RrED9<6-P^mJ_=3aM2gcx|VEutT`s2<-f6$KA7@V$^OZ~9(RdZLp+zq zbI0PR<&7XOqkE)dw7WaAYG$p=>f%&sLP_K7_n{Q_ScD>qt8PR&zm)kJnA zIwmH@OUEb6de1McUU+@s^umVmpW=_<5`QLNSNU0&(Tk>QxtVf;`o)syJU<_a_KKE^ z?s69631?F-f&NR`)+Jc|eQ<4pRrlSADTyJ@zg?SXljxGT4yIf$cW9ZYmuRLAGz4Dl zP4q}Ev<|XB_3b@A@VpxDtGhTMj~U-V2gp-%OE z^p)tP>NpSLm7nsD+>B*xp*4HjK!)9{g6Ag=s%B?8lQj_Q-=mfmvhZLT-^22K>>^84 z?v+0O^O3xCz8cANtnv}KI>P>?m()0`M}}iFC2{nJqxX_kxTmwDeaoH1tk1%!~r$17jf+FqR3EL{RT3b=Jd!wtsWMyTs0|cnytSYDyJ_%fLQ7`oZ&Mb^qXuh zn*}#d)k{^gK2(&Q6w&Hs*3l-xvI8o7wQ%5U>yn@I@;-8_N>H(^e6@{vjbgEHo7q;8 zzN*!|f3v==^0!{rJN~lY`YkqZ*LLgy=jN)r>+*W%JX<3f^KEa>4>v2uRw_N>5HyxkMc0#Ea>5qYNqxsDT za)makh8?0?q9bCbRDy5L4LON; z6%qAo74dJ}{qdE0?SC`YV?muT)d`X5B=bJTdb6BlEsbYo%q4}o(CDB$3RXen>G~~Gb@XzD6ZL=ampm2m8Bn{62$#U4s?qHA>~tRNFACZFvBz;D z$R3$b&z8_XK9ST40f-Y~ssUyi!(B?5#u?Sw)REfJK zEzhNHU3C=1*{-sYbENw@uG+t9~PP zn6HoAWG7ykSnt?etPB49jP2H#_aDFl=BgskD;Tunh z7-y2L;LCW{@~cW=DJa?&iuSQ$pGW)6RGcbi{3!=r3;$mi5%REu%Xvn5?0#! z_B><%#3v%qX0^#EU$L`Bq@JkNYk8QNug02m8fvinck4x8 za+EKg2dU%j%fn#Ywg>>)fgPvmZDhxkr@vQ{l3%67-QgSs!wS;xkAllx1M zrIdW^Gjp13W~=C=7-mr0YE?cdyq<350WqT$dsHKgwvKI&waUCsPV$X2z1P{*v`Zx2 zO)@XQkhfKm!Yv1NQ(Kddh)S8s0HJb zCHW!RzZimFhzmBLr$KrduZA*{TvlT*N9m;)F4YvvD2qu&AnbaNZRz#9^Ua}>dz|yt zoo2%Dd(@`}W27(pYq@v->iOB`_nuiMQ=>)Zb&(0tUa=kI+%>a8=D1h_r++I_FR}#J zdsm(3AXLwhnGCj?)G{^F&9l8#0XE2*8zr7j6efmVwhz4KXCX$Vn?Wa^N4XPHzz7F(P?^4ZxQ71x9I;)Jv2 zm&4C~G=6RR4)R@U@)Jn^30^mrcYa7ROVf`9P-7vVU1;?x;rwoWcGTXZ1#d1-^5y;A z6UyH!df%eA)1uYwu(z1F)(Qh%C?oj?s|zcoJ#pV}%(oTnXhz3B+4q*8@t#|dK8qG} zTj3S460xOD^`DX{w1}LP$9&G8*NfNdt<8-yqotVaMA_*R^0&io$w zP1`#qYBUwMVyVu_5sBGuWn1cAuKk7A#t*wQbY!v)j@1%V_!_VJQie5FT|3Wu--m3h z8uXna<2a2c?umSES7=cy1`{YWc zR17L<_q@nItYbZ;o?O{#%>y)E$a{Cn6l%b+v9jca;_}P-4qW*)Hda%mzYP4k0S2uj z!E0rD4`3T3_-V+`4zai2_)BeeGsb(@t7m+V!;fSWM_9}#2wGPwEuh|DUi>jD{Zrmk zh)tG)Gw0(3tz}CCM6-viaZZ5LQ~AQ%a;c~2>EAMgVJZq8-MIe;9~jRX$3Xb^c|`)s z-$AltyrP{J=gX?DFxL7|;(GY^u@V2quQ#hiyyJb3S|xi!JlSVASrb+30W6^dOD}7M z{T%w(~iTk6JC8DUv5omcN^s!R=%cSUsKg_ z$EQdB3@HucZoZuXm1R1TWoZe#la!pwkOABl^LdC;#=`7CW^>-U&3pMdoz z^j)1C!_Khjs?DoJpC#!`yB`_kT!cv!F&}61&OF z9#BzukQLl0>NGIJ0)ayFhnr%+uw)Hn5{UXnwlp_@3TRGNs51H$tbiTXMcuV6# zeW1<=V;hxb7afeZBJ?Xm=B3U4LSA@@=+Ky?tFXOu+MC5=M#It)Tz#nq-Ab#Jp;Mjva8WiPFKXQqpM&7qTJo;^r*f6@Ix zb6+Xvm~J*xRUfvi%{767fv-+x?@#f%P7pi6A2)0BM|GQ@{I-rXX7QvqS<2IF@=Y;e zK6!4T;e<9yvZ#t?Si)D`v@cziaT#{f(eLe`V)M6_?Tll-CXN#*Eh0l37N}n;3D6jWA`Y0jp z{%-8c$Y23$T?t{%rENb)51IV*EXyiRsu$5lU;*`EKzLu&=uVPV0>`ZfIlJ0HbG4t9 zwYZf$r{RI`lHUT+Gzrh@`(Lo0QdXQc>+M^Q4aQW)NC(05ci`viQ1lifjmw4B}Q4A z%z`Wz!iZhQUXrX^h~i~%m}NZsVe#iqxHO4RgE1}k0xSddkyREZEkg~F8!*{8GLL$8_7>qC#(D6&dYezX!E_2 zM)T=r6DvEwGcriKCCk2A8|_(L75X}2-gC6@v3BM_kgeui%E;S8plfly_WZgE1UaUK z1LhXmNs?_Xa&IjM>PUMRvYTV1xRS*zz+1M^UkU!zIn5bckX7*ORqS^>%YDw*EA;m* zO)T@CO|-Tm?K$7mK-llFlKt&ZkG7$)=JCEEzHan;;1>1Bwzk&W(t8`Y)0r0f8f$wo zy|eF^7~6U5D2HT!OII@&>1!cfPlrNNM6l;@v6qZu3Jds(jJJ4%PgvQ*JCCK2Jz_>R z_0)`Awj;+Y{0xY3F{w39&$ud`pW+$Y%waw)gcb4={IHxlMc`W{;L=XGvP=*2`2Qg| z+~9vz#JkPsX87L4<4Pm$LLc4e?lQBhpZ1^1`Y#EW4tU=dx?QWCE&4i0_9v}yor2_B zw7wiFEYSW+n)!_dMfF=sR(@7=Eijjp|MRknIA=q%37oqXdG%z?z38qj9X8>CW#COz zG)j2=Dc=)p__*G;)6Ht5Ut+a!t~mxf-J$;k^et_cW%z$dHOoJYY(tt2&i9&~{tliU zKGP;d)m`*ElUKdMiYNN*3p!p$@9Wsxw_5(#*K+U5@alT(vj;u)<|V!8HOS~1{*cE9 z?_<@W#^Ho23HBqic4Qi3dppy1^DSA=;Z3{se^zwK6pwyq(aZG^sw=b2W1o3CNlF5@ zYV!tt1W%|6UFwkhrDQTd+r#wMpKcn_W{OAbrH51Ec8=akv8-$uwb#7XYx@WD4z;nL zc)&Iz*+gP1^fb>rKlSyE|Nl&C2S_x0>dGMXAE8GrUG<2z)OOSzUu1( z_BxMM9pVeW)9w!bzfx~Yd|&Um0y?Nle^-)F$jv&CMl&AKlnn)}zW`R1XGay%daCZV z&MoNUPc3fN&i6bb+<>;jxMJ+16-jg?`4B7W&}?BEufwMXl}NWz)ANnA-h`Y&Bndbj zstv2me4{q97(TU!J`Q@ECi(MdI9vP)ks?Ha@QG5n{?0Yh%IVDH zLNcjE>LHee`vbGAtNq2YLruECI76JP#TT2=RpazH1B=>6@*7Ec4=qKEqNuMze=R5d zNxXdw-uD{T_qkbbNTbU>qx#MJ{*WV;({}^D*WBFe>$?gar;I&L0;g!}q`nW(M98Xs zG~e|+Vw=|g#=4@ILT0nWyEK7ftjIE*B2kdPOb%T_GjDlXv`F{=1Ht~vj zljl>al?73v`4KW`FS|y2M}Z;4m1v)jMrX>7CZEQ(g;hi z)M{qmf;Y717oGiVmwvQ2uQv44fTqiP6f=)}+6@t6yHT&C)!@m2SFeO%%i-#GelFAZ zA-JAWQz%2?&9xc$NN@Pvn|Ay7y|1rMo^Pjzz_gm^r3UHclFex-5qekw>sImHkelz+ zVqoRPv{=b_D${2@n(F}1`a++(v4i{614fd{O=i&yW6dUq{od=73E0S#^xR*?D_#-J z>~`~*Lc-HzEHl~CBK`eH&b!IEfcKo^nYsEaDx!p%K*Z0po{d6-98xafzxkdIc}=j) z!$!T6mX7&r7yAi0#vZnG+m zpXIwDMunIi=2FBfFW?b1JVNXaCq;r+2ET4$ticD{!Q0j(+SvcjhhT-?d(iB*l2*8% z^{2G2Y%_5kA!}$oGV?95=teKfy0yo@b!QF<%GhF<8VwzXd*W22SMCa#=mbz2byt z3XCG?Cd9g6X93XzbH2#PDjRD#&m~E47rCz>}`sgX=xZy!ko{_*(#7m8FI|_%mw->#siOQA>TXyGNRDNuaI^Ee|ZcWe=J=K z2sh9zpxYfD1u&!(xi;p8L+lKMOF!AAJ87mTV!fd}B|f>wk4fJn-F=_QAZ!5;uzM&!lw|f(dYjIApF;a diff --git a/test/preview/test_files/audio/the context for this answer is here.wav b/test/preview/test_files/audio/the context for this answer is here.wav deleted file mode 100644 index b6515a85dbda357af854a4df5f7e390702333963..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99884 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 KjOa?vll?zJxZF_y diff --git a/test/preview/test_files/audio/this is the content of the document.wav b/test/preview/test_files/audio/this is the content of the document.wav deleted file mode 100644 index 37d651fa9dd0755ab3aa9d6c71d19958c7d18719..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89644 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

-&{zMv1^W?Xr?>wlI!cm-L192gLef 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 diff --git a/test/preview/test_files/docx/sample_docx.docx b/test/preview/test_files/docx/sample_docx.docx deleted file mode 100644 index 3a740ac96876f71e624fbc80fd2cb2408024e34f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13232 zcmeHuRdgK5vTaMUB#Uf;1r{?igT>5XF*9R}*CR z_kB;T)zPc6YDaZsM`rBIh?0|pc=G`O1$YYp00;oJcOmd9Qo!}FzZN^5m zIKM;VZTOLNGmd_Ze~|TyC39Q*M=4e?dZPN?n9(+e;Ir2E1$&B+z zl#2o```P1ML||g@CdsKpK%Y3^IWWsp4fGKfIO`RMReC6Dx;9tkQOhi2Z`>&5lhi#e z(FoCjMUSndD4J9vUB_s*6 z>63`XDonH~M7HbQ_};=&T4>&w-3^#o?ly1VTBT+AnArw^;c(hpD^+aHWE}d|DvJ+i zU~l=^>Hz`(`1un8Aoq_Ui5rdCcn0}V+Xppm6L9qn)OzeDl=@c8?i>18q9 z=DqareCK{Iev|F;OWhc`GIRzL%NUE$Z#BfFP?i_W=U-nwEi8a*9qNe=Pff>8xY?(P zIBvvgU0^0CA%wQW%sp!NYCN~O0OAAM@)=y_Y`5T3_wI~ehKR%|hWx_RP(#Ntp`ssx zlSaExb}5DKb_?Om2r0&A42WxUGwhV+?~}ZkvXU8Q=gh@4+@VXjg*yUqTE;$l!5Z^c zCo<7QMfR-KXnj*Wifv;2YEE%WjcG|wN0Jt;Uj~bL(KdPHmfKnQJtTk{CL1P%?%Bgw z6IuOrKXmUSFY7shTAj|O4OW^K|1~Hu{rmW+{_>t11{wfB#s&b8K%L@j12m*Fv@vkB z0>!Q00@jGyvh}hEq9@&ix9UYcT|{$RtiXwHOFhL>PjLF4t8fMJ9C6};mV|O=N3EoM z4)Jn!@*6STq#f;dCSz&IIGpF+o|M44Y)!+mHij|_OMG=w>d~Ruua}2FHTbRM=#Sdq zix6MB1eaUDI&!iEh4S5jvHlxG_F!J&RIwC)NBfz3P{k+&nh`OMB!XaTDj zEOesJFX!)`;}3IP@0TAHL~{;-Sc_FRCkifsLfY6_O6pEHwc$ z13Q5O@Md$vd4(aJF^udaWy6vkCu7pcpr8iC;)K+@%~jhQZ~}Zg5PB?55nFS%9aHT3Lq!&Bb2!F7#ZnMfdBvMaAl6_N#f2%;?3B zk4_NnZ3a*o-(9d5YUxrp`#ZL=i6&E>Ua6&e_j*%uWbaL--!M`VzLAdh&kjY7;-Zol zZzR`Ci#LqDRL}B%OIb02QwS(h-=L7%rEuGDlqcZl15PmJhE6j}=7|s?!zjzmN=Z^q zB+OQ2d{(N`V?`;_&DYeq56wk|YB+xXb=#RVs(b#;Rr&_30eoKka5#Ebm%mlbO*$&e zikaJu1hh^-L!<(15T{&;sg6l03o3Fq1=O(aT&^I=H(c+N>@(u78rm3X~?5gd*s070bN0kqo}xqlH{~+=iu_~5o#0&d=x0%4(YF|&qufMLbSJ;UP#8{ z#}SYqwW4;AxQ!`H!@A%#a?~|HE?MDUVRpZ(BZ=x6@}xv6ee~h0zh64whit$ZkFeam zc^@D1RbNcq(1?_Hp1Vm#p-3tf-T|6W(^wWiyps%1bsAT&E&<57zH>#$DO!#yCDtsmPQl0-CRyS|2}h1X zchT8Q8M5Hm%Y`eg@}{F&WSpB`xUH|MG#(+wo|=Qg9i7Z%v@2=2vqOIC><(e4SL5`M z?d-|}Kox#;0rSCk)834C3)X$sWr_SsAFhb}w2nUE?i&7dNK30~qn%kdTzYikqoY_n zHz>*uD)devvskBg_^MJ+vW%(;mRmjByLq}gyT@v1#Kh_yrt!>9eaeO^pPZoYcX89g_XL| z)<-3CrjF!O8EolZU8lK#C%X@ewZ)W8ewHep_hO2~U~q~V3LV*fr_erukGYsp8Lv%x z8=OoYb8c=O=+Z>1QSZXOc;GI4js-inV9bl zsBQxHW-X#;s_nDd(Sy&`R$e#ucdJa&?Td;w|N{p7AYm~`@~!n_#I?z+W) z7hL3CNw|EnmqJo8vY=}7`hqqVM)mN)Eq1Y`$R?H7H@W}kNb3vUe~O!PhPCoHpm@mu zVx8E4w_v}=&3}>0zemqMi6$6m%?FCV|F@5_m|+mn0SX2$ttw z0K&MuMbt2(H=CW6V&caWx-A#LiX{8-FRaVs-J#CmGwy|113<)s@Jjc2N|$2X$}@gM z3l)oF97>F6T@7t*%Zv$g8OfPjAE6v}g$ahB`Zz;QY-S^#EJ?brk$!v3eacW)+Kyqs z_EWS3CI(J+LJrI!?n z+80)q44uFWGhi%~qb}*n{a)Oqmr>fORg*DI``UolcV3lb-=sE`eHlU3-iQ*Jc|cAe z%j5P4+5-4@w$*TBk~9Sc01V;-0H`2m_}yz9OpUCJ=zf8HOVk6QN#}DE4;ww zVYkr(y)u?5u`7#t*0p-S$aV#bg7OTr$8a~AMaAO6k&Ix@n&7Bc!niWMoxsu)ks_)gu0q%&IjxCzPdu8bMd-u7aj^Enj7Vcd zM*GvgA2x`H;J1+)u9wJ{k3bUFB;#i&YG+7}LUMW+C*f?8@4>gJLl}UGA>UD{XdozH z&_7-BgbarycZN-{WT%(Tz{S&bpMNYzOf2QF68dUC%!NK=K|l#-Xb%*8X^hi=4KHM& z>F@2W&)R>~&TXBm1XHx$h8pEJm{2{Q1~6lG%Y3FS7&E51NLauow^DbwPknKl?b^SY zZ@kdb@q}-K%9pl*7Cxsm!q*Zr!LX`ov*v4^WHxmb&m4+ zk6TSDGkM)_U*^4EetZKz2RbAL;YDG0sN2SQci02f#@uOwxEA@Ki41q4{sF6tyU#Sz?l_~Oxc*8xi7eaG zj%R$^n!v$OgKOd9N|(p4iHtR#-yb1`jiL!VfW3Q4-rh2_TKI&PAW-XmAegC}bgEf0 z4VO?k7CTCiD1|Y5m>8;)+VV~JdZ5+a89TR#)zS0AfKbmO-%J7W=iczd3C)~^A&l?L zkt@QCHPI_N7twmQ1l`jF1tTBBs}Wg~u{`u9+M&$)&hEHdf~AQQ4b2752>Y%Ecv7K| zLQRy%h(oRxsPth8Leo;E)Wzwq{fl@Wz(@klIs~gW&l)pNm!kqtz;cZDR-B0GnZ~z$ zWe^)E&aW24sH+b_^N(hIH zhP81>dPFd_IlE2%<{PoA1!SH4^8;Sxoe%8x?0Z?7h3R7Fldf1rPT@hr+75~yD`gh2 zZG)>rbQVu$S!?|!QR<1>l)bWo$Mtz;;r++57>-iIG0Kmv1ENKSHCw{G;Og85lj>VN zg*+$%haH-7ZB!16L>niOLG(pj>ZUJIW@!BO&1pVx98*?7OplP!lV1MDRZ>coh@tsM za_mf`S+-NX%B$wSW82)UGNPrkv-Z-6e2iE@Jr*qQj8ddTu|`k8*3{e20-pGfL%+m$ z;vqMGmqK=g3)wTjpdr2)lL-)B?#aoYL9dz6k*w_6-NC;VTe(*dL9|AkGT1Ft^x9qh zeD4s=FlseVy=*~MQMhg9Cq%mX4D1h$$v;OI8eMFXD~mTm$-JMDaw;L=R-l^)JwmR{+S%I zVA=jEUu?KW{hXrv523+&s?3bt8h>ceod@?-ja=7t;|tD4HRrW=TXHnEcF+YP)Ekoc zr5PIJhDD^!3sn(!5;TI0CvohS8oP~WO5pUBRF2l>!mONJ-V=7dpFK*G$S&@wM2qm9 zr5a>LIha&(YNku)Aj7%m|9seMV1J~Vu* zB(s9sAMe?r+QX)@zj1A~zf|FQCHT8h!SO~7+T8hBY5kV-{X4MXPo^`0r*5tDiJF+2k}o|vlj90AUbDpc^uM-5bj!TyDZ?<^F>(RN@(Zi95gQF7pRmvwi`FH zT(rDTYzR!R>`>X8ZF744k3sO$89g|{8vq~*?r&QT_C^j4X4WS5zXiZL<#iiO4n!~B z!&m6KaZz2L07E4ONCnzr0e8x@v&GgsU(}y5rvVewFXwD`OVnm1p>;!WF8isE#-DJ< zA6B2q#yGih5aRVy^3Y%M1cfLVP*csW?%gKar8xTteUM^(rD7o%2A_}Dz1 zP&IADY2l3?+H1g!l zLbNFq!Iw2*OG(1qRJ~%H-i`7LoA}E?8d79k&Wpeo50cOS`o~9eJx@79{-|;S zX`7vq%x}K(^?B-&o^PH_yA+miS3G?`CC-Y?63uyoO^j90Yx%&vLz!y$ zB7A|@cC|8E*y*6zJau}}ryt>xO>soM`sSG9s{rpl7O+ol)k2@ZDfx1!@LQHVp$6 z))i>iXF-D(HC#^r1XlH#|*YzZ4qVfsc*~ zU=TOupNf3Ef&22_$J$rl4@2ipUo!6r8gca}R7%c(j+dvz)`m}L_~nfQqkAncj49?- zaK3Y4EM8;g{hEv~HJO=^e#X@FAk!#mr`N+iua~Pw=I%JdC^(E*?Y+1Qr`rDG-it#@ zfJI8w3Bvk>;+dvhbd+WXs`Ipe@Uwm``jw#|u)&aFII|G?`T1Hr07;PCqh6ojWuTWZ z+pOZW(6=vUVA^t&RZZF*TflvXaOo}X6m=|g`Zp3f0%|MzS|WgiD!EIa$|II5y;3Bz zM9TQxZ7J~@c!b>-2tZQM4NSj_21CKr)K1mg{$w_6w2#=0V;En;w-Easd29BCaO9CY zI$I>0@rPAo@*=@GMtq=3J8K$`GQB8xU-_Xdzx75s2#2zd&uG{Yl{C(By__@2UQ@ex z$ks$vYb?^i$R=6tUr~=P2%TjU505j7a5`7ahK+ISC19nFt9>K=Jga|~xbn!iH9gqP z-@Yqwn^jLkdZ^X@wZL6PXEftLnitJ{S^+3aCUinG7Pnc)S#{NE*OZ!g?$C>c%((ha zZCBCUj&)sSB8lP<54*E;-Ggo>DVA_wi3n5f)u!q*{gy(GLRA=}%|N@uZ9vdHv-%FO z|4^urgzdGd1t}aZ8B2!j+#waX?Mr$RBa9^d^?iW_+GM6h%Ikn>-RkSJN%^_C;6RvS z2hXSEWAfnIgXzy=Y?K-U?G*Ank0tQ>w}-w~So3>7%k^r9P_*tWBov3>jp>kDp2@%Y z)C`2@o*JXkMQC}$&Q5GvusGbPBj9u@3trEgAMDg)x;?8Sn2oG2B1#fHDZ(PLLKL;K zqp{?0pjBscpwWOn#aSF^Bv~8@e4_T$cHw)fJs-LUT}{ITh!)<{Oslp|M*D-QoBo~J9HJ**Ep9;W*Qpv8UaD< zgLsB$R+cLPHPIIGv~NXGmYJ9*R0D5O$s4~qu>@Rwd%?4|YEw*C+>Vq35(;K8n&bU? zWSDEj85kHyymc0tw%s5)YAjx5SmZQxJ1Oq!jwcv~QA`~y(~*ZMK7&3LC`_gfdU}sN zi(tx>!srsy45+dkSIkC69?!2D!q?2!sH4+ON_CgpPQzEKg)a893|L-gXM}r4BR>zk zs6knU9dO+r4xiJR5Uhjooqa9md^o9Azp_MrTpr_y%<$8|vsV|qs;`(Q01`x|Dcy99QR%%Vt_oz+)}IF^m!X*^dL1PTSC4QBmx}udwc^{!)0*~|e%;w7vZVSl zE@il8IPvusv0G;rsN6g)y0?4>^yqIWyR30bj&F0S6mUB*8tHZCHG8;n6%3#rJ`;`Q zxn|#sWWEyB-0@AUHkNqhYc-78;U!6B8BD5(R}bF&?CCkmXDiK``~HBGl=^nNQPv$s z&u+s8($3ZWg`{a0OUbyz=Mv)6zUvr@&B&3|MyJ=*Zs&``g%n!%VQpb9_qBtYSTAi- zkaz*{rCKdVg-oD*TGnsBwwTZg?z&Xtt?XP5qY3)IMd&9cyib-)~-4UsBDI|4yu81bkF zZuOq)C*9F&&o*2^k?T)s!qaU(-=ewN2L?V~`3F7%{XpM7fv%2S4`Jr;3lCHlATewq zkyJ?X1?(EtM7z*iR%Zl*UdOILuVYK9*W-I~;A3cQZzPyj`3~X2`UeAi&+lnIH*Ax6 z9MY_sh?gfYj}!&Y-+DPlW1NT{BwluW3wIsoaifmvfG2jlu(~l{zu57LX?3lN! z;>2-y)48}xwBBlsT�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 diff --git a/test/preview/test_files/html/what_is_haystack.html b/test/preview/test_files/html/what_is_haystack.html deleted file mode 100644 index 2d62b206c0..0000000000 --- a/test/preview/test_files/html/what_is_haystack.html +++ /dev/null @@ -1,1634 +0,0 @@ - - - - - - - - - - What is Haystack? | Haystack - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - -
- 🎃 We're participating in Hacktoberfest 2023! - - - - - -
-
- - - -
- -
-
-
- - - - - -
-

What is Haystack?

-

Haystack is the open source Python framework by deepset for building custom apps with large language models (LLMs). It lets you quickly try out the latest models in natural language processing (NLP) while being flexible and easy to use. Our inspiring community of users and builders has helped shape Haystack into what it is today: a complete framework for building production-ready NLP apps.

-

Building with Haystack

-

Haystack offers comprehensive tooling for developing state-of-the-art NLP systems that use LLMs (such as GPT-4, Falcon and similar) and Transformer models . With Haystack, you can effortlessly experiment with various models hosted on platforms like Hugging Face, OpenAI, Cohere, or even models deployed on SageMaker and your local models to find the perfect fit for your use case.

- - - - - - - - - - - - - - - - - - - - - Model Providers - - -

Some examples of what you can build include:

-
    -
  • Semantic search on a large collection of documents in any language
  • -
  • Generative question answering on a knowledge base containing mixed types of information: images, text, and tables.
  • -
  • Natural language chatbots powered by cutting-edge generative models like GPT-4
  • -
  • An LLM-based Haystack Agent capable of resolving complex queries
  • -
  • Information extraction from documents to populate your database or build a knowledge graph
  • -
-

This is just a small subset of the kinds of systems that can be created in Haystack.

-

Functionality for all stages of an NLP project

-

A successful NLP project requires more than just the language models. As an end-to-end framework, Haystack assists you in building your system every step of the way, offering tooling for each stage of the NLP project life cycle:

- -

But that’s not all: -metadata filtering, -model distillation, or the prompt hub, whatever your NLP heart desires, you’re likely to find it in Haystack. And if not? We’ll build it together.

- - - - - - - - - - - - - - - - - - - - - - - Rest API - - -

Building blocks

-

Haystack uses a few simple but effective concepts to help you build fully functional and customized end-to-end NLP systems.

-

Components

-

At the core of Haystack are its components—fundamental building blocks that can perform tasks like document retrieval, text generation, or summarization. A single component is already quite powerful. It can manage local language models or communicate with a hosted model through an API.

-

While Haystack offers a bunch of components you can use out of the box, it also lets you create your own custom components. Explore the -collection of integrations that includes custom components developed by our community, which you can freely use.

-

You can chain components together to build pipelines, which are the foundation of the NLP app architecture in Haystack.

-

Pipelines

-

Pipelines are powerful structures made up of components, such as a Retriever and Reader, connected to infrastructure building blocks, such as a DocumentStore (for example, Elasticsearch or Weaviate) to form complex systems.

-

Haystack offers ready-made pipelines for most common tasks, such as question answering, document retrieval, or summarization. But it’s just as easy to design and create a custom pipeline for NLP scenarios that are way more complex than question answering.

-

Agents

-

The Haystack Agent makes use of a large language model to resolve complex tasks. When initializing the Agent, you give it a set of tools, which can be pipeline components or whole pipelines. The Agent can use to those tools iteratively to arrive at an answer. When given a query, the Agent determines which tools are useful to answer this query and calls them in a loop until it gets the answer. This way, it can achieve much more than extractive or generative question answering pipelines.

- - - - - - - - - - - - - - - - - - - - - Agent Tools - - -

Who’s it for?

-

Haystack is for everyone looking to build natural language apps—NLP enthusiasts and newbies alike. You don’t need to understand how the models work under the hood. With Haystack’s modular and flexible components, pipelines, and agents, all you need is some basic knowledge of Python to dive right in.

-

Our community

-

At the heart of Haystack is the vibrant open source community that thrives on the diverse backgrounds and skill sets of its members. We value collaboration greatly and encourage our users to shape Haystack actively through GitHub contributions. Our Discord channel is a space where community members can connect, seek help, and learn from each other.

-

We also organize live online and in-person events, webinars, and office hours, which are an opportunity to learn and grow.

- - - - - - - - -
- - - -
- Join Discord -
- - - -
-
- -

Enter the Haystack universe

- - - - -
- - - -
- -
-
-
-
-
- - - - - - - - - - - - - - - -
-
-
- -
- - - -
- - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - diff --git a/test/preview/test_files/images/apple.jpg b/test/preview/test_files/images/apple.jpg deleted file mode 100644 index f9023fea2cf935f2786880010e662074495b3eec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69286 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

3q(kEeHIL}j@>1~3Iwh(vpa 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

S{>`%&jUFLd>yhBdH@j(fj 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�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�TN zX}fxBbf2@sR?lq{{-o4z)fPohP^|s)+=nM&6Z(wIvcrkKJeT#+G@Lu1(arP7Cnins9d)a}7PrHs> zS@FdmS>lR%j`JOuaJQbXZsW7=Z0(;Tg0$t z>*k|NBXhla(4y|ULT`Uscy7z%Pt#63@xLB)z1g|1I-8$X`E%5Y+>tj6wJUe;$^1*R z3p`L}@%1}(V#EbSfi4$@56k!9L(H&>g||N3d}#Zx#dp4ZIQ7DrB?I&PQ2K45_KGFv zql*33Cw}9&f}KC~_z%rP-){79w?du5&d-~(cT0?`wPw$zx3j%3JNoFd@lKIDvsx?a(&Z5s8xk3< zaILCPc_IE58yXo=A5lAnYyBEpwN|UqXx-Hsty*vFSk;C527SAkrH$23(K|FYL=l3} zUv#uJ2tUx&n$SBkIxq&m=AvpI8JPSJwQA$-r)&@x7Upk>h(M^&wM|H9i~|3n0)z0& z)fMfm(J>f`LZ{T|H3oOB(cq?48r?KXy;`Z&C_1~+{hNixTNN0kx^q=U&nQc5NQ~8@ zi07{~Ds@UN#?dM|GB7TH?!o=G@EaJfZ=8P*Yd|a`zD;Os7^ud(ZK5qvQPx02EG=O% zR*Fnw3M2h{xR^}tqKQj!$W!BP4|%#9(dVp%LaUPmJeRjWIJTszz_tg~`i#y}|7O>U z-HT5rPgmeCC_`!BhS3)s}VgD7a(h4-Y+dB;7GgG=v z`)yss%R5JUyzlvXLETXUw)eYV=VeLno`bUad?;6_Q1Mm9hUF`?u>R0B_wC?;Lv!8! z!GBLrMcHbuHI-IW>Qq0<=HpCH(Qs-edq0RlWTsHz2XLo=HeUU@|D|5tn08UqV)5N zE5jlgj+Oq&v&d&&)^_Gr?nm38Y3*wNZ$!s5oGemt6|T90nU4EKg=*Jm%$ zbkgpQi<_LzdZtp709+B-U3%mN2&n=yAQ7+v!(N3RGFL;!X9_LABmR-23Lf zA?c@icN&D0+AwMQeV>bq=k}}h_l#d3TYF@!Gbc`eWWlpJvC9|q+8A=FbBVi4!WJ)k zbN08%M}N;!;o{n=mUf*kbo{-*^Y!U#jfYaW-sCz6w*G=-aL=j*NdXBtUW*Kk77R6Evk8u%DJ1Ymqxw* zeSKTMQBRgXy1jCKWbSO|E;-;iTjn4YK;9*f9%Pj3-7PJ`D=?~OYUAPJ3H}B%)J&_gA*?1PrCEr?b>VoAGhe( z<3i~=AN$oR9p83)Y|^~g&8;>cY_<86&tF!TGwu_kx6T?~rmFry->WZw-G8flN7MO9 zs*0uWFKl%G-a`F@s$+V)n0}i(#*#2%mf!H)58CXFeZTPZ<2NxekyG4ycREz$i0S;} zX@UDI*6bhq#>-H9_ww+rqd#u1yGGM|>Z9Cm&J9p)JHP$Gu*Xp!)?WO0^krblx`GCQUEz^`A0_KX_S6M$hV; z)PlIxTcq1qh&q?+B*fKC$wc4mq`9wy>;v)@x0F1fw);!(fA*s8-FJQWo-@80xT{Y$&3aAu_&mlOTL*R+ttqa)zPS9l z37c!RS>k{3xOe}rDkg+nY|!LPlOj!~R;+k@VUJUl!?ocqzcy}VJm6F4#G=WC+O~Z@ zeo>Fpn?F6dR_i;x?(WTgM?VbE1xFP2zI*qU>&OZdR*m@K>?`xgQvqwU`B$nQ@X+;2 zHtpy*ozdHrfA#2>>iD4%bqcH34_noE^Y^XxoHqvyzhBgAL}JqACi^PiQx5idwApiC zz7t10z6p4>E$96yUf;)W3wW66%HCE7&llSF>eTWqb#7kzrsS_%vQ5zjz1}b`!tiua zV%C6u_m#s-?8{NSRv^aKE>XwuUCJF_oVJQ za#!w^r)WdZ;*GkvW?x;j`n`AIoBBP>zp`kSp5^ct&ggfGam`-cP9u5P&Gmi27IMVZ=pFUoe-Gis;HS4{_v9zOcOpLyT!s6J$8 z@A>P0Zttav9dx_L*VmeU)qm_0t1-*c@~1JD(^;!2^jS9LKT_|aSGDoaj_1p!EU>ZO zS6*Wd%&Zizs$a+N&qo#J*It%ITjoH_%1#w_4ssnHcX@n~=?`hoWrGrYsfUq5dR;4UQ=rP!+NW;9?VyH4)plS>{_)oGoW zu;clkvnO?{z2rmRR&S3OZdd(y&$sNiJGRWJl{B$=@scgO-^vwMYtWb~KNn8;*d^(} z>|<5iuW&sVzw6rM9c^4yoqf7qp0hQ!!=}Ve37gs^SZ6=~tN)>%+nzSO9aks%my(G~ zFaP;_N%z}F%4}S+b#L?SSLS}0Jbe4Q_6c{}-oB!IGcLMcwXMY)YquSoqKvH8dvL;U zgNn4u9oH&<-s4|2Kc2_!xJx+1bZ+i%z3VhtzV^||JT-P*&D|-a?Vur*Ru*1TNf|b- z@XWg#@A)oxQ?p6qoa%NyN z&Yd3AK=Iqm)_?80o_9fF=g>FXPcCx*VMgLI{fp9W-F!NZ`!@WOPmS$)&P|?Nw^h6F zHk%*&Y~C?vrS5*SzE|GPw!R;bFH7f{cQ5qqZutB665FDhuh@5O{nDP6(qRL~tjuK` z()@h8d&3_^Wm~v&${z({n{MrES^Q{V=Hf$Vt{fBn!t+IN#aVu#vuox!6;-Nw&MLcJ zP4$f`adc<(KC@1iX*$muCGWVPDd|^x}Md^ly zh8Igc@!0V7(VfaNe~et6(|mAas5#e~?kmc-Fh$gk+S4n3<%Qh8tZXoGQ`=X!I}JK+ z&Nq9kY1ySLKbJ18%F?@l*|>RkR5r7w?;pFSRlK|ZQOnX<$IgwYTWWCQX%m|rIk&vb z+j`||H4JN+`MY)#hlh7~TmDtvhy%Wvx>Y?GF!fQE+t1omzCEJWf~S8}OnCmymL_Ai z&YZk>_=?GY-Y@nr%lFUY%?q{LTTi_E>%%N(p2wHKi?$Pcwv8XO`E9v!zn z?%qwFKcCZoXpe-N6VLkutnNRqVx;o&;+ZZFE5x_E|Kp$QmuHVOwWzu^X~U6;9|9J4 z>ArefZN2xaq>10HjC%d^+O0Fa8+LCrG5@hk*+*si)u&~)S`F35M!#>}xK7m#<3?w` z-6Gcyzeao`<^5vo_jeafxcK&1 zqRZ-a^OsGZlvK%D>DOf!Cmo-U|M<;+Rq}9RY=?S{TJ+daa)fus3ezV)xm0ZV_`{Fa zon12daOo-M*S64Jd$_LMwm@a#zK(e(CcHBAG&H%hE$M8JiHAS5=xJzr_4%<5OTC}J zEB3MB(m(q@Sh((D#U~AWcU;=#`PIj3+YJxgQg`)~nN_NW^w@FVZ(Y@|XI$K)y;ZfY z*=I(J_iy)(Y&rcQf@<^46CYl#R?uxw@j?gcl?g0Y)@4qCk#mYS>0WS>XTC#oN`2d1 z)5f!i`QX*K);nHzw){Qd>*nU#PSy)q1LXA7s-;lXSGdN&Pl7+D)R5bxP&JHf6DjhH z#D)%;G4A9j!OZaM&=KekvqI~xQ<}_rMFc!P&Y0j`Dlvw{1BM2>s)e-=>=EvHD@(LB zB35C7-^4!S+%yAwVfW1FH4WBPqi7rv%N#x$ZB@Z@GxDE15TlhPhVEpiyb~Nux?Dt! zj;Jx1ovEQQ3s-t5_^TK-8s6@5B&SuhHC{oJfS>0`<7UoIn&5J}Y85_KkAT=33f}~H z%)z03LnDImmL(=Kf}T}V;6psI(X3I6MkC^&PL^nml;5CQ#}7;3p{%H8=Xl4NlNX!2=n;R`-H5_#ZR94Vb;K4s~beX8WBUW0sHi{vBdO@vCoZI?r3h}`~?9=z(Ib31R3LGNM?d`M!iyN z(#lMbSo|R665@$Vm@S@oK~8vjIia@;gGP`|R}53(YmJSIveP7%K1Z75C0!#3SF$u| zv}Q0}<4luQEM1_9=M%?JoUgqc^$(|<1m&!AR6y(vMyUaOCAedtqJ3m^Pur?wT2#2Ph(Mz} zfsRVX0EH_dP^VTaO^_$2os>}_+z1qkm5g4hWNe!$lU;$)+=|pWsvPzPqtK}Eii`*d ziz9B@=@W|(=%dhSL||wirH4F$jvB|vs{os@nlx&qMklkmG9p9-@*0QRW`HB*@TEYM zviV}$j6p2WMm7n_Y%_ukaFd{SR7>m)HVGaI#FN9LV(qI{pS)7?NMIr^!(%=>=JH#V{3(B4eW>V_SRJ7^`Oi1@o7I1oUPgqSZ|vqa0;LK_N+8cujzEK>{$X!$f~nLg+FSi&LL%)Z zfkC{3k@69fD8HL9f`oC>ere>^jop?{mWOP$372{(*v&zrgETU>2`uHQa@08nL6E>U zy~3ke1E|!nKs)Ee!Xnl=p0Z+T5QIS3AdMzo9|~fr)wnCQ?lK!Bqg_N2&kRoIAacs- z9HUt0z{_C@h;1!qc0n>aN8mFz2{5wgI=ijM-e9ctR3IfqiK@uxP^(?>7?W2#p2;X4 zwwnm_Fzz}|g+`C8nOVk&Wkd>>!nxrWB_q?w&kN6LL1K8q92Jkzrg$(+MFa14-ag3? zn7j(|OhyGUH&GZRBM`JmHc-9VU8#1Labp+}!lfXPUz84R4D5UR{vw!?9IqMUZM+sH zfyuidA^eOuswtx&p{}6IChzi-!WbqDlV8*JWsU2TVZF&ocR9|C#-LH^VDC7Y7K~=$ zmL3X0WEj;ZsqPXafg{x9sIJ%>^h-o2n9n>~MXVs*3AQC}lF?jJD&@BzlOS8RwazU; zyZX}LtVn|#{~0ABDkbY!YCs88GTsUj!ISK$%S<+1MkIr~$T!Oo?4Z*~RZ9t$-;yLL zX(sTXO(O6bgIZ~JcjhV+UJ<;jw0lKKUL0^=vdQD;G< zxO=;LDVj${TNNG=mav4FQ2YMmp1gtOm5h?Y9&9Iwq}gdMjP8(T83&KiWuFRmZ@cj! zC;^TZeudb#TJ{EA?23UYeB%NFtTCK_-oO$X76;-0X0gV|km#VMY^<&?%W8K)hy?9q z62c*ZC89GJlqM)YCmqaKC8CelPP`;bIp;vFqef!k0Y+_oo7t~u5E_AK5{o3(x5k2M z5g)};MobZ+yA48PDYNVx4rX5v2Fu}OT3LdvE-H_Rh)6^}kXGih&3NSGQKROxmWE&2 z8a1~7HGJ$<1E-$W1O=1v=%P_`@~GjXjZ{KNpCj`PCl%p8?k;>JhEm|{Q-!LzH89lT z8Of!_)e0jb879aMuH&atBlyDz(?rHaLlP7kK3r)R818mb3K?w-QxwaJA)AFp^aRBU z;x#u16rcs9i6*SAPh;*st|76pQ86`CDqydmG{eG`!2ngo`71*stCBIO^7LiDfNSut zJwoJl>L^s6Kv@CNMCpAsOV6vk*bAJbNE~T|bia$rhoDWJ6gs0?sYf`#U{-42E*R8W zr3nl&x+|g9Sz@1$#dADKIZ@Ew0im&0r6mkwK@=J%IvPR=%_Wh24Az+fkd-V2Y9|V` zdV^9;Sp=O)X+oe{XVfb7V47BMRvI-12O>zI{)0s5V0Y-@*V>3Mo0Mvp!tz9jw>(~W zXz3&)UIHiL6N0h4dL{sIW{M4g{82uD367Ajkz>Z^#L1Z%dQ3iSNv)P*idu*hav@kB zM3xaApj4p5>hZrt1{8}@3qMnA=nrYSFkG4N#_H zE~LYf(C9QuBZ7~%;bY&>!PF$SI1gWJko-4>4`rmoZfG0689EBif&B2r22#q0A7qV= zNC=6H3l4!|QVUB%!iZFBSLfxRF+=|00tl2KZ3Z=)3?br;^uI<3Enp2C8yg`8#6%Dy zlxK-x{E0TV6zC^^OJJZi%nJSFt_4syL)4Q7>IsPqfegKY_6m?Cw=g6r(v&mgOOS!} zY(TcpMuy1*uQeIToQ&SqP(pYIgjhl&B+DWl^poDGQEH%8wPq{_M9?j zj;$P0B~h=E1oD4l{IEA+a$vqqNVLLj1PUIp5k`(0{+)FAsot!ELWAK{C<81}GMI`~ z%|7`{&PoZY6+t{K0)XXpu%z{{`QTY7w21I%VL3~OpR!Gw(q^37!drbIF!;3EK$xjw z58&S$7*IkG8QL4LN5Iw7A|YfzrdkUYP>@zS_>}F;Uu^JNjZWzfpT;(L9n1_Q$K?l~ zwtYFm($f+ViOJXWw)zK0#Yz~ED(NBL!C88=I(MZR@eUge?kJ@|1XG@dv~A8APJ=t# z8|0$|8gyDMtZgzdF!RuQws^l#B$~ zU@NJ!hccW9Jt=%r{JbFPO=hJUK~Z^5q|I}-5s@Ljzup8kz(CNL^hyKsH}y()1j96D zxYLgQrUdMh23g7w3N$($Ts5;oV?;`yJWZHJYMp^mq<8c*S)@sbUNk@?i|x4$DEtKv z70MbhO}ug^gG6|RpA_&c4UkEKYe=8Cs4UTbq4B=49^tJ}&cTJ#D4+!VLI99shzzM( z)o*~<6rn98o2m`2MA9tV%d43+A{eEAIx|co{HCpQ*W`|!df(bNA#rxMT~n`o|LO;I zi7`uGmz(~qPpO0|0l^Kfy3a7@FL1uRWl*LuP1nY@`1n5YOxwHz3s<_7ztFp3kN?{I zU0miNXX1-2+n(_9MYZpPSNc>d61Y^KFR6NycDFyCx%RQlGUK#2@kQR$IM!}l{Ds5? zFR~r$`||#oZe6pl9?@_6rv5MY)+{o=+T?e0NB(l=(Y7nbgSxt7cb|C_^wRX?)|qRO zJ0@E%wmjA9lJ(HJ#y@m<_|uAFJ?|`!uDf>lrAjMiUu&?%(5=hzwx#-PhzQ+&*)7ip z#jN%3pZA!#XTs~yRk2qM%|5u!obpjMW!Tl6AKEJ>T`c?PQ{=+~W5Pe3`M6_xyOR$q zm2XqvjivCW<#oH(>iYfmv40;fS>w*SD}Vo(^l4`4w^26^#FYwOJ?Vb`e$z*`dT)6% zY2Dp674HvTbN)qquUkJ>xN>~K?z1HxHX9tcuHB~D!!?m_K0cpbAlUe$;o7Z|mp^G1Bz6>T088(gTs@X zY^~7pL!7B#zvz{}7>9QDDm~Mu&>01U7qc1IN)ND*QM|Ex&NX5t!yv* z9c$@RcR+*eAGh2a)}>P6?_4*}Tk&ek)Db^i_n27Sz0%G0FV!P6=UTkE(a;|*AH2SF z>SWch0$GZ5>z-v`l}2;EIng8Uy<=W`+eJ?>sH)1U)iZGAOs zcDHr!cblh;?Kk_-A^n0)cdowf^8L}{#>+`_7ytJA`S&@$*>V3}4|CVejk5L1wDj!a zz2EhVe{-qPhlSxA*4f+3$S zWIZ|d*0XnmUM_fhu5A9te{H|eS^txv!(yMQPu?Exb@SVgZ-3mh?8Tj#QKt65Kcd@!!l9|d-V6)QHZ;lXfM&wOrwpDOTI0q>|M&%VAk^?k2G zRddy<{Q7-R^wvY~>c0BeWb61BqjsEVdOm7jmR|Fd@;t7RBYvMc_tN#Vdr$r`xJzvN zZ(pomzw~(c`hZoF5BA@pi97bF>=1LGPalufo;C8^|M8`W{D~!cdLGLEd(6lohg%*P_4Quw3+rmTW?t0C>qOl~KgN!m zrMqyt{7hJ=>$BtWkvkWTjLzhJqJF{VVa+-=qjGM|UFn>AzRS~> z^c#EFwPjMp#zPNpS@PSkLj{gaJioihrW4cWMV4%uH*Un#eb?T_w)=B(&GW}MZ|jzS zOZ?@>Il9K&%akc=*Mh?W4s`w9c<9E>t{0E49rKIfz|j#$3e}BM_gUojw|B*)9L?sM zZmUOoZ2oq(by&e^SGV6=^6tQungM^lOe}7iJh{)Le%`7fLwmN$_BijOx!?9HudP?0 z%9CC5`z79QRj=G{^LBdbg35HL_SL~-k>mFzmbm?0R@9Wy2 z`p-8TFR9kEW8$F&)+=3VKHRu&Qjz$<#;FauS~DLn(`r}TfPse;elAU?uiN^h%Awig z$2L+FIrNP_v|(b$b<6TrZ<6bnsaEE%Lxav9FBx~m=gr>-3i}UTmuHMyqhjL<_Dz~_ zwZf@7t2bSpeq(}n(V7if-zwO8-nsHkPecuSab@IWaU_|{#a zeVWvvgtF~Iea>ptZibHUH*WOsZ;w?j7rp;^jaOwhb;|ppV&2vL3tg=JEdO`c=k6bQ z`oQ+4reF5Ga>bc3K(aL@& z8eJP(x?0bLTNA7IuGRU#0WYs|rfZ9gB}YA4*JICAytsMON%hAiJxh*kb|9+4 zqWyQ5>eqw}T(I{>_X(Mn_F7V*OYONcJ?Hs-$anSr#lb^{yT1I%Kfhte^`Gtztov&B zDYrU5ox2@XHLOFKG7VOh-TAS_)k$6#KKQPi#VJ;FnoH24`9wbN9k|-QK2m57t>)qVKG^-&OV8KEGDX&5b2a{V{UNyW*p3 z`RGavdS5b6%_3_W{JrOwihphHvFyajTzT`?ocZU~<8>NtJQ`E?_{|3g)~SwOS=sB! zzS@=&hUE!6lkz_-GOpI4*WcgIf8~7ccdeotP95~qUiF9-7Z>j7nd4pd*NqxY2<%#I z%g0(jC2EU~j6Hg!OYrov^B3QW_!LrOP!sFS+p{m;IG%4n!lNwT0xQ%_ZXa=84>g)@Kab}}IE>BAl;bA=x3@%*_@iF_qBzu%uR_Cc)Czg5qzbBZks zn)h_YqN;QIv=|-wSGCC_ioZ_`EO2bu<_GGWv(CQsYuEfnlsTqe-WN}5c;@Qi`Ej^^ zzwCjt&b591U5V4j@BO~vqqILDBwY~YtQDb2j_JSa69{U`p?#v ztxD(d>{F(1Qqqx^Bkmt9H}2BOzaDLF(88ril*g!-&%=|(6)QDwZ06Rx9u^odc*OcH zdrMWS+v)t$tg4#3z22W)S2TxD$*zWn%U%w>F=Fw&rtKpdUU)Ec%-lDYPaO(P7^bLW zDRga2>9^&}56+W2bmioq&ngRDKU%)otgO9%@*8@v#?OjzL667Jd0aqo@qFhs)3+vm zI{I$(`lY5?O|nEynW6g1`{!-HD-)-UnEn2{$sPrI&yIg|qy3|%w+F3=P29X|)T_xY z_s%`ua9*!>mETly zvHsMvcb)4ivJM)(yZWF7HSb*w$$HxF$C@g)tEVRISzi23w8y>s_pY_r*{t8U;|o^o zo-?L=lPqV4W^M9laM_Lt!P%;=n_pJxvMxvPnFl{dbt+b@{aK^2{G9R)DkOBR_HtC? zO<_Lgm;F)HW#6;h<9hw7&;>P5oY>Z@!vs^6SBtA=z3%yYM~`D$XEePKx2S)K@gLsp zp11nLi94tN==d=I`lPk*K3qxI^>*!1{TcrW*>1MlxpMaR^BPRr*z-_>YHzCd2)&00HRRCH)!p@{C)%D)(Ma+>GjOoNLaDLgwS_*V0J`?YO7r&gbpy+y$R z0rPV`(i!iJD%fMkZ&gi^GhSZuT(bSs(Q&kSf6<5`z05r85fS< zad+7KxvQGjio)T`!M}JH|7A+IdA}DZl;>vE>~)%$T|G*b%Jut%!AIPmZC*cR+RBO9 zTXZbgE8$X`d_@Z#+^vo%k?CDwmn{39oSS*($s@m(>WO_LHtqRN-z{nNv?}o*kM+8g z4=1P}etBwb8PCD1%C|Tgws>;Sc?nZK7TbO7{i>xSdqym+HNyI)>Qc>D?Q$yL_b$-z zs~n~}*CPk79bC51#@f~AN6$;MMxQSoET!o&Ic?Q>d8o299^D|F2S}RSTAF<fJi~-md!C zt#$EE6PIRx8q;CasZ$}B!aI+QTwXWm)2E%ycfMLb<8roxH}M* z3g2J%X|?Fjm(iv6G`!rXd1e>iQBBW(vtf+&@6NshpM+R*ME9z^=-7Clax3!|T0G2e zyU+bBdm0?Q`fF#mUT?e%0>uqpgS*)}aFBpq8aJ8-{u?XPZZE6{bujnxlN6)$M+mKa%l#l}GIS=%ec ze^ukvUcW;d`@A|(bK34faRX1!E8A>U2d#4KodwIvuV3k#wM6OBCEniY)y{L}#@bUW zyveld!oF!^CLI60SCHvJ+iV{@YlGeuSmeLyue>F*dtM!qclh!yg>obfYNq=8>ZPqS z4mYU$wo}#(m+oZCv1oLop)+<}xpKGp!t&W4`TlUfXxz0H@rUw0Se|d8yGJh1@ijjj zZ2dgn#tqZ+4=A+#q+!C1#hq&gC@1u9w5`S1A11HKKYx0cbr&`#DDszFk>!bHOUdV_ zl79Yqb+*Mn$0&})u09jGs?aUp$`9rYdeN;+%=DPLcYm+A=h)DkN7u~%qo()Vq{}^? z;4HOei(=c}Dw(Lr>Gx-|(wSD=EN|?at=91kb*p<9o_}?D;ly3X5*~%_I$3mD=>ora zO^SAD)Nke)OGxz1J^oiGKY8ap*3{OTW!u)J?GmRoX|r?Z_+Hgj#Wb(V$8>e=^s!dE z+e4QA(DLAd!2AziZhkd(yyw8GHLI4ej+)oHvU640$ejWAwpcP%p*EVje-*iy>c3s>b->;UdeV$^wHc#@XzjSa+vz=X@wrJQobN|1d zRvFT0%H13X?ss1I{+Ft^-!DAUU|-*R-!wW}x7@DfZH`QeTRG&!n)PKbZeOwRjcLkX zb=pO3t?9ncTK6HhjvF79xj82> z)3zL*PabW^?{}s5>XEwn^*daPuigLopgVryzxNxJr+VuTGnKcW#5P_zI|<|?RIU}r#sEwY6jmb zmKfcjLs;N|Qv2%GTvJ!))vA#1y0O*reB02o*l&gMcvr15@z)*WJpPX+9TIqR<UHn6k8<|T{mZ7;{m=Pq8d0Ud z){S{PY+L1#YulS><pK-HLm3*KiIuotZM zMsWPaAR7)DWC8}y=!^=|I2Hd0*c<^+lq`0P3nvJ7$#=<52` zn1JZeC}5ZsCWh}ugo27R*q1a2IjGu)2BL$H!sJwf!o@mLH`G>*8n$TIgdeI;WO$26 z_mqrQ>pWP7nHCRLi!hXZuna;cf^BoLm{bn7iI+F#)vLL02eb!bs693!}pO`QS^|wMrs!+kF=vyN5#mPBZR|WwcM=J!$ zd%}VnfC<+lsOW^c)37osfthrkpF#v2K&_JL(BzcmdK`cZmry(XJIy4X( zm~vE>uprK#n(-A0)(rw@kU`aYukx)&Wlox4BcYX;vv>KCA2U)Sv zq1PjQs7;0r@v=%0KQcPsug6kwhpGaaR%ynx6Gl%>DFd*BibbVtNNcu}@@*NnthIU# zL`bVJnvKZRB1L6JstRcfBeKu#SSnH)(wg(kU>e*tP({Gj@-*le8Xy@OB=v_W5+jDT zrEZN8R^mhuD`yn1=yXW_>ro140?I-lL65wf9xGdx4U&4u{|*V1^fT4wH1)xKT1{ieI z0br5q8AafXM72t)cvHTnGZIXw1*Q`aPE?aCwMc?%F?ubGQzIU!T0=VgRBz5qt7ho% z(SCzUYHj0(elnnako@??yK_na08|blpVD_x`L*Qw+mcRb9FF1;zz{h9NbDj>>Z3W1 zJ67v3R{+&Fe7QZr^XPX-me?ahpyD+{a?&H`rE0iVH!d0=qPg!0-zP57_qg$BSR)#| z=KH8<_@)pVuIGvipcCBp8NdVIW1R-1)jk@bUM;x_A6q3lU*SUqGPVjIu!9C9avW{t z*ED(LJPGb7U+bS-M8=^i+vz&9!i16oBPe(FxEPs&x0&*_m$3*O0SarwuMmU|%a9N` zPWTl`mAGs_Y1nk=I%?LNI;z!G@K2^wH2)QY+5QzsIN!uM+!D~z*VY~ynn8zV(4KLj zVS(^IybW5d$%1ast>Y}wv3(T)xUh1tKC#sm9;`D774j*7O=iVaLYTx>kq%5uV=pfX zcm;w!F1*o2XdYswoPLC9LlJOJ zP`G-alR3jm#zjSiS;MKf9+O&Cebg30ik#w~8dhNr*cm=IIcZ5roxbd}f0nG|q~i;a z#aFQtS$rD@F}IvU6{+D?azgbb$l~L}L>7shWDzs|kpe?iDE@B*yRmbV(@oD1KY&R{ z&tM@Mxo5x}!=1%@2u0!_dxo%;*@d2=t^@zrGo6AAGQ0<`7XR2Y6bHiUr}xbCkDg(V zX8-V@b-_-_-os6~`@+jdNh-aEfkN!)8Q6sSfA-9vK$MT3p|~6W@XQ58p!i4c*|oH! zBZ6HdMfnGn2kAz%m_(#XbTFDsh(Dudma0M6=RitY@ebDjhKrWBu!Li0_{<8%eiLg63k~py2*&9HxMOom zAC?DUM5%nSvfF`+DnEXV2280)PHjCLL<|N0c!t7d}V-V?KtI>E5_^e%F2m6loh zu?lu1F%o0cDcnJj1E<(7;>0O=rSxw(CFp+`gz#s$|4__MG#lbn#b*WA9hQhlaLrg< zcELsAPcnjQgjw!pfMJU+FBB|f@M($YvK`K8Lgdw1@)N>--dT2ykS($7itQto8xc1{ z;8QC`R4~cqCRh?jFvZx#GI<#-Ao~W{#EPwXg<#wpY&6#tpCABg^A*TYX)eJ*QXF_cdW}S2qHZG~;i1l`gBqyV1 zU5h3Z1crg?amV(H5CGy8I361QR}Khj)L3)R4$rf97qRG zW}+bO%nc;%k18x^DK2C|FTl0r3baU_@orfIJmIUgU}oWRvIWUY!wVotQ0~vaqFg!; zuW5jgH{a$6=2J4kUXf+R;bnvYbF=Lf(B(XN4Qr5!fGXdQ6fFSR{Wah!3(&H(5`E4Q zO2y-N-$etLfcu2kM`jAHovQ z_r-0Tur;vvl5O4exxlU#n+W}&-R{(~4pPGGkO3p$i#{LNFcaB|YQ(a4O31G3piLv79M!{OF~csS0g{-1`^zKv)7CLJ-H=8dJp(HdmN!)q2B za8CpG$37m~s3c226aUUm4BcELoO^`%^R(;MI{Bjq7;xpU}W&SVwYOSZE# zB&K$%x-F4}I-5&c*hl2u7?FZkqusd{sZ@k^j2dL*v1LiN?G!c^kQuf^XY7A(!Kp5%5O~%UTcX6fdQtPnl&cBQ5Da>rg;CNQE$L0)gtFsAPxjtin?_ z>C!`b(qqo`7(K}fCL8S&8OlTtt3eM{V|Hjegl1wXTB9PF;cdZ;#UE-2Aw#h&=oNw& zYs>)i!MP#C6|%ol!4dB%$*Y+!P6ACof++$CS`94)3R%n1Al}$gPD2JmD0M)x0?DC6 zXG|mfB^^RsW(v9!Q{etdxsbBG)sYJsOo5&bnSlu{kf29Ii@GPtk{~(4CwV4_G*F`; z95{<^rdG!;(CM&aVW%=dl7plq`Y2%$vw(^NWD?Pf)GlHN#h>mv2q6Gghk_2C zhzeoYX24qjG=h_d6VRoSBU4Tzr$I&2o^equiu^!aj1&(-2O7$|7%6E8bwp4SiNqtw zVmEJ?+0I8(J`+HpMuI$a4RI3c=EwulU}C2r9wI5?;Uv@{9)fT)Muj*84?x4Y_xx+l zRn+ppGunl@?{Q%TZX-ZEgVo{D^<2DzVvp>5d}AI-JOkQ|K1*9XgH4~rIddu!66&P> zA)aA7*hxquIb25oMQY+GlYZlvA+=HEJTNg@$*+CWc8whi@(8Q<`SL-njPu2KBdH!l z&_bvl($6tTodXU85a0? zrow%8a;dq9qL{1jdhJ~0;*27$vT)O9S%Y5k^5>ao8EiHAYmvv%Kxj_zX7YO8|91tQSbebh{upvn|pETCoWF$IVfY%z(Y1RlJ2j6Ly$T1)zB-uMz zbY76#6bTPE{d}v$7;9LNTR(83O!LQp6hWzekD=zN{X>G#f3TiO?AGw1_os>S>8)$A4PF(!Yfzg=-LL zwh>`M)hTEqNps?dv*wpGCq;URiB?AUan<4I2P2Fwia?oRgi$ABvQrF5_?ocQOJyq6 zL4_|ugC5(w5fBX<4O*D2MGPSUJa z9ib_A?2!PGN#|d@FQ$AQ^tFbC_6)bMcG5AimUKTpM2C%xV4Bnem?8;?AtO-2)RP{W z4Jiq!-kzBVWjF}{gQ?-82qYj|4V+RkB#6!3fZc#&}Iv94aH0(Z7nuagcJn0aS z)zP_iSg*qPbx2T|s4cnd_*1q8i)64b+BiJy6AgNT?MF8${ic82@yf3 zIIN^%Dn&CdvFS3*c+fz6G!P5grAXJ%xru7B?tlz)CL(P+Z*Sf(%{U^caXOhi0uk88 zuyrLvM9Q{a(Guo3k%7l(04f9*7Wqz0z6O#4+>MfYB^j|FAS>jcg3`AAW~g(6@e**x zHhyHVAVc!QPutcj$WTT*H>i14YN{QpbQ}+3Er+o2plD8Xb+D{6&w;` zLeN&i1<8@ODOat1L*jr-(^vxOc$WBd_sXz-5nh50qP2|*7+~;lumId-m>D`pmxk4B z{IeM&Rc`8!2MTpk2$h&kpRQ%stN?Hhx zI|dKDDPoYY?Wl@@64!`akiR2c&Z*uYr2c^>-EkV!S3`|GfYc{EO0-u1Dv5({Q;b1Sh&|Dj`$r|Xxt;s5neM-kRI9ZxoDYpg4gUUZF=Os zFUhJ2^}K0m|Da_xqmrx|E1ne;FUz9vRl@&}RdYJ5TIg12Pd1VN0x5CME3nGZ&)gFL zEF^}txP{EMVhdbmyeMd*YB<8$&kAk@T4 z=S;`L#O1VshAXDXvSQX)JFYsZeYL z64fa=HamGcPBd=OuH|&p_`lz^a4H?l9YP^s@GwU8@ax?v%7s;eg)JSscx|O@`z*^qRj9w?()hCe1z<+dhElvG-IBqa)*@ zNUXS*$(cf*JL{g~oY)l7v6?JlQe?KAWIAC{y2viU&e!oM3j>a)2ZYIKw?ec%MVDjk z=wX-2TrajvI9_t|qw?~lb_wyJ0al-eo~&13n`lc!Oq3;>vf>hXaI{dIc6lujhGs!iq$Y6|6u4Qk5RNvae(k{*yJaSP{Gji&87XvgZ=U&*`S=l`p zp~Dp#VTA_aBycbmoyK9M0Z#@-2Ut-Ppio7`h5K8hV?u)?09se5LK31vtayrU8hx$N zkqT901iH7x_Qtc=kZ3EqY6V5cMJrT6hzj9nLi-@Iu26B!V+hjZfxssEa-fJ56-9s# z5j+AmjjB0yu~D@~zZn-gDz}XSYguKX1{?}iC?0UXFqA6eJGfI6o}q9Kw~4`RxjjKt zjl>rt3B70lY()x(SsOg%b({rHd1|R=@9k;lmp5kkHn*BvO0kF0-D z8n2~$yO3P!VYcWId@zGgM)1)cfejPOrF7J7y5om{zxR~ zkM1G-8FU&K)_l!~5CGcBLW3Y%NbQoq83S%kkaiQIDkvmle^eD@Kmgy2g>QnsGMO;| zSOIv%wa5=a9*s~aCR|Uy=}y6Y!t_z7oRzzS7#au&qCtQt7b1*01ktqy5YGJu2*-Wd zbBH@dUg(dC*XYq5R*MnuqfrVbfV;0A_b^}$>!|e;#kuiF-V!~sKgwTWvgrQw$R->2 zXOo99u!+SzQM?7+h&O0p!XFbVx)Vwi@F`6Zo-oqrC-9tIPPY^Oc%IpQihF_qaz6pR zLYEn6R2q#<3>;ZWqqPl!Mx}=#NBv=F_#_9y@3VQNH<>UI{EY_43kMC3UbD~mA8^NZ zA^edba56x0ApK)=O>J^ZI{*%1DiTu_0c4wFbS zAoJ%=5GIlfD1PyuAQSu}uIKp99_hL0kMUagoaBc8Be@{1(xa^*7V5`DYVI?3j7jbO ze_+S35(H-@43#H7KPM%23|BcXf=s869ixguBgn`=IeW%{Dr`qvMiqm8@)6X|O%)t% zMN89Y1Ey^kmIoHN)YETZ}=!UnKyhO91{f83cHSM8a^DZ@?koZSq-ph_@FqZ4jwi4 zUDz~yLk&!#j*pIGGAT#PE+cc5SuS838K2aCkbS^eIefg_#y8SO>|-3!%*D=K1mjl6 z`>!xOb-Zgz77*v1N-SbJj|4yBLKYD0TK0R)0^&VgLA-R(U+hDeK@bx56lM_T=fWal zQl;bFU1l}wc%N6Wig?!-)~-%)(U}>N3N=k~4h*pzsbI3O%Z13H=rA;Dxk=us%;n;| z6{lq7kU*l4Is?dkkuye+=QCg*!A^zF4{%cOXn+%etpX*f4xx2Pj`lxB2LN(}XR^_O z{02qLB{O<)csR{mh_+uzD!dJvofCHoyX4*Quo@ma7IVFu*^hXgjqFm1N2!y?~P+{QrqsLRU>f$q004 z%|J7-YIZp~$d3MJCj6h!ftpYxq69J!*+uma73Is2fq2V*l?>KxlVH@OWg#}~urtKQ zaRLB(yg2LLV`IuMgzifg# zPAkI5in(qX15WoRTnn;Yc?5;fuFumTIK<+UHxgG9At5 ziW;Q47pnvG0C97^52_SxAcL-PKpSu<5FZ@4!R3H-%ygyKT;xaxvxEXJoEbuoT%bnB z_Cy~S0`;@cnNr5*OsUZ$|9fm{`AGV$q7^Q?hDQ@nl$^zM{t-<87!>zS`?*@^eZ<}p zMQRv+K@cXE7Uz%+LN|_N&;;BtgzGqUoZ56gMcwUNlgzzgEuk3#>{ln>2X)8)bScB} zV+qMz>#VhL)XTbiphhRcSV7oL}t_nvKRt^cEio;%mzpYEdVe_b~hJ9)%6^}fKnAOq_|(;pakf1#=m6D)eAs_AbgSC zl@Gt`IgkM_B0x}bR|Vs1{Jlf)l)rZ04j*YQ%0!v~44}$PN2!bQ@KzRNCI|u@u zQ*Bps$wkbmPXrl=&Xymdi1y3>TQLL_Yavt%P>Tupe;8vrKS-lSufZ$OP~y@CRmqIO3^iU;*jo zn}(os@5tz|^bejRkf5f9O#&GZCo-O7$spq;^<-phT!-qCG}JNzMLy(?84x!o@uET# z*hf-6kcs!7BY>LABQ8K~zA*7HVi7vWA&v+=Bm68SAcZY=L?LHsTuNh3_Y4vW!O%CB zj9d(tQb(wf!E(@}?Fb>5xQpS5h#?-f2y|yXA06ivI)PE^>`5| zfJcQQ?c^v(^@fnq5=7?_BGUq~H&Zuyiibn}XrV?a5hk5}^3hTy{w(*HvB$;Jxs}vf zTZ@Q3g(HQ~ETa8bz|syKdKR?$uTenddG7Fv=}ba$=rvTK1P_`~1gBlf1F?aaB8wu! z1&HeM5K!SyWrIzJeh{+9vQhP3Dg`K`2anQ!u1>P7~R+t6yNQGR%=NnU?Y8Susnd?v(K)4qK zPo3RZ2E_gBwR$oWkW7-fR;(ap#!F@MdMGRzyc##X!hKVuT))5uCp2ufZ4cP6&9GEu zw+AdH>8#Va2uvJ=-tz`DkD}O&b9b^-PYmKPor&A|ou1&RV=>QE&|3i(a zcZoX!?9>)o4GUi|4Ct63cc^%Yc4T?Z{~G7fco-oQ*7kw4A{wzEg-$00!Q#R4k`j<= z!}1G=t`PqJP)nE#q(-T5njMUVU<10d(($8kRvZ{W z`I6L2P=*d4F%bfz+J;Xd2K2Cy8@*U-rhM}IhD1{DI&GNM5)+$t*HFG+8s)~dNO%f} z7ew6qj-KzLf~a-86+vXe) zZH?(|4GSavgoe3XAW^bUq^d`&{60bb(&0uB#ufo{G?aletfz2<8DJZ!hDzHW{Y9ui z+Z$%43S?k$1B53_MH=_&3_kXN_11(#ESzvpCxZj|0(u-zxCfdtP3s}UPd&h9N7d{C z1@IHqh#<;Rkha}gEJ;C@(AbcmxO8tbizpk*TnLBy34V{`z+gh)A$PHECi(B6~};QmO#Y@>+@F>gXPa7CmUc zO!;nz5uvddIS%=5g9PoFa^o)}hmTkyY9Z~z#{n#6B%0)hFW#ZWu{pARxsVm%pUG~d zbwic&;*PSP3CB4gn-Sw=HH8$R5(TL^*s9cUa}3q-RpdAvn2i7pAh|~#>5Kzs_}6^M zjYm48+Z?^`qUVv$oTGt|9glR@^GIhsk95}aNM}8dbmsPcT+fC42tm%>hesiEx=@OU zrphBWOKAz=QSOy4wukVLQN}yM>`+LIn=e~nCm9Gd!0n&Ej*7hq7cUcrGcaW4XYL7K z0vSF!QpqWpWPqU9$LmGCA&`Eoj}8$E*ZMWclmKwdpjD%*n69H*&G!>)9vLY26`jWZ z6?^xva7#c-Uqw6W#T6L=sZ{C|on1ZSLc;St!)aj!UvO=x)rY6F1?4~JU;P)l;y8i$J1Y~o2G(K|DPYgBvS+V#mS;(N?)MSaCVaJD&fM-};*zD;Up*dj2;Te+L z_(#vswZ_2p&w|g4f9yS2-Z1azQ-VXWXGoB-e|W$RxO*TeM*r|VquB+iB6^0{6gF^r zhJ72cKzfFq8UNTbtpeLJJ+td!CXvqEkpYyOftDBbv@-$cZtrS`iYcM19l8+FViTI$ zAu{7eeRE7O?r^Rvg^Ste4MM}L@RB%7h-;n5HM#rD8ikIHwS@NCqV20R2u`9OpV+Piu8x^-1A-(SClO*MZP%lC36Y)Y`vHV+J2s*qI zmJON2^dCB6Qln)7F#N|IuSA0C>@3qbW#+~mX9gko7pq`;`29Y7wX8nG*9 z+mThaNG=zu1jQ{!0Q%FS++XH$VWrS6$nGw4IYBf{SPn9mi{+SE+FW5ImqO(SkD^K# zzA}Y-bV8xs@a2|4N^rIreJCoS2^ln%jyy(ZEOz8EGzB}s8UC2W6K54d0t5a#GzeLA zLaCad@95ZLH9%JE7-v*vNof!}AStII$P(dZg`RfP;w%|Agxd^~phCKYItkth4j>U} zqC_g%)2k8XWJm0xW>Z3i7(Q1rRHQAk6B7~#i#D3PMV#3g z0dv+rGFFOTN(X)r;>t`#4O9dm!Wb9;PcfmSP09~vUoIu9`^!*5AVg#Y$Sy&=7I_Q= zKXI}Lql`LuN-1&nGgCr|B^+yl z)X9}PC}6{uijjD?OwqtuV$Tv1LCZQYGQEWdHLpy&0=5UB0me+FN;GzaZaJ$+WT$|( zl#4vJ|k!t^aH&i3CU}#7Ot{R|smOpATzBK}Fqk z3Kg9Tg039^qB~d>tjX*@4_j2i!^ypWv?V+yl2)!dG&~g1&J580RDz+#b_}_PB+*$z zWT+$h0Klh1E|{A2rDXm7pU^?*2s0~&;pw2`3XzDACnRl~aZG4npao}J#D;(+ny}C? zOS-F5z@2bzs*M{$B2u1;wC%+iPX(R1NbSmTx&SM4(?GVUVt{0fl0XuzgkVTogvzFs z!hj@-L#q}EE7BnuNsvE^GYCXz)s(kEQc{+Plx@q7D)rW`PFm5J-e2Lqpnj=?tdVPmkT?-+iItjp>A(Y|o>8HS@v8waz$23*#JF%K3|6{BD1cRHq<*{0 z$nld5(Se?}@#FAXls_jk{?xh8ycNz0K3Nx`wmyv&D*BJBa43@{x=(04oyVlI_{XRa z>OcXu!DvEVC9LDr9Ck>!2^NWMgqR^!p#;Pt!$_wjlQbC#)d=H*Qp|{J;^YRP`5hbx zctmz}%Mx9w>a<4VrK6pEt#o4*^1L`K329!bPG_yKq!QqWE#S{cqq7+)S;C=e=#eI$ z9=S{&7vIBczKf25YoOx-ua5g3-!{jL8E)DN(PUv8 zCn5~wG0E9elFCb2R#JfmIa`?-wx}2yJYn1e%E&x7vY)E8SWN-pAYFi<7hg08->158*DaO!52&X~A+lf;n!iE5q zR4&E(E^WlQNKN&a*vM!^6O_S`k-&{uqC#Vo0g>UVK&v$>JTxNEE!-LyY5_#R8r2*x z*=HVcn2hKcw-%AH))+c4DLOi`xA5o|6dGX(Q{uqYbebIvTuG|Y)gV;Fnt?$txC0?e zPJj_G6Y2bj6?(eN50X#;a5NE#j8TS%MJwU=$Hb`OHAcOvrv>rKfDmhJs8(xHhQx*g zr=KqV4Nen9rxk!&R$9>K_5oY@fsda_}91OT%$BCe&1r z@^u73FdhmMlRm^{u&x!T1B?l|M?jhAa2^<9S_FLDL9IlaFp>OsHPfr-RPfW8-XKi0O@5Vp`ruJ6j%%-OQ=VQpjgq%98F~$sr&bP z$v;ttw5th9KApA=Exdg-yaLp}umcm&tQzTEq`2ULAUq>wUFu@r{Mt@M%uhoxKX;r{ zoleA|4>ogTSl-KSC84AA|JU8Q{Mel=d3|6pLPvuH(jAFq&EiUJF7~g^qs~Jbjc~i$ z{eIlr5BuJ^n<~4?uG{r+*Q2}L5)!Ksj|K^`LYft8Hn5sID`v}p#F9S%39&`6K`i)u zBQwu0Gvaru%5GaDO}9>+r5^<)iIwi51z*EYqBQLt$45|rJ zKEM5`_VtVoR{?Un5GL}Kj?V5PuE&P|@|`(~ymoA_|I9oEqZo@`(b$yCM}|zLzLGTr zHn}Yy`gmV=Zu;x8SH31OG-)VGBZ)Y?7@F?lp@qu&>3pYdZWqdP_4Mk9DZ4^_}kX!6r<*_rw z_MEu#S8@0$5*jIg%vnjHjF3h+3@Pkn>o1vR^~VkQx;^^UN+4x)*ljE;;Y7a1$e)r( zq#Da7r1ZgxqCt+SVqV?c34)_(QGZG`s%XCnvBw z`^P6o&u$;%X2DVPl?hu@`63|vvjitxJtrv;H<~G_VT)=5q4~HnY=1l3;F8K z7_U@(&9Muy=ImUf6M>>uw3GPi6{S&*9B;$3Uy(GT(4R;jq^_}W?u62@s0lg(teD4k zaP`sjb%`T(>!{s*B#z*Yd2(`kp}WxX6)`cw2uWf@)^aJ&7Wc(Q@sCYs5D0*!(9~B*GjaDv(5lbww%+Lnp{TMpr`n*_hjl4jInJr&~yyi||LSAC;x!zUC zGw-xA%GX7QwyTszhEbWl;a+Y@XBWE)l^j1E9p;_wI#dQaZ0o_y(eQDe7+~fvet9V0 z{rLNL#%Pq4-tnE&&re@e$t4w4yt1WR6H=Az5UEOAn-rC@=qV$Ovh_8xm$?|0`MDVp zplp0Gi=-^S{gez7L3VAt0TE_<5yljY8oVBT}{N=61VFG;?0Fe_q>fjG0XZkEZ+*|Riz!x_XDI4P+g@3VyF*&`GT z-he$0+=}p`boO9H9PYR7Y>yo0<(gGMseEQl}p{5tb&_K0*%{p zzou)l&Q9VlhrH}%T_Z1$b!H0JA+ILuguKK&ZXgLXez~s>d5xp&F!IbF_6lu$FU}9} zgyimCi%}^0`i;hrkU8l!$g><9g?a|+UB&jBgc`39d9^sQ0eMM`d^yUqZa2&*c*vIR z=Q`vC%FEl<4Ja@A#rnDgV#Gi`;H@){PO-i&Srza_9PkF7o8wcN@K-l!1Foo^*Jc;1 z8%$cZj@5$BI#}lV)J23JNAcO0>zYY3Pp*OhqxiPoRTOU>D|LI)91?W$OXbn6t#6(k z$>zy|M=Y(h%jw$IwSvwIJI~#W#cyzzt6BVYu5CS+8k)0oAgfq>aBVB-xn|CSYn!Wg z)ts3}UzxL4NALqqWuD-(ui(oOe0D^vkyrCYBiu|=*CDS~6%5q75_$EW{0fm*GtPb; z)s&Yl+%JhdPkD>RVIv_SExfCdw=hF`p`L+yS0GPri9Lr(qa3ny*Kra09F;glz3SsJ zm%J@i0jEYHM-lK2lM=}VIRgPluU;RI7Zxq74E!cD-$p|2Uifg8IAUpWkSG?{g%iGt z9i z729h}WrKCG8DiTWkWLp(q}jo`J4%Aa;VI6hAtWxJwQz<->4U?^vUjG5uw}GSyQJ$$ zX|hNZh}$)0YWuqmXPL}RZF?n>Gy$g%4-WRv_D__H@Q=49TiwASpCdfE6&m}DSlw6+ zy00&}obq_@2uQ?H`n% z8H_oLNXO@2QQ+v3L32Omcz3IN`uy3`{gaX2dU*PHt2;m1J%75@eZPNpy49VYU0McB%wDS(SIHt6Cc}t9&y`*<-OHCQH=9_ zUh(zi{<(skGYa~7m+SPfECr(M-bRmv(s!%VW%;e{eP(5=`w+LZt?pyuHDjgN>V8g) zX44ISj%UzT_a&O7QTK4G`{h>mtF4XzJdeKGzc3Ua0+oL!Jm5*!T`Ef6`J>Y_I%_Ca z(NPCB_UJvB-s*UAKzG5#czW~&2nM_b+TR!5&f$TO;Lb&Ld5GJ?yk?we8f?N$d) zo`b&BDv4k;4SSk4^$HwoipD4b*K?cNE;F4cYwz;th`ah7CM27V{bT!{V1Dd&cOP96 zL0wKC-R|QScYnBqB8-x^#lo;jE|M`VhC8zdNQ6 z(z6-VBZ$h#in2Yjzv*}WTb@s_PFQU;%nAmf4>=16PMu$_hkQo%kS0n6eXD^|>6-6R zevfriU9}JGtgtt0t5e7IU4AQ&kvtOhVJorr^;_knC$+NrcPU3*A*p_k4J5yGqzCFm z!Q2iS2X$I7>pYc(IlZJ{u)wKf1JEHy36xziHkZZvG6}7y|{9tf-TNujEyjR>W9Hm;n6(zzHwC|mNqpBkf4yu^eh$8 zB~xBT+zF~;qNPEu!vVn#irhZFvoo3#m&uk14eGZNz44`XxibZbLRv*YNZQORs=p&L zqUhcEi>$(W3B86A@Y!QAFeVrSSAv0HG~Q8kF8eDQA-nHLZi|XPNt9o<0CjOnmUZD) z>E-Ani2w-#jBx`rsV%Z9(8nFT=XgCnkdqO)hJf_=U3;Lw`ot*^naTF1{u4%vm*Hr~ z@+0z}P8=i_< zuL@Q_x8HT$#rh5QE7fPHOdn*l;O-)GAwS)%CDREM^n-Eo{KD16Ow7uH+xCzH^dl-17-2r` zU3#ijt4XQ|Nf7PpmKv{VKbV{wmhCay}yUs?R#jEk`P+ z3+1A{B`X^dLGPe%lx7Tw+=LThUYS3|E`^?qdT{eCTHwXnr3fM0kn2!vQ;=X+{vD;K zHn=jIS5{!nh#IQD`4tr;KV4(2E?tpWZg13=vtQ{&JrHIJO48pR+7}&(yV*bI5IJOT zf0s6a>96<~j_x2m{Y@p*JWQ39W`CPbrq&h;dCT%3%8v!25W~=tA|j|zK>9@{3?{G| zEp2*romm4}soDl7U4SURLUg-2;q5%*2M zcerfC!nwiU>ZSbwvtC~|9frEp3pO7{n|_zRy6-_glxoy}6G4d=CyoqfNK-@t6O=rL zQ$1T4J5C%0zYyWw)}(lpxVLzw7yMQg1C~BPR+jTot@2H=V{RnEIjL^|#;! zAJlueHslPQWn|`K9(^NicyZUUbZ)m1p^*ig|wtUJIWRw-u>052r z|1Q5Z)zxQ^g&Ce0tC6(6-KSd5{c-q5C@5a5lH!K_dTmOQ*VTSC3FY9^-zJk%y>b%k z?w*@!6EDCjy$@NSM@%F^TDQQjgx&fV%rfyHC!yX2~6` zB&@=maC@TUNMqgkSa3+FvLs2UX3b(SQRh;Ur2CDVTa%%xS`LkzcNz?yHCWq-c$AI#M!V z*+3O;I1W?{xtZKgfBDX*A3U&uqL!^Ew;Ctfo{i%yg2j&Oaqc)0;L!U& z#s+gtJo_^&8rV{9MhO`7Z?6zEh_HZ=$B~eGZK9M+3C8t~cQ2maj0$Fq6lO=pA7~!qcEj)BR6xu<^ZT9YlgR~w|G+QdCUEV zsn_+!1{I4I6>ox!x4O5x``tU;yWM-;``rhyg^#+AVGW;lKL=5tfvPXMFS`fbFG1Pv zMYor1ixP<{D`OFj)?iGciLFSAz$}|(iO1mF#1=4b{8l#JuHST@W)m@!@*A5L8FRa1 zW=Gau1QFYzhWDssRPMfBn|s$*&-5epM>A}8WSNwUP?Na5?Q)lu!M6LR`?mY8d)|E? zH$Pcbv*^=qekSkfwk>OZ$r-2NcpO{Z-uK{(&p&2exZ9+TD%Omz77VxxTFHIQ9fm6z z-_=BMk7?E{Jz)w=^3!_gzH3%QHTFc{W#i`t!)@I(Uf|YjV^63_+&*1ysaM42o3!ot zZ-djq`Vmss9HPa5U7u)OAT})vTtBO2F<&)8|J}|ni?k@}CGh|(?U{ru+HR>j6LuD< zp1@&}+e2nFk(kXH!W<=x|Mb*((a`0Z81a^vTo zw0X~V&-e1K`DxxUzw91vU=Yd2#_fLBa){z&RUgUnc6a@Lwux_$uw~=aAhu7?R3L@O zE>Dc5W@^F#?pJTW`}w^O9pBK@NUm+C=Qe_4InJ}}*Ss7qRZmb+iT7tZIVa-&bno;M zeajRUk^ScoxNT3J<&?rD*2FwtRU%lVJ(r$PdvD`dj6TqdQlDI293gA0o02qXcMZ4l zuyvoaF16Zy10sPLc*_D*#j#6T+YYudFl+rIaBDOg!f=@qwYkzO|@ncT)&k2_~`S$ee zD@zZUx`s>W$@8bDmrqDlH{Sl{`4i&reD%DY(#`J}mr1rFkDi`>`}pbp*>lVDwR?2T zlv+@oGMeE`Fgtcru%M(ab55r%bShJx<22-6$86AGB`wXX$2HFHX=z7HVFT{gsn_!S zq}Lx^o*nLzRCDKulX6Wwh6rCrO0s@V*H8P5bmGE2O?`Fxo5N$v<%^<43G47MuIeA{ ze|vbYFki}~cDVn|F5%OsY@z)NVwSR$^rFl|)B7gQPc76$?H(OKW=D?)P~$7_91aq@hh_m9sZG_kE3D{WRV z97Za9xW1vrx`hQO^Cz1zS0eR(q)coPHJZx}#Ih;PPa55~WFXYifUFO0KRP|ZiIzxK zkA_y;rWX-M!g`cmIFO-{Wsv%uN+{vL2{;=s4{)1?y+{<(kkaF_-p4pITs%EIG0rd| zMMG7l^!kjnqNk)NfF4PWt^%0_mI85DlR^B|VwWSWZ}%-n;G?HRt}BG~J={rHBgw)! z)D{+7TW6fQmdE$pFg{*BJ-v8#dhs=BILC>j=dM5BJr%uvMV1C0^=_RKp+O{!2rJ3`Ov>%u8yJr&`os`v*x^ZiyRJ=w< z>wYOsUR*4-Xw^@4sw6QY{UYv4J+JG>I|Zo;f)tU0yh6VTLbp@gwlxGfBE=9}Ly&Oh zsAH?%(i}PO26{Uzy``oWrMDCM9ec~;d68981p+0{vza6Jb_K+A)1_|{5Sar$q$0xg z+*Et(BD6No=b(msY?34SSlbgYm0w7|LTB|@R`4Dr6CpC~!=)nPBK}f!nH^ATcbwOK z6C#@_ng|U!ba2~S?N6I}3#L+UX)|_~r|Gifx>@j;Zpsyis5f6*PA{5n-x{TRW9uT| z5x=l%JTx{Lk_POa^oFiNOKI&}pcDNs?5Yfv=JcS%!;<*m`I2ro8$ zj1Mo{RHbQclI77BwZnULUs1%+|BIK`IPM=%Y+8`mL%|ZsFnwPbaO=AONP?0REm4e{B#jWlZqbIK??2e9v zt`tMe(eBEy?`VAu>^x0`f_~>YzuYT*z@1n4?*Vt!iKhSkGb@~LGuCzRU6$mKJ6@Nc z()%*%9)4GiuBWqx^PaTHGI=$eKIJ{5ot5Fes;LT<*t`1Iy?KZ5*&7FW7nI{w7Y6gD z>X<55WZ%4vEulza{F{bg?Tx7xkoVOdD6`I{-2vo@v&S64TH1Oba9DZ1BxNT5@R}z|{;4lX!Em zEjpQQ)q4sT)~};*KWs!9oOlwd6wHA4^)}j@sOcwW@$ik*vaZ=++ z*2FtL|2wUbMW)}T6S?6_-R;(~9CoX_&3mU8kVTSbjqBEmd?PX6iKMYIuMz7+-Y;Vt@X3l)KYe_>1P3G}swZtOp3b>7wn#{AP;NhXkYOhLygIi@< zgUy)h{khD3-ZJnJ>1WlwXPD5NH%r>^L(3Y*MLv6RnI#V?4><>UYOq+-Q`-xzt*Xf- zeyx3zcz$BlSmYy2Z~Rcc?Y>kp_TJmY_H)Xm_GBMOFT$Yr3C4rFDJxd)S+_R1ERsZ_ z$SF_uKRSKP#rQAI_t%dNO#3<3U)_Fvbim!fuKnYlak2l_{v&(Ohx;c_E}ml1nrL+A z7sLiQ9)0(xKl|^K|2qBR*@NHtSO4?ZAOFFx|L;G2`|tjn{lEC>zx?gr7~TKf(H|U+ zes}aIqd)(XfB2vN+q-}9xBkh!v)|7C<3Ia%Km71}fBS#`-Vgufhkx*wKm6n;|Mp-1 z^Z)aw|7h>`fAYhB{{8>=zy9(c|M|c8VRr97{MoPX{n_9A?eF}Z|MBS`LOWS&|WiZlYaZhi{o^72Z*fu$fw7=`W0#i8=mxIac%fUGM zv2NOOvuy#1a4@q6d6>w>!MMQ}U}gw(<;YkZ%2~QiS8gtKyN8Js6^;OlbVF~FvAKDYuG-#61a z%k{;~6&^M)HWQs(ITH~)Oin`%wv;;7!Bzv~&-~ODd{>7Xkx?y4aP&ydlyD zyv$AII%7JRC9d)?=`}q}Td9MY1Ga;4lQ)!GN0*x`hvf=! z?Q(HFlv^k_nk%=IP20l;%B|!S*p}ncbQs4{frflJ^A>P*#}b7U%b8&8>Mo_m^)Shh-XQtCcdR z`EmpNbTyG`-o4NCr5+Y>%5}vsA0(}eeYU%U?tKG$&uXQkXj5)V5?D_M)9q=X1AIur zo-*BD48T@8uD5NB$#ENCQ>l?$Ibtyc*doyVbi9gn3Beue5`W0k!IbSS;9z2E|F$n! z>%#kRjtOJKk3YO`N(2Ev4wj5x3-m6~% z>aGI6nvKa&?%Nofp|-6{65LpBJ^)(=UN@U4_puwtWD?f?4EHm?J|Nw&R@qqnoa^Ob zpCKJt#(j@xt}zC=7+BboXH&AO`#zJh+0)t#F*b~i&?|(LLpsfj=X8OI@P>3Qca~H5>RYPAy>_%{W7Z@p2S$@1u!X zI2+7%255+_!i|HzfN9r1n(%ybdqEVPfGd`M=nL6hT^n%3?I6d_$O{+ffTU&deUrd< zp=Non)i|XMU3za|W45EPhf*%^XX5zC+0?y{@FN~Z*w+Y~1lb#tu&iJ0`xfTy;9#bB zaj=!gJ&bM~&L6X7*r&-;9QV-KavtPZV39X*x8LCp!FyZYFKz!;<4EHxC1>;HnDqd& zbjI#|3%@>ScL4XRxbM%9n|vEP_+$FHB>qpJm!0jMa6ZSv9L}NGm&4voq^d9mk)6nPfGVlZ98HK%jXEF)&w}Y|V(vJLLQH?}N8H{tSR9UZEUO-#m=v7Rte6W4S2*;S&~g zOL$cT9zmF*z+>@BkMp)*l6?O-LU{gy`*oxZbmIYk1bXs&1<}XCS%Y|aK|e$augDLU zE58q-YYwu&n3G1(LE%9X@w}RbeUMW?kfqo{<$YJv?l$Z+oTtgs9_M_;e{Ed|ay!rv z8Qg;o3RsYR*t&yG2G~FwL~aW0Mm~gUF03uQ0K*({>Ivr$U}4Qjj0o*AApr+tMWSzT zilT)e9xY5FNZ7Yf9R}V>y4tW7P$&9*3xR(g=8Z^!LEgp7F`O%a1)hU?GVI+*z(EcJ z7WSSw>J7infq5AiVJO%VBwqQw0+=}v)^rMN>E$|RBgnhJEUZagm&3xg?!-9Q*vlo9 zn+}v?_YQTJVb0m&{kdYi&1MnW#W&mWNhNXid=ePDg>RQEbB?c3Zt3@AU@N~LAw_I^ z8Ui+zA6|obTX3okI!2vzJuiVz zaxbv$3&PMUu2sm{^L9?AL06B|6m%mv*LuD&9wQHVT#Z?R5f*G?L?(qQ+dc#1>P>(l zc?Q@@H?$g@qTL9zi+0klEtC-pucJjXhsMOeZztHxC`WSSP;NO;7v+dA2mU-gkG1$i zIXW0%12`p;LcjqjCEWau$AqR0_>1xZ}A$5B6Zl$(X5Ac$wd** zC)q56-V9ifW04M`jD#z8+#45D-|h^TrMNe;&K<5Ohf<-%0WcPvhjB)9cp=DX)EBez zgUp4dU;q}+_Us;UZ197yC!+=qeOWF1oD|TLaDxlD8sM==NMXNZwZ%R!gRPtx-$6$U426@QH&o`X ze;oFMO%9qU_TNVLpXHnumgcR6C0wmxw1K6m?F%pt-T}54faxxMvmdE{neNwZ07J=W z<<>A%g3(sKkb=B@ALXKa&r0z7Ams-55XvgoKm3^$;;88}Ft(5Yn|r#a97ZZ%jze8& zgTqTK7te!OKI3>LN6_x`<{$$FoF?^?g|)7RS0#FN#lF(2F8r4!|Y@u;}xI$|%@P5t{;kre=^2 zh%_E_4@=moUq`^g+FDL9%6VLIG<7@?vMM&s9{0e)SqSp;=M?Hj9)V=*^^;@tOPc_<@i68gNd~;%0>M>R&0-Z+Q4AZ*2N1io+XzUFx@^)xmmEG z;lby}0TtNw@S4WCT+G3&ZI{_a3(&%7!`z(fy1;<8CyLP70&@n zj^eSdqEEKtz@@{VnV>2QkkgD8k5sQyke8~LcY#H}O;%;l$*i_<2=RSU-j)ap`!pVg zVO^pg5A+8toNGa;=NqgT9Arbg(XPwUG>&6zIWAjVMSf2kK?g7+n^EiwY~0fU5}}ui zsY~7!-)C%Se*I!1@Vtqa1%2m)ts>~}u~q~*jALV1Z@|L(SRs&yalm6kpK+Q9eU{eT zk4?sC4?{)k`3pzbAd8|Zh<(OSAo16jXfV{Qy7>xwn4uJ2?`VBmlaDU z-~c5>zyan}hXW3NiYo2aGy;BTmpesaY>T*8U~ve(2B-l1zQE2Mbj_I5F@E~K;QJBQ z^Z-v>E$7~b!S(xa7NU)CP6jrxwg%)=%=^I}ipP$Z1$3n$(hv(Sj6(~_j~8`Qgw4GC z%;kk>H(jyKczF$2kY7;0gmdGHiF3H;Ubv-xZFv(%oUmr7?CP>$g3mj!vEO@WBgkBo zn|S&IHuXH#h&IvW`V6abURD8yGNZ*au)uR*bSV2mIRzSR)(3TwMkCCx58OP0zay}4 z#>T}f=!PjbfCH9R=pR)hjH@_p|FFm@KZ_d&ejaX}Q!Myk2#Na%|s*-*~n>8R7?oQDT4f@dg)_AtUC4Y6v%czK`e--I-55if`gZvD#o5orTA zc=tZaE&Q4W7S4^pR$;GTCSw~oR69NnQiy- zhPpw{!Mn-sH@EmBMO+x$EXZNB5q$1A!NSNxyTKoXy1|DODJSR!1o60%SE;wzsGMKt zz$^w$JvJ1+K^~$U3(5C6`hij|_?!sl_1VCJUI19Ii3sN9PCS{y`2$$k1Aqk`GA?() zo(yb&t^kGkMfVl>6-izLA7bARdnm4hf!5-=OxEduE3`iTtcH>++!YiRct0atiNjf( za#Qa|fmcS*0Z=Z?5!cs)o|ST8O=A{9m~!-j*3qAtKwi+BK|q0rp}3CwFGoKwuc0v- zhjoc}leYX1!E@NXk6p&= zXek%{6p&y1`FCfG4an0Uc6)z5r(Bc;z+S|k9H!d-;Z|x9v0A*~a1q+YF67q{Tqwwx zc<_1I7H$>%Mu4sSIe7klYrkziao zCdrKrPo^MaVs`dt5)90`Ua!}!WkO&UKGDOxjKo|<9XI{+`=DHy2l}#8)A+^NE{TTD zMhe9I-dhA={=@yn-rnwf?-6spJa}|)fT4AHfD&>KP58m~{_Y=*{tyqi)d4iSx3m5D z@g9#6PUf?{U5S>*P)-y)JQgpvKJ-y&xNHzM`XNTYK>)L$^FF?R& UyNOSKNdz(a=}+JOC4GEwSlPy1TnWx*O^4E-7g#=@5_(r5ou6>5}elkdM&k=zGq4&UxN{ zzMpRP-fORW*0pA?nR{kf8xmPTVJbRm1{jj|)uYwz569_4?O$LR0ki-sz1J|DoB$e0 zLrY_O69CiwkUW4!*wozK(Dwe>T-V-E&`{sXz!1RA4P$3-Yp81h;|yG+G4pno8QEi9 z`G6qQ1W5hsZ8;PO@iOr?;0yb9i=}rzH!I4*w@s|N8OI%W^E2g76H&&CtfuR}0xL3uvFHwkz|K`M)rS z>Q_jpcZqr=&!kX=kWzgtEYH>iuPnrw#)?8{t4bv$WKR^eVki63TtU6XI9T^Jn)H^d zpHYhE6Ra~tC524oT=2V^jTuL!8SI>Kp*{suh1B#=P{H%Y0YPaQJJ)E^uQ(^rtBTXH zWE*dd-!ZG2VMpTh`WPhFhq6&x^dOz2QD=QQ&P^-WpR$m@v3W{#>6{?tJG z;X2d^V?!&knD5zfMAIo7ylzE|OQw3Y2YoY@Js*WJO}$2J@zDrmWKb#tY+;CmWyhIK zCthW8m?*!iEQlZ!<@c$CmKhNmkR0(NhL08`j;tm3l+3wbZzzSSu5yof#qX2286wZ? zs*uY3wQPs49|E!|ROX1(Ty)g|6+QkW;Oc=NxkE5=m7$3j@idO_y?xF6O4sW@~qAiqpgq z0=DQuv2e=msi?FlAAzul`)Yy;2ocX=OFhS%8sx4x$4lj@wz9pxTtDl*UKfDwFI4Pe zk*Cz;dM2(hQ!N>dab|1WR9`B@#CYIE?RYldAw>P2I(2FV*3G%x&Es^8yY{Z?R`Yx6 zRH*ijGKM^7ldCfxyaglRs6w6~Q1O&sRoI;yNe|O8}0Er9w1-)0Uy-0~+ZH zJB--Ltwr*%n>CregK?P(O?gAk9uVua;Si8#e9ncc)ZQ5w)^Tnji9AL2Hoon}(m};Csll+K!TY73ot1p=HJj&}J*nB{EbQvR{K?C+~Euh zqGbq)HGw$7Nw_e}N``l1mDnC=b<^UvG-IXQ?eJIlkj&aLTtbapr|v>8>p#rn4;lod zd}1C(62X5jz{)(g>ntWbbo4wTiYwVU2CN0O+NFY5K8&G|^bky`6?-oHTIjEZg8kIL&DN7;}#Hs3UuvY8J9 zLN!u2@Dp@#uS7;;pviowxrUj;rey00V)4qJkvkgzs*S7K^B+wS8Zq7KkuUzC>U zuJ3~DaEtUk8s31@mXE}uh;CICyAK(#E!~~{uASu5nV=`**erbWldwZq@80FKzBGG}GoLuV2cpXLoJR5+ zjrqvZ*LsAUEGSeQc~_2E$7ueKLU3OEj@Fb#MjvbAisDctzS^KsO~E*SWAsJlje(rk z8AH=_mk1V`&(s|XtR~_8_Q@Xo=E8|5vlyf+zM)&?UA5;h*Yi;mQX`rTR5F5D7dUDI zw`XmmB=tFqoK*0z0IZN#k~Y1BRY7z-T!&>5qK9$%jcSm=JdWMsE#DBwst_O&a|5 z;tZkqu*&Fq1!-p&N9l-BFdP02Q7dKZot!gOzYQBr3-5)5!U2d<7P$`_m%%#>Z%e!H ziUZi6re@(_?M|B``fe0Yt0OgFQG{&X#Nd;e)LMyH@C?Zi@0O|CiyAvXkbdwk9j6kA zu0T&aHQDM=?WG+)*8A>K_2w98(j=yEXomd(g|0oo~{} z#N0t*pHETa&dCnZ$3>H{`CC4CDZPWxroOU&hK^|@8BhtjUy3maSeRNNqLTjHe1gAE zC`_kaxdm_g%>>6E8Ywk;+QB>!fw>G76;kTWt54u}lXKN=1!9n8d9>wI$**vTkjrA2 zTB0^!(kX8z2;POyEQ_S#V7esGakE)dJS!L~h`K_IXBpF^<&5Grq;YXzH4%{xp`+Y| zkglyb4#C;$L&g@+k{GQFjum#yO>?2Mcpl6C8p-)J7&^3ImXUGvhI9`kRRgV>(Q?kA zW(wB^Gp^7Y=6r06B)tB65TWt#=gSi5Zyb?X=Y3yR77louu#N#-aYmvDa0j^M8hmZ+ zjiI1cvguFXm_#M7b9YqZjgKy3iI4gadL%G?^?)hBzF7F+azYTVLMPhKA?j2OY({s& zil^3$dW8z>lv%TIcjpdC*}zK?T2Jm*LYi8@)|iN=8@9w>@+ zP849~t`jn}Bs$vI@-3n507NQ(G#EP3iO|6Zk?}c{fv9o$Aghrqb2^bfaKv&FSAwrR z%8~`&W;i908x)n*H^r&VaAV7FI$NlcFn1dt>GCHOlxRToTt2xhL6@1@3xO~o`z9rF znx49_Y1Y`N(bBwaN9SrXR9h55>n6lRVKspA=2f76N6w+#8KyO_Ha)$TU=IH^HdJ0v z3Lv$E0^tKY{mMITuDYz=RQQMt1n#vhS?pB8#*#Z=Kxs^7K}Z)Qv`=tDTLv%Ed6e)> ziB*A3K4HgZ(bhA(rq6C0k5*xR&XO}%m&P7XLh9TSW$5Nyi5AWZ>MdsCQp;f^03#=I zJFY^@6=<-C6oI|$)Gc?Ng}=_aa1eZe zqN1}^q8UxgAf*XcWBJSZT0gC+l^UCQOs{ToA^Gf!{i^;|K+DeihMI`w-GcwufD&#>YzYhwlV@{%ow5)V`jw&1YYu@C^q4ReP+_EBLJfHk+_heDL zVUl14hCtr#JRiatJV+Ptb4NzBs1w`~Jqd!Ws4QN+_+rhS5se~e+aq0ItYEL1e7fh} z+F-R3yL9Y#73L!}8&n1nN?7=1nQ?UGTf}Hzg`UjR30zQ|`8tA4wZ>+(vXB+-d2ZAw zUrja9i|1@g32U0Mv_V-oF%0M~WY-d)e%w?H!{cd0)&$=)B$Lr>* z;&3+(N;@?!I9ap?h9INk2^#8scbbP+Pa!tLakg;`6GgRqqH3dB$hp?CKjhF1^c9?N z+R;U&g7-D>?s}^j$e+9mw^|#gWcDP{I>J#}b=vl{*|lE#qOPqJVRxAVNk~&j>M zPRD)J0fdAKj`wkf5v3aSYtF!@_n?-315)tt@^iiXtS^ZMHZ@#Rn%=}c8;f!Q5roSJ z2QkzjyXIAW=O&PjYAaqE=OVh)p#_clMiYI!b}_m|WF{$iF_f-c&U!h$36lu%=1s#~ z4K)-s*{&l^NwLv$6{T!7rG&WlF#lW)i!Y>fBU7R$#b5ZLkuKv{&24|RV zI(S4K@?n#isy*7^`t625;Sdz-F()sY)SZ~$ua86!(}qKw*VZXmv0?M?ySudF!c9Ww zVG^}XAqci*&OWdADVHk@<+2f0oy&(!3^Fd0;1`o1@T7_C$qesZ#q~ca7I9 zzT|!LAxl*$9)l)znq^;0tFfGCcd(H7%gWk>+4atvpJn1?1~~$exaPsQ*xIqW$8~Sr z-1Y~andEbs^Rps2r_&Xd&+Z}x6CYq#s20d4-z2A5rGDsnov#+#$B7a|+er4xYU=EH zXDlM<$gdWl8}t!7$ZRIj+Pxjl@oWl_C5+{U+~b2=8dDZS z@a=2+-Ue{Tc-yOi09Te7HO@EZp5l; z%yBO>%vAX&YTSVY(UE;AFAe-w%B;m&a-PM9)xM<92Jg+($>_h=xxJ}z<2AOKf@9S$ULb|YzZ@WJtBpXn*Xq=bDe$| z6QO%PZjcBtDRKF=uattKwBPVn$uYcNA(3`D9;~1+%s}kU*?OKCULXG`a!y)x;=EkB zG(b!a&2*+mPcPPw2b6DCsfG3hTW(Kb%no|gCZGqxLqiY_KP`{rO>3GDIayJN^n4`2 zXi;W0Rse%AiITYiO;PmzjzL^@LsV&h;aeYfsC?><&pJB7t&R{i?l#$oNL?+iJ#jY0 z4m0B*0#i9VfmE;ASt980lZ91&aaF%q}#9zaNTP(AA@WN{}6U2 zFn{^o%O*;O7RJ!h;77CO{?$W|=b^Fl@XW|W$N2E+!)y9q`ag;;)`kEYSzY7DpSFgU z_5g;bUXZ+@ot1;FzM&m}`DsAF%F_P+b34F8|LDG1BxPt|s>^TX3{a=NA7El*qNZmB zurkt7v$AQz+;@$BtoCsA&u){ft(Crlp*=wTen~-L0F9!dvpql)KqFvfZe^=rt*dVc zcxXxq*wF!49v9)}2G9sO+lwgJ-#4TlKNGQ|gQ0uaf&6`E>b_HT59)DTQIQ_N_Bc$V zs7ME3{rQpsK=%OearPge9wq!l^`wJ8mZ8zty}#m@$qyR&Ir&G!jP8dLr0>;5!}riN zvwT2EBWUPos&6PS!vD|Bw3K&FiV9QcL4yq}4MWI;0Fo1IS7^WphbS7DT(Fq8EJ&a~ zsiPpcFe(y-l7V6~ut*T0z7Q&`pRe8o^bw-$mlioeq1CxCGwA&yrsMgf+fLPmsrAnj zQ^xxq6SY9HH@;x4_A0>Mj2SAZd)qA<$SA$-=OCDf!0;--%}Yi`U?LZSAY(TTT#1Q! zqF*Y`_NcuL>SkUx$&%WAz4MA9WDy{S0D=)3XXf-I#aIQ2RjBB|qQ3*{IfBwTJUF+cmT_M)ykA)a(416y?ftDZ46tWI`5iDW4&3@k$PDB*)nSFgr5@dj*fy!9H_noSCzWt-X)(M@tlkL$lc-uGnndju;q30JeqA%U zY?@lY^HjLy>Toi}ho~A&y^Tgi0YbB0c3?3gMF*F84 zq1D_Exe|hXeQ@ycg9f+fh z8ej6lWF~{e@=P&9HF3}e@(5s)trmmqvf=^~AOa<}eN3Tz4-nBZ`3fS@vk?;czaljRNf4rW zWMj!mQDww&g!reV*~DvjU3gsta^#=oS&pd`B-Ey;$utOhhPo3f_G!auT3iv9?Ca5(&5jfNjfN9w zLlQ&vRq|DwRqWM@9(jmXTAd39d90kUn6SFAh-1dJn!)A48RimGt-E@PxJyb)O7}Rz zIHNe=xQGJDS61Vw<0%Ce1u|o_uSs9)zMgu0Jx={GeDK4dQ|e%fHsjsOP0&_pZLn+g z)$7x?Ev~H?>?G_)Y%J_o*zt^Cn6Q#8le&{ulhzqKD^1j3tH9J&)z+)DXyX$1`V@^w zDni*Zii&qXPz`fu&wb=7($A|gR)0OlF_7_s++Z>PgD@~ahNhoEsNs|tNtgOkVEceb<)z{ z6tQTr@NOxE$+3Awo!Y_5K{&HD6H>;HOm&@^rL(H4g~DIc+1fa}+_PMgFJHiNg{p+6 zpwFRezubMPK(+l6UzI`?T*abttMsaEx;@EEQ{SUA@b$8O#X#=dT2)a#)q-Qwt_Mew zX6YPyopjyc8T=XHHR&}SBv+7gPy_t=q`f0~oxjI8()Pwu^}u1+G9;NBS=p%AhlLc? z0g8cV16iJw>V8UQZkle8J(8%BpU~MhHEp@*5G$Y!?8e`}n{5JxQ z0i4iGaGGdWc9_&J84F2wT&iC=dpbLTQRxW@3bfaWMs$sIiG+ruOQ1UmcZ+lgjf+SL zxe3J!NyMwXRIOMM4pR#Q*0r*7lt=cVDbLt{=*V#Q);V#^>hd{v7M<=RI+EBUdJu~`%h`NR4Brv8-|m81M5x*?@6G#R+a8fe-W zvuFisdrZgoUZG_24o69n+o>AWCA|GEc`6!4oJ``U3S05IeBM~ZFs(hR_r=4j@-b4C?KuV>;3wT4gidcTEylOdUn z-d<4kP`;e&JM-Ud$T`coc3O0Va+;mntUXpq)Kk@(8Y*7YEL!b5$~^j9r@Ygsq^oYD zgJ2qA5K&2=SKc6A@>*p4!&df|4|YV-TEmvx=iQX)&Y6j(1q-iH;Be3mNNzk-oO-XW zb6K+@K8p_B*`>m^!R^eg+9qQ!Z7;!H(Zi@gkpPjL&ejNragMPGmOPeN=GO4&FJ;R% zomRJtrvh#$A2($gvT82CSP9q;zja>V@2A3XPPv456l{{+rk#BnQVUVb_&WPF_)|8s zshgzR!lw6V3hn?d6;JY=`$^bYVp2tqvBY510NVRxwlpWtBmOS|hoQk3LF~%*+_jY} zBPqqD#q+&B@g-V^HMaRP4c9lX;y)S}vuSd8q>bv(XeS< zar54t;g!yyOigkp%7Pl)QxU;TC-f69`m=ukogE&JbkXLw>1w2K!;};V~`3)yK=@UAG6>p3q0;>gR?&2;>WZJ+7^9NrDR|;>56p0Lm zU53rMmYsAQ^oGTcE(bJGeE)PFH~cZp)9JdwSg*VNz-)Lskw<(!uaWKQ>zT#6!N$s{ zlj(l$hmiS?jXY;Qw;9*^C!JB7J2DqCacSW^PVUe*N6w@cMr#Y_{>RsUPVXPn(8rvf znU4Nv#{G~wKaAaH*Q-k+J@^+i+#T$EgCBDz}gO4=Z)q)u8u$0v(I>=I68Nm95&!`m=I-h8MX ziG&zN|6LB>(wq5-4pQ(uG6;&OFf2ug|u>gJkLp#Qud|f9jB) zj_yAy`&mmplqmnEZle29g#4#)dQ|!^l@rWgR8GIv9S_(4tULZsrPD9f(T|b{=Ds>& z1kgQIO7{iSA5{)L-7l;CUpP-}v6=@UsvC@=^F;H2}TS_bkb z@dJSHEiuCWY?nts4=Q10`)L;Xhw|>Hmi~=d=zkQr|1rzs&i}nx9y8H=uEYJdO`luo^Fwk^JR`hoUnQw zhF9iYkLkzOkUrg`#fEw-2pn48BTQreo+vZqnl?8-w|YVfVSX;(H7v8URW)dR5(4Y? z?>%|Rt8u984$?8goolcz{Gr*cuHgpSv$3@P{SY(d30PsMv1%f;N|C)#Nj}+?FsiTp z>6owD82Kb zS5Pkucjud8TsqfS?XSc$bT=+fYAcUQRb_x)YnLKm7RhxXZud$B!`h2S0wCj``W}_y@P8 z{~0?T;>7>p$G_`%{Da%xyYOFkHRvDQ`X@7=wyI!n>!5G1XlrOFZ)IfQ zA^%a(uVLobtAxxAEdVqM_e~8eeFuw&))0*>;Bk$6XOXyX;xIgQRu~>4hdhAcDOfN( zx)%(?Q-g%zNp}oS)Mj{6ttQ}?xbrCFuR#B2dD+Kxe*{xmW5AD~BmtoNP0p|J>o2$Q zzsjNiP0oY({~I2L-{kz-HTv)HF#aaz&-{Hd>VNd3sH5Jhh(*LgU ze>AwFsG+XGgTxz{CnY|55j){qxpjxeERkdy5^7FMRFkvYkL=fzM;7}#XW0bSbx>+BZmKCg-7^L zjQ(8>zee-_-PS+0{0q=~4%7Yy=pRX=_}i8r0X_NF?|^;=!as7A|3R8ZXZWG~-vRyR z4UcyDZ#DE6Ue5SyQ2ZMWJ<;HIK)<`g{}$u^a0h-X1D8LM_lbavPwv3dh`sYCn_^NQJLv!4@^IK@(ZIMC;q)Q1s@sxi@PxWg?S&x{#MSTd4J&g zU7No-z$4_p6{PrQK!0IgrhjMN9~$}%(0%^)%iZ8ndH*e-CvE*^My7u>~G~f?)C?+-*?OMtNI@y|E-`O>i-qcPtW@m&_9~-3DA>< zeh0+%8=!wo5B>tsU$`*yzl+d6Z1o$U`+V!yo&EDInHX9i13f$ndOP^zr}~gvA+fR#D^zbzweg$ zH}(IcU;hQ5pPu)N8Cm|(j8A}m@cfTlN8%y5`c=?_^8R*be*x$(T$try>~G~f0{UUA z-!yceko^ki!Hj<^=yyPWZ0&E-xcl7jmn9x=;tp`1h5fSIN67yrAl4_B`VG+I*x$-|0`#Pz z-!1%`4?HUGZw39Z@Q>*FkhQY@bnag^_Bi&pa-IM^+3I&dzx%+yjS@cq{X5G2b{}{w zZ2uYSk81k?E8P#nN&xPI(=WSztd0LGN%TLmmU~IRxxr(3{ePA8Th{aEVDxZA#P078 zeLS4waJcUhFy8xsse#?Sk)HzZ)5c*Q8+G!4AK*2A(E#h8@2LX1_PXX)#`nqa z6D5s`s@i>^dOE;z|KdNb@;|%nV}FK5!9mad@$`)1{hCiD%|oyK$A^+CiUNW!L=DXy z4ed?u50pHfR1q|^)3-IXwzsl{Vg0csA*Yb}MVN+W>djS(& z+lSr?jim06H}v-%NSgbTxb`NGI%Z*od2sHZ|LEB001Wp{n1{#182x=rdwBiue1A;m zzI6EY@pRQMBQQ_%eoCVSFh3>+EcaRQ!{gx{^S$m^?xj7vf0)fg`!M@K2J7RR51-x7 zyGQlw6+b0C$@@e8&ubqs+)Mh2;QFW>!tlk{^pzd6vK{q=!{f6(oTe$u)Y_kALTKU33O*Vyin zV-HvHKc4BLV!MwmRE+l_nU0Z>2|!0jPp=6>BX;kVruuxA#`g^;+WUh~cKQ!(L6-X@ z;Qr*((;F)K`;H!sfUdQup{cRS{fBfc_iNf4S||b7SOGLDPs1=w4x6}!#_p7Kiv3n;AhDD(MP|R_t200@!iJz4X`oY z-$4KIml)&jF7B^7&<^@$Pm5fx8`oAXrp^Pvj^L(L4CO@g_&)IhMF~NL>SdJ+hrpIY zSylsmiv~ghd6NSJGES)?)RFTEB*I*;SGSGlwnl4xvrhYo z48{ZR?Tjqf2Gxh&Fy?{NnY332+vO5oDjWm&cMiKNNC(6g*CG&GlSx?y&q=KJSo!;s z^6hf_#TP@85#_m9cRzJ-HGTdLp3zq!6^`*ZkUB1bA|O z?%4pxn(uJnp2~sLmCqb<>5A*q{#QW9Gw~Z# zPh=@Rts5MeZzOr>LI-PH!!z|L>6HRR5-}mNRLY#TeeuoI*XVjTX4f7q zkf5x*`)E4dbT?UA=g}59?6Au*ce-0sIka2cdbTQ6~%)kGLuWy{2XKdEHWtl)8Q_^usDc-$@nNL-_4|Iz*x zqfAbJzfX^*$?I_`hq~M^*e{$|I+H$vpuE^Vmn=w_u%4?>I_Eai>WJX{=sRzY=kb;J z(#a!roHvbso>9i zhN~d{)or4kxco@P;B1pGSA_De^}QJw<1P2N2k*}>n<||zB+6rs-=e-z)Cz>p(oUz! zdv<678B1isIqfgKh(%jt#fQ7Wc$O}y@?2#sI56|Gjng>=BN`X%#Gpt?)~N}Ge4CXK z0YhbRN6Q|GcV{{$G}X2zc22rpB?vA*iSCJM&!L3I5lYpgk4Kei+Ff;bFeXLfs!SFU2Yxplq`kpIi@SJg-{c}SIhagw|lvJ zYkM(sO^~Wp1Q^)Q7ILaV?@~?mmF;=H-oE+__Wn(}qVpD5B|I1Cxs~6(uI%kAXVTP& ziKSCw;XM-E9oqrQ4j%-$pP8P#e6-AFa?z3J)H=;XMKZAE<`5ZXY z{xRFC7QqpJ`#X@TuIdF=QqW%De8XGm%+L*FqeU%hoc5e*kcc2F*m?f5HMg0i++tPULVEuol3SE{*w zsH?m_pIh*Gix!11oD1?yK?IT1yvo z0<~jN5Gd!v))H{?t+W)D7=FEmURiZbWJGp_NE|SYL1n_B?lU_pjG5vl$1FOGu;4(u zC6j@nt5px=&QC5?tD5TMxyOY1R!AW$%9=a@S@i5$N6MNF*D-lqj~VNkYl?7@Jf)z| zx=NrY^Efl!>u`&-6502qY!rhMmhK0MxQ zf#)cNNvt^@?OnA6;vW{Y5gcTh3gA&bsKy0H zr+j?ZCZ6TMu;(UPTc77ry6&8{Gru>qF@B!&Sb^;d3fnRS%Ef_#17V->h*=E+^Pbd8 zVnomS+TGDib}^>Ev1Xrctiu7EsO!6{VLjA-_HJ0Y+oV7ekwsT{)iPj?phO<+)cV$6 zNs|YYqFM{%pvn`p_tKwtLc6#maZ;fT(qREGEectytTcVt{&Cf)HVGnQLh2JQ3hsiD zizG0@WI=I0(EIYhky%S$G*7{lra5Hf06Gep%bz zG8?4MdWG{->gI(d`;h0|ndd%f&8tPH)XAf^>{kN)Nq5!et6-3u(Z zrSX;*hDfsNK3idrkW_LU*f57cefJp-ct9u zIi&9;yNOrfKKxMXJ|d%cxuPXrqJN2+?m}K^yo;J}g^#07N3O-0TgwqTciLjUn1aUcjk&As;jb@GSEP*&!vnJ4t z;2*lw=Cl5Sm)aLR7wnGtP)^`LIyBm~R1gA~FdOo2_KoD?xq7ox6<;_m8^EDw(18dc zoTvF1@gl*4r<)uo$_D_GLF>FGcaT5QJTd6aDu8~W!e;G4-235=ILQ| zct@cX(qqsAn_=v*$n_mKxj7yWeE!CTKH`v8s?g8Em}eDdR$8k+VsRf$K0vG$dFjL6pBk0!3R?_S%$ye{ zb@il{r7-$sk{2o%RoRQ+w>hl`UI3^)k-rJTg`G7EBC7Ol3BL5s+ZNm)2I|P@bB8hP z0`J1=LQBg8Ee{J5;{bB{Hk44g^PaOo8y_N1XTnkI64Ys9q*>Z=(h!Q$H=FBAUq{x8 zae$ui5vw2T$8n#`u#{ELRUQ>s;Kn74ppGDACZ$x7)wWvOayk^91M)j}9w+j7zQ7T; z!niLn!LyJ6Gf+L47%kn(W-e67Lw!I#>5Yoyb3apBMPnBA@0(2)u|*19n$!!QdJhw` zwr`75ljGYOf}6J;ltSqugWEE!^LUjNO^BEA2Me$g>nso3#)tJn3o=ds=RPNUercjj z@!WSaL?g<^`;EI%xcXM@Q9Q+Md^Iw0yb)*{beXLQYGXuMSR1-Qq*xSUr9wc&6oPf# zH!@zADZ%g{Xla@PLBq0&c@qHwlBtmDT$6<;&be#deXqqk)}HfE4Ikz}W*=5t?*>gq z+mk@O2Zq|HBxIaW)L)QeO#%W5NPq??Ac`Db##yQ1-qJd&`9z`jjwDO6FBCYVF7g2%8>%YsSnsxFMw^ zfrnCO);d|2!y`H}H`<$Q!?*oOh=!|%`s10QWWD1i2USAU2=vAlr})gyyR%$yT&Nz*Cr^Rw{^azll#jItpz9 z{)!IQEaYvTX{63x2lX6_rm>uhYJFk7gcW%=y@~|wN?poS?lRx= zo=Kcf_)mIBc7Whqp7nN&mlUQda(1+sln9>dx4oYdfDs zlXwDNn`XwyHsFgYP2M#;$?X5)G^I2rF8k%cd#QT41un^imGKp=wz%R@=oZjijj*%{ zhfQ)Xaw~&(s78h}0-`Mevn?x#5eZOwSai=PmtdTUx{xY`XhXqul+lUtnoyk4eY2G@ z@B5wD`!$fxfzMUsBaMNldUIk}BKu&$@!rS3kXZ$LeoXP4pcV=TEI^MR*qrUWfL*@9 zYB#jRoCz$vI5Ouj&?On0qkw;QZ*q6?rhDP_$j9~X8dsf8y5?M-UYD~{p?IcWF2n*a zLh#?SdC7G|e^|ru(!VR0bFthnezzcsT#9Dk1l6UEJ^^xUYhM_tFeO%2kX9?mpkl{> z)h11N4YsP81m7?BOaN6SYd0ubB_PQ}N5bKg(o3cKyzz+*19)GR6i{h70?2Lgno{r?hz^29%7<0a? zkUa$5LVVEB4j9Ka^|B;7=R!RV`34&pOT|9PBufoUMYxeUQ!DsB*mBc z3HI$x~{c z3FM&d!YS@V62@f%R-QqV+b>{Pg#q z+@u0{943RviFfJ$zP#4_PKwZ^w* z-;`#Q*_hc-$1@iZf?#9Clbd)u%fJWpnZtQ|z+~XfIp%Q`Ol8MhWW>uz)le>d0uG~d z1ca&7q76Zx!BgTGA*TpHOYJ?I!lfiKg5yILmlh8Oq|JjCmiu*J$^&B$T@g*1`Gavx zLctWYO_qAHz7_OgWv>uop4ob6z@F_IhSauYDD%Wb?q zZ_;Wefqh|FFT;K|j}=|F-OUkip<0C(p|1Vy5)q-+0h83K!G~z%lFBQ((M!Vha@2zL zB=n5z(~he_zg?`dcB|l~&TY9z+;cnYe2|zBXTE%IlE^j%6$1=3g`3bl@t3}U@5)rM zy6OD9OKswLkQYa&?kabQ7qzKO!=R+DUZpuuD(i74=D_C5dqoAHy)S!f@aZnPfS@va z&~Sr7v zV7MINouwEb;SS&CZtGtiXrX=sxf>G4UK8#N+UZ;t-+)oFqV&9Gn6RR?Xd?O2(^7bD zu)V+sIyQ#=uHG zV^Kr76~b___|Rup#3_p)VFHrWU;GVYnH{Lb>3Ip+FEnIxB4q=D?<0mCsl2M6?-gnp znq=dBd9V$qXo0@!6CI`OR0?zp{MGOHwB$Ns@4L#?+P=1ztBBX&_S>MDY=pYu-gPZb zZ!^MKw#~kls+nx3oUCZ#+oQ{?AZkuelU}QX(CV!fd|n!4FwjA%z_MX)GDfO3&HM!& zKuu;(j9_FZWeA&akO0cq^zPfy?`z}AMfg3Sig+{;y|@Uvq1-0$hahC7AVi2h%W>>T zu4X=hy0WW!Enrtt%&+A6L>R$LWj53bh=7|?53ieF(?Ju5ba#JwzJ@^UKuv+Puro29#pVlwcD%HQgXlEXIj}kNC)lfWU)_5D#z3ffn(xk=F;HiipqBOI1+Y7k9pDJ@G3X7b6Q%z{GSIqB_6&As) z71#{er{I;o3B3zg_96K|j3Wwq#_Uy(2v$&C(XKt=L; zA7Zi}Mkc(~wM&rK9NK`QwBtNOWu2q5i17OIetU(BIvEN&D!vN$K(v_BG~E&OrC&L5 zHpi{#F3eSP=q^bNiZJt94;dQMV9GNn|J+2(%`%tG^cdxf}0%i)RkEH2>ml z4TZ3mk2XG(-wDAsD2=37+NF<% zk`s>LC8;={K>%QojZpgcg~ z@Q}}`0A=a-Zz{-|v1Y|B8yF)iJ4AX0Ie=g9=sM&16`o9v0Mkk>R$`e5PC1aMPO(|1 zH3y>{^h$tc&<)+vF~AnHWQM4e`+hziRg}00_g8P%3-Dz^>~AaN#}|aJpeOg<{u(T@ zcUV$md5{+Y-1;(^LdU2DWCY(20h}4DkJkf_;iE~xNzi&DJFDGZ3|nfJ^S+O1$sQYv z$5m!W1q7asr-TKWHT)C-FKCdhp0FV1H(jj@Zma=b4vV$93-`5Vy+ldoEx#_69 zx5aDp&UhTdG^**5YfmDWs~3b>eT?F+Pm~H9kr8yEBGAIXXA%+UjN7TdWZM>Weq&$r zxPd-y+(M~}epQ}CS?*g()Gf@P+4Ck>MYS-WKwpdj$yb3rtTWGnm#9RXv zUHC|E-bFQbO!XJoZ;gf}NK;bwfR;`$n89h9cd^ZByDxD^{28nQ2*9<02Ud4R@;D9$ z;6aXnMjh8J`4i%1OZ!M{95x=~U5Zq9 z$b19mh@+Pim^H$>$Fw)Jis-gB!fRyvr8^)#YbOXRAZ=CK1;-y~fY#BgS<|~Fg}5_Q zO@H$R0k0!m6W`lVve+Bm;aQqsn==$t>TkCBaBN|1`n_^fbEy$mFJ&4+V>l`7qWoOn zad{e2pT4jw6R2F3Z=#RCimMqb=>u;XNrd$zA-3l}yNdIe700cP>yk|uu#ZlH^9-D` zJy_Zm$vz#CC}}$BGP8c!acR7{@5TMX<50%@tL-{<13^mcS<0rcw!1&%^4NLV-pHu) zA&a$Owj?`ggPYuRM>u4Iu*13&07X3AO0Q6-*0ol*%r)4jh-o}ANm4?x5a4>ko`P%^ zlV_wH;?I(&8qgiv#{NmYlftIlz@OVmTKndBAmo0pz zMB5QJyC>`Ndvv*maviuM?)63;j(bDKo!UEZOZ3mrkH!1yA@kt_8BAvh(_aBIuM*Mu zrDOS<)wpVOO0DN6pxb1w0&ud+h{zl}C_y~6O?-8Wb$x>Aoa;&B6A#^9k}?~JLc2!t zFQIZ$BHOYFX%Fo@Q%iWCy(ulC2nXIK?ndl7-NdcyL~MjsD2E8^g8y#5y!^;HlY`{S zUtQvM8bY?i6p`w6^VnSUb;pHWew0+o3mPVhA|u%nRZKn6%6-ZHB;&QyG)lI&QMepN z4IE}831#n_^WYLO;$ACJNm8g1#^LE}*x`BBw}TQy7qKlb_d2eCuT|kRP^eiPJ@5VE zeysDBVIGDvap)HKnyQOB+)upha5&o8N&_;i|dcfde6 ze>eVpbfJMtyhQ-Tix>UlgYBiaBdAk?`DmvU-SeW3pXSjk;gS82M2cDR@cn&N&)&~| zxM6TrvM6o!r2PWts^Sk+OxLr~W&GtBd#$HJM}=5nUx*MoN>(^Bh!r*lzHBB;l5JVO zgK>x1bu@C+T%yhK`GC%7Bw?X=8z0($e>q}6yS_DF7C5p?;D-GXh%lK%>w3f1X-_W4 zsZAR};yCyvg-cB#8*fc&UortYQAEI0`^c*zDE){?p39N<>W&ClS0`mG!o@9tyV=id7~|GeWJYh=w@znZAjtWh;r zeb?t(kVtz=qixz+z$Iw|YqUMLO)m3=MTForAV1*C%s@{e#osN(CvhxVZ{cO2;}SNo zCIlP;?QC&*5K27|_yWaYHUmOHcZJtnr3$O1*C-RKMa8DN=&7MFT{8Xav$ipW*j12y zp%Uq3?I8HNR|D($1966`V8fC=dKT?M5bUxVdrOSRWvU?K!t$}YY2Y{pxkGdY1el(H z1C>z8ucCVaBfJ*LT+W$(KJGr%aPb!loBebztZ!sftv2@`9k;Jm%DqxH4;})F8w)F$ zA2cL2>6n@mCR2npX;`1K3uq}+X{Z>YDmCjDe&%kesji+LvS28{mY%EVu+VIQVuc>4 z?k)WE2o*#brD_;d7dL*1oPanBiOj%_P)d1U&_Kany;hCcka_o)y3VQXuwE-s$KJEe z&xupYj;|C{dB)A7x##qaj7-uKj*CW)+ug8lK0pV<-c#Cxo53oKUQuW}Puj6DcJJ{& z;yU9RcQ2yXT)enL)_^R>zMsqPD_V-%;a04@lsYHCEW^l-slze3KzZgtY+O z9Yaw-w#iu0P8DIyD|nU=H@C5v%0^7jcn!3KF{fX>|7>Y zv`#F5FXVC&ZWa*TMBlSubH1idBM8SJS{}f5)_eJPQaCTdW%|{!npN5Vjaqc$&+(oX=6u@T7Hj2He_x=yazZQ+0vaBQ4oxw8{8(YHr%r zrnr0^dGk=hBzfPWgAk!K<7%YjZW8lKy(N>`*?R)4c~ReV#Q#ShVZQrJG4s zhvtHCfA(iQ3$j!?R1Us#TjkG@XvgQ@&vaGIOVYL7zZ|&BT4Q%Y|3DM4Q;6djh6~DI zAzQ&6U9lNVv*PN@U^S`wzG+~9j&$J!fyt)k<~M6T9|()y6YmzJiP5m4ZRe%Ew42pf zCu(41h()N(_v7n28LQ`=JNRD{RXXmTr$;Ge5+cvp@4 zdL%W(ZPtK}b<`-XO4txfoUT9|uLbqeY!~Nf;HH&sE)$;jtdpB?9nO$g8_w0et95uYb%yn;%Lf@+a?CNrjc_40 zje1I-8kPHvF`2%~w|8$V2jVzIITv{A^w+OPWr5ON{jC1Fqy3U5t8*u<0DcA<9mo2(fyWbNT#c4Y>_Y zrGxtVusa7P;gY8g+y1RfSynj{ZoX=9oWZUP-GTZ7G-xK9=~#+0Q58HcgMwgB@Eqa3 z2}OkZz`ENd%1Sfu#i(avZ8^O40he=P4WiVfyTc7_w*``N-dKdQvqyGuve=u*=8 zY|GB4JjL~9Xcdg_b!_@uPwGq%qiB<3RmD}+Pi3(j(D{dR^ZqQ-1tv5pPO1l7q1d4Q z84$KgM<(`RC4=cpumT)ZjdBsZdQ1?(*o9J(6)Mu!aj$;9v-5!9ek_Sdxm7jivmIU9 zi&l(KC;}}@7E`~WOB2j(e_H{aZYtkvIM2{|W7UZ3v-72PFCNtxZ)k$aPaZd;mX%Wa zxSFlN5QcKz#qrGmH9HB;!w>!6P*TA{{E@#Jv>PP+bUlE3xY@Qgf6gdL9#V_VTOk@T zQd3alJ#L_awJ}yGj7}MDPL`515s7y&Dy`Qa_mNvNo*d~g@zXk`%=v`C_Y52EC94Hj zr$@+75asTMzjOKhmO?#rTO(A>wrp9;*IL$1r`rr!5tvu9cGxWJ ziYsm|59MjP9CNI$7{k$LiN;+@C5qHlQ893}B6UzI3}fFSpaVd2&E^n@to)AG+w@eZ zSmXcQ!iK0@HjhKoQ8+8TO3jW(dw!+{DT zoEc*{gx@IdnCQGDKRF+cKtCokbMb~WX%wvPzmuA?0O#CQL0rai=&ZSpsl(qki((W$0zpphiQJ%hX(Jlnu?Dt! z&&~vaG=m%IXfR&y=dR*heX=v}kETZ@m|YBguTx=jPMW{8M%2Ghs)7qqd+I*@C{?L+ z5xKuOX^oMYsc`SOE843FKY`79uT^QqSlzURzj69TaMzII`O%PD-xUdumF4CJbRwUWG!3qdt#4IHB52DF z|Etor-L{nGT;!QidP6mC~bZa;!S&!`T1OYPBV z@U7?hX{+IMcR$7iTpP|I9^dhBwe0K@N3m7tfPy@|v)hX{KB`Cy@_q8FURp814_i_A zfos+fEXOE~Ohf13RmVw9&i>tsEU9BJ!ixq-i;v#~p`AK0L6UK#hgL10`*;e2 zs+sXn+4Ue^%(p`(Ko7QkrpyCUk>r8Y?}j3g0VXLB$MJiD3NZup51;-_SXsm@4jf0) zj}K1wy$Gf?sOk_K9(o?I&Y!g%5VAx*d9+Cm23x_fHfd}ieUl>vcXa?xO&7hwpG~L%0ixO zAe{5v6~QZEA4%tXP6TODr7);hy0+fB=6 z#&=&%FSK01U>{V z)urd-imPq@Dzjb^q+fDMxAr-C5q*Z0t|MdudTG(*zDDq{m^~fCJzK3hWL3{)u5S3l z$|@=!q&M)*!dqHG+C^$)>2faKJg5J-Ked15N7qaIQ}VBU~iGW6~O*z_F!)UB<5FjD9 zgNZ~>U{AoZ?FxCgOr0yoAt;IUnc@$#g<3UZLCs}BAn)&t5@I26VEZwMjlM<4=$_mW zfwnD%5?JhsR`DGV+5pdSNVD?YlaM^e@Tr#~_Db-jDg}60 zhL(A2x_GvfuotaEhv9X1vXhr|%hRQ=>9pF=TO|Q#!O8wO+nu$&#Ys!?eJypn`dF!T zIwL{nZU~r+&nwgezQ*Y6EtS0KOE!*3d+x9j?m~}x*0ox6eY_fmPU##MzjiN{m%PzJ zY8*P9U@{fz5&?*N&7k3lp{$8K2@c)KO#kFa7WHu;0Gxp(Z%jmVL8c%q_G3~tfQmxt7@-|!d=o_`cTr= zX`9eX=oEPUYTaDO5uC5-p+=gx6ZD7vO-nJJo23cekr~9%eAesD(-@BK^g4``-aV0DqL9w^6=iQ_ z+mEK@ONT|_IQaAdA(JXn*X9Z*6W`DD7%Ay8{nR6 zrRPR&9v@*&1WsQ+mUz}}-G<{QgJAT$6XvpsfaSOF&$1nBtV)e1+C`nY_(vkGGP-J~<31mto#XT_9@*LXp z)YPbH31P~kfhq~HI+7O2fo6z<;Sp+tTd=hSP4S)pd|lU2s7#bgQG4@tHfl;G=mN!{ zxZM?Zs4@Cc3P(VX8KQjk21E(eV(H1HPl|N2daBKwN@V2$ZXQq55im z3En%OXqP^p$}I1O+1jvxVlJ8S%#)4Ud8U_pFE& zSJC|*iQq5y3v!CK=WES+XB^_0an}l8ZVufgc+MB%*Qg;7rNE)+ud&pbA0`;r=&T${8r<|{a0FNCS?zG(|lQ{=eE$XJ1D z#-_$)@_9bLlFf*;J+|GJ6KC{?EmND5=#2H#4f1uvOE$*gXQY{!N1tBEkM1m1uWF8$ zTCG~c)PLwE_1`#qne_>3<N-_;H~#8IN^xlu#iY81XUt8?1;sHDELS~XRv&*lf^ceYVWpKbMUIv{Dgp4&;^TtZDB%RQ##k`9>K629v_9=^~C%Xo!Ta>8w=X5(R@pukU)~6b`Kt=PoUJtXz1q5F4`I=xGXkC9Opb4scV=^owOx7+p1$OCJRfKX0f(&^n6p zr5o??_E&^K6u<^$@9@XcNbqQ{1bh57_%}+KKHkzw;uGa1uX!;v2&K+IB0HgJc zhAJE&Qo%EE$OA|};!2t}fBnW!={9ug9f>0WFGn^3&Ntqtvv`yk(Wq`SB#>`-FzMUh zUKM_l0!P%gL}L-!du~c+bJ^dP2lR14k{~=(3Mqfrc;f@~-|{w`dIzJ2*~ssde~SXm z+v-GE3m=dZ5V|wNZd>+-#y;Sp?ktvJVM-j{*>I1>gz)!=E!;jQO z=%Wmf2H*tX2!sv(2(bm!<=jP%*ae0FNDlTL^gFN_kQzu8lpazSoR5spE&v{&7QiFi zi^JdVF>#&=f97F}L8Obvy#^k2+nwimdCSWl0i^BzKE~_fgqPkCXMc9r?0MP7>mrYb z{nK;(Vp}@@O zx>3xNpf{~Vmdkh0f*>H&fA=o>javCdvM?}m{9!uw?;zv<0zQ*c5*CqD{R2LeG5%^y z^V!zQ=y#On4=f0Z{U2=SAI#bR1%>v(r2R(L`v8G{ z{eAFT9KVxnf8G0^92x6xh6sPn`{NS}8}mO%t&h}il-MtN?VtIKA2tyj^dGd?#}$6X z|Cz_~8%gMO%Vl#TE)obZBywlCmnI*g`) z;&X<(xUhXX;nMXYxd(TY`@~bng>%PZ0j=ltHDUd%@~2|AP)q%fm;CUCm7N?2CCdMUWd1tqWbD> zH8;-0zCBaGBuS$GLato>EmC-WQo|y*-}$ zz1{S_gWonfCRI1oe*wxS`1J0?Bf|U2!~Gurmgx3rbLOQzxLaZHW8dLLhdhzosmSos)YA(QI_ zSC_=_Q!NZsokOuu`3)uK7wmmsYU{Za50uuHVQ>=_Mgyrz14+Q5AbMj3Gzj!SaZ;?k zZae&?8MJiC|hKtygCdd9;ht;JWw4RcE%bC8;mSe4Jq*JURl0Eal!^B2n-Q|;9)>X zS3}H0dNv_)=q}iri3}{*61p4<2}IFWvcA&$Vor;tmGfF_dgZCdTBnc^$JP?&;DC~z zw|k%mok%yn9Z-ZxmaSY=179tcwSuCBn2@sEUQ4AqxSw$Rn2rqvYDrZ&K337jN*na! zN7YCb0R{07)$Czv0TV%>NzFRN`&}4phQ_+S$v!c;&s3E(G{cfpLKz_eDi|!J*%Z1& zB9^6PoJ^y5%0vVyvO=F@5cZO>2A9i=wVzEbqlx=S7&_n;FgHQWSHESet-@WcD2Z|L z5{gOVfhpVs`OVc39HEMpM=$pfoOy8P$LIndg7wS~;(I6yJ{}t)G*}BPy`3OBSq;Gw zWJE9I5T5=FMsV^*58%Or+OB{VZ^Wk-{6QO8Q@8o8v3aHd?-qMUT!@k^tKfNw^%so?&bC0WyVDc4(6jN*Tos z!*^GhVNt3%dY(D+)O)lCa{760vae20dk@S{!} z_OTvbrD{`8xLukbbhCWUrfvayg9FjrL?8Oh^EtuR=4xRPTi2;7zP?vlcp-{*xgnn}GGK-IMM&9hK zPV8)MxtK=YKX8VAfR|hm}c!dj9!3ye?~juC{mqt=*N~H)dI~ z?I16O&_9dNZzzAq#_uht=v7Y?1K;6#Iqv@}y_E*O$^H1Fo9(wyX_LF1#wHK`IuzZHMe$euRP9+^`QUx<9@yEdm!(6F5ZiPI4c5dHC-83^M;CCZq4QWx=2;_Y`sy*5~) zp~Lrq%sT_MO{cGr`~&k8hpQ!cvzEN@cmfH@5Ru zd)`KJI&uVY{Ek9CJC!UV6;C9L#x>VIKFT>ml5*ZHM-D5c+8q`MFpwRE=n?`=@Frkxo|DyVYxj zUYmEP*UgD;=a^1e){}LWt~aR z%98gav}lL&dlcWXsr5czMd$eGn4EV|`*qdJeC3W*WM|ErZ4b^i8TsC1WelFwqpuhA z+PF^+QohefHsln+l=&1>_9kFEfE(m3)FF6wq$~Pa)YKEGSn}YyyAL7Xj6WBvoX{E8 zmT_4$VzVH}=?dtBuvGY_i}+XGPW%8HiTzho@CVOxFK0u)5x$+N%V^4yDAU zs98bSyO5186?Be)E!5ZjdT;-Nz(q?I5K|dcQJ%4;~8|`f0M* z_5(vQpE*47S_-gO#Ubb~L|e=TQ`qX1oep!nw9nhU;;DVvtaaag>dh+AHr#}dz@~*i zr(zEb3EJ=2UCc~wWlihx{Nf3gBTFTKRIDHKsSe@1htq|6!wMzX0xNj643x?M%r0m8|WpYRo`Yr){C@h>22$ah-q61B47$FawW3CY33$W5Lo{b|{$Y!CVoe zbk{Kbo+kUGCp<3tc7uW168Z)+9iH9`2Jqypx;kGV*=m_&?++qnP4fh2ua<7*=F)~Z zfD()rJ0o%Mt^$ML_kx^Eu{ld;m-p?S&S*rO3EAzNHfNPLR+B1?Jq8+NoEv2$mq;A4 z(VRXBZP-JzwjroO?ySi3S|MHWZhp4H&ERcv$x{>#qE{{yxV0k6bxTlbokbb0TKd+A zWl*93Hf;RKwZ*#6UqMDH0#9ndWZ)Cc0NwE|^$S+B84DTsb>4hTdQu7xfhJ**rey-K zeksWoW6GLe3>FaP=W>EAH zqyqGi*L6;^Y`C)zhXouhcRD2!aq%ZOK(?QGN)S% z6t{h4)nL@g4>#Qv2S)Gg7ai`*5ZtWfqh4#qg4&r z0@DT?N^p_LZiANfw^d=#>vxvZ&%~~!+J~-zJpe!~=XEt>4giTedFM!`cB4l72+6uejaIYi zF72X#y!JY)OANbdXiKapzl)+enEv(GBOLvqb zaL#DdyJ-)BHVGc#x}7091TS&?E)YDx`M~|X04$WOKsEuX9%bNV?uaBY>O+Tusm-iR z1c>~y5N-5?ju{D|F=Id@hWtc~1c+*~5!I(Y@`X^DKf)jTt_7!X%ZO~{Vvj&fo{y(l z?^UZY-LuWCjh=OAIXiDGZk&%gJ0EzQjgIsUZ*MCig))AzaCzJV@w$b8G4|*flf$P^ zzH_Vf_l@~jizP)19YZ}myq^@USwTr>lO(+Kk>qfKvcOKBRtp}j7u4EyYnS9J)GIzS zn`LTRPIGfHhiW&>`1sV?e|WQaJ1DZO{ss;Y-mHYTT?=_BWwadu2Z01Cr-go&A*S-I zu@^J%(#wWLQL-|VhZsaai}qrb$LQxQBwbZ&C{*L~m@L2R+USE$PT!2=^&CQ;%x$wdDOa8GQS`&1bw87o3-V| z;H0*-RdV0-3TazdT0Toues6niF&n;k8651BD-$r_H;EO?FRrTzURNy@rbe=a42ls~ z;IrUd`_apYH>(};Wh9*CAsRVccr}s=t;^Q6OX0bdWNamBW78J4>{wOeiF#8%7$ikR z#gyA&_~NfxJ23IWBb-TykhkCcw_GmSpaA=T(}6W^t`Z-8g0pFvzh41Pe{NTNF)-M_ zXq5Yl*BQy(cfy5LPrxm|d*(%$ZfB0M(f6TW19{H2C)rSwz zzpSl3e4YN7Q~dDY`jbi6Z%6)@d6^HZtKZK5KRM=WN4MMb;UfgNXBv!3e(7;SgCr0} z%h)623ylNss%nUyfY#XUvBkrm5SFM0WxDCEx}M>C*h3m}hI8ryi6a|q`W8Xc^YPpM zX{W6=>A_+PCkFW;82IN@yCZX`?0A4^JPdWD3p+P^{l1{w_G-a<=H8*=G(#?~q9z!ab1`GjGb}ExU*TLabEz?}gLHZu3CH zueES`>5)c@7Rp6OxR!5=6lPC5?Xm004rwhx8gA&Hk-k ze$^Etbm{l{eR?~}v7dqn+rxA?c2zF&Sy|L}_X|H(uj$M0rze@yiK7Wl6X z>eo#3U#z44Hxqs3%F?!2%E&yn1DPJHxMbFGQi7&-Ga?wOa5

1p2->@_y0X+Ac~V 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 LF=KrFr&;|4#k|z* diff --git a/test/preview/test_files/pdf/sample_pdf_2.pdf b/test/preview/test_files/pdf/sample_pdf_2.pdf deleted file mode 100644 index 6384246e891c59abf174e7225ed7f793e814ed69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26093 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 S4>n+N3&DvwIj^b75&sVaB5i5_ diff --git a/test/preview/test_files/txt/doc_1.txt b/test/preview/test_files/txt/doc_1.txt deleted file mode 100644 index 4121890801..0000000000 --- a/test/preview/test_files/txt/doc_1.txt +++ /dev/null @@ -1,2 +0,0 @@ -Some text for testing. -Two lines in here. diff --git a/test/preview/test_files/txt/doc_2.txt b/test/preview/test_files/txt/doc_2.txt deleted file mode 100644 index 6f950eedcf..0000000000 --- a/test/preview/test_files/txt/doc_2.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a test line. -123 456 789 -987 654 321. diff --git a/test/preview/test_files/txt/doc_3.txt b/test/preview/test_files/txt/doc_3.txt deleted file mode 100644 index a7c4fd5e55..0000000000 --- a/test/preview/test_files/txt/doc_3.txt +++ /dev/null @@ -1,11 +0,0 @@ -That's yet another file! - -it contains - - - - -many - - -empty lines. diff --git a/test/preview/test_files/yaml/test_pipeline.yaml b/test/preview/test_files/yaml/test_pipeline.yaml deleted file mode 100644 index 0690b86f1a..0000000000 --- a/test/preview/test_files/yaml/test_pipeline.yaml +++ /dev/null @@ -1,14 +0,0 @@ -components: - Comp1: - init_parameters: - an_init_param: null - type: test_pipeline.TestComponent - Comp2: - init_parameters: - an_init_param: null - type: test_pipeline.TestComponent -connections: -- receiver: Comp2.input_ - sender: Comp1.value -max_loops_allowed: 99 -metadata: {} diff --git a/test/preview/test_pipeline.py b/test/preview/test_pipeline.py deleted file mode 100644 index be60e4be89..0000000000 --- a/test/preview/test_pipeline.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Optional - -import pytest - -from haystack.preview import Pipeline, component - - -@component -class TestComponent: - def __init__(self, an_init_param: Optional[str] = None): - pass - - @component.output_types(value=str) - def run(self, input_: str): - return {"value": input_} - - -@pytest.fixture -def pipeline(): - return Pipeline() - - -@pytest.mark.unit -def test_pipeline_dumps(pipeline, test_files_path): - pipeline.add_component("Comp1", TestComponent("Foo")) - pipeline.add_component("Comp2", TestComponent()) - pipeline.connect("Comp1.value", "Comp2.input_") - pipeline.max_loops_allowed = 99 - result = pipeline.dumps() - with open(f"{test_files_path}/yaml/test_pipeline.yaml", "r") as f: - assert f.read() == result - - -@pytest.mark.unit -def test_pipeline_loads(test_files_path): - with open(f"{test_files_path}/yaml/test_pipeline.yaml", "r") as f: - pipeline = Pipeline.loads(f.read()) - assert pipeline.max_loops_allowed == 99 - assert isinstance(pipeline.get_component("Comp1"), TestComponent) - assert isinstance(pipeline.get_component("Comp2"), TestComponent) - - -@pytest.mark.unit -def test_pipeline_dump(pipeline, test_files_path, tmp_path): - pipeline.add_component("Comp1", TestComponent("Foo")) - pipeline.add_component("Comp2", TestComponent()) - pipeline.connect("Comp1.value", "Comp2.input_") - pipeline.max_loops_allowed = 99 - with open(tmp_path / "out.yaml", "w") as f: - pipeline.dump(f) - # re-open and ensure it's the same data as the test file - with open(f"{test_files_path}/yaml/test_pipeline.yaml", "r") as test_f, open(tmp_path / "out.yaml", "r") as f: - assert f.read() == test_f.read() - - -@pytest.mark.unit -def test_pipeline_load(test_files_path): - with open(f"{test_files_path}/yaml/test_pipeline.yaml", "r") as f: - pipeline = Pipeline.load(f) - assert pipeline.max_loops_allowed == 99 - assert isinstance(pipeline.get_component("Comp1"), TestComponent) - assert isinstance(pipeline.get_component("Comp2"), TestComponent) diff --git a/test/preview/test_telemetry.py b/test/preview/test_telemetry.py deleted file mode 100644 index ba9105eee7..0000000000 --- a/test/preview/test_telemetry.py +++ /dev/null @@ -1,54 +0,0 @@ -import datetime -from unittest.mock import Mock, patch -import pytest - -from haystack.preview import Pipeline, component -from haystack.preview.telemetry._telemetry import pipeline_running - - -@pytest.mark.unit -@patch("haystack.preview.telemetry._telemetry.telemetry") -def test_pipeline_running(telemetry): - telemetry.send_event = Mock() - - @component - class Component: - def _get_telemetry_data(self): - return {"key": "values"} - - @component.output_types(value=int) - def run(self): - pass - - pipe = Pipeline() - pipe.add_component("component", Component()) - pipeline_running(pipe) - - # First run is always sent - telemetry.send_event.assert_called_once_with( - "Pipeline run (2.x)", - { - "pipeline_id": str(id(pipe)), - "runs": 1, - "components": {"test_telemetry.Component": [{"name": "component", "key": "values"}]}, - }, - ) - - # Running again before one minute has passed should not send another event - telemetry.send_event.reset_mock() - pipeline_running(pipe) - telemetry.send_event.assert_not_called() - - # Set the last telemetry sent time to pretend one minute has passed - pipe._last_telemetry_sent = pipe._last_telemetry_sent - datetime.timedelta(minutes=1) - - telemetry.send_event.reset_mock() - pipeline_running(pipe) - telemetry.send_event.assert_called_once_with( - "Pipeline run (2.x)", - { - "pipeline_id": str(id(pipe)), - "runs": 3, - "components": {"test_telemetry.Component": [{"name": "component", "key": "values"}]}, - }, - ) diff --git a/test/preview/testing/test_factory.py b/test/preview/testing/test_factory.py deleted file mode 100644 index 5a7fdc78a8..0000000000 --- a/test/preview/testing/test_factory.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest - -from haystack.preview.dataclasses import Document -from haystack.preview.testing.factory import document_store_class -from haystack.preview.document_stores.decorator import document_store - - -@pytest.mark.unit -def test_document_store_class_default(): - MyStore = document_store_class("MyStore") - store = MyStore() - assert store.count_documents() == 0 - assert store.filter_documents() == [] - assert store.write_documents([]) is None - assert store.delete_documents([]) is None - assert store.to_dict() == {"type": "haystack.preview.testing.factory.MyStore", "init_parameters": {}} - - -@pytest.mark.unit -def test_document_store_from_dict(): - MyStore = document_store_class("MyStore") - - store = MyStore.from_dict({"type": "haystack.preview.testing.factory.MyStore", "init_parameters": {}}) - assert isinstance(store, MyStore) - - -@pytest.mark.unit -def test_document_store_class_is_registered(): - MyStore = document_store_class("MyStore") - assert document_store.registry["haystack.preview.testing.factory.MyStore"] == MyStore - - -@pytest.mark.unit -def test_document_store_class_with_documents(): - doc = Document(id="fake_id", content="This is a document") - MyStore = document_store_class("MyStore", documents=[doc]) - store = MyStore() - assert store.count_documents() == 1 - assert store.filter_documents() == [doc] - - -@pytest.mark.unit -def test_document_store_class_with_documents_count(): - MyStore = document_store_class("MyStore", documents_count=100) - store = MyStore() - assert store.count_documents() == 100 - assert store.filter_documents() == [] - - -@pytest.mark.unit -def test_document_store_class_with_documents_and_documents_count(): - doc = Document(id="fake_id", content="This is a document") - MyStore = document_store_class("MyStore", documents=[doc], documents_count=100) - store = MyStore() - assert store.count_documents() == 100 - assert store.filter_documents() == [doc] - - -@pytest.mark.unit -def test_document_store_class_with_bases(): - MyStore = document_store_class("MyStore", bases=(Exception,)) - store = MyStore() - assert isinstance(store, Exception) - - -@pytest.mark.unit -def test_document_store_class_with_extra_fields(): - MyStore = document_store_class("MyStore", extra_fields={"my_field": 10}) - store = MyStore() - assert store.my_field == 10 diff --git a/test/preview/utils/test_filters.py b/test/preview/utils/test_filters.py deleted file mode 100644 index 1b3baaf771..0000000000 --- a/test/preview/utils/test_filters.py +++ /dev/null @@ -1,725 +0,0 @@ -import pytest -import pandas as pd - -from haystack.preview import Document -from haystack.preview.errors import FilterError -from haystack.preview.utils.filters import convert, document_matches_filter - -document_matches_filter_data = [ - # == operator params - pytest.param( - {"field": "meta.name", "operator": "==", "value": "test"}, - Document(meta={"name": "test"}), - True, - id="== operator with equal values", - ), - pytest.param( - {"field": "meta.name", "operator": "==", "value": "test"}, - Document(meta={"name": "different value"}), - False, - id="== operator with different values", - ), - pytest.param( - {"field": "meta.name", "operator": "==", "value": "test"}, - Document(meta={"name": ["test"]}), - False, - id="== operator with different types values", - ), - pytest.param( - {"field": "dataframe", "operator": "==", "value": pd.DataFrame([1])}, - Document(dataframe=pd.DataFrame([1])), - True, - id="== operator with equal pandas.DataFrame values", - ), - pytest.param( - {"field": "dataframe", "operator": "==", "value": pd.DataFrame([1])}, - Document(dataframe=pd.DataFrame([10])), - False, - id="== operator with different pandas.DataFrame values", - ), - pytest.param( - {"field": "meta.name", "operator": "==", "value": "test"}, - Document(), - False, - id="== operator with missing Document value", - ), - pytest.param( - {"field": "meta.name", "operator": "==", "value": "test"}, - Document(meta={"name": None}), - False, - id="== operator with None Document value", - ), - pytest.param( - {"field": "meta.name", "operator": "==", "value": None}, - Document(meta={"name": "test"}), - False, - id="== operator with None filter value", - ), - # != operator params - pytest.param( - {"field": "meta.name", "operator": "!=", "value": "test"}, - Document(meta={"name": "test"}), - False, - id="!= operator with equal values", - ), - pytest.param( - {"field": "meta.name", "operator": "!=", "value": "test"}, - Document(meta={"name": "different value"}), - True, - id="!= operator with different values", - ), - pytest.param( - {"field": "meta.name", "operator": "!=", "value": "test"}, - Document(meta={"name": ["test"]}), - True, - id="!= operator with different types values", - ), - pytest.param( - {"field": "dataframe", "operator": "!=", "value": pd.DataFrame([1])}, - Document(dataframe=pd.DataFrame([1])), - False, - id="!= operator with equal pandas.DataFrame values", - ), - pytest.param( - {"field": "dataframe", "operator": "!=", "value": pd.DataFrame([1])}, - Document(dataframe=pd.DataFrame([10])), - True, - id="!= operator with different pandas.DataFrame values", - ), - pytest.param( - {"field": "meta.name", "operator": "!=", "value": "test"}, Document(), True, id="!= operator with missing value" - ), - pytest.param( - {"field": "meta.name", "operator": "!=", "value": "test"}, - Document(meta={"name": None}), - True, - id="!= operator with None Document value", - ), - pytest.param( - {"field": "meta.name", "operator": "!=", "value": None}, - Document(meta={"name": "test"}), - True, - id="!= operator with None filter value", - ), - # > operator params - pytest.param( - {"field": "meta.page", "operator": ">", "value": 10}, - Document(meta={"page": 10}), - False, - id="> operator with equal Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": 10}, - Document(meta={"page": 11}), - True, - id="> operator with greater Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": 10}, - Document(meta={"page": 9}), - False, - id="> operator with smaller Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - False, - id="> operator with equal ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1972-12-11T19:54:58"}), - True, - id="> operator with greater ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">", "value": "1972-12-11T19:54:58"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - False, - id="> operator with smaller ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": 10}, - Document(), - False, - id="> operator with missing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": 10}, - Document(meta={"page": None}), - False, - id="> operator with None Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": None}, - Document(meta={"page": 10}), - False, - id="> operator with None filter value", - ), - pytest.param( - {"field": "meta.page", "operator": ">", "value": None}, - Document(meta={"page": None}), - False, - id="> operator with None Document and filter value", - ), - # >= operator params - pytest.param( - {"field": "meta.page", "operator": ">=", "value": 10}, - Document(meta={"page": 10}), - True, - id=">= operator with equal Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": 10}, - Document(meta={"page": 11}), - True, - id=">= operator with greater Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": 10}, - Document(meta={"page": 9}), - False, - id=">= operator with smaller Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">=", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - True, - id=">= operator with equal ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">=", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1972-12-11T19:54:58"}), - True, - id=">= operator with greater ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": ">=", "value": "1972-12-11T19:54:58"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - False, - id=">= operator with smaller ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": 10}, - Document(), - False, - id=">= operator with missing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": 10}, - Document(meta={"page": None}), - False, - id=">= operator with None Document value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": None}, - Document(meta={"page": 10}), - False, - id=">= operator with None filter value", - ), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": None}, - Document(meta={"page": None}), - False, - id=">= operator with None Document and filter value", - ), - # < operator params - pytest.param( - {"field": "meta.page", "operator": "<", "value": 10}, - Document(meta={"page": 10}), - False, - id="< operator with equal Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": 10}, - Document(meta={"page": 11}), - False, - id="< operator with greater Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": 10}, - Document(meta={"page": 9}), - True, - id="< operator with smaller Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - False, - id="< operator with equal ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1972-12-11T19:54:58"}), - False, - id="< operator with greater ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<", "value": "1972-12-11T19:54:58"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - True, - id="< operator with smaller ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": 10}, - Document(), - False, - id="< operator with missing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": 10}, - Document(meta={"page": None}), - False, - id="< operator with None Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": None}, - Document(meta={"page": 10}), - False, - id="< operator with None filter value", - ), - pytest.param( - {"field": "meta.page", "operator": "<", "value": None}, - Document(meta={"page": None}), - False, - id="< operator with None Document and filter value", - ), - # <= operator params - pytest.param( - {"field": "meta.page", "operator": "<=", "value": 10}, - Document(meta={"page": 10}), - True, - id="<= operator with equal Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": 10}, - Document(meta={"page": 11}), - False, - id="<= operator with greater Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": 10}, - Document(meta={"page": 9}), - True, - id="<= operator with smaller Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<=", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - True, - id="<= operator with equal ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<=", "value": "1969-07-21T20:17:40"}, - Document(meta={"date": "1972-12-11T19:54:58"}), - False, - id="<= operator with greater ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.date", "operator": "<=", "value": "1972-12-11T19:54:58"}, - Document(meta={"date": "1969-07-21T20:17:40"}), - True, - id="<= operator with smaller ISO 8601 datetime Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": 10}, - Document(), - False, - id="<= operator with missing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": 10}, - Document(meta={"page": None}), - False, - id="<= operator with None Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": None}, - Document(meta={"page": 10}), - False, - id="<= operator with None filter value", - ), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": None}, - Document(meta={"page": None}), - False, - id="<= operator with None Document and filter value", - ), - # in operator params - pytest.param( - {"field": "meta.page", "operator": "in", "value": [9, 10]}, - Document(meta={"page": 1}), - False, - id="in operator with filter value not containing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "in", "value": [9, 10]}, - Document(meta={"page": 10}), - True, - id="in operator with filter value containing Document value", - ), - # not in operator params - pytest.param( - {"field": "meta.page", "operator": "not in", "value": [9, 10]}, - Document(meta={"page": 1}), - True, - id="not in operator with filter value not containing Document value", - ), - pytest.param( - {"field": "meta.page", "operator": "not in", "value": [9, 10]}, - Document(meta={"page": 10}), - False, - id="not in operator with filter value containing Document value", - ), - # AND operator params - pytest.param( - { - "operator": "AND", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 10, "type": "article"}), - True, - id="AND operator with Document matching all conditions", - ), - pytest.param( - { - "operator": "AND", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 20, "type": "article"}), - False, - id="AND operator with Document matching a single condition", - ), - pytest.param( - { - "operator": "AND", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 11, "value": "blog post"}), - False, - id="AND operator with Document matching no condition", - ), - # OR operator params - pytest.param( - { - "operator": "OR", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 10, "type": "article"}), - True, - id="OR operator with Document matching all conditions", - ), - pytest.param( - { - "operator": "OR", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 20, "type": "article"}), - True, - id="OR operator with Document matching a single condition", - ), - pytest.param( - { - "operator": "OR", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 11, "value": "blog post"}), - False, - id="OR operator with Document matching no condition", - ), - # NOT operator params - pytest.param( - { - "operator": "NOT", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 10, "type": "article"}), - False, - id="NOT operator with Document matching all conditions", - ), - pytest.param( - { - "operator": "NOT", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 20, "type": "article"}), - True, - id="NOT operator with Document matching a single condition", - ), - pytest.param( - { - "operator": "NOT", - "conditions": [ - {"field": "meta.page", "operator": "==", "value": 10}, - {"field": "meta.type", "operator": "==", "value": "article"}, - ], - }, - Document(meta={"page": 11, "value": "blog post"}), - True, - id="NOT operator with Document matching no condition", - ), -] - - -@pytest.mark.parametrize("filter, document, expected_result", document_matches_filter_data) -def test_document_matches_filter(filter, document, expected_result): - assert document_matches_filter(filter, document) == expected_result - - -document_matches_filter_raises_error_data = [ - # > operator params - pytest.param({"field": "meta.page", "operator": ">", "value": "10"}, id="> operator with string filter value"), - pytest.param({"field": "meta.page", "operator": ">", "value": [10]}, id="> operator with list filter value"), - pytest.param( - {"field": "meta.page", "operator": ">", "value": pd.DataFrame([10])}, - id="> operator with pandas.DataFrame filter value", - ), - # >= operator params - pytest.param({"field": "meta.page", "operator": ">=", "value": "10"}, id=">= operator with string filter value"), - pytest.param({"field": "meta.page", "operator": ">=", "value": [10]}, id=">= operator with list filter value"), - pytest.param( - {"field": "meta.page", "operator": ">=", "value": pd.DataFrame([10])}, - id=">= operator with pandas.DataFrame filter value", - ), - # < operator params - pytest.param({"field": "meta.page", "operator": "<", "value": "10"}, id="< operator with string filter value"), - pytest.param({"field": "meta.page", "operator": "<", "value": [10]}, id="< operator with list filter value"), - pytest.param( - {"field": "meta.page", "operator": "<", "value": pd.DataFrame([10])}, - id="< operator with pandas.DataFrame filter value", - ), - # <= operator params - pytest.param({"field": "meta.page", "operator": "<=", "value": "10"}, id="<= operator with string filter value"), - pytest.param({"field": "meta.page", "operator": "<=", "value": [10]}, id="<= operator with list filter value"), - pytest.param( - {"field": "meta.page", "operator": "<=", "value": pd.DataFrame([10])}, - id="<= operator with pandas.DataFrame filter value", - ), - # in operator params - pytest.param({"field": "meta.page", "operator": "in", "value": 1}, id="in operator with non list filter value"), - # at some point we might want to support any iterable and this test should fail - pytest.param( - {"field": "meta.page", "operator": "in", "value": (10, 11)}, id="in operator with non list filter value" - ), - # not in operator params - pytest.param( - {"field": "meta.page", "operator": "not in", "value": 1}, id="not in operator with non list filter value" - ), - # at some point we might want to support any iterable and this test should fail - pytest.param( - {"field": "meta.page", "operator": "not in", "value": (10, 11)}, id="not in operator with non list filter value" - ), - # Malformed filters - pytest.param( - {"conditions": [{"field": "meta.name", "operator": "==", "value": "test"}]}, id="Missing root operator key" - ), - pytest.param({"operator": "AND"}, id="Missing root conditions key"), - pytest.param({"operator": "==", "value": "test"}, id="Missing condition field key"), - pytest.param({"field": "meta.name", "value": "test"}, id="Missing condition operator key"), - pytest.param({"field": "meta.name", "operator": "=="}, id="Missing condition value key"), -] - - -@pytest.mark.parametrize("filter", document_matches_filter_raises_error_data) -def test_document_matches_filter_raises_error(filter): - with pytest.raises(FilterError): - document = Document(meta={"page": 10}) - document_matches_filter(filter, document) - - -filters_data = [ - pytest.param( - { - "$and": { - "type": {"$eq": "article"}, - "date": {"$gte": "2015-01-01", "$lt": "2021-01-01"}, - "rating": {"$gte": 3}, - "$or": {"genre": {"$in": ["economy", "politics"]}, "publisher": {"$eq": "nytimes"}}, - } - }, - { - "operator": "AND", - "conditions": [ - {"field": "type", "operator": "==", "value": "article"}, - {"field": "date", "operator": ">=", "value": "2015-01-01"}, - {"field": "date", "operator": "<", "value": "2021-01-01"}, - {"field": "rating", "operator": ">=", "value": 3}, - { - "operator": "OR", - "conditions": [ - {"field": "genre", "operator": "in", "value": ["economy", "politics"]}, - {"field": "publisher", "operator": "==", "value": "nytimes"}, - ], - }, - ], - }, - id="All operators explicit", - ), - pytest.param( - { - "type": "article", - "date": {"$gte": "2015-01-01", "$lt": "2021-01-01"}, - "rating": {"$gte": 3}, - "$or": {"genre": ["economy", "politics"], "publisher": "nytimes"}, - }, - { - "operator": "AND", - "conditions": [ - {"field": "type", "operator": "==", "value": "article"}, - {"field": "date", "operator": ">=", "value": "2015-01-01"}, - {"field": "date", "operator": "<", "value": "2021-01-01"}, - {"field": "rating", "operator": ">=", "value": 3}, - { - "operator": "OR", - "conditions": [ - {"field": "genre", "operator": "in", "value": ["economy", "politics"]}, - {"field": "publisher", "operator": "==", "value": "nytimes"}, - ], - }, - ], - }, - id="Root $and implicit", - ), - pytest.param( - { - "$or": [ - {"Type": "News Paper", "Date": {"$lt": "2019-01-01"}}, - {"Type": "Blog Post", "Date": {"$gte": "2019-01-01"}}, - ] - }, - { - "operator": "OR", - "conditions": [ - { - "operator": "AND", - "conditions": [ - {"field": "Type", "operator": "==", "value": "News Paper"}, - {"field": "Date", "operator": "<", "value": "2019-01-01"}, - ], - }, - { - "operator": "AND", - "conditions": [ - {"field": "Type", "operator": "==", "value": "Blog Post"}, - {"field": "Date", "operator": ">=", "value": "2019-01-01"}, - ], - }, - ], - }, - id="Root $or with list and multiple comparisons", - ), - pytest.param( - {"text": "A Foo Document 1"}, - {"operator": "AND", "conditions": [{"field": "text", "operator": "==", "value": "A Foo Document 1"}]}, - id="Implicit root $and and field $eq", - ), - pytest.param( - {"$or": {"name": {"$or": [{"$eq": "name_0"}, {"$eq": "name_1"}]}, "number": {"$lt": 1.0}}}, - { - "operator": "OR", - "conditions": [ - { - "operator": "OR", - "conditions": [ - {"field": "name", "operator": "==", "value": "name_0"}, - {"field": "name", "operator": "==", "value": "name_1"}, - ], - }, - {"field": "number", "operator": "<", "value": 1.0}, - ], - }, - id="Root $or with dict and field $or with list", - ), - pytest.param( - {"number": {"$lte": 2, "$gte": 0}, "name": ["name_0", "name_1"]}, - { - "operator": "AND", - "conditions": [ - {"field": "number", "operator": "<=", "value": 2}, - {"field": "number", "operator": ">=", "value": 0}, - {"field": "name", "operator": "in", "value": ["name_0", "name_1"]}, - ], - }, - id="Implicit $and and field $in", - ), - pytest.param( - {"number": {"$and": [{"$lte": 2}, {"$gte": 0}]}}, - { - "operator": "AND", - "conditions": [ - {"field": "number", "operator": "<=", "value": 2}, - {"field": "number", "operator": ">=", "value": 0}, - ], - }, - id="Implicit root $and and field $and with list", - ), - pytest.param( - { - "$not": { - "number": {"$lt": 1.0}, - "$and": {"name": {"$in": ["name_0", "name_1"]}, "$not": {"chapter": {"$eq": "intro"}}}, - } - }, - { - "operator": "NOT", - "conditions": [ - {"field": "number", "operator": "<", "value": 1.0}, - { - "operator": "AND", - "conditions": [ - {"field": "name", "operator": "in", "value": ["name_0", "name_1"]}, - {"operator": "NOT", "conditions": [{"field": "chapter", "operator": "==", "value": "intro"}]}, - ], - }, - ], - }, - id="Root explicit $not", - ), - pytest.param( - {"page": {"$not": 102}}, - {"operator": "NOT", "conditions": [{"field": "page", "operator": "==", "value": 102}]}, - id="Explicit $not with implicit $eq", - ), -] - - -@pytest.mark.parametrize("old_style, new_style", filters_data) -def test_convert(old_style, new_style): - assert convert(old_style) == new_style - - -def test_convert_with_incorrect_input_type(): - with pytest.raises(ValueError): - convert("some string") - - -def test_convert_with_incorrect_filter_nesting(): - with pytest.raises(FilterError): - convert({"number": {"page": "100"}}) - - with pytest.raises(FilterError): - convert({"number": {"page": {"chapter": "intro"}}}) From c31968ea4ba964b3b6fd75f9b4843a06165793fc Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Wed, 29 Nov 2023 15:54:51 +0100 Subject: [PATCH 3/3] fix linter --- .github/workflows/linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index c4a4f850e0..fdd385a23c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -71,7 +71,7 @@ jobs: - name: Install Haystack run: | - pip install ".[all,dev]" + pip install ".[all,dev]" pydoc-markdown pip install ./haystack-linter - name: Pylint