Repository: deepset-ai/haystack

Issue:
RAG pipeline e2e test fails to resolve api key
**Describe the bug**
`e2e/pipelines/test_rag_pipelines_e2e.py` fails resolving the OpenAI api key after the PR that changed the secret management. https://github.com/deepset-ai/haystack/pull/6887

Here is the failing test run: https://github.com/deepset-ai/haystack/actions/runs/7792596449/job/21250892136

**Error message**
```python
self.client = OpenAI(api_key=api_key.resolve_value(), organization=organization, base_url=api_base_url)
E       AttributeError: 'str' object has no attribute 'resolve_value'
```

Comments:

Patch:
diff --git a/haystack/components/extractors/named_entity_extractor.py b/haystack/components/extractors/named_entity_extractor.py
index 6ca988fa..f8d8d717 100644
--- a/haystack/components/extractors/named_entity_extractor.py
+++ b/haystack/components/extractors/named_entity_extractor.py
@@ -126,6 +126,9 @@ class NamedEntityExtractor:
             raise ComponentError(f"Unknown NER backend '{type(backend).__name__}' for extractor")
 
     def warm_up(self):
+        """
+        Initialize the named entity extractor backend.
+        """
         try:
             self._backend.initialize()
         except Exception as e:
@@ -135,6 +138,16 @@ class NamedEntityExtractor:
 
     @component.output_types(documents=List[Document])
     def run(self, documents: List[Document], batch_size: int = 1) -> Dict[str, Any]:
+        """
+         Run the named-entity extractor.
+
+        :param documents:
+             Documents to process.
+        :param batch_size:
+             Batch size used for processing the documents.
+        :returns:
+             The processed documents.
+        """
         texts = [doc.content if doc.content is not None else "" for doc in documents]
         annotations = self._backend.annotate(texts, batch_size=batch_size)
 
@@ -150,6 +163,9 @@ class NamedEntityExtractor:
         return {"documents": documents}
 
     def to_dict(self) -> Dict[str, Any]:
