Repository: deepset-ai/haystack

Issue:
Auto-connect between pipeline components intermittently failing
**Describe the bug**
The connections between components when using the `.connect()` method of a pipeline are non-deterministic. 

We did find that *explicitly* defining the connection (as showcased in the documentation found [here](https://docs.haystack.deepset.ai/v2.0/docs/transformerssimilarityranker)) worked to resolve this issue, however it seems like it might be possible to ensure that if there *are* default connections that share a name, they be preferentially connected during graph construction.

Hopefully all this makes sense - if not, I'm happy to follow-up!

**Error message**
There is no specific error.

**Expected behavior**
The logic should not connect two components by incompatible IDs or should not default to a random selection/decision.

**Additional context**
Creating connections like this will lead to a semi-random incidence of the pipeline graph being constructed incorrectly:

```python
pipeline.connect("tgi_fetcher", "tgi_converter")
pipeline.connect("tgi_converter", "tgi_splitter")
pipeline.connect("tgi_splitter", "tgi_ranker")
pipeline.connect("tgi_ranker", "tgi_prompt_builder")
pipeline.connect("tgi_prompt_builder", "tgi_llm")
```

![image](https://i.imgur.com/zUBwJaB.png)

As you can hopefully see, the output of `ranker` (documents) is being connected to the `question` input of the `prompt_builder`. This is despite the fact that the `prompt_builder` has a `documents` input. 

You can get around the error by keying `documents` in your input - and swapping any potential prompt-templates, but this seems like undesirable behaviour.

```python
pipeline.connect("tgi_fetcher", "tgi_converter")
pipeline.connect("tgi_converter", "tgi_splitter")
pipeline.connect("tgi_splitter", "tgi_ranker")
pipeline.connect("tgi_ranker.documents", "tgi_prompt_builder.documents")
pipeline.connect("tgi_prompt_builder", "tgi_llm")
```

**To Reproduce**

```python
prompt_template = """
According to these documents:

{% for doc in documents %}
  {{ doc.content }}
{% endfor %}

Answer the given question: {{question}}
Answer:
"""
prompt_builder = PromptBuilder(template=prompt_template)
fetcher = LinkContentFetcher()
converter = HTMLToDocument()
splitter = DocumentSplitter(split_length=100, split_overlap=5)
llm = GPTGenerator(api_key = openai_api_key, model_name = "gpt-4")
ranker = TransformersSimilarityRanker()

pipeline = Pipeline()
pipeline.add_component(name="fetcher", instance=fetcher)
pipeline.add_component(name="converter", instance=converter)
pipeline.add_component(name="splitter", instance=splitter)
pipeline.add_component(name="ranker", instance=ranker)
pipeline.add_component(name="prompt_builder", instance=prompt_builder)
pipeline.add_component(name="llm", instance=llm)

pipeline.connect("fetcher", "converter")
pipeline.connect("converter", "splitter")
pipeline.connect("splitter", "ranker")
pipeline.connect("ranker", "prompt_builder")
pipeline.connect("prompt_builder", "llm")

question = "What is our favorite animal?"
result = pipeline.run({"prompt_builder": {"question": question},
                       "ranker": {"query": question},
                      "fetcher": {"urls": ["https://haystack.deepset.ai/advent-of-haystack/day-1#challenge"]}})

print(result['llm']['replies'][0])
```

**FAQ Check**
- [x ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS: Linux
 - GPU/CPU: CPU
 - Haystack version (commit or version number): 2.0.0b2
 - DocumentStore: N/A
 - Reader: N/A
 - Retriever: N/A



Discussion:

Resolving patch:
diff --git a/haystack/core/component/connection.py b/haystack/core/component/connection.py
--- a/haystack/core/component/connection.py
+++ b/haystack/core/component/connection.py
@@ -116,21 +116,24 @@ def from_list_of_sockets(
             name_matches = [
                 (out_sock, in_sock) for out_sock, in_sock in possible_connections if in_sock.name == out_sock.name
             ]
-            if len(name_matches) != 1:
-                # TODO allow for multiple connections at once if there is no ambiguity?
-                # TODO give priority to sockets that have no default values?
-                connections_status_str = _connections_status(
-                    sender_node=sender_node,
-                    sender_sockets=sender_sockets,
-                    receiver_node=receiver_node,
-                    receiver_sockets=receiver_sockets,
-                )
-                raise PipelineConnectError(
-                    f"Cannot connect '{sender_node}' with '{receiver_node}': more than one connection is possible "
-                    "between these components. Please specify the connection name, like: "
-                    f"pipeline.connect('{sender_node}.{possible_connections[0][0].name}', "
-                    f"'{receiver_node}.{possible_connections[0][1].name}').\n{connections_status_str}"
-                )
+            if len(name_matches) == 1:
+                # Sockets match by type and name, let's use this
+                return Connection(sender_node, name_matches[0][0], receiver_node, name_matches[0][1])
+
+            # TODO allow for multiple connections at once if there is no ambiguity?
+            # TODO give priority to sockets that have no default values?
+            connections_status_str = _connections_status(
+                sender_node=sender_node,
+                sender_sockets=sender_sockets,
+                receiver_node=receiver_node,
+                receiver_sockets=receiver_sockets,
+            )
+            raise PipelineConnectError(
+                f"Cannot connect '{sender_node}' with '{receiver_node}': more than one connection is possible "
+                "between these components. Please specify the connection name, like: "
+                f"pipeline.connect('{sender_node}.{possible_connections[0][0].name}', "
+                f"'{receiver_node}.{possible_connections[0][1].name}').\n{connections_status_str}"
+            )
 
         match = possible_connections[0]
         return Connection(sender_node, match[0], receiver_node, match[1])


Test patch:
diff --git a/test/core/pipeline/test_connections.py b/test/core/pipeline/test_connections.py
--- a/test/core/pipeline/test_connections.py
+++ b/test/core/pipeline/test_connections.py
@@ -386,3 +386,18 @@ def test_parse_connection():
     assert parse_connect_string("foobar") == ("foobar", None)
     assert parse_connect_string("foo.bar") == ("foo", "bar")
     assert parse_connect_string("foo.bar.baz") == ("foo", "bar.baz")
+
+
+def test_connect_with_same_socket_names():
+    SimpleComponent = factory.component_class("SimpleComponent", output_types={"documents": List})
+    ComponentWithMultipleInputs = factory.component_class(
+        "ComponentWithMultipleInputs", input_types={"question": Any, "documents": Any}
+    )
+
+    pipe = Pipeline()
+    pipe.add_component("simple", SimpleComponent())
+    pipe.add_component("multiple", ComponentWithMultipleInputs())
+
+    pipe.connect("simple", "multiple")
+
+    assert list(pipe.graph.edges) == [("simple", "multiple", "documents/documents")]