|
11 | 11 |
|
12 | 12 | from haystack import component, default_from_dict, default_to_dict |
13 | 13 | from haystack.components.generators.chat.fallback import FallbackChatGenerator |
| 14 | +from haystack.core.errors import SerializationError |
14 | 15 | from haystack.dataclasses import ChatMessage, StreamingCallbackT |
15 | 16 | from haystack.tools import ToolsType |
16 | 17 |
|
@@ -441,3 +442,61 @@ def test_warm_up_mixed_generators(): |
441 | 442 | # Verify the fallback still works correctly |
442 | 443 | result = fallback.run([ChatMessage.from_user("test")]) |
443 | 444 | 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