diff --git a/haystack/components/generators/chat/fallback.py b/haystack/components/generators/chat/fallback.py index 091c4aa5ab9..01968f5d8be 100644 --- a/haystack/components/generators/chat/fallback.py +++ b/haystack/components/generators/chat/fallback.py @@ -60,6 +60,7 @@ def __init__(self, chat_generators: list[ChatGenerator]) -> None: raise ValueError(msg) self.chat_generators = list(chat_generators) + self._warmed_up = False def to_dict(self) -> dict[str, Any]: """Serialize the component, including nested chat generators.""" @@ -87,18 +88,22 @@ def from_dict(cls, data: dict[str, Any]) -> FallbackChatGenerator: return default_from_dict(cls, data) def warm_up(self) -> None: - """Warm up all underlying chat generators.""" - for gen in self.chat_generators: - if hasattr(gen, "warm_up"): - gen.warm_up() + """Warm up all underlying chat generators (called at most once).""" + if not self._warmed_up: + for gen in self.chat_generators: + if hasattr(gen, "warm_up"): + gen.warm_up() + self._warmed_up = True async def warm_up_async(self) -> None: - """Warm up all underlying chat generators on the serving event loop.""" - for gen in self.chat_generators: - if hasattr(gen, "warm_up_async"): - await gen.warm_up_async() - elif hasattr(gen, "warm_up"): - gen.warm_up() + """Warm up all underlying chat generators on the serving event loop (called at most once).""" + if not self._warmed_up: + for gen in self.chat_generators: + if hasattr(gen, "warm_up_async"): + await gen.warm_up_async() + elif hasattr(gen, "warm_up"): + gen.warm_up() + self._warmed_up = True def close(self) -> None: """Release the underlying chat generators' resources.""" diff --git a/releasenotes/notes/fix-fallback-warm-up-lifecycle-da9d4b4af6a25872.yaml b/releasenotes/notes/fix-fallback-warm-up-lifecycle-da9d4b4af6a25872.yaml new file mode 100644 index 00000000000..60929ac81a9 --- /dev/null +++ b/releasenotes/notes/fix-fallback-warm-up-lifecycle-da9d4b4af6a25872.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + ``FallbackChatGenerator`` now follows the same ``warm_up`` lifecycle semantics as + every other Haystack component. Previously, ``warm_up()`` was called on every + ``run()`` and ``run_async()`` invocation, meaning wrapped generators could be + initialized multiple times. This is now guarded by a ``_warmed_up`` flag so that + each underlying generator is warmed up at most once, regardless of how many times + the component is invoked. diff --git a/test/components/generators/chat/test_fallback.py b/test/components/generators/chat/test_fallback.py index c654e209221..e12897bd2b3 100644 --- a/test/components/generators/chat/test_fallback.py +++ b/test/components/generators/chat/test_fallback.py @@ -385,6 +385,19 @@ def test_warm_up_delegates_to_every_generator(self): for gen in gens: gen.warm_up.assert_called_once() + def test_warm_up_only_called_once_across_multiple_run_calls(self): + """warm_up() must not be re-invoked on every run(); generators are initialized at most once.""" + gens = [Mock(spec=["run", "warm_up"]) for _ in range(2)] + for gen in gens: + gen.run.return_value = {"replies": [ChatMessage.from_assistant("ok")], "meta": {}} + fallback = FallbackChatGenerator(chat_generators=gens) + # Call run() three times + for _ in range(3): + fallback.run([ChatMessage.from_user("hi")]) + # warm_up must have been called exactly once despite three run() calls + for gen in gens: + gen.warm_up.assert_called_once() + async def test_warm_up_async_delegates_to_every_generator(self): gens = [Mock(spec=["run", "warm_up_async"]) for _ in range(3)] for gen in gens: @@ -394,6 +407,18 @@ async def test_warm_up_async_delegates_to_every_generator(self): for gen in gens: gen.warm_up_async.assert_awaited_once() + async def test_warm_up_async_only_called_once_across_multiple_run_async_calls(self): + """warm_up_async() must not be re-invoked on every run_async(); generators are initialized at most once.""" + gens = [Mock(spec=["run_async", "warm_up_async"]) for _ in range(2)] + for gen in gens: + gen.warm_up_async = AsyncMock() + gen.run_async = AsyncMock(return_value={"replies": [ChatMessage.from_assistant("ok")], "meta": {}}) + fallback = FallbackChatGenerator(chat_generators=gens) + for _ in range(3): + await fallback.run_async([ChatMessage.from_user("hi")]) + for gen in gens: + gen.warm_up_async.assert_awaited_once() + async def test_warm_up_async_falls_back_to_sync_warm_up(self): gens = [Mock(spec=["run", "warm_up"]) for _ in range(3)] fallback = FallbackChatGenerator(chat_generators=gens)