Repository: deepset-ai/haystack

Issue:
Normalise the use of `importlib`
Recently we introduced a generalised approach to load a class from it's qualified named string:

- https://github.com/deepset-ai/haystack/blob/main/haystack/core/serialization.py#L195

There are a few places in the code where this approach should be used:

- [x] document_writer.py:82
- [x] cache_checker.py:81
- [x] file_retriever.py:85

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 9888179574](https://coveralls.io/builds/68586178)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **5** unchanged lines in **2** files lost coverage.
* Overall coverage decreased (**-0.007%**) to **90.001%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/writers/document_writer.py](https://coveralls.io/builds/68586178/source?filename=components%2Fwriters%2Fdocument_writer.py#L53) | 1 | 97.22% |
| [components/retrievers/filter_retriever.py](https://coveralls.io/builds/68586178/source?filename=components%2Fretrievers%2Ffilter_retriever.py#L58) | 4 | 87.88% |
<!-- | **Total:** | **5** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/68586178/badge)](https://coveralls.io/builds/68586178) |
| :-- | --: |
| Change from base [Build 9874914711](https://coveralls.io/builds/68565962): |  -0.007% |
| Covered Lines: | 6841 |
| Relevant Lines: | 7601 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/haystack/components/caching/cache_checker.py b/haystack/components/caching/cache_checker.py
index f4630b38..2de3ee52 100644
--- a/haystack/components/caching/cache_checker.py
+++ b/haystack/components/caching/cache_checker.py
@@ -2,10 +2,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-import importlib
 from typing import Any, Dict, List
 
 from haystack import DeserializationError, Document, component, default_from_dict, default_to_dict, logging
+from haystack.core.serialization import import_class_by_name
 from haystack.document_stores.types import DocumentStore
 
 logger = logging.getLogger(__name__)
@@ -77,19 +77,13 @@ class CacheChecker:
         if "type" not in init_params["document_store"]:
             raise DeserializationError("Missing 'type' in document store's serialization data")
 
+        doc_store_data = data["init_parameters"]["document_store"]
         try:
-            module_name, type_ = init_params["document_store"]["type"].rsplit(".", 1)
-            logger.debug("Trying to import module '{module_name}'", module_name=module_name)
-            module = importlib.import_module(module_name)
-        except (ImportError, DeserializationError) as e:
-            raise DeserializationError(
-                f"DocumentStore of type '{init_params['document_store']['type']}' not correctly imported"
-            ) from e
-
-        docstore_class = getattr(module, type_)
-        docstore = docstore_class.from_dict(init_params["document_store"])
-
-        data["init_parameters"]["document_store"] = docstore
+            doc_store_class = import_class_by_name(doc_store_data["type"])
+        except ImportError as e:
+            raise DeserializationError(f"Class '{doc_store_data['type']}' not correctly imported") from e
+        data["init_parameters"]["document_store"] = default_from_dict(doc_store_class, doc_store_data)
+
         return default_from_dict(cls, data)
 
     @component.output_types(hits=List[Document], misses=List)
diff --git a/haystack/components/converters/pypdf.py b/haystack/components/converters/pypdf.py
index 9f559d08..8f5aa5ce 100644
--- a/haystack/components/converters/pypdf.py
+++ b/haystack/components/converters/pypdf.py
@@ -106,14 +106,8 @@ class PyPDFToDocument:
         :returns:
             Deserialized component.
         """
-
-        if converter := data["init_parameters"].get("converter"):
-            converter_class = deserialize_type(converter["type"])
-            data["init_parameters"]["converter"] = converter_class.from_dict(data["init_parameters"]["converter"])
-        else:
-            # Ensures backwards compatibility with Pipelines dumped with < 2.3.0
-            data["init_parameters"]["converter"] = DefaultConverter()
-
+        converter_class = deserialize_type(data["init_parameters"]["converter"]["type"])
+        data["init_parameters"]["converter"] = converter_class.from_dict(data["init_parameters"]["converter"])
         return default_from_dict(cls, data)
 
     @component.output_types(documents=List[Document])
diff --git a/haystack/components/evaluators/context_relevance.py b/haystack/components/evaluators/context_relevance.py
index b3dbae58..d6fe3adf 100644
--- a/haystack/components/evaluators/context_relevance.py
+++ b/haystack/components/evaluators/context_relevance.py
@@ -93,6 +93,7 @@ class ContextRelevanceEvaluator(LLMEvaluator):
         progress_bar: bool = True,
         api: str = "openai",
         api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"),
+        api_params: Optional[Dict[str, Any]] = None,
         raise_on_failure: bool = True,
     ):
         """
@@ -121,6 +122,8 @@ class ContextRelevanceEvaluator(LLMEvaluator):
             Supported APIs: "openai".
         :param api_key:
             The API key.
+        :param api_params:
+            Parameters for an OpenAI API compatible completions call.
         :param raise_on_failure:
             Whether to raise an exception if the API call fails.
 
@@ -137,6 +140,7 @@ class ContextRelevanceEvaluator(LLMEvaluator):
         self.examples = examples or _DEFAULT_EXAMPLES
         self.api = api
         self.api_key = api_key
+        self.api_params = api_params or {}
 
         warnings.warn(
             "The output of the ContextRelevanceEvaluator will change in Haystack 2.4.0. "
@@ -152,6 +156,7 @@ class ContextRelevanceEvaluator(LLMEvaluator):
             examples=self.examples,
             api=self.api,
             api_key=self.api_key,
+            api_params=self.api_params,
             raise_on_failure=raise_on_failure,
             progress_bar=progress_bar,
         )
@@ -202,6 +207,7 @@ class ContextRelevanceEvaluator(LLMEvaluator):
             api_key=self.api_key.to_dict() if self.api_key else None,
             examples=self.examples,
             progress_bar=self.progress_bar,
+            api_params=self.api_params,
             raise_on_failure=self.raise_on_failure,
         )
 
diff --git a/haystack/components/evaluators/faithfulness.py b/haystack/components/evaluators/faithfulness.py
index 25b38673..5cdfceaf 100644
--- a/haystack/components/evaluators/faithfulness.py
+++ b/haystack/components/evaluators/faithfulness.py
@@ -88,6 +88,7 @@ class FaithfulnessEvaluator(LLMEvaluator):
         progress_bar: bool = True,
         api: str = "openai",
         api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"),
+        api_params: Optional[Dict[str, Any]] = None,
         raise_on_failure: bool = True,
     ):
         """
@@ -117,6 +118,8 @@ class FaithfulnessEvaluator(LLMEvaluator):
             Supported APIs: "openai".
         :param api_key:
             The API key.
+        :param api_params:
+            Parameters for an OpenAI API compatible completions call.
         :param raise_on_failure:
             Whether to raise an exception if the API call fails.
 
@@ -133,6 +136,7 @@ class FaithfulnessEvaluator(LLMEvaluator):
         self.examples = examples or _DEFAULT_EXAMPLES
         self.api = api
         self.api_key = api_key
+        self.api_params = api_params or {}
 
         super(FaithfulnessEvaluator, self).__init__(
             instructions=self.instructions,
@@ -141,6 +145,7 @@ class FaithfulnessEvaluator(LLMEvaluator):
             examples=self.examples,
             api=self.api,
             api_key=self.api_key,
+            api_params=self.api_params,
             raise_on_failure=raise_on_failure,
             progress_bar=progress_bar,
         )
@@ -193,6 +198,7 @@ class FaithfulnessEvaluator(LLMEvaluator):
             self,
             api=self.api,
             api_key=self.api_key.to_dict() if self.api_key else None,
+            api_params=self.api_params,
             examples=self.examples,
             progress_bar=self.progress_bar,
             raise_on_failure=self.raise_on_failure,
diff --git a/haystack/components/evaluators/llm_evaluator.py b/haystack/components/evaluators/llm_evaluator.py
index 22885f87..511bc1cd 100644
--- a/haystack/components/evaluators/llm_evaluator.py
+++ b/haystack/components/evaluators/llm_evaluator.py
@@ -58,6 +58,7 @@ class LLMEvaluator:
         raise_on_failure: bool = True,
         api: str = "openai",
         api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"),
+        api_params: Optional[Dict[str, Any]] = None,
     ):
         """
         Creates an instance of LLMEvaluator.
