diff --git a/ee/codegen/src/__test__/nodes/generic-nodes/__snapshots__/generic-node.test.ts.snap b/ee/codegen/src/__test__/nodes/generic-nodes/__snapshots__/generic-node.test.ts.snap index dd098078e..19f2580d3 100644 --- a/ee/codegen/src/__test__/nodes/generic-nodes/__snapshots__/generic-node.test.ts.snap +++ b/ee/codegen/src/__test__/nodes/generic-nodes/__snapshots__/generic-node.test.ts.snap @@ -401,3 +401,70 @@ class MyCustomNode(BaseNode): pass " `; + +exports[`GenericNode > node_id skipping when it matches hash-generated UUID > getNodeDisplayFile should include node_id when it does not match hash 1`] = ` +"from uuid import UUID + +from vellum_ee.workflows.display.editor import NodeDisplayData, NodeDisplayPosition +from vellum_ee.workflows.display.nodes import BaseNodeDisplay +from vellum_ee.workflows.display.nodes.types import ( + NodeOutputDisplay, + PortDisplayOverrides, +) + +from ...nodes.my_custom_node import MyCustomNode + + +class MyCustomNodeDisplay(BaseNodeDisplay[MyCustomNode]): + label = "MyCustomNode" + node_id = UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + attribute_ids_by_name = { + "default_attribute": UUID("990d55db-9d72-452a-b074-9bee1f89ecb9"), + "default_attribute_2": UUID("70652383-d93f-4c3a-b194-1ea5cdced8f1"), + } + output_display = { + MyCustomNode.Outputs.output: NodeOutputDisplay( + id=UUID("output-1"), name="output" + ) + } + port_displays = { + MyCustomNode.Ports.default_port: PortDisplayOverrides( + id=UUID("2544f9e4-d6e6-4475-b6a9-13393115d77c") + ) + } + display_data = NodeDisplayData(position=NodeDisplayPosition(x=0, y=0)) +" +`; + +exports[`GenericNode > node_id skipping when it matches hash-generated UUID > getNodeDisplayFile should skip node_id when it matches hash 1`] = ` +"from uuid import UUID + +from vellum_ee.workflows.display.editor import NodeDisplayData, NodeDisplayPosition +from vellum_ee.workflows.display.nodes import BaseNodeDisplay +from vellum_ee.workflows.display.nodes.types import ( + NodeOutputDisplay, + PortDisplayOverrides, +) + +from ...nodes.my_custom_node import MyCustomNode + + +class MyCustomNodeDisplay(BaseNodeDisplay[MyCustomNode]): + label = "MyCustomNode" + attribute_ids_by_name = { + "default_attribute": UUID("990d55db-9d72-452a-b074-9bee1f89ecb9"), + "default_attribute_2": UUID("70652383-d93f-4c3a-b194-1ea5cdced8f1"), + } + output_display = { + MyCustomNode.Outputs.output: NodeOutputDisplay( + id=UUID("output-1"), name="output" + ) + } + port_displays = { + MyCustomNode.Ports.default_port: PortDisplayOverrides( + id=UUID("2544f9e4-d6e6-4475-b6a9-13393115d77c") + ) + } + display_data = NodeDisplayData(position=NodeDisplayPosition(x=0, y=0)) +" +`; diff --git a/ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts b/ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts index e42ef0911..24abfe331 100644 --- a/ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts +++ b/ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts @@ -756,4 +756,70 @@ describe("GenericNode", () => { expect(await writer.toStringFormatted()).toMatchSnapshot(); }); }); + + // TODO: Unskip this test once vembda-side issues are resolved + describe.skip("node_id skipping when it matches hash-generated UUID", () => { + /** + * Tests that node_id is omitted from the display class when it matches + * the deterministically generated UUID from the node's module path and class name. + */ + it("getNodeDisplayFile should skip node_id when it matches hash", async () => { + // GIVEN a generic node with an ID that matches the hash-generated UUID + // The hash of "my_nodes.my_custom_node.MyCustomNode" is "1be19097-e8d7-4acc-8bb5-2a99dc12dece" + const nodeData = genericNodeFactory({ + id: "1be19097-e8d7-4acc-8bb5-2a99dc12dece", + label: "MyCustomNode", + nodePorts: [ + nodePortFactory({ + id: "2544f9e4-d6e6-4475-b6a9-13393115d77c", + }), + ], + }); + + const nodeContext = (await createNodeContext({ + workflowContext, + nodeData, + })) as GenericNodeContext; + + node = new GenericNode({ + workflowContext, + nodeContext, + }); + + // WHEN we generate the node display file + node.getNodeDisplayFile().write(writer); + + // THEN the output should match the snapshot (without node_id attribute) + expect(await writer.toStringFormatted()).toMatchSnapshot(); + }); + + it("getNodeDisplayFile should include node_id when it does not match hash", async () => { + // GIVEN a generic node with an ID that does NOT match the hash-generated UUID + const nodeData = genericNodeFactory({ + id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + label: "MyCustomNode", + nodePorts: [ + nodePortFactory({ + id: "2544f9e4-d6e6-4475-b6a9-13393115d77c", + }), + ], + }); + + const nodeContext = (await createNodeContext({ + workflowContext, + nodeData, + })) as GenericNodeContext; + + node = new GenericNode({ + workflowContext, + nodeContext, + }); + + // WHEN we generate the node display file + node.getNodeDisplayFile().write(writer); + + // THEN the output should match the snapshot (with node_id attribute) + expect(await writer.toStringFormatted()).toMatchSnapshot(); + }); + }); }); diff --git a/ee/codegen/src/generators/nodes/bases/base.ts b/ee/codegen/src/generators/nodes/bases/base.ts index 3cee2f153..58b54b25e 100644 --- a/ee/codegen/src/generators/nodes/bases/base.ts +++ b/ee/codegen/src/generators/nodes/bases/base.ts @@ -43,6 +43,8 @@ import { pascalToTitleCase, toValidPythonIdentifier } from "src/utils/casing"; import { findNodeDefinitionByBaseClassName } from "src/utils/node-definitions"; import { doesModulePathStartWith } from "src/utils/paths"; import { isNilOrEmpty } from "src/utils/typing"; +// TODO: Uncomment when vembda-side issues are resolved +// import { getNodeIdFromDefinition } from "src/utils/uuids"; export declare namespace BaseNode { interface Args> { @@ -778,12 +780,17 @@ export abstract class BaseNode< ); } + // TODO: Uncomment this check once vembda-side issues are resolved + // Only add node_id if it differs from the hash-generated UUID + // const expectedNodeId = getNodeIdFromDefinition(this.nodeData.definition); + // if (expectedNodeId === undefined || this.nodeData.id !== expectedNodeId) { nodeClass.add( python.field({ name: "node_id", initializer: python.TypeInstantiation.uuid(this.nodeData.id), }) ); + // } this.getNodeDisplayClassBodyStatements().forEach((statement) => nodeClass.add(statement) diff --git a/ee/codegen/src/utils/uuids.ts b/ee/codegen/src/utils/uuids.ts new file mode 100644 index 000000000..9e8c018ce --- /dev/null +++ b/ee/codegen/src/utils/uuids.ts @@ -0,0 +1,57 @@ +import { createHash } from "crypto"; + +/** + * Generate a deterministic UUID v4 from a string input using SHA-256 hashing. + * This matches the Python implementation in src/vellum/workflows/utils/uuids.py + * + * @param inputStr - The string to hash + * @returns A UUID v4 string derived from the hash + */ +export function uuid4FromHash(inputStr: string): string { + // Create a SHA-256 hash of the input string + const hashBytes = createHash("sha256").update(inputStr).digest(); + + // Convert first 16 bytes into a mutable array + // SHA-256 always produces 32 bytes, so we're guaranteed to have at least 16 bytes + const hashList: number[] = Array.from(hashBytes.subarray(0, 16)); + + // Set the version to 4 (UUID4) + // Version bits (4 bits) should be set to 4 + const byte6 = hashList[6]; + const byte8 = hashList[8]; + if (byte6 !== undefined && byte8 !== undefined) { + hashList[6] = (byte6 & 0x0f) | 0x40; + // Set the variant to 0b10xxxxxx + hashList[8] = (byte8 & 0x3f) | 0x80; + } + + // Convert to UUID string format (8-4-4-4-12) + const hex = hashList.map((b) => b.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice( + 12, + 16 + )}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; +} + +/** + * Generate a deterministic node ID from a code resource definition. + * This matches the Python SDK's node ID generation pattern: + * uuid4_from_hash(f"{node_class.__module__}.{node_class.__qualname__}") + * + * @param definition - The code resource definition containing module path and name + * @returns A UUID v4 string, or undefined if definition is not provided + */ +export function getNodeIdFromDefinition( + definition: + | { + module: readonly string[]; + name: string; + } + | undefined +): string | undefined { + if (!definition) { + return undefined; + } + const moduleStr = definition.module.join("."); + return uuid4FromHash(`${moduleStr}.${definition.name}`); +}