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:
Lucas Oliveira 2025-01-21 14:52:28 -03:00 • committed by GitHub
commit f08c18f54a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2594 additions and 85 deletions

View file

@ -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}
/>
);
}

View file

@ -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>

View file

@ -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";

View 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)),

View file

@ -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> };

View file

@ -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 };
};

View file

@ -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) => {

View file

@ -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,

View 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);
},
);