refactor: rename mention node to nested_node for generic sub-graph support

This commit is contained in:
Novice
2026-01-22 13:15:13 +08:00
parent c7d106cfa4
commit 5cb8d4cc11
35 changed files with 319 additions and 289 deletions

View File

@ -46,8 +46,8 @@ from models.workflow import Workflow
from services.app_generate_service import AppGenerateService
from services.errors.app import WorkflowHashNotEqualError
from services.errors.llm import InvokeRateLimitError
from services.workflow.entities import MentionGraphRequest, MentionParameterSchema
from services.workflow.mention_graph_service import MentionGraphService
from services.workflow.entities import NestedNodeGraphRequest, NestedNodeParameterSchema
from services.workflow.nested_node_graph_service import NestedNodeGraphService
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
logger = logging.getLogger(__name__)
@ -190,8 +190,8 @@ class DraftWorkflowTriggerRunAllPayload(BaseModel):
node_ids: list[str]
class MentionGraphPayload(BaseModel):
"""Request payload for generating mention graph."""
class NestedNodeGraphPayload(BaseModel):
"""Request payload for generating nested node graph."""
parent_node_id: str = Field(description="ID of the parent node that uses the extracted value")
parameter_key: str = Field(description="Key of the parameter being extracted")
@ -216,7 +216,7 @@ reg(WorkflowListQuery)
reg(WorkflowUpdatePayload)
reg(DraftWorkflowTriggerRunPayload)
reg(DraftWorkflowTriggerRunAllPayload)
reg(MentionGraphPayload)
reg(NestedNodeGraphPayload)
# TODO(QuantumGhost): Refactor existing node run API to handle file parameter parsing
@ -1180,20 +1180,20 @@ class DraftWorkflowTriggerRunAllApi(Resource):
), 400
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/mention-graph")
class MentionGraphApi(Resource):
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nested-node-graph")
class NestedNodeGraphApi(Resource):
"""
API for generating Mention LLM node graph structures.
API for generating Nested Node LLM graph structures.
This endpoint creates a complete graph structure containing an LLM node
configured to extract values from list[PromptMessage] variables.
"""
@console_ns.doc("generate_mention_graph")
@console_ns.doc(description="Generate a Mention LLM node graph structure")
@console_ns.doc("generate_nested_node_graph")
@console_ns.doc(description="Generate a Nested Node LLM graph structure")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[MentionGraphPayload.__name__])
@console_ns.response(200, "Mention graph generated successfully")
@console_ns.expect(console_ns.models[NestedNodeGraphPayload.__name__])
@console_ns.response(200, "Nested node graph generated successfully")
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(403, "Permission denied")
@setup_required
@ -1203,21 +1203,21 @@ class MentionGraphApi(Resource):
@edit_permission_required
def post(self, app_model: App):
"""
Generate a Mention LLM node graph structure.
Generate a Nested Node LLM graph structure.
Returns a complete graph structure containing a single LLM node
configured for extracting values from list[PromptMessage] context.
"""
payload = MentionGraphPayload.model_validate(console_ns.payload or {})
payload = NestedNodeGraphPayload.model_validate(console_ns.payload or {})
parameter_schema = MentionParameterSchema(
parameter_schema = NestedNodeParameterSchema(
name=payload.parameter_schema.get("name", payload.parameter_key),
type=payload.parameter_schema.get("type", "string"),
description=payload.parameter_schema.get("description", ""),
)
request = MentionGraphRequest(
request = NestedNodeGraphRequest(
parent_node_id=payload.parent_node_id,
parameter_key=payload.parameter_key,
context_source=payload.context_source,
@ -1225,7 +1225,7 @@ class MentionGraphApi(Resource):
)
with Session(db.engine) as session:
service = MentionGraphService(session)
response = service.generate_mention_graph(tenant_id=app_model.tenant_id, request=request)
service = NestedNodeGraphService(session)
response = service.generate_nested_node_graph(tenant_id=app_model.tenant_id, request=request)
return response.model_dump()

View File

@ -70,8 +70,8 @@ class _NodeSnapshot:
"""Empty string means the node is not executing inside an iteration."""
loop_id: str = ""
"""Empty string means the node is not executing inside a loop."""
mention_parent_id: str = ""
"""Empty string means the node is not an extractor node."""
parent_node_id: str = ""
"""Empty string means the node is not an nested node (extractor node)."""
class WorkflowResponseConverter:
@ -133,7 +133,7 @@ class WorkflowResponseConverter:
start_at=event.start_at,
iteration_id=event.in_iteration_id or "",
loop_id=event.in_loop_id or "",
mention_parent_id=event.in_mention_parent_id or "",
parent_node_id=event.in_parent_node_id or "",
)
node_execution_id = NodeExecutionId(event.node_execution_id)
self._node_snapshots[node_execution_id] = snapshot
@ -290,7 +290,7 @@ class WorkflowResponseConverter:
created_at=int(snapshot.start_at.timestamp()),
iteration_id=event.in_iteration_id,
loop_id=event.in_loop_id,
mention_parent_id=event.in_mention_parent_id,
parent_node_id=event.in_parent_node_id,
agent_strategy=event.agent_strategy,
),
)
@ -377,7 +377,7 @@ class WorkflowResponseConverter:
files=self.fetch_files_from_node_outputs(event.outputs or {}),
iteration_id=event.in_iteration_id,
loop_id=event.in_loop_id,
mention_parent_id=event.in_mention_parent_id,
parent_node_id=event.in_parent_node_id,
),
)
@ -427,7 +427,7 @@ class WorkflowResponseConverter:
files=self.fetch_files_from_node_outputs(event.outputs or {}),
iteration_id=event.in_iteration_id,
loop_id=event.in_loop_id,
mention_parent_id=event.in_mention_parent_id,
parent_node_id=event.in_parent_node_id,
retry_index=event.retry_index,
),
)

View File

@ -385,7 +385,7 @@ class WorkflowBasedAppRunner:
start_at=event.start_at,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
inputs=inputs,
process_data=process_data,
outputs=outputs,
@ -406,7 +406,7 @@ class WorkflowBasedAppRunner:
start_at=event.start_at,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
agent_strategy=event.agent_strategy,
provider_type=event.provider_type,
provider_id=event.provider_id,
@ -430,7 +430,7 @@ class WorkflowBasedAppRunner:
execution_metadata=execution_metadata,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
)
)
elif isinstance(event, NodeRunFailedEvent):
@ -447,7 +447,7 @@ class WorkflowBasedAppRunner:
execution_metadata=event.node_run_result.metadata,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
)
)
elif isinstance(event, NodeRunExceptionEvent):
@ -464,7 +464,7 @@ class WorkflowBasedAppRunner:
execution_metadata=event.node_run_result.metadata,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
)
)
elif isinstance(event, NodeRunStreamChunkEvent):
@ -482,7 +482,7 @@ class WorkflowBasedAppRunner:
chunk_type=QueueChunkType(event.chunk_type.value),
tool_call=event.tool_call,
tool_result=event.tool_result,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
)
)
elif isinstance(event, NodeRunRetrieverResourceEvent):
@ -491,7 +491,7 @@ class WorkflowBasedAppRunner:
retriever_resources=event.retriever_resources,
in_iteration_id=event.in_iteration_id,
in_loop_id=event.in_loop_id,
in_mention_parent_id=event.in_mention_parent_id,
in_parent_node_id=event.in_parent_node_id,
)
)
elif isinstance(event, NodeRunAgentLogEvent):

