feat: Loop uplift dataframe input and output (#8177)
* 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 <cristhian.lousa@gmail.com> Co-authored-by: Rodrigo <rodrigosilvanader@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com> Co-authored-by: Mike Fortman <michael.fortman@datastax.com>
This commit is contained in:
parent
ff37170693
commit
70b8f29099
4 changed files with 339 additions and 516 deletions
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue