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
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:
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,130 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack import Document
|
||||
from haystack.components.joiners.answer_joiner import AnswerJoiner, JoinMode
|
||||
from haystack.dataclasses.answer import ExtractedAnswer, GeneratedAnswer
|
||||
|
||||
|
||||
class TestAnswerJoiner:
|
||||
def test_init(self):
|
||||
joiner = AnswerJoiner()
|
||||
assert joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert joiner.top_k is None
|
||||
assert joiner.sort_by_score is False
|
||||
|
||||
def test_init_with_custom_parameters(self):
|
||||
joiner = AnswerJoiner(join_mode="concatenate", top_k=5, sort_by_score=True)
|
||||
assert joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert joiner.top_k == 5
|
||||
assert joiner.sort_by_score is True
|
||||
|
||||
def test_to_dict(self):
|
||||
joiner = AnswerJoiner()
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.answer_joiner.AnswerJoiner",
|
||||
"init_parameters": {"join_mode": "concatenate", "top_k": None, "sort_by_score": False},
|
||||
}
|
||||
|
||||
def test_to_from_dict_custom_parameters(self):
|
||||
joiner = AnswerJoiner("concatenate", top_k=5, sort_by_score=True)
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.answer_joiner.AnswerJoiner",
|
||||
"init_parameters": {"join_mode": "concatenate", "top_k": 5, "sort_by_score": True},
|
||||
}
|
||||
|
||||
deserialized_joiner = AnswerJoiner.from_dict(data)
|
||||
assert deserialized_joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert deserialized_joiner.top_k == 5
|
||||
assert deserialized_joiner.sort_by_score is True
|
||||
|
||||
def test_from_dict(self):
|
||||
data = {"type": "haystack.components.joiners.answer_joiner.AnswerJoiner", "init_parameters": {}}
|
||||
answer_joiner = AnswerJoiner.from_dict(data)
|
||||
assert answer_joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert answer_joiner.top_k is None
|
||||
assert answer_joiner.sort_by_score is False
|
||||
|
||||
def test_from_dict_customs_parameters(self):
|
||||
data = {
|
||||
"type": "haystack.components.joiners.answer_joiner.AnswerJoiner",
|
||||
"init_parameters": {"join_mode": "concatenate", "top_k": 5, "sort_by_score": True},
|
||||
}
|
||||
answer_joiner = AnswerJoiner.from_dict(data)
|
||||
assert answer_joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert answer_joiner.top_k == 5
|
||||
assert answer_joiner.sort_by_score is True
|
||||
|
||||
def test_empty_list(self):
|
||||
joiner = AnswerJoiner()
|
||||
result = joiner.run([])
|
||||
assert result == {"answers": []}
|
||||
|
||||
def test_list_of_empty_lists(self):
|
||||
joiner = AnswerJoiner()
|
||||
result = joiner.run([[], []])
|
||||
assert result == {"answers": []}
|
||||
|
||||
def test_list_of_single_answer(self):
|
||||
joiner = AnswerJoiner()
|
||||
answers = [
|
||||
GeneratedAnswer(query="a", data="a", meta={}, documents=[Document(content="a")]),
|
||||
GeneratedAnswer(query="b", data="b", meta={}, documents=[Document(content="b")]),
|
||||
GeneratedAnswer(query="c", data="c", meta={}, documents=[Document(content="c")]),
|
||||
]
|
||||
result = joiner.run([answers])
|
||||
assert result == {"answers": answers}
|
||||
|
||||
def test_two_lists_of_generated_answers(self):
|
||||
joiner = AnswerJoiner()
|
||||
answers1 = [GeneratedAnswer(query="a", data="a", meta={}, documents=[Document(content="a")])]
|
||||
answers2 = [GeneratedAnswer(query="d", data="d", meta={}, documents=[Document(content="d")])]
|
||||
result = joiner.run([answers1, answers2])
|
||||
assert result == {"answers": answers1 + answers2}
|
||||
|
||||
def test_multiple_lists_of_mixed_answers(self):
|
||||
joiner = AnswerJoiner()
|
||||
answers1 = [GeneratedAnswer(query="a", data="a", meta={}, documents=[Document(content="a")])]
|
||||
answers2 = [ExtractedAnswer(query="d", score=0.9, meta={}, document=Document(content="d"))]
|
||||
answers3 = [GeneratedAnswer(query="f", data="f", meta={}, documents=[Document(content="f")])]
|
||||
all_answers = answers1 + answers2 + answers3 # type: ignore
|
||||
result = joiner.run([answers1, answers2, answers3])
|
||||
assert result == {"answers": all_answers}
|
||||
|
||||
def test_unsupported_join_mode(self):
|
||||
unsupported_mode = "unsupported_mode"
|
||||
with pytest.raises(ValueError):
|
||||
AnswerJoiner(join_mode=unsupported_mode)
|
||||
|
||||
def test_sort_by_score(self):
|
||||
joiner = AnswerJoiner(sort_by_score=True)
|
||||
answers1 = [ExtractedAnswer(query="a", score=0.3, meta={}, document=Document(content="a"))]
|
||||
answers2 = [ExtractedAnswer(query="b", score=0.9, meta={}, document=Document(content="b"))]
|
||||
result = joiner.run([answers1, answers2])
|
||||
scores = [answer.score for answer in result["answers"]]
|
||||
assert scores == [0.9, 0.3]
|
||||
|
||||
def test_sort_by_score_with_none_score(self):
|
||||
# The docstring promises that an answer with no score is handled as if its score is -infinity.
|
||||
# ExtractedAnswer with score=None must not raise a TypeError during sorting and must be sorted last.
|
||||
joiner = AnswerJoiner(sort_by_score=True)
|
||||
answers1 = [ExtractedAnswer(query="a", score=0.5, meta={}, document=Document(content="a"))]
|
||||
answers2 = [ExtractedAnswer(query="b", score=None, meta={}, document=Document(content="b"))] # type: ignore[arg-type]
|
||||
result = joiner.run([answers1, answers2])
|
||||
assert [answer.data for answer in result["answers"]] == [None, None]
|
||||
assert [answer.score for answer in result["answers"]] == [0.5, None]
|
||||
|
||||
def test_sort_by_score_with_answers_missing_score_attribute(self):
|
||||
# GeneratedAnswer has no score attribute at all; it must be handled as -infinity and sorted last.
|
||||
joiner = AnswerJoiner(sort_by_score=True)
|
||||
answers1 = [GeneratedAnswer(query="a", data="a", meta={}, documents=[Document(content="a")])]
|
||||
answers2 = [ExtractedAnswer(query="b", score=0.9, meta={}, document=Document(content="b"))]
|
||||
result = joiner.run([answers1, answers2])
|
||||
# The ExtractedAnswer (score 0.9) comes first, the GeneratedAnswer (no score) comes last.
|
||||
assert isinstance(result["answers"][0], ExtractedAnswer)
|
||||
assert isinstance(result["answers"][1], GeneratedAnswer)
|
||||
@@ -0,0 +1,36 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack.components.joiners import BranchJoiner
|
||||
|
||||
|
||||
class TestBranchJoiner:
|
||||
def test_one_value(self):
|
||||
joiner = BranchJoiner(int)
|
||||
output = joiner.run(value=[2])
|
||||
assert output == {"value": 2}
|
||||
|
||||
def test_one_value_of_wrong_type(self):
|
||||
# BranchJoiner does not type check the input
|
||||
joiner = BranchJoiner(int)
|
||||
output = joiner.run(value=["hello"])
|
||||
assert output == {"value": "hello"}
|
||||
|
||||
def test_one_value_of_none_type(self):
|
||||
# BranchJoiner does not type check the input
|
||||
joiner = BranchJoiner(int)
|
||||
output = joiner.run(value=[None])
|
||||
assert output == {"value": None}
|
||||
|
||||
def test_more_values_of_expected_type(self):
|
||||
joiner = BranchJoiner(int)
|
||||
with pytest.raises(ValueError, match="BranchJoiner expects only one input, but 3 were received."):
|
||||
joiner.run(value=[2, 3, 4])
|
||||
|
||||
def test_no_values(self):
|
||||
joiner = BranchJoiner(int)
|
||||
with pytest.raises(ValueError, match="BranchJoiner expects only one input, but 0 were received."):
|
||||
joiner.run(value=[])
|
||||
@@ -0,0 +1,310 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack import Document
|
||||
from haystack.components.joiners.document_joiner import DocumentJoiner, JoinMode
|
||||
|
||||
|
||||
class TestDocumentJoiner:
|
||||
def test_init(self):
|
||||
joiner = DocumentJoiner()
|
||||
assert joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert joiner.weights is None
|
||||
assert joiner.top_k is None
|
||||
assert joiner.sort_by_score
|
||||
|
||||
def test_init_with_custom_parameters(self):
|
||||
joiner = DocumentJoiner(join_mode="merge", weights=[0.4, 0.6], top_k=5, sort_by_score=False)
|
||||
assert joiner.join_mode == JoinMode.MERGE
|
||||
assert joiner.weights == [0.4, 0.6]
|
||||
assert joiner.top_k == 5
|
||||
assert not joiner.sort_by_score
|
||||
|
||||
def test_init_with_zero_sum_weights_raises(self):
|
||||
# weights that sum to zero would divide by zero during normalization
|
||||
with pytest.raises(ValueError, match="must not sum to zero"):
|
||||
DocumentJoiner(join_mode="merge", weights=[0.0, 0.0, 0.0])
|
||||
|
||||
def test_to_dict(self):
|
||||
joiner = DocumentJoiner()
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.document_joiner.DocumentJoiner",
|
||||
"init_parameters": {"join_mode": "concatenate", "sort_by_score": True, "top_k": None, "weights": None},
|
||||
}
|
||||
|
||||
def test_to_dict_custom_parameters(self):
|
||||
joiner = DocumentJoiner("merge", weights=[0.4, 0.6], top_k=4, sort_by_score=False)
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.document_joiner.DocumentJoiner",
|
||||
"init_parameters": {"join_mode": "merge", "weights": [0.4, 0.6], "top_k": 4, "sort_by_score": False},
|
||||
}
|
||||
|
||||
def test_from_dict(self):
|
||||
data = {"type": "haystack.components.joiners.document_joiner.DocumentJoiner", "init_parameters": {}}
|
||||
document_joiner = DocumentJoiner.from_dict(data)
|
||||
assert document_joiner.join_mode == JoinMode.CONCATENATE
|
||||
assert document_joiner.weights is None
|
||||
assert document_joiner.top_k is None
|
||||
assert document_joiner.sort_by_score
|
||||
|
||||
def test_from_dict_customs_parameters(self):
|
||||
data = {
|
||||
"type": "haystack.components.joiners.document_joiner.DocumentJoiner",
|
||||
"init_parameters": {"join_mode": "merge", "weights": [0.5, 0.6], "top_k": 6, "sort_by_score": False},
|
||||
}
|
||||
document_joiner = DocumentJoiner.from_dict(data)
|
||||
assert document_joiner.join_mode == JoinMode.MERGE
|
||||
assert document_joiner.weights == pytest.approx([0.5, 0.6], rel=0.1)
|
||||
assert document_joiner.top_k == 6
|
||||
assert not document_joiner.sort_by_score
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"join_mode",
|
||||
[
|
||||
JoinMode.CONCATENATE,
|
||||
JoinMode.MERGE,
|
||||
JoinMode.RECIPROCAL_RANK_FUSION,
|
||||
JoinMode.DISTRIBUTION_BASED_RANK_FUSION,
|
||||
],
|
||||
)
|
||||
def test_empty_list(self, join_mode: JoinMode):
|
||||
joiner = DocumentJoiner(join_mode=join_mode)
|
||||
result = joiner.run([])
|
||||
assert result == {"documents": []}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"join_mode",
|
||||
[
|
||||
JoinMode.CONCATENATE,
|
||||
JoinMode.MERGE,
|
||||
JoinMode.RECIPROCAL_RANK_FUSION,
|
||||
JoinMode.DISTRIBUTION_BASED_RANK_FUSION,
|
||||
],
|
||||
)
|
||||
def test_list_of_empty_lists(self, join_mode: JoinMode):
|
||||
joiner = DocumentJoiner(join_mode=join_mode)
|
||||
result = joiner.run([[], []])
|
||||
assert result == {"documents": []}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"join_mode",
|
||||
[
|
||||
JoinMode.CONCATENATE,
|
||||
JoinMode.MERGE,
|
||||
JoinMode.RECIPROCAL_RANK_FUSION,
|
||||
JoinMode.DISTRIBUTION_BASED_RANK_FUSION,
|
||||
],
|
||||
)
|
||||
def test_list_with_one_empty_list(self, join_mode: JoinMode):
|
||||
joiner = DocumentJoiner(join_mode=join_mode)
|
||||
documents = [Document(content="a"), Document(content="b"), Document(content="c")]
|
||||
result = joiner.run([[], documents])
|
||||
# Verify the same documents are returned (scoring functions assign scores to the results;
|
||||
# compare by ID to avoid relying on in-place score mutation of the input list).
|
||||
result_ids = {doc.id for doc in result["documents"]}
|
||||
expected_ids = {doc.id for doc in documents}
|
||||
assert result_ids == expected_ids
|
||||
|
||||
def test_unsupported_join_mode(self):
|
||||
unsupported_mode = "unsupported_mode"
|
||||
expected_error_pattern = (
|
||||
re.escape(f"Unknown join mode '{unsupported_mode}'") + r".*Supported modes in DocumentJoiner are: \[.*\]"
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=expected_error_pattern):
|
||||
DocumentJoiner(join_mode=unsupported_mode)
|
||||
|
||||
def test_run_with_concatenate_join_mode_and_top_k(self):
|
||||
joiner = DocumentJoiner(top_k=6)
|
||||
documents_1 = [Document(content="a"), Document(content="b"), Document(content="c")]
|
||||
documents_2 = [
|
||||
Document(content="d"),
|
||||
Document(content="e"),
|
||||
Document(content="f", meta={"key": "value"}),
|
||||
Document(content="g"),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 6
|
||||
assert sorted(documents_1 + documents_2[:-1], key=lambda d: d.id) == sorted(
|
||||
output["documents"], key=lambda d: d.id
|
||||
)
|
||||
|
||||
def test_run_with_concatenate_join_mode_and_duplicate_documents(self):
|
||||
joiner = DocumentJoiner()
|
||||
documents_1 = [Document(content="a", score=0.3), Document(content="b"), Document(content="c")]
|
||||
documents_2 = [
|
||||
Document(content="a", score=0.2),
|
||||
Document(content="a"),
|
||||
Document(content="f", meta={"key": "value"}),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 4
|
||||
assert sorted(documents_1 + [documents_2[-1]], key=lambda d: d.id) == sorted(
|
||||
output["documents"], key=lambda d: d.id
|
||||
)
|
||||
|
||||
def test_run_with_concatenate_join_mode_keeps_zero_score_over_negative_duplicate(self):
|
||||
joiner = DocumentJoiner(sort_by_score=False)
|
||||
documents_1 = [Document(content="a", score=0.0)]
|
||||
documents_2 = [Document(content="a", score=-0.5)]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 1
|
||||
assert output["documents"][0].score == 0.0
|
||||
|
||||
def test_run_with_concatenate_join_mode_keeps_zero_score_over_none_duplicate(self):
|
||||
joiner = DocumentJoiner(sort_by_score=False)
|
||||
documents_1 = [Document(content="a", score=0.0)]
|
||||
documents_2 = [Document(content="a")]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 1
|
||||
assert output["documents"][0].score == 0.0
|
||||
|
||||
def test_run_with_merge_join_mode_handles_zero_score(self):
|
||||
joiner = DocumentJoiner(join_mode="merge", weights=[0.5, 0.5])
|
||||
documents_1 = [Document(content="a", score=0.0)]
|
||||
documents_2 = [Document(content="a", score=0.0)]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert output["documents"][0].score == 0.0
|
||||
|
||||
def test_run_with_merge_join_mode(self):
|
||||
joiner = DocumentJoiner(join_mode="merge", weights=[1.5, 0.5])
|
||||
documents_1 = [Document(content="a", score=1.0), Document(content="b", score=2.0)]
|
||||
documents_2 = [
|
||||
Document(content="a", score=0.5),
|
||||
Document(content="b", score=3.0),
|
||||
Document(content="f", score=4.0, meta={"key": "value"}),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 3
|
||||
expected_document_ids = [
|
||||
doc.id
|
||||
for doc in [
|
||||
Document(content="a", score=1.25),
|
||||
Document(content="b", score=2.25),
|
||||
Document(content="f", score=4.0, meta={"key": "value"}),
|
||||
]
|
||||
]
|
||||
assert all(doc.id in expected_document_ids for doc in output["documents"])
|
||||
|
||||
def test_run_with_reciprocal_rank_fusion_join_mode(self):
|
||||
joiner = DocumentJoiner(join_mode="reciprocal_rank_fusion")
|
||||
documents_1 = [Document(content="a"), Document(content="b"), Document(content="c")]
|
||||
documents_2 = [
|
||||
Document(content="b", score=1000.0),
|
||||
Document(content="c"),
|
||||
Document(content="a"),
|
||||
Document(content="f", meta={"key": "value"}),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 4
|
||||
expected_document_ids = [
|
||||
doc.id
|
||||
for doc in [
|
||||
Document(content="b"),
|
||||
Document(content="a"),
|
||||
Document(content="c"),
|
||||
Document(content="f", meta={"key": "value"}),
|
||||
]
|
||||
]
|
||||
assert all(doc.id in expected_document_ids for doc in output["documents"])
|
||||
|
||||
def test_run_with_distribution_based_rank_fusion_join_mode(self):
|
||||
joiner = DocumentJoiner(join_mode="distribution_based_rank_fusion")
|
||||
documents_1 = [
|
||||
Document(content="a", score=0.6),
|
||||
Document(content="b", score=0.2),
|
||||
Document(content="c", score=0.5),
|
||||
]
|
||||
documents_2 = [
|
||||
Document(content="d", score=0.5),
|
||||
Document(content="e", score=0.8),
|
||||
Document(content="f", score=1.1, meta={"key": "value"}),
|
||||
Document(content="g", score=0.3),
|
||||
Document(content="a", score=0.3),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 7
|
||||
expected_document_ids = [
|
||||
doc.id
|
||||
for doc in [
|
||||
Document(content="a", score=0.66),
|
||||
Document(content="b", score=0.27),
|
||||
Document(content="c", score=0.56),
|
||||
Document(content="d", score=0.44),
|
||||
Document(content="e", score=0.60),
|
||||
Document(content="f", score=0.76, meta={"key": "value"}),
|
||||
Document(content="g", score=0.33),
|
||||
]
|
||||
]
|
||||
assert all(doc.id in expected_document_ids for doc in output["documents"])
|
||||
|
||||
def test_run_with_distribution_based_rank_fusion_join_mode_same_scores(self):
|
||||
joiner = DocumentJoiner(join_mode="distribution_based_rank_fusion")
|
||||
documents_1 = [
|
||||
Document(content="a", score=0.2),
|
||||
Document(content="b", score=0.2),
|
||||
Document(content="c", score=0.2),
|
||||
]
|
||||
documents_2 = [
|
||||
Document(content="d", score=0.5),
|
||||
Document(content="e", score=0.8),
|
||||
Document(content="f", score=1.1, meta={"key": "value"}),
|
||||
Document(content="g", score=0.3),
|
||||
Document(content="a", score=0.3),
|
||||
]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 7
|
||||
expected_document_ids = [
|
||||
doc.id
|
||||
for doc in [
|
||||
Document(content="a", score=0),
|
||||
Document(content="b", score=0),
|
||||
Document(content="c", score=0),
|
||||
Document(content="d", score=0.44),
|
||||
Document(content="e", score=0.60),
|
||||
Document(content="f", score=0.76, meta={"key": "value"}),
|
||||
Document(content="g", score=0.33),
|
||||
]
|
||||
]
|
||||
assert all(doc.id in expected_document_ids for doc in output["documents"])
|
||||
|
||||
def test_run_with_distribution_based_rank_fusion_join_mode_with_none_score(self):
|
||||
# Documents with score=None (e.g. from a non-scoring source) must not crash DBSF;
|
||||
# a missing score is treated as 0, consistent with how the statistics are computed.
|
||||
joiner = DocumentJoiner(join_mode="distribution_based_rank_fusion")
|
||||
documents_1 = [Document(content="a", score=0.6), Document(content="b", score=None)]
|
||||
documents_2 = [Document(content="c", score=0.5), Document(content="d", score=0.3)]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert len(output["documents"]) == 4
|
||||
assert all(doc.score is not None for doc in output["documents"])
|
||||
|
||||
def test_run_with_top_k_in_run_method(self):
|
||||
joiner = DocumentJoiner()
|
||||
documents_1 = [Document(content="a"), Document(content="b"), Document(content="c")]
|
||||
documents_2 = [Document(content="d"), Document(content="e"), Document(content="f")]
|
||||
top_k = 4
|
||||
output = joiner.run([documents_1, documents_2], top_k=top_k)
|
||||
assert len(output["documents"]) == top_k
|
||||
|
||||
def test_sort_by_score_without_scores(self, caplog):
|
||||
joiner = DocumentJoiner()
|
||||
with caplog.at_level(logging.INFO):
|
||||
documents = [Document(content="a"), Document(content="b", score=0.5)]
|
||||
output = joiner.run([documents])
|
||||
assert "those with score=None were sorted as if they had a score of -infinity" in caplog.text
|
||||
assert output["documents"] == documents[::-1]
|
||||
|
||||
def test_output_documents_not_sorted_by_score(self):
|
||||
joiner = DocumentJoiner(sort_by_score=False)
|
||||
documents_1 = [Document(content="a", score=0.1)]
|
||||
documents_2 = [Document(content="d", score=0.2)]
|
||||
output = joiner.run([documents_1, documents_2])
|
||||
assert output["documents"] == documents_1 + documents_2
|
||||
@@ -0,0 +1,172 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack import Document, Pipeline
|
||||
from haystack.components.builders import AnswerBuilder, ChatPromptBuilder
|
||||
from haystack.components.embedders import OpenAITextEmbedder
|
||||
from haystack.components.generators.chat.openai import OpenAIChatGenerator
|
||||
from haystack.components.joiners.list_joiner import ListJoiner
|
||||
from haystack.core.errors import PipelineConnectError
|
||||
from haystack.dataclasses import ChatMessage
|
||||
from haystack.dataclasses.answer import GeneratedAnswer
|
||||
from haystack.utils.auth import Secret
|
||||
|
||||
|
||||
class TestListJoiner:
|
||||
def test_init(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
assert isinstance(joiner, ListJoiner)
|
||||
assert joiner.list_type_ == list[ChatMessage]
|
||||
|
||||
def test_init_typing_list(self):
|
||||
joiner = ListJoiner(List[ChatMessage])
|
||||
assert isinstance(joiner, ListJoiner)
|
||||
assert joiner.list_type_ == List[ChatMessage]
|
||||
|
||||
def test_to_dict_defaults(self):
|
||||
joiner = ListJoiner()
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.list_joiner.ListJoiner",
|
||||
"init_parameters": {"list_type_": None},
|
||||
}
|
||||
|
||||
def test_to_dict_non_default(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.list_joiner.ListJoiner",
|
||||
"init_parameters": {"list_type_": "list[haystack.dataclasses.chat_message.ChatMessage]"},
|
||||
}
|
||||
|
||||
def test_to_dict_non_default_typing_list(self):
|
||||
joiner = ListJoiner(List[ChatMessage])
|
||||
data = joiner.to_dict()
|
||||
assert data == {
|
||||
"type": "haystack.components.joiners.list_joiner.ListJoiner",
|
||||
"init_parameters": {"list_type_": "typing.List[haystack.dataclasses.chat_message.ChatMessage]"},
|
||||
}
|
||||
|
||||
def test_from_dict_default(self):
|
||||
data = {"type": "haystack.components.joiners.list_joiner.ListJoiner", "init_parameters": {"list_type_": None}}
|
||||
list_joiner = ListJoiner.from_dict(data)
|
||||
assert isinstance(list_joiner, ListJoiner)
|
||||
assert list_joiner.list_type_ is None
|
||||
|
||||
def test_from_dict_non_default(self):
|
||||
data = {
|
||||
"type": "haystack.components.joiners.list_joiner.ListJoiner",
|
||||
"init_parameters": {"list_type_": "list[haystack.dataclasses.chat_message.ChatMessage]"},
|
||||
}
|
||||
list_joiner = ListJoiner.from_dict(data)
|
||||
assert isinstance(list_joiner, ListJoiner)
|
||||
assert list_joiner.list_type_ == list[ChatMessage]
|
||||
|
||||
def test_from_dict_non_default_typing_list(self):
|
||||
data = {
|
||||
"type": "haystack.components.joiners.list_joiner.ListJoiner",
|
||||
"init_parameters": {"list_type_": "typing.List[haystack.dataclasses.chat_message.ChatMessage]"},
|
||||
}
|
||||
list_joiner = ListJoiner.from_dict(data)
|
||||
assert isinstance(list_joiner, ListJoiner)
|
||||
assert list_joiner.list_type_ == List[ChatMessage]
|
||||
|
||||
def test_empty_list(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
result = joiner.run([])
|
||||
assert result == {"values": []}
|
||||
|
||||
def test_list_of_empty_lists(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
result = joiner.run([[], []])
|
||||
assert result == {"values": []}
|
||||
|
||||
def test_single_list_of_chat_messages(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
messages = [ChatMessage.from_user("Hello"), ChatMessage.from_assistant("Hi there")]
|
||||
result = joiner.run([messages])
|
||||
assert result == {"values": messages}
|
||||
|
||||
def test_multiple_lists_of_chat_messages(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
messages1 = [ChatMessage.from_user("Hello")]
|
||||
messages2 = [ChatMessage.from_assistant("Hi there")]
|
||||
messages3 = [ChatMessage.from_system("System message")]
|
||||
result = joiner.run([messages1, messages2, messages3])
|
||||
assert result == {"values": messages1 + messages2 + messages3}
|
||||
|
||||
def test_list_of_generated_answers(self):
|
||||
joiner = ListJoiner(list[GeneratedAnswer])
|
||||
answers1 = [GeneratedAnswer(query="q1", data="a1", meta={}, documents=[Document(content="d1")])]
|
||||
answers2 = [GeneratedAnswer(query="q2", data="a2", meta={}, documents=[Document(content="d2")])]
|
||||
result = joiner.run([answers1, answers2])
|
||||
assert result == {"values": answers1 + answers2}
|
||||
|
||||
def test_list_two_different_types(self):
|
||||
joiner = ListJoiner()
|
||||
result = joiner.run([["a", "b"], [1, 2]])
|
||||
assert result == {"values": ["a", "b", 1, 2]}
|
||||
|
||||
def test_mixed_empty_and_non_empty_lists(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
messages = [ChatMessage.from_user("Hello")]
|
||||
result = joiner.run([messages, [], messages])
|
||||
assert result == {"values": messages + messages}
|
||||
|
||||
def test_pipeline_connection_validation(self):
|
||||
joiner = ListJoiner()
|
||||
llm = OpenAIChatGenerator(api_key=Secret.from_token("test-api-key"))
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("joiner", joiner)
|
||||
pipe.add_component("llm", llm)
|
||||
with pytest.raises(PipelineConnectError):
|
||||
pipe.connect("joiner.values", "llm.messages")
|
||||
assert pipe is not None
|
||||
|
||||
def test_pipeline_connection_validation_list_chatmessage(self):
|
||||
joiner = ListJoiner(list[ChatMessage])
|
||||
llm = OpenAIChatGenerator(api_key=Secret.from_token("test-api-key"))
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("joiner", joiner)
|
||||
pipe.add_component("llm", llm)
|
||||
pipe.connect("joiner", "llm.messages")
|
||||
assert pipe is not None
|
||||
|
||||
def test_pipeline_bad_connection(self):
|
||||
with pytest.raises(PipelineConnectError):
|
||||
joiner = ListJoiner()
|
||||
query_embedder = OpenAITextEmbedder(api_key=Secret.from_token("test-api-key"))
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("joiner", joiner)
|
||||
pipe.add_component("query_embedder", query_embedder)
|
||||
pipe.connect("joiner.values", "query_embedder.text")
|
||||
|
||||
def test_pipeline_bad_connection_different_list_types(self):
|
||||
with pytest.raises(PipelineConnectError):
|
||||
joiner = ListJoiner(list[int])
|
||||
llm = OpenAIChatGenerator(api_key=Secret.from_token("test-api-key"))
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("joiner", joiner)
|
||||
pipe.add_component("llm", llm)
|
||||
pipe.connect("joiner.values", "llm.messages")
|
||||
|
||||
def test_result_two_different_types(self):
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("answer_builder", AnswerBuilder())
|
||||
pipe.add_component("chat_prompt_builder", ChatPromptBuilder())
|
||||
pipe.add_component("joiner", ListJoiner())
|
||||
pipe.connect("answer_builder", "joiner.values")
|
||||
pipe.connect("chat_prompt_builder", "joiner.values")
|
||||
result = pipe.run(
|
||||
data={
|
||||
"answer_builder": {"query": "What is nuclear physics?", "replies": ["This is an answer."]},
|
||||
"chat_prompt_builder": {"template": [ChatMessage.from_user("Hello")]},
|
||||
}
|
||||
)
|
||||
assert isinstance(result["joiner"]["values"][0], GeneratedAnswer)
|
||||
assert isinstance(result["joiner"]["values"][1], ChatMessage)
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from haystack.components.joiners.string_joiner import StringJoiner
|
||||
from haystack.core.serialization import component_from_dict, component_to_dict
|
||||
|
||||
|
||||
class TestStringJoiner:
|
||||
def test_init(self):
|
||||
joiner = StringJoiner()
|
||||
assert isinstance(joiner, StringJoiner)
|
||||
|
||||
def test_to_dict(self):
|
||||
joiner = StringJoiner()
|
||||
data = component_to_dict(joiner, name="string_joiner")
|
||||
assert data == {"type": "haystack.components.joiners.string_joiner.StringJoiner", "init_parameters": {}}
|
||||
|
||||
def test_from_dict(self):
|
||||
data = {"type": "haystack.components.joiners.string_joiner.StringJoiner", "init_parameters": {}}
|
||||
string_joiner = component_from_dict(StringJoiner, data=data, name="string_joiner")
|
||||
assert isinstance(string_joiner, StringJoiner)
|
||||
|
||||
def test_empty_list(self):
|
||||
joiner = StringJoiner()
|
||||
result = joiner.run([])
|
||||
assert result == {"strings": []}
|
||||
|
||||
def test_single_string(self):
|
||||
joiner = StringJoiner()
|
||||
result = joiner.run("a")
|
||||
assert result == {"strings": ["a"]}
|
||||
|
||||
def test_two_strings(self):
|
||||
joiner = StringJoiner()
|
||||
result = joiner.run(["a", "b"])
|
||||
assert result == {"strings": ["a", "b"]}
|
||||
Reference in New Issue
Block a user