View File

@ -201,7 +201,7 @@ class QueueTextChunkEvent(AppQueueEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
# Extended fields for Agent/Tool streaming
@ -252,7 +252,7 @@ class QueueRetrieverResourcesEvent(AppQueueEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
@ -331,7 +331,7 @@ class QueueNodeStartedEvent(AppQueueEvent):
node_run_index: int = 1 # FIXME(-LAN-): may not used
in_iteration_id: str | None = None
in_loop_id: str | None = None
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
start_at: datetime
agent_strategy: AgentNodeStrategyInit | None = None
@ -355,7 +355,7 @@ class QueueNodeSucceededEvent(AppQueueEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
start_at: datetime
@ -412,7 +412,7 @@ class QueueNodeExceptionEvent(AppQueueEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
start_at: datetime
@ -438,7 +438,7 @@ class QueueNodeFailedEvent(AppQueueEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
in_parent_node_id: str | None = None
"""parent node id if this is an extractor node event"""
start_at: datetime

View File

@ -294,7 +294,7 @@ class NodeStartStreamResponse(StreamResponse):
extras: dict[str, object] = Field(default_factory=dict)
iteration_id: str | None = None
loop_id: str | None = None
mention_parent_id: str | None = None
parent_node_id: str | None = None
agent_strategy: AgentNodeStrategyInit | None = None
event: StreamEvent = StreamEvent.NODE_STARTED
@ -318,7 +318,7 @@ class NodeStartStreamResponse(StreamResponse):
"extras": {},
"iteration_id": self.data.iteration_id,
"loop_id": self.data.loop_id,
"mention_parent_id": self.data.mention_parent_id,
"parent_node_id": self.data.parent_node_id,
},
}
@ -354,7 +354,7 @@ class NodeFinishStreamResponse(StreamResponse):
files: Sequence[Mapping[str, Any]] | None = []
iteration_id: str | None = None
loop_id: str | None = None
mention_parent_id: str | None = None
parent_node_id: str | None = None
event: StreamEvent = StreamEvent.NODE_FINISHED
workflow_run_id: str
@ -384,7 +384,7 @@ class NodeFinishStreamResponse(StreamResponse):
"files": [],
"iteration_id": self.data.iteration_id,
"loop_id": self.data.loop_id,
"mention_parent_id": self.data.mention_parent_id,
"parent_node_id": self.data.parent_node_id,
},
}
@ -420,7 +420,7 @@ class NodeRetryStreamResponse(StreamResponse):
files: Sequence[Mapping[str, Any]] | None = []
iteration_id: str | None = None
loop_id: str | None = None
mention_parent_id: str | None = None
parent_node_id: str | None = None
retry_index: int = 0
event: StreamEvent = StreamEvent.NODE_RETRY
@ -451,7 +451,7 @@ class NodeRetryStreamResponse(StreamResponse):
"files": [],
"iteration_id": self.data.iteration_id,
"loop_id": self.data.loop_id,
"mention_parent_id": self.data.mention_parent_id,
"parent_node_id": self.data.parent_node_id,
"retry_index": self.data.retry_index,
},
}

View File