@@ -84,6 +85,8 @@ class LLMEvaluator:
             Supported APIs: "openai".
         :param api_key:
             The API key.
+        :param api_params:
+            Parameters for an OpenAI API compatible completions call.
 
         """
         self.validate_init_parameters(inputs, outputs, examples)
@@ -94,12 +97,16 @@ class LLMEvaluator:
         self.examples = examples
         self.api = api
         self.api_key = api_key
+        self.api_params = api_params or {}
         self.progress_bar = progress_bar
 
+        default_generation_kwargs = {"response_format": {"type": "json_object"}, "seed": 42}
+        user_generation_kwargs = self.api_params.get("generation_kwargs", {})
+        merged_generation_kwargs = {**default_generation_kwargs, **user_generation_kwargs}
+        self.api_params["generation_kwargs"] = merged_generation_kwargs
+
         if api == "openai":
-            self.generator = OpenAIGenerator(
-                api_key=api_key, generation_kwargs={"response_format": {"type": "json_object"}, "seed": 42}
-            )
+            self.generator = OpenAIGenerator(api_key=api_key, **self.api_params)
         else:
             raise ValueError(f"Unsupported API: {api}")
 
@@ -282,6 +289,7 @@ class LLMEvaluator:
             examples=self.examples,
             api=self.api,
             api_key=self.api_key.to_dict(),
+            api_params=self.api_params,
             progress_bar=self.progress_bar,
         )
 
diff --git a/haystack/components/retrievers/filter_retriever.py b/haystack/components/retrievers/filter_retriever.py
index b74c7b14..51e293f4 100644
--- a/haystack/components/retrievers/filter_retriever.py
+++ b/haystack/components/retrievers/filter_retriever.py
@@ -2,10 +2,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-import importlib
 from typing import Any, Dict, List, Optional
 
 from haystack import DeserializationError, Document, component, default_from_dict, default_to_dict, logging
+from haystack.core.serialization import import_class_by_name
 from haystack.document_stores.types import DocumentStore
 
 logger = logging.getLogger(__name__)
@@ -81,17 +81,14 @@ class FilterRetriever:
             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")
+
+        doc_store_data = data["init_parameters"]["document_store"]
         try:
-            module_name, type_ = init_params["document_store"]["type"].rsplit(".", 1)
-            logger.debug("Trying to import module '{module_name}'", module_name=module_name)
-            module = importlib.import_module(module_name)
-        except (ImportError, DeserializationError) as e:
-            raise DeserializationError(
-                f"DocumentStore of type '{init_params['document_store']['type']}' not correctly imported"
-            ) from e
-
-        docstore_class = getattr(module, type_)
-        data["init_parameters"]["document_store"] = docstore_class.from_dict(data["init_parameters"]["document_store"])
+            doc_store_class = import_class_by_name(doc_store_data["type"])
+        except ImportError as e:
+            raise DeserializationError(f"Class '{doc_store_data['type']}' not correctly imported") from e
+        data["init_parameters"]["document_store"] = default_from_dict(doc_store_class, doc_store_data)
+
         return default_from_dict(cls, data)
 
     @component.output_types(documents=List[Document])
diff --git a/haystack/components/writers/document_writer.py b/haystack/components/writers/document_writer.py
index 44b8528f..1f384bb5 100644
--- a/haystack/components/writers/document_writer.py
+++ b/haystack/components/writers/document_writer.py
@@ -2,10 +2,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-import importlib
 from typing import Any, Dict, List, Optional
 
 from haystack import DeserializationError, Document, component, default_from_dict, default_to_dict, logging
+from haystack.core.serialization import import_class_by_name
 from haystack.document_stores.types import DocumentStore, DuplicatePolicy
 
 logger = logging.getLogger(__name__)
@@ -79,20 +79,14 @@ class DocumentWriter:
         if "type" not in init_params["document_store"]:
             raise DeserializationError("Missing 'type' in document store's serialization data")
 
+        doc_store_data = data["init_parameters"]["document_store"]
         try:
-            module_name, type_ = init_params["document_store"]["type"].rsplit(".", 1)
-            logger.debug("Trying to import module '{module_name}'", module_name=module_name)
-            module = importlib.import_module(module_name)
-        except (ImportError, DeserializationError) as e:
-            raise DeserializationError(
-                f"DocumentStore of type '{init_params['document_store']['type']}' not correctly imported"
-            ) from e
-
-        docstore_class = getattr(module, type_)
-        docstore = docstore_class.from_dict(init_params["document_store"])
-
-        data["init_parameters"]["document_store"] = docstore
+            doc_store_class = import_class_by_name(doc_store_data["type"])
+        except ImportError as e:
+            raise DeserializationError(f"Class '{doc_store_data['type']}' not correctly imported") from e
+        data["init_parameters"]["document_store"] = default_from_dict(doc_store_class, doc_store_data)
         data["init_parameters"]["policy"] = DuplicatePolicy[data["init_parameters"]["policy"]]
+
         return default_from_dict(cls, data)
 
     @component.output_types(documents_written=int)
diff --git a/releasenotes/notes/allow-parameters-for-openai-generator-8ce86ba1bac80817.yaml b/releasenotes/notes/allow-parameters-for-openai-generator-8ce86ba1bac80817.yaml
new file mode 100644
index 00000000..0c5e2b93
--- /dev/null
+++ b/releasenotes/notes/allow-parameters-for-openai-generator-8ce86ba1bac80817.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    LLM based evaluators can pass in supported OpenAIGenerator parameters via api_params.
+    This allows for custom generation_kwargs, changing the api_base_url (for local evaluation),
+    and all other supported parameters as described in the OpenAIGenerator docs.
diff --git a/releasenotes/notes/improve-pypdf-backwards-compatibility-bcc75871005e9aba.yaml b/releasenotes/notes/improve-pypdf-backwards-compatibility-bcc75871005e9aba.yaml
deleted file mode 100644
index 48a46dfb..00000000
--- a/releasenotes/notes/improve-pypdf-backwards-compatibility-bcc75871005e9aba.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-enhancements:
- - |
-   Enhanced the PyPDF converter to ensure backwards compatibility with Pipelines dumped with versions older than 2.3.0. The update includes a conditional check to automatically default to the `DefaultConverter` if a specific converter is not provided, improving the component's robustness and ease of use.


Test patch:
diff --git a/test/components/converters/test_pypdf_to_document.py b/test/components/converters/test_pypdf_to_document.py
index ecaba457..00530192 100644
--- a/test/components/converters/test_pypdf_to_document.py
+++ b/test/components/converters/test_pypdf_to_document.py
@@ -40,12 +40,6 @@ class TestPyPDFToDocument:
         assert isinstance(instance, PyPDFToDocument)
         assert isinstance(instance.converter, DefaultConverter)
 
-    def test_from_dict_no_converter(self):
-        data = {"type": "haystack.components.converters.pypdf.PyPDFToDocument", "init_parameters": {}}
-        instance = PyPDFToDocument.from_dict(data)
-        assert isinstance(instance, PyPDFToDocument)
-        assert isinstance(instance.converter, DefaultConverter)
-
     @pytest.mark.integration
     def test_run(self, test_files_path, pypdf_converter):
         """
