Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions haystack/components/generators/chat/fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 62 to +63

def to_dict(self) -> dict[str, Any]:
"""Serialize the component, including nested chat generators."""
Expand Down Expand Up @@ -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
Comment on lines 98 to +106

def close(self) -> None:
"""Release the underlying chat generators' resources."""
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions test/components/generators/chat/test_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading