Coverage for haystack/dataclasses/answer.py: 100%
57 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from dataclasses import asdict, dataclass, field
6from typing import Any, Optional, Protocol, runtime_checkable
8from haystack.dataclasses import ChatMessage, Document
9from haystack.utils.dataclasses import _warn_on_inplace_mutation
12@runtime_checkable
13@dataclass
14class Answer(Protocol):
15 data: Any
16 query: str
17 meta: dict[str, Any]
19 def to_dict(self) -> dict[str, Any]: # noqa: D102
20 ...
22 @classmethod
23 def from_dict(cls, data: dict[str, Any]) -> "Answer": # noqa: D102
24 ...
27@_warn_on_inplace_mutation
28@dataclass
29class ExtractedAnswer:
30 """
31 Holds an answer extracted by an extractive Reader (query, score, text, and optional document/context).
32 """
34 query: str
35 score: float
36 data: str | None = None
37 document: Document | None = None
38 context: str | None = None
39 document_offset: Optional["Span"] = None
40 context_offset: Optional["Span"] = None
41 meta: dict[str, Any] = field(default_factory=dict)
43 @_warn_on_inplace_mutation
44 @dataclass
45 class Span:
46 start: int
47 end: int
49 def to_dict(self) -> dict[str, Any]:
50 """
51 Serialize the object to a dictionary.
53 :returns:
54 Serialized dictionary representation of the object.
55 """
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 }
67 @classmethod
68 def from_dict(cls, data: dict[str, Any]) -> "ExtractedAnswer":
69 """
70 Deserialize the object from a dictionary.
72 :param data:
73 Dictionary representation of the object.
74 :returns:
75 Deserialized object.
76 """
77 # Backward compatibility: the old format wrapped the fields in an `init_parameters` envelope.
78 if "init_parameters" in data:
79 data = data["init_parameters"]
81 document = data.get("document")
82 if document is not None:
83 document = Document.from_dict(document)
85 document_offset = data.get("document_offset")
86 if document_offset is not None:
87 document_offset = ExtractedAnswer.Span(**document_offset)
89 context_offset = data.get("context_offset")
90 if context_offset is not None:
91 context_offset = ExtractedAnswer.Span(**context_offset)
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 )
105@_warn_on_inplace_mutation
106@dataclass
107class GeneratedAnswer:
108 """
109 Holds a generated answer from a Generator (answer text, query, referenced documents, and metadata).
110 """
112 data: str
113 query: str
114 documents: list[Document]
115 meta: dict[str, Any] = field(default_factory=dict)
117 def to_dict(self) -> dict[str, Any]:
118 """
119 Serialize the object to a dictionary.
121 :returns:
122 Serialized dictionary representation of the object.
123 """
124 # all_messages is either a list of ChatMessage objects or a list of strings
125 meta = self.meta
126 all_messages = meta.get("all_messages")
127 if all_messages and isinstance(all_messages[0], ChatMessage):
128 meta = {**meta, "all_messages": [msg.to_dict() for msg in all_messages]}
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 }
137 @classmethod
138 def from_dict(cls, data: dict[str, Any]) -> "GeneratedAnswer":
139 """
140 Deserialize the object from a dictionary.
142 :param data:
143 Dictionary representation of the object.
145 :returns:
146 Deserialized object.
147 """
148 # Backward compatibility: the old format wrapped the fields in an `init_parameters` envelope.
149 if "init_parameters" in data:
150 data = data["init_parameters"]
152 documents = [Document.from_dict(d) for d in data.get("documents", [])]
154 # Copy `meta` before converting `all_messages` so the caller's input dict is left untouched.
155 meta = dict(data.get("meta", {}))
156 if (all_messages := meta.get("all_messages")) and isinstance(all_messages[0], dict):
157 meta["all_messages"] = [ChatMessage.from_dict(m) for m in all_messages]
159 return cls(data=data["data"], query=data["query"], documents=documents, meta=meta)