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:

Resolving patch:
diff --git a/haystack/components/evaluators/llm_evaluator.py b/haystack/components/evaluators/llm_evaluator.py
--- a/haystack/components/evaluators/llm_evaluator.py
+++ b/haystack/components/evaluators/llm_evaluator.py
@@ -11,7 +11,7 @@
 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 @@ def to_dict(self) -> Dict[str, Any]:
         :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 @@ def from_dict(cls, data: Dict[str, Any]) -> "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)
 


Test patch:
diff --git a/test/components/evaluators/test_llm_evaluator.py b/test/components/evaluators/test_llm_evaluator.py
--- 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 @@ def test_to_dict_default(self, monkeypatch):
                 "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 @@ def test_from_dict(self, monkeypatch):
                 "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 @@ def test_to_dict_with_parameters(self, monkeypatch):
                 "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 @@ def test_to_dict_with_parameters(self, monkeypatch):
             },
         }
 
+    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(