-
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 25 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
edwinjosechittilappilly marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| from collections.abc import AsyncIterator, Iterator | ||
| from typing import Any | ||
|
|
||
| from langflow.custom import Component | ||
| from langflow.io import HandleInput, Output, TabInput | ||
| from langflow.schema import Data, DataFrame, Message | ||
| from langflow.services.database.models.message.model import MessageBase | ||
|
|
||
|
|
||
| def get_message_converter(v) -> Message: | ||
| # If v is a instance of Message, then its fine | ||
| if isinstance(v, dict): | ||
| return Message(**v) | ||
| if isinstance(v, Message): | ||
| return v | ||
| if isinstance(v, str | AsyncIterator | Iterator): | ||
| return Message(text=v) | ||
| if isinstance(v, MessageBase): | ||
| return Message(**v.model_dump()) | ||
| if isinstance(v, DataFrame): | ||
|
||
| # Process DataFrame similar to the _safe_convert method | ||
| # Remove empty rows | ||
| processed_df = v.dropna(how="all") | ||
| # Remove empty lines in each cell | ||
| processed_df = processed_df.replace(r"^\s*$", "", regex=True) | ||
| # Replace multiple newlines with a single newline | ||
| processed_df = processed_df.replace(r"\n+", "\n", regex=True) | ||
| # Replace pipe characters to avoid markdown table issues | ||
| processed_df = processed_df.replace(r"\|", r"\\|", regex=True) | ||
| processed_df = processed_df.map(lambda x: str(x).replace("\n", "<br/>") if isinstance(x, str) else x) | ||
| # Convert to markdown and wrap in a Message | ||
| return Message(text=processed_df.to_markdown(index=False)) | ||
| if isinstance(v, Data): | ||
| if v.text_key in v.data: | ||
| return Message(text=v.get_text()) | ||
| return Message(text=str(v.data)) | ||
| msg = f"Invalid value type {type(v)}" | ||
| raise ValueError(msg) | ||
|
|
||
|
|
||
| def get_data_converter(v: DataFrame | Data | Message | dict) -> Data: | ||
| """Get the data conversion dispatcher.""" | ||
| if isinstance(v, DataFrame): | ||
| # Convert DataFrame to a list of dictionaries and wrap in a Data object | ||
| dict_list = v.to_dict(orient="records") | ||
| return Data(data={"results": dict_list}) | ||
| if isinstance(v, Message): | ||
| return Data(data=v.data) | ||
| if isinstance(v, dict): | ||
| return Data(data=v) | ||
| if not isinstance(v, Data): | ||
| msg = f"Invalid value type {type(v)} for input Expected Data." | ||
| raise ValueError(msg) # noqa: TRY004 | ||
| return v | ||
|
|
||
|
|
||
| def get_dataframe_converter(v: DataFrame | Data | Message | dict) -> DataFrame: | ||
| """Get the dataframe conversion dispatcher.""" | ||
| if isinstance(v, Data): | ||
| data_dict = v.data | ||
| # If data contains only one key and the value is a list of dictionaries, convert to DataFrame | ||
| if ( | ||
| len(data_dict) == 1 | ||
| and isinstance(next(iter(data_dict.values())), list) | ||
| and all(isinstance(item, dict) for item in next(iter(data_dict.values()))) | ||
| ): | ||
| return DataFrame(data=next(iter(data_dict.values()))) | ||
| return DataFrame(data=[v]) | ||
| if isinstance(v, Message): | ||
| return DataFrame(data=[v]) | ||
| if isinstance(v, dict): | ||
| return DataFrame(data=[v]) | ||
| if not isinstance(v, DataFrame): | ||
| msg = f"Invalid value type {type(v)}. Expected DataFrame." | ||
| raise ValueError(msg) # noqa: TRY004 | ||
| return v | ||
|
|
||
|
|
||
| class TypeConverterComponent(Component): | ||
| display_name = "Type Convert" | ||
| description = "Convert between different types (Message, Data, DataFrame)" | ||
| icon = "repeat" | ||
|
|
||
| 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 convert_to_message(self) -> Message: | ||
| """Convert input to Message type.""" | ||
| return get_message_converter(self.input_data) | ||
|
|
||
| def convert_to_data(self) -> Data: | ||
| """Convert input to Data type.""" | ||
| return get_data_converter(self.input_data) | ||
|
|
||
| def convert_to_dataframe(self) -> DataFrame: | ||
| """Convert input to DataFrame type.""" | ||
| return get_dataframe_converter(self.input_data) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| 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 "text" in 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 "text" in result.columns | ||
| assert result.iloc[0]["text"] == "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 == "Hello World" | ||
|
|
||
| def test_data_to_data(self, component_class): | ||
| """Test converting Data to Data.""" | ||
| component = component_class(input_data=Data(data={"key": "value"}), output_type="Data") | ||
| result = component.convert_to_data() | ||
| assert isinstance(result, Data) | ||
| assert result.data == {"key": "value"} | ||
|
|
||
| def test_data_to_dataframe(self, component_class): | ||
| """Test converting Data to DataFrame.""" | ||
| component = component_class(input_data=Data(data={"text": "Hello World"}), output_type="DataFrame") | ||
| result = component.convert_to_dataframe() | ||
| assert isinstance(result, DataFrame) | ||
| assert "text" in result.columns | ||
| assert result.iloc[0]["text"] == "Hello World" | ||
|
|
||
| # DataFrame to other types | ||
| def test_dataframe_to_message(self, component_class): | ||
| """Test converting DataFrame to Message.""" | ||
| df_data = pd.DataFrame({"col1": ["Hello"], "col2": ["World"]}) | ||
| component = component_class(input_data=DataFrame(data=df_data), output_type="Message") | ||
| result = component.convert_to_message() | ||
| assert isinstance(result, Message) | ||
| assert result.text == "| col1 | col2 |\n|:-------|:-------|\n| Hello | World |" | ||
|
|
||
| def test_dataframe_to_data(self, component_class): | ||
| """Test converting DataFrame to Data.""" | ||
| df_data = pd.DataFrame({"col1": ["Hello"]}) | ||
| component = component_class(input_data=DataFrame(data=df_data), output_type="Data") | ||
| result = component.convert_to_data() | ||
| assert isinstance(result, Data) | ||
| assert isinstance(result.data, dict) | ||
|
|
||
| def test_dataframe_to_dataframe(self, component_class): | ||
| """Test converting DataFrame to DataFrame.""" | ||
| df_data = pd.DataFrame({"col1": ["Hello"], "col2": ["World"]}) | ||
| component = component_class(input_data=DataFrame(data=df_data), output_type="DataFrame") | ||
| result = component.convert_to_dataframe() | ||
| assert isinstance(result, DataFrame) | ||
| assert "col1" in result.columns | ||
| assert "col2" in result.columns | ||
| assert result.iloc[0]["col1"] == "Hello" | ||
| assert result.iloc[0]["col2"] == "World" | ||
|
|
||
| def test_update_outputs(self, component_class): | ||
| """Test the update_outputs method.""" | ||
| component = component_class(input_data=Message(text="Hello"), output_type="Message") | ||
| frontend_node = {"outputs": []} | ||
|
|
||
| # Test with Message output | ||
| updated = component.update_outputs(frontend_node, "output_type", "Message") | ||
| assert len(updated["outputs"]) == 1 | ||
| assert updated["outputs"][0]["name"] == "message_output" | ||
|
|
||
| # Test with Data output | ||
| updated = component.update_outputs(frontend_node, "output_type", "Data") | ||
| assert len(updated["outputs"]) == 1 | ||
| assert updated["outputs"][0]["name"] == "data_output" | ||
|
|
||
| # Test with DataFrame output | ||
| updated = component.update_outputs(frontend_node, "output_type", "DataFrame") | ||
| assert len(updated["outputs"]) == 1 | ||
| assert updated["outputs"][0]["name"] == "dataframe_output" |
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.
Nit: shouldn't it be converter?
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.
Will Do