feat: import Graph without position information (#3203)

* refactor: Add utility functions for getting handle IDs in CustomNodes

- Added `getRightHandleId` function to generate the right handle ID for source handles.
- Added `getLeftHandleId` function to generate the left handle ID for target handles.
- These functions improve code readability and maintainability by encapsulating the logic for generating handle IDs.

* refactor: Add type for escaped handle IDs in edges to improve type safety in reactflowUtils

* feat: Add function to escape handle IDs in edges, enhancing edge management in reactflowUtils

* feat: Add function to check edges without escaped handle IDs, improving edge validation in reactflowUtils

* feat: Enhance edge processing in reactflowUtils to handle edges without escaped handle IDs more effectively

* feat: Add layoutUtils module for handling node layout using elkjs

* feat: update processDataFromFlow to add layout to nodes if needed

* Refactor import paths to use 'initialize' module in 'base.py'

* feat: Add method to set class source code and integrate it with frontend node input field

* refactor: Update sourceHandle dataType to use custom component class name

* fix: Raise error for unknown vertex types instead of returning default Vertex class

* refactor: Remove redundant call to _import_vertex_types() in VertexTypesDict initialization

* refactor: Simplify add_code_field by removing unnecessary field_config parameter from function signature

* feat: Add elkjs dependency to package.json and package-lock.json for enhanced functionality in the frontend

* refactor: Update fields type in Template class to use InputTypes for improved type safety

* refactor: Reorganize imports in __init__.py for better structure and consistency across the inputs module

* refactor: Clean up imports in types.py for better organization and consistency in the graph vertex module

* refactor: Change vertex type annotations to strings for better compatibility and consistency in the graph module methods

* refactor: Update component instantiation to include _code parameter and fix input type annotations for improved type handling

* refactor: Remove unused CustomComponent import from __init__.py for cleaner module structure and improved organization

* refactor: Modify custom_component instantiation to include _code argument for enhanced functionality and clarity in CodeParser class

* refactor: Update CustomComponent import in __init__.py for improved module structure and organization

* refactor: Update launch.json to include correct path for backend source files

* refactor: Update dependencies in poetry.lock to latest versions and resolve merge conflicts in backend files

* refactor: Update dataType assignment in Component class to use component name if available, or fallback to class name

* refactor: Correct flow_id reference in MemoryComponent to improve clarity and consistency in memory handling

* refactor: Update import path for DefaultPromptField to improve code organization and maintainability in api_utils.py

* refactor: Add loading module to __init__.py for improved organization of interface package

* refactor: Clean up imports in base.py and enforce edge validation in Graph class for improved maintainability and error handling

* refactor: Remove edge component additions in test_base.py to streamline graph tests and emphasize error handling for unprepared graphs

* refactor: Mark @clack/prompts is-unicode-supported as extraneous in package-lock.json for better dependency management

* refactor: Update dataType assignment in Component class to use component name if available, or fallback to class name

* refactor: Fix edge existence check in Graph class to use correct variable, ensuring accurate validation of graph structure

* refactor: Add test for graph with edge and improve graph preparation logic

* refactor: Set default node type to "genericNode" in getLayoutedNodes for consistent layout structure

* create consts for node widht and height

* refactor: Catch and log errors when processing flow data in flowsManagerStore and reactflowUtils

* [autofix.ci] apply automated fixes

* fix: Validate custom components for source and target vertices in Graph edges

* test: Add fixture for client and raise TypeError for invalid class parsing in CodeParser tests

* test: Add unit test for listing flows as Flow objects in custom component with client

* test: Update assertions for memory chatbot component types in unit tests

* test: Refactor assertions to use updated component names in vector store RAG unit tests

* fix: Change error handling to return default Vertex for unknown node types in graph class

* [autofix.ci] apply automated fixes

* test: Add pytest fixture for CustomComponent in unit tests to enhance test structure and readability

* chore: Update component names in vector store RAG unit tests

* test: Refactor imports and make flow name generation unique in database unit tests

* chore: Remove unused upload and flow management functions from flowsManagerStore for cleaner codebase

* chore: Await processDataFromFlow in useAddFlow hook

* chore: Correct NODE_HEIGHT calculation to use NODE_WIDTH constant for consistency in constants file

* chore: Remove extraneous flag for is-unicode-supported in package-lock.json for cleaner dependency management

---------

Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-08-07 11:04:48 -03:00 • committed by GitHub
commit 919073a0db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 364 additions and 106 deletions

View file

@ -45,6 +45,7 @@
"cmdk": "^1.0.0",
"dompurify": "^3.1.5",
"dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0",
"esbuild": "^0.21.5",
"file-saver": "^2.0.5",
@ -7424,6 +7425,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz",
"integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg=="
},
"node_modules/elkjs": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
},
"node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",

