Repository: deepset-ai/haystack

Issue:
Pipeline - `include_outputs_from` parameter of `run` method has unexpected behavior
**To Reproduce**
```python
from haystack.components.fetchers import LinkContentFetcher
from haystack.components.generators import OpenAIGenerator
from haystack.components.builders import PromptBuilder
from haystack.components.websearch.serper_dev import SerperDevWebSearch

from haystack.utils import Secret
from haystack import Pipeline

from haystack import Document, component, logging
import os
from typing import List, Dict, Any, Union

os.environ["SERPERDEV_API_KEY"] = "..."


web_rag_template = """
Answer the question using the provided snippets.
Question: {{question}}

Snippets:
{% for doc in documents %}
- snippet: "{{doc.content}}"
{% endfor %}

chosen option:
"""

web_rag_pipeline = Pipeline()
web_rag_pipeline.add_component("websearch", SerperDevWebSearch(top_k=3))
web_rag_pipeline.add_component("prompt_builder", PromptBuilder(template=web_rag_template))
web_rag_pipeline.add_component("generator", OpenAIGenerator())
web_rag_pipeline.connect("websearch.documents", "prompt_builder.documents")
web_rag_pipeline.connect("prompt_builder", "generator")

result = web_rag_pipeline.run(data={"websearch": {"query": "What did OpenAI yesterday?"}},
                                      include_outputs_from=["websearch", "generator", "prompt_builder"])

print(result)
```

```
{'websearch': {'links': ['https://abcnews.go.com/Business/openai-announces-new-version-ai-language-model-fuels/story?id=110167092', 'https://www.businessinsider.com/openai-event-live-updates-sam-altman-announcement-chatgpt-news-2024-5', 'https://qz.com/openai-chat-gpt-updates-sam-altman-magic-1851472690']}, 
'generator': {'replies': ['OpenAI announced its new flagship model ChatGPT-4o on Monday, an AI chatbot that can see, hear, and have real-time conversations.'], 'meta': [{'model': 'gpt-3.5-turbo-0125', 'index': 0, 'finish_reason': 'stop', 'usage': {'completion_tokens': 32, 'prompt_tokens': 137, 'total_tokens': 169}}]}, 
'prompt_builder': {'prompt': '\nAnswer the question using the provided snippets.\nQuestion: \n\nSnippets:\n\n- snippet: "OpenAI announced a desktop version of its conversation bot ChatGPT on Monday as well as the latest iteration of the AI language model that fuels ..."\n\n- snippet: "But the big news on Monday was OpenAI\'s new flagship AI model, GPT-4o, which will be free to all users and "can reason across audio, vision, and ..."\n\n- snippet: "OpenAI on Monday announced its new flagship model ChatGPT-4o — an AI chatbot that can see, hear, and have real-time conversations."\n\n\nchosen option:'}}
```

`SerperDevWebSearch` component has 2 outputs: `links` and `documents`.
`documents`, connected to the `PromptBuilder` is not returned in the Pipeline output.

**Expected behavior**
I would expect `documents` to be returned in the Pipeline output.

**System:**
 - Haystack version (commit or version number): 2.1.1



Discussion:

Resolving patch:
diff --git a/haystack/core/pipeline/pipeline.py b/haystack/core/pipeline/pipeline.py
--- a/haystack/core/pipeline/pipeline.py
+++ b/haystack/core/pipeline/pipeline.py
@@ -23,7 +23,7 @@ class Pipeline(PipelineBase):
     """
 
     # TODO: We're ignoring these linting rules for the time being, after we properly optimize this function we'll remove the noqa
-    def run(  # noqa: C901, PLR0912, PLR0915 pylint: disable=too-many-branches
+    def run(  # noqa: C901, PLR0912, PLR0915 pylint: disable=too-many-branches,too-many-locals
         self, data: Dict[str, Any], debug: bool = False, include_outputs_from: Optional[Set[str]] = None
     ) -> Dict[str, Any]:
         """
@@ -431,7 +431,16 @@ def run(self, word: str):
 
             if len(include_outputs_from) > 0:
                 for name, output in extra_outputs.items():
-                    if name not in final_outputs:
+                    inner = final_outputs.get(name)
+                    if inner is None:
                         final_outputs[name] = output
+                    else:
+                        # Let's not override any keys that are already
+                        # in the final_outputs as they might be different
+                        # from what we cached in extra_outputs, e.g. when loops
+                        # are involved.
+                        for k, v in output.items():
+                            if k not in inner:
+                                inner[k] = v
 
             return final_outputs


Test patch:
diff --git a/test/core/pipeline/test_intermediate_outputs.py b/test/core/pipeline/test_intermediate_outputs.py
--- a/test/core/pipeline/test_intermediate_outputs.py
+++ b/test/core/pipeline/test_intermediate_outputs.py
@@ -5,11 +5,23 @@
 
 from haystack.components.others import Multiplexer
 from haystack.core.pipeline import Pipeline
+from haystack.core.component import component
 from haystack.testing.sample_components import Accumulate, AddFixedValue, Double, Threshold
 
 logging.basicConfig(level=logging.DEBUG)
 
 
+@component
+class DoubleWithOriginal:
+    """
+    Doubles the input value and returns the original value as well.
+    """
+
+    @component.output_types(value=int, original=int)
+    def run(self, value: int):
+        return {"value": value * 2, "original": value}
+
+
 def test_pipeline_intermediate_outputs():
     pipeline = Pipeline()
     pipeline.add_component("first_addition", AddFixedValue(add=2))
@@ -62,3 +74,24 @@ def test_pipeline_with_loops_intermediate_outputs():
         "below_5": {"above": 8},
         "add_three": {"result": 11},
     }
+
+
+def test_pipeline_intermediate_outputs_multiple_output_sockets():
+    pipeline = Pipeline()
+    pipeline.add_component("first_addition", AddFixedValue(add=2))
+    pipeline.add_component("second_addition", AddFixedValue())
+    pipeline.add_component("double", DoubleWithOriginal())
+    pipeline.connect("first_addition", "double")
+    pipeline.connect("double.value", "second_addition")
+
+    results = pipeline.run(
+        {"first_addition": {"value": 1}}, include_outputs_from={"first_addition", "second_addition", "double"}
+    )
+    assert results == {
+        "second_addition": {"result": 7},
+        "first_addition": {"result": 3},
+        "double": {"value": 6, "original": 3},
+    }
+
+    results = pipeline.run({"first_addition": {"value": 1}}, include_outputs_from={"double"})
+    assert results == {"second_addition": {"result": 7}, "double": {"value": 6, "original": 3}}