From 1d6a9f652a3ce2e50b9ca8a9bb1a4d76542c4b3b Mon Sep 17 00:00:00 2001 From: Amna Mubashar Date: Fri, 6 Jun 2025 11:46:24 +0200 Subject: [PATCH] fix: serialization of nested `ChatMessage` in `GeneratedAnswer`dataclass (#9497) * Fix serialization * small fix * fix the erros * Fix tests * PR comments --- haystack/dataclasses/answer.py | 21 +++++- ...ion-generated-answer-fa8f3692a184f8fd.yaml | 3 + .../builders/test_answer_builder.py | 4 + test/dataclasses/test_answer.py | 74 +++++++++++++++++-- 4 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-serialization-generated-answer-fa8f3692a184f8fd.yaml diff --git a/haystack/dataclasses/answer.py b/haystack/dataclasses/answer.py index db840ba22..57d5cd5c0 100644 --- a/haystack/dataclasses/answer.py +++ b/haystack/dataclasses/answer.py @@ -6,7 +6,7 @@ from dataclasses import asdict, dataclass, field from typing import Any, Dict, List, Optional, Protocol, runtime_checkable from haystack.core.serialization import default_from_dict, default_to_dict -from haystack.dataclasses.document import Document +from haystack.dataclasses import ChatMessage, Document @runtime_checkable @@ -99,7 +99,16 @@ class GeneratedAnswer: Serialized dictionary representation of the object. """ documents = [doc.to_dict(flatten=False) for doc in self.documents] - return default_to_dict(self, data=self.data, query=self.query, documents=documents, meta=self.meta) + + # Serialize ChatMessage objects to dicts + meta = self.meta + all_messages = meta.get("all_messages") + + # all_messages is either a list of ChatMessage objects or a list of strings + if all_messages and isinstance(all_messages[0], ChatMessage): + meta = {**meta, "all_messages": [msg.to_dict() for msg in all_messages]} + + return default_to_dict(self, data=self.data, query=self.query, documents=documents, meta=meta) @classmethod def from_dict(cls, data: Dict[str, Any]) -> "GeneratedAnswer": @@ -113,7 +122,13 @@ class GeneratedAnswer: Deserialized object. """ init_params = data.get("init_parameters", {}) + if (documents := init_params.get("documents")) is not None: - data["init_parameters"]["documents"] = [Document.from_dict(d) for d in documents] + init_params["documents"] = [Document.from_dict(d) for d in documents] + + meta = init_params.get("meta", {}) + if (all_messages := meta.get("all_messages")) is not None and isinstance(all_messages[0], dict): + meta["all_messages"] = [ChatMessage.from_dict(m) for m in all_messages] + init_params["meta"] = meta return default_from_dict(cls, data) diff --git a/releasenotes/notes/fix-serialization-generated-answer-fa8f3692a184f8fd.yaml b/releasenotes/notes/fix-serialization-generated-answer-fa8f3692a184f8fd.yaml new file mode 100644 index 000000000..82c9c0899 --- /dev/null +++ b/releasenotes/notes/fix-serialization-generated-answer-fa8f3692a184f8fd.yaml @@ -0,0 +1,3 @@ +--- +fixes: + - Fix serialization of `GeneratedAnswer` when `ChatMessage` objects are nested in `meta`. diff --git a/test/components/builders/test_answer_builder.py b/test/components/builders/test_answer_builder.py index 199647bcc..e85e4d71c 100644 --- a/test/components/builders/test_answer_builder.py +++ b/test/components/builders/test_answer_builder.py @@ -52,6 +52,7 @@ class TestAnswerBuilder: assert answers[0].data == "reply1" _check_metadata_excluding_all_messages(answers[0].meta, {"test": "meta"}) assert "all_messages" in answers[0].meta + assert answers[0].meta["all_messages"] == ["reply1"] assert answers[0].query == "query" assert answers[0].documents == [] assert isinstance(answers[0], GeneratedAnswer) @@ -254,6 +255,9 @@ class TestAnswerBuilder: assert answers[0].query == "test query" assert len(answers[0].documents) == 1 assert answers[0].documents[0].content == "test doc 2" + assert answers[0].meta["all_messages"] == [ + ChatMessage.from_assistant("Answer: AnswerString[2]", meta=message_meta) + ] def test_run_with_chat_message_replies_with_pattern_set_at_runtime(self): component = AnswerBuilder(pattern="unused pattern") diff --git a/test/dataclasses/test_answer.py b/test/dataclasses/test_answer.py index e3b879166..c1aec46f6 100644 --- a/test/dataclasses/test_answer.py +++ b/test/dataclasses/test_answer.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 from haystack.dataclasses.answer import Answer, ExtractedAnswer, GeneratedAnswer -from haystack.dataclasses.document import Document +from haystack.dataclasses import Document, ChatMessage class TestExtractedAnswer: @@ -133,13 +133,40 @@ class TestGeneratedAnswer: assert isinstance(answer, Answer) def test_to_dict(self): + answer = GeneratedAnswer(data="42", query="What is the answer?", documents=[]) + assert answer.to_dict() == { + "type": "haystack.dataclasses.answer.GeneratedAnswer", + "init_parameters": {"data": "42", "query": "What is the answer?", "documents": [], "meta": {}}, + } + + def test_to_dict_with_meta(self): + answer = GeneratedAnswer( + data="42", + query="What is the answer?", + documents=[], + meta={"meta_key": "meta_value", "all_messages": ["What is the answer?"]}, + ) + assert answer.to_dict() == { + "type": "haystack.dataclasses.answer.GeneratedAnswer", + "init_parameters": { + "data": "42", + "query": "What is the answer?", + "documents": [], + "meta": {"meta_key": "meta_value", "all_messages": ["What is the answer?"]}, + }, + } + + def test_to_dict_with_chat_message_in_meta(self): documents = [ Document(id="1", content="The answer is 42."), Document(id="2", content="I believe the answer is 42."), Document(id="3", content="42 is definitely the answer."), ] answer = GeneratedAnswer( - data="42", query="What is the answer?", documents=documents, meta={"meta_key": "meta_value"} + data="42", + query="What is the answer?", + documents=documents, + meta={"meta_key": "meta_value", "all_messages": [ChatMessage.from_user("What is the answer?")]}, ) assert answer.to_dict() == { "type": "haystack.dataclasses.answer.GeneratedAnswer", @@ -147,11 +174,44 @@ class TestGeneratedAnswer: "data": "42", "query": "What is the answer?", "documents": [d.to_dict(flatten=False) for d in documents], - "meta": {"meta_key": "meta_value"}, + "meta": { + "meta_key": "meta_value", + "all_messages": [ChatMessage.from_user("What is the answer?").to_dict()], + }, }, } def test_from_dict(self): + answer = GeneratedAnswer.from_dict( + { + "type": "haystack.dataclasses.answer.GeneratedAnswer", + "init_parameters": {"data": "42", "query": "What is the answer?", "documents": [], "meta": {}}, + } + ) + assert answer.data == "42" + assert answer.query == "What is the answer?" + assert answer.documents == [] + assert answer.meta == {} + + def test_from_dict_with_meta(self): + answer = GeneratedAnswer.from_dict( + { + "type": "haystack.dataclasses.answer.GeneratedAnswer", + "init_parameters": { + "data": "42", + "query": "What is the answer?", + "documents": [], + "meta": {"meta_key": "meta_value", "all_messages": ["What is the answer?"]}, + }, + } + ) + assert answer.data == "42" + assert answer.query == "What is the answer?" + assert answer.documents == [] + assert answer.meta["meta_key"] == "meta_value" + assert answer.meta["all_messages"] == ["What is the answer?"] + + def test_from_dict_with_chat_message_in_meta(self): answer = GeneratedAnswer.from_dict( { "type": "haystack.dataclasses.answer.GeneratedAnswer", @@ -163,7 +223,10 @@ class TestGeneratedAnswer: {"id": "2", "content": "I believe the answer is 42."}, {"id": "3", "content": "42 is definitely the answer."}, ], - "meta": {"meta_key": "meta_value"}, + "meta": { + "meta_key": "meta_value", + "all_messages": [ChatMessage.from_user("What is the answer?").to_dict()], + }, }, } ) @@ -174,4 +237,5 @@ class TestGeneratedAnswer: Document(id="2", content="I believe the answer is 42."), Document(id="3", content="42 is definitely the answer."), ] - assert answer.meta == {"meta_key": "meta_value"} + assert answer.meta["meta_key"] == "meta_value" + assert answer.meta["all_messages"] == [ChatMessage.from_user("What is the answer?")]