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,219 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import json
|
||||
from typing import Any, List
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack import Pipeline, component
|
||||
from haystack.components.converters import OutputAdapter
|
||||
from haystack.components.converters.output_adapter import OutputAdaptationException
|
||||
from haystack.core.component.sockets import InputSocket
|
||||
from haystack.dataclasses import Document
|
||||
|
||||
|
||||
def custom_filter_to_sede(value):
|
||||
return value.upper()
|
||||
|
||||
|
||||
def another_custom_filter(value):
|
||||
return value.upper()
|
||||
|
||||
|
||||
class TestOutputAdapter:
|
||||
# OutputAdapter can be initialized with a valid Jinja2 template string and output type.
|
||||
def test_initialized_with_valid_template_and_output_type(self):
|
||||
template = "{{ documents[0].content }}"
|
||||
output_type = str
|
||||
adapter = OutputAdapter(template="{{ documents[0].content }}", output_type=str)
|
||||
|
||||
assert adapter.template == template
|
||||
assert adapter.__haystack_output__.output.name == "output"
|
||||
assert adapter.__haystack_output__.output.type == output_type
|
||||
|
||||
# OutputAdapter can adapt the output of one component to be compatible with the input of another
|
||||
# component using Jinja2 template expressions.
|
||||
def test_output_adaptation(self):
|
||||
adapter = OutputAdapter(template="{{ documents[0].content }}", output_type=str)
|
||||
|
||||
input_data = {"documents": [{"content": "Test content"}]}
|
||||
expected_output = {"output": "Test content"}
|
||||
|
||||
assert adapter.run(**input_data) == expected_output
|
||||
|
||||
# OutputAdapter can add filter 'json_loads' and use it
|
||||
def test_predefined_filters(self):
|
||||
adapter = OutputAdapter(
|
||||
template="{{ documents[0].content|json_loads }}",
|
||||
output_type=dict,
|
||||
custom_filters={"json_loads": lambda s: json.loads(str(s))},
|
||||
)
|
||||
|
||||
input_data = {"documents": [{"content": '{"key": "value"}'}]}
|
||||
expected_output = {"output": {"key": "value"}}
|
||||
|
||||
assert adapter.run(**input_data) == expected_output
|
||||
|
||||
# OutputAdapter can handle custom filters provided in the component configuration.
|
||||
def test_custom_filters(self):
|
||||
def custom_filter(value):
|
||||
return value.upper()
|
||||
|
||||
custom_filters = {"custom_filter": custom_filter}
|
||||
adapter = OutputAdapter(
|
||||
template="{{ documents[0].content|custom_filter }}", output_type=str, custom_filters=custom_filters
|
||||
)
|
||||
|
||||
input_data = {"documents": [{"content": "test content"}]}
|
||||
expected_output = {"output": "TEST CONTENT"}
|
||||
|
||||
assert adapter.run(**input_data) == expected_output
|
||||
|
||||
# OutputAdapter raises an exception on init if the Jinja2 template string is invalid.
|
||||
def test_invalid_template_string(self):
|
||||
with pytest.raises(ValueError):
|
||||
OutputAdapter(template="{{ documents[0].content }", output_type=str)
|
||||
|
||||
# OutputAdapter raises an exception if no input data is provided for output adaptation.
|
||||
def test_no_input_data_provided(self):
|
||||
adapter = OutputAdapter(template="{{ documents[0].content }}", output_type=str)
|
||||
with pytest.raises(ValueError):
|
||||
adapter.run()
|
||||
|
||||
# OutputAdapter raises an exception if there's an error during the adaptation process.
|
||||
def test_error_during_adaptation(self):
|
||||
adapter = OutputAdapter(template="{{ documents[0].content }}", output_type=str)
|
||||
input_data = {"documents": [{"title": "Test title"}]}
|
||||
|
||||
with pytest.raises(OutputAdaptationException):
|
||||
adapter.run(**input_data)
|
||||
|
||||
# OutputAdapter can be serialized to a dictionary and deserialized back to an OutputAdapter instance.
|
||||
def test_sede(self):
|
||||
adapter = OutputAdapter(template="{{ documents[0].content }}", output_type=str)
|
||||
adapter_dict = adapter.to_dict()
|
||||
deserialized_adapter = OutputAdapter.from_dict(adapter_dict)
|
||||
|
||||
assert adapter.template == deserialized_adapter.template
|
||||
assert adapter.output_type == deserialized_adapter.output_type
|
||||
|
||||
# OutputAdapter can be serialized to a dictionary and deserialized along with custom filters
|
||||
def test_sede_with_custom_filters(self):
|
||||
# NOTE: filters need to be declared in a namespace visible to the deserialization function
|
||||
custom_filters = {"custom_filter": custom_filter_to_sede}
|
||||
adapter = OutputAdapter(
|
||||
template="{{ documents[0].content|custom_filter }}", output_type=str, custom_filters=custom_filters
|
||||
)
|
||||
adapter_dict = adapter.to_dict()
|
||||
deserialized_adapter = OutputAdapter.from_dict(adapter_dict)
|
||||
|
||||
assert adapter.template == deserialized_adapter.template
|
||||
assert adapter.output_type == deserialized_adapter.output_type
|
||||
assert adapter.custom_filters == deserialized_adapter.custom_filters == custom_filters
|
||||
|
||||
# invoke the custom filter to check if it is deserialized correctly
|
||||
assert deserialized_adapter.custom_filters["custom_filter"]("test") == "TEST"
|
||||
|
||||
# OutputAdapter can be serialized to a dictionary and deserialized along with multiple custom filters
|
||||
def test_sede_with_multiple_custom_filters(self):
|
||||
# NOTE: filters need to be declared in a namespace visible to the deserialization function
|
||||
custom_filters = {"custom_filter": custom_filter_to_sede, "another_custom_filter": another_custom_filter}
|
||||
adapter = OutputAdapter(
|
||||
template="{{ documents[0].content|custom_filter }}", output_type=str, custom_filters=custom_filters
|
||||
)
|
||||
adapter_dict = adapter.to_dict()
|
||||
deserialized_adapter = OutputAdapter.from_dict(adapter_dict)
|
||||
|
||||
assert adapter.template == deserialized_adapter.template
|
||||
assert adapter.output_type == deserialized_adapter.output_type
|
||||
assert adapter.custom_filters == deserialized_adapter.custom_filters == custom_filters
|
||||
|
||||
# invoke the custom filter to check if it is deserialized correctly
|
||||
assert deserialized_adapter.custom_filters["custom_filter"]("test") == "TEST"
|
||||
|
||||
def test_sede_with_list_output_type_in_pipeline(self):
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("adapter", OutputAdapter(template="{{ test }}", output_type=list[str]))
|
||||
serialized_pipe = pipe.dumps()
|
||||
|
||||
# we serialize the pipeline and check if the output type is serialized correctly (as list[str])
|
||||
assert "list[str]" in serialized_pipe
|
||||
|
||||
deserialized_pipe = Pipeline.loads(serialized_pipe)
|
||||
assert deserialized_pipe.get_component("adapter").output_type == list[str]
|
||||
|
||||
def test_sede_with_typing_list_output_type_in_pipeline(self):
|
||||
pipe = Pipeline()
|
||||
pipe.add_component("adapter", OutputAdapter(template="{{ test }}", output_type=List[str]))
|
||||
serialized_pipe = pipe.dumps()
|
||||
|
||||
# we serialize the pipeline and check if the output type is serialized correctly (as list[str])
|
||||
assert "typing.List[str]" in serialized_pipe
|
||||
|
||||
deserialized_pipe = Pipeline.loads(serialized_pipe)
|
||||
assert deserialized_pipe.get_component("adapter").output_type == List[str]
|
||||
|
||||
def test_output_adapter_from_dict_custom_filters_none(self):
|
||||
component = OutputAdapter.from_dict(
|
||||
data={
|
||||
"type": "haystack.components.converters.output_adapter.OutputAdapter",
|
||||
"init_parameters": {
|
||||
"template": "{{ documents[0].content}}",
|
||||
"output_type": "str",
|
||||
"custom_filters": None,
|
||||
"unsafe": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert component.template == "{{ documents[0].content}}"
|
||||
assert component.output_type == str
|
||||
assert component.custom_filters == {}
|
||||
assert not component._unsafe
|
||||
|
||||
def test_output_adapter_in_pipeline(self):
|
||||
@component
|
||||
class DocumentProducer:
|
||||
@component.output_types(documents=dict)
|
||||
def run(self):
|
||||
return {"documents": [{"content": '{"framework": "Haystack"}'}]}
|
||||
|
||||
pipe = Pipeline()
|
||||
pipe.add_component(
|
||||
name="output_adapter",
|
||||
instance=OutputAdapter(
|
||||
template="{{ documents[0].content | json_loads}}",
|
||||
output_type=str,
|
||||
custom_filters={"json_loads": lambda s: json.loads(str(s))},
|
||||
),
|
||||
)
|
||||
pipe.add_component(name="document_producer", instance=DocumentProducer())
|
||||
pipe.connect("document_producer", "output_adapter")
|
||||
result = pipe.run(data={})
|
||||
assert result
|
||||
assert result["output_adapter"]["output"] == {"framework": "Haystack"}
|
||||
|
||||
def test_unsafe(self):
|
||||
adapter = OutputAdapter(template="{{ documents[0] }}", output_type=Document, unsafe=True)
|
||||
documents = [
|
||||
Document(content="Test document"),
|
||||
Document(content="Another test document"),
|
||||
Document(content="Yet another test document"),
|
||||
]
|
||||
res = adapter.run(documents=documents)
|
||||
assert res["output"] == documents[0]
|
||||
|
||||
def test_variables_correct_with_assignment(self) -> None:
|
||||
template = """{% if control == 'something' %}
|
||||
{% set output = 1 %}
|
||||
{% else %}
|
||||
{% set output = 3 %}
|
||||
{% endif %}
|
||||
{{ output }}
|
||||
"""
|
||||
adapter = OutputAdapter(template=template, output_type=int)
|
||||
assert adapter.__haystack_input__._sockets_dict == {"control": InputSocket(name="control", type=Any)}
|
||||
res = adapter.run(control="something")
|
||||
assert res["output"] == 1
|
||||
Reference in New Issue
Block a user