Skip to content

Commit afdb998

Browse files
committed
Revert "fix: updating generated answer and extracted answers serialisation (#11805)" (#11895)
1 parent 0f04657 commit afdb998

4 files changed

Lines changed: 124 additions & 219 deletions

File tree

haystack/dataclasses/answer.py

Lines changed: 37 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dataclasses import asdict, dataclass, field
66
from typing import Any, Optional, Protocol, runtime_checkable
77

8+
from haystack.core.serialization import default_from_dict, default_to_dict
89
from haystack.dataclasses import ChatMessage, Document
910
from haystack.utils.dataclasses import _warn_on_inplace_mutation
1011

@@ -53,16 +54,20 @@ def to_dict(self) -> dict[str, Any]:
5354
:returns:
5455
Serialized dictionary representation of the object.
5556
"""
56-
return {
57-
"data": self.data,
58-
"query": self.query,
59-
"document": self.document.to_dict(flatten=False) if self.document is not None else None,
60-
"context": self.context,
61-
"score": self.score,
62-
"document_offset": asdict(self.document_offset) if self.document_offset is not None else None,
63-
"context_offset": asdict(self.context_offset) if self.context_offset is not None else None,
64-
"meta": self.meta,
65-
}
57+
document = self.document.to_dict(flatten=False) if self.document is not None else None
58+
document_offset = asdict(self.document_offset) if self.document_offset is not None else None
59+
context_offset = asdict(self.context_offset) if self.context_offset is not None else None
60+
return default_to_dict(
61+
self,
62+
data=self.data,
63+
query=self.query,
64+
document=document,
65+
context=self.context,
66+
score=self.score,
67+
document_offset=document_offset,
68+
context_offset=context_offset,
69+
meta=self.meta,
70+
)
6671

6772
@classmethod
6873
def from_dict(cls, data: dict[str, Any]) -> "ExtractedAnswer":
@@ -74,32 +79,16 @@ def from_dict(cls, data: dict[str, Any]) -> "ExtractedAnswer":
7479
:returns:
7580
Deserialized object.
7681
"""
77-
# backward compat: old format wrapped fields in init_parameters
78-
if "init_parameters" in data:
79-
data = data["init_parameters"]
80-
81-
document = data.get("document")
82-
if document is not None:
83-
document = Document.from_dict(document)
84-
85-
document_offset = data.get("document_offset")
86-
if document_offset is not None:
87-
document_offset = ExtractedAnswer.Span(**document_offset)
88-
89-
context_offset = data.get("context_offset")
90-
if context_offset is not None:
91-
context_offset = ExtractedAnswer.Span(**context_offset)
92-
93-
return cls(
94-
data=data.get("data"),
95-
query=data["query"],
96-
score=data["score"],
97-
document=document,
98-
context=data.get("context"),
99-
document_offset=document_offset,
100-
context_offset=context_offset,
101-
meta=data.get("meta", {}),
102-
)
82+
init_params = data.get("init_parameters", {})
83+
if (doc := init_params.get("document")) is not None:
84+
data["init_parameters"]["document"] = Document.from_dict(doc)
85+
86+
if (offset := init_params.get("document_offset")) is not None:
87+
data["init_parameters"]["document_offset"] = ExtractedAnswer.Span(**offset)
88+
89+
if (offset := init_params.get("context_offset")) is not None:
90+
data["init_parameters"]["context_offset"] = ExtractedAnswer.Span(**offset)
91+
return default_from_dict(cls, data)
10392

10493

10594
@_warn_on_inplace_mutation
@@ -121,18 +110,17 @@ def to_dict(self) -> dict[str, Any]:
121110
:returns:
122111
Serialized dictionary representation of the object.
123112
"""
124-
# all_messages is either a list of ChatMessage objects or a list of strings
113+
documents = [doc.to_dict(flatten=False) for doc in self.documents]
114+
115+
# Serialize ChatMessage objects to dicts
125116
meta = self.meta
126117
all_messages = meta.get("all_messages")
118+
119+
# all_messages is either a list of ChatMessage objects or a list of strings
127120
if all_messages and isinstance(all_messages[0], ChatMessage):
128121
meta = {**meta, "all_messages": [msg.to_dict() for msg in all_messages]}
129122

130-
return {
131-
"data": self.data,
132-
"query": self.query,
133-
"documents": [doc.to_dict(flatten=False) for doc in self.documents],
134-
"meta": meta,
135-
}
123+
return default_to_dict(self, data=self.data, query=self.query, documents=documents, meta=meta)
136124

137125
@classmethod
138126
def from_dict(cls, data: dict[str, Any]) -> "GeneratedAnswer":
@@ -145,15 +133,14 @@ def from_dict(cls, data: dict[str, Any]) -> "GeneratedAnswer":
145133
:returns:
146134
Deserialized object.
147135
"""
148-
# backward compatibility: old format wrapped fields in init_parameters
149-
if "init_parameters" in data:
150-
data = data["init_parameters"]
136+
init_params = data.get("init_parameters", {})
151137

152-
documents = [Document.from_dict(d) for d in data.get("documents", [])]
138+
if (documents := init_params.get("documents")) is not None:
139+
init_params["documents"] = [Document.from_dict(d) for d in documents]
153140

154-
# copy to avoid mutating the caller's input dict when converting all_messages
155-
meta = dict(data.get("meta", {}))
141+
meta = init_params.get("meta", {})
156142
if (all_messages := meta.get("all_messages")) and isinstance(all_messages[0], dict):
157143
meta["all_messages"] = [ChatMessage.from_dict(m) for m in all_messages]
144+
init_params["meta"] = meta
158145

159-
return cls(data=data["data"], query=data["query"], documents=documents, meta=meta)
146+
return default_from_dict(cls, data)

releasenotes/notes/fix-dataclasses-deseria-0bb839b5edd3fca6.yaml

Lines changed: 0 additions & 8 deletions
This file was deleted.

test/dataclasses/test_answer.py

Lines changed: 36 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,20 @@ def test_to_dict(self):
5757
meta={"meta_key": "meta_value"},
5858
)
5959
assert answer.to_dict() == {
60-
"data": "42",
61-
"query": "What is the answer?",
62-
"document": document.to_dict(flatten=False),
63-
"context": "The answer is 42.",
64-
"score": 1.0,
65-
"document_offset": {"start": 42, "end": 44},
66-
"context_offset": {"start": 14, "end": 16},
67-
"meta": {"meta_key": "meta_value"},
60+
"type": "haystack.dataclasses.answer.ExtractedAnswer",
61+
"init_parameters": {
62+
"data": "42",
63+
"query": "What is the answer?",
64+
"document": document.to_dict(flatten=False),
65+
"context": "The answer is 42.",
66+
"score": 1.0,
67+
"document_offset": {"start": 42, "end": 44},
68+
"context_offset": {"start": 14, "end": 16},
69+
"meta": {"meta_key": "meta_value"},
70+
},
6871
}
6972

