Skip to content

Commit 3228cc7

Browse files
Add uuid4FromHash utility to codegen (node_id skipping disabled pending vembda fix) (#3272)
1 parent b0f1bb8 commit 3228cc7

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

ee/codegen/src/__test__/nodes/generic-nodes/__snapshots__/generic-node.test.ts.snap

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,70 @@ class MyCustomNode(BaseNode):
401401
pass
402402
"
403403
`;
404+
405+
exports[`GenericNode > node_id skipping when it matches hash-generated UUID > getNodeDisplayFile should include node_id when it does not match hash 1`] = `
406+
"from uuid import UUID
407+
408+
from vellum_ee.workflows.display.editor import NodeDisplayData, NodeDisplayPosition
409+
from vellum_ee.workflows.display.nodes import BaseNodeDisplay
410+
from vellum_ee.workflows.display.nodes.types import (
411+
NodeOutputDisplay,
412+
PortDisplayOverrides,
413+
)
414+
415+
from ...nodes.my_custom_node import MyCustomNode
416+
417+
418+
class MyCustomNodeDisplay(BaseNodeDisplay[MyCustomNode]):
419+
label = "MyCustomNode"
420+
node_id = UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
421+
attribute_ids_by_name = {
422+
"default_attribute": UUID("990d55db-9d72-452a-b074-9bee1f89ecb9"),
423+
"default_attribute_2": UUID("70652383-d93f-4c3a-b194-1ea5cdced8f1"),
424+
}
425+
output_display = {
426+
MyCustomNode.Outputs.output: NodeOutputDisplay(
427+
id=UUID("output-1"), name="output"
428+
)
429+
}
430+
port_displays = {
431+
MyCustomNode.Ports.default_port: PortDisplayOverrides(
432+
id=UUID("2544f9e4-d6e6-4475-b6a9-13393115d77c")
433+
)
434+
}
435+
display_data = NodeDisplayData(position=NodeDisplayPosition(x=0, y=0))
436+
"
437+
`;
438+
439+
exports[`GenericNode > node_id skipping when it matches hash-generated UUID > getNodeDisplayFile should skip node_id when it matches hash 1`] = `
440+
"from uuid import UUID
441+
442+
from vellum_ee.workflows.display.editor import NodeDisplayData, NodeDisplayPosition
443+
from vellum_ee.workflows.display.nodes import BaseNodeDisplay
444+
from vellum_ee.workflows.display.nodes.types import (
445+
NodeOutputDisplay,
446+
PortDisplayOverrides,
447+
)
448+
449+
from ...nodes.my_custom_node import MyCustomNode
450+
451+
452+
class MyCustomNodeDisplay(BaseNodeDisplay[MyCustomNode]):
453+
label = "MyCustomNode"
454+
attribute_ids_by_name = {
455+
"default_attribute": UUID("990d55db-9d72-452a-b074-9bee1f89ecb9"),
456+
"default_attribute_2": UUID("70652383-d93f-4c3a-b194-1ea5cdced8f1"),
457+
}
458+
output_display = {
459+
MyCustomNode.Outputs.output: NodeOutputDisplay(
460+
id=UUID("output-1"), name="output"
461+
)
462+
}
463+
port_displays = {
464+
MyCustomNode.Ports.default_port: PortDisplayOverrides(
465+
id=UUID("2544f9e4-d6e6-4475-b6a9-13393115d77c")
466+
)
467+
}
468+
display_data = NodeDisplayData(position=NodeDisplayPosition(x=0, y=0))
469+
"
470+
`;

ee/codegen/src/__test__/nodes/generic-nodes/generic-node.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,4 +756,70 @@ describe("GenericNode", () => {
756756
expect(await writer.toStringFormatted()).toMatchSnapshot();
757757
});
758758
});
759+
760+
// TODO: Unskip this test once vembda-side issues are resolved
761+
describe.skip("node_id skipping when it matches hash-generated UUID", () => {
762+
/**
763+
* Tests that node_id is omitted from the display class when it matches
764+
* the deterministically generated UUID from the node's module path and class name.
765+
*/
766+
it("getNodeDisplayFile should skip node_id when it matches hash", async () => {
767+
// GIVEN a generic node with an ID that matches the hash-generated UUID
768+
// The hash of "my_nodes.my_custom_node.MyCustomNode" is "1be19097-e8d7-4acc-8bb5-2a99dc12dece"
769+
const nodeData = genericNodeFactory({
770+
id: "1be19097-e8d7-4acc-8bb5-2a99dc12dece",
771+
label: "MyCustomNode",
772+
nodePorts: [
773+
nodePortFactory({
774+
id: "2544f9e4-d6e6-4475-b6a9-13393115d77c",
775+
}),
776+
],
777+
});
778+
779+
const nodeContext = (await createNodeContext({
780+
workflowContext,
781+
nodeData,
782+
})) as GenericNodeContext;
783+
784+
node = new GenericNode({
785+
workflowContext,
786+
nodeContext,
787+
});
788+
789+
// WHEN we generate the node display file
790+
node.getNodeDisplayFile().write(writer);
791+
792+
// THEN the output should match the snapshot (without node_id attribute)
793+
expect(await writer.toStringFormatted()).toMatchSnapshot();
794+
});
795+
796+
it("getNodeDisplayFile should include node_id when it does not match hash", async () => {
797+
// GIVEN a generic node with an ID that does NOT match the hash-generated UUID
798+
const nodeData = genericNodeFactory({
799+
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
800+
label: "MyCustomNode",
801+
nodePorts: [
802+
nodePortFactory({
803+
id: "2544f9e4-d6e6-4475-b6a9-13393115d77c",
804+
}),
805+
],
806+
});
807+
808+
const nodeContext = (await createNodeContext({
809+
workflowContext,
810+
nodeData,
811+
})) as GenericNodeContext;
812+
813+
node = new GenericNode({
814+
workflowContext,
815+
nodeContext,
816+
});
817+
818+
// WHEN we generate the node display file
819+
node.getNodeDisplayFile().write(writer);
820+
821+
// THEN the output should match the snapshot (with node_id attribute)
822+
expect(await writer.toStringFormatted()).toMatchSnapshot();
823+
});
824+
});
759825
});

ee/codegen/src/generators/nodes/bases/base.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { pascalToTitleCase, toValidPythonIdentifier } from "src/utils/casing";
4444
import { findNodeDefinitionByBaseClassName } from "src/utils/node-definitions";
4545
import { doesModulePathStartWith } from "src/utils/paths";
4646
import { isNilOrEmpty } from "src/utils/typing";
47+
// TODO: Uncomment when vembda-side issues are resolved
48+
// import { getNodeIdFromDefinition } from "src/utils/uuids";
4749

4850
export declare namespace BaseNode {
4951
interface Args<T extends WorkflowDataNode, V extends BaseNodeContext<T>> {
@@ -779,12 +781,17 @@ export abstract class BaseNode<
779781
);
780782
}
781783

784+
// TODO: Uncomment this check once vembda-side issues are resolved
785+
// Only add node_id if it differs from the hash-generated UUID
786+
// const expectedNodeId = getNodeIdFromDefinition(this.nodeData.definition);
787+
// if (expectedNodeId === undefined || this.nodeData.id !== expectedNodeId) {
782788
nodeClass.add(
783789
python.field({
784790
name: "node_id",
785791
initializer: python.TypeInstantiation.uuid(this.nodeData.id),
786792
})
787793
);
794+
// }
788795

789796
this.getNodeDisplayClassBodyStatements().forEach((statement) =>
790797
nodeClass.add(statement)

ee/codegen/src/utils/uuids.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createHash } from "crypto";
2+
3+
/**
4+
* Generate a deterministic UUID v4 from a string input using SHA-256 hashing.
5+
* This matches the Python implementation in src/vellum/workflows/utils/uuids.py
6+
*
7+
* @param inputStr - The string to hash
8+
* @returns A UUID v4 string derived from the hash
9+
*/
10+
export function uuid4FromHash(inputStr: string): string {
11+
// Create a SHA-256 hash of the input string
12+
const hashBytes = createHash("sha256").update(inputStr).digest();
13+
14+
// Convert first 16 bytes into a mutable array
15+
// SHA-256 always produces 32 bytes, so we're guaranteed to have at least 16 bytes
16+
const hashList: number[] = Array.from(hashBytes.subarray(0, 16));
17+
18+
// Set the version to 4 (UUID4)
19+
// Version bits (4 bits) should be set to 4
20+
const byte6 = hashList[6];
21+
const byte8 = hashList[8];
22+
if (byte6 !== undefined && byte8 !== undefined) {
23+
hashList[6] = (byte6 & 0x0f) | 0x40;
24+
// Set the variant to 0b10xxxxxx
25+
hashList[8] = (byte8 & 0x3f) | 0x80;
26+
}
27+
28+
// Convert to UUID string format (8-4-4-4-12)
29+
const hex = hashList.map((b) => b.toString(16).padStart(2, "0")).join("");
30+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
31+
12,
32+
16
33+
)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
34+
}
35+
36+
/**
37+
* Generate a deterministic node ID from a code resource definition.
38+
* This matches the Python SDK's node ID generation pattern:
39+
* uuid4_from_hash(f"{node_class.__module__}.{node_class.__qualname__}")
40+
*
41+
* @param definition - The code resource definition containing module path and name
42+
* @returns A UUID v4 string, or undefined if definition is not provided
43+
*/
44+
export function getNodeIdFromDefinition(
45+
definition:
46+
| {
47+
module: readonly string[];
48+
name: string;
49+
}
50+
| undefined
51+
): string | undefined {
52+
if (!definition) {
53+
return undefined;
54+
}
55+
const moduleStr = definition.module.join(".");
56+
return uuid4FromHash(`${moduleStr}.${definition.name}`);
57+
}

0 commit comments

Comments
 (0)