Repository: deepset-ai/haystack

Issue:
serialize_type does not handle typing.Any correctly
**Describe the bug**
serialize_type(typing.Any) returns "typing._SpecialForm"

**Error message**
None

**Expected behavior**
serialize_type(typing.Any) should return "typing.Any"

**Additional context**
get_origin(Any) returns None so it is not recognizing Any as a typing object. I don't know what's the best way to do this tbh.
<img width="966" alt="Image" src="https://github.com/user-attachments/assets/86e25fa7-ae18-4011-bd32-e239cd9e3e66" />

**To Reproduce**
Run serialize_type(typing.Any)

**FAQ Check**
- [x] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
Irrelevent

Hints from issue comments:

Solution patch:
diff --git a/docs/pydoc/config/evaluation_api.yml b/docs/pydoc/config/evaluation_api.yml
index e445e9a568..6964b0cbc7 100644
--- a/docs/pydoc/config/evaluation_api.yml
+++ b/docs/pydoc/config/evaluation_api.yml
@@ -3,7 +3,6 @@ loaders:
     search_path: [../../../haystack/evaluation]
     modules:
       [
-        "base",
         "eval_run_result",
       ]
     ignore_when_discovered: ["__init__"]
diff --git a/haystack/evaluation/__init__.py b/haystack/evaluation/__init__.py
index 734699a03f..02b5e812f7 100644
--- a/haystack/evaluation/__init__.py
+++ b/haystack/evaluation/__init__.py
@@ -2,7 +2,6 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-from .base import BaseEvaluationRunResult
 from .eval_run_result import EvaluationRunResult
 
-__all__ = ["BaseEvaluationRunResult", "EvaluationRunResult"]
+__all__ = ["EvaluationRunResult"]
diff --git a/haystack/evaluation/base.py b/haystack/evaluation/base.py
deleted file mode 100644
index 617fa17638..0000000000
--- a/haystack/evaluation/base.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-
-from abc import ABC, abstractmethod
-from typing import List, Optional
-
-from pandas import DataFrame
-
-
-class BaseEvaluationRunResult(ABC):
-    """
-    Represents the results of an evaluation run.
-    """
-
-    @abstractmethod
-    def to_pandas(self) -> "DataFrame":
-        """
-        Creates a Pandas DataFrame containing the scores of each metric for every input sample.
-
-        :returns:
-            Pandas DataFrame with the scores.
-        """
-
-    @abstractmethod
-    def score_report(self) -> "DataFrame":
-        """
-        Transforms the results into a Pandas DataFrame with the aggregated scores for each metric.
-
-        :returns:
-            Pandas DataFrame with the aggregated scores.
-        """
-
-    @abstractmethod
-    def comparative_individual_scores_report(
-        self, other: "BaseEvaluationRunResult", keep_columns: Optional[List[str]] = None
-    ) -> "DataFrame":
-        """
-        Creates a Pandas DataFrame with the scores for each metric in the results of two different evaluation runs.
-
-        The inputs to both evaluation runs is assumed to be the same.
-
-        :param other:
-            Results of another evaluation run to compare with.
-        :param keep_columns:
-            List of common column names to keep from the inputs of the evaluation runs to compare.
-        :returns:
-            Pandas DataFrame with the score comparison.
-        """
diff --git a/haystack/evaluation/eval_run_result.py b/haystack/evaluation/eval_run_result.py
index 1d04419ebe..486a5082ec 100644
--- a/haystack/evaluation/eval_run_result.py
+++ b/haystack/evaluation/eval_run_result.py
@@ -2,17 +2,18 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
+import csv
 from copy import deepcopy
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Literal, Optional, Union
 from warnings import warn
 
-from pandas import DataFrame
-from pandas import concat as pd_concat
+from haystack.lazy_imports import LazyImport
 
-from .base import BaseEvaluationRunResult
+with LazyImport("Run 'pip install pandas'") as pandas_import:
+    from pandas import DataFrame
 
 
-class EvaluationRunResult(BaseEvaluationRunResult):
+class EvaluationRunResult:
     """
     Contains the inputs and the outputs of an evaluation pipeline and provides methods to inspect them.
     """
