-
Notifications
You must be signed in to change notification settings - Fork 8.3k
feat: add convert component with dynamic output support #7773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
db7d221
0d8388b
52d667c
a12ef7f
4658c14
bf30499
c3164f9
588fc41
765d40f
e65c60e
87ef657
3dd3307
2c0ba21
6ab11ff
00a4b16
7ff74ae
ec023e2
c02b3ef
c555554
2c97cab
db08285
b4e83cc
5343410
b779bd3
78b0701
d00eb5c
357d1e3
f0ff9df
13a8d70
08136b9
7705c72
bd01612
61370c1
d1a62ef
6e108ae
484faf6
54d66f5
33f467b
8613a67
9475ff6
19dcfdc
f7ef291
5953a6b
9d2ffae
3903d7b
835b7ce
821f9f0
5a2f9f6
b7314f0
53af8db
84ed405
dc498f6
619cd6a
1d8fc2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||
| import json | ||||||
|
|
||||||
| from langflow.schema import Data, DataFrame, Message | ||||||
|
|
||||||
| # Type conversion dispatchers | ||||||
| _message_converters = { | ||||||
| Message: lambda msg: Message(text=msg.get_text()), | ||||||
| Data: lambda data: Message(text=json.dumps(data.data)), | ||||||
| DataFrame: lambda df: Message(text=df.to_markdown(index=False)), | ||||||
| } | ||||||
|
|
||||||
| _data_converters = { | ||||||
| Message: lambda msg: Data(data=msg.data), | ||||||
|
||||||
| Message: lambda msg: Data(data=msg.data), | |
| Message: lambda msg: Data(data=msg.get_text()), |
Outdated
Copilot
AI
Apr 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda for Message in _dataframe_converters uses msg.data, but Message objects do not have a data attribute. Update this lambda to extract the text from the Message instead, for example by using msg.get_text().
| Message: lambda msg: DataFrame([dict(msg.data) if msg.data else {}]), | |
| Message: lambda msg: DataFrame([{"text": msg.get_text()} if msg.get_text() else {}]), |
edwinjosechittilappilly marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import json | ||
| from typing import Any | ||
|
|
||
| from langflow.base.processing.type_conversion import ( | ||
| get_data_converter, | ||
| get_dataframe_converter, | ||
| get_message_converter, | ||
| ) | ||
| from langflow.custom import Component | ||
| from langflow.io import HandleInput, Output, TabInput | ||
| from langflow.schema import Data, DataFrame | ||
| from langflow.schema.message import Message | ||
|
|
||
|
|
||
| class TypeConverterComponent(Component): | ||
| display_name = "Type Convert" | ||
| description = "Convert between different types (Message, Data, DataFrame)" | ||
| icon = "repeat" | ||
|
|
||
| # Class-level conversion dispatchers | ||
| _message_converters = get_message_converter() | ||
| _data_converters = get_data_converter() | ||
| _dataframe_converters = get_dataframe_converter() | ||
|
|
||
| inputs = [ | ||
| HandleInput( | ||
| name="input_data", | ||
| display_name="Input", | ||
| input_types=["Message", "Data", "DataFrame"], | ||
| info="Accept Message, Data or DataFrame as input", | ||
| required=True, | ||
| ), | ||
| TabInput( | ||
| name="output_type", | ||
| display_name="Output Type", | ||
| options=["Message", "Data", "DataFrame"], | ||
| info="Select the desired output data type", | ||
| real_time_refresh=True, | ||
| value="Message", | ||
| ), | ||
| ] | ||
|
|
||
| outputs = [Output(display_name="Message Output", name="message_output", method="convert_to_message")] | ||
|
|
||
| def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict: | ||
| """Dynamically show only the relevant output based on the selected output type.""" | ||
| if field_name == "output_type": | ||
| # Start with empty outputs | ||
| frontend_node["outputs"] = [] | ||
|
|
||
| # Add only the selected output type | ||
| if field_value == "Message": | ||
| frontend_node["outputs"].append( | ||
| Output(display_name="Message Output", name="message_output", method="convert_to_message").to_dict() | ||
| ) | ||
| elif field_value == "Data": | ||
| frontend_node["outputs"].append( | ||
| Output(display_name="Data Output", name="data_output", method="convert_to_data").to_dict() | ||
| ) | ||
| elif field_value == "DataFrame": | ||
| frontend_node["outputs"].append( | ||
| Output( | ||
| display_name="DataFrame Output", name="dataframe_output", method="convert_to_dataframe" | ||
| ).to_dict() | ||
| ) | ||
|
|
||
| return frontend_node | ||
|
|
||
| def _safe_convert(self, data: Any) -> str: | ||
| """Safely convert input data to string.""" | ||
| try: | ||
| if isinstance(data, str): | ||
| return data | ||
| if isinstance(data, Message): | ||
| return data.get_text() | ||
| if isinstance(data, Data): | ||
| return json.dumps(data.data) | ||
| if isinstance(data, DataFrame): | ||
| # Remove empty rows | ||
| data = data.dropna(how="all") | ||
| # Remove empty lines in each cell | ||
| data = data.replace(r"^\s*$", "", regex=True) | ||
| # Replace multiple newlines with a single newline | ||
| data = data.replace(r"\n+", "\n", regex=True) | ||
| return data.to_markdown(index=False) | ||
| return str(data) | ||
| except (ValueError, TypeError, AttributeError) as e: | ||
| msg = f"Error converting data: {e!s}" | ||
| raise ValueError(msg) from e | ||
|
|
||
| def convert_to_message(self) -> Message: | ||
| """Convert input data to string with proper error handling.""" | ||
| result = "" | ||
| if isinstance(self.input_data, list): | ||
| result = "\n".join([self._safe_convert(item) for item in self.input_data]) | ||
| else: | ||
| result = self._safe_convert(self.input_data) | ||
| self.log(f"Converted to string with length: {len(result)}") | ||
| message = Message(text=result) | ||
| self.status = message | ||
| return message | ||
|
|
||
| def convert_to_data(self) -> Data: | ||
| """Convert input to Data type.""" | ||
| input_data = self.input_data | ||
|
|
||
| converter = self._data_converters.get(type(input_data)) | ||
| if converter: | ||
| try: | ||
| return converter(input_data) | ||
| except (ValueError, TypeError, AttributeError) as e: | ||
| self.log(f"Error converting to Data: {e!s}") | ||
| return Data(data={"text": str(input_data)}) | ||
|
|
||
| # Default fallback | ||
| return Data(data={"value": str(input_data)}) | ||
|
|
||
| def convert_to_dataframe(self) -> DataFrame: | ||
| """Convert input to DataFrame type.""" | ||
| input_data = self.input_data | ||
| converter = self._dataframe_converters.get(type(input_data)) | ||
| if converter: | ||
| try: | ||
| return converter(input_data) | ||
| except (ValueError, TypeError, AttributeError) as e: | ||
| self.log(f"Error converting to DataFrame: {e!s}") | ||
| import pandas as pd | ||
|
|
||
| return DataFrame(pd.DataFrame({"value": [str(input_data)]})) | ||
|
|
||
| # Default fallback | ||
| import pandas as pd | ||
|
|
||
| return DataFrame(pd.DataFrame({"value": [str(input_data)]})) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||
| import pandas as pd | ||||||
| import pytest | ||||||
| from langflow.components.processing.convertor import TypeConverterComponent | ||||||
| from langflow.schema import Data, DataFrame, Message | ||||||
|
|
||||||
| from tests.base import ComponentTestBaseWithoutClient | ||||||
|
|
||||||
|
|
||||||
| class TestTypeConverterComponent(ComponentTestBaseWithoutClient): | ||||||
| @pytest.fixture | ||||||
| def component_class(self): | ||||||
| """Return the component class to test.""" | ||||||
| return TypeConverterComponent | ||||||
|
|
||||||
| @pytest.fixture | ||||||
| def file_names_mapping(self): | ||||||
| """Return an empty list since this component doesn't have version-specific files.""" | ||||||
| return [] | ||||||
|
|
||||||
| # Message to other types | ||||||
| def test_message_to_message(self, component_class): | ||||||
| """Test converting Message to Message.""" | ||||||
| component = component_class(input_data=Message(text="Hello World"), output_type="Message") | ||||||
| result = component.convert_to_message() | ||||||
| assert isinstance(result, Message) | ||||||
| assert result.text == "Hello World" | ||||||
|
|
||||||
| def test_message_to_data(self, component_class): | ||||||
| """Test converting Message to Data.""" | ||||||
| component = component_class(input_data=Message(text="Hello"), output_type="Data") | ||||||
| result = component.convert_to_data() | ||||||
| assert isinstance(result, Data) | ||||||
| assert result.data == {"text": "Hello"} | ||||||
|
|
||||||
| def test_message_to_dataframe(self, component_class): | ||||||
| """Test converting Message to DataFrame.""" | ||||||
| component = component_class(input_data=Message(text="Hello"), output_type="DataFrame") | ||||||
| result = component.convert_to_dataframe() | ||||||
| assert isinstance(result, DataFrame) | ||||||
| assert isinstance(result.data, pd.DataFrame) | ||||||
| assert "value" in result.data.columns | ||||||
| assert result.data.iloc[0]["value"] == "Hello" | ||||||
|
|
||||||
| # Data to other types | ||||||
| def test_data_to_message(self, component_class): | ||||||
| """Test converting Data to Message.""" | ||||||
| component = component_class(input_data=Data(data={"text": "Hello World"}), output_type="Message") | ||||||
| result = component.convert_to_message() | ||||||
| assert isinstance(result, Message) | ||||||
| assert result.text == "{'text': 'Hello World'}" | ||||||
|
||||||
| assert result.text == "{'text': 'Hello World'}" | |
| assert result.text == '{"text": "Hello World"}' |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a PR (#7412) that does something similar. What we need are classmethods (which I think we have most of them).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, should we build a component on top of this ?
Or should we keep this component on hold?