From 70b8f2909925fb505db9e220cb74a86dfcef66a9 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Thu, 22 May 2025 18:44:48 -0400 Subject: [PATCH] feat: Loop uplift dataframe input and output (#8177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests cases * update to loop * Update component.py * 📝 (LoopTemplate.json): update value of a configuration key from "OPENAI_API_KEY" to "ANTHROPIC_API_KEY" in order to reflect the correct API key being used * update json test loop * add dataframe support for the loop component * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * fix: starter project * update loop component and tests * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * update logic * Update loop_basic.py * Update Research Translation Loop.json * fix lint * format fix * [autofix.ci] apply automated fixes * reverting changes in component and vertex base * [autofix.ci] apply automated fixes * fix lint errors * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * update in the loop templates and component * [autofix.ci] apply automated fixes * Update Research Translation Loop.json * update tests * update the code and deprecate the old loop * [autofix.ci] apply automated fixes * Update loop_basic.py * WIP FIX Loop Tests * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * ✨ (loop-component.spec.ts): Update test cases to use more descriptive names for components and actions for better clarity and understanding. * ✨ (loop-component.spec.ts): refactor loop component tests to improve readability and maintainability by updating test selectors and removing redundant test steps * update * Update loop-component.spec.ts * Update Research Translation Loop.json * Update Research Translation Loop.json * Update Research Translation Loop.json * Update Research Translation Loop.json * loop test fix --------- Co-authored-by: cristhianzl Co-authored-by: Rodrigo Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: italojohnny Co-authored-by: Mike Fortman --- .../base/langflow/components/logic/loop.py | 31 +- ...te.json => Research Translation Loop.json} | 758 +++++++----------- .../tests/unit/components/logic/test_loop.py | 6 +- .../extended/features/loop-component.spec.ts | 60 +- 4 files changed, 339 insertions(+), 516 deletions(-) rename src/backend/base/langflow/initial_setup/starter_projects/{LoopTemplate.json => Research Translation Loop.json} (76%) diff --git a/src/backend/base/langflow/components/logic/loop.py b/src/backend/base/langflow/components/logic/loop.py index 781eb0e73..462883e19 100644 --- a/src/backend/base/langflow/components/logic/loop.py +++ b/src/backend/base/langflow/components/logic/loop.py @@ -1,6 +1,7 @@ from langflow.custom import Component -from langflow.io import DataInput, Output +from langflow.io import HandleInput, Output from langflow.schema import Data +from langflow.schema.dataframe import DataFrame class LoopComponent(Component): @@ -11,10 +12,11 @@ class LoopComponent(Component): icon = "infinity" inputs = [ - DataInput( + HandleInput( name="data", - display_name="Data", - info="The initial list of Data objects to iterate over.", + display_name="Data or DataFrame", + info="The initial list of Data objects or DataFrame to iterate over.", + input_types=["Data", "DataFrame"], ), ] @@ -43,11 +45,13 @@ class LoopComponent(Component): def _validate_data(self, data): """Validate and return a list of Data objects.""" + if isinstance(data, DataFrame): + return data.to_data_list() if isinstance(data, Data): return [data] if isinstance(data, list) and all(isinstance(item, Data) for item in data): return data - msg = "The 'data' input must be a list of Data objects or a single Data object." + msg = "The 'data' input must be a DataFrame, a list of Data objects, or a single Data object." raise TypeError(msg) def evaluate_stop_loop(self) -> bool: @@ -77,7 +81,7 @@ class LoopComponent(Component): self.update_ctx({f"{self._id}_index": current_index + 1}) return current_item - def done_output(self) -> Data: + def done_output(self) -> DataFrame: """Trigger the done output when iteration is complete.""" self.initialize_data() @@ -85,9 +89,11 @@ class LoopComponent(Component): self.stop("item") self.start("done") - return self.ctx.get(f"{self._id}_aggregated", []) + aggregated = self.ctx.get(f"{self._id}_aggregated", []) + + return DataFrame(aggregated) self.stop("done") - return Data(text="") + return DataFrame([]) def loop_variables(self): """Retrieve loop variables from context.""" @@ -96,16 +102,15 @@ class LoopComponent(Component): self.ctx.get(f"{self._id}_index", 0), ) - def aggregated_output(self) -> Data: + def aggregated_output(self) -> list[Data]: """Return the aggregated list once all items are processed.""" self.initialize_data() # Get data list and aggregated list data_list = self.ctx.get(f"{self._id}_data", []) aggregated = self.ctx.get(f"{self._id}_aggregated", []) - - # Check if loop input is provided and append to aggregated list - if self.item is not None and not isinstance(self.item, str) and len(aggregated) <= len(data_list): - aggregated.append(self.item) + loop_input = self.item + if loop_input is not None and not isinstance(loop_input, str) and len(aggregated) <= len(data_list): + aggregated.append(loop_input) self.update_ctx({f"{self._id}_aggregated": aggregated}) return aggregated diff --git a/src/backend/base/langflow/initial_setup/starter_projects/LoopTemplate.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json similarity index 76% rename from src/backend/base/langflow/initial_setup/starter_projects/LoopTemplate.json rename to src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json index 4bc953b97..4b01bc2d0 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/LoopTemplate.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json @@ -7,7 +7,7 @@ "data": { "sourceHandle": { "dataType": "AnthropicModel", - "id": "AnthropicModel-VobAp", + "id": "AnthropicModel-G2MkR", "name": "text_output", "output_types": [ "Message" @@ -15,91 +15,149 @@ }, "targetHandle": { "fieldName": "message", - "id": "MessagetoData-8vLbf", + "id": "MessagetoData-jYebP", "inputTypes": [ "Message" ], "type": "str" } }, - "id": "reactflow__edge-AnthropicModel-VobAp{œdataTypeœ:œAnthropicModelœ,œidœ:œAnthropicModel-VobApœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-MessagetoData-8vLbf{œfieldNameœ:œmessageœ,œidœ:œMessagetoData-8vLbfœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-AnthropicModel-G2MkR{œdataTypeœ:œAnthropicModelœ,œidœ:œAnthropicModel-G2MkRœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-MessagetoData-jYebP{œfieldNameœ:œmessageœ,œidœ:œMessagetoData-jYebPœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", "selected": false, - "source": "AnthropicModel-VobAp", - "sourceHandle": "{œdataTypeœ: œAnthropicModelœ, œidœ: œAnthropicModel-VobApœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", - "target": "MessagetoData-8vLbf", - "targetHandle": "{œfieldNameœ: œmessageœ, œidœ: œMessagetoData-8vLbfœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" + "source": "AnthropicModel-G2MkR", + "sourceHandle": "{œdataTypeœ: œAnthropicModelœ, œidœ: œAnthropicModel-G2MkRœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", + "target": "MessagetoData-jYebP", + "targetHandle": "{œfieldNameœ: œmessageœ, œidœ: œMessagetoData-jYebPœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { - "dataType": "MessagetoData", - "id": "MessagetoData-8vLbf", - "name": "data", + "dataType": "ChatInput", + "id": "ChatInput-5o3G0", + "name": "message", "output_types": [ - "Data" + "Message" ] }, "targetHandle": { - "dataType": "LoopComponent", - "id": "LoopComponent-0aBsp", - "name": "item", - "output_types": [ - "Data" - ] - } - }, - "id": "reactflow__edge-MessagetoData-8vLbf{œdataTypeœ:œMessagetoDataœ,œidœ:œMessagetoData-8vLbfœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-LoopComponent-0aBsp{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-0aBspœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}", - "selected": false, - "source": "MessagetoData-8vLbf", - "sourceHandle": "{œdataTypeœ: œMessagetoDataœ, œidœ: œMessagetoData-8vLbfœ, œnameœ: œdataœ, œoutput_typesœ: [œDataœ]}", - "target": "LoopComponent-0aBsp", - "targetHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-0aBspœ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ]}" - }, - { - "animated": false, - "className": "", - "data": { - "sourceHandle": { - "dataType": "LoopComponent", - "id": "LoopComponent-0aBsp", - "name": "done", - "output_types": [ - "Data" - ] - }, - "targetHandle": { - "fieldName": "data", - "id": "ParseData-J2Axc", + "fieldName": "search_query", + "id": "ArXivComponent-ylKFb", "inputTypes": [ - "Data" + "Message" ], - "type": "other" + "type": "str" } }, - "id": "reactflow__edge-LoopComponent-0aBsp{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-0aBspœ,œnameœ:œdoneœ,œoutput_typesœ:[œDataœ]}-ParseData-J2Axc{œfieldNameœ:œdataœ,œidœ:œParseData-J2Axcœ,œinputTypesœ:[œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-ChatInput-5o3G0{œdataTypeœ:œChatInputœ,œidœ:œChatInput-5o3G0œ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-ArXivComponent-ylKFb{œfieldNameœ:œsearch_queryœ,œidœ:œArXivComponent-ylKFbœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", "selected": false, - "source": "LoopComponent-0aBsp", - "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-0aBspœ, œnameœ: œdoneœ, œoutput_typesœ: [œDataœ]}", - "target": "ParseData-J2Axc", - "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œParseData-J2Axcœ, œinputTypesœ: [œDataœ], œtypeœ: œotherœ}" + "source": "ChatInput-5o3G0", + "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-5o3G0œ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", + "target": "ArXivComponent-ylKFb", + "targetHandle": "{œfieldNameœ: œsearch_queryœ, œidœ: œArXivComponent-ylKFbœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { - "dataType": "ParseData", - "id": "ParseData-J2Axc", - "name": "text", + "dataType": "ParserComponent", + "id": "ParserComponent-ZhlLQ", + "name": "parsed_text", "output_types": [ "Message" ] }, "targetHandle": { "fieldName": "input_value", - "id": "ChatOutput-01dLy", + "id": "AnthropicModel-G2MkR", + "inputTypes": [ + "Message" + ], + "type": "str" + } + }, + "id": "reactflow__edge-ParserComponent-ZhlLQ{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-ZhlLQœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-AnthropicModel-G2MkR{œfieldNameœ:œinput_valueœ,œidœ:œAnthropicModel-G2MkRœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "selected": false, + "source": "ParserComponent-ZhlLQ", + "sourceHandle": "{œdataTypeœ: œParserComponentœ, œidœ: œParserComponent-ZhlLQœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", + "target": "AnthropicModel-G2MkR", + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œAnthropicModel-G2MkRœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "ArXivComponent", + "id": "ArXivComponent-ylKFb", + "name": "dataframe", + "output_types": [ + "DataFrame" + ] + }, + "targetHandle": { + "fieldName": "data", + "id": "LoopComponent-9BK8B", + "inputTypes": [ + "Data", + "DataFrame" + ], + "type": "other" + } + }, + "id": "xy-edge__ArXivComponent-ylKFb{œdataTypeœ:œArXivComponentœ,œidœ:œArXivComponent-ylKFbœ,œnameœ:œdataframeœ,œoutput_typesœ:[œDataFrameœ]}-LoopComponent-9BK8B{œfieldNameœ:œdataœ,œidœ:œLoopComponent-9BK8Bœ,œinputTypesœ:[œDataœ,œDataFrameœ],œtypeœ:œotherœ}", + "selected": false, + "source": "ArXivComponent-ylKFb", + "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-ylKFbœ, œnameœ: œdataframeœ, œoutput_typesœ: [œDataFrameœ]}", + "target": "LoopComponent-9BK8B", + "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-9BK8Bœ, œinputTypesœ: [œDataœ, œDataFrameœ], œtypeœ: œotherœ}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "LoopComponent", + "id": "LoopComponent-9BK8B", + "name": "item", + "output_types": [ + "Data" + ] + }, + "targetHandle": { + "fieldName": "input_data", + "id": "ParserComponent-ZhlLQ", + "inputTypes": [ + "DataFrame", + "Data" + ], + "type": "other" + } + }, + "id": "xy-edge__LoopComponent-9BK8B{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-9BK8Bœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}-ParserComponent-ZhlLQ{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-ZhlLQœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "selected": false, + "source": "LoopComponent-9BK8B", + "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-9BK8Bœ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ]}", + "target": "ParserComponent-ZhlLQ", + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-ZhlLQœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "LoopComponent", + "id": "LoopComponent-9BK8B", + "name": "done", + "output_types": [ + "DataFrame" + ] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-oTh2r", "inputTypes": [ "Data", "DataFrame", @@ -108,128 +166,46 @@ "type": "str" } }, - "id": "reactflow__edge-ParseData-J2Axc{œdataTypeœ:œParseDataœ,œidœ:œParseData-J2Axcœ,œnameœ:œtextœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-01dLy{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-01dLyœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__LoopComponent-9BK8B{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-9BK8Bœ,œnameœ:œdoneœ,œoutput_typesœ:[œDataFrameœ]}-ChatOutput-oTh2r{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-oTh2rœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, - "source": "ParseData-J2Axc", - "sourceHandle": "{œdataTypeœ: œParseDataœ, œidœ: œParseData-J2Axcœ, œnameœ: œtextœ, œoutput_typesœ: [œMessageœ]}", - "target": "ChatOutput-01dLy", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-01dLyœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "source": "LoopComponent-9BK8B", + "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-9BK8Bœ, œnameœ: œdoneœ, œoutput_typesœ: [œDataFrameœ]}", + "target": "ChatOutput-oTh2r", + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-oTh2rœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { - "dataType": "ChatInput", - "id": "ChatInput-HBlBP", - "name": "message", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "search_query", - "id": "ArXivComponent-sSqo4", - "inputTypes": [ - "Message" - ], - "type": "str" - } - }, - "id": "reactflow__edge-ChatInput-HBlBP{œdataTypeœ:œChatInputœ,œidœ:œChatInput-HBlBPœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-ArXivComponent-sSqo4{œfieldNameœ:œsearch_queryœ,œidœ:œArXivComponent-sSqo4œ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", - "selected": false, - "source": "ChatInput-HBlBP", - "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-HBlBPœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", - "target": "ArXivComponent-sSqo4", - "targetHandle": "{œfieldNameœ: œsearch_queryœ, œidœ: œArXivComponent-sSqo4œ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" - }, - { - "animated": false, - "data": { - "sourceHandle": { - "dataType": "ArXivComponent", - "id": "ArXivComponent-sSqo4", + "dataType": "MessagetoData", + "id": "MessagetoData-jYebP", "name": "data", "output_types": [ "Data" ] }, "targetHandle": { - "fieldName": "data", - "id": "LoopComponent-0aBsp", - "inputTypes": [ - "Data" - ], - "type": "other" - } - }, - "id": "xy-edge__ArXivComponent-sSqo4{œdataTypeœ:œArXivComponentœ,œidœ:œArXivComponent-sSqo4œ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-LoopComponent-0aBsp{œfieldNameœ:œdataœ,œidœ:œLoopComponent-0aBspœ,œinputTypesœ:[œDataœ],œtypeœ:œotherœ}", - "selected": false, - "source": "ArXivComponent-sSqo4", - "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-sSqo4œ, œnameœ: œdataœ, œoutput_typesœ: [œDataœ]}", - "target": "LoopComponent-0aBsp", - "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-0aBspœ, œinputTypesœ: [œDataœ], œtypeœ: œotherœ}" - }, - { - "animated": false, - "data": { - "sourceHandle": { - "dataType": "parser", - "id": "parser-lOwhH", - "name": "parsed_text", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "input_value", - "id": "AnthropicModel-VobAp", - "inputTypes": [ - "Message" - ], - "type": "str" - } - }, - "id": "xy-edge__parser-lOwhH{œdataTypeœ:œparserœ,œidœ:œparser-lOwhHœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-AnthropicModel-VobAp{œfieldNameœ:œinput_valueœ,œidœ:œAnthropicModel-VobApœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", - "selected": false, - "source": "parser-lOwhH", - "sourceHandle": "{œdataTypeœ: œparserœ, œidœ: œparser-lOwhHœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", - "target": "AnthropicModel-VobAp", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œAnthropicModel-VobApœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" - }, - { - "animated": false, - "data": { - "sourceHandle": { "dataType": "LoopComponent", - "id": "LoopComponent-0aBsp", + "id": "LoopComponent-9BK8B", "name": "item", "output_types": [ "Data" ] - }, - "targetHandle": { - "fieldName": "input_data", - "id": "parser-lOwhH", - "inputTypes": [ - "DataFrame", - "Data" - ], - "type": "other" } }, - "id": "xy-edge__LoopComponent-0aBsp{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-0aBspœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}-parser-lOwhH{œfieldNameœ:œinput_dataœ,œidœ:œparser-lOwhHœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "xy-edge__MessagetoData-jYebP{œdataTypeœ:œMessagetoDataœ,œidœ:œMessagetoData-jYebPœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-LoopComponent-9BK8B{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-9BK8Bœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}", "selected": false, - "source": "LoopComponent-0aBsp", - "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-0aBspœ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ]}", - "target": "parser-lOwhH", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-lOwhHœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "source": "MessagetoData-jYebP", + "sourceHandle": "{œdataTypeœ: œMessagetoDataœ, œidœ: œMessagetoData-jYebPœ, œnameœ: œdataœ, œoutput_typesœ: [œDataœ]}", + "target": "LoopComponent-9BK8B", + "targetHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-9BK8Bœ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ]}" } ], "nodes": [ { "data": { - "id": "ArXivComponent-sSqo4", + "id": "ArXivComponent-ylKFb", "node": { "base_classes": [ "Data" @@ -249,7 +225,7 @@ "frozen": false, "icon": "arXiv", "legacy": false, - "lf_version": "1.1.5", + "lf_version": "1.4.2", "metadata": {}, "minimized": false, "output_types": [], @@ -376,7 +352,7 @@ "type": "ArXivComponent" }, "dragging": false, - "id": "ArXivComponent-sSqo4", + "id": "ArXivComponent-ylKFb", "measured": { "height": 443, "width": 320 @@ -390,124 +366,7 @@ }, { "data": { - "id": "LoopComponent-0aBsp", - "node": { - "base_classes": [ - "Data" - ], - "beta": false, - "category": "logic", - "conditional_paths": [], - "custom_fields": {}, - "description": "Iterates over a list of Data objects, outputting one item at a time and aggregating results from loop inputs.", - "display_name": "Loop", - "documentation": "", - "edited": false, - "field_order": [ - "data" - ], - "frozen": false, - "icon": "infinity", - "key": "LoopComponent", - "legacy": false, - "lf_version": "1.1.5", - "metadata": {}, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": true, - "cache": true, - "display_name": "Item", - "method": "item_output", - "name": "item", - "selected": "Data", - "tool_mode": true, - "types": [ - "Data" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Done", - "method": "done_output", - "name": "done", - "selected": "Data", - "tool_mode": true, - "types": [ - "Data" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "score": 2.220446049250313e-16, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from langflow.custom import Component\nfrom langflow.io import DataInput, Output\nfrom langflow.schema import Data\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates over a list of Data objects, outputting one item at a time and aggregating results from loop inputs.\"\n )\n icon = \"infinity\"\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The initial list of Data objects to iterate over.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Item\", name=\"item\", method=\"item_output\", allows_loop=True),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\"),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n if isinstance(data, Data):\n return [data]\n if isinstance(data, list) and all(isinstance(item, Data) for item in data):\n return data\n msg = \"The 'data' input must be a list of Data objects or a single Data object.\"\n raise TypeError(msg)\n\n def evaluate_stop_loop(self) -> bool:\n \"\"\"Evaluate whether to stop item or done output.\"\"\"\n current_index = self.ctx.get(f\"{self._id}_index\", 0)\n data_length = len(self.ctx.get(f\"{self._id}_data\", []))\n return current_index > data_length\n\n def item_output(self) -> Data:\n \"\"\"Output the next item in the list or stop if done.\"\"\"\n self.initialize_data()\n current_item = Data(text=\"\")\n\n if self.evaluate_stop_loop():\n self.stop(\"item\")\n return Data(text=\"\")\n\n # Get data list and current index\n data_list, current_index = self.loop_variables()\n if current_index < len(data_list):\n # Output current item and increment index\n try:\n current_item = data_list[current_index]\n except IndexError:\n current_item = Data(text=\"\")\n self.aggregated_output()\n self.update_ctx({f\"{self._id}_index\": current_index + 1})\n return current_item\n\n def done_output(self) -> Data:\n \"\"\"Trigger the done output when iteration is complete.\"\"\"\n self.initialize_data()\n\n if self.evaluate_stop_loop():\n self.stop(\"item\")\n self.start(\"done\")\n\n return self.ctx.get(f\"{self._id}_aggregated\", [])\n self.stop(\"done\")\n return Data(text=\"\")\n\n def loop_variables(self):\n \"\"\"Retrieve loop variables from context.\"\"\"\n return (\n self.ctx.get(f\"{self._id}_data\", []),\n self.ctx.get(f\"{self._id}_index\", 0),\n )\n\n def aggregated_output(self) -> Data:\n \"\"\"Return the aggregated list once all items are processed.\"\"\"\n self.initialize_data()\n\n # Get data list and aggregated list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n aggregated = self.ctx.get(f\"{self._id}_aggregated\", [])\n\n # Check if loop input is provided and append to aggregated list\n if self.item is not None and not isinstance(self.item, str) and len(aggregated) <= len(data_list):\n aggregated.append(self.item)\n self.update_ctx({f\"{self._id}_aggregated\": aggregated})\n return aggregated\n" - }, - "data": { - "_input_type": "DataInput", - "advanced": false, - "display_name": "Data", - "dynamic": false, - "info": "The initial list of Data objects to iterate over.", - "input_types": [ - "Data" - ], - "list": false, - "list_add_label": "Add More", - "name": "data", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "other", - "value": "" - } - }, - "tool_mode": false - }, - "showNode": true, - "type": "LoopComponent" - }, - "dragging": false, - "id": "LoopComponent-0aBsp", - "measured": { - "height": 280, - "width": 320 - }, - "position": { - "x": 517.2087344858119, - "y": 55.497845490859035 - }, - "selected": false, - "type": "genericNode" - }, - { - "data": { - "id": "AnthropicModel-VobAp", + "id": "AnthropicModel-G2MkR", "node": { "base_classes": [ "LanguageModel", @@ -584,7 +443,7 @@ "dynamic": false, "info": "Your Anthropic API key.", "input_types": [], - "load_from_db": true, + "load_from_db": false, "name": "api_key", "password": true, "placeholder": "", @@ -593,7 +452,7 @@ "show": true, "title_case": false, "type": "str", - "value": "ANTHROPIC_API_KEY" + "value": "" }, "base_url": { "_input_type": "MessageTextInput", @@ -704,7 +563,7 @@ "tool_mode": false, "trace_as_metadata": true, "type": "str", - "value": "claude-3-5-sonnet-20241022" + "value": "claude-3-7-sonnet-latest" }, "prefill": { "_input_type": "MessageTextInput", @@ -825,9 +684,9 @@ "type": "AnthropicModel" }, "dragging": false, - "id": "AnthropicModel-VobAp", + "id": "AnthropicModel-G2MkR", "measured": { - "height": 670, + "height": 588, "width": 320 }, "position": { @@ -839,7 +698,7 @@ }, { "data": { - "id": "MessagetoData-8vLbf", + "id": "MessagetoData-jYebP", "node": { "base_classes": [ "Data" @@ -930,7 +789,7 @@ "type": "MessagetoData" }, "dragging": false, - "id": "MessagetoData-8vLbf", + "id": "MessagetoData-jYebP", "measured": { "height": 230, "width": 320 @@ -944,172 +803,7 @@ }, { "data": { - "id": "ParseData-J2Axc", - "node": { - "base_classes": [ - "Data", - "Message" - ], - "beta": false, - "category": "processing", - "conditional_paths": [], - "custom_fields": {}, - "description": "Convert Data objects into Messages using any {field_name} from input data.", - "display_name": "Data to Message", - "documentation": "", - "edited": false, - "field_order": [ - "data", - "template", - "sep" - ], - "frozen": false, - "icon": "message-square", - "key": "ParseData", - "legacy": true, - "lf_version": "1.1.5", - "metadata": { - "legacy_name": "Parse Data" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Message", - "method": "parse_data", - "name": "text", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Data List", - "method": "parse_data_as_list", - "name": "data_list", - "selected": "Data", - "tool_mode": true, - "types": [ - "Data" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "score": 0.008664293911514902, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from langflow.custom import Component\nfrom langflow.helpers.data import data_to_text, data_to_text_list\nfrom langflow.io import DataInput, MultilineInput, Output, StrInput\nfrom langflow.schema import Data\nfrom langflow.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n legacy = True\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n" - }, - "data": { - "_input_type": "DataInput", - "advanced": false, - "display_name": "Data", - "dynamic": false, - "info": "The data to convert to text.", - "input_types": [ - "Data" - ], - "list": true, - "list_add_label": "Add More", - "name": "data", - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "other", - "value": "" - }, - "sep": { - "_input_type": "StrInput", - "advanced": true, - "display_name": "Separator", - "dynamic": false, - "info": "", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "sep", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "\n" - }, - "template": { - "_input_type": "MultilineInput", - "advanced": false, - "display_name": "Template", - "dynamic": false, - "info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "template", - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "{text}" - } - }, - "tool_mode": false - }, - "showNode": true, - "type": "ParseData" - }, - "dragging": false, - "id": "ParseData-J2Axc", - "measured": { - "height": 342, - "width": 320 - }, - "position": { - "x": 982.6945056277784, - "y": 823.4439549777719 - }, - "selected": false, - "type": "genericNode" - }, - { - "data": { - "id": "ChatOutput-01dLy", + "id": "ChatOutput-oTh2r", "node": { "base_classes": [ "Message" @@ -1135,7 +829,7 @@ "frozen": false, "icon": "MessagesSquare", "legacy": false, - "lf_version": "1.1.5", + "lf_version": "1.4.2", "metadata": {}, "minimized": true, "output_types": [], @@ -1404,21 +1098,21 @@ "type": "ChatOutput" }, "dragging": false, - "id": "ChatOutput-01dLy", + "id": "ChatOutput-oTh2r", "measured": { "height": 66, "width": 192 }, "position": { - "x": 1498.3739454769902, - "y": 1061.1606281984073 + "x": 1123.1326758440127, + "y": 502.35977645059677 }, "selected": false, "type": "genericNode" }, { "data": { - "id": "ChatInput-HBlBP", + "id": "ChatInput-5o3G0", "node": { "base_classes": [ "Message" @@ -1444,7 +1138,7 @@ "frozen": false, "icon": "MessagesSquare", "legacy": false, - "lf_version": "1.1.5", + "lf_version": "1.4.2", "metadata": {}, "minimized": true, "output_types": [], @@ -1593,7 +1287,7 @@ "trace_as_input": true, "trace_as_metadata": true, "type": "str", - "value": "" + "value": "ai" }, "sender": { "_input_type": "DropdownInput", @@ -1708,17 +1402,17 @@ }, "tool_mode": false }, - "showNode": false, + "showNode": true, "type": "ChatInput" }, "dragging": false, - "id": "ChatInput-HBlBP", + "id": "ChatInput-5o3G0", "measured": { - "height": 66, - "width": 192 + "height": 230, + "width": 320 }, "position": { - "x": -235.49853728839307, + "x": -333.65585758816223, "y": 107.75353484470551 }, "selected": false, @@ -1726,7 +1420,7 @@ }, { "data": { - "id": "note-EXWDb", + "id": "note-Wcn5J", "node": { "description": "### 💡 Add your Anthropic API key here 👇", "display_name": "", @@ -1739,7 +1433,7 @@ }, "dragging": false, "height": 324, - "id": "note-EXWDb", + "id": "note-Wcn5J", "measured": { "height": 324, "width": 358 @@ -1755,9 +1449,9 @@ }, { "data": { - "id": "note-chYLV", + "id": "note-jY0b1", "node": { - "description": "# **Langflow Loop Component Template - ArXiv search result Translator** \nThis template translates research paper summaries on ArXiv into Portuguese and summarizes them. \n Using **Langflow’s looping mechanism**, the template iterates through multiple research papers, translates them with the **Anthropic** model component, and outputs an aggregated version of all translated papers. \n\n# # Quickstart \n 1. Add your Anthropic API key to the **Anthropic** component. \n2. In the **Playground**, enter a query related to a research topic (for example, “Quantum Computing Advancements”). \n\n The flow fetches a list of research papers from ArXiv matching the query. Each paper in the retrieved list is processed one-by-one using the Langflow **Loop component**. \n\n The abstract of each paper is translated into Portuguese by the **Anthropic** model component. \n\n Once all papers are translated, the system aggregates them into a **single structured output**.", + "description": "# **Langflow Loop Component Template - ArXiv search result Translator** \nThis template translates research paper summaries on ArXiv into Portuguese and summarizes them. \n Using **Langflow’s looping mechanism**, the template iterates through multiple research papers, translates them with the **Anthropic** model component, and outputs an aggregated version of all translated papers. \n\n## Quickstart \n 1. Add your Anthropic API key to the **Anthropic** component. \n2. In the **Playground**, enter a query related to a research topic (for example, “Quantum Computing Advancements”). \n\n The flow fetches a list of research papers from ArXiv matching the query. Each paper in the retrieved list is processed one-by-one using the Langflow **Loop component**. \n\n The abstract of each paper is translated into Portuguese by the **Anthropic** model component. \n\n Once all papers are translated, the system aggregates them into a **single structured output**.", "display_name": "", "documentation": "", "template": {} @@ -1766,7 +1460,7 @@ }, "dragging": false, "height": 647, - "id": "note-chYLV", + "id": "note-jY0b1", "measured": { "height": 647, "width": 577 @@ -1782,7 +1476,7 @@ }, { "data": { - "id": "parser-lOwhH", + "id": "ParserComponent-ZhlLQ", "node": { "base_classes": [ "Message" @@ -1803,8 +1497,9 @@ ], "frozen": false, "icon": "braces", - "key": "parser", + "key": "ParserComponent", "legacy": false, + "lf_version": "1.4.2", "metadata": {}, "minimized": false, "output_types": [], @@ -1824,7 +1519,7 @@ } ], "pinned": false, - "score": 2.220446049250313e-16, + "score": 0.001, "template": { "_type": "Component", "code": { @@ -1843,7 +1538,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom typing import Any\n\nfrom langflow.custom import Component\nfrom langflow.io import (\n BoolInput,\n HandleInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TabInput,\n)\nfrom langflow.schema import Data, DataFrame\nfrom langflow.schema.message import Message\n\n\nclass ParserComponent(Component):\n name = \"parser\"\n display_name = \"Parser\"\n description = (\n \"Format a DataFrame or Data object into text using a template. \"\n \"Enable 'Stringify' to convert input into a readable string instead.\"\n )\n icon = \"braces\"\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n formatted_text = self.pattern.format(**data.data)\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def _safe_convert(self, data: Any) -> str:\n \"\"\"Safely convert input data to string.\"\"\"\n try:\n if isinstance(data, str):\n return data\n if isinstance(data, Message):\n return data.get_text()\n if isinstance(data, Data):\n return json.dumps(data.data)\n if isinstance(data, DataFrame):\n if hasattr(self, \"clean_data\") and self.clean_data:\n # Remove empty rows\n data = data.dropna(how=\"all\")\n # Remove empty lines in each cell\n data = data.replace(r\"^\\s*$\", \"\", regex=True)\n # Replace multiple newlines with a single newline\n data = data.replace(r\"\\n+\", \"\\n\", regex=True)\n return data.to_markdown(index=False)\n return str(data)\n except (ValueError, TypeError, AttributeError) as e:\n msg = f\"Error converting data: {e!s}\"\n raise ValueError(msg) from e\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([self._safe_convert(item) for item in self.input_data])\n else:\n result = self._safe_convert(self.input_data)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "import json\nfrom typing import Any\n\nfrom langflow.custom import Component\nfrom langflow.io import (\n BoolInput,\n HandleInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TabInput,\n)\nfrom langflow.schema import Data, DataFrame\nfrom langflow.schema.message import Message\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = (\n \"Format a DataFrame or Data object into text using a template. \"\n \"Enable 'Stringify' to convert input into a readable string instead.\"\n )\n icon = \"braces\"\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n formatted_text = self.pattern.format(**data.data)\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def _safe_convert(self, data: Any) -> str:\n \"\"\"Safely convert input data to string.\"\"\"\n try:\n if isinstance(data, str):\n return data\n if isinstance(data, Message):\n return data.get_text()\n if isinstance(data, Data):\n return json.dumps(data.data)\n if isinstance(data, DataFrame):\n if hasattr(self, \"clean_data\") and self.clean_data:\n # Remove empty rows\n data = data.dropna(how=\"all\")\n # Remove empty lines in each cell\n data = data.replace(r\"^\\s*$\", \"\", regex=True)\n # Replace multiple newlines with a single newline\n data = data.replace(r\"\\n+\", \"\\n\", regex=True)\n return data.to_markdown(index=False)\n return str(data)\n except (ValueError, TypeError, AttributeError) as e:\n msg = f\"Error converting data: {e!s}\"\n raise ValueError(msg) from e\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([self._safe_convert(item) for item in self.input_data])\n else:\n result = self._safe_convert(self.input_data)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", @@ -1910,7 +1605,7 @@ "trace_as_input": true, "trace_as_metadata": true, "type": "str", - "value": "Text: {text}" + "value": "Text: {dt}" }, "sep": { "_input_type": "MessageTextInput", @@ -1939,33 +1634,150 @@ "tool_mode": false }, "showNode": true, - "type": "parser" + "type": "ParserComponent" }, "dragging": false, - "id": "parser-lOwhH", + "id": "ParserComponent-ZhlLQ", "measured": { "height": 312, "width": 320 }, "position": { - "x": 961.4680671279477, - "y": -109.99346873464856 + "x": 971.3987248215344, + "y": -186.6658506576822 }, - "selected": true, + "selected": false, + "type": "genericNode" + }, + { + "data": { + "id": "LoopComponent-9BK8B", + "node": { + "base_classes": [ + "Data", + "DataFrame" + ], + "beta": false, + "category": "logic", + "conditional_paths": [], + "custom_fields": {}, + "description": "Iterates over a list of Data objects, outputting one item at a time and aggregating results from loop inputs.", + "display_name": "Loop", + "documentation": "", + "edited": false, + "field_order": [ + "data" + ], + "frozen": false, + "icon": "infinity", + "key": "LoopComponent", + "legacy": false, + "lf_version": "1.4.2", + "metadata": {}, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": true, + "cache": true, + "display_name": "Item", + "method": "item_output", + "name": "item", + "selected": "Data", + "tool_mode": true, + "types": [ + "Data" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Done", + "method": "done_output", + "name": "done", + "selected": "DataFrame", + "tool_mode": true, + "types": [ + "DataFrame" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "score": 2.220446049250313e-16, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from langflow.custom import Component\nfrom langflow.io import HandleInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates over a list of Data objects, outputting one item at a time and aggregating results from loop inputs.\"\n )\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Data or DataFrame\",\n info=\"The initial list of Data objects or DataFrame to iterate over.\",\n input_types=[\"Data\", \"DataFrame\"],\n ),\n ]\n\n outputs = [\n Output(display_name=\"Item\", name=\"item\", method=\"item_output\", allows_loop=True),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\"),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n if isinstance(data, DataFrame):\n return data.to_data_list()\n if isinstance(data, Data):\n return [data]\n if isinstance(data, list) and all(isinstance(item, Data) for item in data):\n return data\n msg = \"The 'data' input must be a DataFrame, a list of Data objects, or a single Data object.\"\n raise TypeError(msg)\n\n def evaluate_stop_loop(self) -> bool:\n \"\"\"Evaluate whether to stop item or done output.\"\"\"\n current_index = self.ctx.get(f\"{self._id}_index\", 0)\n data_length = len(self.ctx.get(f\"{self._id}_data\", []))\n return current_index > data_length\n\n def item_output(self) -> Data:\n \"\"\"Output the next item in the list or stop if done.\"\"\"\n self.initialize_data()\n current_item = Data(text=\"\")\n\n if self.evaluate_stop_loop():\n self.stop(\"item\")\n return Data(text=\"\")\n\n # Get data list and current index\n data_list, current_index = self.loop_variables()\n if current_index < len(data_list):\n # Output current item and increment index\n try:\n current_item = data_list[current_index]\n except IndexError:\n current_item = Data(text=\"\")\n self.aggregated_output()\n self.update_ctx({f\"{self._id}_index\": current_index + 1})\n return current_item\n\n def done_output(self) -> DataFrame:\n \"\"\"Trigger the done output when iteration is complete.\"\"\"\n self.initialize_data()\n\n if self.evaluate_stop_loop():\n self.stop(\"item\")\n self.start(\"done\")\n\n aggregated = self.ctx.get(f\"{self._id}_aggregated\", [])\n\n return DataFrame(aggregated)\n self.stop(\"done\")\n return DataFrame([])\n\n def loop_variables(self):\n \"\"\"Retrieve loop variables from context.\"\"\"\n return (\n self.ctx.get(f\"{self._id}_data\", []),\n self.ctx.get(f\"{self._id}_index\", 0),\n )\n\n def aggregated_output(self) -> list[Data]:\n \"\"\"Return the aggregated list once all items are processed.\"\"\"\n self.initialize_data()\n\n # Get data list and aggregated list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n aggregated = self.ctx.get(f\"{self._id}_aggregated\", [])\n loop_input = self.item\n if loop_input is not None and not isinstance(loop_input, str) and len(aggregated) <= len(data_list):\n aggregated.append(loop_input)\n self.update_ctx({f\"{self._id}_aggregated\": aggregated})\n return aggregated\n" + }, + "data": { + "_input_type": "HandleInput", + "advanced": false, + "display_name": "Data or DataFrame", + "dynamic": false, + "info": "The initial list of Data objects or DataFrame to iterate over.", + "input_types": [ + "Data", + "DataFrame" + ], + "list": false, + "list_add_label": "Add More", + "name": "data", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "other", + "value": "" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "LoopComponent" + }, + "dragging": false, + "id": "LoopComponent-9BK8B", + "measured": { + "height": 280, + "width": 320 + }, + "position": { + "x": 541.1188345961908, + "y": 181.38181401206583 + }, + "selected": false, "type": "genericNode" } ], "viewport": { - "x": 235.58653718700498, - "y": 250.7016431547001, - "zoom": 0.4882257444235303 + "x": -116.69751133397563, + "y": 176.89760533769663, + "zoom": 0.5570453295917549 } }, "description": "This template iterates over search results using LoopComponent and translates each result into Portuguese automatically. 🚀", "endpoint_name": null, - "id": "16326186-363a-46b4-aba2-08f76e31e38c", + "id": "105a54e1-198e-468e-8bb2-3a037f4d595a", "is_component": false, - "last_tested_version": "1.2.0", + "last_tested_version": "1.4.2", "name": "Research Translation Loop", "tags": [ "chatbots", diff --git a/src/backend/tests/unit/components/logic/test_loop.py b/src/backend/tests/unit/components/logic/test_loop.py index e3373dbab..c5925ebe8 100644 --- a/src/backend/tests/unit/components/logic/test_loop.py +++ b/src/backend/tests/unit/components/logic/test_loop.py @@ -3,7 +3,7 @@ from uuid import UUID import orjson import pytest from httpx import AsyncClient -from langflow.components.logic.loop import LoopComponent +from langflow.components.logic import LoopComponent from langflow.memory import aget_messages from langflow.schema.data import Data from langflow.services.database.models.flow import FlowCreate @@ -37,9 +37,9 @@ class TestLoopComponentWithAPI(ComponentTestBaseWithClient): "loop_input": [Data(text=TEXT)], } - def test_latest_version(self, default_kwargs) -> None: + def test_latest_version(self, component_class, default_kwargs) -> None: """Test that the component works with the latest version.""" - result = LoopComponent(**default_kwargs) + result = component_class(**default_kwargs) assert result is not None, "Component returned None for the latest version." async def _create_flow(self, client, json_loop_test, logged_in_headers): diff --git a/src/frontend/tests/extended/features/loop-component.spec.ts b/src/frontend/tests/extended/features/loop-component.spec.ts index 9f2c99e1f..2f2222be5 100644 --- a/src/frontend/tests/extended/features/loop-component.spec.ts +++ b/src/frontend/tests/extended/features/loop-component.spec.ts @@ -43,53 +43,50 @@ test( await page .getByTestId("logicLoop") + .first() .dragTo(page.locator('//*[@id="react-flow-id"]'), { targetPosition: { x: 280, y: 100 }, }); // Add Update Data component await page.getByTestId("sidebar-search-input").click(); - await page.getByTestId("sidebar-search-input").fill("update data"); - await page.waitForSelector('[data-testid="processingUpdate Data"]', { + await page.getByTestId("sidebar-search-input").fill("data operations"); + await page.waitForSelector('[data-testid="processingData Operations"]', { timeout: 1000, }); await page - .getByTestId("processingUpdate Data") + .getByTestId("processingData Operations") .dragTo(page.locator('//*[@id="react-flow-id"]'), { targetPosition: { x: 500, y: 100 }, }); // Add Parse Data component await page.getByTestId("sidebar-search-input").click(); - await page.getByTestId("sidebar-search-input").fill("data to message"); - await page.waitForSelector('[data-testid="processingData to Message"]', { + await page.getByTestId("sidebar-search-input").fill("Parser"); + await page.waitForSelector('[data-testid="processingParser"]', { timeout: 1000, }); await page - .getByTestId("processingData to Message") + .getByTestId("processingParser") .dragTo(page.locator('//*[@id="react-flow-id"]'), { targetPosition: { x: 720, y: 100 }, }); //This one is for testing the wrong loop message + + await page.getByTestId("sidebar-search-input").fill("File"); + await page.waitForSelector('[data-testid="dataFile"]', { + timeout: 1000, + }); + await page - .getByTestId("processingData to Message") + .getByTestId("dataFile") .dragTo(page.locator('//*[@id="react-flow-id"]'), { targetPosition: { x: 720, y: 400 }, }); - await page - .getByTestId("handle-parsedata-shownode-data list-right") - .nth(1) - .click(); - - const loopItemInput = await page - .getByTestId("handle-loopcomponent-shownode-item-left") - .first() - .click(); - // Add Chat Output component await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").fill("chat output"); @@ -114,7 +111,7 @@ test( .first() .click(); await page - .getByTestId("handle-updatedata-shownode-data-left") + .getByTestId("handle-dataoperations-shownode-data-left") .first() .click(); @@ -124,7 +121,7 @@ test( .first() .click(); await page - .getByTestId("handle-loopcomponent-shownode-data-left") + .getByTestId("handle-loopcomponent-shownode-data or dataframe-left") .first() .click(); @@ -134,13 +131,13 @@ test( .first() .click(); await page - .getByTestId("handle-parsedata-shownode-data-left") + .getByTestId("handle-parsercomponent-shownode-data or dataframe-left") .first() .click(); // Parse Data -> Chat Output await page - .getByTestId("handle-parsedata-shownode-message-right") + .getByTestId("handle-parsercomponent-shownode-parsed text-right") .first() .click(); @@ -149,7 +146,14 @@ test( .first() .click(); - await zoomOut(page, 4); + //Loop to File + + await page + .getByTestId("handle-loopcomponent-shownode-item-left") + .first() + .click(); + await page.getByTestId("handle-file-shownode-data-right").first().click(); + await zoomOut(page, 3); await page.getByTestId("div-generic-node").nth(5).click(); @@ -180,14 +184,16 @@ test( .fill("https://en.wikipedia.org/wiki/Human_intelligence"); await page.getByTestId("div-generic-node").nth(2).click(); - await page.getByTestId("int_int_number_of_fields").fill("1"); - await page.getByTestId("div-generic-node").nth(2).click(); + + await page.getByTestId("button_open_list_selection").click(); + + await page.getByTestId("list_item_append_or_update").click(); await page.getByTestId("keypair0").fill("text"); await page.getByTestId("keypair100").fill("modified_value"); // Build and run, expect the wrong loop message - await page.getByTestId("button_run_chat output").click(); + await page.getByTestId("button_run_file").click(); await page.waitForSelector("text=The flow has an incomplete loop.", { timeout: 30000, }); @@ -197,7 +203,7 @@ test( // Delete the second parse data used to test - await page.getByTestId("div-generic-node").nth(4).click(); + await page.getByTestId("title-File").last().click(); await page.getByTestId("more-options-modal").click(); @@ -206,7 +212,7 @@ test( // Update Data -> Loop Item (left side) await page - .getByTestId("handle-updatedata-shownode-data-right") + .getByTestId("handle-dataoperations-shownode-data-right") .first() .click(); await page