@ -813,7 +813,19 @@ Parameter: {parameter_info.get("name")} ({param_type}) - {parameter_info.get("de
if isinstance(v, dict)
]
outputs = content.get("outputs", {"result": {"type": parameter_type}})
# Convert outputs from array format [{name, type}] to dict format {name: {type}}
# Array format is required for OpenAI/Azure strict JSON schema compatibility
raw_outputs = content.get("outputs", [])
if isinstance(raw_outputs, list):
outputs = {
item.get("name", "result"): {"type": item.get("type", parameter_type)}
for item in raw_outputs
if isinstance(item, dict) and item.get("name")
}
if not outputs:
outputs = {"result": {"type": parameter_type}}
else:
outputs = raw_outputs or {"result": {"type": parameter_type}}
return {
"variables": variables,

View File

@ -3,32 +3,65 @@ from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from core.variables.types import SegmentType
from core.workflow.nodes.base.entities import VariableSelector
class SuggestedQuestionsOutput(BaseModel):
"""Output model for suggested questions generation."""
model_config = ConfigDict(extra="forbid")
questions: list[str] = Field(min_length=3, max_length=3)
questions: list[str] = Field(
min_length=3,
max_length=3,
description="Exactly 3 suggested follow-up questions for the user",
)
class CodeNodeOutput(BaseModel):
class VariableSelectorOutput(BaseModel):
"""Variable selector mapping code variable to upstream node output.
Note: Separate from VariableSelector to ensure 'additionalProperties: false'
in JSON schema for OpenAI/Azure strict mode.
"""
model_config = ConfigDict(extra="forbid")
type: SegmentType
variable: str = Field(description="Variable name used in the generated code")
value_selector: list[str] = Field(description="Path to upstream node output, format: [node_id, output_name]")
class CodeNodeOutputItem(BaseModel):
"""Single output variable definition.
Note: OpenAI/Azure strict mode requires 'additionalProperties: false' and
does not support dynamic object keys, so outputs use array format.
"""
model_config = ConfigDict(extra="forbid")
name: str = Field(description="Output variable name returned by the main function")
type: SegmentType = Field(description="Data type of the output variable")
class CodeNodeStructuredOutput(BaseModel):
"""Structured output for code node generation."""
model_config = ConfigDict(extra="forbid")
variables: list[VariableSelector]
code: str
outputs: dict[str, CodeNodeOutput]
explanation: str
variables: list[VariableSelectorOutput] = Field(
description="Input variables mapping code variables to upstream node outputs"
)
code: str = Field(description="Generated code with a main function that processes inputs and returns outputs")
outputs: list[CodeNodeOutputItem] = Field(
description="Output variable definitions specifying name and type for each return value"
)
explanation: str = Field(description="Brief explanation of what the generated code does")
class InstructionModifyOutput(BaseModel):
"""Output model for instruction-based prompt modification."""
model_config = ConfigDict(extra="forbid")
modified: str
message: str
modified: str = Field(description="The modified prompt content after applying the instruction")
message: str = Field(description="Brief explanation of what changes were made")

View File

@ -1058,10 +1058,10 @@ class ToolManager:
elif tool_input.type == "mixed":
segment_group = variable_pool.convert_template(str(tool_input.value))
parameter_value = segment_group.text
elif tool_input.type == "mention":
# Mention type not supported in agent mode
elif tool_input.type == "nested_node":
# Nested node type not supported in agent mode
raise ToolParameterError(
f"Mention type not supported in agent for parameter '{parameter.name}'"
f"Nested node type not supported in agent for parameter '{parameter.name}'"
)
else:
raise ToolParameterError(f"Unknown tool input type '{tool_input.type}'")

View File

@ -256,7 +256,7 @@ class WorkflowNodeExecutionMetadataKey(StrEnum):
LLM_CONTENT_SEQUENCE = "llm_content_sequence"
LLM_TRACE = "llm_trace"
COMPLETED_REASON = "completed_reason" # completed reason for loop node
MENTION_PARENT_ID = "mention_parent_id" # parent node id for extractor nodes
PARENT_NODE_ID = "parent_node_id" # parent node id for nested nodes (extractor nodes)
class WorkflowNodeExecutionStatus(StrEnum):

View File

@ -94,7 +94,7 @@ class EventHandler:
event: The event to handle
"""
# Events in loops, iterations, or extractor groups are always collected
if event.in_loop_id or event.in_iteration_id or event.in_mention_parent_id:
if event.in_loop_id or event.in_iteration_id or event.in_parent_node_id:
self._event_collector.collect(event)
return
return self._dispatch(event)

View File

@ -68,7 +68,7 @@ class _NodeRuntimeSnapshot:
predecessor_node_id: str | None
iteration_id: str | None
loop_id: str | None
mention_parent_id: str | None
parent_node_id: str | None
created_at: datetime
@ -231,7 +231,7 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
metadata = {
WorkflowNodeExecutionMetadataKey.ITERATION_ID: event.in_iteration_id,
WorkflowNodeExecutionMetadataKey.LOOP_ID: event.in_loop_id,
WorkflowNodeExecutionMetadataKey.MENTION_PARENT_ID: event.in_mention_parent_id,
WorkflowNodeExecutionMetadataKey.PARENT_NODE_ID: event.in_parent_node_id,
}
domain_execution = WorkflowNodeExecution(
@ -258,7 +258,7 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
predecessor_node_id=event.predecessor_node_id,
iteration_id=event.in_iteration_id,
loop_id=event.in_loop_id,
mention_parent_id=event.in_mention_parent_id,
parent_node_id=event.in_parent_node_id,
created_at=event.start_at,
)
self._node_snapshots[event.id] = snapshot

View File

@ -21,10 +21,10 @@ class GraphNodeEventBase(GraphEngineEvent):
"""iteration id if node is in iteration"""
in_loop_id: str | None = None
"""loop id if node is in loop"""
in_mention_parent_id: str | None = None
"""Parent node id if this is an extractor node event.
in_parent_node_id: str | None = None
"""Parent node id if this is a nested node event.
When set, indicates this event belongs to an extractor node that
When set, indicates this event belongs to a nested node that
is extracting values for the specified parent node.
"""

View File

@ -288,59 +288,45 @@ class Node(Generic[NodeDataT]):
extractor_configs.append(node_config)
return extractor_configs
def _execute_mention_nodes(self) -> Generator[GraphNodeEventBase, None, None]:
def _execute_nested_nodes(self) -> Generator[GraphNodeEventBase, None, None]:
"""
Execute all extractor nodes associated with this node.
Execute all nested nodes associated with this node.
Extractor nodes are nodes with parent_node_id == self._node_id.
Nested nodes are nodes with parent_node_id == self._node_id.
They are executed before the main node to extract values from list[PromptMessage].
"""
from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
from core.workflow.nodes.node_factory import DifyNodeFactory
extractor_configs = self._find_extractor_node_configs()
logger.debug("[Extractor] Found %d extractor nodes for parent '%s'", len(extractor_configs), self._node_id)
logger.debug("[NestedNode] Found %d nested nodes for parent '%s'", len(extractor_configs), self._node_id)
if not extractor_configs:
return
# Use DifyNodeFactory to properly instantiate nodes with required dependencies
node_factory = DifyNodeFactory(
graph_init_params=self._graph_init_params,
graph_runtime_state=self.graph_runtime_state,
)
for config in extractor_configs:
node_id = config.get("id")
node_data = config.get("data", {})
node_type_str = node_data.get("type")
if not node_id or not node_type_str:
if not node_id:
continue
# Get node class
try:
node_type = NodeType(node_type_str)
nested_node = node_factory.create_node(config)
except ValueError:
# Skip nodes that cannot be created (e.g., unknown type)
continue
node_mapping = NODE_TYPE_CLASSES_MAPPING.get(node_type)
if not node_mapping:
continue
node_version = str(node_data.get("version", "1"))
node_cls = node_mapping.get(node_version) or node_mapping.get(LATEST_VERSION)
if not node_cls:
continue
# Instantiate and execute the extractor node
extractor_node = node_cls(
id=node_id,
config=config,
graph_init_params=self._graph_init_params,
graph_runtime_state=self.graph_runtime_state,
)
# Execute and process extractor node events
for event in extractor_node.run():
# Execute and process nested node events
for event in nested_node.run():
# Tag event with parent node id for stream ordering and history tracking
if isinstance(event, GraphNodeEventBase):
event.in_mention_parent_id = self._node_id
event.in_parent_node_id = self._node_id
if isinstance(event, NodeRunSucceededEvent):
# Store extractor node outputs in variable pool
# Store nested node outputs in variable pool
outputs: Mapping[str, Any] = event.node_run_result.outputs
for variable_name, variable_value in outputs.items():
self.graph_runtime_state.variable_pool.add((node_id, variable_name), variable_value)
@ -351,8 +337,8 @@ class Node(Generic[NodeDataT]):
execution_id = self.ensure_execution_id()
self._start_at = naive_utc_now()
# Step 1: Execute associated extractor nodes before main node execution
yield from self._execute_mention_nodes()
# Step 1: Execute associated nested nodes before main node execution
yield from self._execute_nested_nodes()
# Create and push start event with required fields
start_event = NodeRunStartedEvent(

View File

@ -8,17 +8,17 @@ 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 mention value format: {{@node.context@}}instruction
# 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
MENTION_VALUE_PATTERN = re.compile(r"^\{\{@([a-zA-Z0-9_]+)\.context@\}\}(.*)$", re.DOTALL)
NESTED_NODE_VALUE_PATTERN = re.compile(r"^\{\{@([a-zA-Z0-9_]+)\.context@\}\}(.*)$", re.DOTALL)
def parse_mention_value(value: str) -> tuple[str, str]:
"""Parse mention value into (node_id, instruction).
def parse_nested_node_value(value: str) -> tuple[str, str]:
"""Parse nested_node value into (node_id, instruction).
Args:
value: The mention value string like "{{@llm.context@}}extract keywords"
value: The nested_node value string like "{{@llm.context@}}extract keywords"
Returns:
Tuple of (node_id, instruction)
@ -26,16 +26,16 @@ def parse_mention_value(value: str) -> tuple[str, str]:
Raises:
ValueError: If value format is invalid
"""
match = MENTION_VALUE_PATTERN.match(value)
match = NESTED_NODE_VALUE_PATTERN.match(value)
if not match:
raise ValueError(
"For mention type, value must start with {{@node.context@}} placeholder, "
"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 MentionConfig(BaseModel):
class NestedNodeConfig(BaseModel):
"""Configuration for extracting value from context variable.
Used when a tool parameter needs to be extracted from list[PromptMessage]
@ -87,9 +87,9 @@ class ToolNodeData(BaseNodeData, ToolEntity):
class ToolInput(BaseModel):
# TODO: check this type
value: Union[Any, list[str]]
type: Literal["mixed", "variable", "constant", "mention"]
# Required config for mention type, extracting value from context variable
mention_config: MentionConfig | None = None
type: Literal["mixed", "variable", "constant", "nested_node"]
# Required config for nested_node type, extracting value from context variable
nested_node_config: NestedNodeConfig | None = None
@field_validator("type", mode="before")
@classmethod
@ -102,7 +102,7 @@ class ToolNodeData(BaseNodeData, ToolEntity):
if typ == "mixed" and not isinstance(value, str):
raise ValueError("value must be a string")
elif typ == "mention":
elif typ == "nested_node":
# Skip here, will be validated in model_validator
pass
elif typ == "variable":
@ -116,9 +116,9 @@ class ToolNodeData(BaseNodeData, ToolEntity):
return typ
@model_validator(mode="after")
def check_mention_type(self) -> Self:
"""Validate mention type with mention_config."""
if self.type != "mention":
def check_nested_node_type(self) -> Self:
"""Validate nested_node type with nested_node_config."""
if self.type != "nested_node":
return self
value = self.value
@ -126,13 +126,13 @@ class ToolNodeData(BaseNodeData, ToolEntity):
return self
if not isinstance(value, str):
raise ValueError("value must be a string for mention type")
# For mention type, value must match format: {{@node.context@}}instruction
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_mention_value(value)
# mention_config is required for mention type
if self.mention_config is None:
raise ValueError("mention_config is required for mention type")
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")
return self
tool_parameters: dict[str, ToolInput]

View File

@ -212,16 +212,16 @@ class ToolNode(Node[ToolNodeData]):
raise ToolParameterError(f"Variable {selector} does not exist")
continue
parameter_value = variable.value
elif tool_input.type == "mention":
# Mention type: get value from extractor node's output
if tool_input.mention_config is None:
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"mention_config is required for mention type parameter '{parameter_name}'"
f"nested_node_config is required for nested_node type parameter '{parameter_name}'"
)
mention_config = tool_input.mention_config.model_dump()
nested_node_config = tool_input.nested_node_config.model_dump()
try:
parameter_value, found = variable_pool.resolve_mention(
mention_config, parameter_name=parameter_name
parameter_value, found = variable_pool.resolve_nested_node(
nested_node_config, parameter_name=parameter_name
)
if not found and parameter.required:
raise ToolParameterError(
@ -518,8 +518,8 @@ class ToolNode(Node[ToolNodeData]):
if isinstance(input.value, list):
selector_key = ".".join(input.value)
result[f"#{selector_key}#"] = input.value
elif input.type == "mention":
# Mention type: value is handled by extractor node, no direct variable reference
elif input.type == "nested_node":
# Nested node type: value is handled by extractor node, no direct variable reference
pass
elif input.type == "constant":
pass

View File

@ -79,8 +79,7 @@ class ReadOnlyGraphRuntimeState(Protocol):
...
@property
def sandbox(self) -> Any:
...
def sandbox(self) -> Any: ...
def dumps(self) -> str:
"""Serialize the runtime state into a JSON snapshot (read-only)."""

View File

@ -268,21 +268,21 @@ class VariablePool(BaseModel):
continue
self.add(selector, value)
def resolve_mention(
def resolve_nested_node(
self,
mention_config: Mapping[str, Any],
nested_node_config: Mapping[str, Any],
/,
*,
parameter_name: str = "",
) -> tuple[Any, bool]:
"""
Resolve a mention parameter value from an extractor node's output.
Resolve a nested_node parameter value from an extractor node's output.
Mention parameters reference values extracted by an extractor LLM node
Nested node parameters reference values extracted by an extractor LLM node
from list[PromptMessage] context.
Args:
mention_config: A dict containing:
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"
@ -298,13 +298,13 @@ class VariablePool(BaseModel):
ValueError: If extractor_node_id is missing, or if null_strategy is
"raise_error" and the value is not found
"""
extractor_node_id = mention_config.get("extractor_node_id")
extractor_node_id = nested_node_config.get("extractor_node_id")
if not extractor_node_id:
raise ValueError(f"Missing extractor_node_id for mention parameter '{parameter_name}'")
raise ValueError(f"Missing extractor_node_id for nested_node parameter '{parameter_name}'")
output_selector = list(mention_config.get("output_selector", []))
null_strategy = mention_config.get("null_strategy", "raise_error")
default_value = mention_config.get("default_value")
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]
full_selector = [extractor_node_id] + output_selector

View File

@ -165,27 +165,27 @@ class WorkflowScheduleCFSPlanEntity(BaseModel):
granularity: int = Field(default=-1) # -1 means infinite
# ========== Mention Graph Entities ==========
# ========== Nested Node Graph Entities ==========
class MentionParameterSchema(BaseModel):
"""Schema for the parameter to be extracted from mention context."""
class NestedNodeParameterSchema(BaseModel):
"""Schema for the parameter to be extracted from nested node context."""
name: str = Field(description="Parameter name (e.g., 'query')")
type: str = Field(default="string", description="Parameter type (e.g., 'string', 'number')")
description: str = Field(default="", description="Parameter description for LLM")
class MentionGraphRequest(BaseModel):
"""Request payload for generating mention graph."""
class NestedNodeGraphRequest(BaseModel):
"""Request payload for generating nested node graph."""
parent_node_id: str = Field(description="ID of the parent node that uses the extracted value")
parameter_key: str = Field(description="Key of the parameter being extracted")
context_source: list[str] = Field(description="Variable selector for the context source")
parameter_schema: MentionParameterSchema = Field(description="Schema of the parameter to extract")
parameter_schema: NestedNodeParameterSchema = Field(description="Schema of the parameter to extract")
class MentionGraphResponse(BaseModel):
"""Response containing the generated mention graph."""
class NestedNodeGraphResponse(BaseModel):
"""Response containing the generated nested node graph."""
graph: Mapping[str, Any] = Field(description="Complete graph structure with nodes, edges, viewport")

View File

@ -1,5 +1,5 @@
"""
Service for generating Mention LLM node graph structures.
Service for generating Nested Node LLM graph structures.
This service creates graph structures containing LLM nodes configured for
extracting values from list[PromptMessage] variables.
@ -12,35 +12,35 @@ from sqlalchemy.orm import Session
from core.model_runtime.entities import LLMMode
from core.workflow.enums import NodeType
from services.model_provider_service import ModelProviderService
from services.workflow.entities import MentionGraphRequest, MentionGraphResponse, MentionParameterSchema
from services.workflow.entities import NestedNodeGraphRequest, NestedNodeGraphResponse, NestedNodeParameterSchema
class MentionGraphService:
"""Service for generating Mention LLM node graph structures."""
class NestedNodeGraphService:
"""Service for generating Nested Node LLM graph structures."""
def __init__(self, session: Session):
self._session = session
def generate_mention_node_id(self, node_id: str, parameter_name: str) -> str:
"""Generate mention node ID following the naming convention.
def generate_nested_node_id(self, node_id: str, parameter_name: str) -> str:
"""Generate nested node ID following the naming convention.
Format: {node_id}_ext_{parameter_name}
"""
return f"{node_id}_ext_{parameter_name}"
def generate_mention_graph(self, tenant_id: str, request: MentionGraphRequest) -> MentionGraphResponse:
"""Generate a complete graph structure containing a Mention LLM node.
def generate_nested_node_graph(self, tenant_id: str, request: NestedNodeGraphRequest) -> NestedNodeGraphResponse:
"""Generate a complete graph structure containing a Nested Node LLM node.
Args:
tenant_id: The tenant ID for fetching default model config
request: The mention graph generation request
request: The nested node graph generation request
Returns:
Complete graph structure with nodes, edges, and viewport
"""
node_id = self.generate_mention_node_id(request.parent_node_id, request.parameter_key)
node_id = self.generate_nested_node_id(request.parent_node_id, request.parameter_key)
model_config = self._get_default_model_config(tenant_id)
node = self._build_mention_llm_node(
node = self._build_nested_node_llm_node(
node_id=node_id,
parent_node_id=request.parent_node_id,
context_source=request.context_source,
@ -54,7 +54,7 @@ class MentionGraphService:
"viewport": {},
}
return MentionGraphResponse(graph=graph)
return NestedNodeGraphResponse(graph=graph)
def _get_default_model_config(self, tenant_id: str) -> dict[str, Any]:
"""Get the default LLM model configuration for the tenant."""
@ -80,16 +80,16 @@ class MentionGraphService:
"completion_params": {},
}
def _build_mention_llm_node(
def _build_nested_node_llm_node(
self,
*,
node_id: str,
parent_node_id: str,
context_source: list[str],
parameter_schema: MentionParameterSchema,
parameter_schema: NestedNodeParameterSchema,
model_config: dict[str, Any],
) -> dict[str, Any]:
"""Build the Mention LLM node structure.
"""Build the Nested Node LLM node structure.
The node uses:
- $context in prompt_template to reference the PromptMessage list
@ -124,7 +124,7 @@ class MentionGraphService:
"position": {"x": 0, "y": 0},
"data": {
"type": NodeType.LLM.value,
"title": f"Mention: {parameter_schema.name}",
"title": f"NestedNode: {parameter_schema.name}",
"desc": f"Extract {parameter_schema.name} from conversation context",
"parent_node_id": parent_node_id,
"model": model_config,

View File

@ -207,9 +207,9 @@ workflow:
tool_node_version: '2'
tool_parameters:
query:
type: mention
type: nested_node
value: '{{@llm.context@}}请从对话历史中提取用户想要搜索的关键词,只返回关键词本身'
mention_config:
nested_node_config:
extractor_node_id: 1767773709491_ext_query
output_selector:
- structured_output

View File

@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import type { Item } from '@/app/components/base/select'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
import { RiCheckLine } from '@remixicon/react'
import { memo, useCallback, useMemo, useState } from 'react'
@ -17,43 +17,43 @@ import { cn } from '@/utils/classnames'
type ConfigPanelProps = {
agentName: string
extractorNodeId: string
mentionConfig: MentionConfig
nestedNodeConfig: NestedNodeConfig
availableNodes: Node[]
availableVars: NodeOutPutVar[]
onMentionConfigChange: (config: MentionConfig) => void
onNestedNodeConfigChange: (config: NestedNodeConfig) => void
}
const ConfigPanel: FC<ConfigPanelProps> = ({
agentName,
extractorNodeId,
mentionConfig,
nestedNodeConfig,
availableNodes,
availableVars,
onMentionConfigChange,
onNestedNodeConfigChange,
}) => {
const { t } = useTranslation()
const [tabType, setTabType] = useState<TabType>(TabType.settings)
const resolvedExtractorId = mentionConfig.extractor_node_id || extractorNodeId
const resolvedExtractorId = nestedNodeConfig.extractor_node_id || extractorNodeId
const selectedOutput = useMemo<ValueSelector>(() => {
if (!resolvedExtractorId || !mentionConfig.output_selector?.length)
if (!resolvedExtractorId || !nestedNodeConfig.output_selector?.length)
return []
return [resolvedExtractorId, ...(mentionConfig.output_selector || [])]
}, [mentionConfig.output_selector, resolvedExtractorId])
return [resolvedExtractorId, ...(nestedNodeConfig.output_selector || [])]
}, [nestedNodeConfig.output_selector, resolvedExtractorId])
const handleOutputVarChange = useCallback((value: ValueSelector | string) => {
const selector = Array.isArray(value) ? value : []
const nextExtractorId = selector[0] || resolvedExtractorId
const nextOutputSelector = selector.length > 1 ? selector.slice(1) : []
onMentionConfigChange({
...mentionConfig,
onNestedNodeConfigChange({
...nestedNodeConfig,
extractor_node_id: nextExtractorId,
output_selector: nextOutputSelector,
})
}, [mentionConfig, onMentionConfigChange, resolvedExtractorId])
}, [nestedNodeConfig, onNestedNodeConfigChange, resolvedExtractorId])
const whenOutputNoneOptions = useMemo(() => ([
{
@ -68,17 +68,17 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
},
]), [t])
const selectedWhenOutputNoneOption = useMemo(() => (
whenOutputNoneOptions.find(item => item.value === mentionConfig.null_strategy) ?? whenOutputNoneOptions[0]
), [mentionConfig.null_strategy, whenOutputNoneOptions])
whenOutputNoneOptions.find(item => item.value === nestedNodeConfig.null_strategy) ?? whenOutputNoneOptions[0]
), [nestedNodeConfig.null_strategy, whenOutputNoneOptions])
const handleNullStrategyChange = useCallback((item: Item) => {
if (typeof item.value !== 'string')
return
onMentionConfigChange({
...mentionConfig,
null_strategy: item.value as MentionConfig['null_strategy'],
onNestedNodeConfigChange({
...nestedNodeConfig,
null_strategy: item.value as NestedNodeConfig['null_strategy'],
})
}, [mentionConfig, onMentionConfigChange])
}, [nestedNodeConfig, onNestedNodeConfigChange])
const handleDefaultValueChange = useCallback((value: string) => {
const trimmed = value.trim()
@ -92,12 +92,12 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
}
}
onMentionConfigChange({
...mentionConfig,
onNestedNodeConfigChange({
...nestedNodeConfig,
default_value: nextValue,
})
}, [mentionConfig, onMentionConfigChange])
const defaultValue = mentionConfig.default_value ?? ''
}, [nestedNodeConfig, onNestedNodeConfigChange])
const defaultValue = nestedNodeConfig.default_value ?? ''
const shouldFormatDefaultValue = typeof defaultValue !== 'string'
return (
@ -142,7 +142,7 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
<div className="flex items-center">
<SimpleSelect
items={whenOutputNoneOptions}
defaultValue={mentionConfig.null_strategy}
defaultValue={nestedNodeConfig.null_strategy}
allowSearch={false}
notClearable
wrapperClassName="min-w-[160px]"
@ -170,7 +170,7 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
{selectedWhenOutputNoneOption.description}
</div>
)}
{mentionConfig.null_strategy === 'use_default' && (
{nestedNodeConfig.null_strategy === 'use_default' && (
<div className={cn('overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-normal p-1')}>
<CodeEditor
noWrapper

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { memo, useMemo } from 'react'
import { useStore as useReactFlowStore } from 'reactflow'
@ -14,8 +14,8 @@ type SubGraphChildrenProps
variant: 'agent'
title: string
extractorNodeId: string
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
nestedNodeConfig: NestedNodeConfig
onNestedNodeConfigChange: (config: NestedNodeConfig) => void
}
| {
variant: 'assemble'
@ -72,10 +72,10 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = (props) => {
<ConfigPanel
agentName={title}
extractorNodeId={extractorNodeId}
mentionConfig={agentProps.mentionConfig}
nestedNodeConfig={agentProps.nestedNodeConfig}
availableNodes={availableNodes}
availableVars={availableVars}
onMentionConfigChange={agentProps.onMentionConfigChange}
onNestedNodeConfigChange={agentProps.onNestedNodeConfigChange}
/>
</div>
</div>

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import type { Viewport } from 'reactflow'
import type { SyncWorkflowDraft, SyncWorkflowDraftCallback } from '../types'
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { Edge, Node } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
@ -29,8 +29,8 @@ type SubGraphMainBaseProps = {
type SubGraphMainProps
= | (SubGraphMainBaseProps & {
variant: 'agent'
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
nestedNodeConfig: NestedNodeConfig
onNestedNodeConfigChange: (config: NestedNodeConfig) => void
})
| (SubGraphMainBaseProps & {
variant: 'assemble'
@ -110,8 +110,8 @@ const SubGraphMain: FC<SubGraphMainProps> = (props) => {
variant="agent"
title={title}
extractorNodeId={extractorNodeId}
mentionConfig={props.mentionConfig}
onMentionConfigChange={props.onMentionConfigChange}
nestedNodeConfig={props.nestedNodeConfig}
onNestedNodeConfigChange={props.onNestedNodeConfigChange}
/>
)
: (

View File

@ -230,8 +230,8 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
title={sourceTitle}
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
configsMap={configsMap}
mentionConfig={props.mentionConfig}
onMentionConfigChange={props.onMentionConfigChange}
nestedNodeConfig={props.nestedNodeConfig}
onNestedNodeConfigChange={props.onNestedNodeConfigChange}
selectableNodeTypes={selectableNodeTypes}
onSave={onSave}
onSyncWorkflowDraft={onSyncWorkflowDraft}

View File

@ -1,6 +1,6 @@
import type { StateCreator } from 'zustand'
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { BlockEnum, Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
@ -35,8 +35,8 @@ export type AgentSubGraphProps = BaseSubGraphProps & {
sourceVariable: ValueSelector
agentNodeId: string
agentName: string
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
nestedNodeConfig: NestedNodeConfig
onNestedNodeConfigChange: (config: NestedNodeConfig) => void
extractorNode?: Node<LLMNodeType>
}

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { MentionConfig, ResourceVarInputs } from '../types'
import type { NestedNodeConfig, ResourceVarInputs } from '../types'
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Event, Tool } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
@ -319,7 +319,7 @@ const FormInputItem: FC<Props> = ({
}
}
const handleValueChange = (newValue: any, newType?: VarKindType, mentionConfig?: MentionConfig | null) => {
const handleValueChange = (newValue: any, newType?: VarKindType, nestedNodeConfig?: NestedNodeConfig | null) => {
const normalizedValue = isNumber ? Number.parseFloat(newValue) : newValue
const assemblePlaceholder = nodeId && variable
? `{{#${nodeId}_ext_${variable}.result#}}`
@ -329,9 +329,9 @@ const FormInputItem: FC<Props> = ({
&& normalizedValue.includes(assemblePlaceholder)
const resolvedType = isAssembleValue
? VarKindType.mixed
: newType ?? (varInput?.type === VarKindType.mention ? VarKindType.mention : getVarKindType())
const resolvedMentionConfig = resolvedType === VarKindType.mention
? (mentionConfig ?? varInput?.mention_config ?? {
: newType ?? (varInput?.type === VarKindType.nested_node ? VarKindType.nested_node : getVarKindType())
const resolvedNestedNodeConfig = resolvedType === VarKindType.nested_node
? (nestedNodeConfig ?? varInput?.nested_node_config ?? {
extractor_node_id: '',
output_selector: [],
null_strategy: 'use_default',
@ -345,7 +345,7 @@ const FormInputItem: FC<Props> = ({
...varInput,
type: resolvedType,
value: normalizedValue,
mention_config: resolvedMentionConfig,
nested_node_config: resolvedNestedNodeConfig,
},
})
}

View File

@ -5,10 +5,10 @@ export enum VarKindType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
mention = 'mention',
nested_node = 'nested_node',
}
export type MentionConfig = {
export type NestedNodeConfig = {
extractor_node_id: string
output_selector: ValueSelector
null_strategy: 'raise_error' | 'use_default'
@ -19,7 +19,7 @@ export type MentionConfig = {
export type ResourceVarInputs = Record<string, {
type: VarKindType
value?: string | ValueSelector | any
mention_config?: MentionConfig
nested_node_config?: NestedNodeConfig
}>
// Base resource interface

View File

@ -21,8 +21,8 @@ export type ContextGenerateChatMessage = ContextGenerateMessage & {
const defaultCompletionParams: CompletionParams = {
temperature: 0.7,
max_tokens: 0,
top_p: 0,
max_tokens: 4096,
top_p: 0.1,
echo: false,
stop: [],
presence_penalty: 0,

View File

@ -12,7 +12,7 @@ import { useCallback, useMemo } from 'react'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
import { generateNewNode, getNodeCustomTypeByNodeDataType, mergeNodeDefaultData } from '@/app/components/workflow/utils'
import { fetchMentionGraph } from '@/service/workflow'
import { fetchNestedNodeGraph } from '@/service/workflow'
import { FlowType } from '@/types/common'
// Constants
@ -160,7 +160,7 @@ export function useMixedVariableExtractor({
return `${toolNodeId}_ext_${paramKey}`
}, [paramKey, toolNodeId])
const resolveMentionParameterSchema = useCallback((key: string) => {
const resolveNestedNodeParameterSchema = useCallback((key: string) => {
if (!toolNodeId) {
return {
name: key,
@ -337,37 +337,37 @@ export function useMixedVariableExtractor({
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, paramKey, reactFlowStore, toolNodeId])
const applyMentionGraphNodeData = useCallback((payload: {
const applyNestedNodeGraphData = useCallback((payload: {
extractorNodeId: string
mentionNodeData: Partial<LLMNodeType>
nestedNodeData: Partial<LLMNodeType>
valueText: string
detectAgentFromText: (text: string) => DetectedAgent | null
}) => {
const { extractorNodeId, mentionNodeData, valueText, detectAgentFromText } = payload
const { extractorNodeId, nestedNodeData, valueText, detectAgentFromText } = payload
if (!toolNodeId)
return
const hasPromptTemplate = Array.isArray(mentionNodeData.prompt_template)
? mentionNodeData.prompt_template.length > 0
: Boolean(mentionNodeData.prompt_template)
const hasPromptTemplate = Array.isArray(nestedNodeData.prompt_template)
? nestedNodeData.prompt_template.length > 0
: Boolean(nestedNodeData.prompt_template)
const nextData: Partial<LLMNodeType> = {}
if (mentionNodeData.title)
nextData.title = mentionNodeData.title
if (mentionNodeData.desc)
nextData.desc = mentionNodeData.desc
if (mentionNodeData.model && (mentionNodeData.model.provider || mentionNodeData.model.name))
nextData.model = mentionNodeData.model
if (nestedNodeData.title)
nextData.title = nestedNodeData.title
if (nestedNodeData.desc)
nextData.desc = nestedNodeData.desc
if (nestedNodeData.model && (nestedNodeData.model.provider || nestedNodeData.model.name))
nextData.model = nestedNodeData.model
if (hasPromptTemplate)
nextData.prompt_template = mentionNodeData.prompt_template
if (typeof mentionNodeData.structured_output_enabled === 'boolean')
nextData.structured_output_enabled = mentionNodeData.structured_output_enabled
if (mentionNodeData.structured_output?.schema)
nextData.structured_output = mentionNodeData.structured_output
if (mentionNodeData.context)
nextData.context = mentionNodeData.context
if (mentionNodeData.vision)
nextData.vision = mentionNodeData.vision
if (Object.prototype.hasOwnProperty.call(mentionNodeData, 'memory'))
nextData.memory = mentionNodeData.memory
nextData.prompt_template = nestedNodeData.prompt_template
if (typeof nestedNodeData.structured_output_enabled === 'boolean')
nextData.structured_output_enabled = nestedNodeData.structured_output_enabled
if (nestedNodeData.structured_output?.schema)
nextData.structured_output = nestedNodeData.structured_output
if (nestedNodeData.context)
nextData.context = nestedNodeData.context
if (nestedNodeData.vision)
nextData.vision = nestedNodeData.vision
if (Object.prototype.hasOwnProperty.call(nestedNodeData, 'memory'))
nextData.memory = nestedNodeData.memory
if (Object.keys(nextData).length === 0)
return
@ -396,7 +396,7 @@ export function useMixedVariableExtractor({
syncExtractorPromptFromText(valueText, detectAgentFromText)
}, [handleSyncWorkflowDraft, reactFlowStore, syncExtractorPromptFromText, toolNodeId])
const requestMentionGraph = useCallback(async (payload: {
const requestNestedNodeGraph = useCallback(async (payload: {
agentId: string
extractorNodeId: string
valueText: string
@ -406,28 +406,28 @@ export function useMixedVariableExtractor({
return
if (!configsMap?.flowId || configsMap.flowType !== FlowType.appFlow)
return
const parameterSchema = resolveMentionParameterSchema(paramKey)
const parameterSchema = resolveNestedNodeParameterSchema(paramKey)
try {
const response = await fetchMentionGraph(configsMap.flowType, configsMap.flowId, {
const response = await fetchNestedNodeGraph(configsMap.flowType, configsMap.flowId, {
parent_node_id: toolNodeId,
parameter_key: paramKey,
context_source: [payload.agentId, 'context'],
parameter_schema: parameterSchema,
})
const mentionNode = response?.graph?.nodes?.find(node => node.id === payload.extractorNodeId)
const mentionNodeData = mentionNode?.data as Partial<LLMNodeType> | undefined
if (!mentionNodeData)
const nestedNode = response?.graph?.nodes?.find(node => node.id === payload.extractorNodeId)
const nestedNodeData = nestedNode?.data as Partial<LLMNodeType> | undefined
if (!nestedNodeData)
return
applyMentionGraphNodeData({
applyNestedNodeGraphData({
extractorNodeId: payload.extractorNodeId,
mentionNodeData,
nestedNodeData,
valueText: payload.valueText,
detectAgentFromText: payload.detectAgentFromText,
})
}
catch {
}
}, [applyMentionGraphNodeData, configsMap?.flowId, configsMap?.flowType, paramKey, resolveMentionParameterSchema, toolNodeId])
}, [applyNestedNodeGraphData, configsMap?.flowId, configsMap?.flowType, paramKey, resolveNestedNodeParameterSchema, toolNodeId])
return {
assembleExtractorNodeId,
@ -435,6 +435,6 @@ export function useMixedVariableExtractor({
ensureAssembleExtractorNode,
removeExtractorNode,
syncExtractorPromptFromText,
requestMentionGraph,
requestNestedNodeGraph,
}
}

View File

@ -2,7 +2,7 @@ import type { ContextGenerateModalHandle } from '../context-generate-modal'
import type { DetectedAgent } from './hooks'
import type { AgentNode, WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types'
import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types'
import type {
CommonNodeType,
@ -41,7 +41,7 @@ import {
type WorkflowNodesMap = NonNullable<WorkflowVariableBlockType['workflowNodesMap']>
const DEFAULT_MENTION_CONFIG: MentionConfig = {
const DEFAULT_NESTED_NODE_CONFIG: NestedNodeConfig = {
extractor_node_id: '',
output_selector: [],
null_strategy: 'use_default',
@ -60,7 +60,7 @@ type MixedVariableTextInputProps = {
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: WorkflowNode[]
value?: string
onChange?: (text: string, type?: VarKindType, mentionConfig?: MentionConfig | null) => void
onChange?: (text: string, type?: VarKindType, nestedNodeConfig?: NestedNodeConfig | null) => void
showManageInputField?: boolean
onManageInputField?: () => void
disableVariableInsertion?: boolean
@ -134,7 +134,7 @@ const MixedVariableTextInput = ({
ensureAssembleExtractorNode,
removeExtractorNode,
syncExtractorPromptFromText,
requestMentionGraph,
requestNestedNodeGraph,
} = useMixedVariableExtractor({
toolNodeId,
paramKey,
@ -297,22 +297,22 @@ const MixedVariableTextInput = ({
})
}
const mentionConfigWithOutputSelector: MentionConfig = {
...DEFAULT_MENTION_CONFIG,
const nestedNodeConfigWithOutputSelector: NestedNodeConfig = {
...DEFAULT_NESTED_NODE_CONFIG,
extractor_node_id: extractorNodeId,
output_selector: paramKey ? ['structured_output', paramKey] : [],
}
onChange(newValue, VarKindTypeEnum.mention, mentionConfigWithOutputSelector)
onChange(newValue, VarKindTypeEnum.nested_node, nestedNodeConfigWithOutputSelector)
syncExtractorPromptFromText(newValue, detectAgentFromText)
if (extractorNodeId) {
void requestMentionGraph({
void requestNestedNodeGraph({
agentId: agent.id,
extractorNodeId,
valueText: newValue,
detectAgentFromText,
})
}
}, [detectAgentFromText, ensureExtractorNode, onChange, paramKey, requestMentionGraph, syncExtractorPromptFromText, toolNodeId, value])
}, [detectAgentFromText, ensureExtractorNode, onChange, paramKey, requestNestedNodeGraph, syncExtractorPromptFromText, toolNodeId, value])
const handleAssembleSelect = useCallback((): ValueSelector | null => {
if (!toolNodeId || !paramKey || !assemblePlaceholder)

View File

@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import type { SubGraphModalProps } from './types'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
@ -88,8 +88,8 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
return vars.filter(nodeVar => availableNodeIds.has(nodeVar.nodeId))
}, [getNodeAvailableVars, isChatMode, parentAvailableNodes])
const mentionConfig = useMemo<MentionConfig>(() => {
const current = toolParam?.mention_config
const nestedNodeConfig = useMemo<NestedNodeConfig>(() => {
const current = toolParam?.nested_node_config
const rawSelector = Array.isArray(current?.output_selector) ? current!.output_selector : []
const outputSelector = rawSelector[0] === extractorNodeId ? rawSelector.slice(1) : rawSelector
const defaultOutputSelector = ['structured_output', paramKey]
@ -100,9 +100,9 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
null_strategy: current?.null_strategy || 'use_default',
default_value: current?.default_value ?? '',
}
}, [extractorNodeId, paramKey, toolParam?.mention_config])
}, [extractorNodeId, paramKey, toolParam?.nested_node_config])
const handleMentionConfigChange = useCallback((config: MentionConfig) => {
const handleNestedNodeConfigChange = useCallback((config: NestedNodeConfig) => {
if (!isAgentVariant)
return
@ -124,8 +124,8 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
...toolData.tool_parameters,
[paramKey]: {
...currentParam,
type: currentParam.type || VarKindType.mention,
mention_config: config,
type: currentParam.type || VarKindType.nested_node,
nested_node_config: config,
},
},
},
@ -136,18 +136,18 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
}, [handleSyncWorkflowDraft, isAgentVariant, paramKey, reactflowStore, toolNodeId])
useEffect(() => {
if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.mention))
if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.nested_node))
return
const current = toolParam.mention_config
const current = toolParam.nested_node_config
const needsExtractor = !current?.extractor_node_id
const needsNullStrategy = !current?.null_strategy
const needsOutputSelector = !Array.isArray(current?.output_selector)
const needsDefaultValue = current?.default_value === undefined
if (needsExtractor || needsNullStrategy || needsOutputSelector || needsDefaultValue)
handleMentionConfigChange(mentionConfig)
}, [handleMentionConfigChange, isAgentVariant, mentionConfig, toolParam])
handleNestedNodeConfigChange(nestedNodeConfig)
}, [handleNestedNodeConfigChange, isAgentVariant, nestedNodeConfig, toolParam])
const getUserPromptText = useCallback((promptTemplate?: PromptTemplateItem[] | PromptItem) => {
if (!promptTemplate)
@ -281,8 +281,8 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
agentNodeId={props.agentNodeId}
agentName={props.agentName}
configsMap={configsMap}
mentionConfig={mentionConfig}
onMentionConfigChange={handleMentionConfigChange}
nestedNodeConfig={nestedNodeConfig}
onNestedNodeConfigChange={handleNestedNodeConfigChange}
extractorNode={extractorNode as Node<LLMNodeType> | undefined}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}

View File

@ -77,7 +77,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
}, {} as Record<string, WorkflowNode>)
}, [nodes])
const mentionEntries = useMemo(() => {
const nestedNodeEntries = useMemo(() => {
const entries: Array<{ agentNodeId: string, extractorNodeId?: string, paramKey: string }> = []
const seen = new Set<string>()
const toolParams = data.tool_parameters || {}
@ -97,8 +97,8 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
entries.push({
agentNodeId,
paramKey,
extractorNodeId: param?.mention_config?.extractor_node_id
|| (param?.type === VarType.mention ? `${id}_ext_${paramKey}` : undefined),
extractorNodeId: param?.nested_node_config?.extractor_node_id
|| (param?.type === VarType.nested_node ? `${id}_ext_${paramKey}` : undefined),
})
}
})
@ -106,7 +106,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
}, [data.tool_parameters, id])
const referenceItems = useMemo(() => {
if (!mentionEntries.length)
if (!nestedNodeEntries.length)
return []
const getNodeWarning = (node?: WorkflowNode) => {
@ -132,7 +132,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
return Boolean(errorMessage)
}
return mentionEntries.map(({ agentNodeId, extractorNodeId, paramKey }) => {
return nestedNodeEntries.map(({ agentNodeId, extractorNodeId, paramKey }) => {
const agentNode = nodesById[agentNodeId]
const agentLabel = `@${agentNode?.data.title || agentNodeId}`
const agentWarning = getNodeWarning(agentNode)
@ -148,7 +148,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
hasWarning,
}
})
}, [mentionEntries, nodesById, nodesMetaDataMap, strategyProviders, language, t])
}, [nestedNodeEntries, nodesById, nodesMetaDataMap, strategyProviders, language, t])
const hasConfigs = toolConfigs.length > 0
const hasReferences = referenceItems.length > 0

View File

@ -32,7 +32,7 @@ const useSingleRunFormParams = ({
const { inputs } = useNodeCrud<ToolNodeType>(id, payload)
const hadVarParams = Object.keys(inputs.tool_parameters)
.filter(key => ![VarType.constant, VarType.mention].includes(inputs.tool_parameters[key].type))
.filter(key => ![VarType.constant, VarType.nested_node].includes(inputs.tool_parameters[key].type))
.map(k => inputs.tool_parameters[k])
const hadVarSettings = Object.keys(inputs.tool_configurations)

View File

@ -4,8 +4,8 @@ import type { FlowType } from '@/types/common'
import type {
ConversationVariableResponse,
FetchWorkflowDraftResponse,
MentionGraphPayload,
MentionGraphResponse,
NestedNodeGraphPayload,
NestedNodeGraphResponse,
NodesDefaultConfigsResponse,
VarInInspect,
} from '@/types/workflow'
@ -34,8 +34,8 @@ export const fetchNodesDefaultConfigs = (url: string) => {
return get<NodesDefaultConfigsResponse>(url)
}
export const fetchMentionGraph = (flowType: FlowType, flowId: string, payload: MentionGraphPayload) => {
return post<MentionGraphResponse>(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/mention-graph`, { body: payload }, { silent: true })
export const fetchNestedNodeGraph = (flowType: FlowType, flowId: string, payload: NestedNodeGraphPayload) => {
return post<NestedNodeGraphResponse>(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nested-node-graph`, { body: payload }, { silent: true })
}
export const singleNodeRun = (flowType: FlowType, flowId: string, nodeId: string, params: object) => {

View File

@ -200,20 +200,20 @@ export type FetchWorkflowDraftResponse = {
marked_comment: string
}
export type MentionParameterSchema = {
export type NestedNodeParameterSchema = {
name: string
type: string
description?: string
}
export type MentionGraphPayload = {
export type NestedNodeGraphPayload = {
parent_node_id: string
parameter_key: string
context_source: ValueSelector
parameter_schema: MentionParameterSchema
parameter_schema: NestedNodeParameterSchema
}
export type MentionGraphResponse = {
export type NestedNodeGraphResponse = {
graph: {
nodes: Node[]
edges: Edge[]