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:
parent
a7bfb55e8c
commit
919073a0db
31 changed files with 364 additions and 106 deletions
6
src/frontend/package-lock.json
generated
6
src/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
30
src/frontend/src/CustomNodes/utils/get-handle-id.tsx
Normal file
30
src/frontend/src/CustomNodes/utils/get-handle-id.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export type unselectAllNodesType = {
|
|||
data: Node[];
|
||||
};
|
||||
|
||||
export type addEscapedHandleIdsToEdgesType = {
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export type updateEdgesHandleIdsType = {
|
||||
nodes: NodeType[];
|
||||
edges: Edge[];
|
||||
|
|
|
|||
76
src/frontend/src/utils/layoutUtils.ts
Normal file
76
src/frontend/src/utils/layoutUtils.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue