Repository: deepset-ai/haystack

Issue:
Serialization issues with `LLMEvaluator` and child classes
I'm encountering issues with serializing `LLMEvaluator` and child classes (`ContextRelevanceEvaluator`, `FaithfulnessEvaluator`).

**To Reproduce**
```python
from haystack import Pipeline
from haystack.components.evaluators import FaithfulnessEvaluator

pipeline = Pipeline()
pipeline.add_component("ev", FaithfulnessEvaluator())

pipeline.dumps()
```

<details>
  <summary><b>Error message</b></summary>
<pre>
Traceback (most recent call last):
  File "ev.py", line 7, in <module>
    pipeline.dumps()
  File "/home/anakin87/apps/haystack/haystack/core/pipeline/base.py", line 202, in dumps
    return marshaller.marshal(self.to_dict())
  File "/home/anakin87/apps/haystack/haystack/marshal/yaml.py", line 13, in marshal
    return yaml.dump(dict_)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/__init__.py", line 253, in dump
    return dump_all([data], stream, Dumper=Dumper, **kwds)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/__init__.py", line 241, in dump_all
    dumper.represent(data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 27, in represent
    node = self.represent_data(data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 207, in represent_dict
    return self.represent_mapping('tag:yaml.org,2002:map', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 118, in represent_mapping
    node_value = self.represent_data(item_value)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 207, in represent_dict
    return self.represent_mapping('tag:yaml.org,2002:map', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 118, in represent_mapping
    node_value = self.represent_data(item_value)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 207, in represent_dict
    return self.represent_mapping('tag:yaml.org,2002:map', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 118, in represent_mapping
    node_value = self.represent_data(item_value)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 207, in represent_dict
    return self.represent_mapping('tag:yaml.org,2002:map', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 118, in represent_mapping
    node_value = self.represent_data(item_value)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 199, in represent_list
    return self.represent_sequence('tag:yaml.org,2002:seq', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 92, in represent_sequence
    node_item = self.represent_data(item)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 48, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 286, in represent_tuple
    return self.represent_sequence('tag:yaml.org,2002:python/tuple', data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 92, in represent_sequence
    node_item = self.represent_data(item)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 52, in represent_data
    node = self.yaml_multi_representers[data_type](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 346, in represent_object
    return self.represent_sequence(tag+function_name, args)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 92, in represent_sequence
    node_item = self.represent_data(item)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 52, in represent_data
    node = self.yaml_multi_representers[data_type](self, data)
  File "/home/anakin87/apps/haystack/.hatch/haystack-ai/lib/python3.8/site-packages/yaml/representer.py", line 331, in represent_object
    if function.__name__ == '__newobj__':
AttributeError: 'str' object has no attribute '__name__'
</pre>
</details>


**Additional context**
I encountered several problems, discussed a bit with Massi.

- python types (in `inputs: List[Tuple[str, Type[List]]]`) are not serialized. -> this causes the error I am encountering. We can take the `Multiplexer` as an example to serialize types.
- YAML serialization of tuples is problematic (https://github.com/deepset-ai/haystack-core-integrations/issues/523 https://github.com/deepset-ai/haystack-core-integrations/issues/694). If we want to use tuples in the `__init__` parameters, we should probably better configure the default marshaller and then check that components can do proper deserialization.
- (I initially thought that the absence of the `@component` decorator in the child classes was problematic. It probably isn't. It doesn't hurt to check.)

**System:**
 - Haystack version (commit or version number): 2.1.2

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


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.003%**) to **89.738%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/evaluators/llm_evaluator.py](https://coveralls.io/builds/67931620/source?filename=components%2Fevaluators%2Fllm_evaluator.py#L364) | 2 | 95.5% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67931620/badge)](https://coveralls.io/builds/67931620) |
| :-- | --: |
| Change from base [Build 9400937231](https://coveralls.io/builds/67930848): |  0.003% |
| Covered Lines: | 6777 |
| Relevant Lines: | 7552 |

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

coveralls: ## Pull Request Test Coverage Report for [Build 9401458578](https://coveralls.io/builds/67931724)

### Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

- For more information on this, see <a target="_blank" href="https://docs.coveralls.io/build-types#tracking-coverage-changes-with-pull_request-builds">Tracking coverage changes with pull request builds</a>.
- To avoid this issue with future PRs, see these <a target="_blank" href="https://docs.coveralls.io/build-types#recommended-ci-configurations">Recommended CI Configurations</a>.
- For a quick fix, <a target="_blank" href="https://github.blog/changelog/2022-02-03-more-ways-to-keep-your-pull-request-branch-up-to-date/#update-your-pull-request-branch-by-rebasing">rebase this PR at GitHub</a>. Your next report should be accurate.

### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.003%**) to **89.738%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/evaluators/llm_evaluator.py](https://coveralls.io/builds/67931724/source?filename=components%2Fevaluators%2Fllm_evaluator.py#L364) | 2 | 95.5% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67931724/badge)](https://coveralls.io/builds/67931724) |
| :-- | --: |
| Change from base [Build 9400937231](https://coveralls.io/builds/67930848): |  0.003% |
| Covered Lines: | 6777 |
| Relevant Lines: | 7552 |

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

coveralls: ## Pull Request Test Coverage Report for [Build 9401565701](https://coveralls.io/builds/67931731)

### Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

- For more information on this, see <a target="_blank" href="https://docs.coveralls.io/build-types#tracking-coverage-changes-with-pull_request-builds">Tracking coverage changes with pull request builds</a>.
- To avoid this issue with future PRs, see these <a target="_blank" href="https://docs.coveralls.io/build-types#recommended-ci-configurations">Recommended CI Configurations</a>.
- For a quick fix, <a target="_blank" href="https://github.blog/changelog/2022-02-03-more-ways-to-keep-your-pull-request-branch-up-to-date/#update-your-pull-request-branch-by-rebasing">rebase this PR at GitHub</a>. Your next report should be accurate.

### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.003%**) to **89.738%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/evaluators/llm_evaluator.py](https://coveralls.io/builds/67931731/source?filename=components%2Fevaluators%2Fllm_evaluator.py#L364) | 2 | 95.5% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67931731/badge)](https://coveralls.io/builds/67931731) |
| :-- | --: |
| Change from base [Build 9400937231](https://coveralls.io/builds/67930848): |  0.003% |
| Covered Lines: | 6777 |
| Relevant Lines: | 7552 |

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

coveralls: ## Pull Request Test Coverage Report for [Build 9415331669](https://coveralls.io/builds/67953680)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.003%**) to **89.753%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/evaluators/llm_evaluator.py](https://coveralls.io/builds/67953680/source?filename=components%2Fevaluators%2Fllm_evaluator.py#L364) | 2 | 95.5% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67953680/badge)](https://coveralls.io/builds/67953680) |
| :-- | --: |
| Change from base [Build 9415009179](https://coveralls.io/builds/67952979): |  0.003% |
| Covered Lines: | 6823 |
| Relevant Lines: | 7602 |

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


Patch:
diff --git a/haystack/components/evaluators/llm_evaluator.py b/haystack/components/evaluators/llm_evaluator.py
index fdfe49ff..fedb161a 100644
--- a/haystack/components/evaluators/llm_evaluator.py
+++ b/haystack/components/evaluators/llm_evaluator.py
@@ -11,7 +11,7 @@ from tqdm import tqdm
 from haystack import component, default_from_dict, default_to_dict
 from haystack.components.builders import PromptBuilder
 from haystack.components.generators import OpenAIGenerator
-from haystack.utils import Secret, deserialize_secrets_inplace
+from haystack.utils import Secret, deserialize_secrets_inplace, deserialize_type, serialize_type
 
 
 @component
@@ -266,10 +266,12 @@ class LLMEvaluator:
         :returns:
             The serialized component as a dictionary.
         """
+        # Since we cannot currently serialize tuples, convert the inputs to a list.
+        inputs = [[name, serialize_type(type_)] for name, type_ in self.inputs]
         return default_to_dict(
             self,
             instructions=self.instructions,
-            inputs=self.inputs,
+            inputs=inputs,
             outputs=self.outputs,
             examples=self.examples,
             api=self.api,
@@ -287,6 +289,10 @@ class LLMEvaluator:
         :returns:
             The deserialized component instance.
         """
+        data["init_parameters"]["inputs"] = [
+            (name, deserialize_type(type_)) for name, type_ in data["init_parameters"]["inputs"]
+        ]
+
         deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
         return default_from_dict(cls, data)
 
diff --git a/releasenotes/notes/llm-evaluator-serde-fix-aa5b27d2524db9c5.yaml b/releasenotes/notes/llm-evaluator-serde-fix-aa5b27d2524db9c5.yaml
new file mode 100644
index 00000000..aabfaaa6
--- /dev/null
+++ b/releasenotes/notes/llm-evaluator-serde-fix-aa5b27d2524db9c5.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    Correctly serialize tuples and types in the init parameters of the `LLMEvaluator` component and its subclasses.


Test patch:
diff --git a/test/components/evaluators/test_llm_evaluator.py b/test/components/evaluators/test_llm_evaluator.py
index 793f1861..d3ec4d29 100644
--- a/test/components/evaluators/test_llm_evaluator.py
+++ b/test/components/evaluators/test_llm_evaluator.py
@@ -3,9 +3,9 @@
 # SPDX-License-Identifier: Apache-2.0
 from typing import List
 
-import numpy as np
 import pytest
 
+from haystack import Pipeline
 from haystack.components.evaluators import LLMEvaluator
 from haystack.utils.auth import Secret
 
@@ -205,7 +205,7 @@ class TestLLMEvaluator:
                 "api_key": {"env_vars": ["OPENAI_API_KEY"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
-                "inputs": [("predicted_answers", List[str])],
+                "inputs": [["predicted_answers", "typing.List[str]"]],
                 "outputs": ["score"],
                 "progress_bar": True,
                 "examples": [
@@ -223,7 +223,7 @@ class TestLLMEvaluator:
                 "api_key": {"env_vars": ["OPENAI_API_KEY"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
-                "inputs": [("predicted_answers", List[str])],
+                "inputs": [["predicted_answers", "typing.List[str]"]],
                 "outputs": ["score"],
                 "examples": [
                     {"inputs": {"predicted_answers": "Football is the most popular sport."}, "outputs": {"score": 0}}
@@ -266,7 +266,7 @@ class TestLLMEvaluator:
                 "api_key": {"env_vars": ["ENV_VAR"], "strict": True, "type": "env_var"},
                 "api": "openai",
                 "instructions": "test-instruction",
-                "inputs": [("predicted_answers", List[str])],
+                "inputs": [["predicted_answers", "typing.List[str]"]],
                 "outputs": ["custom_score"],
                 "progress_bar": True,
                 "examples": [
@@ -282,6 +282,21 @@ class TestLLMEvaluator:
             },
         }
 
+    def test_serde(self, monkeypatch):
+        monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
+        pipeline = Pipeline()
+        component = LLMEvaluator(
+            instructions="test-instruction",
+            inputs=[("questions", List[str]), ("predicted_answers", List[List[str]])],
+            outputs=["score"],
+            examples=[
+                {"inputs": {"predicted_answers": "Football is the most popular sport."}, "outputs": {"score": 0}}
+            ],
+        )
+        pipeline.add_component("evaluator", component)
+        serialized_pipeline = pipeline.dumps()
+        deserialized_pipeline = Pipeline.loads(serialized_pipeline)
+
     def test_run_with_different_lengths(self, monkeypatch):
         monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
         component = LLMEvaluator(