diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index f3534b7e9a..085c5cec3f 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -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//workflows/draft/mention-graph") -class MentionGraphApi(Resource): +@console_ns.route("/apps//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() diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index 2ed4ca1d8c..00ed0f7270 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -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, ), ) diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 900d4dd657..423ac61c91 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -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): diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index e3a21bac56..d2c1d3a408 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -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 diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index 60af3256bd..a90f64cf16 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -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, }, } diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index d29332c3fa..aba069333a 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -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, diff --git a/api/core/llm_generator/output_models.py b/api/core/llm_generator/output_models.py index 22c4f5e411..56be4a6ed2 100644 --- a/api/core/llm_generator/output_models.py +++ b/api/core/llm_generator/output_models.py @@ -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") diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 4d29f419d1..9dc99df861 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -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}'") diff --git a/api/core/workflow/enums.py b/api/core/workflow/enums.py index 2d36bfb198..b7bbdc3e9d 100644 --- a/api/core/workflow/enums.py +++ b/api/core/workflow/enums.py @@ -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): diff --git a/api/core/workflow/graph_engine/event_management/event_handlers.py b/api/core/workflow/graph_engine/event_management/event_handlers.py index 9f2d8bcff4..ac02e2b2b1 100644 --- a/api/core/workflow/graph_engine/event_management/event_handlers.py +++ b/api/core/workflow/graph_engine/event_management/event_handlers.py @@ -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) diff --git a/api/core/workflow/graph_engine/layers/persistence.py b/api/core/workflow/graph_engine/layers/persistence.py index 6f7c76defe..509478b3ee 100644 --- a/api/core/workflow/graph_engine/layers/persistence.py +++ b/api/core/workflow/graph_engine/layers/persistence.py @@ -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 diff --git a/api/core/workflow/graph_events/base.py b/api/core/workflow/graph_events/base.py index 16dd49c7ad..c5807f7cc1 100644 --- a/api/core/workflow/graph_events/base.py +++ b/api/core/workflow/graph_events/base.py @@ -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. """ diff --git a/api/core/workflow/nodes/base/node.py b/api/core/workflow/nodes/base/node.py index d80f872486..34bd3fd047 100644 --- a/api/core/workflow/nodes/base/node.py +++ b/api/core/workflow/nodes/base/node.py @@ -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( diff --git a/api/core/workflow/nodes/tool/entities.py b/api/core/workflow/nodes/tool/entities.py index 30bca3b7f2..458bbdd9ed 100644 --- a/api/core/workflow/nodes/tool/entities.py +++ b/api/core/workflow/nodes/tool/entities.py @@ -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] diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 549851302a..915a46f2ab 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -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 diff --git a/api/core/workflow/runtime/graph_runtime_state_protocol.py b/api/core/workflow/runtime/graph_runtime_state_protocol.py index 3f3855d2bd..3361aa422b 100644 --- a/api/core/workflow/runtime/graph_runtime_state_protocol.py +++ b/api/core/workflow/runtime/graph_runtime_state_protocol.py @@ -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).""" diff --git a/api/core/workflow/runtime/variable_pool.py b/api/core/workflow/runtime/variable_pool.py index ff8c215a76..0e83bf770d 100644 --- a/api/core/workflow/runtime/variable_pool.py +++ b/api/core/workflow/runtime/variable_pool.py @@ -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 diff --git a/api/services/workflow/entities.py b/api/services/workflow/entities.py index cf5519527d..9cc5cfb53d 100644 --- a/api/services/workflow/entities.py +++ b/api/services/workflow/entities.py @@ -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") diff --git a/api/services/workflow/mention_graph_service.py b/api/services/workflow/nested_node_graph_service.py similarity index 78% rename from api/services/workflow/mention_graph_service.py rename to api/services/workflow/nested_node_graph_service.py index d0729c6d20..614a672a68 100644 --- a/api/services/workflow/mention_graph_service.py +++ b/api/services/workflow/nested_node_graph_service.py @@ -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, diff --git a/api/tests/fixtures/pav-test-extraction.yml b/api/tests/fixtures/pav-test-extraction.yml index 69fe73c493..1f1e5ddd8f 100644 --- a/api/tests/fixtures/pav-test-extraction.yml +++ b/api/tests/fixtures/pav-test-extraction.yml @@ -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 diff --git a/web/app/components/sub-graph/components/config-panel.tsx b/web/app/components/sub-graph/components/config-panel.tsx index edbd746550..c466de66ea 100644 --- a/web/app/components/sub-graph/components/config-panel.tsx +++ b/web/app/components/sub-graph/components/config-panel.tsx @@ -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 = ({ agentName, extractorNodeId, - mentionConfig, + nestedNodeConfig, availableNodes, availableVars, - onMentionConfigChange, + onNestedNodeConfigChange, }) => { const { t } = useTranslation() const [tabType, setTabType] = useState(TabType.settings) - const resolvedExtractorId = mentionConfig.extractor_node_id || extractorNodeId + const resolvedExtractorId = nestedNodeConfig.extractor_node_id || extractorNodeId const selectedOutput = useMemo(() => { - 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 = ({ }, ]), [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 = ({ } } - 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 = ({
= ({ {selectedWhenOutputNoneOption.description}
)} - {mentionConfig.null_strategy === 'use_default' && ( + {nestedNodeConfig.null_strategy === 'use_default' && (
void + nestedNodeConfig: NestedNodeConfig + onNestedNodeConfigChange: (config: NestedNodeConfig) => void } | { variant: 'assemble' @@ -72,10 +72,10 @@ const SubGraphChildren: FC = (props) => {
diff --git a/web/app/components/sub-graph/components/sub-graph-main.tsx b/web/app/components/sub-graph/components/sub-graph-main.tsx index a6abcb2d94..839f9f38f2 100644 --- a/web/app/components/sub-graph/components/sub-graph-main.tsx +++ b/web/app/components/sub-graph/components/sub-graph-main.tsx @@ -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 = (props) => { variant="agent" title={title} extractorNodeId={extractorNodeId} - mentionConfig={props.mentionConfig} - onMentionConfigChange={props.onMentionConfigChange} + nestedNodeConfig={props.nestedNodeConfig} + onNestedNodeConfigChange={props.onNestedNodeConfigChange} /> ) : ( diff --git a/web/app/components/sub-graph/index.tsx b/web/app/components/sub-graph/index.tsx index 28adbac608..100dc3f576 100644 --- a/web/app/components/sub-graph/index.tsx +++ b/web/app/components/sub-graph/index.tsx @@ -230,8 +230,8 @@ const SubGraphContent: FC = (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} diff --git a/web/app/components/sub-graph/types.ts b/web/app/components/sub-graph/types.ts index 94e3b4584d..cf2ef2f731 100644 --- a/web/app/components/sub-graph/types.ts +++ b/web/app/components/sub-graph/types.ts @@ -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 } diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx index c5970fed97..4a3d0fe70b 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -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 = ({ } } - 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 = ({ && 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 = ({ ...varInput, type: resolvedType, value: normalizedValue, - mention_config: resolvedMentionConfig, + nested_node_config: resolvedNestedNodeConfig, }, }) } diff --git a/web/app/components/workflow/nodes/_base/types.ts b/web/app/components/workflow/nodes/_base/types.ts index f3c64656b5..3f24fe4e1b 100644 --- a/web/app/components/workflow/nodes/_base/types.ts +++ b/web/app/components/workflow/nodes/_base/types.ts @@ -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 // Base resource interface diff --git a/web/app/components/workflow/nodes/tool/components/context-generate-modal/hooks/use-context-generate.ts b/web/app/components/workflow/nodes/tool/components/context-generate-modal/hooks/use-context-generate.ts index b216210cff..8385312aef 100644 --- a/web/app/components/workflow/nodes/tool/components/context-generate-modal/hooks/use-context-generate.ts +++ b/web/app/components/workflow/nodes/tool/components/context-generate-modal/hooks/use-context-generate.ts @@ -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, diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts index 289c242f2f..c38ac71a0b 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts @@ -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 + nestedNodeData: Partial 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 = {} - 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 | undefined - if (!mentionNodeData) + const nestedNode = response?.graph?.nodes?.find(node => node.id === payload.extractorNodeId) + const nestedNodeData = nestedNode?.data as Partial | 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, } } diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 71c7751e50..a3349ad9fc 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -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 -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) diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx index 2fbe5b7fbe..f620f42f3f 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx @@ -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 = (props) => { return vars.filter(nodeVar => availableNodeIds.has(nodeVar.nodeId)) }, [getNodeAvailableVars, isChatMode, parentAvailableNodes]) - const mentionConfig = useMemo(() => { - const current = toolParam?.mention_config + const nestedNodeConfig = useMemo(() => { + 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 = (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 = (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 = (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 = (props) => { agentNodeId={props.agentNodeId} agentName={props.agentName} configsMap={configsMap} - mentionConfig={mentionConfig} - onMentionConfigChange={handleMentionConfigChange} + nestedNodeConfig={nestedNodeConfig} + onNestedNodeConfigChange={handleNestedNodeConfigChange} extractorNode={extractorNode as Node | undefined} toolParamValue={toolParamValue} parentAvailableNodes={parentAvailableNodes} diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 0b20f30333..5bc8c7dfc4 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -77,7 +77,7 @@ const Node: FC> = ({ }, {} as Record) }, [nodes]) - const mentionEntries = useMemo(() => { + const nestedNodeEntries = useMemo(() => { const entries: Array<{ agentNodeId: string, extractorNodeId?: string, paramKey: string }> = [] const seen = new Set() const toolParams = data.tool_parameters || {} @@ -97,8 +97,8 @@ const Node: FC> = ({ 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> = ({ }, [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> = ({ 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> = ({ hasWarning, } }) - }, [mentionEntries, nodesById, nodesMetaDataMap, strategyProviders, language, t]) + }, [nestedNodeEntries, nodesById, nodesMetaDataMap, strategyProviders, language, t]) const hasConfigs = toolConfigs.length > 0 const hasReferences = referenceItems.length > 0 diff --git a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts index f4a042d077..e060b42ed5 100644 --- a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts @@ -32,7 +32,7 @@ const useSingleRunFormParams = ({ const { inputs } = useNodeCrud(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) diff --git a/web/service/workflow.ts b/web/service/workflow.ts index a20814fe72..401a6a10f2 100644 --- a/web/service/workflow.ts +++ b/web/service/workflow.ts @@ -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(url) } -export const fetchMentionGraph = (flowType: FlowType, flowId: string, payload: MentionGraphPayload) => { - return post(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/mention-graph`, { body: payload }, { silent: true }) +export const fetchNestedNodeGraph = (flowType: FlowType, flowId: string, payload: NestedNodeGraphPayload) => { + return post(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nested-node-graph`, { body: payload }, { silent: true }) } export const singleNodeRun = (flowType: FlowType, flowId: string, nodeId: string, params: object) => { diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 3d5bcb477e..7898787f48 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -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[]