diff --git a/test/components/evaluators/test_context_relevance_evaluator.py b/test/components/evaluators/test_context_relevance_evaluator.py
index 1a7daa0b..89f5b5ae 100644
--- a/test/components/evaluators/test_context_relevance_evaluator.py
+++ b/test/components/evaluators/test_context_relevance_evaluator.py
@@ -100,6 +100,7 @@ class TestContextRelevanceEvaluator:
             "init_parameters": {
                 "api_key": {"env_vars": ["ENV_VAR"], "strict": True, "type": "env_var"},
                 "api": "openai",
+                "api_params": {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}},
                 "examples": [{"inputs": {"questions": "What is football?"}, "outputs": {"score": 0}}],
                 "progress_bar": False,
                 "raise_on_failure": False,
diff --git a/test/components/evaluators/test_faithfulness_evaluator.py b/test/components/evaluators/test_faithfulness_evaluator.py
index 7616a884..8164ad9c 100644
--- a/test/components/evaluators/test_faithfulness_evaluator.py
+++ b/test/components/evaluators/test_faithfulness_evaluator.py
@@ -109,6 +109,7 @@ class TestFaithfulnessEvaluator:
             "init_parameters": {
                 "api_key": {"env_vars": ["ENV_VAR"], "strict": True, "type": "env_var"},
                 "api": "openai",
+                "api_params": {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}},
                 "examples": [
                     {"inputs": {"predicted_answers": "Football is the most popular sport."}, "outputs": {"score": 0}}
                 ],
