Coverage for haystack/components/joiners/branch.py: 85%
20 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from typing import Any
7from haystack import component, default_from_dict, default_to_dict
8from haystack.core.component.types import GreedyVariadic
9from haystack.utils import deserialize_type, serialize_type
12@component
13class BranchJoiner:
14 """
15 A component that merges multiple input branches of a pipeline into a single output stream.
17 `BranchJoiner` receives multiple inputs of the same data type and forwards the first received value
18 to its output. This is useful for scenarios where multiple branches need to converge before proceeding.
20 ### Common Use Cases:
21 - **Loop Handling:** `BranchJoiner` helps close loops in pipelines. For example, if a pipeline component validates
22 or modifies incoming data and produces an error-handling branch, `BranchJoiner` can merge both branches and send
23 (or resend in the case of a loop) the data to the component that evaluates errors. See "Usage example" below.
25 - **Decision-Based Merging:** `BranchJoiner` reconciles branches coming from Router components (such as
26 `ConditionalRouter`, `TextLanguageRouter`). Suppose a `TextLanguageRouter` directs user queries to different
27 Retrievers based on the detected language. Each Retriever processes its assigned query and passes the results
28 to `BranchJoiner`, which consolidates them into a single output before passing them to the next component, such
29 as a `PromptBuilder`.
31 ### Example Usage:
32 ```python
33 import json
35 from haystack import Pipeline
36 from haystack.components.generators.chat import OpenAIChatGenerator
37 from haystack.components.joiners import BranchJoiner
38 from haystack.components.validators import JsonSchemaValidator
39 from haystack.dataclasses import ChatMessage
41 # Define a schema for validation
42 person_schema = {
43 "type": "object",
44 "properties": {
45 "first_name": {"type": "string", "pattern": "^[A-Z][a-z]+$"},
46 "last_name": {"type": "string", "pattern": "^[A-Z][a-z]+$"},
47 "nationality": {"type": "string", "enum": ["Italian", "Portuguese", "American"]},
48 },
49 "required": ["first_name", "last_name", "nationality"]
50 }
52 # Initialize a pipeline
53 pipe = Pipeline()
55 # Add components to the pipeline
56 pipe.add_component("joiner", BranchJoiner(list[ChatMessage]))
57 pipe.add_component("generator", OpenAIChatGenerator(model="gpt-4.1-mini"))
58 pipe.add_component("validator", JsonSchemaValidator(json_schema=person_schema))
60 # And connect them
61 pipe.connect("joiner", "generator")
62 pipe.connect("generator.replies", "validator.messages")
63 pipe.connect("validator.validation_error", "joiner")
65 result = pipe.run(
66 data={
67 "generator": {"generation_kwargs": {"response_format": {"type": "json_object"}}},
68 "joiner": {"value": [ChatMessage.from_user("Create json from Peter Parker")]}}
69 )
71 print(json.loads(result["validator"]["validated"][0].text))
74 # >> {'first_name': 'Peter', 'last_name': 'Parker', 'nationality': 'American', 'name': 'Spider-Man', 'occupation':
75 # >> 'Superhero', 'age': 23, 'location': 'New York City'}
76 ```
78 Note that `BranchJoiner` can manage only one data type at a time. In this case, `BranchJoiner` is created for
79 passing `list[ChatMessage]`. This determines the type of data that `BranchJoiner` will receive from the upstream
80 connected components and also the type of data that `BranchJoiner` will send through its output.
82 In the code example, `BranchJoiner` receives a looped back `list[ChatMessage]` from the `JsonSchemaValidator` and
83 sends it down to the `OpenAIChatGenerator` for re-generation. We can have multiple loopback connections in the
84 pipeline. In this instance, the downstream component is only one (the `OpenAIChatGenerator`), but the pipeline could
85 have more than one downstream component.
86 """
88 def __init__(self, type_: type) -> None:
89 """
90 Creates a `BranchJoiner` component.
92 :param type_: The expected data type of inputs and outputs.
93 """
94 self.type_ = type_
95 component.set_input_types(self, value=GreedyVariadic[type_]) # type: ignore
96 component.set_output_types(self, value=type_)
98 def to_dict(self) -> dict[str, Any]:
99 """
100 Serializes the component into a dictionary.
102 :returns:
103 Dictionary with serialized data.
104 """
105 return default_to_dict(self, type_=serialize_type(self.type_))
107 @classmethod
108 def from_dict(cls, data: dict[str, Any]) -> "BranchJoiner":
109 """
110 Deserializes a `BranchJoiner` instance from a dictionary.
112 :param data: The dictionary containing serialized component data.
113 :returns:
114 A deserialized `BranchJoiner` instance.
115 """
116 data["init_parameters"]["type_"] = deserialize_type(data["init_parameters"]["type_"])
117 return default_from_dict(cls, data)
119 def run(self, **kwargs: Any) -> dict[str, Any]:
120 """
121 Executes the `BranchJoiner`, selecting the first available input value and passing it downstream.
123 :param **kwargs: The input data. Must be of the type declared by `type_` during initialization.
124 :returns:
125 A dictionary with a single key `value`, containing the first input received.
126 """
127 if (inputs_count := len(kwargs["value"])) != 1:
128 raise ValueError(f"BranchJoiner expects only one input, but {inputs_count} were received.")
129 return {"value": kwargs["value"][0]}