70-
def test_from_dict_legacy(self):
73+
def test_from_dict(self):
7174
answer = ExtractedAnswer.from_dict(
7275
{
7376
"type": "haystack.dataclasses.answer.ExtractedAnswer",
@@ -98,34 +101,6 @@ def test_from_dict_legacy(self):
98101
assert answer.context_offset == ExtractedAnswer.Span(14, 16)
99102
assert answer.meta == {"meta_key": "meta_value"}
100103

101-
def test_from_dict(self):
102-
answer = ExtractedAnswer.from_dict(
103-
{
104-
"data": "42",
105-
"query": "What is the answer?",
106-
"document": {
107-
"id": "8f800a524b139484fc719ecc35f971a080de87618319bc4836b784d69baca57f",
108-
"content": "I thought a lot about this. The answer is 42.",
109-
},
110-
"context": "The answer is 42.",
111-
"score": 1.0,
112-
"document_offset": {"start": 42, "end": 44},
113-
"context_offset": {"start": 14, "end": 16},
114-
"meta": {"meta_key": "meta_value"},
115-
}
116-
)
117-
assert answer.data == "42"
118-
assert answer.query == "What is the answer?"
119-
assert answer.document == Document(
120-
id="8f800a524b139484fc719ecc35f971a080de87618319bc4836b784d69baca57f",
121-
content="I thought a lot about this. The answer is 42.",
122-
)
123-
assert answer.context == "The answer is 42."
124-
assert answer.score == 1.0
125-
assert answer.document_offset == ExtractedAnswer.Span(42, 44)
126-
assert answer.context_offset == ExtractedAnswer.Span(14, 16)
127-
assert answer.meta == {"meta_key": "meta_value"}
128-
129104
def test_no_warning_on_init(self):
130105
with warnings.catch_warnings():
131106
warnings.simplefilter("error", Warning)
@@ -183,7 +158,10 @@ def test_protocol(self):
183158

184159
def test_to_dict(self):
185160
answer = GeneratedAnswer(data="42", query="What is the answer?", documents=[])
186-
assert answer.to_dict() == {"data": "42", "query": "What is the answer?", "documents": [], "meta": {}}
161+
assert answer.to_dict() == {
162+
"type": "haystack.dataclasses.answer.GeneratedAnswer",
163+
"init_parameters": {"data": "42", "query": "What is the answer?", "documents": [], "meta": {}},
164+
}
187165

188166
def test_to_dict_with_meta(self):
189167
answer = GeneratedAnswer(
@@ -193,10 +171,13 @@ def test_to_dict_with_meta(self):
193171
meta={"meta_key": "meta_value", "all_messages": ["What is the answer?"]},
194172
)
195173
assert answer.to_dict() == {
196-
"data": "42",
197-
"query": "What is the answer?",
198-
"documents": [],
199-
"meta": {"meta_key": "meta_value", "all_messages": ["What is the answer?"]},
174+
"type": "haystack.dataclasses.answer.GeneratedAnswer",
175+
"init_parameters": {
176+
"data": "42",
177+
"query": "What is the answer?",
178+
"documents": [],
179+
"meta": {"meta_key": "meta_value", "all_messages": ["What is the answer?"]},
180+
},
200181
}
201182

202183
def test_to_dict_with_chat_message_in_meta(self):
@@ -212,16 +193,19 @@ def test_to_dict_with_chat_message_in_meta(self):
212193
meta={"meta_key": "meta_value", "all_messages": [ChatMessage.from_user("What is the answer?")]},
213194
)
214195
assert answer.to_dict() == {
215-
"data": "42",
216-
"query": "What is the answer?",
217-
"documents": [d.to_dict(flatten=False) for d in documents],
218-
"meta": {
219-
"meta_key": "meta_value",
220-
"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()],
196+
"type": "haystack.dataclasses.answer.GeneratedAnswer",
197+
"init_parameters": {
198+
"data": "42",
199+
"query": "What is the answer?",
200+
"documents": [d.to_dict(flatten=False) for d in documents],
201+
"meta": {
202+
"meta_key": "meta_value",
203+
"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()],
204+
},
221205
},
222206
}
223207

224-
def test_from_dict_legacy(self):
208+
def test_from_dict(self):
225209
answer = GeneratedAnswer.from_dict(
226210
{
227211
"type": "haystack.dataclasses.answer.GeneratedAnswer",
@@ -233,7 +217,7 @@ def test_from_dict_legacy(self):
233217
assert answer.documents == []
234218
assert answer.meta == {}
235219

236-
def test_from_dict_with_meta_legacy(self):
220+
def test_from_dict_with_meta(self):
237221
answer = GeneratedAnswer.from_dict(
238222
{
239223
"type": "haystack.dataclasses.answer.GeneratedAnswer",
@@ -251,7 +235,7 @@ def test_from_dict_with_meta_legacy(self):
251235
assert answer.meta["meta_key"] == "meta_value"
252236
assert answer.meta["all_messages"] == ["What is the answer?"]
253237

254-
def test_from_dict_with_chat_message_in_meta_legacy(self):
238+
def test_from_dict_with_chat_message_in_meta(self):
255239
answer = GeneratedAnswer.from_dict(
256240
{
257241
"type": "haystack.dataclasses.answer.GeneratedAnswer",
@@ -280,7 +264,7 @@ def test_from_dict_with_chat_message_in_meta_legacy(self):
280264
assert answer.meta["meta_key"] == "meta_value"
281265
assert answer.meta["all_messages"] == [ChatMessage.from_user("What is the answer?")]
282266

283-
def test_from_dict_with_empty_all_messages_legacy(self):
267+
def test_from_dict_with_empty_all_messages(self):
284268
# An empty `all_messages` list must not crash deserialization: `is not None`
285269
# let `[]` through and then indexed `all_messages[0]`, raising IndexError.
286270
answer = GeneratedAnswer.from_dict(
@@ -296,73 +280,6 @@ def test_from_dict_with_empty_all_messages_legacy(self):
296280
)
297281
assert answer.meta["all_messages"] == []
298282

299-
def test_from_dict(self):
300-
answer = GeneratedAnswer.from_dict({"data": "42", "query": "What is the answer?", "documents": [], "meta": {}})
301-
assert answer.data == "42"
302-
assert answer.query == "What is the answer?"
303-
assert answer.documents == []
304-
assert answer.meta == {}
305-
306-
def test_from_dict_with_meta(self):
307-
answer = GeneratedAnswer.from_dict(
308-
{
309-
"data": "42",
310-
"query": "What is the answer?",
311-
"documents": [],
312-
"meta": {"meta_key": "meta_value", "all_messages": ["What is the answer?"]},
313-
}
314-
)
315-
assert answer.data == "42"
316-
assert answer.query == "What is the answer?"
317-
assert answer.documents == []
318-
assert answer.meta["meta_key"] == "meta_value"
319-
assert answer.meta["all_messages"] == ["What is the answer?"]
320-
321-
def test_from_dict_with_chat_message_in_meta(self):
322-
answer = GeneratedAnswer.from_dict(
323-
{
324-
"data": "42",
325-
"query": "What is the answer?",
326-
"documents": [
327-
{"id": "1", "content": "The answer is 42."},
328-
{"id": "2", "content": "I believe the answer is 42."},
329-
{"id": "3", "content": "42 is definitely the answer."},
330-
],
331-
"meta": {
332-
"meta_key": "meta_value",
333-
"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()],
334-
},
335-
}
336-
)
337-
assert answer.data == "42"
338-
assert answer.query == "What is the answer?"
339-
assert answer.documents == [
340-
Document(id="1", content="The answer is 42."),
341-
Document(id="2", content="I believe the answer is 42."),
342-
Document(id="3", content="42 is definitely the answer."),
343-
]
344-
assert answer.meta["meta_key"] == "meta_value"
345-
assert answer.meta["all_messages"] == [ChatMessage.from_user("What is the answer?")]
346-
347-
def test_from_dict_with_empty_all_messages(self):
348-
answer = GeneratedAnswer.from_dict(
349-
{"data": "42", "query": "What is the answer?", "documents": [], "meta": {"all_messages": []}}
350-
)
351-
assert answer.meta["all_messages"] == []
352-
353-
def test_from_dict_does_not_mutate_input(self):
354-
# from_dict must not mutate the caller's input dict while converting
355-
# `all_messages` dicts into ChatMessage objects (regression test).
356-
meta = {"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()]}
357-
serialized = {"data": "42", "query": "What is the answer?", "documents": [], "meta": meta}
358-
answer = GeneratedAnswer.from_dict(serialized)
359-
360-
# the deserialized answer still holds ChatMessage objects
361-
assert answer.meta["all_messages"] == [ChatMessage.from_user("What is the answer?")]
362-
# but the caller's meta dict is left untouched: it still holds plain dicts
363-
assert isinstance(meta["all_messages"][0], dict)
364-
assert meta["all_messages"] == [ChatMessage.from_user("What is the answer?").to_dict()]
365-
366283
def test_to_dict_from_dict_round_trip_with_empty_all_messages(self):
367284
answer = GeneratedAnswer(data="42", query="What is the answer?", documents=[], meta={"all_messages": []})
368285
assert GeneratedAnswer.from_dict(answer.to_dict()) == answer

0 commit comments

Comments
 (0)