chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
from copy import deepcopy
import pytest
from haystack.dataclasses import ChatMessage, Document
from haystack.dataclasses.answer import Answer, ExtractedAnswer, GeneratedAnswer
class TestExtractedAnswer:
def test_init(self):
answer = ExtractedAnswer(
data="42",
query="What is the answer?",
document=Document(content="I thought a lot about this. The answer is 42."),
context="The answer is 42.",
score=1.0,
document_offset=ExtractedAnswer.Span(42, 44),
context_offset=ExtractedAnswer.Span(14, 16),
meta={"meta_key": "meta_value"},
)
assert answer.data == "42"
assert answer.query == "What is the answer?"
assert answer.document == Document(content="I thought a lot about this. The answer is 42.")
assert answer.context == "The answer is 42."
assert answer.score == 1.0
assert answer.document_offset == ExtractedAnswer.Span(42, 44)
assert answer.context_offset == ExtractedAnswer.Span(14, 16)
assert answer.meta == {"meta_key": "meta_value"}
def test_protocol(self):
answer = ExtractedAnswer(
data="42",
query="What is the answer?",
document=Document(content="I thought a lot about this. The answer is 42."),
context="The answer is 42.",
score=1.0,
document_offset=ExtractedAnswer.Span(42, 44),
context_offset=ExtractedAnswer.Span(14, 16),
meta={"meta_key": "meta_value"},
)
assert isinstance(answer, Answer)
def test_to_dict(self):
document = Document(content="I thought a lot about this. The answer is 42.")
answer = ExtractedAnswer(
data="42",
query="What is the answer?",
document=document,
context="The answer is 42.",
score=1.0,
document_offset=ExtractedAnswer.Span(42, 44),
context_offset=ExtractedAnswer.Span(14, 16),
meta={"meta_key": "meta_value"},
)
assert answer.to_dict() == {
"type": "haystack.dataclasses.answer.ExtractedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"document": document.to_dict(flatten=False),
"context": "The answer is 42.",
"score": 1.0,
"document_offset": {"start": 42, "end": 44},
"context_offset": {"start": 14, "end": 16},
"meta": {"meta_key": "meta_value"},
},
}
def test_from_dict(self):
answer = ExtractedAnswer.from_dict(
{
"type": "haystack.dataclasses.answer.ExtractedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"document": {
"id": "8f800a524b139484fc719ecc35f971a080de87618319bc4836b784d69baca57f",
"content": "I thought a lot about this. The answer is 42.",
},
"context": "The answer is 42.",
"score": 1.0,
"document_offset": {"start": 42, "end": 44},
"context_offset": {"start": 14, "end": 16},
"meta": {"meta_key": "meta_value"},
},
}
)
assert answer.data == "42"
assert answer.query == "What is the answer?"
assert answer.document == Document(
id="8f800a524b139484fc719ecc35f971a080de87618319bc4836b784d69baca57f",
content="I thought a lot about this. The answer is 42.",
)
assert answer.context == "The answer is 42."
assert answer.score == 1.0
assert answer.document_offset == ExtractedAnswer.Span(42, 44)
assert answer.context_offset == ExtractedAnswer.Span(14, 16)
assert answer.meta == {"meta_key": "meta_value"}
def test_from_dict_does_not_mutate_input(self):
data = {
"type": "haystack.dataclasses.answer.ExtractedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"document": {
"id": "8f800a524b139484fc719ecc35f971a080de87618319bc4836b784d69baca57f",
"content": "I thought a lot about this. The answer is 42.",
},
"context": "The answer is 42.",
"score": 1.0,
"document_offset": {"start": 42, "end": 44},
"context_offset": {"start": 14, "end": 16},
"meta": {"meta_key": "meta_value"},
},
}
snapshot = deepcopy(data)
first = ExtractedAnswer.from_dict(data)
# from_dict must not mutate its input dictionary
assert data == snapshot
# deserializing the same dictionary again must still work and be equal
assert ExtractedAnswer.from_dict(data) == first
def test_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ExtractedAnswer(query="q", score=1.0)
def test_warn_on_inplace_mutation(self):
answer = ExtractedAnswer(query="q", score=1.0)
with pytest.warns(Warning, match="dataclasses.replace"):
answer.query = "new"
def test_span_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ExtractedAnswer.Span(start=0, end=5)
def test_span_warn_on_inplace_mutation(self):
span = ExtractedAnswer.Span(start=0, end=5)
with pytest.warns(Warning, match="dataclasses.replace"):
span.start = 1
class TestGeneratedAnswer:
def test_init(self):
answer = GeneratedAnswer(
data="42",
query="What is the answer?",
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."),
],
meta={"meta_key": "meta_value"},
)
assert answer.data == "42"
assert answer.query == "What is the answer?"
assert answer.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."),
]
assert answer.meta == {"meta_key": "meta_value"}
def test_protocol(self):
answer = GeneratedAnswer(
data="42",
query="What is the answer?",
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."),
],
meta={"meta_key": "meta_value"},
)
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", "all_messages": [ChatMessage.from_user("What is the answer?")]},
)
assert answer.to_dict() == {
"type": "haystack.dataclasses.answer.GeneratedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"documents": [d.to_dict(flatten=False) for d in documents],
"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",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"documents": [
{"id": "1", "content": "The answer is 42."},
{"id": "2", "content": "I believe the answer is 42."},
{"id": "3", "content": "42 is definitely the answer."},
],
"meta": {
"meta_key": "meta_value",
"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()],
},
},
}
)
assert answer.data == "42"
assert answer.query == "What is the answer?"
assert answer.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."),
]
assert answer.meta["meta_key"] == "meta_value"
assert answer.meta["all_messages"] == [ChatMessage.from_user("What is the answer?")]
def test_from_dict_does_not_mutate_input(self):
data = {
"type": "haystack.dataclasses.answer.GeneratedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"documents": [
{"id": "1", "content": "The answer is 42."},
{"id": "2", "content": "I believe the answer is 42."},
],
"meta": {
"meta_key": "meta_value",
"all_messages": [ChatMessage.from_user("What is the answer?").to_dict()],
},
},
}
snapshot = deepcopy(data)
first = GeneratedAnswer.from_dict(data)
# from_dict must not mutate its input dictionary
assert data == snapshot
# deserializing the same dictionary again must still work and be equal
assert GeneratedAnswer.from_dict(data) == first
def test_from_dict_with_empty_all_messages(self):
# An empty `all_messages` list must not crash deserialization: `is not None`
# let `[]` through and then indexed `all_messages[0]`, raising IndexError.
answer = GeneratedAnswer.from_dict(
{
"type": "haystack.dataclasses.answer.GeneratedAnswer",
"init_parameters": {
"data": "42",
"query": "What is the answer?",
"documents": [],
"meta": {"all_messages": []},
},
}
)
assert answer.meta["all_messages"] == []
def test_to_dict_from_dict_round_trip_with_empty_all_messages(self):
answer = GeneratedAnswer(data="42", query="What is the answer?", documents=[], meta={"all_messages": []})
assert GeneratedAnswer.from_dict(answer.to_dict()) == answer
def test_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
GeneratedAnswer(data="42", query="q", documents=[])
def test_warn_on_inplace_mutation(self):
answer = GeneratedAnswer(data="42", query="q", documents=[])
with pytest.warns(Warning, match="dataclasses.replace"):
answer.data = "new"
+39
View File
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
import pytest
from haystack.dataclasses.breakpoints import Breakpoint, PipelineSnapshot, PipelineState
def test_pipeline_state_no_warning_on_init():
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
PipelineState(inputs={}, component_visits={}, pipeline_outputs={})
def test_pipeline_state_warn_on_inplace_mutation():
state = PipelineState(inputs={}, component_visits={}, pipeline_outputs={})
with pytest.warns(Warning, match="dataclasses.replace"):
state.inputs = {"new": "value"}
def test_pipeline_snapshot_no_warning_on_init():
state = PipelineState(inputs={}, component_visits={"comp": 1}, pipeline_outputs={})
bp = Breakpoint(component_name="comp")
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
PipelineSnapshot(original_input_data={}, ordered_component_names=["comp"], pipeline_state=state, break_point=bp)
def test_pipeline_snapshot_warn_on_inplace_mutation():
state = PipelineState(inputs={}, component_visits={"comp": 1}, pipeline_outputs={})
bp = Breakpoint(component_name="comp")
snap = PipelineSnapshot(
original_input_data={}, ordered_component_names=["comp"], pipeline_state=state, break_point=bp
)
with pytest.warns(Warning, match="dataclasses.replace"):
snap.original_input_data = {"new": "data"}
+171
View File
@@ -0,0 +1,171 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
import pytest
from haystack.dataclasses import ByteStream
def test_from_file_path(tmp_path, request):
test_bytes = b"Hello, world!\n"
test_path = tmp_path / request.node.name
with open(test_path, "wb") as fd:
assert fd.write(test_bytes)
b = ByteStream.from_file_path(test_path)
assert b.data == test_bytes
assert b.mime_type is None
b = ByteStream.from_file_path(test_path, mime_type="text/plain")
assert b.data == test_bytes
assert b.mime_type == "text/plain"
b = ByteStream.from_file_path(test_path, meta={"foo": "bar"})
assert b.data == test_bytes
assert b.meta == {"foo": "bar"}
@pytest.mark.parametrize(
"file_path, expected_mime_types",
[
("spam.jpeg", {"image/jpeg"}),
("spam.jpg", {"image/jpeg"}),
("spam.png", {"image/png"}),
("spam.gif", {"image/gif"}),
("spam.svg", {"image/svg+xml"}),
("spam.js", {"text/javascript", "application/javascript"}),
("spam.txt", {"text/plain"}),
("spam.html", {"text/html"}),
("spam.htm", {"text/html"}),
("spam.css", {"text/css"}),
("spam.csv", {"text/csv"}),
("spam.md", {"text/markdown"}), # custom mapping
("spam.markdown", {"text/markdown"}), # custom mapping
("spam.msg", {"application/vnd.ms-outlook"}), # custom mapping
("spam.pdf", {"application/pdf"}),
("spam.xml", {"application/xml", "text/xml"}),
("spam.json", {"application/json"}),
("spam.doc", {"application/msword"}),
("spam.docx", {"application/vnd.openxmlformats-officedocument.wordprocessingml.document"}),
("spam.xls", {"application/vnd.ms-excel"}),
("spam.xlsx", {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}),
("spam.ppt", {"application/vnd.ms-powerpoint"}),
("spam.pptx", {"application/vnd.openxmlformats-officedocument.presentationml.presentation"}),
],
)
def test_from_file_path_guess_mime_type(file_path, expected_mime_types, tmp_path):
test_file = tmp_path / file_path
test_file.touch()
b = ByteStream.from_file_path(test_file, guess_mime_type=True)
assert b.mime_type in expected_mime_types
def test_explicit_mime_type_is_not_overwritten_by_guessing(tmp_path):
# create empty file with correct extension
test_file = tmp_path / "sample.md"
test_file.touch()
explicit_mime_type = "text/x-rst"
b = ByteStream.from_file_path(test_file, mime_type=explicit_mime_type, guess_mime_type=True)
assert b.mime_type == explicit_mime_type
def test_from_string():
test_string = "Hello, world!"
b = ByteStream.from_string(test_string)
assert b.data.decode() == test_string
assert b.mime_type is None
b = ByteStream.from_string(test_string, mime_type="text/plain")
assert b.data.decode() == test_string
assert b.mime_type == "text/plain"
b = ByteStream.from_string(test_string, meta={"foo": "bar"})
assert b.data.decode() == test_string
assert b.meta == {"foo": "bar"}
def test_to_string():
test_string = "Hello, world!"
b = ByteStream.from_string(test_string)
assert b.to_string() == test_string
def test_to_from_string_encoding():
test_string = "Hello Baščaršija!"
with pytest.raises(UnicodeEncodeError):
ByteStream.from_string(test_string, encoding="ISO-8859-1")
bs = ByteStream.from_string(test_string) # default encoding is utf-8
assert bs.to_string(encoding="ISO-8859-1") != test_string
assert bs.to_string(encoding="utf-8") == test_string
def test_to_string_encoding_error():
# test that it raises ValueError if the encoding is not valid
b = ByteStream.from_string("Hello, world!")
with pytest.raises(UnicodeDecodeError):
b.to_string("utf-16")
def test_to_file(tmp_path, request):
test_str = "Hello, world!\n"
test_path = tmp_path / request.node.name
ByteStream(test_str.encode()).to_file(test_path)
with open(test_path, "rb") as fd:
assert fd.read().decode() == test_str
def test_str_truncation():
test_str = "1234567890" * 100
b = ByteStream.from_string(test_str, mime_type="text/plain", meta={"foo": "bar"})
string_repr = str(b)
assert len(string_repr) < 200
assert "text/plain" in string_repr
assert "foo" in string_repr
def test_to_dict():
test_str = "Hello, world!"
b = ByteStream.from_string(test_str, mime_type="text/plain", meta={"foo": "bar"})
d = b.to_dict()
assert d["data"] == list(test_str.encode())
assert d["mime_type"] == "text/plain"
assert d["meta"] == {"foo": "bar"}
def test_to_trace_dict():
b = ByteStream(data=b"Hello, world!", mime_type="text/plain", meta={"foo": "bar"})
d = b._to_trace_dict()
assert d["data"] == "Binary data (13 bytes)"
assert d["mime_type"] == "text/plain"
assert d["meta"] == {"foo": "bar"}
def test_from_dict():
test_str = "Hello, world!"
b = ByteStream.from_string(test_str, mime_type="text/plain", meta={"foo": "bar"})
d = b.to_dict()
b2 = ByteStream.from_dict(d)
assert b2.data == b.data
assert b2.mime_type == b.mime_type
assert b2.meta == b.meta
assert str(b2) == str(b)
def test_no_warning_on_init():
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ByteStream(data=b"hello", mime_type="text/plain", meta={"k": "v"})
def test_warn_on_inplace_mutation():
b = ByteStream(data=b"hello")
with pytest.warns(Warning, match="dataclasses.replace"):
b.data = b"world"
File diff suppressed because it is too large Load Diff
+412
View File
@@ -0,0 +1,412 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
from copy import deepcopy
from dataclasses import replace
import pytest
from haystack import Document
from haystack.dataclasses.byte_stream import ByteStream
from haystack.dataclasses.sparse_embedding import SparseEmbedding
@pytest.mark.parametrize(
"doc,doc_str",
[
(Document(content="test text"), "content: 'test text'"),
(Document(blob=ByteStream(b"hello, test string")), "blob: 18 bytes"),
(Document(content="test text", blob=ByteStream(b"hello, test string")), "content: 'test text', blob: 18 bytes"),
],
)
def test_document_str(doc, doc_str):
assert f"Document(id={doc.id}, {doc_str})" == str(doc)
def test_init():
doc = Document()
assert doc.id == "d4675c57fcfe114db0b95f1da46eea3c5d6f5729c17d01fb5251ae19830a3455"
assert doc.content is None
assert doc.blob is None
assert doc.meta == {}
assert doc.score is None
assert doc.embedding is None
assert doc.sparse_embedding is None
def test_init_with_wrong_parameters():
with pytest.raises(TypeError):
Document(text="") # type: ignore[call-arg]
def test_init_with_parameters():
blob_data = b"some bytes"
sparse_embedding = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
doc = Document(
content="test text",
blob=ByteStream(data=blob_data, mime_type="text/markdown"),
meta={"text": "test text"},
score=0.812,
embedding=[0.1, 0.2, 0.3],
sparse_embedding=sparse_embedding,
)
assert doc.id == "c31efd4986b1f2424e5058482c6f668ccad2309043c2346524cd81d255e159fe"
assert doc.content == "test text"
assert doc.blob is not None
assert doc.blob.data == blob_data
assert doc.blob.mime_type == "text/markdown"
assert doc.meta == {"text": "test text"}
assert doc.score == 0.812
assert doc.embedding == [0.1, 0.2, 0.3]
assert doc.sparse_embedding == sparse_embedding
def test_init_with_legacy_fields():
doc = Document(
content="test text",
content_type="text",
id_hash_keys=["content"],
dataframe="placeholder",
score=0.812,
embedding=[0.1, 0.2, 0.3], # type: ignore
)
assert doc.id == "18fc2c114825872321cf5009827ca162f54d3be50ab9e9ffa027824b6ec223af"
assert doc.content == "test text"
assert doc.blob is None
assert doc.meta == {}
assert doc.score == 0.812
assert doc.embedding == [0.1, 0.2, 0.3]
assert doc.sparse_embedding is None
assert doc.content_type == "text" # this is a property now
assert not hasattr(doc, "id_hash_keys")
assert not hasattr(doc, "dataframe")
def test_init_with_legacy_field():
doc = Document(
content="test text",
content_type="text", # type: ignore
id_hash_keys=["content"],
score=0.812,
embedding=[0.1, 0.2, 0.3],
meta={"date": "10-10-2023", "type": "article"},
)
assert doc.id == "dcd4914f727544e89ce8082f6f2e298d244dd0803a4dc167f19d24e7d43b28ac"
assert doc.content == "test text"
assert doc.meta == {"date": "10-10-2023", "type": "article"}
assert doc.score == 0.812
assert doc.embedding == [0.1, 0.2, 0.3]
assert doc.sparse_embedding is None
assert doc.content_type == "text" # this is a property now
assert not hasattr(doc, "id_hash_keys")
def test_basic_equality_type_mismatch():
doc = Document(content="test text")
assert doc != "test text"
def test_basic_equality_id():
doc1 = Document(content="test text")
doc2 = Document(content="test text")
assert doc1 == doc2
doc1 = replace(doc1, id="1234")
doc2 = replace(doc2, id="5678")
assert doc1 != doc2
def test_id_is_independent_of_meta_key_order():
doc1 = Document(content="hello", meta={"a": 1, "b": 2})
doc2 = Document(content="hello", meta={"b": 2, "a": 1})
assert doc1.meta == doc2.meta
assert doc1.id == doc2.id
def test_id_is_independent_of_nested_meta_key_order():
doc1 = Document(content="hello", meta={"outer": {"a": 1, "b": 2}})
doc2 = Document(content="hello", meta={"outer": {"b": 2, "a": 1}})
assert doc1.id == doc2.id
def test_to_dict():
doc = Document()
assert doc.to_dict() == {
"id": doc._create_id(),
"content": None,
"blob": None,
"score": None,
"embedding": None,
"sparse_embedding": None,
}
def test_to_dict_without_flattening():
doc = Document()
assert doc.to_dict(flatten=False) == {
"id": doc._create_id(),
"content": None,
"blob": None,
"meta": {},
"score": None,
"embedding": None,
"sparse_embedding": None,
}
def test_to_dict_with_custom_parameters():
doc = Document(
content="test text",
blob=ByteStream(b"some bytes", mime_type="application/pdf", meta={"foo": "bar"}),
meta={"some": "values", "test": 10},
score=0.99,
embedding=[10.0, 10.0],
sparse_embedding=SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3]),
)
assert doc.to_dict() == {
"id": doc.id,
"content": "test text",
"blob": {"data": list(b"some bytes"), "mime_type": "application/pdf", "meta": {"foo": "bar"}},
"some": "values",
"test": 10,
"score": 0.99,
"embedding": [10.0, 10.0],
"sparse_embedding": {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]},
}
def test_to_dict_with_custom_parameters_without_flattening():
doc = Document(
content="test text",
blob=ByteStream(b"some bytes", mime_type="application/pdf"),
meta={"some": "values", "test": 10},
score=0.99,
embedding=[10.0, 10.0],
sparse_embedding=SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3]),
)
assert doc.to_dict(flatten=False) == {
"id": doc.id,
"content": "test text",
"blob": {"data": list(b"some bytes"), "mime_type": "application/pdf", "meta": {}},
"meta": {"some": "values", "test": 10},
"score": 0.99,
"embedding": [10.0, 10.0],
"sparse_embedding": {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]},
}
def test_to_dict_field_precedence():
"""
Test for Document.to_dict() with flatten=True.
Test that Document's first-level fields take precedence over meta fields when flattening the dictionary
representation.
"""
doc = Document(content="from-content", score=0.9, meta={"content": "from-meta", "score": 0.5, "source": "web"})
flat_dict = doc.to_dict(flatten=True)
# First-level fields should take precedence
assert flat_dict["content"] == "from-content"
assert flat_dict["score"] == 0.9
# Meta-only fields should be preserved
assert flat_dict["source"] == "web"
def test_from_dict():
assert Document.from_dict({}) == Document()
def test_from_dict_with_parameters():
blob_data = b"some bytes"
assert Document.from_dict(
{
"content": "test text",
"blob": {"data": list(blob_data), "mime_type": "text/markdown", "meta": {"text": "test text"}},
"meta": {"text": "test text"},
"score": 0.812,
"embedding": [0.1, 0.2, 0.3],
"sparse_embedding": {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]},
}
) == Document(
content="test text",
blob=ByteStream(blob_data, mime_type="text/markdown", meta={"text": "test text"}),
meta={"text": "test text"},
score=0.812,
embedding=[0.1, 0.2, 0.3],
sparse_embedding=SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3]),
)
def test_from_dict_does_not_mutate_input():
blob_data = b"some bytes"
data = {
"content": "test text",
"blob": {"data": list(blob_data), "mime_type": "text/markdown"},
"score": 0.812,
"embedding": [0.1, 0.2, 0.3],
"sparse_embedding": {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]},
"date": "10-10-2023",
"type": "article",
}
original_data = deepcopy(data)
assert Document.from_dict(data) == Document(
content="test text",
blob=ByteStream(blob_data, mime_type="text/markdown"),
score=0.812,
embedding=[0.1, 0.2, 0.3],
sparse_embedding=SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3]),
meta={"date": "10-10-2023", "type": "article"},
)
assert data == original_data
def test_from_dict_does_not_mutate_input_with_explicit_meta():
data = {"content": "test text", "meta": {"date": "10-10-2023", "type": "article"}, "score": 0.812}
original_data = deepcopy(data)
assert Document.from_dict(data) == Document(
content="test text", meta={"date": "10-10-2023", "type": "article"}, score=0.812
)
assert data == original_data
def test_from_dict_with_legacy_fields():
assert Document.from_dict(
{
"content": "test text",
"content_type": "text",
"id_hash_keys": ["content"],
"score": 0.812,
"embedding": [0.1, 0.2, 0.3],
}
) == Document(
content="test text",
content_type="text",
id_hash_keys=["content"],
score=0.812,
embedding=[0.1, 0.2, 0.3], # type: ignore
)
def test_from_dict_with_legacy_field_and_flat_meta():
assert Document.from_dict(
{
"content": "test text",
"content_type": "text",
"id_hash_keys": ["content"],
"score": 0.812,
"embedding": [0.1, 0.2, 0.3],
"date": "10-10-2023",
"type": "article",
}
) == Document(
content="test text",
content_type="text", # type: ignore
id_hash_keys=["content"],
score=0.812,
embedding=[0.1, 0.2, 0.3],
meta={"date": "10-10-2023", "type": "article"},
)
def test_from_dict_with_flat_meta():
blob_data = b"some bytes"
assert Document.from_dict(
{
"content": "test text",
"blob": {"data": list(blob_data), "mime_type": "text/markdown"},
"score": 0.812,
"embedding": [0.1, 0.2, 0.3],
"sparse_embedding": {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]},
"date": "10-10-2023",
"type": "article",
}
) == Document(
content="test text",
blob=ByteStream(blob_data, mime_type="text/markdown"),
score=0.812,
embedding=[0.1, 0.2, 0.3],
sparse_embedding=SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3]),
meta={"date": "10-10-2023", "type": "article"},
)
def test_from_dict_with_flat_and_non_flat_meta():
with pytest.raises(ValueError, match="Pass either the 'meta' parameter or flattened metadata keys"):
Document.from_dict(
{
"content": "test text",
"blob": {"data": list(b"some bytes"), "mime_type": "text/markdown"},
"score": 0.812,
"meta": {"test": 10},
"embedding": [0.1, 0.2, 0.3],
"date": "10-10-2023",
"type": "article",
}
)
def test_from_dict_with_dataframe():
"""
Test for legacy support of Document.from_dict() with dataframe field.
Test that Document.from_dict() can properly deserialize a Document dictionary obtained with
document.to_dict(flatten=False) in haystack-ai<=2.10.0.
We make sure that Document.from_dict() does not raise an error and that dataframe is skipped (legacy field).
"""
# Document dictionary obtained with document.to_dict(flatten=False) in haystack-ai<=2.10.0
doc_dict = {
"id": "my_id",
"content": "my_content",
"dataframe": None,
"blob": None,
"meta": {"key": "value"},
"score": None,
"embedding": None,
"sparse_embedding": None,
}
doc = Document.from_dict(doc_dict)
assert doc.id == "my_id"
assert doc.content == "my_content"
assert doc.meta == {"key": "value"}
assert doc.score is None
assert doc.embedding is None
assert doc.sparse_embedding is None
assert not hasattr(doc, "dataframe")
def test_content_type():
assert Document(content="text").content_type == "text"
with pytest.raises(ValueError):
_ = Document().content_type
def test_no_warning_on_init():
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
Document(content="test")
def test_warn_on_inplace_mutation():
doc = Document(content="test")
with pytest.warns(Warning, match="dataclasses.replace"):
doc.content = "other"
+177
View File
@@ -0,0 +1,177 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import base64
import logging
import warnings
from pathlib import Path
from unittest.mock import Mock, patch
import httpx
import pytest
from haystack.dataclasses.file_content import FileContent
def test_file_content_init(base64_pdf_string):
file_content = FileContent(
base64_data=base64_pdf_string, mime_type="application/pdf", filename="test.pdf", extra={"key": "value"}
)
assert file_content.base64_data == base64_pdf_string
assert file_content.mime_type == "application/pdf"
assert file_content.filename == "test.pdf"
assert file_content.extra == {"key": "value"}
assert file_content.validation
def test_file_content_to_dict(base64_pdf_string):
file_content = FileContent(
base64_data=base64_pdf_string, mime_type="application/pdf", filename="test.pdf", extra={"key": "value"}
)
assert file_content.to_dict() == {
"base64_data": base64_pdf_string,
"mime_type": "application/pdf",
"filename": "test.pdf",
"extra": {"key": "value"},
"validation": True,
}
def test_file_content_from_dict(base64_pdf_string):
file_content = FileContent.from_dict(
{
"base64_data": base64_pdf_string,
"mime_type": "application/pdf",
"filename": "test.pdf",
"extra": {"key": "value"},
"validation": False,
}
)
assert file_content.base64_data == base64_pdf_string
assert file_content.mime_type == "application/pdf"
assert file_content.filename == "test.pdf"
assert file_content.extra == {"key": "value"}
assert not file_content.validation
def test_file_content_init_with_invalid_base64_string():
with pytest.raises(ValueError):
FileContent(base64_data="invalid_base64_string")
def test_file_content_init_with_invalid_base64_string_and_validation_false():
file_content = FileContent(base64_data="invalid_base64_string", validation=False)
assert file_content.base64_data == "invalid_base64_string"
assert file_content.mime_type is None
assert file_content.filename is None
assert file_content.extra == {}
assert not file_content.validation
def test_file_content_mime_type_guessing(test_files_path):
with open(test_files_path / "pdf" / "sample_pdf_3.pdf", "rb") as f:
base64_data = base64.b64encode(f.read()).decode("utf-8")
file_content = FileContent(base64_data=base64_data)
assert file_content.mime_type == "application/pdf"
# do not guess mime type if mime type is provided
file_content = FileContent(base64_data=base64_data, mime_type="application/octet-stream")
assert file_content.mime_type == "application/octet-stream"
def test_file_content_mime_type_guessing_warning(caplog):
# A valid base64 string but with content that filetype cannot identify
plain_text = base64.b64encode(b"just some plain text content").decode("utf-8")
with caplog.at_level(logging.WARNING):
file_content = FileContent(base64_data=plain_text)
assert file_content.mime_type is None
assert "Failed to guess the MIME type" in caplog.text
def test_file_content_repr(base64_pdf_string):
file_content = FileContent(base64_data=base64_pdf_string, mime_type="application/pdf", validation=False)
repr_str = repr(file_content)
assert "FileContent(" in repr_str
assert "mime_type='application/pdf'" in repr_str
# base64_data should be truncated
assert "..." in repr_str
def test_file_content_from_file_path(test_files_path):
str_path = Path(test_files_path / "pdf" / "sample_pdf_3.pdf").as_posix()
file_content = FileContent.from_file_path(file_path=str_path, filename="custom.pdf", extra={"test": "test"})
assert isinstance(file_content.base64_data, str)
assert file_content.mime_type == "application/pdf"
assert file_content.filename == "custom.pdf"
assert file_content.extra == {"test": "test"}
def test_file_content_from_file_path_default_filename(test_files_path):
file_content = FileContent.from_file_path(file_path=test_files_path / "pdf" / "sample_pdf_3.pdf")
assert isinstance(file_content.base64_data, str)
assert file_content.mime_type == "application/pdf"
assert file_content.filename == "sample_pdf_3.pdf"
assert file_content.extra == {}
def test_file_content_from_url(test_files_path):
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
with open(test_files_path / "pdf" / "sample_pdf_3.pdf", "rb") as f:
pdf_bytes = f.read()
mock_response = Mock(status_code=200, content=pdf_bytes, headers={"Content-Type": "application/pdf"})
mock_get.return_value = mock_response
file_content = FileContent.from_url(
url="https://example.com/sample.pdf", filename="custom.pdf", extra={"test": "test"}
)
assert isinstance(file_content.base64_data, str)
assert file_content.mime_type == "application/pdf"
assert file_content.filename == "custom.pdf"
assert file_content.extra == {"test": "test"}
def test_file_content_from_url_default_filename(test_files_path):
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
with open(test_files_path / "pdf" / "sample_pdf_3.pdf", "rb") as f:
pdf_bytes = f.read()
mock_response = Mock(status_code=200, content=pdf_bytes, headers={"Content-Type": "application/pdf"})
mock_get.return_value = mock_response
file_content = FileContent.from_url(url="https://example.com/documents/sample.pdf")
assert file_content.filename == "sample.pdf"
def test_file_content_from_url_bad_request():
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
mock_get.side_effect = httpx.HTTPStatusError("403 Client Error", request=Mock(), response=Mock())
with pytest.raises(httpx.HTTPStatusError):
FileContent.from_url(url="https://non_existent_website_dot.com/file.pdf", retry_attempts=0, timeout=1)
def test_file_content_no_warning_on_init(base64_pdf_string):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
FileContent(base64_data=base64_pdf_string, mime_type="application/pdf")
def test_file_content_warn_on_inplace_mutation():
fc = FileContent(base64_data="dGVzdA==", mime_type="text/plain", validation=False)
with pytest.warns(Warning, match="dataclasses.replace"):
fc.mime_type = "application/pdf"
def test_file_content_to_trace_dict(base64_pdf_string):
file_content = FileContent(
base64_data=base64_pdf_string, mime_type="application/pdf", filename="test.pdf", extra={"key": "value"}
)
data = file_content._to_trace_dict()
assert data["base64_data"] == f"Base64 string ({len(base64_pdf_string)} characters)"
assert data["mime_type"] == "application/pdf"
assert data["filename"] == "test.pdf"
assert data["extra"] == {"key": "value"}
+248
View File
@@ -0,0 +1,248 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import base64
import logging
import warnings
from unittest.mock import Mock, patch
import httpx
import pytest
from PIL import Image
from haystack.dataclasses.image_content import ImageContent
def test_image_content_init(base64_image_string):
image_content = ImageContent(
base64_image=base64_image_string, mime_type="image/png", detail="auto", meta={"key": "value"}
)
assert image_content.base64_image == base64_image_string
assert image_content.mime_type == "image/png"
assert image_content.detail == "auto"
assert image_content.meta == {"key": "value"}
assert image_content.validation
def test_image_content_to_dict(base64_image_string):
image_content = ImageContent(
base64_image=base64_image_string, mime_type="image/png", detail="auto", meta={"key": "value"}
)
assert image_content.to_dict() == {
"base64_image": base64_image_string,
"mime_type": "image/png",
"detail": "auto",
"meta": {"key": "value"},
"validation": True,
}
def test_image_content_from_dict(base64_image_string):
image_content = ImageContent.from_dict(
{
"base64_image": base64_image_string,
"mime_type": "image/png",
"detail": "auto",
"meta": {"key": "value"},
"validation": False,
}
)
assert image_content.base64_image == base64_image_string
assert image_content.mime_type == "image/png"
assert image_content.detail == "auto"
assert image_content.meta == {"key": "value"}
assert not image_content.validation
def test_image_content_init_with_invalid_base64_string():
with pytest.raises(ValueError):
ImageContent(base64_image="invalid_base64_string")
def test_image_content_init_with_invalid_base64_string_and_validation_false():
image_content = ImageContent(base64_image="invalid_base64_string", validation=False)
assert image_content.base64_image == "invalid_base64_string"
assert image_content.mime_type is None
assert image_content.detail is None
assert image_content.meta == {}
assert not image_content.validation
def test_image_content_init_with_invalid_mime_type(test_files_path, base64_image_string):
with pytest.raises(ValueError):
ImageContent(base64_image=base64_image_string, mime_type="text/xml")
with open(test_files_path / "docx" / "sample_docx.docx", "rb") as docx_file:
docx_base64 = base64.b64encode(docx_file.read()).decode("utf-8")
with pytest.raises(ValueError):
ImageContent(base64_image=docx_base64)
def test_image_content_init_with_invalid_mime_type_and_validation_false(test_files_path, base64_image_string):
image_content = ImageContent(base64_image=base64_image_string, mime_type="text/xml", validation=False)
assert image_content.base64_image == base64_image_string
assert image_content.mime_type == "text/xml"
assert image_content.detail is None
assert image_content.meta == {}
assert not image_content.validation
with open(test_files_path / "docx" / "sample_docx.docx", "rb") as docx_file:
docx_base64 = base64.b64encode(docx_file.read()).decode("utf-8")
image_content = ImageContent(base64_image=docx_base64, validation=False)
assert image_content.base64_image == docx_base64
assert image_content.mime_type is None
assert image_content.detail is None
assert image_content.meta == {}
assert not image_content.validation
def test_image_content_mime_type_guessing(test_files_path):
image_path = test_files_path / "images" / "apple.jpg"
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
image_content = ImageContent(base64_image=base64_image)
assert image_content.mime_type == "image/jpeg"
# do not guess mime type if mime type is provided
image_content = ImageContent(base64_image=base64_image, mime_type="image/png")
assert image_content.mime_type == "image/png"
def test_image_content_show_in_jupyter(test_files_path):
image_path = test_files_path / "images" / "apple.jpg"
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
image_content = ImageContent(base64_image=base64_image)
with (
patch("haystack.dataclasses.image_content.is_in_jupyter", return_value=True),
patch("IPython.display.display") as mock_display,
):
image_content.show()
mock_display.assert_called_once()
displayed_image = mock_display.call_args[0][0]
assert isinstance(displayed_image, Image.Image)
def test_image_content_show_outside_jupyter(test_files_path):
image_path = test_files_path / "images" / "apple.jpg"
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
image_content = ImageContent(base64_image=base64_image)
# mocking is_in_jupyter is not needed because we don't test in a Jupyter notebook
with patch.object(Image.Image, "show") as mock_show:
image_content.show()
mock_show.assert_called_once()
def test_image_content_from_file_path(test_files_path):
image_content = ImageContent.from_file_path(
file_path=test_files_path / "images" / "apple.jpg", size=(100, 100), detail="high", meta={"test": "test"}
)
assert isinstance(image_content.base64_image, str)
assert image_content.mime_type == "image/jpeg"
assert image_content.detail == "high"
assert image_content.meta == {"test": "test", "file_path": str(test_files_path / "images" / "apple.jpg")}
def test_image_content_from_file_path_pdf_unsupported(test_files_path, caplog):
with pytest.raises(IndexError):
ImageContent.from_file_path(
file_path=test_files_path / "pdf" / "sample_pdf_1.pdf",
size=(100, 100),
detail="high",
meta={"test": "test"},
)
assert "Could not convert file" in caplog.text
assert "PDF" in caplog.text
def test_image_content_from_file_path_non_existing(test_files_path, caplog):
caplog.set_level(logging.WARNING)
with pytest.raises(IndexError):
ImageContent.from_file_path(file_path=test_files_path / "images" / "non_existing.jpg")
assert "No such file" in caplog.text
def test_image_content_from_url(test_files_path):
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
with open(test_files_path / "images" / "apple.jpg", "rb") as image_file:
image_bytes = image_file.read()
mock_response = Mock(status_code=200, content=image_bytes, headers={"Content-Type": "image/jpeg"})
mock_get.return_value = mock_response
image_content = ImageContent.from_url(
url="https://example.com/apple.jpg", size=(100, 100), detail="high", meta={"test": "test"}
)
assert isinstance(image_content.base64_image, str)
assert image_content.mime_type == "image/jpeg"
assert image_content.detail == "high"
assert image_content.meta == {"test": "test", "url": "https://example.com/apple.jpg", "content_type": "image/jpeg"}
def test_image_content_from_url_bad_request():
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
mock_get.side_effect = httpx.HTTPStatusError("403 Client Error", request=Mock(), response=Mock())
with pytest.raises(httpx.HTTPStatusError):
ImageContent.from_url(url="https://non_existent_website_dot.com/image.jpg", retry_attempts=0, timeout=1)
def test_image_content_from_url_wrong_mime_type_text():
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
mock_response = Mock(status_code=200, text="a text", headers={"Content-Type": "text/plain"})
mock_get.return_value = mock_response
with pytest.raises(ValueError):
ImageContent.from_url(
url="https://example.com/text.txt", size=(100, 100), detail="high", meta={"test": "test"}
)
def test_image_content_from_url_wrong_mime_type_pdf(test_files_path):
with patch("haystack.components.fetchers.link_content.httpx.Client.get") as mock_get:
with open(test_files_path / "pdf" / "sample_pdf_1.pdf", "rb") as pdf_file:
pdf_bytes = pdf_file.read()
mock_response = Mock(status_code=200, content=pdf_bytes, headers={"Content-Type": "application/pdf"})
mock_get.return_value = mock_response
with pytest.raises(ValueError):
ImageContent.from_url(
url="https://example.com/sample_pdf_1.pdf", size=(100, 100), detail="high", meta={"test": "test"}
)
@pytest.mark.integration
def test_image_content_from_url_wrong_mime_type():
with pytest.raises(ValueError):
ImageContent.from_url(url="https://www.google.com/", size=(100, 100), detail="high", meta={"test": "test"})
def test_image_content_no_warning_on_init(base64_image_string):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ImageContent(base64_image=base64_image_string, mime_type="image/png")
def test_image_content_warn_on_inplace_mutation(base64_image_string):
ic = ImageContent(base64_image=base64_image_string, mime_type="image/png")
with pytest.warns(Warning, match="dataclasses.replace"):
ic.detail = "high"
def test_image_content_to_trace_dict(base64_image_string):
image_content = ImageContent(
base64_image=base64_image_string, mime_type="image/png", detail="auto", meta={"key": "value"}
)
data = image_content._to_trace_dict()
assert data["base64_image"] == f"Base64 string ({len(base64_image_string)} characters)"
assert data["mime_type"] == "image/png"
assert data["detail"] == "auto"
assert data["meta"] == {"key": "value"}
+47
View File
@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
import pytest
from haystack.dataclasses.sparse_embedding import SparseEmbedding
class TestSparseEmbedding:
def test_init(self):
se = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
assert se.indices == [0, 2, 4]
assert se.values == [0.1, 0.2, 0.3]
def test_init_with_wrong_parameters(self):
with pytest.raises(ValueError):
SparseEmbedding(indices=[0, 2], values=[0.1, 0.2, 0.3, 0.4])
def test_to_dict(self):
se = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
assert se.to_dict() == {"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]}
def test_from_dict(self):
se = SparseEmbedding.from_dict({"indices": [0, 2, 4], "values": [0.1, 0.2, 0.3]})
assert se.indices == [0, 2, 4]
assert se.values == [0.1, 0.2, 0.3]
def test_eq(self):
se1 = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
se2 = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
assert se1 == se2
se3 = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.4])
assert se1 != se3
def test_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
def test_warn_on_inplace_mutation(self):
se = SparseEmbedding(indices=[0, 2, 4], values=[0.1, 0.2, 0.3])
with pytest.warns(Warning, match="dataclasses.replace"):
se.indices = [1, 3, 5]
+422
View File
@@ -0,0 +1,422 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import logging
import warnings
import pytest
from haystack import Pipeline, component
from haystack.dataclasses import (
ComponentInfo,
ReasoningContent,
StreamingChunk,
ToolCall,
ToolCallDelta,
ToolCallResult,
)
from haystack.dataclasses.streaming_chunk import FinishReason, _invoke_streaming_callback, select_streaming_callback
@component
class ExampleComponent:
def __init__(self):
self.name = "test_component"
def run(self) -> dict[str, str]:
return {"test": "Test content"}
class TestStreamingChunk:
def test_create_chunk_with_content_and_metadata(self):
chunk = StreamingChunk(content="Test content", meta={"key": "value"})
assert chunk.content == "Test content"
assert chunk.meta == {"key": "value"}
def test_create_chunk_with_only_content(self):
chunk = StreamingChunk(content="Test content")
assert chunk.content == "Test content"
assert chunk.meta == {}
def test_access_content(self):
chunk = StreamingChunk(content="Test content", meta={"key": "value"})
assert chunk.content == "Test content"
def test_create_chunk_with_empty_content(self):
chunk = StreamingChunk(content="")
assert chunk.content == ""
assert chunk.meta == {}
def test_create_chunk_with_all_fields(self):
component_info = ComponentInfo(type="test.component", name="test_component")
chunk = StreamingChunk(content="Test content", meta={"key": "value"}, component_info=component_info)
assert chunk.content == "Test content"
assert chunk.meta == {"key": "value"}
assert chunk.component_info == component_info
def test_create_chunk_with_content_and_tool_call(self):
with pytest.raises(ValueError):
# Can't have content + tool_call at the same time
StreamingChunk(
content="Test content",
meta={"key": "value"},
tool_calls=[ToolCallDelta(id="123", tool_name="test_tool", arguments='{"arg1": "value1"}', index=0)],
)
def test_create_chunk_with_content_and_tool_call_result(self):
with pytest.raises(ValueError):
# Can't have content + tool_call_result at the same time
StreamingChunk(
content="Test content",
meta={"key": "value"},
tool_call_result=ToolCallResult(
result="output",
origin=ToolCall(id="123", tool_name="test_tool", arguments={"arg1": "value1"}),
error=False,
),
)
def test_create_chunk_with_content_and_reasoning(self):
with pytest.raises(ValueError, match="Only one of `content`, `tool_call`, `tool_call_result`"):
StreamingChunk(
content="Test content", meta={"key": "value"}, reasoning=ReasoningContent(reasoning_text="thinking")
)
def test_reasoning_and_no_index(self):
with pytest.raises(
ValueError, match="If `tool_call`, `tool_call_result` or `reasoning` is set, `index` must also be set."
):
StreamingChunk(content="", meta={"key": "value"}, reasoning=ReasoningContent(reasoning_text="thinking"))
def test_component_info_from_component(self):
component_info = ComponentInfo.from_component(ExampleComponent())
assert component_info.type == "test_streaming_chunk.ExampleComponent"
def test_component_info_from_component_with_name_from_pipeline(self):
pipeline = Pipeline()
comp = ExampleComponent()
pipeline.add_component("pipeline_component", comp)
component_info = ComponentInfo.from_component(comp)
assert component_info.type == "test_streaming_chunk.ExampleComponent"
assert component_info.name == "pipeline_component"
def test_tool_call_delta(self):
tool_call = ToolCallDelta(id="123", tool_name="test_tool", arguments='{"arg1": "value1"}', index=0)
assert tool_call.id == "123"
assert tool_call.tool_name == "test_tool"
assert tool_call.arguments == '{"arg1": "value1"}'
assert tool_call.index == 0
def test_create_chunk_with_finish_reason(self):
"""Test creating a chunk with the new finish_reason field."""
chunk = StreamingChunk(content="Test content", finish_reason="stop")
assert chunk.content == "Test content"
assert chunk.finish_reason == "stop"
assert chunk.meta == {}
def test_create_chunk_with_finish_reason_and_meta(self):
"""Test creating a chunk with both finish_reason field and meta."""
chunk = StreamingChunk(
content="Test content", finish_reason="stop", meta={"model": "gpt-4", "usage": {"tokens": 10}}
)
assert chunk.content == "Test content"
assert chunk.finish_reason == "stop"
assert chunk.meta["model"] == "gpt-4"
assert chunk.meta["usage"]["tokens"] == 10
def test_finish_reason_standard_values(self):
"""Test all standard finish_reason values including the new Haystack-specific ones."""
standard_values: list[FinishReason] = ["stop", "length", "tool_calls", "content_filter", "tool_call_results"]
for value in standard_values:
chunk = StreamingChunk(content="Test content", finish_reason=value)
assert chunk.finish_reason == value
def test_finish_reason_tool_call_results(self):
"""Test specifically the new tool_call_results finish reason."""
chunk = StreamingChunk(
content="", finish_reason="tool_call_results", meta={"finish_reason": "tool_call_results"}
)
assert chunk.finish_reason == "tool_call_results"
assert chunk.meta["finish_reason"] == "tool_call_results"
assert chunk.content == ""
def test_to_dict_tool_call_result(self):
"""Test the to_dict method for StreamingChunk with tool_call_result."""
component_info = ComponentInfo.from_component(ExampleComponent())
tool_call_result = ToolCallResult(
result="output", origin=ToolCall(id="123", tool_name="test_tool", arguments={"arg1": "value1"}), error=False
)
chunk = StreamingChunk(
content="",
meta={"key": "value"},
index=0,
component_info=component_info,
tool_call_result=tool_call_result,
finish_reason="tool_call_results",
)
d = chunk.to_dict()
assert d["content"] == ""
assert d["meta"] == {"key": "value"}
assert d["index"] == 0
assert d["component_info"]["type"] == "test_streaming_chunk.ExampleComponent"
assert d["tool_call_result"]["result"] == "output"
assert d["tool_call_result"]["error"] is False
assert d["tool_call_result"]["origin"]["id"] == "123"
assert d["tool_call_result"]["origin"]["arguments"]["arg1"] == "value1"
assert d["finish_reason"] == "tool_call_results"
assert d["reasoning"] is None
def test_to_dict_tool_calls(self):
"""Test the to_dict method for StreamingChunk with tool_calls."""
component_info = ComponentInfo.from_component(ExampleComponent())
tool_calls = [
ToolCallDelta(id="123", tool_name="test_tool", arguments='{"arg1": "value1"}', index=0),
ToolCallDelta(id="456", tool_name="another_tool", arguments='{"arg2": "value2"}', index=1),
]
chunk = StreamingChunk(
content="",
meta={"key": "value"},
index=0,
component_info=component_info,
tool_calls=tool_calls,
finish_reason="tool_calls",
)
d = chunk.to_dict()
assert d["content"] == ""
assert d["meta"] == {"key": "value"}
assert d["index"] == 0
assert d["component_info"]["type"] == "test_streaming_chunk.ExampleComponent"
assert len(d["tool_calls"]) == 2
assert d["tool_calls"][0]["id"] == "123"
assert d["tool_calls"][0]["index"] == 0
assert d["tool_calls"][1]["id"] == "456"
assert d["tool_calls"][1]["index"] == 1
assert d["finish_reason"] == "tool_calls"
assert d["reasoning"] is None
def test_to_dict_reasoning(self):
"""Test the to_dict method for StreamingChunk with reasoning."""
component_info = ComponentInfo.from_component(ExampleComponent())
reasoning = ReasoningContent(reasoning_text="thinking", extra={"step": 1})
chunk = StreamingChunk(
content="",
meta={"key": "value"},
index=0,
component_info=component_info,
reasoning=reasoning,
finish_reason="stop",
)
d = chunk.to_dict()
assert d["content"] == ""
assert d["meta"] == {"key": "value"}
assert d["index"] == 0
assert d["component_info"]["type"] == "test_streaming_chunk.ExampleComponent"
assert d["reasoning"]["reasoning_text"] == "thinking"
assert d["reasoning"]["extra"]["step"] == 1
assert d["finish_reason"] == "stop"
assert d["tool_calls"] is None
assert d["tool_call_result"] is None
def test_from_dict_tool_call_result(self):
"""Test the from_dict method for StreamingChunk with tool_call_result."""
component_info = {"type": "test_streaming_chunk.ExampleComponent", "name": "test_component"}
tool_call_result = {
"result": "output",
"origin": {"id": "123", "tool_name": "test_tool", "arguments": {"arg1": "value1"}},
"error": False,
}
data = {
"content": "",
"meta": {"key": "value"},
"index": 0,
"component_info": component_info,
"tool_call_result": tool_call_result,
"finish_reason": "tool_call_results",
}
chunk = StreamingChunk.from_dict(data)
assert chunk.content == ""
assert chunk.meta == {"key": "value"}
assert chunk.index == 0
assert chunk.component_info is not None
assert chunk.component_info.type == "test_streaming_chunk.ExampleComponent"
assert chunk.component_info.name == "test_component"
assert chunk.tool_call_result is not None
assert chunk.tool_call_result.result == "output"
assert chunk.tool_call_result.error is False
assert chunk.tool_call_result.origin.id == "123"
assert chunk.reasoning is None
def test_from_dict_tool_calls(self):
"""Test the from_dict method for StreamingChunk with tool_calls."""
component_info = {"type": "test_streaming_chunk.ExampleComponent", "name": "test_component"}
tool_calls = [{"id": "123", "tool_name": "test_tool", "arguments": '{"arg1": "value1"}', "index": 0}]
data = {
"content": "",
"meta": {"key": "value"},
"index": 0,
"component_info": component_info,
"tool_calls": tool_calls,
"finish_reason": "tool_calls",
}
chunk = StreamingChunk.from_dict(data)
assert chunk.content == ""
assert chunk.meta == {"key": "value"}
assert chunk.index == 0
assert chunk.component_info is not None
assert chunk.component_info.type == "test_streaming_chunk.ExampleComponent"
assert chunk.component_info.name == "test_component"
assert chunk.tool_calls is not None
assert chunk.tool_calls[0].tool_name == "test_tool"
assert chunk.tool_calls[0].index == 0
assert chunk.finish_reason == "tool_calls"
assert chunk.reasoning is None
def test_from_dict_reasoning(self):
"""Test the from_dict method for StreamingChunk with reasoning."""
component_info = {"type": "test_streaming_chunk.ExampleComponent", "name": "test_component"}
reasoning = {"reasoning_text": "thinking", "extra": {"step": 1}}
data = {
"content": "",
"meta": {"key": "value"},
"index": 0,
"component_info": component_info,
"reasoning": reasoning,
"finish_reason": "stop",
}
chunk = StreamingChunk.from_dict(data)
assert chunk.content == ""
assert chunk.meta == {"key": "value"}
assert chunk.index == 0
assert chunk.component_info is not None
assert chunk.component_info.type == "test_streaming_chunk.ExampleComponent"
assert chunk.component_info.name == "test_component"
assert chunk.reasoning is not None
assert chunk.reasoning.reasoning_text == "thinking"
assert chunk.reasoning.extra["step"] == 1
assert chunk.finish_reason == "stop"
assert chunk.tool_calls is None
assert chunk.tool_call_result is None
def test_tool_call_delta_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ToolCallDelta(index=0, tool_name="t")
def test_tool_call_delta_warn_on_inplace_mutation(self):
tcd = ToolCallDelta(index=0, tool_name="t")
with pytest.warns(Warning, match="dataclasses.replace"):
tcd.tool_name = "other"
def test_component_info_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
ComponentInfo(type="test.component", name="my_component")
def test_component_info_warn_on_inplace_mutation(self):
ci = ComponentInfo(type="test.component", name="my_component")
with pytest.warns(Warning, match="dataclasses.replace"):
ci.name = "other"
def test_streaming_chunk_no_warning_on_init(self):
with warnings.catch_warnings():
warnings.simplefilter("error", Warning)
StreamingChunk(content="test")
def test_streaming_chunk_warn_on_inplace_mutation(self):
chunk = StreamingChunk(content="test")
with pytest.warns(Warning, match="dataclasses.replace"):
chunk.content = "other"
def sync_callback(chunk):
pass
async def async_callback(chunk):
pass
class TestSelectStreamingCallback:
def test_runtime_precedes_init(self):
selected = select_streaming_callback(sync_callback, async_callback, requires_async=True)
assert selected is async_callback
def test_runtime_only(self):
selected = select_streaming_callback(None, sync_callback, requires_async=False)
assert selected is sync_callback
def test_init_fallback_when_no_runtime(self):
selected = select_streaming_callback(async_callback, None, requires_async=True)
assert selected is async_callback
def test_both_none(self):
assert select_streaming_callback(None, None, requires_async=False) is None
assert select_streaming_callback(None, None, requires_async=True) is None
def test_async_callback_in_sync_context_raises(self):
with pytest.raises(ValueError, match="cannot be a coroutine"):
select_streaming_callback(None, async_callback, requires_async=False)
with pytest.raises(ValueError, match="cannot be a coroutine"):
select_streaming_callback(async_callback, None, requires_async=False)
def test_sync_callback_in_async_context_warns_but_returns(self, caplog):
with caplog.at_level(logging.WARNING):
selected = select_streaming_callback(None, sync_callback, requires_async=True)
assert selected is sync_callback
assert "sync streaming callback" in caplog.text
assert "runtime" in caplog.text
caplog.clear()
with caplog.at_level(logging.WARNING):
selected = select_streaming_callback(sync_callback, None, requires_async=True)
assert selected is sync_callback
assert "sync streaming callback" in caplog.text
assert "initialization" in caplog.text
class TestInvokeStreamingCallback:
async def test_invokes_sync_callback(self):
captured: list[StreamingChunk] = []
def callback(chunk):
captured.append(chunk)
chunk = StreamingChunk(content="hello")
await _invoke_streaming_callback(callback, chunk)
assert captured == [chunk]
async def test_invokes_async_callback(self):
captured: list[StreamingChunk] = []
async def callback(chunk):
captured.append(chunk)
chunk = StreamingChunk(content="world")
await _invoke_streaming_callback(callback, chunk)
assert captured == [chunk]