Skip to content

Commit 8d22008

Browse files
authored
feat: Enhance agent snapshot serialization with error handling for non-serializable inputs (#11108)
* feat: Enhance agent snapshot serialization with error handling for non-serializable inputs * fix release note formatting
1 parent ec9b810 commit 8d22008

3 files changed

Lines changed: 102 additions & 7 deletions

File tree

haystack/core/pipeline/breakpoint.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,32 @@ def _create_agent_snapshot(
375375
:param agent_breakpoint: AgentBreakpoint object containing breakpoints
376376
:return: An AgentSnapshot containing the agent's state and component visits.
377377
"""
378+
try:
379+
serialized_chat_generator = _serialize_value_with_schema(
380+
_deepcopy_with_exceptions(component_inputs["chat_generator"])
381+
)
382+
except Exception as error:
383+
logger.warning(
384+
"Failed to serialize the agent's chat_generator inputs. "
385+
"The inputs in the snapshot will be replaced with an empty dictionary. Error: {e}",
386+
e=error,
387+
)
388+
serialized_chat_generator = {}
389+
390+
try:
391+
serialized_tool_invoker = _serialize_value_with_schema(
392+
_deepcopy_with_exceptions(component_inputs["tool_invoker"])
393+
)
394+
except Exception as error:
395+
logger.warning(
396+
"Failed to serialize the agent's tool_invoker inputs. "
397+
"The inputs in the snapshot will be replaced with an empty dictionary. Error: {e}",
398+
e=error,
399+
)
400+
serialized_tool_invoker = {}
401+
378402
return AgentSnapshot(
379-
component_inputs={
380-
"chat_generator": _serialize_value_with_schema(
381-
_deepcopy_with_exceptions(component_inputs["chat_generator"])
382-
),
383-
"tool_invoker": _serialize_value_with_schema(_deepcopy_with_exceptions(component_inputs["tool_invoker"])),
384-
},
403+
component_inputs={"chat_generator": serialized_chat_generator, "tool_invoker": serialized_tool_invoker},
385404
component_visits=component_visits,
386405
break_point=agent_breakpoint,
387406
timestamp=datetime.now(),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
enhancements:
3+
- |
4+
Made ``_create_agent_snapshot`` robust towards serialization errors. If serializing
5+
agent component inputs fails, a warning is logged and an empty dictionary is used
6+
as a fallback, preventing the serialization error from masking the real pipeline
7+
runtime error.

test/core/pipeline/test_breakpoint.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
from haystack.core.pipeline import Pipeline
1313
from haystack.core.pipeline.breakpoint import (
1414
HAYSTACK_PIPELINE_SNAPSHOT_SAVE_ENABLED,
15+
_create_agent_snapshot,
1516
_create_pipeline_snapshot,
1617
_is_snapshot_save_enabled,
1718
_save_pipeline_snapshot,
1819
_transform_json_structure,
1920
load_pipeline_snapshot,
2021
)
2122
from haystack.dataclasses import ChatMessage
22-
from haystack.dataclasses.breakpoints import Breakpoint, PipelineSnapshot, PipelineState
23+
from haystack.dataclasses.breakpoints import AgentBreakpoint, Breakpoint, PipelineSnapshot, PipelineState
2324

2425

2526
def test_transform_json_structure_unwraps_sender_value():
@@ -239,6 +240,74 @@ def to_dict(self):
239240
assert any("Failed to serialize original input data for `pipeline.run`." in msg for msg in caplog.messages)
240241

241242

243+
class TestCreateAgentSnapshot:
244+
def test_create_agent_snapshot_non_serializable_chat_generator(self, caplog):
245+
class NonSerializable:
246+
def to_dict(self):
247+
raise TypeError("Cannot serialize")
248+
249+
agent_breakpoint = AgentBreakpoint(
250+
agent_name="agent", break_point=Breakpoint(component_name="chat_generator", visit_count=1)
251+
)
252+
253+
with caplog.at_level(logging.WARNING):
254+
snapshot = _create_agent_snapshot(
255+
component_visits={"chat_generator": 1, "tool_invoker": 0},
256+
agent_breakpoint=agent_breakpoint,
257+
component_inputs={"chat_generator": {"messages": NonSerializable()}, "tool_invoker": {"messages": []}},
258+
)
259+
260+
assert snapshot.component_inputs["chat_generator"] == {}
261+
assert snapshot.component_inputs["tool_invoker"] != {}
262+
assert "Failed to serialize the agent's chat_generator inputs" in caplog.text
263+
264+
def test_create_agent_snapshot_non_serializable_tool_invoker(self, caplog):
265+
class NonSerializable:
266+
def to_dict(self):
267+
raise TypeError("Cannot serialize")
268+
269+
agent_breakpoint = AgentBreakpoint(
270+
agent_name="agent", break_point=Breakpoint(component_name="chat_generator", visit_count=1)
271+
)
272+
273+
with caplog.at_level(logging.WARNING):
274+
snapshot = _create_agent_snapshot(
275+
component_visits={"chat_generator": 1, "tool_invoker": 0},
276+
agent_breakpoint=agent_breakpoint,
277+
component_inputs={"chat_generator": {"messages": []}, "tool_invoker": {"messages": NonSerializable()}},
278+
)
279+
280+
assert snapshot.component_inputs["tool_invoker"] == {}
281+
assert snapshot.component_inputs["chat_generator"] != {}
282+
assert "Failed to serialize the agent's tool_invoker inputs" in caplog.text
283+
284+
def test_create_agent_snapshot_both_non_serializable(self, caplog):
285+
class NonSerializable:
286+
def to_dict(self):
287+
raise TypeError("Cannot serialize")
288+
289+
agent_breakpoint = AgentBreakpoint(
290+
agent_name="agent", break_point=Breakpoint(component_name="chat_generator", visit_count=1)
291+
)
292+
293+
with caplog.at_level(logging.WARNING):
294+
snapshot = _create_agent_snapshot(
295+
component_visits={"chat_generator": 1, "tool_invoker": 0},
296+
agent_breakpoint=agent_breakpoint,
297+
component_inputs={
298+
"chat_generator": {"messages": NonSerializable()},
299+
"tool_invoker": {"messages": NonSerializable()},
300+
},
301+
)
302+
303+
assert snapshot.component_inputs["chat_generator"] == {}
304+
assert snapshot.component_inputs["tool_invoker"] == {}
305+
assert "Failed to serialize the agent's chat_generator inputs" in caplog.text
306+
assert "Failed to serialize the agent's tool_invoker inputs" in caplog.text
307+
assert snapshot.component_visits == {"chat_generator": 1, "tool_invoker": 0}
308+
assert snapshot.break_point == agent_breakpoint
309+
310+
242311
def test_save_pipeline_snapshot_raises_on_failure(tmp_path, caplog, monkeypatch):
243312
monkeypatch.setenv(HAYSTACK_PIPELINE_SNAPSHOT_SAVE_ENABLED, "true")
244313

0 commit comments

Comments
 (0)