From e9f4bde18f75de1707a67a7cb274267afad0a877 Mon Sep 17 00:00:00 2001 From: Novice Date: Fri, 23 Jan 2026 17:22:28 +0800 Subject: [PATCH] fix: assemble variable support nested node format --- api/core/workflow/nodes/tool/entities.py | 38 +++++++--------------- api/core/workflow/nodes/tool/tool_node.py | 20 +++++------- api/core/workflow/runtime/variable_pool.py | 38 +++++++--------------- 3 files changed, 32 insertions(+), 64 deletions(-) diff --git a/api/core/workflow/nodes/tool/entities.py b/api/core/workflow/nodes/tool/entities.py index 458bbdd9ed..285e6e2dab 100644 --- a/api/core/workflow/nodes/tool/entities.py +++ b/api/core/workflow/nodes/tool/entities.py @@ -8,31 +8,16 @@ from pydantic_core.core_schema import ValidationInfo from core.tools.entities.tool_entities import ToolProviderType from core.workflow.nodes.base.entities import BaseNodeData -# Pattern to match nested_node value format: {{@node.context@}}instruction -# The placeholder {{@node.context@}} must appear at the beginning -# Format: {{@agent_node_id.context@}} where agent_node_id is dynamic, context is fixed -NESTED_NODE_VALUE_PATTERN = re.compile(r"^\{\{@([a-zA-Z0-9_]+)\.context@\}\}(.*)$", re.DOTALL) +# Pattern to match mention format: {{@node.context@}}instruction +MENTION_VALUE_PATTERN = re.compile(r"^\{\{@([a-zA-Z0-9_]+)\.context@\}\}(.*)$", re.DOTALL) + +# Pattern to match variable format: {{#node_id.variable#}} +VARIABLE_VALUE_PATTERN = re.compile(r"^\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}$") -def parse_nested_node_value(value: str) -> tuple[str, str]: - """Parse nested_node value into (node_id, instruction). - - Args: - value: The nested_node value string like "{{@llm.context@}}extract keywords" - - Returns: - Tuple of (node_id, instruction) - - Raises: - ValueError: If value format is invalid - """ - match = NESTED_NODE_VALUE_PATTERN.match(value) - if not match: - raise ValueError( - "For nested_node type, value must start with {{@node.context@}} placeholder, " - "e.g., '{{@llm.context@}}extract keywords'" - ) - return match.group(1), match.group(2) +def is_variable_format(value: str) -> bool: + """Check if value is variable format {{#node_id.variable#}}.""" + return VARIABLE_VALUE_PATTERN.match(value) is not None class NestedNodeConfig(BaseModel): @@ -127,12 +112,11 @@ class ToolNodeData(BaseNodeData, ToolEntity): if not isinstance(value, str): raise ValueError("value must be a string for nested_node type") - # For nested_node type, value must match format: {{@node.context@}}instruction - # This will raise ValueError if format is invalid - parse_nested_node_value(value) - # nested_node_config is required for nested_node type if self.nested_node_config is None: raise ValueError("nested_node_config is required for nested_node type") + # Validate format: must be variable {{#...#}} or mention {{@...@}} + if not is_variable_format(value) and not MENTION_VALUE_PATTERN.match(value): + raise ValueError("value must be variable format {{#node.var#}} or mention format {{@node.context@}}") return self tool_parameters: dict[str, ToolInput] diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 915a46f2ab..d49d8330f9 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -31,7 +31,7 @@ from factories import file_factory from models import ToolFile from services.tools.builtin_tools_manage_service import BuiltinToolManageService -from .entities import ToolNodeData +from .entities import ToolNodeData, is_variable_format from .exc import ( ToolFileError, ToolNodeError, @@ -213,20 +213,18 @@ class ToolNode(Node[ToolNodeData]): continue parameter_value = variable.value elif tool_input.type == "nested_node": - # Nested node type: get value from extractor node's output - if tool_input.nested_node_config is None: - raise ToolParameterError( - f"nested_node_config is required for nested_node type parameter '{parameter_name}'" - ) - nested_node_config = tool_input.nested_node_config.model_dump() + if not isinstance(tool_input.value, str) or tool_input.nested_node_config is None: + raise ToolParameterError(f"Invalid nested_node parameter '{parameter_name}'") + config = tool_input.nested_node_config + # Variable format: use output_selector directly + # Mention format: use extractor_node_id + output_selector + use_extractor = not is_variable_format(tool_input.value) try: parameter_value, found = variable_pool.resolve_nested_node( - nested_node_config, parameter_name=parameter_name + config.model_dump(), use_extractor=use_extractor, parameter_name=parameter_name ) if not found and parameter.required: - raise ToolParameterError( - f"Extractor output not found for required parameter '{parameter_name}'" - ) + raise ToolParameterError(f"Value not found for required parameter '{parameter_name}'") if not found: continue except ValueError as e: diff --git a/api/core/workflow/runtime/variable_pool.py b/api/core/workflow/runtime/variable_pool.py index 0e83bf770d..9e4c4e6757 100644 --- a/api/core/workflow/runtime/variable_pool.py +++ b/api/core/workflow/runtime/variable_pool.py @@ -273,50 +273,36 @@ class VariablePool(BaseModel): nested_node_config: Mapping[str, Any], /, *, + use_extractor: bool = True, parameter_name: str = "", ) -> tuple[Any, bool]: """ - Resolve a nested_node parameter value from an extractor node's output. - - Nested node parameters reference values extracted by an extractor LLM node - from list[PromptMessage] context. + Resolve a nested_node parameter value. Args: - nested_node_config: A dict containing: - - extractor_node_id: ID of the extractor LLM node - - output_selector: Selector path for the output variable (e.g., ["text"]) - - null_strategy: "raise_error" or "use_default" - - default_value: Value to use when null_strategy is "use_default" + nested_node_config: Config dict containing output_selector, null_strategy, default_value, + and optionally extractor_node_id + use_extractor: If True (mention format), build selector as extractor_node_id + output_selector. + If False (variable format), use output_selector directly. parameter_name: Name of the parameter being resolved (for error messages) Returns: - Tuple of (resolved_value, found): - - resolved_value: The extracted value, or default_value if not found - - found: True if value was found, False if using default - - Raises: - ValueError: If extractor_node_id is missing, or if null_strategy is - "raise_error" and the value is not found + Tuple of (resolved_value, found) """ - extractor_node_id = nested_node_config.get("extractor_node_id") - if not extractor_node_id: - raise ValueError(f"Missing extractor_node_id for nested_node parameter '{parameter_name}'") - output_selector = list(nested_node_config.get("output_selector", [])) null_strategy = nested_node_config.get("null_strategy", "raise_error") default_value = nested_node_config.get("default_value") - # Build full selector: [extractor_node_id, ...output_selector] + extractor_node_id = nested_node_config.get("extractor_node_id") + if not extractor_node_id: + raise ValueError(f"Missing extractor_node_id for parameter '{parameter_name}'") full_selector = [extractor_node_id] + output_selector - variable = self.get(full_selector) + variable = self.get(full_selector) if variable is None: if null_strategy == "use_default": return default_value, False - raise ValueError( - f"Extractor node '{extractor_node_id}' output '{'.'.join(output_selector)}' " - f"not found for parameter '{parameter_name}'" - ) + raise ValueError(f"Variable {full_selector} not found for parameter '{parameter_name}'") return variable.value, True