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,551 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from haystack import Document, Pipeline, SuperComponent, component, super_component
|
||||
from haystack.components.builders import AnswerBuilder, PromptBuilder
|
||||
from haystack.components.joiners import DocumentJoiner
|
||||
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
|
||||
from haystack.core.pipeline.base import component_from_dict, component_to_dict
|
||||
from haystack.core.serialization import default_from_dict, default_to_dict
|
||||
from haystack.core.super_component.super_component import InvalidMappingTypeError, InvalidMappingValueError
|
||||
from haystack.dataclasses import GeneratedAnswer
|
||||
from haystack.document_stores.in_memory import InMemoryDocumentStore
|
||||
from haystack.document_stores.types import DuplicatePolicy
|
||||
from haystack.testing.sample_components import AddFixedValue, Double
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def documents():
|
||||
"""Create test documents for the document store."""
|
||||
return [
|
||||
Document(content="Paris is the capital of France."),
|
||||
Document(content="Berlin is the capital of Germany."),
|
||||
Document(content="Rome is the capital of Italy."),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def document_store(documents):
|
||||
"""Create and populate a test document store."""
|
||||
store = InMemoryDocumentStore()
|
||||
store.write_documents(documents, policy=DuplicatePolicy.OVERWRITE)
|
||||
yield store
|
||||
store.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rag_pipeline(document_store):
|
||||
"""Create a simple RAG pipeline."""
|
||||
|
||||
@component
|
||||
class FakeGenerator:
|
||||
@component.output_types(replies=list[str])
|
||||
def run(self, prompt: str, **kwargs: Any) -> dict[str, list[str]]:
|
||||
return {"replies": ["This is a test response about capitals."]}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("retriever", InMemoryBM25Retriever(document_store=document_store))
|
||||
pipeline.add_component(
|
||||
"prompt_builder",
|
||||
PromptBuilder(
|
||||
template="Given these documents: {{documents|join(', ',attribute='content')}} Answer: {{query}}",
|
||||
required_variables="*",
|
||||
),
|
||||
)
|
||||
pipeline.add_component("llm", FakeGenerator())
|
||||
pipeline.add_component("answer_builder", AnswerBuilder())
|
||||
pipeline.add_component("joiner", DocumentJoiner())
|
||||
|
||||
pipeline.connect("retriever", "prompt_builder.documents")
|
||||
pipeline.connect("prompt_builder", "llm")
|
||||
pipeline.connect("llm.replies", "answer_builder.replies")
|
||||
pipeline.connect("retriever.documents", "joiner.documents")
|
||||
|
||||
return pipeline
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_rag_pipeline(document_store):
|
||||
"""Create a simple asyncRAG pipeline."""
|
||||
|
||||
@component
|
||||
class FakeGenerator:
|
||||
@component.output_types(replies=list[str])
|
||||
def run(self, prompt: str, **kwargs: Any) -> dict[str, list[str]]:
|
||||
return {"replies": ["This is a test response about capitals."]}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("retriever", InMemoryBM25Retriever(document_store=document_store))
|
||||
pipeline.add_component(
|
||||
"prompt_builder",
|
||||
PromptBuilder(
|
||||
template="Given these documents: {{documents|join(', ',attribute='content')}} Answer: {{query}}",
|
||||
required_variables="*",
|
||||
),
|
||||
)
|
||||
pipeline.add_component("llm", FakeGenerator())
|
||||
pipeline.add_component("answer_builder", AnswerBuilder())
|
||||
pipeline.add_component("joiner", DocumentJoiner())
|
||||
|
||||
pipeline.connect("retriever", "prompt_builder.documents")
|
||||
pipeline.connect("prompt_builder", "llm")
|
||||
pipeline.connect("llm.replies", "answer_builder.replies")
|
||||
pipeline.connect("retriever.documents", "joiner.documents")
|
||||
|
||||
return pipeline
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_super_component():
|
||||
"""Creates a sample SuperComponent for testing visualization methods"""
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("comp1", AddFixedValue(add=3))
|
||||
pipe.add_component("comp2", Double())
|
||||
pipe.connect("comp1.result", "comp2.value")
|
||||
|
||||
return SuperComponent(pipeline=pipe)
|
||||
|
||||
|
||||
@super_component
|
||||
class CustomSuperComponent:
|
||||
def __init__(self, var1: int, var2: str = "test"):
|
||||
self.var1 = var1
|
||||
self.var2 = var2
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("joiner", DocumentJoiner())
|
||||
self.pipeline = pipeline
|
||||
|
||||
|
||||
class TestSuperComponent:
|
||||
def test_split_component_path(self):
|
||||
path = "router.chat_query"
|
||||
components = SuperComponent._split_component_path(path)
|
||||
assert components == ("router", "chat_query")
|
||||
|
||||
def test_split_component_path_error(self):
|
||||
path = "router"
|
||||
with pytest.raises(InvalidMappingValueError):
|
||||
SuperComponent._split_component_path(path)
|
||||
|
||||
def test_invalid_input_mapping_type(self, rag_pipeline):
|
||||
input_mapping = {"search_query": "not_a_list"} # Should be a list
|
||||
with pytest.raises(InvalidMappingTypeError):
|
||||
SuperComponent(pipeline=rag_pipeline, input_mapping=input_mapping) # type: ignore[arg-type]
|
||||
|
||||
def test_invalid_input_mapping_value(self, rag_pipeline):
|
||||
input_mapping = {"search_query": ["nonexistent_component.query"]}
|
||||
with pytest.raises(InvalidMappingValueError):
|
||||
SuperComponent(pipeline=rag_pipeline, input_mapping=input_mapping)
|
||||
|
||||
def test_invalid_output_mapping_type(self, rag_pipeline):
|
||||
output_mapping = {"answer_builder.answers": 123} # Should be a string
|
||||
with pytest.raises(InvalidMappingTypeError):
|
||||
SuperComponent(pipeline=rag_pipeline, output_mapping=output_mapping) # type: ignore[arg-type]
|
||||
|
||||
def test_invalid_output_mapping_value(self, rag_pipeline):
|
||||
output_mapping = {"nonexistent_component.answers": "final_answers"}
|
||||
with pytest.raises(InvalidMappingValueError):
|
||||
SuperComponent(pipeline=rag_pipeline, output_mapping=output_mapping)
|
||||
|
||||
def test_duplicate_output_names(self, rag_pipeline):
|
||||
output_mapping = {
|
||||
"answer_builder.answers": "final_answers",
|
||||
"llm.replies": "final_answers", # Different path but same output name
|
||||
}
|
||||
with pytest.raises(InvalidMappingValueError):
|
||||
SuperComponent(pipeline=rag_pipeline, output_mapping=output_mapping)
|
||||
|
||||
def test_explicit_input_mapping(self, rag_pipeline):
|
||||
input_mapping = {"search_query": ["retriever.query", "prompt_builder.query", "answer_builder.query"]}
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline, input_mapping=input_mapping)
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(input_sockets.keys()) == {"search_query"}
|
||||
assert input_sockets["search_query"].type == str
|
||||
|
||||
def test_explicit_output_mapping(self, rag_pipeline):
|
||||
output_mapping = {"answer_builder.answers": "final_answers"}
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline, output_mapping=output_mapping)
|
||||
output_sockets = wrapper.__haystack_output__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(output_sockets.keys()) == {"final_answers"}
|
||||
assert output_sockets["final_answers"].type == list[GeneratedAnswer]
|
||||
|
||||
def test_auto_input_mapping(self, rag_pipeline):
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline)
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(input_sockets.keys()) == {
|
||||
"documents",
|
||||
"expand_reference_ranges",
|
||||
"filters",
|
||||
"meta",
|
||||
"pattern",
|
||||
"query",
|
||||
"reference_pattern",
|
||||
"scale_score",
|
||||
"template",
|
||||
"template_variables",
|
||||
"top_k",
|
||||
}
|
||||
|
||||
def test_auto_output_mapping(self, rag_pipeline):
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline)
|
||||
output_sockets = wrapper.__haystack_output__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(output_sockets.keys()) == {"answers", "documents"}
|
||||
|
||||
def test_auto_mapping_sockets(self, rag_pipeline):
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline)
|
||||
|
||||
output_sockets = wrapper.__haystack_output__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(output_sockets.keys()) == {"answers", "documents"}
|
||||
assert output_sockets["answers"].type == list[GeneratedAnswer]
|
||||
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert set(input_sockets.keys()) == {
|
||||
"documents",
|
||||
"expand_reference_ranges",
|
||||
"filters",
|
||||
"meta",
|
||||
"pattern",
|
||||
"query",
|
||||
"reference_pattern",
|
||||
"scale_score",
|
||||
"template",
|
||||
"template_variables",
|
||||
"top_k",
|
||||
}
|
||||
assert input_sockets["query"].type == str
|
||||
|
||||
def test_super_component_run(self, rag_pipeline):
|
||||
input_mapping = {"search_query": ["retriever.query", "prompt_builder.query", "answer_builder.query"]}
|
||||
output_mapping = {"answer_builder.answers": "final_answers"}
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline, input_mapping=input_mapping, output_mapping=output_mapping)
|
||||
wrapper.warm_up()
|
||||
result = wrapper.run(search_query="What is the capital of France?")
|
||||
assert "final_answers" in result
|
||||
assert isinstance(result["final_answers"][0], GeneratedAnswer)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_super_component_run_async(self, async_rag_pipeline):
|
||||
input_mapping = {"search_query": ["retriever.query", "prompt_builder.query", "answer_builder.query"]}
|
||||
output_mapping = {"answer_builder.answers": "final_answers"}
|
||||
wrapper = SuperComponent(
|
||||
pipeline=async_rag_pipeline, input_mapping=input_mapping, output_mapping=output_mapping
|
||||
)
|
||||
wrapper.warm_up()
|
||||
result = await wrapper.run_async(search_query="What is the capital of France?")
|
||||
assert "final_answers" in result
|
||||
assert isinstance(result["final_answers"][0], GeneratedAnswer)
|
||||
|
||||
def test_wrapper_serialization(self, document_store):
|
||||
"""Test serialization and deserialization of pipeline wrapper."""
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("retriever", InMemoryBM25Retriever(document_store=document_store))
|
||||
|
||||
wrapper = SuperComponent(
|
||||
pipeline=pipeline,
|
||||
input_mapping={"query": ["retriever.query"]},
|
||||
output_mapping={"retriever.documents": "documents"},
|
||||
)
|
||||
|
||||
# Test serialization
|
||||
serialized = wrapper.to_dict()
|
||||
assert "type" in serialized
|
||||
assert "init_parameters" in serialized
|
||||
assert "pipeline" in serialized["init_parameters"]
|
||||
|
||||
# Test deserialization
|
||||
deserialized = SuperComponent.from_dict(serialized)
|
||||
assert isinstance(deserialized, SuperComponent)
|
||||
assert deserialized.input_mapping == wrapper.input_mapping
|
||||
assert deserialized.output_mapping == wrapper.output_mapping
|
||||
|
||||
deserialized.warm_up()
|
||||
result = deserialized.run(query="What is the capital of France?")
|
||||
assert "documents" in result
|
||||
assert result["documents"][0].content == "Paris is the capital of France."
|
||||
|
||||
def test_from_dict_ignores_legacy_is_pipeline_async(self, document_store):
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("retriever", InMemoryBM25Retriever(document_store=document_store))
|
||||
wrapper = SuperComponent(pipeline=pipeline, output_mapping={"retriever.documents": "documents"})
|
||||
|
||||
serialized = wrapper.to_dict()
|
||||
serialized["init_parameters"]["is_pipeline_async"] = True
|
||||
|
||||
deserialized = SuperComponent.from_dict(serialized)
|
||||
assert isinstance(deserialized.pipeline, Pipeline)
|
||||
|
||||
def test_subclass_serialization(self, rag_pipeline):
|
||||
super_comp = SuperComponent(rag_pipeline)
|
||||
serialized = super_comp.to_dict()
|
||||
|
||||
@component
|
||||
class CustomSuperComponent(SuperComponent):
|
||||
def __init__(self, pipeline, instance_attribute="test"):
|
||||
self.instance_attribute = instance_attribute
|
||||
super(CustomSuperComponent, self).__init__(pipeline) # noqa: UP008
|
||||
|
||||
def to_dict(self):
|
||||
return default_to_dict(
|
||||
self, instance_attribute=self.instance_attribute, pipeline=self.pipeline.to_dict()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
data["init_parameters"]["pipeline"] = Pipeline.from_dict(data["init_parameters"]["pipeline"])
|
||||
return default_from_dict(cls, data)
|
||||
|
||||
custom_super_component = CustomSuperComponent(rag_pipeline)
|
||||
custom_serialized = custom_super_component.to_dict()
|
||||
|
||||
assert custom_serialized["type"] == "test_super_component.CustomSuperComponent"
|
||||
assert custom_super_component._to_super_component_dict() == serialized
|
||||
|
||||
def test_super_component_non_leaf_output(self, rag_pipeline):
|
||||
# 'retriever' is not a leaf, but should now be allowed
|
||||
output_mapping = {"retriever.documents": "retrieved_docs", "answer_builder.answers": "final_answers"}
|
||||
wrapper = SuperComponent(pipeline=rag_pipeline, output_mapping=output_mapping)
|
||||
wrapper.warm_up()
|
||||
result = wrapper.run(query="What is the capital of France?")
|
||||
assert "final_answers" in result # leaf output
|
||||
assert "retrieved_docs" in result # non-leaf output
|
||||
assert isinstance(result["retrieved_docs"][0], Document)
|
||||
|
||||
def test_custom_super_component_to_dict(self, rag_pipeline):
|
||||
custom_super_component = CustomSuperComponent(1)
|
||||
data = component_to_dict(custom_super_component, "custom_super_component")
|
||||
assert data == {
|
||||
"type": "test_super_component.CustomSuperComponent",
|
||||
"init_parameters": {"var1": 1, "var2": "test"},
|
||||
}
|
||||
|
||||
def test_custom_super_component_from_dict(self):
|
||||
data = {"type": "test_super_component.CustomSuperComponent", "init_parameters": {"var1": 1, "var2": "test"}}
|
||||
custom_super_component = component_from_dict(CustomSuperComponent, data, "custom_super_component")
|
||||
assert isinstance(custom_super_component, CustomSuperComponent)
|
||||
assert custom_super_component.var1 == 1
|
||||
assert custom_super_component.var2 == "test"
|
||||
|
||||
@patch("haystack.core.pipeline.Pipeline.show")
|
||||
def test_show_delegates_to_pipeline(self, mock_show, sample_super_component):
|
||||
"""Test that SuperComponent.show() correctly delegates to Pipeline.show() with all parameters"""
|
||||
|
||||
server_url = "https://custom.mermaid.server"
|
||||
params = {"theme": "dark", "format": "svg"}
|
||||
timeout = 60
|
||||
|
||||
sample_super_component.show(server_url=server_url, params=params, timeout=timeout)
|
||||
mock_show.assert_called_once_with(server_url=server_url, params=params, timeout=timeout)
|
||||
|
||||
@patch("haystack.core.pipeline.Pipeline.draw")
|
||||
def test_draw_delegates_to_pipeline(self, mock_draw, sample_super_component, tmp_path):
|
||||
"""Test that SuperComponent.draw() correctly delegates to Pipeline.draw() with all parameters"""
|
||||
|
||||
path = tmp_path / "test_pipeline.png"
|
||||
server_url = "https://custom.mermaid.server"
|
||||
params = {"theme": "dark", "format": "png"}
|
||||
timeout = 60
|
||||
|
||||
sample_super_component.draw(path=path, server_url=server_url, params=params, timeout=timeout)
|
||||
mock_draw.assert_called_once_with(path=path, server_url=server_url, params=params, timeout=timeout)
|
||||
|
||||
@patch("haystack.core.pipeline.Pipeline.show")
|
||||
def test_show_with_default_parameters(self, mock_show, sample_super_component):
|
||||
"""Test that SuperComponent.show() works with default parameters"""
|
||||
|
||||
sample_super_component.show()
|
||||
mock_show.assert_called_once_with(server_url="https://mermaid.ink", params=None, timeout=30)
|
||||
|
||||
@patch("haystack.core.pipeline.Pipeline.draw")
|
||||
def test_draw_with_default_parameters(self, mock_draw, sample_super_component, tmp_path):
|
||||
"""Test that SuperComponent.draw() works with default parameters except path"""
|
||||
|
||||
path = tmp_path / "test_pipeline.png"
|
||||
|
||||
sample_super_component.draw(path=path)
|
||||
mock_draw.assert_called_once_with(path=path, server_url="https://mermaid.ink", params=None, timeout=30)
|
||||
|
||||
def test_input_types_reconciliation(self):
|
||||
"""Test that input types are properly reconciled when they are compatible but not identical."""
|
||||
|
||||
@component
|
||||
class TypeTestComponent:
|
||||
@component.output_types(result_int=int, result_any=Any)
|
||||
def run(self, input_int: int, input_any: Any) -> dict[str, Any]:
|
||||
return {"result_int": input_int, "result_any": input_any}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("test1", TypeTestComponent())
|
||||
pipeline.add_component("test2", TypeTestComponent())
|
||||
|
||||
input_mapping = {"number": ["test1.input_int", "test2.input_any"]}
|
||||
output_mapping = {"test2.result_int": "result_int"}
|
||||
wrapper = SuperComponent(pipeline=pipeline, input_mapping=input_mapping, output_mapping=output_mapping)
|
||||
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert "number" in input_sockets
|
||||
assert input_sockets["number"].type == int
|
||||
|
||||
def test_union_type_reconciliation(self):
|
||||
"""Test that Union types are properly reconciled when creating a SuperComponent."""
|
||||
|
||||
@component
|
||||
class UnionTypeComponent1:
|
||||
@component.output_types(result=int | str)
|
||||
def run(self, inp: int | str) -> dict[str, int | str]:
|
||||
return {"result": inp}
|
||||
|
||||
@component
|
||||
class UnionTypeComponent2:
|
||||
@component.output_types(result=float | str)
|
||||
def run(self, inp: float | str) -> dict[str, float | str]:
|
||||
return {"result": inp}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("test1", UnionTypeComponent1())
|
||||
pipeline.add_component("test2", UnionTypeComponent2())
|
||||
|
||||
input_mapping = {"data": ["test1.inp", "test2.inp"]}
|
||||
output_mapping = {"test2.result": "result"}
|
||||
wrapper = SuperComponent(pipeline=pipeline, input_mapping=input_mapping, output_mapping=output_mapping)
|
||||
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert "data" in input_sockets
|
||||
assert input_sockets["data"].type == str
|
||||
|
||||
def test_input_types_with_any(self):
|
||||
"""Test that Any type is properly handled when reconciling types."""
|
||||
|
||||
@component
|
||||
class AnyTypeComponent:
|
||||
@component.output_types(result=str)
|
||||
def run(self, specific: str, generic: Any) -> dict[str, str]:
|
||||
return {"result": specific}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("test", AnyTypeComponent())
|
||||
|
||||
input_mapping = {"text": ["test.specific", "test.generic"]}
|
||||
wrapper = SuperComponent(pipeline=pipeline, input_mapping=input_mapping)
|
||||
|
||||
input_sockets = wrapper.__haystack_input__._sockets_dict # type: ignore[attr-defined]
|
||||
assert "text" in input_sockets
|
||||
assert input_sockets["text"].type == str
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_super_component_async_serialization_deserialization(self):
|
||||
"""
|
||||
Test for async SuperComponent serialization and deserialization.
|
||||
|
||||
Test that when using the SuperComponent class, a SuperComponent based on an async pipeline can be serialized and
|
||||
deserialized correctly.
|
||||
"""
|
||||
|
||||
@component
|
||||
class AsyncComponent:
|
||||
@component.output_types(output=str)
|
||||
def run(self):
|
||||
return {"output": "irrelevant"}
|
||||
|
||||
@component.output_types(output=str)
|
||||
async def run_async(self):
|
||||
return {"output": "Hello world"}
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_component("hello", AsyncComponent())
|
||||
|
||||
async_super_component = SuperComponent(pipeline=pipeline)
|
||||
serialized_super_component = async_super_component.to_dict()
|
||||
|
||||
deserialized_super_component = SuperComponent.from_dict(serialized_super_component)
|
||||
assert isinstance(deserialized_super_component.pipeline, Pipeline)
|
||||
|
||||
result = await deserialized_super_component.run_async()
|
||||
assert result == {"output": "Hello world"}
|
||||
|
||||
|
||||
class TestSuperComponentLifecycle:
|
||||
def test_warm_up_delegates_to_pipeline(self, sample_super_component):
|
||||
with patch.object(sample_super_component.pipeline, "warm_up") as mock_warm_up:
|
||||
sample_super_component.warm_up()
|
||||
mock_warm_up.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_and_async_warm_up_are_independent(self, sample_super_component):
|
||||
with (
|
||||
patch.object(sample_super_component.pipeline, "warm_up") as mock_warm_up,
|
||||
patch.object(sample_super_component.pipeline, "warm_up_async", new=AsyncMock()) as mock_warm_up_async,
|
||||
):
|
||||
sample_super_component.warm_up()
|
||||
mock_warm_up.assert_called_once()
|
||||
await sample_super_component.warm_up_async()
|
||||
mock_warm_up_async.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_warm_up_async_delegates_to_pipeline(self, sample_super_component):
|
||||
with patch.object(sample_super_component.pipeline, "warm_up_async", new=AsyncMock()) as mock_warm_up_async:
|
||||
await sample_super_component.warm_up_async()
|
||||
mock_warm_up_async.assert_awaited_once()
|
||||
|
||||
def test_close_delegates_to_pipeline(self, sample_super_component):
|
||||
with patch.object(sample_super_component.pipeline, "close") as mock_close:
|
||||
sample_super_component.close()
|
||||
mock_close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_async_delegates_to_pipeline(self, sample_super_component):
|
||||
with patch.object(sample_super_component.pipeline, "close_async", new=AsyncMock()) as mock_close_async:
|
||||
await sample_super_component.close_async()
|
||||
mock_close_async.assert_awaited_once()
|
||||
|
||||
|
||||
@component
|
||||
class _CaptureValue:
|
||||
"""Captures whatever value reaches the inner pipeline, for both sync and async runs."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.captured: dict[str, Any] = {}
|
||||
|
||||
@component.output_types(seen=bool)
|
||||
def run(self, value: Any = None) -> dict[str, bool]:
|
||||
self.captured["value"] = value
|
||||
return {"seen": True}
|
||||
|
||||
@component.output_types(seen=bool)
|
||||
async def run_async(self, value: Any = None) -> dict[str, bool]:
|
||||
self.captured["value"] = value
|
||||
return {"seen": True}
|
||||
|
||||
|
||||
class TestSuperComponentDelegateDefaultFiltering:
|
||||
"""
|
||||
Regression for the `_delegate_default` filter: must be `is not`, not `!=`, otherwise
|
||||
numpy / pandas / torch inputs raise `ValueError: The truth value of ... is ambiguous`.
|
||||
"""
|
||||
|
||||
def test_run_accepts_numpy_ndarray_input(self):
|
||||
inner = _CaptureValue()
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("capture", inner)
|
||||
|
||||
result = SuperComponent(pipe).run(value=np.array([1, 2, 3]))
|
||||
|
||||
assert result == {"seen": True}
|
||||
assert np.array_equal(inner.captured["value"], np.array([1, 2, 3]))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_async_accepts_numpy_ndarray_input(self):
|
||||
inner = _CaptureValue()
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("capture", inner)
|
||||
|
||||
result = await SuperComponent(pipe).run_async(value=np.array([4, 5, 6]))
|
||||
|
||||
assert result == {"seen": True}
|
||||
assert np.array_equal(inner.captured["value"], np.array([4, 5, 6]))
|
||||
@@ -0,0 +1,264 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack.core.component.types import GreedyVariadic, Variadic
|
||||
from haystack.core.super_component.utils import _is_compatible
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"left,right,expected_common",
|
||||
[
|
||||
(str, str, str),
|
||||
(int, int, int),
|
||||
(float, float, float),
|
||||
(bool, bool, bool),
|
||||
(list, list, list),
|
||||
(dict, dict, dict),
|
||||
(set, set, set),
|
||||
(tuple, tuple, tuple),
|
||||
],
|
||||
)
|
||||
def test_basic_types_compatible(left, right, expected_common):
|
||||
"""Test compatible basic Python types."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert is_compat
|
||||
assert common == expected_common
|
||||
|
||||
|
||||
@pytest.mark.parametrize("left,right", [(str, int), (float, int), (bool, str), (list, dict), (set, tuple)])
|
||||
def test_basic_types_incompatible(left, right):
|
||||
"""Test incompatible basic Python types."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert not is_compat
|
||||
assert common is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"left,right,expected_common",
|
||||
[
|
||||
(Any, Any, Any),
|
||||
# int
|
||||
(int, Any, int),
|
||||
(Any, int, int),
|
||||
# str
|
||||
(Any, str, str),
|
||||
(str, Any, str),
|
||||
# float
|
||||
(float, Any, float),
|
||||
(Any, float, float),
|
||||
# list
|
||||
(list, Any, list),
|
||||
(Any, list, list),
|
||||
# dict
|
||||
(dict, Any, dict),
|
||||
(Any, dict, dict),
|
||||
],
|
||||
)
|
||||
def test_any_type(left, right, expected_common):
|
||||
"""Test Any type compatibility."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert is_compat
|
||||
assert common == expected_common
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"left,right,expected_common",
|
||||
[
|
||||
(int, Union[int, str], int),
|
||||
(Union[int, str], int, int),
|
||||
(Union[int, str], Union[str, int], Union[int, str]),
|
||||
(str, Union[int, str], str),
|
||||
(str, Union[str, None], str),
|
||||
(Union[str, None], str, str),
|
||||
(Union[str, None], Optional[str], Optional[str]),
|
||||
(Optional[str], Union[str, None], Optional[str]),
|
||||
(Union[str, None], Union[str, None], Union[str, None]),
|
||||
],
|
||||
)
|
||||
def test_union_types_compatible(left, right, expected_common):
|
||||
"""Test compatible Union types."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert is_compat
|
||||
assert common == expected_common
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"left,right,expected_common",
|
||||
[
|
||||
(int, int | str, int),
|
||||
(int | str, int, int),
|
||||
(int | str, str | int, int | str),
|
||||
(str, str | None, str),
|
||||
(str | None, str, str),
|
||||
(str | None, str | None, str | None),
|
||||
# Mixed PEP 604 and typing.Union and Optional
|
||||
(int | str, Union[int, str], int | str),
|
||||
(Union[int, str], int | str, Union[int, str]),
|
||||
(str | None, Optional[str], str | None),
|
||||
(Optional[str], str | None, Optional[str]),
|
||||
],
|
||||
)
|
||||
def test_union_types_pep604(left, right, expected_common):
|
||||
"""Test PEP 604 union type compatibility."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert is_compat
|
||||
assert common == expected_common
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"left,right",
|
||||
[
|
||||
(bool, Union[int, str]),
|
||||
(float, Union[int, str]),
|
||||
(Union[int, str], Union[float, bool]),
|
||||
# Pep 604
|
||||
(bool, int | str),
|
||||
(float, int | str),
|
||||
(int | str, float | bool),
|
||||
],
|
||||
)
|
||||
def test_union_types_incompatible(left, right):
|
||||
"""Test incompatible Union types."""
|
||||
is_compat, common = _is_compatible(left, right)
|
||||
assert not is_compat
|
||||
assert common is None
|
||||
|
||||
|
||||
def test_variadic_type_compatibility():
|
||||
"""Test compatibility with Variadic and GreedyVariadic types."""
|
||||
# Basic type compatibility
|
||||
variadic_int = Variadic[int]
|
||||
greedy_int = GreedyVariadic[int]
|
||||
|
||||
is_compat, common = _is_compatible(variadic_int, int)
|
||||
assert is_compat and common == int
|
||||
|
||||
is_compat, common = _is_compatible(int, variadic_int)
|
||||
assert is_compat and common == int
|
||||
|
||||
is_compat, common = _is_compatible(greedy_int, int)
|
||||
assert is_compat and common == int
|
||||
|
||||
is_compat, common = _is_compatible(int, greedy_int)
|
||||
assert is_compat and common == int
|
||||
|
||||
# List type compatibility
|
||||
variadic_list = Variadic[list[int]]
|
||||
greedy_list = GreedyVariadic[list[int]]
|
||||
|
||||
is_compat, common = _is_compatible(variadic_list, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(list[int], variadic_list)
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(greedy_list, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(list[int], greedy_list)
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
# PEP 604 with Variadic
|
||||
variadic_union = Variadic[int | str]
|
||||
|
||||
is_compat, common = _is_compatible(variadic_union, int | str)
|
||||
assert is_compat and common == int | str
|
||||
|
||||
is_compat, common = _is_compatible(int | str, variadic_union)
|
||||
assert is_compat and common == int | str
|
||||
|
||||
# PEP 604 optional with GreedyVariadic
|
||||
greedy_opt = GreedyVariadic[int | None]
|
||||
|
||||
is_compat, common = _is_compatible(greedy_opt, int)
|
||||
assert is_compat and common == int
|
||||
|
||||
is_compat, common = _is_compatible(int, greedy_opt)
|
||||
assert is_compat and common == int
|
||||
|
||||
|
||||
def test_nested_type_unwrapping():
|
||||
"""Test nested type unwrapping behavior with unwrap_nested parameter."""
|
||||
# Test with unwrap_nested=True (default)
|
||||
nested_optional = Variadic[list[int | None]]
|
||||
|
||||
is_compat, common = _is_compatible(nested_optional, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(list[int], nested_optional)
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
nested_union = Variadic[list[Union[int, None]]]
|
||||
|
||||
is_compat, common = _is_compatible(nested_union, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(list[int], nested_union)
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
# PEP 604 (X | Y and X | None syntax)
|
||||
nested_pep604_optional = Variadic[list[int | None]]
|
||||
|
||||
is_compat, common = _is_compatible(nested_pep604_optional, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
is_compat, common = _is_compatible(list[int], nested_pep604_optional)
|
||||
assert is_compat and common == list[int]
|
||||
|
||||
nested_pep604_union = Variadic[list[str | int]]
|
||||
|
||||
is_compat, common = _is_compatible(nested_pep604_union, list[str | int])
|
||||
assert is_compat and common == list[str | int]
|
||||
|
||||
|
||||
def test_complex_nested_types():
|
||||
"""Test complex nested type scenarios."""
|
||||
# Multiple levels of nesting
|
||||
complex_type = Variadic[list[list[Variadic[int]]]]
|
||||
target_type = list[list[int]]
|
||||
|
||||
# With unwrap_nested=True
|
||||
is_compat, common = _is_compatible(complex_type, target_type)
|
||||
assert is_compat and common == list[list[int]]
|
||||
|
||||
is_compat, common = _is_compatible(target_type, complex_type)
|
||||
assert is_compat and common == list[list[int]]
|
||||
|
||||
# With unwrap_nested=False
|
||||
is_compat, common = _is_compatible(complex_type, target_type, unwrap_nested=False)
|
||||
assert not is_compat and common is None
|
||||
|
||||
is_compat, common = _is_compatible(target_type, complex_type, unwrap_nested=False)
|
||||
assert not is_compat and common is None
|
||||
|
||||
|
||||
def test_mixed_variadic_types():
|
||||
"""Test mixing Variadic and GreedyVariadic with other type constructs."""
|
||||
# Variadic with Union
|
||||
var_union = Variadic[Union[int, str]]
|
||||
|
||||
is_compat, common = _is_compatible(var_union, Union[int, str]) # type: ignore[arg-type]
|
||||
assert is_compat and common == Union[int, str]
|
||||
|
||||
is_compat, common = _is_compatible(Union[int, str], var_union) # type: ignore[arg-type]
|
||||
assert is_compat and common == Union[int, str]
|
||||
|
||||
# GreedyVariadic with Optional
|
||||
greedy_opt = GreedyVariadic[Optional[int]]
|
||||
|
||||
is_compat, common = _is_compatible(greedy_opt, int)
|
||||
assert is_compat and common == int
|
||||
|
||||
is_compat, common = _is_compatible(int, greedy_opt)
|
||||
assert is_compat and common == int
|
||||
|
||||
# Nested Variadic and GreedyVariadic
|
||||
nested_var = Variadic[list[GreedyVariadic[int]]]
|
||||
|
||||
is_compat, common = _is_compatible(nested_var, list[int])
|
||||
assert is_compat and common == list[int]
|
||||
Reference in New Issue
Block a user