Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 13 additions & 5 deletions ee/codegen/src/generators/nodes/bases/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ 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";
import { getNodeIdFromModuleAndName } from "src/utils/uuids";

export declare namespace BaseNode {
interface Args<T extends WorkflowDataNode, V extends BaseNodeContext<T>> {
Expand Down Expand Up @@ -778,12 +779,19 @@ export abstract class BaseNode<
);
}

nodeClass.add(
python.field({
name: "node_id",
initializer: python.TypeInstantiation.uuid(this.nodeData.id),
})
// Only add node_id if it differs from the hash-generated UUID
const expectedNodeId = getNodeIdFromModuleAndName(
nodeContext.nodeModulePath,
nodeContext.nodeClassName
);
if (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
51 changes: 51 additions & 0 deletions ee/codegen/src/utils/uuids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 module path and class name.
* This matches the Python SDK's node ID generation pattern:
* uuid4_from_hash(f"{node_class.__module__}.{node_class.__qualname__}")
*
* @param modulePath - The module path as an array of strings
* @param className - The class name
* @returns A UUID v4 string
*/
export function getNodeIdFromModuleAndName(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevinAI update this helper to accept the full definition instead of forcing the consumer to split the fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed all feedback:

  1. Updated helper to accept full definition - Renamed getNodeIdFromModuleAndName to getNodeIdFromDefinition and updated it to accept the full definition object (with module and name properties) instead of separate fields

  2. Commented out the node_id check - The check in base.ts is now commented out with a TODO marker pending vembda-side fixes

  3. Skipped the tests - The describe block for node_id skipping tests is now using describe.skip with a TODO marker

modulePath: readonly string[],
className: string
): string {
const moduleStr = modulePath.join(".");
return uuid4FromHash(`${moduleStr}.${className}`);
}