mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 20:22:46 +08:00
fix: accept Data and Message inputs in LoopComponent (#13636)
LoopComponent only declared input_types=["DataFrame", "Table"] on its `data` handle, even though the class is documented to iterate over Data or Message objects and ships a `_convert_message_to_data` helper. Any Message-producing component (ChatInput, Agent, ...) was therefore rejected at connect time, which made agent-assisted flow builders retry until they exhausted the LangGraph recursion limit and crashed. Changes: - Add "Data" and "Message" to the `data` handle input_types (covers Data/JSON via TYPE_MIGRATIONS) and clarify the info text. - Declare explicit output types: item -> ["Data"], done -> ["DataFrame", "Table"], so describe_component and the registry surface type metadata for downstream planning. - Convert Message inputs to a clean Data object in `_validate_data` before validation. Message subclasses Data, so without this it was accepted verbatim as a single Data carrying the whole message envelope instead of its payload. - Regenerate the component index, the "Research Translation Loop" starter project, and the en.json locale string to match. Add lfx regression tests covering the input/output type metadata, the connect-time compatibility check, and the Message -> Data conversion.
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -4411,7 +4411,7 @@
|
||||
"components.loopcomponent.description.dc14755d": "Iterates through Data or Message objects, processing items individually and aggregating results from loop inputs.",
|
||||
"components.loopcomponent.display_name.f2f6a018": "Loop",
|
||||
"components.loopcomponent.inputs.data.display_name.7abc49df": "Inputs",
|
||||
"components.loopcomponent.inputs.data.info.bb8c0783": "The initial DataFrame to iterate over.",
|
||||
"components.loopcomponent.inputs.data.info.cb3819f9": "Data to iterate over. Accepts DataFrame, Table, Data, or Message.",
|
||||
"components.loopcomponent.outputs.done.display_name.11a6767d": "Done",
|
||||
"components.loopcomponent.outputs.item.display_name.652bcc3a": "Item",
|
||||
"components.maritalk.description.f2ad2e6b": "Generates text using MariTalk LLMs.",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -28,8 +28,8 @@ class LoopComponent(Component):
|
||||
HandleInput(
|
||||
name="data",
|
||||
display_name="Inputs",
|
||||
info="The initial DataFrame to iterate over.",
|
||||
input_types=["DataFrame", "Table"],
|
||||
info="Data to iterate over. Accepts DataFrame, Table, Data, or Message.",
|
||||
input_types=["DataFrame", "Table", "Data", "Message"],
|
||||
),
|
||||
]
|
||||
|
||||
@ -38,11 +38,18 @@ class LoopComponent(Component):
|
||||
display_name="Item",
|
||||
name="item",
|
||||
method="item_output",
|
||||
types=["Data"],
|
||||
allows_loop=True,
|
||||
loop_types=["Message"],
|
||||
group_outputs=True,
|
||||
),
|
||||
Output(display_name="Done", name="done", method="done_output", group_outputs=True),
|
||||
Output(
|
||||
display_name="Done",
|
||||
name="done",
|
||||
method="done_output",
|
||||
types=["DataFrame", "Table"],
|
||||
group_outputs=True,
|
||||
),
|
||||
]
|
||||
|
||||
def initialize_data(self) -> None:
|
||||
@ -71,7 +78,16 @@ class LoopComponent(Component):
|
||||
return convert_to_data(message, auto_parse=False)
|
||||
|
||||
def _validate_data(self, data):
|
||||
"""Validate and return a list of Data objects."""
|
||||
"""Validate and return a list of Data objects.
|
||||
|
||||
Message inputs (e.g. from ChatInput or Agent) are converted to a clean
|
||||
Data object first so iteration sees the message payload rather than the
|
||||
full Message envelope. `Message` subclasses `Data`, so without this it
|
||||
would be accepted verbatim as a single Data carrying all of the
|
||||
message metadata. `validate_data_input` handles DataFrame/Data/list.
|
||||
"""
|
||||
if isinstance(data, Message):
|
||||
data = self._convert_message_to_data(data)
|
||||
return validate_data_input(data)
|
||||
|
||||
def get_loop_body_vertices(self) -> set[str]:
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
"""Regression tests for LoopComponent input/output type metadata.
|
||||
|
||||
Covers the bug where the Loop's ``data`` handle only declared
|
||||
``input_types=["DataFrame", "Table"]`` even though the component is
|
||||
documented to iterate over ``Data`` or ``Message`` objects (and ships a
|
||||
``_convert_message_to_data`` helper). The missing types caused any
|
||||
Message/Data-producing component (ChatInput, Agent, ...) to be rejected at
|
||||
connect time, which made agent-assisted flow builders retry until they hit
|
||||
the LangGraph recursion limit. See issue #13636.
|
||||
"""
|
||||
|
||||
from lfx.components.flow_controls.loop import LoopComponent
|
||||
from lfx.graph.edge.base import types_compatible
|
||||
from lfx.schema.data import Data
|
||||
from lfx.schema.dataframe import DataFrame
|
||||
from lfx.schema.message import Message
|
||||
|
||||
|
||||
def _data_input(loop: LoopComponent):
|
||||
return next(i for i in loop.inputs if i.name == "data")
|
||||
|
||||
|
||||
def _output(loop: LoopComponent, name: str):
|
||||
return next(o for o in loop.outputs if o.name == name)
|
||||
|
||||
|
||||
def test_data_input_accepts_data_and_message():
|
||||
"""The data handle must accept Data and Message alongside DataFrame/Table."""
|
||||
loop = LoopComponent()
|
||||
input_types = _data_input(loop).input_types
|
||||
assert "DataFrame" in input_types
|
||||
assert "Table" in input_types
|
||||
assert "Data" in input_types
|
||||
assert "Message" in input_types
|
||||
|
||||
|
||||
def test_outputs_declare_types():
|
||||
"""Both outputs must advertise their types so agents can plan downstream."""
|
||||
loop = LoopComponent()
|
||||
assert _output(loop, "item").types == ["Data"]
|
||||
assert _output(loop, "done").types == ["DataFrame", "Table"]
|
||||
|
||||
|
||||
def test_message_outputs_are_connectable_to_loop():
|
||||
"""A Message- or Data-producing output must be compatible with Loop.data.
|
||||
|
||||
This is the connect-time check that previously failed and triggered the
|
||||
agent-builder retry loop.
|
||||
"""
|
||||
loop_input = _data_input(LoopComponent()).input_types
|
||||
# ChatInput / Agent emit Message; Agent.structured_response emits Data/JSON.
|
||||
assert types_compatible(["Message"], loop_input)
|
||||
assert types_compatible(["Data", "JSON"], loop_input)
|
||||
assert types_compatible(["JSON"], loop_input)
|
||||
assert types_compatible(["DataFrame"], loop_input)
|
||||
|
||||
|
||||
def test_validate_data_converts_message_to_clean_data():
|
||||
"""A Message input is converted to a Data carrying just its payload.
|
||||
|
||||
Message subclasses Data, so without the explicit conversion it would be
|
||||
accepted verbatim as a single Data object holding the whole message
|
||||
envelope (sender, session_id, timestamp, ...) instead of the payload.
|
||||
"""
|
||||
loop = LoopComponent()
|
||||
validated = loop._validate_data(Message(text="hello world"))
|
||||
|
||||
assert isinstance(validated, list)
|
||||
assert len(validated) == 1
|
||||
assert isinstance(validated[0], Data)
|
||||
assert validated[0].data == {"text": "hello world"}
|
||||
|
||||
|
||||
def test_validate_data_still_handles_dataframe():
|
||||
"""The existing DataFrame path is preserved."""
|
||||
loop = LoopComponent()
|
||||
validated = loop._validate_data(DataFrame([Data(text="a"), Data(text="b")]))
|
||||
|
||||
assert isinstance(validated, list)
|
||||
assert len(validated) == 2
|
||||
assert all(isinstance(item, Data) for item in validated)
|
||||
Reference in New Issue
Block a user