View file

@ -40,6 +40,7 @@
"cmdk": "^1.0.0",
"dompurify": "^3.1.5",
"dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0",
"esbuild": "^0.21.5",
"file-saver": "^2.0.5",

View file

@ -0,0 +1,30 @@
import { sourceHandleType, targetHandleType } from "@/types/flow";
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
export function getRightHandleId({
output_types,
id,
dataType,
name,
}: sourceHandleType): string {
return scapedJSONStringfy({
dataType,
id,
output_types,
name,
});
}
export function getLeftHandleId({
inputTypes,
type,
fieldName,
id,
}: targetHandleType): string {
return scapedJSONStringfy({
inputTypes,
id,
type,
fieldName,
});
}

View file

@ -879,3 +879,6 @@ export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS = 60 * 60 - 60 * 60 * 0.1;
export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV =
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) -
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) * 0.1;
export const NODE_WIDTH = 384;
export const NODE_HEIGHT = NODE_WIDTH * 3;

View file

@ -34,7 +34,7 @@ const useAddFlow = () => {
return new Promise(async (resolve, reject) => {
const flow = cloneDeep(params?.flow) ?? undefined;
let flowData = flow
? processDataFromFlow(flow)
? await processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
flowData?.nodes.forEach((node) => {
updateGroupRecursion(

View file

@ -6,6 +6,10 @@ export type unselectAllNodesType = {
data: Node[];
};
export type addEscapedHandleIdsToEdgesType = {
edges: Edge[];
};
export type updateEdgesHandleIdsType = {
nodes: NodeType[];
edges: Edge[];

View file

@ -0,0 +1,76 @@
import { NODE_HEIGHT, NODE_WIDTH } from "@/constants/constants";
import { NodeType } from "@/types/flow";
import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js";
import { cloneDeep } from "lodash";
import { Edge } from "reactflow";
const layoutOptions = {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.layered.spacing.edgeNodeBetweenLayers": "40",
"elk.spacing.nodeNode": "40",
"elk.layered.nodePlacement.strategy": "SIMPLE",
};
const elk = new ELK();
// uses elkjs to give each node a layouted position
export const getLayoutedNodes = async (nodes: NodeType[], edges: Edge[]) => {
const graph = {
id: "root",
layoutOptions,
children: cloneDeep(nodes).map((n) => {
const targetPorts = edges
.filter((e) => e.source === n.id)
.map((e) => ({
id: e.sourceHandle,
properties: {
side: "EAST",
},
}));
const sourcePorts = edges
.filter((e) => e.target === n.id)
.map((e) => ({
id: e.targetHandle,
properties: {
side: "WEST",
},
}));
return {
id: n.id,
width: NODE_WIDTH,
height: NODE_HEIGHT,
// ⚠️ we need to tell elk that the ports are fixed, in order to reduce edge crossings
properties: {
"org.eclipse.elk.portConstraints": "FIXED_ORDER",
},
// we are also passing the id, so we can also handle edges without a sourceHandle or targetHandle option
ports: [{ id: n.id }, ...targetPorts, ...sourcePorts],
};
}) as ElkNode[],
edges: edges.map((e) => ({
id: e.id,
sources: [e.sourceHandle || e.source],
targets: [e.targetHandle || e.target],
})),
};
const layoutedGraph = await elk.layout(graph);
const layoutedNodes = nodes.map((node) => {
const layoutedNode = layoutedGraph.children?.find(
(lgNode) => lgNode.id === node.id,
);
return {
...node,
position: {
x: layoutedNode?.x ?? 0,
y: layoutedNode?.y ?? 0,
},
type: "genericNode",
};
});
return layoutedNodes;
};

View file

@ -1,3 +1,7 @@
import {
getLeftHandleId,
getRightHandleId,
} from "@/CustomNodes/utils/get-handle-id";
import { cloneDeep } from "lodash";
import {
Connection,
@ -34,11 +38,13 @@ import {
targetHandleType,
} from "../types/flow";
import {
addEscapedHandleIdsToEdgesType,
findLastNodeType,
generateFlowType,
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { getLayoutedNodes } from "./layoutUtils";
import { createRandomKey, toTitleCase } from "./utils";
const uid = new ShortUniqueId();
@ -274,7 +280,7 @@ export function updateTemplate(
export const processFlows = (DbData: FlowType[], skipUpdate = true) => {
let savedComponents: { [key: string]: APIClassType } = {};
DbData.forEach((flow: FlowType) => {
DbData.forEach(async (flow: FlowType) => {
try {
if (!flow.data) {
return;
@ -290,15 +296,24 @@ export const processFlows = (DbData: FlowType[], skipUpdate = true) => {
] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return;
}
processDataFromFlow(flow, !skipUpdate);
await processDataFromFlow(flow, !skipUpdate).catch((e) => {
console.error(e);
});
} catch (e) {
console.log(e);
console.error(e);
}
});
return { data: savedComponents, flows: DbData };
};
export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
const needsLayout = (nodes: NodeType[]) => {
return nodes.some((node) => !node.position);
};
export async function processDataFromFlow(
flow: FlowType,
refreshIds = true,
): Promise<ReactFlowJsonObject | null> {
let data = flow?.data ? flow.data : null;
if (data) {
processFlowEdges(flow);
@ -308,9 +323,14 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
updateEdges(data.edges);
// updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere
// add layout to nodes if not present
if (needsLayout(data.nodes)) {
const layoutedNodes = await getLayoutedNodes(data.nodes, data.edges);
data.nodes = layoutedNodes;
}
}
return data;
};
}
export function updateIds(
{ edges, nodes }: { edges: Edge[]; nodes: Node[] },
@ -341,6 +361,7 @@ export function updateIds(
concatedEdges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!,
);
@ -475,6 +496,26 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
return newName;
}
export function addEscapedHandleIdsToEdges({
edges,
}: addEscapedHandleIdsToEdgesType): Edge[] {
let newEdges = cloneDeep(edges);
newEdges.forEach((edge) => {
let escapedSourceHandle = edge.sourceHandle;
let escapedTargetHandle = edge.targetHandle;
if (!escapedSourceHandle) {
let sourceHandle = edge.data?.sourceHandle;
escapedSourceHandle = getRightHandleId(sourceHandle);
edge.sourceHandle = escapedSourceHandle;
}
if (!escapedTargetHandle) {
let targetHandle = edge.data?.targetHandle;
escapedTargetHandle = getLeftHandleId(targetHandle);
edge.targetHandle = escapedTargetHandle;
}
});
return newEdges;
}
export function updateEdgesHandleIds({
edges,
nodes,
@ -740,6 +781,13 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean {
);
}
export function checkEdgeWithoutEscapedHandleIds(edges: Edge[]): boolean {
return edges.some(
(edge) =>
(!edge.sourceHandle || !edge.targetHandle) && edge.data?.sourceHandle,
);
}
export function checkOldNodesOutput(nodes: NodeType[]): boolean {
return nodes.some((node) => !node.data.node?.outputs);
}
@ -1231,7 +1279,10 @@ export function updateEdgesIds(
export function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
if (checkEdgeWithoutEscapedHandleIds(flow.data.edges)) {
const newEdges = addEscapedHandleIdsToEdges({ edges: flow.data.edges });
flow.data.edges = newEdges;
} else if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}