Skip to content

Commit 4686335

Browse files
authored
Fix appending None to a list in State (#10990)
1 parent f77b09e commit 4686335

3 files changed

Lines changed: 40 additions & 1 deletion

File tree

haystack/components/tools/tool_invoker.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,13 @@ def _merge_tool_outputs(tool: Tool, result: Any, state: State) -> None:
459459
for state_key, config in tool.outputs_to_state.items():
460460
# Get the source key from the output config, otherwise use the entire result
461461
source_key = config.get("source", None)
462+
463+
# If a source key is specified but absent from the result, skip this mapping to avoid passing a None value
464+
# to state. This can be a common scenario with PipelineTool wrapping a pipeline that has conditional
465+
# branches where not all outputs are always produced even if defined in outputs_to_state.
466+
if source_key and source_key not in result:
467+
continue
468+
462469
output_value = result.get(source_key) if source_key else result
463470

464471
# Merge other outputs into the state
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fix ``ToolInvoker._merge_tool_outputs`` silently appending ``None`` to list-typed state when a
5+
tool's ``outputs_to_state`` source key is absent from the tool result. This is a common scenario
6+
with ``PipelineTool`` wrapping a pipeline that has conditional branches where not all outputs are
7+
always produced even if defined in ``outputs_to_state``. The mapping is now skipped entirely when
8+
the source key is not present in the result dict.

test/components/tools/test_tool_invoker.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import pytest
1212

13-
from haystack import Pipeline
13+
from haystack import Document, Pipeline
1414
from haystack.components.agents.state import State
1515
from haystack.components.builders.prompt_builder import PromptBuilder
1616
from haystack.components.generators.chat.openai import OpenAIChatGenerator
@@ -1062,6 +1062,30 @@ def test_merge_tool_outputs_with_output_mapping_2(self):
10621062
)
10631063
assert state.data == {"all_weather_results": {"weather": "sunny", "temperature": 14, "unit": "celsius"}}
10641064

1065+
def test_merge_tool_outputs_source_key_absent_does_not_corrupt_list_state(self):
1066+
"""
1067+
Simulates a PipelineTool wrapping a pipeline with a conditional branch that may not execute, resulting in the
1068+
source key being absent from the tool result. The test verifies that in this case, the existing list in state
1069+
is not corrupted by appending None.
1070+
"""
1071+
tool = Tool(
1072+
name="retrieval",
1073+
description="mock",
1074+
parameters={"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
1075+
function=lambda query: {},
1076+
outputs_to_state={"documents": {"source": "documents_output"}},
1077+
)
1078+
invoker = ToolInvoker(tools=[tool])
1079+
existing_doc = Document(content="from first call")
1080+
state = State(schema={"documents": {"type": list[Document]}})
1081+
state.set("documents", [existing_doc])
1082+
1083+
# Tool result where the source key is absent (document extraction branch did not execute)
1084+
invoker._merge_tool_outputs(tool=tool, result={"result": "no web results found"}, state=state)
1085+
1086+
assert state.data["documents"] == [existing_doc]
1087+
assert None not in state.data["documents"]
1088+
10651089
def test_merge_tool_outputs_with_output_mapping_and_handler(self):
10661090
handler = lambda _, new: f"{new}" # noqa: E731
10671091
weather_tool = Tool(

0 commit comments

Comments
 (0)