c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
560 lines
23 KiB
Python
560 lines
23 KiB
Python
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import logging
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import pytest
|
|
|
|
from haystack.components.agents import Agent
|
|
from haystack.components.joiners import BranchJoiner
|
|
from haystack.core.component import component
|
|
from haystack.core.errors import PipelineConnectError, PipelineRuntimeError
|
|
from haystack.core.pipeline import Pipeline
|
|
from haystack.dataclasses import ChatMessage, Document
|
|
|
|
|
|
@component
|
|
class MockChatGenerator:
|
|
@component.output_types(replies=list[ChatMessage])
|
|
def run(self, messages: list[ChatMessage]) -> dict[str, list[ChatMessage]]:
|
|
return {"replies": [ChatMessage.from_assistant("Hello, world!")]}
|
|
|
|
|
|
@component
|
|
class StringProducer:
|
|
def __init__(self, value: str = "Hello"):
|
|
self.value = value
|
|
|
|
@component.output_types(text=str)
|
|
def run(self) -> dict[str, str]:
|
|
return {"text": self.value}
|
|
|
|
|
|
@component
|
|
class ListStrProducer:
|
|
def __init__(self, values: list[str] | None = None):
|
|
self.values = values or ["Hello", "Hi"]
|
|
|
|
@component.output_types(texts=list[str])
|
|
def run(self) -> dict[str, list[str]]:
|
|
return {"texts": self.values}
|
|
|
|
|
|
@component
|
|
class ListStrAcceptor:
|
|
@component.output_types(result=list[str])
|
|
def run(self, texts: list[str]) -> dict[str, list[str]]:
|
|
return {"result": texts}
|
|
|
|
|
|
@component
|
|
class ChatMessageProducer:
|
|
def __init__(self, value: str = "Hello"):
|
|
self.value = value
|
|
|
|
@component.output_types(message=ChatMessage)
|
|
def run(self) -> dict[str, ChatMessage]:
|
|
return {"message": ChatMessage.from_user(self.value)}
|
|
|
|
|
|
@component
|
|
class ListChatMessageProducer:
|
|
def __init__(self, values: list[str] | None = None):
|
|
self.values = values or ["Hello", "Hi"]
|
|
|
|
@component.output_types(messages=list[ChatMessage])
|
|
def run(self) -> dict[str, list[ChatMessage]]:
|
|
return {"messages": [ChatMessage.from_user(v) for v in self.values]}
|
|
|
|
|
|
@component
|
|
class ListChatMessageAcceptor:
|
|
@component.output_types(result=list[ChatMessage])
|
|
def run(self, messages: list[ChatMessage]) -> dict[str, list[ChatMessage]]:
|
|
return {"result": messages}
|
|
|
|
|
|
@component
|
|
class WrongOutput:
|
|
@component.output_types(output=str)
|
|
def run(self, value: str) -> dict[str, str]:
|
|
return "not_a_dict" # type: ignore[return-value]
|
|
|
|
|
|
class TestPipeline:
|
|
"""
|
|
This class contains only unit tests for the Pipeline class.
|
|
|
|
It doesn't test Pipeline.run(), that is done separately in a different way.
|
|
"""
|
|
|
|
def test_pipeline_thread_safety(self, waiting_component, spying_tracer):
|
|
# Initialize pipeline with synchronous components
|
|
pp = Pipeline()
|
|
pp.add_component("wait", waiting_component())
|
|
|
|
run_data = [{"wait_for": 0.001}, {"wait_for": 0.002}]
|
|
|
|
# Use ThreadPoolExecutor to run pipeline calls in parallel
|
|
with ThreadPoolExecutor(max_workers=len(run_data)) as executor:
|
|
# Submit pipeline runs to the executor
|
|
futures = [executor.submit(pp.run, data) for data in run_data]
|
|
|
|
# Wait for all futures to complete
|
|
for future in futures:
|
|
future.result()
|
|
|
|
# Verify component visits using tracer
|
|
component_spans = [sp for sp in spying_tracer.spans if sp.operation_name == "haystack.component.run"]
|
|
|
|
for span in component_spans:
|
|
assert span.tags["haystack.component.visits"] == 1
|
|
|
|
def test_prepare_component_inputs(self):
|
|
pp = Pipeline()
|
|
component_name = "joiner_1"
|
|
pp.add_component(component_name, BranchJoiner(type_=str))
|
|
pp.add_component("joiner_2", BranchJoiner(type_=str))
|
|
pp.connect(component_name, "joiner_2")
|
|
inputs = {"joiner_1": {"value": [{"sender": None, "value": "test_value"}]}}
|
|
comp_dict = pp._get_component_with_graph_metadata_and_visits(component_name, 0)
|
|
|
|
_ = pp._consume_component_inputs(component_name=component_name, component=comp_dict, inputs=inputs)
|
|
# We remove input in greedy variadic sockets, even if they are from the user
|
|
assert inputs == {"joiner_1": {}}
|
|
|
|
def test__run_component_success(self):
|
|
"""Test successful component execution"""
|
|
pp = Pipeline()
|
|
component_name = "joiner_1"
|
|
pp.add_component(component_name, BranchJoiner(type_=str))
|
|
pp.add_component("joiner_2", BranchJoiner(type_=str))
|
|
pp.connect(component_name, "joiner_2")
|
|
|
|
outputs = pp._run_component(
|
|
component_name=component_name,
|
|
component=pp._get_component_with_graph_metadata_and_visits(component_name, 0),
|
|
inputs={"value": ["test_value"]},
|
|
component_visits={component_name: 0, "joiner_2": 0},
|
|
)
|
|
assert outputs == {"value": "test_value"}
|
|
|
|
def test__run_component_fail(self):
|
|
"""Test error when component doesn't return a dictionary"""
|
|
pp = Pipeline()
|
|
pp.add_component("wrong", WrongOutput())
|
|
|
|
with pytest.raises(PipelineRuntimeError) as exc_info:
|
|
pp._run_component(
|
|
component_name="wrong",
|
|
component=pp._get_component_with_graph_metadata_and_visits("wrong", 0),
|
|
inputs={"value": "test_value"},
|
|
component_visits={"wrong": 0},
|
|
)
|
|
assert "Expected a dict" in str(exc_info.value)
|
|
|
|
def test_run_component_error(self):
|
|
"""Test error when component fails to run"""
|
|
|
|
@component
|
|
class ErroringComponent:
|
|
@component.output_types(output=str)
|
|
def run(self):
|
|
raise ValueError("Test error")
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("erroring_component", ErroringComponent())
|
|
|
|
with pytest.raises(PipelineRuntimeError) as exc_info:
|
|
pp._run_component(
|
|
component_name="erroring_component",
|
|
component=pp._get_component_with_graph_metadata_and_visits("erroring_component", 0),
|
|
inputs={"wrong": {"value": [{"sender": None, "value": "test_value"}]}},
|
|
component_visits={"erroring_component": 0},
|
|
)
|
|
assert "Component name: 'erroring_component'" in str(exc_info.value)
|
|
|
|
def test_component_with_empty_dict_as_output_appears_in_results(self):
|
|
"""Test that components that return an empty dict as output appear in results as an empty dict"""
|
|
|
|
@component
|
|
class Producer:
|
|
def __init__(self, prefix: str):
|
|
self.prefix = prefix
|
|
|
|
@component.output_types(value=str | None)
|
|
def run(self, text: str | None) -> dict[str, str | None]:
|
|
return {"value": f"{self.prefix}: {text}"}
|
|
|
|
@component
|
|
class EmptyProcessor:
|
|
@component.output_types()
|
|
def run(self, sources: list[str]) -> dict:
|
|
# Returns empty dict when sources is empty
|
|
return {}
|
|
|
|
@component
|
|
class Combiner:
|
|
@component.output_types(combined=str)
|
|
def run(self, input_a: str | None, input_b: str | None) -> dict[str, str]:
|
|
if input_a is None:
|
|
input_a = ""
|
|
if input_b is None:
|
|
input_b = ""
|
|
return {"combined": f"{input_a} | {input_b}"}
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("producer_a", Producer("A"))
|
|
pp.add_component("producer_b", Producer("B"))
|
|
pp.add_component("empty_processor", EmptyProcessor())
|
|
pp.add_component("combiner", Combiner())
|
|
|
|
pp.connect("producer_a.value", "combiner.input_a")
|
|
pp.connect("producer_b.value", "combiner.input_b")
|
|
|
|
result = pp.run(
|
|
{"producer_a": {"text": "hello"}, "producer_b": {"text": "world"}, "empty_processor": {"sources": []}},
|
|
include_outputs_from={"producer_a", "empty_processor", "combiner"},
|
|
)
|
|
|
|
# Producer A should appear in results because it's in include_outputs_from
|
|
assert "producer_a" in result
|
|
assert result["producer_a"] == {"value": "A: hello"}
|
|
# Producer B should NOT appear since it's not in include_outputs_from
|
|
assert "producer_b" not in result
|
|
# Combiner should appear in results
|
|
assert "combiner" in result
|
|
assert result["combiner"] == {"combined": "A: hello | B: world"}
|
|
# Empty processor should appear in results even though it returns an empty dict
|
|
# because it's in include_outputs_from
|
|
assert "empty_processor" in result
|
|
assert result["empty_processor"] == {}
|
|
|
|
def test__run_component_warns_on_extra_output_keys(self, caplog):
|
|
"""Test that a warning is raised when a component returns undeclared output keys."""
|
|
caplog.set_level(logging.WARNING)
|
|
|
|
@component
|
|
class ExtraKeyComponent:
|
|
@component.output_types(output=str)
|
|
def run(self, value: str) -> dict[str, str]:
|
|
return {"output": value, "extra_key": "unexpected"}
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("extra", ExtraKeyComponent())
|
|
|
|
pp._run_component(
|
|
component_name="extra",
|
|
component=pp._get_component_with_graph_metadata_and_visits("extra", 0),
|
|
inputs={"value": "test"},
|
|
component_visits={"extra": 0},
|
|
)
|
|
assert "returned output keys" in caplog.text
|
|
assert "extra_key" in caplog.text
|
|
assert "not declared" in caplog.text
|
|
|
|
def test__run_component_no_warning_on_correct_output_keys(self, caplog):
|
|
"""Test that no warning is raised when a component returns the correct output keys."""
|
|
caplog.set_level(logging.WARNING)
|
|
|
|
@component
|
|
class CorrectComponent:
|
|
@component.output_types(output=str)
|
|
def run(self, value: str) -> dict[str, str]:
|
|
return {"output": value}
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("correct", CorrectComponent())
|
|
|
|
pp._run_component(
|
|
component_name="correct",
|
|
component=pp._get_component_with_graph_metadata_and_visits("correct", 0),
|
|
inputs={"value": "test"},
|
|
component_visits={"correct": 0},
|
|
)
|
|
assert "returned output keys" not in caplog.text
|
|
assert "did not produce output keys" not in caplog.text
|
|
|
|
def test_pipeline_is_possibly_blocked_warning_message(self, caplog):
|
|
"""
|
|
Test that the pipeline raises a warning when it is possibly blocked due to missing inputs.
|
|
|
|
The situation below looks a little contrived, but it has happened in practice that users create pipelines
|
|
and accidentally made a mistake in their component code.
|
|
"""
|
|
caplog.set_level(logging.WARNING)
|
|
|
|
@component
|
|
class MisconfiguredComponent:
|
|
# Here we purposely declare other_output which is not actually returned by the run() method
|
|
@component.output_types(output=str, other_output=str)
|
|
def run(self, required_input: str) -> dict[str, str]:
|
|
return {"output": "test"}
|
|
|
|
@component
|
|
class SimpleComponentTwoInputs:
|
|
@component.output_types(output=str)
|
|
def run(self, required_input: str, second_required_input: str) -> dict[str, str]:
|
|
return {"output": "test"}
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("first", MisconfiguredComponent())
|
|
pp.add_component("second", SimpleComponentTwoInputs())
|
|
|
|
# NOTE: We connect both outputs from the first component to the second component, but the first component
|
|
# doesn't actually produce other_output, so the second component will be blocked due to missing input.
|
|
pp.connect("first.output", "second.required_input")
|
|
pp.connect("first.other_output", "second.second_required_input")
|
|
|
|
pp.run({"first": {"required_input": "test"}})
|
|
assert "Cannot run pipeline - the pipeline appears to be blocked." in caplog.text
|
|
assert " - 'second' (SimpleComponentTwoInputs)" in caplog.text
|
|
|
|
def test_pipeline_ensure_inputs_are_deep_copied(self):
|
|
"""
|
|
Test to ensure that pipeline deep copies the inputs before passing them to components.
|
|
|
|
This is important to prevent unintended side effects when components modify their inputs especially when
|
|
the output from one component is passed to multiple other components.
|
|
|
|
Some other notes about how this situation can arise in practice:
|
|
- When a component returns a mutable object (like a Document) and that output is passed to multiple other
|
|
components.
|
|
- This doesn't happen when using output types like strings or integers, because they are not shared by
|
|
reference so we will only commonly see this for objects like our dataclasses.
|
|
"""
|
|
|
|
@component
|
|
class SimpleComponent:
|
|
@component.output_types(output=Document)
|
|
def run(self, document: Document) -> dict[str, Document]:
|
|
# Creates a new document to avoid modifying in place
|
|
new_document = Document(content=document.content)
|
|
return {"output": new_document}
|
|
|
|
@component
|
|
class ModifyingComponent:
|
|
@component.output_types(output=Document)
|
|
def run(self, document: Document) -> dict[str, Document]:
|
|
# Modifies the incoming document inplace
|
|
document.content = "modified"
|
|
return {"output": document}
|
|
|
|
pp = Pipeline()
|
|
pp.add_component("first", SimpleComponent())
|
|
pp.add_component("modifier", ModifyingComponent())
|
|
# It's important that the following component has a name lower down the alphabetical order than "modifier",
|
|
# since the pipeline runs components in a first-in-first-out manner based on ordered_component_names which is
|
|
# sorted alphabetically.
|
|
pp.add_component("second", SimpleComponent())
|
|
|
|
pp.connect("first.output", "modifier.document")
|
|
pp.connect("first.output", "second.document")
|
|
|
|
result = pp.run({"first": {"document": Document(content="original")}})
|
|
|
|
assert result["modifier"]["output"].content == "modified"
|
|
# Without deep copying the inputs, the second component would also see the modified document and produce
|
|
# "modified" instead of "original"
|
|
assert result["second"]["output"].content == "original"
|
|
|
|
def test_pipeline_does_not_corrupt_outputs(self):
|
|
"""
|
|
Test that a component's output collected via include_outputs_from is not corrupted when a downstream
|
|
component receives and mutates the same data in-place.
|
|
"""
|
|
|
|
@component
|
|
class Producer:
|
|
@component.output_types(doc=Document)
|
|
def run(self) -> dict:
|
|
return {"doc": Document(content="original")}
|
|
|
|
@component
|
|
class Mutator:
|
|
@component.output_types(doc=Document)
|
|
def run(self, doc: Document) -> dict:
|
|
# Modifies the incoming document inplace
|
|
doc.content = "mutated"
|
|
return {"doc": doc}
|
|
|
|
pipe = Pipeline()
|
|
pipe.add_component("producer", Producer())
|
|
pipe.add_component("mutator", Mutator())
|
|
pipe.connect("producer.doc", "mutator.doc")
|
|
|
|
result = pipe.run({}, include_outputs_from={"producer"})
|
|
|
|
assert result["producer"]["doc"].content == "original"
|
|
assert result["mutator"]["doc"].content == "mutated"
|
|
|
|
def test_auto_variadic_connection_to_agent(self):
|
|
@component
|
|
class MessageProducer:
|
|
@component.output_types(messages=list[ChatMessage])
|
|
def run(self) -> dict[str, list[ChatMessage]]:
|
|
return {"messages": [ChatMessage.from_user("Hello, world!")]}
|
|
|
|
p = Pipeline()
|
|
p.add_component("message_producer", MessageProducer())
|
|
p.add_component("message_producer2", MessageProducer())
|
|
p.add_component("agent", Agent(chat_generator=MockChatGenerator()))
|
|
p.connect("message_producer", "agent.messages")
|
|
p.connect("message_producer2", "agent.messages")
|
|
|
|
result = p.run({})
|
|
assert result["agent"]["messages"] == [
|
|
ChatMessage.from_user("Hello, world!"),
|
|
ChatMessage.from_user("Hello, world!"),
|
|
ChatMessage.from_assistant("Hello, world!"),
|
|
]
|
|
|
|
def test_run_auto_variadic_str_to_list_str(self):
|
|
"""Two str producers connected to a list[str] input are auto-joined and flattened at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("producer1", StringProducer("hello"))
|
|
p.add_component("producer2", StringProducer("world"))
|
|
p.add_component("receiver", ListStrAcceptor())
|
|
p.connect("producer1.text", "receiver.texts")
|
|
p.connect("producer2.text", "receiver.texts")
|
|
result = p.run({})
|
|
assert result["receiver"]["result"] == ["hello", "world"]
|
|
|
|
def test_run_auto_variadic_str_and_list_str_to_list_str(self):
|
|
"""A str producer and a list[str] producer connected to a list[str] input are auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("str_producer", StringProducer("hello"))
|
|
p.add_component("list_producer", ListStrProducer(["world", "!"]))
|
|
p.add_component("receiver", ListStrAcceptor())
|
|
p.connect("str_producer.text", "receiver.texts")
|
|
p.connect("list_producer.texts", "receiver.texts")
|
|
result = p.run({})
|
|
assert result["receiver"]["result"] == ["world", "!", "hello"]
|
|
|
|
def test_run_auto_variadic_chat_message_to_list_str(self):
|
|
"""Two ChatMessage producers connected to a list[str] input are converted and auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("producer1", ChatMessageProducer("hello"))
|
|
p.add_component("producer2", ChatMessageProducer("world"))
|
|
p.add_component("receiver", ListStrAcceptor())
|
|
p.connect("producer1.message", "receiver.texts")
|
|
p.connect("producer2.message", "receiver.texts")
|
|
result = p.run({})
|
|
assert result["receiver"]["result"] == ["hello", "world"]
|
|
|
|
def test_run_auto_variadic_str_and_chat_message_to_list_str(self):
|
|
"""A str producer and a ChatMessage producer connected to a list[str] input are auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("str_producer", StringProducer("hello"))
|
|
p.add_component("chat_producer", ChatMessageProducer("world"))
|
|
p.add_component("receiver", ListStrAcceptor())
|
|
p.connect("str_producer.text", "receiver.texts")
|
|
p.connect("chat_producer.message", "receiver.texts")
|
|
result = p.run({})
|
|
assert result["receiver"]["result"] == ["world", "hello"]
|
|
|
|
def test_run_auto_variadic_chat_message_to_list_chat_message(self):
|
|
"""Two ChatMessage producers connected to a list[ChatMessage] input are auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("producer1", ChatMessageProducer("hello"))
|
|
p.add_component("producer2", ChatMessageProducer("world"))
|
|
p.add_component("receiver", ListChatMessageAcceptor())
|
|
p.connect("producer1.message", "receiver.messages")
|
|
p.connect("producer2.message", "receiver.messages")
|
|
result = p.run({})
|
|
assert [m.text for m in result["receiver"]["result"]] == ["hello", "world"]
|
|
|
|
def test_run_auto_variadic_str_to_list_chat_message(self):
|
|
"""Two str producers connected to a list[ChatMessage] input are converted and auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("producer1", StringProducer("hello"))
|
|
p.add_component("producer2", StringProducer("world"))
|
|
p.add_component("receiver", ListChatMessageAcceptor())
|
|
p.connect("producer1.text", "receiver.messages")
|
|
p.connect("producer2.text", "receiver.messages")
|
|
result = p.run({})
|
|
assert [m.text for m in result["receiver"]["result"]] == ["hello", "world"]
|
|
|
|
def test_run_auto_variadic_str_and_chat_message_to_list_chat_message(self):
|
|
"""A str and a ChatMessage producer connected to a list[ChatMessage] input are auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("str_producer", StringProducer("hello"))
|
|
p.add_component("chat_producer", ChatMessageProducer("world"))
|
|
p.add_component("receiver", ListChatMessageAcceptor())
|
|
p.connect("str_producer.text", "receiver.messages")
|
|
p.connect("chat_producer.message", "receiver.messages")
|
|
result = p.run({})
|
|
assert [m.text for m in result["receiver"]["result"]] == ["world", "hello"]
|
|
|
|
def test_run_auto_variadic_chat_message_and_list_chat_message_to_list_chat_message(self):
|
|
"""A ChatMessage and a list[ChatMessage] producer connected to list[ChatMessage] are auto-joined at runtime."""
|
|
p = Pipeline()
|
|
p.add_component("chat_producer", ChatMessageProducer("hello"))
|
|
p.add_component("list_producer", ListChatMessageProducer(["world", "!"]))
|
|
p.add_component("receiver", ListChatMessageAcceptor())
|
|
p.connect("chat_producer.message", "receiver.messages")
|
|
p.connect("list_producer.messages", "receiver.messages")
|
|
result = p.run({})
|
|
assert [m.text for m in result["receiver"]["result"]] == ["hello", "world", "!"]
|
|
|
|
def test_connect_rejects_list_of_documents_to_single_document(self):
|
|
@component
|
|
class DocsProducer:
|
|
@component.output_types(docs=list[Document])
|
|
def run(self) -> dict[str, list[Document]]:
|
|
return {"docs": [Document(content="a"), Document(content="b"), Document(content="c")]}
|
|
|
|
@component
|
|
class DocConsumer:
|
|
@component.output_types(out=Document)
|
|
def run(self, doc: Document) -> dict[str, Document]:
|
|
return {"out": doc}
|
|
|
|
p = Pipeline()
|
|
p.add_component("producer", DocsProducer())
|
|
p.add_component("consumer", DocConsumer())
|
|
with pytest.raises(PipelineConnectError):
|
|
p.connect("producer.docs", "consumer.doc")
|
|
|
|
def test_run_raises_when_multi_element_list_is_unwrapped_at_runtime(self):
|
|
@component
|
|
class MultiStrProducer:
|
|
@component.output_types(texts=list[str])
|
|
def run(self) -> dict[str, list[str]]:
|
|
return {"texts": ["first", "second", "third"]}
|
|
|
|
@component
|
|
class SingleStrConsumer:
|
|
@component.output_types(out=str)
|
|
def run(self, text: str) -> dict[str, str]:
|
|
return {"out": text}
|
|
|
|
p = Pipeline()
|
|
p.add_component("producer", MultiStrProducer())
|
|
p.add_component("consumer", SingleStrConsumer())
|
|
p.connect("producer.texts", "consumer.text")
|
|
|
|
with pytest.raises(PipelineRuntimeError, match="Cannot unwrap a list of 3 items"):
|
|
p.run({})
|
|
|
|
def test_run_single_element_list_unwrap_still_works(self):
|
|
@component
|
|
class SingleStrProducer:
|
|
@component.output_types(texts=list[str])
|
|
def run(self) -> dict[str, list[str]]:
|
|
return {"texts": ["only-one"]}
|
|
|
|
@component
|
|
class SingleStrConsumer:
|
|
@component.output_types(out=str)
|
|
def run(self, text: str) -> dict[str, str]:
|
|
return {"out": text}
|
|
|
|
p = Pipeline()
|
|
p.add_component("producer", SingleStrProducer())
|
|
p.add_component("consumer", SingleStrConsumer())
|
|
p.connect("producer.texts", "consumer.text")
|
|
assert p.run({}) == {"consumer": {"out": "only-one"}}
|