@@ -23,16 +24,14 @@ def __init__(self, run_name: str, inputs: Dict[str, List[Any]], results: Dict[st
 
         :param run_name:
             Name of the evaluation run.
+
         :param inputs:
-            Dictionary containing the inputs used for the run.
-            Each key is the name of the input and its value is
-            a list of input values. The length of the lists should
-            be the same.
+            Dictionary containing the inputs used for the run. Each key is the name of the input and its value is a list
+            of input values. The length of the lists should be the same.
+
         :param results:
-            Dictionary containing the results of the evaluators
-            used in the evaluation pipeline. Each key is the name
-            of the metric and its value is dictionary with the following
-            keys:
+            Dictionary containing the results of the evaluators used in the evaluation pipeline. Each key is the name
+            of the metric and its value is dictionary with the following keys:
                 - 'score': The aggregated score for the metric.
                 - 'individual_scores': A list of scores for each input sample.
         """
@@ -59,77 +58,186 @@ def __init__(self, run_name: str, inputs: Dict[str, List[Any]], results: Dict[st
                     f"Got {len(outputs['individual_scores'])} but expected {expected_len}."
                 )
 
-    def score_report(self) -> DataFrame:
+    @staticmethod
+    def _write_to_csv(csv_file: str, data: Dict[str, List[Any]]) -> str:
+        """
+        Write data to a CSV file.
+
+        :param csv_file: Path to the CSV file to write
+        :param data: Dictionary containing the data to write
+        :return: Status message indicating success or failure
+        """
+        list_lengths = [len(value) for value in data.values()]
+
+        if len(set(list_lengths)) != 1:
+            raise ValueError("All lists in the JSON must have the same length")
+
+        try:
+            headers = list(data.keys())
+            num_rows = list_lengths[0]
+            rows = []
+
+            for i in range(num_rows):
+                row = [data[header][i] for header in headers]
+                rows.append(row)
+
+            with open(csv_file, "w", newline="") as csvfile:
+                writer = csv.writer(csvfile)
+                writer.writerow(headers)
+                writer.writerows(rows)
+
+            return f"Data successfully written to {csv_file}"
+        except PermissionError:
+            return f"Error: Permission denied when writing to {csv_file}"
+        except IOError as e:
+            return f"Error writing to {csv_file}: {str(e)}"
+        except Exception as e:
+            return f"Error: {str(e)}"
+
+    @staticmethod
+    def _handle_output(
+        data: Dict[str, List[Any]], output_format: Literal["json", "csv", "df"] = "csv", csv_file: Optional[str] = None
+    ) -> Union[str, DataFrame, Dict[str, List[Any]]]:
+        """
+        Handles output formatting based on `output_format`.
+
+        :returns: DataFrame for 'df', dict for 'json', or confirmation message for 'csv'
         """
-        Transforms the results into a Pandas DataFrame with the aggregated scores for each metric.
+        if output_format == "json":
+            return data
+
+        elif output_format == "df":
+            pandas_import.check()
+            return DataFrame(data)
+
+        elif output_format == "csv":
+            if not csv_file:
+                raise ValueError("A file path must be provided in 'csv_file' parameter to save the CSV output.")
+            return EvaluationRunResult._write_to_csv(csv_file, data)
+
+        else:
+            raise ValueError(f"Invalid output format '{output_format}' provided. Choose from 'json', 'csv', or 'df'.")
+
+    def aggregated_report(
+        self, output_format: Literal["json", "csv", "df"] = "json", csv_file: Optional[str] = None
+    ) -> Union[Dict[str, List[Any]], "DataFrame", str]:
+        """
+        Generates a report with aggregated scores for each metric.
+
+        :param output_format: The output format for the report, "json", "csv", or "df", default to "json".
+        :param csv_file: Filepath to save CSV output if `output_format` is "csv", must be provided.
 
         :returns:
-            Pandas DataFrame with the aggregated scores.
+            JSON or DataFrame with aggregated scores, in case the output is set to a CSV file, a message confirming the
+            successful write or an error message.
         """
         results = {k: v["score"] for k, v in self.results.items()}
-        df = DataFrame.from_dict(results, orient="index", columns=["score"]).reset_index()
-        df.columns = ["metrics", "score"]
-        return df
+        data = {"metrics": list(results.keys()), "score": list(results.values())}
+        return self._handle_output(data, output_format, csv_file)
 
-    def to_pandas(self) -> DataFrame:
+    def detailed_report(
+        self, output_format: Literal["json", "csv", "df"] = "json", csv_file: Optional[str] = None
+    ) -> Union[Dict[str, List[Any]], "DataFrame", str]:
         """
-        Creates a Pandas DataFrame containing the scores of each metric for every input sample.
+        Generates a report with detailed scores for each metric.
+
+        :param output_format: The output format for the report, "json", "csv", or "df", default to "json".
+        :param csv_file: Filepath to save CSV output if `output_format` is "csv", must be provided.
 
         :returns:
-            Pandas DataFrame with the scores.
+            JSON or DataFrame with the detailed scores, in case the output is set to a CSV file, a message confirming
+            the successful write or an error message.
         """
-        inputs_columns = list(self.inputs.keys())
-        inputs_values = list(self.inputs.values())
-        inputs_values = list(map(list, zip(*inputs_values)))  # transpose the values
-        df_inputs = DataFrame(inputs_values, columns=inputs_columns)
 
-        scores_columns = list(self.results.keys())
-        scores_values = [v["individual_scores"] for v in self.results.values()]
-        scores_values = list(map(list, zip(*scores_values)))  # transpose the values
-        df_scores = DataFrame(scores_values, columns=scores_columns)
-
-        return df_inputs.join(df_scores)
+        combined_data = {col: self.inputs[col] for col in self.inputs}
 
-    def comparative_individual_scores_report(
-        self, other: "BaseEvaluationRunResult", keep_columns: Optional[List[str]] = None
-    ) -> DataFrame:
+        # enforce columns type consistency
+        scores_columns = list(self.results.keys())
+        for col in scores_columns:
+            col_values = self.results[col]["individual_scores"]
+            if any(isinstance(v, float) for v in col_values):
+                col_values = [float(v) for v in col_values]
+            combined_data[col] = col_values
+
+        return self._handle_output(combined_data, output_format, csv_file)
+
+    def comparative_detailed_report(
+        self,
+        other: "EvaluationRunResult",
+        keep_columns: Optional[List[str]] = None,
+        output_format: Literal["json", "csv", "df"] = "json",
+        csv_file: Optional[str] = None,
+    ) -> Union[str, "DataFrame", None]:
         """
-        Creates a Pandas DataFrame with the scores for each metric in the results of two different evaluation runs.
+        Generates a report with detailed scores for each metric from two evaluation runs for comparison.
 
-        The inputs to both evaluation runs is assumed to be the same.
+        :param other: Results of another evaluation run to compare with.
+        :param keep_columns: List of common column names to keep from the inputs of the evaluation runs to compare.
+        :param output_format: The output format for the report, "json", "csv", or "df", default to "json".
+        :param csv_file: Filepath to save CSV output if `output_format` is "csv", must be provided.
 
-        :param other:
-            Results of another evaluation run to compare with.
-        :param keep_columns:
-            List of common column names to keep from the inputs of the evaluation runs to compare.
         :returns:
-            Pandas DataFrame with the score comparison.
+            JSON or DataFrame with a comparison of the detailed scores, in case the output is set to a CSV file,
+             a message confirming the successful write or an error message.
         """
+
         if not isinstance(other, EvaluationRunResult):
             raise ValueError("Comparative scores can only be computed between EvaluationRunResults.")
 
-        this_name = self.run_name
-        other_name = other.run_name
-        if this_name == other_name:
-            warn(f"The run names of the two evaluation results are the same ('{this_name}')")
-            this_name = f"{this_name}_first"
-            other_name = f"{other_name}_second"
+        if not hasattr(other, "run_name") or not hasattr(other, "inputs") or not hasattr(other, "results"):
+            raise ValueError("The 'other' parameter must have 'run_name', 'inputs', and 'results' attributes.")
+
+        if self.run_name == other.run_name:
+            warn(f"The run names of the two evaluation results are the same ('{self.run_name}')")
 
         if self.inputs.keys() != other.inputs.keys():
-            warn(f"The input columns differ between the results; using the input columns of '{this_name}'.")
+            warn(f"The input columns differ between the results; using the input columns of '{self.run_name}'.")
+
+        # got both detailed reports
+        detailed_a = self.detailed_report(output_format="json")
+        detailed_b = other.detailed_report(output_format="json")
 
-        pipe_a_df = self.to_pandas()
-        pipe_b_df = other.to_pandas()
+        # ensure both detailed reports are in dictionaries format
+        if not isinstance(detailed_a, dict) or not isinstance(detailed_b, dict):
+            raise ValueError("Detailed reports must be dictionaries.")
 
+        # determine which columns to ignore
         if keep_columns is None:
             ignore = list(self.inputs.keys())
         else:
             ignore = [col for col in list(self.inputs.keys()) if col not in keep_columns]
 
-        pipe_b_df.drop(columns=ignore, inplace=True, errors="ignore")
-        pipe_b_df.columns = [f"{other_name}_{column}" for column in pipe_b_df.columns]  # type: ignore
-        pipe_a_df.columns = [f"{this_name}_{col}" if col not in ignore else col for col in pipe_a_df.columns]  # type: ignore
-
-        results_df = pd_concat([pipe_a_df, pipe_b_df], axis=1)
-
-        return results_df
+        # filter out ignored columns from pipe_b_dict
+        filtered_detailed_b = {
+            f"{other.run_name}_{key}": value for key, value in detailed_b.items() if key not in ignore
+        }
+
+        # rename columns in pipe_a_dict based on ignore list
+        renamed_detailed_a = {
+            (key if key in ignore else f"{self.run_name}_{key}"): value for key, value in detailed_a.items()
+        }
+
+        # combine both detailed reports
+        combined_results = {**renamed_detailed_a, **filtered_detailed_b}
+        return self._handle_output(combined_results, output_format, csv_file)
+
+    def score_report(self) -> "DataFrame":
+        """Generates a DataFrame report with aggregated scores for each metric."""
+        msg = "The `score_report` method is deprecated and will be changed to `aggregated_report` in Haystack 2.11.0."
+        warn(msg, DeprecationWarning, stacklevel=2)
+        return self.aggregated_report(output_format="df")
+
+    def to_pandas(self) -> "DataFrame":
+        """Generates a DataFrame report with detailed scores for each metric."""
+        msg = "The `to_pandas` method is deprecated and will be changed to `detailed_report` in Haystack 2.11.0."
+        warn(msg, DeprecationWarning, stacklevel=2)
+        return self.detailed_report(output_format="df")
+
+    def comparative_individual_scores_report(self, other: "EvaluationRunResult") -> "DataFrame":
+        """Generates a DataFrame report with detailed scores for each metric from two evaluation runs for comparison."""
+        msg = (
+            "The `comparative_individual_scores_report` method is deprecated and will be changed to "
+            "`comparative_detailed_report` in Haystack 2.11.0."
+        )
+        warn(msg, DeprecationWarning, stacklevel=2)
+        return self.comparative_detailed_report(other, output_format="df")
diff --git a/haystack/tools/component_tool.py b/haystack/tools/component_tool.py
index cc77ceca01..93e384c377 100644
--- a/haystack/tools/component_tool.py
+++ b/haystack/tools/component_tool.py
@@ -152,9 +152,7 @@ def component_invoker(**kwargs):
                 ]
             ).lstrip("_")
 
-        # Generate a description for the tool if not provided and truncate to 512 characters
-        # as most LLMs have a limit for the description length
-        description = (description or component.__doc__ or name)[:512]
+        description = description or component.__doc__ or name
 
         # Create the Tool instance with the component invoker as the function to be called and the schema
         super().__init__(name, description, tool_schema, component_invoker)
diff --git a/haystack/utils/type_serialization.py b/haystack/utils/type_serialization.py
index 5ffb505bb1..8a14a2f495 100644
--- a/haystack/utils/type_serialization.py
+++ b/haystack/utils/type_serialization.py
@@ -35,7 +35,7 @@ def serialize_type(target: Any) -> str:
         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))
+    is_type_or_typing = isinstance(target, type) or bool(get_origin(target)) or target == Any
     type_obj = target if is_type_or_typing else type(target)
     type_obj_repr = repr(type_obj)
 
diff --git a/releasenotes/notes/fix-serialize-any-type-ea0ca14eadb21b3d.yaml b/releasenotes/notes/fix-serialize-any-type-ea0ca14eadb21b3d.yaml
new file mode 100644
index 0000000000..5074706765
--- /dev/null
+++ b/releasenotes/notes/fix-serialize-any-type-ea0ca14eadb21b3d.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    Fixes serialization of typing.Any when using serialize_type utility
diff --git a/releasenotes/notes/fix-tool-description-truncation-011e8275fea4ccb0.yaml b/releasenotes/notes/fix-tool-description-truncation-011e8275fea4ccb0.yaml
new file mode 100644
index 0000000000..7e09c17c2d
--- /dev/null
+++ b/releasenotes/notes/fix-tool-description-truncation-011e8275fea4ccb0.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    ComponentTool does not truncate 'description' anymore.
diff --git a/releasenotes/notes/refactored-evalaution-nopandas-bcab00e4797a3c60.yaml b/releasenotes/notes/refactored-evalaution-nopandas-bcab00e4797a3c60.yaml
new file mode 100644
index 0000000000..ea9be2baf3
--- /dev/null
+++ b/releasenotes/notes/refactored-evalaution-nopandas-bcab00e4797a3c60.yaml
@@ -0,0 +1,8 @@
+---
+enhancements:
+  - |
+    `EvaluationRunResult` can now output the results in JSON, a pandas Dataframe or in a CSV file.
+
+deprecations:
+  - |
+    The use of pandas Dataframe in `EvaluationRunResult` is now optional and the methods `score_report`, `to_pandas` and `comparative_individual_scores_report` are deprecated and will be removed in the next haystack release.


Test patch:
diff --git a/test/components/generators/test_openai.py b/test/components/generators/test_openai.py
index 816412ee97..3940660d2b 100644
--- a/test/components/generators/test_openai.py
+++ b/test/components/generators/test_openai.py
@@ -311,10 +311,10 @@ def test_run_with_system_prompt(self):
         assert "teorema" in result["replies"][0].lower()
 
         result = generator.run(
-            "Can you explain the Pitagoras therom?",
+            "Can you explain the Pitagoras therom? Repeat the name of the theorem in German.",
             system_prompt="You answer in German, regardless of the language on which a question is asked.",
         )
-        assert "pythagoras" in result["replies"][0].lower()
+        assert "pythag" in result["replies"][0].lower()
 
     @pytest.mark.skipif(
         not os.environ.get("OPENAI_API_KEY", None),
diff --git a/test/core/pipeline/test_type_utils.py b/test/core/pipeline/test_type_utils.py
index e009853144..6173e4fdfb 100644
--- a/test/core/pipeline/test_type_utils.py
+++ b/test/core/pipeline/test_type_utils.py
@@ -3,10 +3,12 @@
 # SPDX-License-Identifier: Apache-2.0
 from enum import Enum
 from pathlib import Path
-from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union
+from typing import Any, Dict, Iterable, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union
 
 import pytest
 
+from haystack.dataclasses import ByteStream, ChatMessage, Document, GeneratedAnswer
+from haystack.core.component.types import Variadic
 from haystack.core.type_utils import _type_name, _types_are_compatible
 
 
@@ -27,8 +29,184 @@ class Enum1(Enum):
     TEST2 = Class2
 
 
+# Basic type groups
+primitive_types = [bool, bytes, dict, float, int, list, str, Path]
+class_types = [Class1, Class3]
+container_types = [
+    Iterable[int],
+    List[int],
+    List[Class1],
+    Set[str],
+    Dict[str, int],
+    Dict[str, Class1],
+    Sequence[bool],
+    Mapping[str, Dict[str, int]],
+    Variadic[int],
+]
+nested_container_types = [
+    List[Set[Sequence[bool]]],
+    List[Set[Sequence[Class1]]],
+    Dict[str, Mapping[str, Dict[str, int]]],
+    Dict[str, Mapping[str, Dict[str, Class1]]],
+    Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])],
+]
+literals = [Literal["a", "b", "c"], Literal[Enum1.TEST1]]
+haystack_types = [ByteStream, ChatMessage, Document, GeneratedAnswer]
+extras = [Union[int, str]]
+
+
+def generate_id(sender, receiver):
+    """Generate readable test case IDs."""
+    return f"{_type_name(sender)}-to-{_type_name(receiver)}"
+
+
+def generate_symmetric_cases():
+    """Generate symmetric test cases where sender and receiver types are the same."""
+    return [
+        pytest.param(t, t, id=generate_id(t, t))
+        for t in (
+            primitive_types
+            + class_types
+            + container_types
+            + literals
+            + haystack_types
+            + nested_container_types
+            + extras
+        )
+    ]
+
+
+def generate_asymmetric_cases():
+    """Generate asymmetric test cases with different sender and receiver types."""
+    cases = []
+
+    # Primitives: Optional, Union, Any
+    for t in (
+        primitive_types + class_types + container_types + literals + haystack_types + nested_container_types + extras
+    ):
+        cases.append(pytest.param(t, Optional[t], id=generate_id(t, Optional[t])))
+        cases.append(pytest.param(t, Union[t, complex], id=generate_id(t, Union[t, complex])))
+        cases.append(pytest.param(t, Any, id=generate_id(t, Any)))
+
+    # Classes: Optional, Union, Any
+    for cls in class_types:
+        cases.append(pytest.param(cls, Optional[cls], id=generate_id(cls, Optional[cls])))
+        cases.append(pytest.param(cls, Union[cls, complex], id=generate_id(cls, Union[cls, complex])))
+        cases.append(pytest.param(cls, Any, id=generate_id(cls, Any)))
+
+    # Subclass → Superclass
+    cases.extend(
+        [
+            # Subclass → Superclass
+            pytest.param(Class3, Class1, id=generate_id(Class3, Class1)),
+            # Subclass → Union of Superclass and other type
+            pytest.param(Class3, Union[int, Class1], id=generate_id(Class3, Union[int, Class1])),
+            # List of Class3 → List of Class1
+            pytest.param(List[Class3], List[Class1], id=generate_id(List[Class3], List[Class1])),
+            # Dict of subclass → Dict of superclass
+            pytest.param(Dict[str, Class3], Dict[str, Class1], id=generate_id(Dict[str, Class3], Dict[str, Class1])),
+        ]
+    )
+
+    # Containers: Optional, Union, and Any compatibility
+    for container in container_types:
+        cases.append(pytest.param(container, Optional[container], id=generate_id(container, Optional[container])))
+        cases.append(pytest.param(container, Union[container, int], id=generate_id(container, Union[container, int])))
+        cases.append(pytest.param(container, Any, id=generate_id(container, Any)))
+
+    # Extra cases
+    cases.extend(
+        [
+            pytest.param(bool, int, id="different-primitives"),
+            pytest.param((Literal["a", "b"]), (Literal["a", "b", "c"]), id="sending-subset-literal"),
+        ]
+    )
+
+    # Extra container cases
+    cases.extend(
+        [
+            pytest.param((List[int]), (List[Any]), id="list-of-primitive-to-list-of-any"),
+            pytest.param((List[Class1]), (List[Any]), id="list-of-classes-to-list-of-any"),
+            pytest.param(
+                (List[Set[Sequence[bool]]]),
+                (List[Set[Sequence[Any]]]),
+                id="nested-sequences-of-primitives-to-nested-sequences-of-any",
+            ),
+            pytest.param(
+                (List[Set[Sequence[Class3]]]),
+                (List[Set[Sequence[Class1]]]),
+                id="nested-sequences-of-subclasses-to-nested-sequences-of-classes",
+            ),
+            pytest.param(
+                (List[Set[Sequence[Class1]]]),
+                (List[Set[Sequence[Any]]]),
+                id="nested-sequences-of-classes-to-nested-sequences-of-any",
+            ),
+            pytest.param((Dict[(str, int)]), (Dict[(Any, int)]), id="dict-of-primitives-to-dict-of-any-keys"),
+            pytest.param((Dict[(str, int)]), (Dict[(str, Any)]), id="dict-of-primitives-to-dict-of-any-values"),
+            pytest.param((Dict[(str, int)]), (Dict[(Any, Any)]), id="dict-of-primitives-to-dict-of-any-key-and-values"),
+            pytest.param((Dict[(str, Class3)]), (Dict[(str, Class1)]), id="dict-of-subclasses-to-dict-of-classes"),
+            pytest.param((Dict[(str, Class1)]), (Dict[(Any, Class1)]), id="dict-of-classes-to-dict-of-any-keys"),
+            pytest.param((Dict[(str, Class1)]), (Dict[(str, Any)]), id="dict-of-classes-to-dict-of-any-values"),
+            pytest.param((Dict[(str, Class1)]), (Dict[(Any, Any)]), id="dict-of-classes-to-dict-of-any-key-and-values"),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+                (Dict[(str, Mapping[(str, Dict[(Any, int)])])]),
+                id="nested-mapping-of-primitives-to-nested-mapping-of-any-keys",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+                (Dict[(str, Mapping[(Any, Dict[(str, int)])])]),
+                id="nested-mapping-of-primitives-to-nested-mapping-of-higher-level-any-keys",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+                (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
+                id="nested-mapping-of-primitives-to-nested-mapping-of-any-values",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+                (Dict[(str, Mapping[(Any, Dict[(Any, Any)])])]),
+                id="nested-mapping-of-primitives-to-nested-mapping-of-any-keys-and-values",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, Class3)])])]),
+                (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+                id="nested-mapping-of-subclasses-to-nested-mapping-of-classes",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+                (Dict[(str, Mapping[(str, Dict[(Any, Class1)])])]),
+                id="nested-mapping-of-classes-to-nested-mapping-of-any-keys",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+                (Dict[(str, Mapping[(Any, Dict[(str, Class1)])])]),
+                id="nested-mapping-of-classes-to-nested-mapping-of-higher-level-any-keys",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+                (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
+                id="nested-mapping-of-classes-to-nested-mapping-of-any-values",
+            ),
+            pytest.param(
+                (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+                (Dict[(str, Mapping[(Any, Dict[(Any, Any)])])]),
+                id="nested-mapping-of-classes-to-nested-mapping-of-any-keys-and-values",
+            ),
+        ]
+    )
+
+    return cases
+
+
+# Precompute test cases for reuse
+symmetric_cases = generate_symmetric_cases()
+asymmetric_cases = generate_asymmetric_cases()
+
+
 @pytest.mark.parametrize(
-    "type_,repr",
+    "type_,repr_",
     [
         pytest.param(str, "str", id="primitive-types"),
         pytest.param(Any, "Any", id="any"),
@@ -78,252 +256,110 @@ class Enum1(Enum):
         ),
     ],
 )
-def test_type_name(type_, repr):
-    assert _type_name(type_) == repr
+def test_type_name(type_, repr_):
+    assert _type_name(type_) == repr_
+
+
+@pytest.mark.parametrize("sender_type, receiver_type", symmetric_cases)
+def test_same_types_are_compatible(sender_type, receiver_type):
+    assert _types_are_compatible(sender_type, receiver_type)
+
+
+@pytest.mark.parametrize("sender_type, receiver_type", asymmetric_cases)
+def test_asymmetric_types_are_compatible(sender_type, receiver_type):
+    assert _types_are_compatible(sender_type, receiver_type)
+
+
+@pytest.mark.parametrize("sender_type, receiver_type", asymmetric_cases)
+def test_asymmetric_types_are_not_compatible(sender_type, receiver_type):
+    assert not _types_are_compatible(receiver_type, sender_type)
+
+
+incompatible_type_cases = [
+    pytest.param(int, str, id="different-primitives"),
+    pytest.param(Class1, Class2, id="different-classes"),
+    pytest.param((List[int]), (List[str]), id="different-lists-of-primitives"),
+    pytest.param((List[Class1]), (List[Class2]), id="different-lists-of-classes"),
+    pytest.param(
+        (List[Set[Sequence[str]]]), (List[Set[Sequence[bool]]]), id="nested-sequences-of-different-primitives"
+    ),
+    pytest.param(
+        (List[Set[Sequence[str]]]), (Set[List[Sequence[str]]]), id="different-nested-sequences-of-same-primitives"
+    ),
+    pytest.param(
+        (List[Set[Sequence[Class1]]]), (List[Set[Sequence[Class2]]]), id="nested-sequences-of-different-classes"
+    ),
+    pytest.param(
+        (List[Set[Sequence[Class1]]]), (Set[List[Sequence[Class1]]]), id="different-nested-sequences-of-same-class"
+    ),
+    pytest.param((Dict[(str, int)]), (Dict[(int, int)]), id="different-dict-of-primitive-keys"),
+    pytest.param((Dict[(str, int)]), (Dict[(str, float)]), id="different-dict-of-primitive-values"),
+    pytest.param((Dict[(str, Class1)]), (Dict[(str, Class2)]), id="different-dict-of-class-values"),
+    pytest.param((Dict[(str, Any)]), (Dict[(int, int)]), id="dict-of-Any-values-to-dict-of-primitives"),
+    pytest.param((Dict[(str, Any)]), (Dict[(int, Class1)]), id="dict-of-Any-values-to-dict-of-classes"),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+        (Mapping[(str, Dict[(str, Dict[(str, int)])])]),
+        id="different-nested-mappings-of-same-primitives",
+    ),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+        (Dict[(str, Mapping[(str, Dict[(int, int)])])]),
+        id="same-nested-mappings-of-different-primitive-keys",
+    ),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+        (Dict[(str, Mapping[(int, Dict[(str, int)])])]),
+        id="same-nested-mappings-of-different-higher-level-primitive-keys",
+    ),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
+        (Dict[(str, Mapping[(str, Dict[(str, dict)])])]),
+        id="same-nested-mappings-of-different-primitive-values",
+    ),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+        (Dict[(str, Mapping[(str, Dict[(str, Class2)])])]),
+        id="same-nested-mappings-of-different-class-values",
+    ),
+    pytest.param(
+        (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
+        (Dict[(str, Mapping[(str, Dict[(str, Class2)])])]),
+        id="same-nested-mappings-of-class-to-subclass-values",
+    ),
+    pytest.param((Literal["a", "b", "c"]), (Literal["x", "y"]), id="different-literal-of-same-primitive"),
+    pytest.param((Literal[Enum1.TEST1]), (Literal[Enum1.TEST2]), id="different-literal-of-same-enum"),
+]
+
+
+@pytest.mark.parametrize("sender_type, receiver_type", incompatible_type_cases)
+def test_types_are_always_not_compatible(sender_type, receiver_type):
+    assert not _types_are_compatible(sender_type, receiver_type)
+
+
+def test_deeply_nested_type_is_compatible_but_cannot_be_checked():
+    sender_type = Tuple[Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])]]
+    receiver_type = Tuple[Literal["a", "b", "c"], Union[(Path, Dict[(int, Class1)])]]
+    assert not _types_are_compatible(sender_type, receiver_type)
 
 
 @pytest.mark.parametrize(
     "sender_type,receiver_type",
     [
-        pytest.param(str, str, id="same-primitives"),
-        pytest.param(str, (Optional[str]), id="receiving-primitive-is-optional"),
-        pytest.param(str, (Union[(int, str)]), id="receiving-type-is-union-of-primitives"),
-        pytest.param((Union[(int, str)]), (Union[(int, str)]), id="identical-unions"),
-        pytest.param((Union[(int, str)]), (Union[(int, str, bool)]), id="receiving-union-is-superset-of-sender"),
-        pytest.param(str, Any, id="primitive-to-any"),
-        pytest.param(Class1, Class1, id="same-class"),
-        pytest.param(Class1, (Optional[Class1]), id="receiving-class-is-optional"),
-        pytest.param(Class1, Class1, id="class-to-any"),
-        pytest.param(Class3, Class1, id="subclass-to-class"),
-        pytest.param(Class1, (Union[(int, Class1)]), id="receiving-type-is-union-of-classes"),
-        pytest.param(Class3, (Union[(int, Class1)]), id="receiving-type-is-union-of-superclasses"),
-        pytest.param((List[int]), (List[int]), id="same-lists"),
-        pytest.param((List[int]), (Optional[List[int]]), id="receiving-list-is-optional"),
-        pytest.param((List[int]), (List[Any]), id="list-of-primitive-to-list-of-any"),
-        pytest.param((List[Class1]), (List[Class1]), id="list-of-same-classes"),
-        pytest.param((List[Class3]), (List[Class1]), id="list-of-subclass-to-list-of-class"),
-        pytest.param((List[Class1]), (List[Any]), id="list-of-classes-to-list-of-any"),
-        pytest.param(
-            (List[Set[Sequence[bool]]]), (List[Set[Sequence[bool]]]), id="nested-sequences-of-same-primitives"
-        ),
-        pytest.param(
-            (List[Set[Sequence[bool]]]),
-            (List[Set[Sequence[Any]]]),
-            id="nested-sequences-of-primitives-to-nested-sequences-of-any",
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class1]]]), (List[Set[Sequence[Class1]]]), id="nested-sequences-of-same-classes"
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class3]]]),
-            (List[Set[Sequence[Class1]]]),
-            id="nested-sequences-of-subclasses-to-nested-sequences-of-classes",
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class1]]]),
-            (List[Set[Sequence[Any]]]),
-            id="nested-sequences-of-classes-to-nested-sequences-of-any",
-        ),
-        pytest.param((Dict[(str, int)]), (Dict[(str, int)]), id="same-dicts-of-primitives"),
-        pytest.param((Dict[(str, int)]), (Dict[(Any, int)]), id="dict-of-primitives-to-dict-of-any-keys"),
-        pytest.param((Dict[(str, int)]), (Dict[(str, Any)]), id="dict-of-primitives-to-dict-of-any-values"),
-        pytest.param((Dict[(str, int)]), (Dict[(Any, Any)]), id="dict-of-primitives-to-dict-of-any-key-and-values"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(str, Class1)]), id="same-dicts-of-classes-values"),
-        pytest.param((Dict[(str, Class3)]), (Dict[(str, Class1)]), id="dict-of-subclasses-to-dict-of-classes"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(Any, Class1)]), id="dict-of-classes-to-dict-of-any-keys"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(str, Any)]), id="dict-of-classes-to-dict-of-any-values"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(Any, Any)]), id="dict-of-classes-to-dict-of-any-key-and-values"),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            id="nested-mappings-of-same-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(Any, int)])])]),
-            id="nested-mapping-of-primitives-to-nested-mapping-of-any-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(Any, Dict[(str, int)])])]),
-            id="nested-mapping-of-primitives-to-nested-mapping-of-higher-level-any-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
-            id="nested-mapping-of-primitives-to-nested-mapping-of-any-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(Any, Dict[(Any, Any)])])]),
-            id="nested-mapping-of-primitives-to-nested-mapping-of-any-keys-and-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            id="nested-mappings-of-same-classes",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class3)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            id="nested-mapping-of-subclasses-to-nested-mapping-of-classes",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(str, Dict[(Any, Class1)])])]),
-            id="nested-mapping-of-classes-to-nested-mapping-of-any-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(Any, Dict[(str, Class1)])])]),
-            id="nested-mapping-of-classes-to-nested-mapping-of-higher-level-any-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
-            id="nested-mapping-of-classes-to-nested-mapping-of-any-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(Any, Dict[(Any, Any)])])]),
-            id="nested-mapping-of-classes-to-nested-mapping-of-any-keys-and-values",
-        ),
-        pytest.param((Literal["a", "b", "c"]), (Literal["a", "b", "c"]), id="same-primitive-literal"),
-        pytest.param((Literal[Enum1.TEST1]), (Literal[Enum1.TEST1]), id="same-enum-literal"),
-        pytest.param(
-            (Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])]),
-            (Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])]),
-            id="identical-deeply-nested-complex-type",
-        ),
+        pytest.param((Union[(int, bool)]), (Union[(int, str)]), id="partially-overlapping-unions-with-primitives"),
+        pytest.param((Union[(int, Class1)]), (Union[(int, Class2)]), id="partially-overlapping-unions-with-classes"),
     ],
 )
-def test_types_are_compatible(sender_type, receiver_type):
-    assert _types_are_compatible(sender_type, receiver_type)
+def test_partially_overlapping_unions(sender_type, receiver_type):
+    assert not _types_are_compatible(sender_type, receiver_type)
 
 
 @pytest.mark.parametrize(
     "sender_type,receiver_type",
     [
-        pytest.param(int, bool, id="different-primitives"),
-        pytest.param(Class1, Class2, id="different-classes"),
-        pytest.param(Class1, Class3, id="class-to-subclass"),
-        pytest.param(Any, int, id="any-to-primitive"),
-        pytest.param(Any, Class2, id="any-to-class"),
-        pytest.param((Optional[str]), str, id="sending-primitive-is-optional"),
-        pytest.param((Optional[Class1]), Class1, id="sending-class-is-optional"),
-        pytest.param((Optional[List[int]]), (List[int]), id="sending-list-is-optional"),
-        pytest.param((Union[(int, str)]), str, id="sending-type-is-union"),
-        pytest.param((Union[(int, str, bool)]), (Union[(int, str)]), id="sending-union-is-superset-of-receiver"),
-        pytest.param((Union[(int, bool)]), (Union[(int, str)]), id="partially-overlapping-unions-with-primitives"),
-        pytest.param((Union[(int, Class1)]), (Union[(int, Class2)]), id="partially-overlapping-unions-with-classes"),
-        pytest.param((List[int]), (List[str]), id="different-lists-of-primitives"),
         pytest.param((List[int]), List, id="list-of-primitive-to-bare-list"),
         pytest.param((List[int]), list, id="list-of-primitive-to-list-object"),
-        pytest.param((List[Class1]), (List[Class2]), id="different-lists-of-classes"),
-        pytest.param((List[Class1]), (List[Class3]), id="lists-of-classes-to-subclasses"),
-        pytest.param((List[Any]), (List[str]), id="list-of-any-to-list-of-primitives"),
-        pytest.param((List[Any]), (List[Class2]), id="list-of-any-to-list-of-classes"),
-        pytest.param(
-            (List[Set[Sequence[str]]]), (List[Set[Sequence[bool]]]), id="nested-sequences-of-different-primitives"
-        ),
-        pytest.param(
-            (List[Set[Sequence[str]]]), (Set[List[Sequence[str]]]), id="different-nested-sequences-of-same-primitives"
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class1]]]), (List[Set[Sequence[Class2]]]), id="nested-sequences-of-different-classes"
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class1]]]), (List[Set[Sequence[Class3]]]), id="nested-sequences-of-classes-to-subclasses"
-        ),
-        pytest.param(
-            (List[Set[Sequence[Class1]]]), (Set[List[Sequence[Class1]]]), id="different-nested-sequences-of-same-class"
-        ),
-        pytest.param(
-            (List[Set[Sequence[Any]]]),
-            (List[Set[Sequence[bool]]]),
-            id="nested-list-of-Any-to-nested-list-of-primitives",
-        ),
-        pytest.param(
-            (List[Set[Sequence[Any]]]), (List[Set[Sequence[Class2]]]), id="nested-list-of-Any-to-nested-list-of-classes"
-        ),
-        pytest.param((Dict[(str, int)]), (Dict[(int, int)]), id="different-dict-of-primitive-keys"),
-        pytest.param((Dict[(str, int)]), (Dict[(str, bool)]), id="different-dict-of-primitive-values"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(str, Class2)]), id="different-dict-of-class-values"),
-        pytest.param((Dict[(str, Class1)]), (Dict[(str, Class3)]), id="different-dict-of-class-to-subclass-values"),
-        pytest.param((Dict[(Any, int)]), (Dict[(int, int)]), id="dict-of-Any-keys-to-dict-of-primitives"),
-        pytest.param((Dict[(str, Any)]), (Dict[(int, int)]), id="dict-of-Any-values-to-dict-of-primitives"),
-        pytest.param((Dict[(str, Any)]), (Dict[(int, Class1)]), id="dict-of-Any-values-to-dict-of-classes"),
-        pytest.param((Dict[(Any, Any)]), (Dict[(int, int)]), id="dict-of-Any-keys-and-values-to-dict-of-primitives"),
-        pytest.param((Dict[(Any, Any)]), (Dict[(int, Class1)]), id="dict-of-Any-keys-and-values-to-dict-of-classes"),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Mapping[(str, Dict[(str, Dict[(str, int)])])]),
-            id="different-nested-mappings-of-same-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(int, int)])])]),
-            id="same-nested-mappings-of-different-primitive-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(int, Dict[(str, int)])])]),
-            id="same-nested-mappings-of-different-higer-level-primitive-keys",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, bool)])])]),
-            id="same-nested-mappings-of-different-primitive-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class2)])])]),
-            id="same-nested-mappings-of-different-class-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class2)])])]),
-            id="same-nested-mappings-of-class-to-subclass-values",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(Any, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            id="nested-mapping-of-Any-keys-to-nested-mapping-of-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(Any, Dict[(Any, int)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            id="nested-mapping-of-higher-level-Any-keys-to-nested-mapping-of-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            id="nested-mapping-of-Any-values-to-nested-mapping-of-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(str, Any)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            id="nested-mapping-of-Any-values-to-nested-mapping-of-classes",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(Any, Any)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, int)])])]),
-            id="nested-mapping-of-Any-keys-and-values-to-nested-mapping-of-primitives",
-        ),
-        pytest.param(
-            (Dict[(str, Mapping[(str, Dict[(Any, Any)])])]),
-            (Dict[(str, Mapping[(str, Dict[(str, Class1)])])]),
-            id="nested-mapping-of-Any-keys-and-values-to-nested-mapping-of-classes",
-        ),
-        pytest.param((Literal["a", "b", "c"]), (Literal["x", "y"]), id="different-literal-of-same-primitive"),
-        pytest.param((Literal["a", "b", "c"]), (Literal["a", "b"]), id="subset-literal"),
-        pytest.param((Literal[Enum1.TEST1]), (Literal[Enum1.TEST2]), id="different-literal-of-same-enum"),
-        pytest.param(
-            (Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])]),
-            (Tuple[(Literal["a", "b", "c"], Union[(Path, Dict[(int, Class1)])])]),
-            id="deeply-nested-complex-type-is-compatible-but-cannot-be-checked",
-        ),
     ],
 )
-def test_types_are_not_compatible(sender_type, receiver_type):
+def test_list_of_primitive_to_list(sender_type, receiver_type):
     assert not _types_are_compatible(sender_type, receiver_type)
diff --git a/test/evaluation/test_eval_run_result.py b/test/evaluation/test_eval_run_result.py
index 49d54b4bae..d59ef9b639 100644
--- a/test/evaluation/test_eval_run_result.py
+++ b/test/evaluation/test_eval_run_result.py
@@ -1,6 +1,8 @@
 # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
 #
 # SPDX-License-Identifier: Apache-2.0
+import json
+
 from haystack.evaluation import EvaluationRunResult
 import pytest
 
@@ -87,16 +89,24 @@ def test_score_report():
     }
 
     result = EvaluationRunResult("testing_pipeline_1", inputs=data["inputs"], results=data["metrics"])
-    report = result.score_report().to_json()
+    report = result.aggregated_report(output_format="json")
 
     assert report == (
-        '{"metrics":{"0":"reciprocal_rank","1":"single_hit","2":"multi_hit","3":"context_relevance",'
-        '"4":"faithfulness","5":"semantic_answer_similarity"},'
-        '"score":{"0":0.476932,"1":0.75,"2":0.46428375,"3":0.58177975,"4":0.40585375,"5":0.53757075}}'
+        {
+            "metrics": [
+                "reciprocal_rank",
+                "single_hit",
+                "multi_hit",
+                "context_relevance",
+                "faithfulness",
+                "semantic_answer_similarity",
+            ],
+            "score": [0.476932, 0.75, 0.46428375, 0.58177975, 0.40585375, 0.53757075],
+        }
     )
 
 
-def test_to_pandas():
+def test_to_df():
     data = {
         "inputs": {
             "query_id": ["53c3b3e6", "225f87f7", "53c3b3e6", "225f87f7"],
@@ -121,19 +131,25 @@ def test_to_pandas():
     }
 
     result = EvaluationRunResult("testing_pipeline_1", inputs=data["inputs"], results=data["metrics"])
-    assert result.to_pandas().to_json() == (
-        '{"query_id":{"0":"53c3b3e6","1":"225f87f7","2":"53c3b3e6","3":"225f87f7"},'
-        '"question":{"0":"What is the capital of France?","1":"What is the capital of Spain?",'
-        '"2":"What is the capital of Luxembourg?","3":"What is the capital of Portugal?"},'
-        '"contexts":{"0":"wiki_France","1":"wiki_Spain","2":"wiki_Luxembourg","3":"wiki_Portugal"},'
-        '"answer":{"0":"Paris","1":"Madrid","2":"Luxembourg","3":"Lisbon"},'
-        '"predicted_answer":{"0":"Paris","1":"Madrid","2":"Luxembourg","3":"Lisbon"},'
-        '"reciprocal_rank":{"0":0.378064,"1":0.534964,"2":0.216058,"3":0.778642},'
-        '"single_hit":{"0":1,"1":1,"2":0,"3":1},'
-        '"multi_hit":{"0":0.706125,"1":0.454976,"2":0.445512,"3":0.250522},'
-        '"context_relevance":{"0":0.805466,"1":0.410251,"2":0.75007,"3":0.361332},'
-        '"faithfulness":{"0":0.135581,"1":0.695974,"2":0.749861,"3":0.041999},'
-        '"semantic_answer_similarity":{"0":0.971241,"1":0.15932,"2":0.019722,"3":1.0}}'
+    assert result.detailed_report() == (
+        {
+            "query_id": ["53c3b3e6", "225f87f7", "53c3b3e6", "225f87f7"],
+            "question": [
+                "What is the capital of France?",
+                "What is the capital of Spain?",
+                "What is the capital of Luxembourg?",
+                "What is the capital of Portugal?",
+            ],
+            "contexts": ["wiki_France", "wiki_Spain", "wiki_Luxembourg", "wiki_Portugal"],
+            "answer": ["Paris", "Madrid", "Luxembourg", "Lisbon"],
+            "predicted_answer": ["Paris", "Madrid", "Luxembourg", "Lisbon"],
+            "reciprocal_rank": [0.378064, 0.534964, 0.216058, 0.778642],
+            "single_hit": [1, 1, 0, 1],
+            "multi_hit": [0.706125, 0.454976, 0.445512, 0.250522],
+            "context_relevance": [0.805466, 0.410251, 0.75007, 0.361332],
+            "faithfulness": [0.135581, 0.695974, 0.749861, 0.041999],
+            "semantic_answer_similarity": [0.971241, 0.15932, 0.019722, 1.0],
+        }
     )
 
 
@@ -176,73 +192,9 @@ def test_comparative_individual_scores_report():
 
     result1 = EvaluationRunResult("testing_pipeline_1", inputs=data_1["inputs"], results=data_1["metrics"])
     result2 = EvaluationRunResult("testing_pipeline_2", inputs=data_2["inputs"], results=data_2["metrics"])
-    results = result1.comparative_individual_scores_report(result2)
-
-    expected = {
-        "query_id": {0: "53c3b3e6", 1: "225f87f7"},
-        "question": {0: "What is the capital of France?", 1: "What is the capital of Spain?"},
-        "contexts": {0: "wiki_France", 1: "wiki_Spain"},
-        "answer": {0: "Paris", 1: "Madrid"},
-        "predicted_answer": {0: "Paris", 1: "Madrid"},
-        "testing_pipeline_1_reciprocal_rank": {0: 0.378064, 1: 0.534964},
-        "testing_pipeline_1_single_hit": {0: 1, 1: 1},
-        "testing_pipeline_1_multi_hit": {0: 0.706125, 1: 0.454976},
-        "testing_pipeline_1_context_relevance": {0: 1, 1: 1},
-        "testing_pipeline_1_faithfulness": {0: 0.135581, 1: 0.695974},
-        "testing_pipeline_1_semantic_answer_similarity": {0: 0.971241, 1: 0.15932},
-        "testing_pipeline_2_reciprocal_rank": {0: 0.378064, 1: 0.534964},
-        "testing_pipeline_2_single_hit": {0: 1, 1: 1},
-        "testing_pipeline_2_multi_hit": {0: 0.706125, 1: 0.454976},
-        "testing_pipeline_2_context_relevance": {0: 1, 1: 1},
-        "testing_pipeline_2_faithfulness": {0: 0.135581, 1: 0.695974},
-        "testing_pipeline_2_semantic_answer_similarity": {0: 0.971241, 1: 0.15932},
-    }
-
-    assert expected == results.to_dict()
-
-
-def test_comparative_individual_scores_report_keep_truth_answer_in_df():
-    data_1 = {
-        "inputs": {
-            "query_id": ["53c3b3e6", "225f87f7"],
-            "question": ["What is the capital of France?", "What is the capital of Spain?"],
-            "contexts": ["wiki_France", "wiki_Spain"],
-            "answer": ["Paris", "Madrid"],
-            "predicted_answer": ["Paris", "Madrid"],
-        },
-        "metrics": {
-            "reciprocal_rank": {"individual_scores": [0.378064, 0.534964], "score": 0.476932},
-            "single_hit": {"individual_scores": [1, 1], "score": 0.75},
-            "multi_hit": {"individual_scores": [0.706125, 0.454976], "score": 0.46428375},
-            "context_relevance": {"individual_scores": [1, 1], "score": 1},
-            "faithfulness": {"individual_scores": [0.135581, 0.695974], "score": 0.40585375},
-            "semantic_answer_similarity": {"individual_scores": [0.971241, 0.159320], "score": 0.53757075},
-        },
-    }
-
-    data_2 = {
-        "inputs": {
-            "query_id": ["53c3b3e6", "225f87f7"],
-            "question": ["What is the capital of France?", "What is the capital of Spain?"],
-            "contexts": ["wiki_France", "wiki_Spain"],
-            "answer": ["Paris", "Madrid"],
-            "predicted_answer": ["Paris", "Madrid"],
-        },
-        "metrics": {
-            "reciprocal_rank": {"individual_scores": [0.378064, 0.534964], "score": 0.476932},
-            "single_hit": {"individual_scores": [1, 1], "score": 0.75},
-            "multi_hit": {"individual_scores": [0.706125, 0.454976], "score": 0.46428375},
-            "context_relevance": {"individual_scores": [1, 1], "score": 1},
-            "faithfulness": {"individual_scores": [0.135581, 0.695974], "score": 0.40585375},
-            "semantic_answer_similarity": {"individual_scores": [0.971241, 0.159320], "score": 0.53757075},
-        },
-    }
-
-    result1 = EvaluationRunResult("testing_pipeline_1", inputs=data_1["inputs"], results=data_1["metrics"])
-    result2 = EvaluationRunResult("testing_pipeline_2", inputs=data_2["inputs"], results=data_2["metrics"])
-    results = result1.comparative_individual_scores_report(result2, keep_columns=["predicted_answer"])
+    results = result1.comparative_detailed_report(result2, keep_columns=["predicted_answer"])
 
-    assert list(results.columns) == [
+    assert list(results.keys()) == [
         "query_id",
         "question",
         "contexts",
diff --git a/test/tools/test_component_tool.py b/test/tools/test_component_tool.py
index 38f4e20464..4c0033e045 100644
--- a/test/tools/test_component_tool.py
+++ b/test/tools/test_component_tool.py
@@ -142,6 +142,12 @@ def test_from_component_basic(self):
         assert "reply" in result
         assert result["reply"] == "Hello, world!"
 
+    def test_from_component_long_description(self):
+        component = SimpleComponent()
+        tool = ComponentTool(component=component, description="".join(["A"] * 1024))
+
+        assert len(tool.description) == 1024
+
     def test_from_component_with_dataclass(self):
         component = UserGreeter()
 
diff --git a/test/utils/test_type_serialization.py b/test/utils/test_type_serialization.py
index 515ac50580..51ab6d015a 100644
--- a/test/utils/test_type_serialization.py
+++ b/test/utils/test_type_serialization.py
@@ -1,10 +1,9 @@
 # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
 #
 # SPDX-License-Identifier: Apache-2.0
-import copy
 import sys
 import typing
-from typing import List, Dict
+from typing import Any, List, Dict, Set, Tuple
 
 import pytest
 
@@ -14,16 +13,59 @@
 
 def test_output_type_serialization():
     assert serialize_type(str) == "str"
+    assert serialize_type(int) == "int"
+    assert serialize_type(dict) == "dict"
+    assert serialize_type(float) == "float"
+    assert serialize_type(bool) == "bool"
+
+
+def test_output_type_serialization_typing():
+    assert serialize_type(Any) == "typing.Any"
+    assert serialize_type(List) == "typing.List"
+    assert serialize_type(Dict) == "typing.Dict"
+    assert serialize_type(Set) == "typing.Set"
+    assert serialize_type(Tuple) == "typing.Tuple"
+
+
+def test_output_type_serialization_typing_and_type():
+    # List
+    assert serialize_type(List[str]) == "typing.List[str]"
     assert serialize_type(List[int]) == "typing.List[int]"
+    assert serialize_type(List[dict]) == "typing.List[dict]"
+    assert serialize_type(List[float]) == "typing.List[float]"
+    assert serialize_type(List[bool]) == "typing.List[bool]"
+    # Dict
+    assert serialize_type(Dict[str, str]) == "typing.Dict[str, str]"
+    assert serialize_type(Dict[int, int]) == "typing.Dict[int, int]"
+    assert serialize_type(Dict[dict, dict]) == "typing.Dict[dict, dict]"
+    assert serialize_type(Dict[float, float]) == "typing.Dict[float, float]"
+    assert serialize_type(Dict[bool, bool]) == "typing.Dict[bool, bool]"
+    # Set
+    assert serialize_type(Set[str]) == "typing.Set[str]"
+    assert serialize_type(Set[int]) == "typing.Set[int]"
+    assert serialize_type(Set[dict]) == "typing.Set[dict]"
+    assert serialize_type(Set[float]) == "typing.Set[float]"
+    assert serialize_type(Set[bool]) == "typing.Set[bool]"
+    # Tuple
+    assert serialize_type(Tuple[str]) == "typing.Tuple[str]"
+    assert serialize_type(Tuple[int]) == "typing.Tuple[int]"
+    assert serialize_type(Tuple[dict]) == "typing.Tuple[dict]"
+    assert serialize_type(Tuple[float]) == "typing.Tuple[float]"
+    assert serialize_type(Tuple[bool]) == "typing.Tuple[bool]"
+
+
+def test_output_type_serialization_nested():
     assert serialize_type(List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]"
-    assert serialize_type(ChatMessage) == "haystack.dataclasses.chat_message.ChatMessage"
     assert serialize_type(typing.List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]"
+
+
+def test_output_type_serialization_haystack_dataclasses():
+    assert serialize_type(ChatMessage) == "haystack.dataclasses.chat_message.ChatMessage"
     assert serialize_type(List[ChatMessage]) == "typing.List[haystack.dataclasses.chat_message.ChatMessage]"
     assert (
         serialize_type(typing.Dict[int, ChatMessage])
         == "typing.Dict[int, haystack.dataclasses.chat_message.ChatMessage]"
     )
-    assert serialize_type(int) == "int"
     assert serialize_type(ChatMessage.from_user("ciao")) == "haystack.dataclasses.chat_message.ChatMessage"
 
 
@@ -41,18 +83,35 @@ def test_output_type_serialization_pep585():
 
 def test_output_type_deserialization():
     assert deserialize_type("str") == str
+    assert deserialize_type("int") == int
+    assert deserialize_type("dict") == dict
+    assert deserialize_type("float") == float
+    assert deserialize_type("bool") == bool
+
+
+def test_output_type_deserialization_typing():
+    assert deserialize_type("typing.Any") == Any
+    assert deserialize_type("typing.List") == List
+    assert deserialize_type("typing.Dict") == Dict
+    assert deserialize_type("typing.Set") == Set
+    assert deserialize_type("typing.Tuple") == Tuple
+
+
+def test_output_type_deserialization_typing_and_type():
     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.List[typing.Dict[str, int]]") == typing.List[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]]]
+
+
+def test_output_type_deserialization_haystack_dataclasses():
     assert deserialize_type("typing.List[haystack.dataclasses.chat_message.ChatMessage]") == typing.List[ChatMessage]
     assert (
         deserialize_type("typing.Dict[int, haystack.dataclasses.chat_message.ChatMessage]")
         == typing.Dict[int, ChatMessage]
     )
     assert deserialize_type("haystack.dataclasses.chat_message.ChatMessage") == ChatMessage
-    assert deserialize_type("int") == int
 
 
 def test_output_type_deserialization_pep585():