mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
fix: assemble variable support nested node format
This commit is contained in:
@ -8,31 +8,16 @@ from pydantic_core.core_schema import ValidationInfo
|
|||||||
from core.tools.entities.tool_entities import ToolProviderType
|
from core.tools.entities.tool_entities import ToolProviderType
|
||||||
from core.workflow.nodes.base.entities import BaseNodeData
|
from core.workflow.nodes.base.entities import BaseNodeData
|
||||||
|
|
||||||
# Pattern to match nested_node value format: {{@node.context@}}instruction
|
# Pattern to match mention format: {{@node.context@}}instruction
|
||||||
# The placeholder {{@node.context@}} must appear at the beginning
|
MENTION_VALUE_PATTERN = re.compile(r"^\{\{@([a-zA-Z0-9_]+)\.context@\}\}(.*)$", re.DOTALL)
|
||||||
# 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 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]:
|
def is_variable_format(value: str) -> bool:
|
||||||
"""Parse nested_node value into (node_id, instruction).
|
"""Check if value is variable format {{#node_id.variable#}}."""
|
||||||
|
return VARIABLE_VALUE_PATTERN.match(value) is not None
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class NestedNodeConfig(BaseModel):
|
class NestedNodeConfig(BaseModel):
|
||||||
@ -127,12 +112,11 @@ class ToolNodeData(BaseNodeData, ToolEntity):
|
|||||||
|
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise ValueError("value must be a string for nested_node type")
|
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:
|
if self.nested_node_config is None:
|
||||||
raise ValueError("nested_node_config is required for nested_node type")
|
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
|
return self
|
||||||
|
|
||||||
tool_parameters: dict[str, ToolInput]
|
tool_parameters: dict[str, ToolInput]
|
||||||
|
|||||||
@ -31,7 +31,7 @@ from factories import file_factory
|
|||||||
from models import ToolFile
|
from models import ToolFile
|
||||||
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
|
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
|
||||||
|
|
||||||
from .entities import ToolNodeData
|
from .entities import ToolNodeData, is_variable_format
|
||||||
from .exc import (
|
from .exc import (
|
||||||
ToolFileError,
|
ToolFileError,
|
||||||
ToolNodeError,
|
ToolNodeError,
|
||||||
@ -213,20 +213,18 @@ class ToolNode(Node[ToolNodeData]):
|
|||||||
continue
|
continue
|
||||||
parameter_value = variable.value
|
parameter_value = variable.value
|
||||||
elif tool_input.type == "nested_node":
|
elif tool_input.type == "nested_node":
|
||||||
# Nested node type: get value from extractor node's output
|
if not isinstance(tool_input.value, str) or tool_input.nested_node_config is None:
|
||||||
if tool_input.nested_node_config is None:
|
raise ToolParameterError(f"Invalid nested_node parameter '{parameter_name}'")
|
||||||
raise ToolParameterError(
|
config = tool_input.nested_node_config
|
||||||
f"nested_node_config is required for nested_node type parameter '{parameter_name}'"
|
# Variable format: use output_selector directly
|
||||||
)
|
# Mention format: use extractor_node_id + output_selector
|
||||||
nested_node_config = tool_input.nested_node_config.model_dump()
|
use_extractor = not is_variable_format(tool_input.value)
|
||||||
try:
|
try:
|
||||||
parameter_value, found = variable_pool.resolve_nested_node(
|
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:
|
if not found and parameter.required:
|
||||||
raise ToolParameterError(
|
raise ToolParameterError(f"Value not found for required parameter '{parameter_name}'")
|
||||||
f"Extractor output not found for required parameter '{parameter_name}'"
|
|
||||||
)
|
|
||||||
if not found:
|
if not found:
|
||||||
continue
|
continue
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@ -273,50 +273,36 @@ class VariablePool(BaseModel):
|
|||||||
nested_node_config: Mapping[str, Any],
|
nested_node_config: Mapping[str, Any],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
|
use_extractor: bool = True,
|
||||||
parameter_name: str = "",
|
parameter_name: str = "",
|
||||||
) -> tuple[Any, bool]:
|
) -> tuple[Any, bool]:
|
||||||
"""
|
"""
|
||||||
Resolve a nested_node parameter value from an extractor node's output.
|
Resolve a nested_node parameter value.
|
||||||
|
|
||||||
Nested node parameters reference values extracted by an extractor LLM node
|
|
||||||
from list[PromptMessage] context.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nested_node_config: A dict containing:
|
nested_node_config: Config dict containing output_selector, null_strategy, default_value,
|
||||||
- extractor_node_id: ID of the extractor LLM node
|
and optionally extractor_node_id
|
||||||
- output_selector: Selector path for the output variable (e.g., ["text"])
|
use_extractor: If True (mention format), build selector as extractor_node_id + output_selector.
|
||||||
- null_strategy: "raise_error" or "use_default"
|
If False (variable format), use output_selector directly.
|
||||||
- default_value: Value to use when null_strategy is "use_default"
|
|
||||||
parameter_name: Name of the parameter being resolved (for error messages)
|
parameter_name: Name of the parameter being resolved (for error messages)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (resolved_value, found):
|
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
|
|
||||||
"""
|
"""
|
||||||
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", []))
|
output_selector = list(nested_node_config.get("output_selector", []))
|
||||||
null_strategy = nested_node_config.get("null_strategy", "raise_error")
|
null_strategy = nested_node_config.get("null_strategy", "raise_error")
|
||||||
default_value = nested_node_config.get("default_value")
|
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
|
full_selector = [extractor_node_id] + output_selector
|
||||||
variable = self.get(full_selector)
|
|
||||||
|
|
||||||
|
variable = self.get(full_selector)
|
||||||
if variable is None:
|
if variable is None:
|
||||||
if null_strategy == "use_default":
|
if null_strategy == "use_default":
|
||||||
return default_value, False
|
return default_value, False
|
||||||
raise ValueError(
|
raise ValueError(f"Variable {full_selector} not found for parameter '{parameter_name}'")
|
||||||
f"Extractor node '{extractor_node_id}' output '{'.'.join(output_selector)}' "
|
|
||||||
f"not found for parameter '{parameter_name}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
return variable.value, True
|
return variable.value, True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user