Files
dify/api/services/dify_builder/revision.py
T

96 lines
3.8 KiB
Python

"""Execution revisions for Builder, separate from the editor's full graph hash."""
import hashlib
import json
from typing import TYPE_CHECKING
from graphon.enums import BuiltinNodeTypes
if TYPE_CHECKING:
from models.workflow import Workflow
# Only strip presentation fields at their defined canvas paths. A node's
# configuration may legitimately contain keys such as "position" or "width".
_NODE_PRESENTATION_FIELDS = frozenset(
{
"type", # ReactFlow renderer; the executable node type is data.type.
"position",
"positionAbsolute",
"width",
"height",
"selected",
"dragging",
"resizing",
"measured",
"zIndex",
"style",
"sourcePosition",
"targetPosition",
}
)
_EDGE_PRESENTATION_FIELDS = frozenset({"type", "selected", "animated", "style", "zIndex"})
# initialEdges derives these rendering hints from the connected nodes. Builder
# creates edges without them, so the first canvas save adds them even when the
# user only moves a node. Container membership in NODE data remains executable.
_EDGE_DATA_PRESENTATION_FIELDS = frozenset(
{"sourceType", "targetType", "isInIteration", "iteration_id", "isInLoop", "loop_id"}
)
def execution_revision(workflow: "Workflow") -> str:
"""Hash execution inputs while retaining unknown configuration fields.
Canvas hydration and the editor's transient data keys (prefixed with ``_``)
do not affect execution. Array order is preserved: sorting configuration
arrays can change branch, prompt, or variable semantics. Secret variables
use their resolved values so re-encryption alone cannot invalidate a plan.
Only the digest leaves the adapter; plaintext values are neither returned
nor persisted here.
Mutation callers resolve secrets while holding the workflow row lock.
Key-provider access is required there to compare the same configuration
that will be mutated, including any concurrent secret changes.
"""
graph = dict(workflow.graph_dict)
graph.pop("viewport", None)
graph["nodes"] = [
{
**{key: value for key, value in node.items() if key not in _NODE_PRESENTATION_FIELDS},
"data": {
key: value
for key, value in node.get("data", {}).items()
if key != "selected"
and not key.startswith("_")
and not (
node.get("data", {}).get("type") in {BuiltinNodeTypes.ITERATION, BuiltinNodeTypes.LOOP}
and key in {"width", "height"}
)
},
}
for node in graph.get("nodes", [])
if node.get("type") != "custom-note"
]
graph["edges"] = [
{
**{key: value for key, value in edge.items() if key not in _EDGE_PRESENTATION_FIELDS},
# The canvas fills missing handles with these defaults. Preserve
# explicit values, especially source handles that select branches.
"sourceHandle": edge.get("sourceHandle", "source"),
"targetHandle": edge.get("targetHandle", "target"),
"data": {
key: value
for key, value in edge.get("data", {}).items()
if key not in _EDGE_DATA_PRESENTATION_FIELDS and not key.startswith("_")
},
}
for edge in graph.get("edges", [])
]
content = {
"graph": graph,
"features": workflow.features_dict,
"environment_variables": [variable.model_dump(mode="json") for variable in workflow.environment_variables],
"conversation_variables": [variable.model_dump(mode="json") for variable in workflow.conversation_variables],
}
return hashlib.sha256(json.dumps(content, sort_keys=True, separators=(",", ":")).encode()).hexdigest()