feat: loop components handle ui and logic (#5744)
* Added backend to allow loop on output * Added custom edge for looping components * Added allows_loop to output type * Added output_types to target handle if its a loop * Fixed valid_connection to allow loops * Added the loop handle to the outputs * Added infinity icon * Fixed clean edges to not delete loop edge * Implement loop checking before build. * Implemented looping indicator * Fixed belzier path * [autofix.ci] apply automated fixes * 🔧 (reactflowUtils.ts): refactor cleanEdges and detectBrokenEdges functions to improve code readability and maintainability by extracting repeated logic into variables and reducing code duplication. * [autofix.ci] apply automated fixes * Add from_loop_target_handle method to TargetHandle class and update type field * Enhance Edge class to handle loop target handles and validate loop edges * Add output_names attribute and get_value_from_output_names method to Vertex class * Add overlap check for input and output names in Component class * Fix default value assignment in ComponentVertex to handle output names correctly * Clarify error message for missing attributes in Component class * Added backend to allow loop on output * Added custom edge for looping components * Added allows_loop to output type * Added output_types to target handle if its a loop * Fixed valid_connection to allow loops * Added the loop handle to the outputs * Added infinity icon * Fixed clean edges to not delete loop edge * Implement loop checking before build. * Implemented looping indicator * Fixed belzier path * [autofix.ci] apply automated fixes * 🔧 (reactflowUtils.ts): refactor cleanEdges and detectBrokenEdges functions to improve code readability and maintainability by extracting repeated logic into variables and reducing code duplication. * [autofix.ci] apply automated fixes * Add from_loop_target_handle method to TargetHandle class and update type field * Enhance Edge class to handle loop target handles and validate loop edges * Add output_names attribute and get_value_from_output_names method to Vertex class * Add overlap check for input and output names in Component class * Fix default value assignment in ComponentVertex to handle output names correctly * Clarify error message for missing attributes in Component class * feat: add loop component 🎁🎄 (#5429) * add loop component 🎁🎄 * [autofix.ci] apply automated fixes * fix: add loop component to init * [autofix.ci] apply automated fixes * refactor(loop): rename loop input variable and improve code quality - Renamed 'loop' input to 'loop_input' for clarity. - Simplified logic for checking loop input and aggregating results. - Enhanced type hints for better code readability and maintainability. * refactor(loop): add type hint to initialize_data method for improved clarity * fix: mypy error incompatible return value type * feat: adds test cases for loop component compatibility with the APIs, Loop component updates to support API (#5615) * add loop component 🎁🎄 * [autofix.ci] apply automated fixes * fix: add loop component to init * [autofix.ci] apply automated fixes * refactor(loop): rename loop input variable and improve code quality - Renamed 'loop' input to 'loop_input' for clarity. - Simplified logic for checking loop input and aggregating results. - Enhanced type hints for better code readability and maintainability. * refactor(loop): add type hint to initialize_data method for improved clarity * adding test * test cases added * Update test_loop.py * adding test * test cases added * Update test_loop.py * update with the new test case method! * Update test_loop.py * tests updates * Update loop.py * update fix * issues loop issues * reverting debug mode params * solves lint errors and fix the tests * fix: mypy error incompatible return value type * [autofix.ci] apply automated fixes --------- Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com> * feat: improve model input fields for Cohere component (#5712) feat: improve model input fields for cohere component 1. Make api_key field required 2. Convert temperature to SliderInput with range 0-2 3. Add info description to temperature slider * refactor: improve naming consistency in DataCombiner component (#5471) * refactor: improve naming consistency in DataCombiner component - Rename MergeOperation to DataOperation - Rename component to DataCombinerComponent - Convert operation enum values to uppercase - Update method names for consistency * [autofix.ci] apply automated fixes * fix: resolved linting errors in __init__.py * [autofix.ci] apply automated fixes * Changed operation names to capitalize only first letter * refactor: rename DataCombinerComponent to MergeDataComponent for better clarity and backwards compatibility * [autofix.ci] apply automated fixes * fix: Translate Portuguese text to English in merge_data.py * feat: add required to data_inputs in MergeDataComponent --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> * refactor: Refactor Wikipedia API component (#5432) * refactor(wikipedia): Refactor Wikipedia API component * test: add unit tests for WikipediaAPIComponent * [autofix.ci] apply automated fixes * refactor: improve WikipediaAPIComponent tests and fix lint issues * [autofix.ci] apply automated fixes * fix: resolve lint issues in WikipediaAPIComponent tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> * fix: pass slider input values correctly, add test (#5735) * ✨ (base.py): Update field validation to include "slider" type in addition to "float" type for better parameter handling 📝 (constants.py): Add "slider" type to the list of DIRECT_TYPES for consistency and completeness * ✅ (test_inputs.py): add unit test for SliderInput class to ensure it initializes with correct value * 🐛 (base.py): fix comparison of field type with a list by changing it to a set to ensure correct condition evaluation * [autofix.ci] apply automated fixes * fix format * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat: make AWS credentials required in bedrock component (#5710) 1. Make aws_access_key_id field required 2. Make aws_secret_access_key field required * chore: update test durations (#5736) Co-authored-by: ogabrielluiz <24829397+ogabrielluiz@users.noreply.github.com> * feat: add truncation to ResultDataResponse (#5704) * chore: Update dependencies and improve platform markers in configuration files - Added 'hypothesis' version 6.123.17 to dev-dependencies in pyproject.toml. - Updated platform markers from 'sys_platform' to 'platform_system' for better compatibility in uv.lock, affecting multiple packages including 'jinxed', 'colorama', and 'appnope'. - Ensured consistency in platform checks across various dependencies to enhance cross-platform support. This update improves the project's dependency management and ensures better compatibility across different operating systems. * feat: Enhance ResultDataResponse serialization with truncation support - Introduced a new method `_serialize_and_truncate` to handle serialization and truncation of various data types, including strings, bytes, datetime, Decimal, UUID, and BaseModel instances. - Updated the `serialize_results` method to utilize the new truncation logic for both individual results and dictionary outputs. - Enhanced the `serialize_model` method to ensure all relevant fields are serialized and truncated according to the defined maximum text length. This update improves the handling of large data outputs, ensuring that responses remain concise and manageable. * fix: Reduce MAX_TEXT_LENGTH in constants.py from 99999 to 20000 This change lowers the maximum text length limit to improve data handling and ensure more manageable output sizes across the application. * test: Add comprehensive unit tests for ResultDataResponse and VertexBuildResponse - Introduced a new test suite in `test_api_schemas.py` to validate the serialization and truncation behavior of `ResultDataResponse` and `VertexBuildResponse`. - Implemented tests for handling long strings, special data types, nested structures, and combined fields, ensuring proper serialization and truncation. - Enhanced coverage for logging and output handling, verifying that all fields are correctly processed and truncated as per the defined maximum text length. - Utilized Hypothesis for property-based testing to ensure robustness and reliability of the serialization logic. This update significantly improves the test coverage for the API response schemas, ensuring better data handling and output management. * feat: Add function to validate models with tool calling function and related fixes in agent component (#5720) * Update nvidia.py * update agent experience with improving model selection update agent experience with improving model selection and making only the tool calling models available. * variable clean up * [autofix.ci] apply automated fixes * Update src/backend/base/langflow/base/models/model_input_constants.py Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> * Update src/backend/base/langflow/base/models/model_input_constants.py Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> * added default models * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * format errors solved * [autofix.ci] apply automated fixes * Update model.py --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> * feat: assistants agent improvements (#5581) * assistants agent improvements * remove alembic init file * vector store / file upload support * use sync file object (required by sdk) * steps * self.tools initialization * improvements for edwin * add name and switch to MultilineInput * ci fixes * refactor: enhance flow type safety and clean up unused code (#5669) * 📝 (use-save-flow.ts): add AllNodeType and EdgeType imports to improve type safety in useSaveFlow hook 📝 (index.tsx): remove unused setNoticeData function to clean up code and improve readability * refactor: Remove unused code in GeneralPage component * refactor: Remove unused code in cardComponent/index.tsx --------- Co-authored-by: anovazzi1 <otavio2204@gmail.com> * feat: Add `required=True` to essential inputs across Langflow components (#5739) * fix: add required validation to input fields Ensures mandatory fields are properly marked as required across components. * fix: add required validation to input fields Ensures mandatory fields are properly marked as required across components. * fix: add required validation to input fields field: model_name * fix: add required validation to input fields field: model and base_url * fix: add required validation to input fields input: mistral_api_key * fix: add required validation to input fields inputs: model, base_url, nvidia_api_key * fix: add required validation to input fields inputs: model, base_url * fix: add required validation to input fields input: openai_api_key * fix: add required validation to input fields inputs: message, embedding_model * fix: add required validation to input fields inputs: model_name, credentials * fix: add required validation to input fields inputs: aws_secret_access_key, aws_access_key_id * fix: add required validation to input fields inputs: input_text, match_text * fix: add required validation to input fields inputs: input_message * fix: add required validation to input fields inputs: input_value * fix: add required validation to input fields input: data_input * fix: add required validation to input fields inputs: input_value * fix: add required validation to input fields input: data_input * fix: add required validation to input fields input: data_input * fix: add required validation to input fields input: data_input * fix: add required validation to input fields input: data_input * fix: add required validation to input fields inputs: data_inputs, embeddings * fix: add required validation to input fields inputs: api_key, input_value * fix: add required validation to input fields inputs: password, username, openai_api_key, prompt * fix: add required validation to input fields inputs: api_key, transcription_result * fix: add required validation to input fields inputs: api_key, transcription_result, prompt * fix: add required validation to input fields input: prompt * fix: add required validation to input fields input: api_key * fix: add required validation to input fields inputs: api_key, transcript_id * fix: add required validation to input fields inputs: audio_file, api_key * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat: make YouTube Transcripts URL field required (#5686) feat: Enhance YouTube Transcripts component by adding required field validation to URL input This change ensures that users provide a video URL before using the YouTube Transcripts component, preventing potential runtime errors due to missing video source. * fix: Fix memory leak when creating components (#5733) Fix memory leak when creating components * test: Update API key requirements and test configurations for frontend tests (#5752) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Vinícios Batista da Silva <vinicios.batsi@gmail.com> Co-authored-by: Raphael Valdetaro <79842132+raphaelchristi@users.noreply.github.com> Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ogabrielluiz <24829397+ogabrielluiz@users.noreply.github.com> Co-authored-by: Sebastián Estévez <estevezsebastian@gmail.com> Co-authored-by: anovazzi1 <otavio2204@gmail.com> Co-authored-by: VICTOR CORREA GOMES <112295415+Vigtu@users.noreply.github.com> Co-authored-by: Christophe Bornet <cbornet@hotmail.com> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> * Updated loop.py component * [autofix.ci] apply automated fixes * update test file * fix: handle None values in input names and improve type hints * [autofix.ci] apply automated fixes * Added loop component test * Added comments * test: add 'allow_loop' field to Output dictionary in test_output_to_dict method * fix: correct key name in Output dictionary from 'allow_loop' to 'allows_loop' in test_output_to_dict method * Updated frontend loop test * Updated examples * 🐛 (generalBugs-shard-9.spec.ts): Fix incorrect test selector for chat memory output element 🐛 (loop-component.spec.ts): Fix incorrect test selector for chat output element 🐛 (generalBugs-shard-3.spec.ts): Fix incorrect test selector for open AI model output element * [autofix.ci] apply automated fixes * refactor: update return type in AgentComponent to use dotdict for build_config This change modifies the return statement in the AgentComponent class to utilize a dotdict for the build_config, enhancing the structure and accessibility of the returned configuration data. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Vinícios Batista da Silva <vinicios.batsi@gmail.com> Co-authored-by: Raphael Valdetaro <79842132+raphaelchristi@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ogabrielluiz <24829397+ogabrielluiz@users.noreply.github.com> Co-authored-by: Sebastián Estévez <estevezsebastian@gmail.com> Co-authored-by: anovazzi1 <otavio2204@gmail.com> Co-authored-by: VICTOR CORREA GOMES <112295415+Vigtu@users.noreply.github.com> Co-authored-by: Christophe Bornet <cbornet@hotmail.com>
This commit is contained in:
parent
86b83b0f64
commit
f08c18f54a
44 changed files with 2594 additions and 85 deletions
|
|
@ -264,4 +264,4 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
build_config = await update_component_build_config(
|
||||
component_class, build_config, field_value, "model_name"
|
||||
)
|
||||
return {k: v.to_dict() if hasattr(v, "to_dict") else v for k, v in build_config.items()}
|
||||
return dotdict({k: v.to_dict() if hasattr(v, "to_dict") else v for k, v in build_config.items()})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from .conditional_router import ConditionalRouterComponent
|
|||
from .data_conditional_router import DataConditionalRouterComponent
|
||||
from .flow_tool import FlowToolComponent
|
||||
from .listen import ListenComponent
|
||||
from .loop import LoopComponent
|
||||
from .notify import NotifyComponent
|
||||
from .pass_message import PassMessageComponent
|
||||
from .run_flow import RunFlowComponent
|
||||
|
|
@ -12,6 +13,7 @@ __all__ = [
|
|||
"DataConditionalRouterComponent",
|
||||
"FlowToolComponent",
|
||||
"ListenComponent",
|
||||
"LoopComponent",
|
||||
"NotifyComponent",
|
||||
"PassMessageComponent",
|
||||
"RunFlowComponent",
|
||||
|
|
|
|||
111
src/backend/base/langflow/components/logic/loop.py
Normal file
111
src/backend/base/langflow/components/logic/loop.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from langflow.custom import Component
|
||||
from langflow.io import DataInput, Output
|
||||
from langflow.schema import Data
|
||||
|
||||
|
||||
class LoopComponent(Component):
|
||||
display_name = "Loop"
|
||||
description = (
|
||||
"Iterates over a list of Data objects, outputting one item at a time and aggregating results from loop inputs."
|
||||
)
|
||||
icon = "infinity"
|
||||
|
||||
inputs = [
|
||||
DataInput(
|
||||
name="data",
|
||||
display_name="Data",
|
||||
info="The initial list of Data objects to iterate over.",
|
||||
),
|
||||
]
|
||||
|
||||
outputs = [
|
||||
Output(display_name="Item", name="item", method="item_output", allows_loop=True),
|
||||
Output(display_name="Done", name="done", method="done_output"),
|
||||
]
|
||||
|
||||
def initialize_data(self) -> None:
|
||||
"""Initialize the data list, context index, and aggregated list."""
|
||||
if self.ctx.get(f"{self._id}_initialized", False):
|
||||
return
|
||||
|
||||
# Ensure data is a list of Data objects
|
||||
data_list = self._validate_data(self.data)
|
||||
|
||||
# Store the initial data and context variables
|
||||
self.update_ctx(
|
||||
{
|
||||
f"{self._id}_data": data_list,
|
||||
f"{self._id}_index": 0,
|
||||
f"{self._id}_aggregated": [],
|
||||
f"{self._id}_initialized": True,
|
||||
}
|
||||
)
|
||||
|
||||
def _validate_data(self, data):
|
||||
"""Validate and return a list of Data objects."""
|
||||
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."
|
||||
raise TypeError(msg)
|
||||
|
||||
def evaluate_stop_loop(self) -> bool:
|
||||
"""Evaluate whether to stop item or done output."""
|
||||
current_index = self.ctx.get(f"{self._id}_index", 0)
|
||||
data_length = len(self.ctx.get(f"{self._id}_data", []))
|
||||
return current_index > data_length
|
||||
|
||||
def item_output(self) -> Data:
|
||||
"""Output the next item in the list or stop if done."""
|
||||
self.initialize_data()
|
||||
current_item = Data(text="")
|
||||
|
||||
if self.evaluate_stop_loop():
|
||||
self.stop("item")
|
||||
return Data(text="")
|
||||
|
||||
# Get data list and current index
|
||||
data_list, current_index = self.loop_variables()
|
||||
if current_index < len(data_list):
|
||||
# Output current item and increment index
|
||||
try:
|
||||
current_item = data_list[current_index]
|
||||
except IndexError:
|
||||
current_item = Data(text="")
|
||||
self.aggregated_output()
|
||||
self.update_ctx({f"{self._id}_index": current_index + 1})
|
||||
return current_item
|
||||
|
||||
def done_output(self) -> Data:
|
||||
"""Trigger the done output when iteration is complete."""
|
||||
self.initialize_data()
|
||||
|
||||
if self.evaluate_stop_loop():
|
||||
self.stop("item")
|
||||
self.start("done")
|
||||
|
||||
return self.ctx.get(f"{self._id}_aggregated", [])
|
||||
self.stop("done")
|
||||
return Data(text="")
|
||||
|
||||
def loop_variables(self):
|
||||
"""Retrieve loop variables from context."""
|
||||
return (
|
||||
self.ctx.get(f"{self._id}_data", []),
|
||||
self.ctx.get(f"{self._id}_index", 0),
|
||||
)
|
||||
|
||||
def aggregated_output(self) -> 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)
|
||||
self.update_ctx({f"{self._id}_aggregated": aggregated})
|
||||
return aggregated
|
||||
|
|
@ -97,6 +97,9 @@ class Component(CustomComponent):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
# Initialize instance-specific attributes first
|
||||
if overlap := self._there_is_overlap_in_inputs_and_outputs():
|
||||
msg = f"Inputs and outputs have overlapping names: {overlap}"
|
||||
raise ValueError(msg)
|
||||
self._output_logs: dict[str, list[Log]] = {}
|
||||
self._current_output: str = ""
|
||||
self._metadata: dict = {}
|
||||
|
|
@ -157,6 +160,19 @@ class Component(CustomComponent):
|
|||
self.set_class_code()
|
||||
self._set_output_required_inputs()
|
||||
|
||||
def _there_is_overlap_in_inputs_and_outputs(self) -> set[str]:
|
||||
"""Check the `.name` of inputs and outputs to see if there is overlap.
|
||||
|
||||
Returns:
|
||||
set[str]: Set of names that overlap between inputs and outputs.
|
||||
"""
|
||||
# Create sets of input and output names for O(1) lookup
|
||||
input_names = {input_.name for input_ in self.inputs if input_.name is not None}
|
||||
output_names = {output.name for output in self.outputs}
|
||||
|
||||
# Return the intersection of the sets
|
||||
return input_names & output_names
|
||||
|
||||
@property
|
||||
def ctx(self):
|
||||
if not hasattr(self, "graph") or self.graph is None:
|
||||
|
|
@ -676,7 +692,7 @@ class Component(CustomComponent):
|
|||
return PlaceholderGraph(
|
||||
flow_id=flow_id, user_id=str(user_id), session_id=session_id, context={}, flow_name=flow_name
|
||||
)
|
||||
msg = f"{name} not found in {self.__class__.__name__}"
|
||||
msg = f"Attribute {name} not found in {self.__class__.__name__}"
|
||||
raise AttributeError(msg)
|
||||
|
||||
def _set_input_value(self, name: str, value: Any) -> None:
|
||||
|
|
@ -808,6 +824,7 @@ class Component(CustomComponent):
|
|||
for key, input_obj in self._inputs.items():
|
||||
if key not in attributes and key not in self._attributes:
|
||||
attributes[key] = input_obj.value or None
|
||||
|
||||
self._attributes.update(attributes)
|
||||
|
||||
def _set_outputs(self, outputs: list[dict]) -> None:
|
||||
|
|
|
|||
|
|
@ -138,6 +138,21 @@ class CustomComponent(BaseComponent):
|
|||
msg = f"Error stopping {self.display_name}: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
def start(self, output_name: str | None = None) -> None:
|
||||
if not output_name and self._vertex and len(self._vertex.outputs) == 1:
|
||||
output_name = self._vertex.outputs[0]["name"]
|
||||
elif not output_name:
|
||||
msg = "You must specify an output name to call start"
|
||||
raise ValueError(msg)
|
||||
if not self._vertex:
|
||||
msg = "Vertex is not set"
|
||||
raise ValueError(msg)
|
||||
try:
|
||||
self.graph.mark_branch(vertex_id=self._vertex.id, output_name=output_name, state="ACTIVE")
|
||||
except Exception as e:
|
||||
msg = f"Error starting {self.display_name}: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
def append_state(self, name: str, value: Any) -> None:
|
||||
if not self._vertex:
|
||||
msg = "Vertex is not set"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ class Edge:
|
|||
self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
|
||||
if isinstance(self._target_handle, dict):
|
||||
try:
|
||||
self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
|
||||
if "name" in self._target_handle:
|
||||
self.target_handle: TargetHandle = TargetHandle.from_loop_target_handle(self._target_handle)
|
||||
else:
|
||||
self.target_handle = TargetHandle(**self._target_handle)
|
||||
except Exception as e:
|
||||
if "inputTypes" in self._target_handle and self._target_handle["inputTypes"] is None:
|
||||
# Check if self._target_handle['fieldName']
|
||||
|
|
@ -79,6 +82,17 @@ class Edge:
|
|||
def _validate_handles(self, source, target) -> None:
|
||||
if self.target_handle.input_types is None:
|
||||
self.valid_handles = self.target_handle.type in self.source_handle.output_types
|
||||
elif self.target_handle.type is None:
|
||||
# ! This is not a good solution
|
||||
# This is a loop edge
|
||||
# If the target_handle.type is None, it means it's a loop edge
|
||||
# and we should check if the source_handle.output_types is not empty
|
||||
# and if the target_handle.input_types is empty or if any of the source_handle.output_types
|
||||
# is in the target_handle.input_types
|
||||
self.valid_handles = bool(self.source_handle.output_types) and (
|
||||
not self.target_handle.input_types
|
||||
or any(output_type in self.target_handle.input_types for output_type in self.source_handle.output_types)
|
||||
)
|
||||
|
||||
elif self.source_handle.output_types is not None:
|
||||
self.valid_handles = (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,32 @@ from typing_extensions import TypedDict
|
|||
from langflow.helpers.base_model import BaseModel
|
||||
|
||||
|
||||
class SourceHandleDict(TypedDict, total=False):
|
||||
baseClasses: list[str]
|
||||
dataType: str
|
||||
id: str
|
||||
name: str | None
|
||||
output_types: list[str]
|
||||
|
||||
|
||||
class TargetHandleDict(TypedDict):
|
||||
fieldName: str
|
||||
id: str
|
||||
inputTypes: list[str] | None
|
||||
type: str
|
||||
|
||||
|
||||
class EdgeDataDetails(TypedDict):
|
||||
sourceHandle: SourceHandleDict
|
||||
targetHandle: TargetHandleDict
|
||||
|
||||
|
||||
class EdgeData(TypedDict, total=False):
|
||||
source: str
|
||||
target: str
|
||||
data: EdgeDataDetails
|
||||
|
||||
|
||||
class ResultPair(BaseModel):
|
||||
result: Any
|
||||
extra: Any
|
||||
|
|
@ -45,7 +71,22 @@ class TargetHandle(BaseModel):
|
|||
input_types: list[str] = Field(
|
||||
default_factory=list, alias="inputTypes", description="List of input types for the target handle."
|
||||
)
|
||||
type: str = Field(..., description="Type of the target handle.")
|
||||
type: str = Field(None, description="Type of the target handle.")
|
||||
|
||||
@classmethod
|
||||
def from_loop_target_handle(cls, target_handle: TargetHandleDict) -> "TargetHandle":
|
||||
# The target handle is a loop edge
|
||||
# The target handle is a dict with the following keys:
|
||||
# - name: str
|
||||
# - id: str
|
||||
# - inputTypes: list[str]
|
||||
# - type: str
|
||||
# It is built from an Output, which is why it has a different structure
|
||||
return cls(
|
||||
field_name=target_handle.get("name"),
|
||||
id=target_handle.get("id"),
|
||||
input_types=target_handle.get("output_types"),
|
||||
)
|
||||
|
||||
|
||||
class SourceHandle(BaseModel):
|
||||
|
|
@ -69,29 +110,3 @@ class SourceHandle(BaseModel):
|
|||
raise ValueError(msg)
|
||||
v = splits[1]
|
||||
return v
|
||||
|
||||
|
||||
class SourceHandleDict(TypedDict, total=False):
|
||||
baseClasses: list[str]
|
||||
dataType: str
|
||||
id: str
|
||||
name: str | None
|
||||
output_types: list[str]
|
||||
|
||||
|
||||
class TargetHandleDict(TypedDict):
|
||||
fieldName: str
|
||||
id: str
|
||||
inputTypes: list[str] | None
|
||||
type: str
|
||||
|
||||
|
||||
class EdgeDataDetails(TypedDict):
|
||||
sourceHandle: SourceHandleDict
|
||||
targetHandle: TargetHandleDict
|
||||
|
||||
|
||||
class EdgeData(TypedDict, total=False):
|
||||
source: str
|
||||
target: str
|
||||
data: EdgeDataDetails
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ class Vertex:
|
|||
self.build_times: list[float] = []
|
||||
self.state = VertexStates.ACTIVE
|
||||
self.log_transaction_tasks: set[asyncio.Task] = set()
|
||||
self.output_names: list[str] = [
|
||||
output["name"] for output in self.outputs if isinstance(output, dict) and "name" in output
|
||||
]
|
||||
|
||||
def set_input_value(self, name: str, value: Any) -> None:
|
||||
if self.custom_component is None:
|
||||
|
|
@ -262,20 +265,19 @@ class Vertex:
|
|||
self.base_type = base_type
|
||||
break
|
||||
|
||||
def get_value_from_output_names(self, key: str):
|
||||
if key in self.output_names:
|
||||
return self.graph.get_vertex(key)
|
||||
return None
|
||||
|
||||
def get_value_from_template_dict(self, key: str):
|
||||
template_dict = self.data.get("node", {}).get("template", {})
|
||||
|
||||
if key not in template_dict:
|
||||
msg = f"Key {key} not found in template dict"
|
||||
raise ValueError(msg)
|
||||
return template_dict.get(key, {}).get("value")
|
||||
|
||||
def get_task(self):
|
||||
# using the task_id, get the task from celery
|
||||
# and return it
|
||||
from celery.result import AsyncResult
|
||||
|
||||
return AsyncResult(self.task_id)
|
||||
|
||||
def _set_params_from_normal_edge(self, params: dict, edge: Edge, template_dict: dict):
|
||||
param_key = edge.target_param
|
||||
|
||||
|
|
@ -299,6 +301,8 @@ class Vertex:
|
|||
|
||||
else:
|
||||
params[param_key] = self.graph.get_vertex(edge.source_id)
|
||||
elif param_key in self.output_names:
|
||||
params[param_key] = self.graph.get_vertex(edge.source_id)
|
||||
return params
|
||||
|
||||
def build_params(self) -> None:
|
||||
|
|
|
|||
|
|
@ -97,11 +97,14 @@ class ComponentVertex(Vertex):
|
|||
"""
|
||||
flow_id = self.graph.flow_id
|
||||
if not self.built:
|
||||
default_value = UNDEFINED
|
||||
default_value: Any = UNDEFINED
|
||||
for edge in self.get_edge_with_target(requester.id):
|
||||
# We need to check if the edge is a normal edge
|
||||
if edge.is_cycle and edge.target_param:
|
||||
default_value = requester.get_value_from_template_dict(edge.target_param)
|
||||
if edge.target_param in requester.output_names:
|
||||
default_value = None
|
||||
else:
|
||||
default_value = requester.get_value_from_template_dict(edge.target_param)
|
||||
|
||||
if flow_id:
|
||||
self._log_transaction_async(source=self, target=requester, flow_id=str(flow_id), status="error")
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -349,6 +350,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -638,6 +640,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -909,6 +912,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -1041,6 +1045,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1053,6 +1058,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1353,6 +1359,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1365,6 +1372,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1654,6 +1662,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -1852,6 +1861,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1864,6 +1874,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -390,6 +391,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -591,6 +593,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -875,6 +878,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -887,6 +891,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "fetch_content",
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "fetch_content_text",
|
||||
|
|
@ -189,6 +191,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -312,6 +315,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "parse_data",
|
||||
|
|
@ -323,6 +327,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data List",
|
||||
"method": "parse_data_as_list",
|
||||
|
|
@ -462,6 +467,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -626,6 +632,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -731,6 +738,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -998,6 +1006,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1010,6 +1019,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -530,6 +531,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "retrieve_messages",
|
||||
|
|
@ -541,6 +543,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "retrieve_messages_as_text",
|
||||
|
|
@ -773,6 +776,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -1014,6 +1018,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -1348,6 +1353,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1360,6 +1366,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1643,6 +1650,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "fetch_content",
|
||||
|
|
@ -1654,6 +1662,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "fetch_content_text",
|
||||
|
|
@ -1665,6 +1674,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -1789,6 +1799,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "fetch_content",
|
||||
|
|
@ -1800,6 +1811,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "fetch_content_text",
|
||||
|
|
@ -1811,6 +1823,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -1941,6 +1954,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "fetch_content",
|
||||
|
|
@ -1952,6 +1966,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "fetch_content_text",
|
||||
|
|
@ -1963,6 +1978,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -449,6 +450,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -720,6 +722,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "parse_data",
|
||||
|
|
@ -731,6 +734,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data List",
|
||||
"method": "parse_data_as_list",
|
||||
|
|
@ -904,6 +908,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -916,6 +921,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1234,6 +1240,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "load_files",
|
||||
|
|
@ -1482,6 +1489,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -661,6 +662,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Embeddings",
|
||||
"method": "build_embeddings",
|
||||
|
|
@ -1163,6 +1165,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Search Results",
|
||||
"method": "search_documents",
|
||||
|
|
@ -1179,6 +1182,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -1677,6 +1681,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "parse_data",
|
||||
|
|
@ -1688,6 +1693,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data List",
|
||||
"method": "parse_data_as_list",
|
||||
|
|
@ -1831,6 +1837,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -2003,6 +2010,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -2015,6 +2023,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -2317,6 +2326,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -2587,6 +2597,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "fetch_content",
|
||||
|
|
@ -2598,6 +2609,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "fetch_content_text",
|
||||
|
|
@ -2609,6 +2621,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -2768,6 +2781,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Search Results",
|
||||
"method": "search_documents",
|
||||
|
|
@ -2784,6 +2798,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -3280,6 +3295,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "transform_data",
|
||||
|
|
@ -3464,6 +3480,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "transform_data",
|
||||
|
|
@ -3622,6 +3639,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Embeddings",
|
||||
"method": "build_embeddings",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -495,6 +496,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -1024,6 +1026,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "parse_data",
|
||||
|
|
@ -1035,6 +1038,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data List",
|
||||
"method": "parse_data_as_list",
|
||||
|
|
@ -1188,6 +1192,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1200,6 +1205,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1489,6 +1495,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -145,6 +145,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -442,6 +443,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -781,6 +783,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -793,6 +796,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1085,6 +1089,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "retrieve_messages",
|
||||
|
|
@ -1096,6 +1101,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "retrieve_messages_as_text",
|
||||
|
|
@ -1328,6 +1334,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -123,6 +123,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -413,6 +414,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -540,6 +542,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -822,6 +825,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -834,6 +838,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -286,6 +286,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -565,6 +566,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -678,6 +680,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -690,6 +693,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -985,6 +989,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -1252,6 +1257,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1350,6 +1356,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1448,6 +1455,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1546,6 +1554,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1644,6 +1653,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1786,6 +1796,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -573,6 +574,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "parse_data",
|
||||
|
|
@ -584,6 +586,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data List",
|
||||
"method": "parse_data_as_list",
|
||||
|
|
@ -729,6 +732,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Prompt Message",
|
||||
"method": "build_prompt",
|
||||
|
|
@ -895,6 +899,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Chunks",
|
||||
"method": "split_text",
|
||||
|
|
@ -906,6 +911,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -1129,6 +1135,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "text_response",
|
||||
|
|
@ -1141,6 +1148,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Language Model",
|
||||
"method": "build_model",
|
||||
|
|
@ -1436,6 +1444,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
|
|
@ -1723,6 +1732,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Embeddings",
|
||||
"method": "build_embeddings",
|
||||
|
|
@ -2248,6 +2258,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Embeddings",
|
||||
"method": "build_embeddings",
|
||||
|
|
@ -2724,6 +2735,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Data",
|
||||
"method": "load_files",
|
||||
|
|
@ -3058,6 +3070,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Search Results",
|
||||
"method": "search_documents",
|
||||
|
|
@ -3075,6 +3088,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
@ -3491,6 +3505,7 @@
|
|||
"output_types": [],
|
||||
"outputs": [
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "Search Results",
|
||||
"method": "search_documents",
|
||||
|
|
@ -3508,6 +3523,7 @@
|
|||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
"allows_loop": false,
|
||||
"cache": true,
|
||||
"display_name": "DataFrame",
|
||||
"method": "as_dataframe",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ from typing import ( # type: ignore[attr-defined]
|
|||
_UnionGenericAlias, # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from langflow.field_typing import Text
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
|
|
@ -189,6 +197,9 @@ class Output(BaseModel):
|
|||
required_inputs: list[str] | None = Field(default=None)
|
||||
"""List of required inputs for this output."""
|
||||
|
||||
allows_loop: bool = Field(default=False)
|
||||
"""Specifies if the output allows looping."""
|
||||
|
||||
def to_dict(self):
|
||||
return self.model_dump(by_alias=True, exclude_none=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ def pytest_configure(config):
|
|||
pytest.VECTOR_STORE_PATH = data_path / "Vector_store.json"
|
||||
pytest.SIMPLE_API_TEST = data_path / "SimpleAPITest.json"
|
||||
pytest.MEMORY_CHATBOT_NO_LLM = data_path / "MemoryChatbotNoLLM.json"
|
||||
pytest.LOOP_TEST = data_path / "LoopTest.json"
|
||||
pytest.CODE_WITH_SYNTAX_ERROR = """
|
||||
def get_text():
|
||||
retun "Hello World"
|
||||
|
|
@ -121,6 +122,7 @@ def get_text():
|
|||
pytest.TWO_OUTPUTS,
|
||||
pytest.VECTOR_STORE_PATH,
|
||||
pytest.MEMORY_CHATBOT_NO_LLM,
|
||||
pytest.LOOP_TEST,
|
||||
]:
|
||||
assert path.exists(), f"File {path} does not exist. Available files: {list(data_path.iterdir())}"
|
||||
|
||||
|
|
@ -324,6 +326,11 @@ def json_memory_chatbot_no_llm():
|
|||
return pytest.MEMORY_CHATBOT_NO_LLM.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_loop_test():
|
||||
return pytest.LOOP_TEST.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def deactivate_tracing(monkeypatch):
|
||||
monkeypatch.setenv("LANGFLOW_DEACTIVATE_TRACING", "true")
|
||||
|
|
|
|||
1604
src/backend/tests/data/LoopTest.json
Normal file
1604
src/backend/tests/data/LoopTest.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -88,12 +88,12 @@ async def test_update_component_model_name_options(client: AsyncClient, logged_i
|
|||
assert "template" in result
|
||||
assert "model_name" in result["template"]
|
||||
assert isinstance(result["template"]["model_name"]["options"], list)
|
||||
assert (
|
||||
len(result["template"]["model_name"]["options"]) > 0
|
||||
), f"Model names: {result['template']['model_name']['options']}"
|
||||
assert (
|
||||
current_model_names != result["template"]["model_name"]["options"]
|
||||
), f"Current model names: {current_model_names}, New model names: {result['template']['model_name']['options']}"
|
||||
assert len(result["template"]["model_name"]["options"]) > 0, (
|
||||
f"Model names: {result['template']['model_name']['options']}"
|
||||
)
|
||||
assert current_model_names != result["template"]["model_name"]["options"], (
|
||||
f"Current model names: {current_model_names}, New model names: {result['template']['model_name']['options']}"
|
||||
)
|
||||
# Now test with Custom provider
|
||||
template["agent_llm"]["value"] = "Custom"
|
||||
request.field_value = "Custom"
|
||||
|
|
|
|||
|
|
@ -78,9 +78,9 @@ class TestAgentComponent(ComponentTestBaseWithoutClient):
|
|||
assert all(provider in updated_config["agent_llm"]["options"] for provider in MODEL_PROVIDERS_DICT)
|
||||
assert "Anthropic" in updated_config["agent_llm"]["options"]
|
||||
assert updated_config["agent_llm"]["input_types"] == []
|
||||
assert any(
|
||||
"sonnet" in option.lower() for option in updated_config["model_name"]["options"]
|
||||
), f"Options: {updated_config['model_name']['options']}"
|
||||
assert any("sonnet" in option.lower() for option in updated_config["model_name"]["options"]), (
|
||||
f"Options: {updated_config['model_name']['options']}"
|
||||
)
|
||||
|
||||
# Test updating build config for Custom
|
||||
updated_config = await component.update_build_config(build_config, "Custom", "agent_llm")
|
||||
|
|
|
|||
0
src/backend/tests/unit/components/logic/__init__.py
Normal file
0
src/backend/tests/unit/components/logic/__init__.py
Normal file
91
src/backend/tests/unit/components/logic/test_loop.py
Normal file
91
src/backend/tests/unit/components/logic/test_loop.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from langflow.components.logic.loop import LoopComponent
|
||||
from langflow.memory import aget_messages
|
||||
from langflow.schema.data import Data
|
||||
from langflow.services.database.models.flow import FlowCreate
|
||||
from orjson import orjson
|
||||
|
||||
from tests.base import ComponentTestBaseWithClient
|
||||
|
||||
TEXT = (
|
||||
"lorem ipsum dolor sit amet lorem ipsum dolor sit amet lorem ipsum dolor sit amet. "
|
||||
"lorem ipsum dolor sit amet lorem ipsum dolor sit amet lorem ipsum dolor sit amet. "
|
||||
"lorem ipsum dolor sit amet lorem ipsum dolor sit amet lorem ipsum dolor sit amet."
|
||||
)
|
||||
|
||||
|
||||
class TestLoopComponentWithAPI(ComponentTestBaseWithClient):
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
"""Return the component class to test."""
|
||||
return LoopComponent
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
"""Return an empty list since this component doesn't have version-specific files."""
|
||||
return []
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
"""Return the default kwargs for the component."""
|
||||
return {
|
||||
"data": [[Data(text="Hello World")]],
|
||||
"loop_input": [Data(text=TEXT)],
|
||||
}
|
||||
|
||||
def test_latest_version(self, default_kwargs) -> None:
|
||||
"""Test that the component works with the latest version."""
|
||||
result = LoopComponent(**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):
|
||||
vector_store = orjson.loads(json_loop_test)
|
||||
data = vector_store["data"]
|
||||
vector_store = FlowCreate(name="Flow", description="description", data=data, endpoint_name="f")
|
||||
response = await client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
|
||||
response.raise_for_status()
|
||||
return response.json()["id"]
|
||||
|
||||
async def check_messages(self, flow_id):
|
||||
messages = await aget_messages(flow_id=UUID(flow_id), order="ASC")
|
||||
assert len(messages) == 2
|
||||
assert messages[0].session_id == flow_id
|
||||
assert messages[0].sender == "User"
|
||||
assert messages[0].sender_name == "User"
|
||||
assert messages[0].text != ""
|
||||
assert messages[1].session_id == flow_id
|
||||
assert messages[1].sender == "Machine"
|
||||
assert messages[1].sender_name == "AI"
|
||||
assert len(messages[1].text) > 0
|
||||
|
||||
async def test_build_flow_loop(self, client, json_loop_test, logged_in_headers):
|
||||
# TODO: Add a test for the loop where the loop component gets updated even the component in json
|
||||
flow_id = await self._create_flow(client, json_loop_test, logged_in_headers)
|
||||
|
||||
async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r:
|
||||
async for line in r.aiter_lines():
|
||||
# httpx split by \n, but ndjson sends two \n for each line
|
||||
if line:
|
||||
# Process the line if needed
|
||||
pass
|
||||
|
||||
await self.check_messages(flow_id)
|
||||
|
||||
async def test_run_flow_loop(self, client: AsyncClient, created_api_key, json_loop_test, logged_in_headers):
|
||||
flow_id = await self._create_flow(client, json_loop_test, logged_in_headers)
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
payload = {
|
||||
"input_value": TEXT,
|
||||
"input_type": "chat",
|
||||
"session_id": f"{flow_id}run",
|
||||
"output_type": "chat",
|
||||
"tweaks": {},
|
||||
}
|
||||
response = await client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers)
|
||||
data = response.json()
|
||||
assert "outputs" in data
|
||||
assert "session_id" in data
|
||||
assert len(data["outputs"][-1]["outputs"]) > 0
|
||||
|
|
@ -133,6 +133,7 @@ class TestOutput:
|
|||
def test_output_to_dict(self):
|
||||
output_obj = Output(name="test_output")
|
||||
assert output_obj.to_dict() == {
|
||||
"allows_loop": False,
|
||||
"types": [],
|
||||
"name": "test_output",
|
||||
"display_name": "test_output",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import useFlowStore from "@/stores/flowStore";
|
||||
import { scapeJSONParse } from "@/utils/reactflowUtils";
|
||||
import { BaseEdge, EdgeProps, getBezierPath, Position } from "@xyflow/react";
|
||||
|
||||
export function DefaultEdge({
|
||||
|
|
@ -17,10 +18,17 @@ export function DefaultEdge({
|
|||
const sourceNode = getNode(source);
|
||||
const targetNode = getNode(target);
|
||||
|
||||
const targetHandleObject = scapeJSONParse(targetHandleId!);
|
||||
|
||||
const sourceXNew =
|
||||
(sourceNode?.position.x ?? 0) + (sourceNode?.measured?.width ?? 0);
|
||||
const targetXNew = targetNode?.position.x ?? 0;
|
||||
|
||||
const distance = 200 + 0.1 * (Math.abs(sourceXNew - targetXNew) / 2);
|
||||
const distanceY = 200 + 0.3 * Math.abs(sourceY - targetY);
|
||||
|
||||
const edgePathLoop = `M ${sourceXNew} ${sourceY} C ${sourceXNew + distance} ${sourceY + distanceY}, ${targetXNew - distance} ${targetY + distanceY}, ${targetXNew} ${targetY}`;
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX: sourceXNew,
|
||||
sourceY,
|
||||
|
|
@ -30,5 +38,11 @@ export function DefaultEdge({
|
|||
targetY,
|
||||
});
|
||||
|
||||
return <BaseEdge path={edgePath} {...props} />;
|
||||
return (
|
||||
<BaseEdge
|
||||
path={targetHandleObject.output_types ? edgePathLoop : edgePath}
|
||||
strokeDasharray={targetHandleObject.output_types ? "5 5" : "0"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||
import { targetHandleType } from "@/types/flow";
|
||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { TextSearch } from "lucide-react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import ForwardedIconComponent, {
|
||||
default as IconComponent,
|
||||
|
|
@ -14,6 +15,7 @@ import { NodeOutputFieldComponentType } from "../../../../types/components";
|
|||
import {
|
||||
getGroupOutputNodeId,
|
||||
scapedJSONStringfy,
|
||||
scapeJSONParse,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import {
|
||||
cn,
|
||||
|
|
@ -205,6 +207,18 @@ function NodeOutputField({
|
|||
[edges, id],
|
||||
);
|
||||
|
||||
const looping = useMemo(() => {
|
||||
return edges.some((edge) => {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!,
|
||||
);
|
||||
return (
|
||||
targetHandleObject.output_types &&
|
||||
edge.sourceHandle === scapedJSONStringfy(id)
|
||||
);
|
||||
});
|
||||
}, [edges, id]);
|
||||
|
||||
const handleUpdateOutputHide = useCallback(
|
||||
(value?: boolean) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
|
|
@ -235,6 +249,41 @@ function NodeOutputField({
|
|||
}
|
||||
}, [disabledOutput, data.node?.outputs, handleUpdateOutputHide, index]);
|
||||
|
||||
const LoopHandle = useMemo(() => {
|
||||
if (data.node?.outputs![index].allows_loop) {
|
||||
return (
|
||||
<HandleRenderComponent
|
||||
left={true}
|
||||
nodes={nodes}
|
||||
tooltipTitle={tooltipTitle}
|
||||
id={id}
|
||||
title={title}
|
||||
edges={edges}
|
||||
nodeId={data.id}
|
||||
myData={myData}
|
||||
colors={colors}
|
||||
setFilterEdge={setFilterEdge}
|
||||
showNode={showNode}
|
||||
testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`}
|
||||
colorName={colorName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [
|
||||
nodes,
|
||||
tooltipTitle,
|
||||
id,
|
||||
title,
|
||||
edges,
|
||||
data.id,
|
||||
myData,
|
||||
colors,
|
||||
setFilterEdge,
|
||||
showNode,
|
||||
data?.type,
|
||||
colorName,
|
||||
]);
|
||||
|
||||
const Handle = useMemo(
|
||||
() => (
|
||||
<HandleRenderComponent
|
||||
|
|
@ -280,8 +329,14 @@ function NodeOutputField({
|
|||
isToolMode && "bg-primary",
|
||||
)}
|
||||
>
|
||||
{LoopHandle}
|
||||
<div className="flex w-full items-center justify-end truncate text-sm">
|
||||
<div className="flex flex-1">
|
||||
{data.node?.outputs![index].allows_loop && (
|
||||
<Badge variant="pinkStatic" size="xq" className="mr-2 px-1">
|
||||
<ForwardedIconComponent name="Infinity" className="h-4 w-4" />
|
||||
</Badge>
|
||||
)}
|
||||
<HideShowButton
|
||||
disabled={disabledOutput}
|
||||
onClick={() => handleUpdateOutputHide()}
|
||||
|
|
@ -324,7 +379,7 @@ function NodeOutputField({
|
|||
: "Please build the component first"
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex items-center gap-2">
|
||||
<OutputModal
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
nodeId={flowPoolId}
|
||||
|
|
@ -343,6 +398,11 @@ function NodeOutputField({
|
|||
id={data?.type}
|
||||
/>
|
||||
</OutputModal>
|
||||
{looping && (
|
||||
<Badge variant="pinkStatic" size="xq" className="px-1">
|
||||
Looping
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// ERROR
|
||||
export const MISSED_ERROR_ALERT = "Oops! Looks like you missed something";
|
||||
export const INCOMPLETE_LOOP_ERROR_ALERT =
|
||||
"The flow has an incomplete loop. Check your connections and try again.";
|
||||
export const INVALID_FILE_ALERT =
|
||||
"Please select a valid file. Only these file types are allowed:";
|
||||
export const CONSOLE_ERROR_MSG = "Error occurred while uploading file";
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
scapedJSONStringfy,
|
||||
unselectAllNodesEdges,
|
||||
updateGroupRecursion,
|
||||
validateEdge,
|
||||
validateNodes,
|
||||
} from "../utils/reactflowUtils";
|
||||
import { getInputsAndOutputs } from "../utils/storeUtils";
|
||||
|
|
@ -606,6 +607,25 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
const setSuccessData = useAlertStore.getState().setSuccessData;
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const setNoticeData = useAlertStore.getState().setNoticeData;
|
||||
|
||||
const edges = get().edges;
|
||||
let error = false;
|
||||
for (const edge of edges) {
|
||||
const errors = validateEdge(edge, get().nodes, edges);
|
||||
if (errors.length > 0) {
|
||||
error = true;
|
||||
setErrorData({
|
||||
title: MISSED_ERROR_ALERT,
|
||||
list: errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
get().setIsBuilding(false);
|
||||
get().setLockChat(false);
|
||||
throw new Error("Invalid components");
|
||||
}
|
||||
|
||||
function validateSubgraph(nodes: string[]) {
|
||||
const errorsObjs = validateNodes(
|
||||
get().nodes.filter((node) => nodes.includes(node.id)),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { Edge, Node, Viewport } from "@xyflow/react";
|
||||
import { ChatInputType, ChatOutputType } from "../chat";
|
||||
import { FlowType } from "../flow";
|
||||
//kind and class are just representative names to represent the actual structure of the object received by the API
|
||||
|
|
@ -103,6 +102,7 @@ export type OutputFieldType = {
|
|||
display_name: string;
|
||||
hidden?: boolean;
|
||||
proxy?: OutputFieldProxyType;
|
||||
allows_loop?: boolean;
|
||||
};
|
||||
export type errorsTypeAPI = {
|
||||
function: { errors: Array<string> };
|
||||
|
|
|
|||
|
|
@ -101,8 +101,10 @@ export type sourceHandleType = {
|
|||
//left side
|
||||
export type targetHandleType = {
|
||||
inputTypes?: string[];
|
||||
output_types?: string[];
|
||||
type: string;
|
||||
fieldName: string;
|
||||
name?: string;
|
||||
id: string;
|
||||
proxy?: { field: string; id: string };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import {
|
|||
getLeftHandleId,
|
||||
getRightHandleId,
|
||||
} from "@/CustomNodes/utils/get-handle-id";
|
||||
import { INCOMPLETE_LOOP_ERROR_ALERT } from "@/constants/alerts_constants";
|
||||
import {
|
||||
Connection,
|
||||
Edge,
|
||||
getOutgoers,
|
||||
Node,
|
||||
OnSelectionChangeParams,
|
||||
ReactFlowJsonObject,
|
||||
|
|
@ -18,8 +20,8 @@ import {
|
|||
IS_MAC,
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
OUTPUT_TYPES,
|
||||
SUCCESS_BUILD,
|
||||
specialCharsRegex,
|
||||
SUCCESS_BUILD,
|
||||
} from "../constants/constants";
|
||||
import { DESCRIPTIONS } from "../flow_constants";
|
||||
import {
|
||||
|
|
@ -68,14 +70,39 @@ export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) {
|
|||
if (targetHandle) {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle);
|
||||
const field = targetHandleObject.fieldName;
|
||||
const id: targetHandleType = {
|
||||
type: targetNode.data.node!.template[field]?.type,
|
||||
fieldName: field,
|
||||
id: targetNode.data.id,
|
||||
inputTypes: targetNode.data.node!.template[field]?.input_types,
|
||||
};
|
||||
if (targetNode.data.node!.template[field]?.proxy) {
|
||||
id.proxy = targetNode.data.node!.template[field]?.proxy;
|
||||
let id: targetHandleType | sourceHandleType;
|
||||
|
||||
const templateFieldType = targetNode.data.node!.template[field]?.type;
|
||||
const inputTypes = targetNode.data.node!.template[field]?.input_types;
|
||||
const hasProxy = targetNode.data.node!.template[field]?.proxy;
|
||||
|
||||
if (
|
||||
!field &&
|
||||
targetHandleObject.name &&
|
||||
targetNode.type === "genericNode"
|
||||
) {
|
||||
const dataType = targetNode.data.type;
|
||||
const outputTypes =
|
||||
targetNode.data.node!.outputs?.find(
|
||||
(output) => output.name === targetHandleObject.name,
|
||||
)?.types ?? [];
|
||||
|
||||
id = {
|
||||
dataType: dataType ?? "",
|
||||
name: targetHandleObject.name,
|
||||
id: targetNode.data.id,
|
||||
output_types: outputTypes,
|
||||
};
|
||||
} else {
|
||||
id = {
|
||||
type: templateFieldType,
|
||||
fieldName: field,
|
||||
id: targetNode.data.id,
|
||||
inputTypes: inputTypes,
|
||||
};
|
||||
if (hasProxy) {
|
||||
id.proxy = targetNode.data.node!.template[field]?.proxy;
|
||||
}
|
||||
}
|
||||
if (scapedJSONStringfy(id) !== targetHandle) {
|
||||
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
||||
|
|
@ -132,7 +159,9 @@ export function detectBrokenEdgesEdges(nodes: AllNodeType[], edges: Edge[]) {
|
|||
displayName: targetNode.data.node!.display_name,
|
||||
field:
|
||||
targetNode.data.node!.template[targetHandleObject.fieldName]
|
||||
?.display_name ?? targetHandleObject.fieldName,
|
||||
?.display_name ??
|
||||
targetHandleObject.fieldName ??
|
||||
targetHandleObject.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -161,14 +190,39 @@ export function detectBrokenEdgesEdges(nodes: AllNodeType[], edges: Edge[]) {
|
|||
if (targetHandle) {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle);
|
||||
const field = targetHandleObject.fieldName;
|
||||
const id: targetHandleType = {
|
||||
type: targetNode.data.node!.template[field]?.type,
|
||||
fieldName: field,
|
||||
id: targetNode.data.id,
|
||||
inputTypes: targetNode.data.node!.template[field]?.input_types,
|
||||
};
|
||||
if (targetNode.data.node!.template[field]?.proxy) {
|
||||
id.proxy = targetNode.data.node!.template[field]?.proxy;
|
||||
let id: sourceHandleType | targetHandleType;
|
||||
|
||||
const templateFieldType = targetNode.data.node!.template[field]?.type;
|
||||
const inputTypes = targetNode.data.node!.template[field]?.input_types;
|
||||
const hasProxy = targetNode.data.node!.template[field]?.proxy;
|
||||
|
||||
if (
|
||||
!field &&
|
||||
targetHandleObject.name &&
|
||||
targetNode.type === "genericNode"
|
||||
) {
|
||||
const dataType = targetNode.data.type;
|
||||
const outputTypes =
|
||||
targetNode.data.node!.outputs?.find(
|
||||
(output) => output.name === targetHandleObject.name,
|
||||
)?.types ?? [];
|
||||
|
||||
id = {
|
||||
dataType: dataType ?? "",
|
||||
name: targetHandleObject.name,
|
||||
id: targetNode.data.id,
|
||||
output_types: outputTypes,
|
||||
};
|
||||
} else {
|
||||
id = {
|
||||
type: templateFieldType,
|
||||
fieldName: field,
|
||||
id: targetNode.data.id,
|
||||
inputTypes: inputTypes,
|
||||
};
|
||||
if (hasProxy) {
|
||||
id.proxy = targetNode.data.node!.template[field]?.proxy;
|
||||
}
|
||||
}
|
||||
if (scapedJSONStringfy(id) !== targetHandle) {
|
||||
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
||||
|
|
@ -219,7 +273,7 @@ export function isValidConnection(
|
|||
{ source, target, sourceHandle, targetHandle }: Connection,
|
||||
nodes: AllNodeType[],
|
||||
edges: EdgeType[],
|
||||
) {
|
||||
): boolean {
|
||||
if (source === target) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -229,6 +283,13 @@ export function isValidConnection(
|
|||
targetHandleObject.inputTypes?.some(
|
||||
(n) => n === sourceHandleObject.dataType,
|
||||
) ||
|
||||
(targetHandleObject.output_types &&
|
||||
(targetHandleObject.output_types?.some(
|
||||
(n) => n === sourceHandleObject.dataType,
|
||||
) ||
|
||||
sourceHandleObject.output_types.some((t) =>
|
||||
targetHandleObject.output_types?.some((n) => n === t),
|
||||
))) ||
|
||||
sourceHandleObject.output_types.some(
|
||||
(t) =>
|
||||
targetHandleObject.inputTypes?.some((n) => n === t) ||
|
||||
|
|
@ -241,9 +302,15 @@ export function isValidConnection(
|
|||
return true;
|
||||
}
|
||||
} else if (
|
||||
(!targetNode.template[targetHandleObject.fieldName].list &&
|
||||
targetHandleObject.output_types &&
|
||||
!edges.find((e) => e.targetHandle === targetHandle)
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
!targetHandleObject.output_types &&
|
||||
((!targetNode.template[targetHandleObject.fieldName].list &&
|
||||
!edges.find((e) => e.targetHandle === targetHandle)) ||
|
||||
targetNode.template[targetHandleObject.fieldName].list
|
||||
targetNode.template[targetHandleObject.fieldName].list)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -485,9 +552,51 @@ Array<{ id: string; errors: Array<string> }> {
|
|||
id: n.id,
|
||||
errors: validateNode(n, edges),
|
||||
}));
|
||||
|
||||
return nodeMap.filter((n) => n.errors?.length);
|
||||
}
|
||||
|
||||
export function validateEdge(
|
||||
e: EdgeType,
|
||||
nodes: AllNodeType[],
|
||||
edges: EdgeType[],
|
||||
): Array<string> {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(e.targetHandle!);
|
||||
|
||||
const loop = hasLoop(e, nodes, edges);
|
||||
if (targetHandleObject.output_types && !loop) {
|
||||
return [INCOMPLETE_LOOP_ERROR_ALERT];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasLoop(
|
||||
e: EdgeType,
|
||||
nodes: AllNodeType[],
|
||||
edges: EdgeType[],
|
||||
): boolean {
|
||||
const source = e.source;
|
||||
const target = e.target;
|
||||
|
||||
// Check if this connection would create a cycle
|
||||
const targetNode = nodes.find((n) => n.id === target);
|
||||
|
||||
const hasCycle = (node, visited = new Set()): boolean => {
|
||||
if (visited.has(node.id)) return false;
|
||||
|
||||
visited.add(node.id);
|
||||
|
||||
for (const outgoer of getOutgoers(node, nodes, edges)) {
|
||||
if (outgoer.id === source) return true;
|
||||
if (hasCycle(outgoer, visited)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (targetNode?.id === source) return false;
|
||||
return hasCycle(targetNode);
|
||||
}
|
||||
|
||||
export function updateEdges(edges: EdgeType[]) {
|
||||
if (edges)
|
||||
edges.forEach((edge) => {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ import {
|
|||
HelpCircle,
|
||||
Home,
|
||||
Image,
|
||||
Infinity,
|
||||
Info,
|
||||
InstagramIcon,
|
||||
Key,
|
||||
|
|
@ -866,6 +867,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Share2,
|
||||
Share,
|
||||
GitBranchPlus,
|
||||
Infinity,
|
||||
Loader2,
|
||||
BookmarkPlus,
|
||||
Heart,
|
||||
|
|
|
|||
243
src/frontend/tests/extended/features/loop-component.spec.ts
Normal file
243
src/frontend/tests/extended/features/loop-component.spec.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
import { zoomOut } from "../../utils/zoom-out";
|
||||
|
||||
test(
|
||||
"should process loop with update data correctly",
|
||||
{ tag: ["@release", "@workspace", "@components"] },
|
||||
async ({ page }) => {
|
||||
await awaitBootstrapTest(page);
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="sidebar-custom-component-button"]',
|
||||
{
|
||||
timeout: 3000,
|
||||
},
|
||||
);
|
||||
|
||||
// Add URL component
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("url");
|
||||
await page.waitForSelector('[data-testid="dataURL"]', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await zoomOut(page, 3);
|
||||
|
||||
await page
|
||||
.getByTestId("dataURL")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 100, y: 100 },
|
||||
});
|
||||
|
||||
// Add Loop component
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("loop");
|
||||
await page.waitForSelector('[data-testid="logicLoop"]', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("logicLoop")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 300, 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"]', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("processingUpdate Data")
|
||||
.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("parse data");
|
||||
await page.waitForSelector('[data-testid="processingParse Data"]', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("processingParse Data")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 700, y: 100 },
|
||||
});
|
||||
|
||||
//This one is for testing the wrong loop message
|
||||
await page
|
||||
.getByTestId("processingParse Data")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 700, y: 400 },
|
||||
});
|
||||
|
||||
const secondParseDataOutput = await page
|
||||
.getByTestId("handle-parsedata-shownode-data list-right")
|
||||
.nth(2);
|
||||
|
||||
const loopItemInput = await page
|
||||
.getByTestId("handle-loopcomponent-shownode-item-left")
|
||||
.first();
|
||||
|
||||
// Connecting the second parse data to the loop item to test the wrong loop message
|
||||
|
||||
await secondParseDataOutput.hover();
|
||||
await page.mouse.down();
|
||||
await loopItemInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
// Add Chat Output component
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("chat output");
|
||||
await page.waitForSelector('[data-testid="outputsChat Output"]', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("outputsChat Output")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 900, y: 100 },
|
||||
});
|
||||
|
||||
await page.getByTestId("fit_view").click();
|
||||
|
||||
await zoomOut(page, 2);
|
||||
|
||||
// Loop Item -> Update Data
|
||||
|
||||
const loopItemHandle = await page
|
||||
.getByTestId("handle-loopcomponent-shownode-item-right")
|
||||
.first();
|
||||
const updateDataInput = await page
|
||||
.getByTestId("handle-updatedata-shownode-data-left")
|
||||
.first();
|
||||
|
||||
await loopItemHandle.hover();
|
||||
await page.mouse.down();
|
||||
await updateDataInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
// URL -> Loop Data
|
||||
const urlOutput = await page
|
||||
.getByTestId("handle-url-shownode-data-right")
|
||||
.first();
|
||||
const loopInput = await page
|
||||
.getByTestId("handle-loopcomponent-shownode-data-left")
|
||||
.first();
|
||||
|
||||
await urlOutput.hover();
|
||||
await page.mouse.down();
|
||||
await loopInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
// Loop Done -> Parse Data
|
||||
const loopDoneHandle = await page
|
||||
.getByTestId("handle-loopcomponent-shownode-done-right")
|
||||
.first();
|
||||
const parseDataInput = await page
|
||||
.getByTestId("handle-parsedata-shownode-data-left")
|
||||
.first();
|
||||
|
||||
await loopDoneHandle.hover();
|
||||
await page.mouse.down();
|
||||
await parseDataInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
await page.getByTestId("div-generic-node").nth(5).click();
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
|
||||
await page.getByTestId("expand-button-modal").click();
|
||||
|
||||
// Parse Data -> Chat Output
|
||||
const parseDataOutput = await page
|
||||
.getByTestId("handle-parsedata-shownode-message-right")
|
||||
.first();
|
||||
|
||||
const chatOutputInput = await page
|
||||
.getByTestId("handle-chatoutput-shownode-text-left")
|
||||
.first();
|
||||
|
||||
await parseDataOutput.hover();
|
||||
await page.mouse.down();
|
||||
await chatOutputInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
await page.getByTestId("input-list-plus-btn_urls-0").click();
|
||||
|
||||
// Configure components
|
||||
await page
|
||||
.getByTestId("inputlist_str_urls_0")
|
||||
.fill("https://en.wikipedia.org/wiki/Artificial_intelligence");
|
||||
await page
|
||||
.getByTestId("inputlist_str_urls_1")
|
||||
.fill("https://en.wikipedia.org/wiki/Artificial_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("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.waitForSelector("text=The flow has an incomplete loop.", {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByText("The flow has an incomplete loop.").last().click({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Delete the second parse data used to test
|
||||
|
||||
await page.getByTestId("div-generic-node").nth(4).click();
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
|
||||
await page.getByText("Delete").first().click();
|
||||
|
||||
// Update Data -> Loop Item (left side)
|
||||
const updateDataOutput = await page
|
||||
.getByTestId("handle-updatedata-shownode-data-right")
|
||||
.first();
|
||||
|
||||
await updateDataOutput.hover();
|
||||
await page.mouse.down();
|
||||
await loopItemInput.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
// Build and run
|
||||
await page.getByTestId("button_run_chat output").click();
|
||||
await page.waitForSelector("text=built successfully", { timeout: 30000 });
|
||||
await page.getByText("built successfully").last().click({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Verify output
|
||||
await page.waitForSelector(
|
||||
'[data-testid="output-inspection-message-chatoutput"]',
|
||||
{
|
||||
timeout: 1000,
|
||||
},
|
||||
);
|
||||
await page
|
||||
.getByTestId("output-inspection-message-chatoutput")
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole("gridcell").nth(4).click();
|
||||
|
||||
const output = await page.getByPlaceholder("Empty").textContent();
|
||||
expect(output).toContain("modified_value");
|
||||
|
||||
// Count occurrences of modified_value in output
|
||||
const matches = output?.match(/modified_value/g) || [];
|
||||
expect(matches).toHaveLength(2);
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue