fix: loop variable not accessible error (#7501)

* 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

* fix: update test URL in loop-component.spec.ts to reflect correct reference

Changed the URL in the test case from "Artificial_intelligence" to "Human_intelligence" to ensure accurate testing of the loop component functionality.

* update FE tests

* [autofix.ci] apply automated fixes

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Edwin Jose 2025-04-10 16:13:33 -04:00 committed by GitHub
commit e135b7f341
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1537 additions and 1396 deletions

View file

@ -105,7 +105,7 @@ class LoopComponent(Component):
aggregated = self.ctx.get(f"{self._id}_aggregated", [])
# Check if loop input is provided and append to aggregated list
if self.data is not None and not isinstance(self.data, str) and len(aggregated) <= len(data_list):
aggregated.append(self.data)
if self.item is not None and not isinstance(self.item, str) and len(aggregated) <= len(data_list):
aggregated.append(self.item)
self.update_ctx({f"{self._id}_aggregated": aggregated})
return aggregated

View file

@ -720,6 +720,7 @@ class Component(CustomComponent):
def __getattr__(self, name: str) -> Any:
if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]:
# It is a dict of attributes that are not inputs or outputs all the raw data it should have the loop input.
return self.__dict__["_attributes"][name]
if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]:
return self.__dict__["_inputs"][name].value

View file

@ -307,6 +307,8 @@ class Vertex:
else:
params[param_key] = self.graph.get_vertex(edge.source_id)
elif param_key in self.output_names:
# if the loop is run the param_key item will be set over here
# validate the edge
params[param_key] = self.graph.get_vertex(edge.source_id)
return params

View file

@ -70,6 +70,11 @@ class ParameterHandler:
params[param_key].append(self.vertex.graph.get_vertex(edge.source_id))
else:
params[param_key] = self.process_non_list_edge_param(field, edge)
elif param_key in self.vertex.output_names:
# If the param_key is in the output_names, it means that the loop is run
# if the loop is run the param_key item will be set over here
# validate the edge
params[param_key] = self.vertex.graph.get_vertex(edge.source_id)
return params
def process_non_list_edge_param(self, field: dict, edge: CycleEdge) -> Any:

View file

@ -462,7 +462,7 @@
"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.data is not None and not isinstance(self.data, str) and len(aggregated) <= len(data_list):\n aggregated.append(self.data)\n self.update_ctx({f\"{self._id}_aggregated\": aggregated})\n return aggregated\n"
"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",
@ -595,7 +595,7 @@
"show": true,
"title_case": false,
"type": "str",
"value": "OPENAI_API_KEY"
"value": "ANTHROPIC_API_KEY"
},
"base_url": {
"_input_type": "MessageTextInput",

File diff suppressed because one or more lines are too long

View file

@ -75,7 +75,6 @@ test(
await expect(page.getByTestId("inputsChat Input")).toBeVisible();
await expect(page.getByTestId("outputsChat Output")).toBeVisible();
await expect(page.getByTestId("promptsPrompt")).toBeVisible();
await expect(page.getByTestId("modelsAmazon Bedrock")).toBeVisible();
await expect(page.getByTestId("helpersMessage History")).toBeVisible();
await expect(page.getByTestId("langchain_utilitiesCSVAgent")).toBeVisible();
await expect(
@ -101,7 +100,6 @@ test(
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
await expect(page.getByTestId("outputsChat Output")).not.toBeVisible();
await expect(page.getByTestId("promptsPrompt")).not.toBeVisible();
await expect(page.getByTestId("modelsAmazon Bedrock")).not.toBeVisible();
await expect(page.getByTestId("helpersMessage History")).not.toBeVisible();
await expect(
page.getByTestId("agentsTool Calling Agent"),

View file

@ -70,7 +70,6 @@ test(
const elementTestIds = [
"outputsChat Output",
"dataAPI Request",
"modelsAmazon Bedrock",
"vectorstoresAstra DB",
"embeddingsAmazon Bedrock Embeddings",
"langchain_utilitiesTool Calling Agent",
@ -97,7 +96,6 @@ test(
const visibleModelSpecsTestIds = [
"modelsAIML",
"modelsAmazon Bedrock",
"modelsAnthropic",
"modelsAzure OpenAI",
"modelsCohere",

View file

@ -177,7 +177,7 @@ test(
.fill("https://en.wikipedia.org/wiki/Artificial_intelligence");
await page
.getByTestId("inputlist_str_urls_1")
.fill("https://en.wikipedia.org/wiki/Artificial_intelligence");
.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");