From b41d0e1782beb768ac75d77d099437383d3b37d2 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Thu, 12 Jun 2025 15:57:16 +0800 Subject: [PATCH] fix(api): fix the issue that variable assigner does not save user input into variable pool --- .../nodes/variable_assigner/v1/node.py | 15 +++++--- .../nodes/variable_assigner/v2/entities.py | 6 +++ .../nodes/variable_assigner/v2/exc.py | 5 +++ .../nodes/variable_assigner/v2/node.py | 37 +++++++++++++++---- .../workflow_draft_variable_service.py | 2 +- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/api/core/workflow/nodes/variable_assigner/v1/node.py b/api/core/workflow/nodes/variable_assigner/v1/node.py index 438c4a7d06..be5083c9c1 100644 --- a/api/core/workflow/nodes/variable_assigner/v1/node.py +++ b/api/core/workflow/nodes/variable_assigner/v1/node.py @@ -61,14 +61,17 @@ class VariableAssignerNode(BaseNode[VariableAssignerData]): node_id: str, node_data: VariableAssignerData, ) -> Mapping[str, Sequence[str]]: + mapping = {} assigned_variable_node_id = node_data.assigned_variable_selector[0] - if assigned_variable_node_id != CONVERSATION_VARIABLE_NODE_ID: - return {} - selector_key = ".".join(node_data.assigned_variable_selector) + if assigned_variable_node_id == CONVERSATION_VARIABLE_NODE_ID: + selector_key = ".".join(node_data.assigned_variable_selector) + key = f"{node_id}.#{selector_key}#" + mapping[key] = node_data.assigned_variable_selector + + selector_key = ".".join(node_data.input_variable_selector) key = f"{node_id}.#{selector_key}#" - return { - key: node_data.assigned_variable_selector, - } + mapping[key] = node_data.input_variable_selector + return mapping def _run(self) -> NodeRunResult: assigned_variable_selector = self.node_data.assigned_variable_selector diff --git a/api/core/workflow/nodes/variable_assigner/v2/entities.py b/api/core/workflow/nodes/variable_assigner/v2/entities.py index 01df33b6d4..d93affcd15 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/entities.py +++ b/api/core/workflow/nodes/variable_assigner/v2/entities.py @@ -12,6 +12,12 @@ class VariableOperationItem(BaseModel): variable_selector: Sequence[str] input_type: InputType operation: Operation + # NOTE(QuantumGhost): The `value` field serves multiple purposes depending on context: + # + # 1. For CONSTANT input_type: Contains the literal value to be used in the operation. + # 2. For VARIABLE input_type: Initially contains the selector of the source variable. + # 3. During the variable updating procedure: The `value` field is reassigned to hold + # the resolved actual value that will be applied to the target variable. value: Any | None = None diff --git a/api/core/workflow/nodes/variable_assigner/v2/exc.py b/api/core/workflow/nodes/variable_assigner/v2/exc.py index b67af6d73c..fd6c304a9a 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/exc.py +++ b/api/core/workflow/nodes/variable_assigner/v2/exc.py @@ -29,3 +29,8 @@ class InvalidInputValueError(VariableOperatorNodeError): class ConversationIDNotFoundError(VariableOperatorNodeError): def __init__(self): super().__init__("conversation_id not found") + + +class InvalidDataError(VariableOperatorNodeError): + def __init__(self, message: str) -> None: + super().__init__(message) diff --git a/api/core/workflow/nodes/variable_assigner/v2/node.py b/api/core/workflow/nodes/variable_assigner/v2/node.py index 71ba96fd45..1804316a31 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/node.py +++ b/api/core/workflow/nodes/variable_assigner/v2/node.py @@ -1,9 +1,10 @@ import json -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Mapping, MutableMapping, Sequence from typing import Any, ClassVar, TypeAlias, cast from core.app.entities.app_invoke_entities import InvokeFrom from core.variables import SegmentType, Variable +from core.variables.consts import MIN_SELECTORS_LENGTH from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.entities.node_entities import NodeRunResult @@ -16,11 +17,12 @@ from core.workflow.nodes.variable_assigner.common.impl import conversation_varia from . import helpers from .constants import EMPTY_VALUE_MAPPING -from .entities import VariableAssignerNodeData +from .entities import VariableAssignerNodeData, VariableOperationItem from .enums import InputType, Operation from .exc import ( ConversationIDNotFoundError, InputTypeNotSupportedError, + InvalidDataError, InvalidInputValueError, OperationNotSupportedError, VariableNotFoundError, @@ -29,6 +31,29 @@ from .exc import ( _CONV_VAR_UPDATER_FACTORY: TypeAlias = Callable[[], ConversationVariableUpdater] +def _target_mapping_from_item(mapping: MutableMapping[str, Sequence[str]], node_id: str, item: VariableOperationItem): + selector_node_id = item.variable_selector[0] + if selector_node_id != CONVERSATION_VARIABLE_NODE_ID: + return + selector_str = ".".join(item.variable_selector) + key = f"{node_id}.#{selector_str}#" + mapping[key] = item.variable_selector + + +def _source_mapping_from_item(mapping: MutableMapping[str, Sequence[str]], node_id: str, item: VariableOperationItem): + # Keep this in sync with the logic in _run methods... + if item.input_type != InputType.VARIABLE: + return + selector = item.value + if not isinstance(selector, list): + raise InvalidDataError(f"selector is not a list, {node_id=}, {item=}") + if len(selector) < MIN_SELECTORS_LENGTH: + raise InvalidDataError(f"selector too short, {node_id=}, {item=}") + selector_str = ".".join(selector) + key = f"{node_id}.#{selector_str}#" + mapping[key] = selector + + class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): _node_data_cls = VariableAssignerNodeData _node_type = NodeType.VARIABLE_ASSIGNER @@ -49,12 +74,8 @@ class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): ) -> Mapping[str, Sequence[str]]: var_mapping: dict[str, Sequence[str]] = {} for item in node_data.items: - selector_node_id = item.variable_selector[0] - if selector_node_id != CONVERSATION_VARIABLE_NODE_ID: - continue - selector_str = ".".join(item.variable_selector) - key = f"{node_id}.#{selector_str}#" - var_mapping[key] = item.variable_selector + _target_mapping_from_item(var_mapping, node_id, item) + _source_mapping_from_item(var_mapping, node_id, item) return var_mapping def _run(self) -> NodeRunResult: diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index fee3a4139d..72243a166a 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -703,11 +703,11 @@ class DraftVariableSaver: Raw variable value if found, None otherwise """ outputs_dict = node_exec.outputs_dict or {} - process_data_dict = node_exec.process_data_dict or {} # Note: Based on the implementation in `_build_from_variable_assigner_mapping`, # VariableAssignerNode (both v1 and v2) can only create conversation draft variables. # For consistency, we should simply return when processing VARIABLE_ASSIGNER nodes. + # # This implementation must remain synchronized with the `_build_from_variable_assigner_mapping` # and `save` methods.