Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
"
`;
66 changes: 66 additions & 0 deletions ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
7 changes: 7 additions & 0 deletions ee/codegen/src/generators/nodes/bases/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends WorkflowDataNode, V extends BaseNodeContext<T>> {
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions ee/codegen/src/utils/uuids.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}