diff --git a/test/components/evaluators/test_llm_evaluator.py b/test/components/evaluators/test_llm_evaluator.py
index edab8734..21d5fd02 100644
--- a/test/components/evaluators/test_llm_evaluator.py
+++ b/test/components/evaluators/test_llm_evaluator.py
@@ -1,6 +1,7 @@
 # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
 #
 # SPDX-License-Identifier: Apache-2.0
+import os
 from typing import List
 
 import pytest
@@ -23,6 +24,7 @@ class TestLLMEvaluator:
         )
         assert component.api == "openai"
         assert component.generator.client.api_key == "test-api-key"
+        assert component.api_params == {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}}
         assert component.instructions == "test-instruction"
         assert component.inputs == [("predicted_answers", List[str])]
         assert component.outputs == ["score"]
@@ -47,6 +49,7 @@ class TestLLMEvaluator:
         component = LLMEvaluator(
             instructions="test-instruction",
             api_key=Secret.from_token("test-api-key"),
+            api_params={"generation_kwargs": {"seed": 43}},
             inputs=[("predicted_answers", List[str])],
             outputs=["custom_score"],
             api="openai",
@@ -62,6 +65,7 @@ class TestLLMEvaluator:
             ],
         )
         assert component.generator.client.api_key == "test-api-key"