+        """
+        Serialize this component to a dictionary.
+        """
         return default_to_dict(
             self,
             backend=self._backend.type,
@@ -160,6 +176,12 @@ class NamedEntityExtractor:
 
     @classmethod
     def from_dict(cls, data: Dict[str, Any]) -> "NamedEntityExtractor":
+        """
+        Deserialize the component from a dictionary.
+
+        :param data:
+            The dictionary to deserialize from.
+        """
         try:
             init_params = data["init_parameters"]
             init_params["device"] = ComponentDevice.from_dict(init_params["device"])
diff --git a/haystack/core/component/component.py b/haystack/core/component/component.py
index 95ae87d7..c231e41d 100644
--- a/haystack/core/component/component.py
+++ b/haystack/core/component/component.py
@@ -160,6 +160,21 @@ class ComponentMeta(type):
         return instance
 
 
+def _component_repr(component: Component) -> str:
+    """
+    All Components override their __repr__ method with this one.
+    It prints the component name and the input/output sockets.
+    """
+    result = object.__repr__(component)
+    if pipeline := getattr(component, "__haystack_added_to_pipeline__"):
+        # This Component has been added in a Pipeline, let's get the name from there.
+        result += f"\n{pipeline.get_component_name(component)}"
+
+    # We're explicitly ignoring the type here because we're sure that the component
+    # has the __haystack_input__ and __haystack_output__ attributes at this point
+    return f"{result}\n{component.__haystack_input__}\n{component.__haystack_output__}"  # type: ignore[attr-defined]
+
+
 class _Component:
     """
     See module's docstring.
@@ -332,6 +347,9 @@ class _Component:
         self.registry[class_path] = class_
         logger.debug("Registered Component %s", class_)
 
+        # Override the __repr__ method with a default one
+        class_.__repr__ = _component_repr
+
         return class_
 
     def __call__(self, class_):
diff --git a/haystack/core/component/sockets.py b/haystack/core/component/sockets.py
index 25bf4fdc..374ae630 100644
--- a/haystack/core/component/sockets.py
+++ b/haystack/core/component/sockets.py
@@ -82,8 +82,9 @@ class Sockets:
             return pipeline.get_component_name(self._component)
 
         # This Component has not been added to a Pipeline yet, so we can't know its name.
-        # Let's use the class name instead.
-        return str(self._component)
+        # Let's use default __repr__. We don't call repr() directly as Components have a custom
+        # __repr__ method and that would lead to infinite recursion since we call Sockets.__repr__ in it.
+        return object.__repr__(self._component)
 
     def __getattribute__(self, name):
         try:
@@ -96,12 +97,10 @@ class Sockets:
         return object.__getattribute__(self, name)
 
     def __repr__(self) -> str:
-        result = self._component_name()
+        result = ""
         if self._sockets_io_type == InputSocket:
-            result += " inputs:\n"
+            result = "Inputs:\n"
         elif self._sockets_io_type == OutputSocket:
-            result += " outputs:\n"
+            result = "Outputs:\n"
 
-        result += "\n".join([f"  - {n}: {_type_name(s.type)}" for n, s in self._sockets_dict.items()])
-
-        return result
+        return result + "\n".join([f"  - {n}: {_type_name(s.type)}" for n, s in self._sockets_dict.items()])
diff --git a/releasenotes/notes/component-repr-a6486af81530bc3b.yaml b/releasenotes/notes/component-repr-a6486af81530bc3b.yaml
new file mode 100644
index 00000000..3a7439e9
--- /dev/null
+++ b/releasenotes/notes/component-repr-a6486af81530bc3b.yaml
@@ -0,0 +1,6 @@
+---
+enhancements:
+  - |
+    Add `__repr__` to all Components to print their I/O.
+    This can also be useful in Jupyter notebooks as this will be shown as a cell output
+    if the it's the last expression in a cell.


Test patch:
diff --git a/e2e/pipelines/test_rag_pipelines_e2e.py b/e2e/pipelines/test_rag_pipelines_e2e.py
index fa3aeb8c..d38053c9 100644
--- a/e2e/pipelines/test_rag_pipelines_e2e.py
+++ b/e2e/pipelines/test_rag_pipelines_e2e.py
@@ -30,7 +30,7 @@ def test_bm25_rag_pipeline(tmp_path):
     rag_pipeline = Pipeline()
     rag_pipeline.add_component(instance=InMemoryBM25Retriever(document_store=InMemoryDocumentStore()), name="retriever")
     rag_pipeline.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder")
-    rag_pipeline.add_component(instance=OpenAIGenerator(api_key=os.environ.get("OPENAI_API_KEY")), name="llm")
+    rag_pipeline.add_component(instance=OpenAIGenerator(), name="llm")
     rag_pipeline.add_component(instance=AnswerBuilder(), name="answer_builder")
     rag_pipeline.connect("retriever", "prompt_builder.documents")
     rag_pipeline.connect("prompt_builder", "llm")
@@ -101,7 +101,7 @@ def test_embedding_retrieval_rag_pipeline(tmp_path):
         instance=InMemoryEmbeddingRetriever(document_store=InMemoryDocumentStore()), name="retriever"
     )
     rag_pipeline.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder")
-    rag_pipeline.add_component(instance=OpenAIGenerator(api_key=os.environ.get("OPENAI_API_KEY")), name="llm")
+    rag_pipeline.add_component(instance=OpenAIGenerator(), name="llm")
     rag_pipeline.add_component(instance=AnswerBuilder(), name="answer_builder")
     rag_pipeline.connect("text_embedder", "retriever")
     rag_pipeline.connect("retriever", "prompt_builder.documents")
diff --git a/test/core/component/test_component.py b/test/core/component/test_component.py
index bbe2605f..b093c32b 100644
--- a/test/core/component/test_component.py
+++ b/test/core/component/test_component.py
@@ -4,6 +4,7 @@ import pytest
 
 from haystack.core.component import Component, InputSocket, OutputSocket, component
 from haystack.core.errors import ComponentError
+from haystack.core.pipeline import Pipeline
 
 
 def test_correct_declaration():
@@ -189,3 +190,31 @@ def test_keyword_only_args():
     comp = MockComponent()
     component_inputs = {name: {"type": socket.type} for name, socket in comp.__haystack_input__._sockets_dict.items()}
     assert component_inputs == {"arg": {"type": int}}
+
+
+def test_repr():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, value: int):
+            return {"value": value}
+
+    comp = MockComponent()
+    assert repr(comp) == f"{object.__repr__(comp)}\nInputs:\n  - value: int\nOutputs:\n  - value: int"
+
+
+def test_repr_added_to_pipeline():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, value: int):
+            return {"value": value}
+
+    pipe = Pipeline()
+    comp = MockComponent()
+    pipe.add_component("my_component", comp)
+    assert repr(comp) == f"{object.__repr__(comp)}\nmy_component\nInputs:\n  - value: int\nOutputs:\n  - value: int"
diff --git a/test/core/component/test_sockets.py b/test/core/component/test_sockets.py
index ac3b01bd..6e942b84 100644
--- a/test/core/component/test_sockets.py
+++ b/test/core/component/test_sockets.py
@@ -23,19 +23,6 @@ class TestSockets:
         assert io._component == comp
         assert io._sockets_dict == {}
 
-    def test_component_name(self):
-        comp = component_class("SomeComponent")()
-        io = Sockets(component=comp, sockets_dict={}, sockets_io_type=InputSocket)
-        assert io._component_name() == str(comp)
-
-    def test_component_name_added_to_pipeline(self):
-        comp = component_class("SomeComponent")()
-        pipeline = Pipeline()
-        pipeline.add_component("my_component", comp)
-
-        io = Sockets(component=comp, sockets_dict={}, sockets_io_type=InputSocket)
-        assert io._component_name() == "my_component"
-
     def test_getattribute(self):
         comp = component_class("SomeComponent", input_types={"input_1": int, "input_2": int})()
         io = Sockets(component=comp, sockets_dict=comp.__haystack_input__._sockets_dict, sockets_io_type=InputSocket)
@@ -54,4 +41,4 @@ class TestSockets:
         comp = component_class("SomeComponent", input_types={"input_1": int, "input_2": int})()
         io = Sockets(component=comp, sockets_dict=comp.__haystack_input__._sockets_dict, sockets_io_type=InputSocket)
         res = repr(io)
-        assert res == f"{comp} inputs:\n  - input_1: int\n  - input_2: int"
+        assert res == "Inputs:\n  - input_1: int\n  - input_2: int"