Skip to content

Commit fe6c923

Browse files
fix: serialize nested chat generators via component_to_dict in FallbackChatGenerator (#11847)
1 parent f787b80 commit fe6c923

3 files changed

Lines changed: 70 additions & 2 deletions

File tree

haystack/components/generators/chat/fallback.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from haystack import component, default_from_dict, default_to_dict, logging
1111
from haystack.components.generators.chat.types import ChatGenerator
1212
from haystack.components.generators.utils import _normalize_messages
13+
from haystack.core.serialization import component_to_dict
1314
from haystack.dataclasses import ChatMessage, StreamingCallbackT
1415
from haystack.tools import ToolsType
1516
from haystack.utils.deserialization import deserialize_component_inplace
@@ -62,9 +63,12 @@ def __init__(self, chat_generators: list[ChatGenerator]) -> None:
6263
self._is_warmed_up = False
6364

6465
def to_dict(self) -> dict[str, Any]:
65-
"""Serialize the component, including nested chat generators when they support serialization."""
66+
"""Serialize the component, including nested chat generators."""
6667
return default_to_dict(
67-
self, chat_generators=[gen.to_dict() for gen in self.chat_generators if hasattr(gen, "to_dict")]
68+
self,
69+
chat_generators=[
70+
component_to_dict(gen, name=f"chat_generator_{idx}") for idx, gen in enumerate(self.chat_generators)
71+
],
6872
)
6973

7074
@classmethod
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed serialization of ``FallbackChatGenerator`` to use the standard ``component_to_dict`` serialization path. This ensures custom chat generator components without an explicit ``to_dict`` method are no longer silently omitted from the serialized ``chat_generators`` list.
5+

test/components/generators/chat/test_fallback.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from haystack import component, default_from_dict, default_to_dict
1313
from haystack.components.generators.chat.fallback import FallbackChatGenerator
14+
from haystack.core.errors import SerializationError
1415
from haystack.dataclasses import ChatMessage, StreamingCallbackT
1516
from haystack.tools import ToolsType
1617

@@ -441,3 +442,61 @@ def test_warm_up_mixed_generators():
441442
# Verify the fallback still works correctly
442443
result = fallback.run([ChatMessage.from_user("test")])
443444
assert result["replies"][0].text == "A"
445+
446+
447+
@component
448+
class CustomGeneratorWithoutSerDe:
449+
def __init__(self, text: str = "custom_ok"):
450+
self.text = text
451+
452+
def run(
453+
self,
454+
messages: list[ChatMessage],
455+
generation_kwargs: dict[str, Any] | None = None,
456+
tools: ToolsType | None = None,
457+
streaming_callback: StreamingCallbackT | None = None,
458+
) -> dict[str, Any]:
459+
return {"replies": [ChatMessage.from_assistant(self.text)], "meta": {}}
460+
461+
462+
@component
463+
class NonSerializableGenerator:
464+
def __init__(self, non_serializable_arg: Any):
465+
self.non_serializable_arg = non_serializable_arg
466+
467+
def run(self, messages: list[ChatMessage]) -> dict[str, Any]:
468+
return {"replies": []}
469+
470+
471+
def test_serialization_with_custom_generators_without_to_dict():
472+
# 1. Test mixed chain serialization, order preservation, execution, and round-trip
473+
gen0 = _DummySuccessGen(text="dummy_has_dict")
474+
gen1 = CustomGeneratorWithoutSerDe(text="custom_no_dict_1")
475+
gen2 = CustomGeneratorWithoutSerDe(text="custom_no_dict_2")
476+
477+
original = FallbackChatGenerator(chat_generators=[gen0, gen1, gen2])
478+
data = original.to_dict()
479+
480+
# Ensure all three components are serialized and not silently omitted
481+
assert len(data["init_parameters"]["chat_generators"]) == 3
482+
483+
# Reconstruct/Deserialize
484+
restored = FallbackChatGenerator.from_dict(data)
485+
assert isinstance(restored, FallbackChatGenerator)
486+
assert len(restored.chat_generators) == 3
487+
488+
# Assert fallback order is exactly preserved
489+
assert restored.chat_generators[0].text == "dummy_has_dict"
490+
assert restored.chat_generators[1].text == "custom_no_dict_1"
491+
assert restored.chat_generators[2].text == "custom_no_dict_2"
492+
assert isinstance(restored.chat_generators[1], CustomGeneratorWithoutSerDe)
493+
assert isinstance(restored.chat_generators[2], CustomGeneratorWithoutSerDe)
494+
495+
# Verify pipeline execution on the restored instance
496+
res = restored.run([ChatMessage.from_user("hi")])
497+
assert res["replies"][0].text == "dummy_has_dict"
498+
499+
# 2. Test failure path (fail loud) when a component is not serializable
500+
non_serializable_fallback = FallbackChatGenerator(chat_generators=[NonSerializableGenerator(object())])
501+
with pytest.raises(SerializationError, match="unsupported value of type"):
502+
non_serializable_fallback.to_dict()

0 commit comments

Comments
 (0)