+        assert component.api_params == {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 43}}
         assert component.api == "openai"
         assert component.examples == [
             {"inputs": {"predicted_answers": "Damn, this is straight outta hell!!!"}, "outputs": {"custom_score": 1}},
@@ -202,6 +206,7 @@ class TestLLMEvaluator:
         assert data == {
             "type": "haystack.components.evaluators.llm_evaluator.LLMEvaluator",
             "init_parameters": {
+                "api_params": {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}},
                 "api_key": {"env_vars": ["OPENAI_API_KEY"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
@@ -220,6 +225,7 @@ class TestLLMEvaluator:
         data = {
             "type": "haystack.components.evaluators.llm_evaluator.LLMEvaluator",
             "init_parameters": {
+                "api_params": {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}},
                 "api_key": {"env_vars": ["OPENAI_API_KEY"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
@@ -233,6 +239,7 @@ class TestLLMEvaluator:
         component = LLMEvaluator.from_dict(data)
         assert component.api == "openai"
         assert component.generator.client.api_key == "test-api-key"
+        assert component.api_params == {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}}
         assert component.instructions == "test-instruction"
         assert component.inputs == [("predicted_answers", List[str])]
         assert component.outputs == ["score"]
@@ -263,6 +270,7 @@ class TestLLMEvaluator:
         assert data == {
             "type": "haystack.components.evaluators.llm_evaluator.LLMEvaluator",
             "init_parameters": {
+                "api_params": {"generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42}},
                 "api_key": {"env_vars": ["ENV_VAR"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
@@ -442,3 +450,63 @@ class TestLLMEvaluator:
                     {"inputs": {"predicted_answers": "Football is the most popular sport."}, "outputs": {"score": 0}}
                 ],
             )
+
+    def test_init_with_base_url(self):
+        component = LLMEvaluator(
+            instructions="test-instruction",
+            api_key=Secret.from_token("test-api-key"),
+            api_params={"api_base_url": "http://127.0.0.1:11434/v1"},
+            inputs=[("predicted_answers", List[str])],
+            outputs=["custom_score"],
+            api="openai",
+            examples=[
+                {
+                    "inputs": {"predicted_answers": "Damn, this is straight outta hell!!!"},
+                    "outputs": {"custom_score": 1},
+                },
+                {
+                    "inputs": {"predicted_answers": "Football is the most popular sport."},
+                    "outputs": {"custom_score": 0},
+                },
+            ],
+        )
+        assert component.generator.client.api_key == "test-api-key"
+        assert component.api_params == {
+            "generation_kwargs": {"response_format": {"type": "json_object"}, "seed": 42},
+            "api_base_url": "http://127.0.0.1:11434/v1",
+        }
+        assert component.api == "openai"
+        assert component.examples == [
+            {"inputs": {"predicted_answers": "Damn, this is straight outta hell!!!"}, "outputs": {"custom_score": 1}},
+            {"inputs": {"predicted_answers": "Football is the most popular sport."}, "outputs": {"custom_score": 0}},
+        ]
+        assert component.instructions == "test-instruction"
+        assert component.inputs == [("predicted_answers", List[str])]
+        assert component.outputs == ["custom_score"]
+
+    @pytest.mark.skipif(
+        not (os.environ.get("API_BASE_URL") and os.environ.get("MODEL_NAME")),
+        reason="Export env vars API_BASE_URL and MODEL_NAME containing the OpenAI API compatible server URL and the model name to run this test.",
+    )
+    @pytest.mark.integration
+    def test_run_with_base_url(self):
+        component = LLMEvaluator(
+            instructions="test-instruction",
+            api_key=Secret.from_token("test-api-key"),
+            api_params={"api_base_url": os.environ["API_BASE_URL"], "model": os.environ["MODEL_NAME"]},
+            inputs=[("predicted_answers", List[str])],
+            outputs=["custom_score"],
+            api="openai",
+            examples=[
+                {
+                    "inputs": {"predicted_answers": "Damn, this is straight outta hell!!!"},
+                    "outputs": {"custom_score": 1},
+                },
+                {
+                    "inputs": {"predicted_answers": "Football is the most popular sport."},
+                    "outputs": {"custom_score": 0},
+                },
+            ],
+        )
+        component.run(predicted_answers=["Damn, this is straight outta hell!!!", "Football is the most popular sport."])
+        assert component.outputs == ["custom_score"]