From 4cd7a4ebdecd76cebc3e6bb9d9fd4d763cdfe6d1 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 15 Apr 2026 19:47:24 +0800 Subject: [PATCH 1/2] refactor(nodes)!: align node initialization signatures Normalize node constructors around typed node data configs and explicit dependency injection. Update direct construction sites in tests and examples to match the unified initialization pattern. --- examples/graphon_openai_slim/workflow.py | 75 ++++++++----------- src/graphon/nodes/base/node.py | 19 ++--- src/graphon/nodes/code/code_node.py | 5 +- src/graphon/nodes/document_extractor/node.py | 5 +- src/graphon/nodes/http_request/node.py | 5 +- .../nodes/human_input/human_input_node.py | 19 +++-- src/graphon/nodes/llm/node.py | 5 +- .../parameter_extractor_node.py | 5 +- .../question_classifier_node.py | 5 +- .../template_transform_node.py | 5 +- src/graphon/nodes/tool/tool_node.py | 13 ++-- .../nodes/variable_assigner/v1/node.py | 4 +- .../nodes/variable_assigner/v2/node.py | 4 +- tests/graph/test_graph_validation.py | 10 +-- tests/http/test_client.py | 42 +++++------ .../nodes/parameter_extractor/test_prompts.py | 43 +++++------ tests/nodes/variable_assigner/test_v1_node.py | 20 ++--- tests/nodes/variable_assigner/test_v2_node.py | 22 +++--- 18 files changed, 134 insertions(+), 172 deletions(-) diff --git a/examples/graphon_openai_slim/workflow.py b/examples/graphon_openai_slim/workflow.py index d9146d8..1ff5b9b 100644 --- a/examples/graphon_openai_slim/workflow.py +++ b/examples/graphon_openai_slim/workflow.py @@ -255,48 +255,42 @@ def build_graph( ) -> Graph: start_node = StartNode( node_id="start", - config={ - "id": "start", - "data": StartNodeData( - title="Start", - variables=[ - VariableEntity( - variable="query", - label="Query", - type=VariableEntityType.PARAGRAPH, - required=True, - ), - ], - ), - }, + config=StartNodeData( + title="Start", + variables=[ + VariableEntity( + variable="query", + label="Query", + type=VariableEntityType.PARAGRAPH, + required=True, + ), + ], + ), graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, ) llm_node = LLMNode( node_id="llm", - config={ - "id": "llm", - "data": LLMNodeData( - title="LLM", - model=ModelConfig( - provider=provider, - name="gpt-5.4", - mode=LLMMode.CHAT, - ), - prompt_template=[ - LLMNodeChatModelMessage( - role=PromptMessageRole.SYSTEM, - text="You are a concise assistant.", - ), - LLMNodeChatModelMessage( - role=PromptMessageRole.USER, - text="{{#start.query#}}", - ), - ], - context=ContextConfig(enabled=False), + config=LLMNodeData( + title="LLM", + model=ModelConfig( + provider=provider, + name="gpt-5.4", + mode=LLMMode.CHAT, ), - }, + prompt_template=[ + LLMNodeChatModelMessage( + role=PromptMessageRole.SYSTEM, + text="You are a concise assistant.", + ), + LLMNodeChatModelMessage( + role=PromptMessageRole.USER, + text="{{#start.query#}}", + ), + ], + context=ContextConfig(enabled=False), + ), graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, model_instance=prepared_llm, @@ -306,13 +300,10 @@ def build_graph( output_node = AnswerNode( node_id="output", - config={ - "id": "output", - "data": AnswerNodeData( - title="Output", - answer="{{#llm.text#}}", - ), - }, + config=AnswerNodeData( + title="Output", + answer="{{#llm.text#}}", + ), graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, ) diff --git a/src/graphon/nodes/base/node.py b/src/graphon/nodes/base/node.py index 52f06a5..b521d65 100644 --- a/src/graphon/nodes/base/node.py +++ b/src/graphon/nodes/base/node.py @@ -523,10 +523,15 @@ def _extract_node_data_type_from_generic(cls) -> type[BaseNodeData] | None: def __init__( self, node_id: str, - config: NodeConfigDict, + config: NodeDataT, + *, graph_init_params: GraphInitParams, graph_runtime_state: GraphRuntimeState, ) -> None: + if not node_id: + msg = "node_id is required" + raise ValueError(msg) + self._graph_init_params = graph_init_params self._run_context = MappingProxyType(dict(graph_init_params.run_context)) self.id = node_id @@ -536,19 +541,11 @@ def __init__( self.graph_runtime_state = graph_runtime_state self.state: NodeState = NodeState.UNKNOWN # node execution state - config_node_id = config["id"] - if node_id != config_node_id: - msg = ( - "node_id must match config['id'], " - f"got node_id={node_id!r}, config['id']={config_node_id!r}" - ) - raise ValueError(msg) - - self._node_id = config_node_id + self._node_id = node_id self._node_execution_id: str = "" self._start_at = datetime.now(UTC).replace(tzinfo=None) - self._node_data = self.validate_node_data(config["data"]) + self._node_data = self.validate_node_data(config) self.post_init() diff --git a/src/graphon/nodes/code/code_node.py b/src/graphon/nodes/code/code_node.py index a06c366..b94d7d5 100644 --- a/src/graphon/nodes/code/code_node.py +++ b/src/graphon/nodes/code/code_node.py @@ -3,7 +3,6 @@ from textwrap import dedent from typing import TYPE_CHECKING, Any, Protocol, cast, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.node_events.base import NodeRunResult from graphon.nodes.base.node import Node @@ -83,10 +82,10 @@ class CodeNode(Node[CodeNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: CodeNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, code_executor: WorkflowCodeExecutor, code_limits: CodeNodeLimits, ) -> None: diff --git a/src/graphon/nodes/document_extractor/node.py b/src/graphon/nodes/document_extractor/node.py index 28a3883..8581ca2 100644 --- a/src/graphon/nodes/document_extractor/node.py +++ b/src/graphon/nodes/document_extractor/node.py @@ -21,7 +21,6 @@ from docx.table import Table from docx.text.paragraph import Paragraph -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.file import file_manager from graphon.file.enums import FileTransferMethod @@ -193,10 +192,10 @@ def version(cls) -> str: def __init__( self, node_id: str, - config: NodeConfigDict, + config: DocumentExtractorNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, unstructured_api_config: UnstructuredApiConfig | None = None, http_client: HttpClientProtocol | None = None, ) -> None: diff --git a/src/graphon/nodes/http_request/node.py b/src/graphon/nodes/http_request/node.py index 5fb2513..7d7bc96 100644 --- a/src/graphon/nodes/http_request/node.py +++ b/src/graphon/nodes/http_request/node.py @@ -3,7 +3,6 @@ from collections.abc import Callable, Mapping, Sequence from typing import TYPE_CHECKING, Any, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.file.enums import FileTransferMethod from graphon.file.models import File @@ -45,10 +44,10 @@ class HttpRequestNode(Node[HttpRequestNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: HttpRequestNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, http_request_config: HttpRequestNodeConfig, http_client: HttpClientProtocol | None = None, tool_file_manager_factory: Callable[[], ToolFileManagerProtocol], diff --git a/src/graphon/nodes/human_input/human_input_node.py b/src/graphon/nodes/human_input/human_input_node.py index c7b5e14..c3d6aaa 100644 --- a/src/graphon/nodes/human_input/human_input_node.py +++ b/src/graphon/nodes/human_input/human_input_node.py @@ -4,7 +4,6 @@ from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, override -from graphon.entities.graph_config import NodeConfigDict from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import ( BuiltinNodeTypes, @@ -64,10 +63,14 @@ class HumanInputNode(Node[HumanInputNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: HumanInputNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - runtime: HumanInputNodeRuntimeProtocol | None = None, + # TODO @-LAN: See https://github.com/langgenius/graphon/issues/new/choose. # noqa: FIX002 + # Make `runtime` optional once Graphon provides a default human-input + # runtime adapter instead of requiring an embedding-specific implementation. + runtime: HumanInputNodeRuntimeProtocol, form_repository: object | None = None, ) -> None: super().__init__( @@ -76,13 +79,9 @@ def __init__( graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, ) - resolved_runtime = runtime - if resolved_runtime is None: - msg = "runtime is required" - raise ValueError(msg) if form_repository is not None: with_form_repository = getattr( - resolved_runtime, + runtime, "with_form_repository", None, ) @@ -91,8 +90,8 @@ def __init__( if not isinstance(updated_runtime, HumanInputNodeRuntimeProtocol): msg = "with_form_repository() must return a HumanInput runtime" raise TypeError(msg) - resolved_runtime = updated_runtime - self._runtime: HumanInputNodeRuntimeProtocol = resolved_runtime + runtime = updated_runtime + self._runtime: HumanInputNodeRuntimeProtocol = runtime @classmethod @override diff --git a/src/graphon/nodes/llm/node.py b/src/graphon/nodes/llm/node.py index 4f6746a..29b1a14 100644 --- a/src/graphon/nodes/llm/node.py +++ b/src/graphon/nodes/llm/node.py @@ -10,7 +10,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, override -from graphon.entities.graph_config import NodeConfigDict from graphon.entities.graph_init_params import GraphInitParams from graphon.enums import ( BuiltinNodeTypes, @@ -132,10 +131,10 @@ class LLMNode(Node[LLMNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: LLMNodeData, + *, graph_init_params: GraphInitParams, graph_runtime_state: GraphRuntimeState, - *, credentials_provider: object | None = None, model_factory: object | None = None, model_instance: PreparedLLMProtocol, diff --git a/src/graphon/nodes/parameter_extractor/parameter_extractor_node.py b/src/graphon/nodes/parameter_extractor/parameter_extractor_node.py index 7542019..2fbee73 100644 --- a/src/graphon/nodes/parameter_extractor/parameter_extractor_node.py +++ b/src/graphon/nodes/parameter_extractor/parameter_extractor_node.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import ( BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey, @@ -139,10 +138,10 @@ class ParameterExtractorNode(Node[ParameterExtractorNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: ParameterExtractorNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, credentials_provider: object | None = None, model_factory: object | None = None, model_instance: PreparedLLMProtocol, diff --git a/src/graphon/nodes/question_classifier/question_classifier_node.py b/src/graphon/nodes/question_classifier/question_classifier_node.py index ed3eceb..ea25ba4 100644 --- a/src/graphon/nodes/question_classifier/question_classifier_node.py +++ b/src/graphon/nodes/question_classifier/question_classifier_node.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, override -from graphon.entities.graph_config import NodeConfigDict from graphon.entities.graph_init_params import GraphInitParams from graphon.enums import ( BuiltinNodeTypes, @@ -87,10 +86,10 @@ class QuestionClassifierNode(Node[QuestionClassifierNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: QuestionClassifierNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, credentials_provider: object | None = None, model_factory: object | None = None, model_instance: PreparedLLMProtocol, diff --git a/src/graphon/nodes/template_transform/template_transform_node.py b/src/graphon/nodes/template_transform/template_transform_node.py index 3696072..0ebc039 100644 --- a/src/graphon/nodes/template_transform/template_transform_node.py +++ b/src/graphon/nodes/template_transform/template_transform_node.py @@ -1,7 +1,6 @@ from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, cast, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.node_events.base import NodeRunResult from graphon.nodes.base.entities import VariableSelector @@ -28,10 +27,10 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: TemplateTransformNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, jinja2_template_renderer: Jinja2TemplateRenderer, max_output_length: int | None = None, ) -> None: diff --git a/src/graphon/nodes/tool/tool_node.py b/src/graphon/nodes/tool/tool_node.py index 991355c..6532e3c 100644 --- a/src/graphon/nodes/tool/tool_node.py +++ b/src/graphon/nodes/tool/tool_node.py @@ -2,7 +2,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import ( BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey, @@ -61,12 +60,15 @@ class ToolNode(Node[ToolNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: ToolNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - *, tool_file_manager_factory: ToolFileManagerProtocol, - runtime: ToolNodeRuntimeProtocol | None = None, + # TODO @-LAN: See https://github.com/langgenius/graphon/issues/new/choose. # noqa: FIX002 + # Make `runtime` optional once Graphon provides a default tool runtime + # adapter at the workflow boundary. + runtime: ToolNodeRuntimeProtocol, ) -> None: super().__init__( node_id=node_id, @@ -75,9 +77,6 @@ def __init__( graph_runtime_state=graph_runtime_state, ) self._tool_file_manager_factory = tool_file_manager_factory - if runtime is None: - msg = "runtime is required" - raise ValueError(msg) self._runtime = runtime def init_tool_runtime( diff --git a/src/graphon/nodes/variable_assigner/v1/node.py b/src/graphon/nodes/variable_assigner/v1/node.py index f65c465..3ca684e 100644 --- a/src/graphon/nodes/variable_assigner/v1/node.py +++ b/src/graphon/nodes/variable_assigner/v1/node.py @@ -1,7 +1,6 @@ from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, cast, override -from graphon.entities.graph_config import NodeConfigDict from graphon.entities.graph_init_params import GraphInitParams from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.node_events.base import ( @@ -34,7 +33,8 @@ class VariableAssignerNode(Node[VariableAssignerData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: VariableAssignerData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", ) -> None: diff --git a/src/graphon/nodes/variable_assigner/v2/node.py b/src/graphon/nodes/variable_assigner/v2/node.py index 63f85b1..c6b4fbc 100644 --- a/src/graphon/nodes/variable_assigner/v2/node.py +++ b/src/graphon/nodes/variable_assigner/v2/node.py @@ -2,7 +2,6 @@ from collections.abc import Generator, Mapping, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, cast, override -from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.node_events.base import ( NodeEventBase, @@ -84,7 +83,8 @@ class VariableAssignerNode(Node[VariableAssignerNodeData]): def __init__( self, node_id: str, - config: NodeConfigDict, + config: VariableAssignerNodeData, + *, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", ) -> None: diff --git a/tests/graph/test_graph_validation.py b/tests/graph/test_graph_validation.py index aba223f..dd3be52 100644 --- a/tests/graph/test_graph_validation.py +++ b/tests/graph/test_graph_validation.py @@ -7,7 +7,6 @@ import pytest from graphon.entities.base_node_data import BaseNodeData -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter from graphon.entities.graph_init_params import GraphInitParams from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeExecutionType, NodeType from graphon.graph.graph import Graph @@ -37,7 +36,7 @@ def __init__( self, *, node_id: str, - config: NodeConfigDict, + config: _TestNodeData, graph_init_params: GraphInitParams, graph_runtime_state: GraphRuntimeState, ) -> None: @@ -76,10 +75,11 @@ class _SimpleNodeFactory: graph_runtime_state: GraphRuntimeState def create_node(self, node_config: Mapping[str, object]) -> _TestNode: - node_id = str(node_config["id"]) return _TestNode( - node_id=node_id, - config=NodeConfigDictAdapter.validate_python(node_config), + node_id=str(node_config["id"]), + config=_TestNode.validate_node_data( + node_config["data"], # type: ignore[arg-type] + ), graph_init_params=self.graph_init_params, graph_runtime_state=self.graph_runtime_state, ) diff --git a/tests/http/test_client.py b/tests/http/test_client.py index c3016c6..e8c077a 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -7,7 +7,6 @@ import pytest from pytest_mock import MockerFixture -from graphon.entities.graph_config import NodeConfigDictAdapter from graphon.file.models import File from graphon.http import ( HttpClientMaxRetriesExceededError, @@ -16,8 +15,13 @@ HttpxHttpClient, get_http_client, ) +from graphon.nodes.document_extractor.entities import DocumentExtractorNodeData from graphon.nodes.document_extractor.node import DocumentExtractorNode -from graphon.nodes.http_request import HttpRequestNode, build_http_request_config +from graphon.nodes.http_request import ( + HttpRequestNode, + HttpRequestNodeData, + build_http_request_config, +) from graphon.nodes.llm.file_saver import FileSaverImpl from graphon.nodes.llm.node import LLMNode from graphon.nodes.question_classifier.question_classifier_node import ( @@ -156,19 +160,15 @@ def test_httpx_http_client_raises_request_error_without_retry_wrapping( def test_http_request_node_uses_default_http_client_when_not_injected() -> None: node = HttpRequestNode( node_id="http", - config=NodeConfigDictAdapter.validate_python({ - "id": "http", - "data": { - "type": "http-request", - "title": "HTTP Request", - "method": "get", - "url": "https://example.com", - "authorization": {"type": "no-auth"}, - "headers": "", - "params": "", - "body": {"type": "none", "data": []}, - }, - }), + config=HttpRequestNodeData( + title="HTTP Request", + method="get", + url="https://example.com", + authorization={"type": "no-auth"}, + headers="", + params="", + body={"type": "none", "data": []}, + ), graph_init_params=build_graph_init_params( graph_config={"nodes": [], "edges": []}, ), @@ -185,14 +185,10 @@ def test_http_request_node_uses_default_http_client_when_not_injected() -> None: def test_document_extractor_node_uses_default_http_client_when_not_injected() -> None: node = DocumentExtractorNode( node_id="extractor", - config=NodeConfigDictAdapter.validate_python({ - "id": "extractor", - "data": { - "type": "document-extractor", - "title": "Document Extractor", - "variable_selector": ["inputs", "file"], - }, - }), + config=DocumentExtractorNodeData( + title="Document Extractor", + variable_selector=["inputs", "file"], + ), graph_init_params=build_graph_init_params( graph_config={"nodes": [], "edges": []}, ), diff --git a/tests/nodes/parameter_extractor/test_prompts.py b/tests/nodes/parameter_extractor/test_prompts.py index 7be515d..95dcf0c 100644 --- a/tests/nodes/parameter_extractor/test_prompts.py +++ b/tests/nodes/parameter_extractor/test_prompts.py @@ -1,10 +1,9 @@ import time from unittest.mock import Mock -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.enums import BuiltinNodeTypes from graphon.model_runtime.entities.llm_entities import LLMMode from graphon.model_runtime.entities.message_entities import PromptMessageRole +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData from graphon.nodes.parameter_extractor.parameter_extractor_node import ( ParameterExtractorNode, ) @@ -31,29 +30,25 @@ def _build_parameter_extractor_node() -> tuple[ParameterExtractorNode, VariableP init_params = build_graph_init_params(graph_config={"nodes": [], "edges": []}) node = ParameterExtractorNode( node_id="extractor", - config=NodeConfigDictAdapter.validate_python({ - "id": "extractor", - "data": { - "type": BuiltinNodeTypes.PARAMETER_EXTRACTOR, - "title": "Parameter Extractor", - "model": { - "provider": "test", - "name": "test-model", - "mode": LLMMode.CHAT, - }, - "query": ["start", "query"], - "parameters": [ - { - "name": "location", - "type": "string", - "description": "The target location", - "required": True, - }, - ], - "instruction": "Follow {{#start.rule#}} instructions.", - "reasoning_mode": "function_call", + config=ParameterExtractorNodeData( + title="Parameter Extractor", + model={ + "provider": "test", + "name": "test-model", + "mode": LLMMode.CHAT, }, - }), + query=["start", "query"], + parameters=[ + { + "name": "location", + "type": "string", + "description": "The target location", + "required": True, + }, + ], + instruction="Follow {{#start.rule#}} instructions.", + reasoning_mode="function_call", + ), graph_init_params=init_params, graph_runtime_state=runtime_state, model_instance=Mock(), diff --git a/tests/nodes/variable_assigner/test_v1_node.py b/tests/nodes/variable_assigner/test_v1_node.py index 4fdb313..c50a92d 100644 --- a/tests/nodes/variable_assigner/test_v1_node.py +++ b/tests/nodes/variable_assigner/test_v1_node.py @@ -1,12 +1,10 @@ import time from collections.abc import Sequence -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.enums import BuiltinNodeTypes from graphon.graph_events.node import NodeRunSucceededEvent, NodeRunVariableUpdatedEvent from graphon.nodes.variable_assigner.common import helpers as common_helpers from graphon.nodes.variable_assigner.v1.node import VariableAssignerNode -from graphon.nodes.variable_assigner.v1.node_data import WriteMode +from graphon.nodes.variable_assigner.v1.node_data import VariableAssignerData, WriteMode from graphon.runtime.graph_runtime_state import GraphRuntimeState from graphon.runtime.variable_pool import VariablePool from graphon.variables.variables import ( @@ -35,16 +33,12 @@ def _build_node( node_id="assigner", graph_init_params=init_params, graph_runtime_state=runtime_state, - config=NodeConfigDictAdapter.validate_python({ - "id": "assigner", - "data": { - "type": BuiltinNodeTypes.VARIABLE_ASSIGNER, - "title": "Variable Assigner", - "assigned_variable_selector": assigned_selector, - "write_mode": write_mode, - "input_variable_selector": input_selector, - }, - }), + config=VariableAssignerData( + title="Variable Assigner", + assigned_variable_selector=assigned_selector, + write_mode=write_mode, + input_variable_selector=input_selector, + ), ) diff --git a/tests/nodes/variable_assigner/test_v2_node.py b/tests/nodes/variable_assigner/test_v2_node.py index 4f6717f..0fe3c39 100644 --- a/tests/nodes/variable_assigner/test_v2_node.py +++ b/tests/nodes/variable_assigner/test_v2_node.py @@ -1,14 +1,16 @@ import time from collections.abc import Sequence -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus +from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph_events.node import ( NodeRunFailedEvent, NodeRunSucceededEvent, NodeRunVariableUpdatedEvent, ) -from graphon.nodes.variable_assigner.v2.entities import VariableOperationItem +from graphon.nodes.variable_assigner.v2.entities import ( + VariableAssignerNodeData, + VariableOperationItem, +) from graphon.nodes.variable_assigner.v2.enums import InputType, Operation from graphon.nodes.variable_assigner.v2.node import VariableAssignerNode from graphon.runtime.graph_runtime_state import GraphRuntimeState @@ -37,15 +39,11 @@ def _build_node( node_id="assigner", graph_init_params=init_params, graph_runtime_state=runtime_state, - config=NodeConfigDictAdapter.validate_python({ - "id": "assigner", - "data": { - "type": BuiltinNodeTypes.VARIABLE_ASSIGNER, - "title": "Variable Assigner", - "version": "2", - "items": items, - }, - }), + config=VariableAssignerNodeData( + title="Variable Assigner", + version="2", + items=items, + ), ) From 3404da118457be20c06f11145446f8a798114576 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 15 Apr 2026 19:56:09 +0800 Subject: [PATCH 2/2] feat(nodes): add node data mapping helper Add a discoverable classmethod for converting Python mappings into typed node data without reopening node constructors to raw mapping inputs. --- src/graphon/nodes/base/node.py | 17 +++++++++++++++++ tests/graph/test_graph_validation.py | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/graphon/nodes/base/node.py b/src/graphon/nodes/base/node.py index b521d65..5acaeb6 100644 --- a/src/graphon/nodes/base/node.py +++ b/src/graphon/nodes/base/node.py @@ -174,6 +174,23 @@ def get_node_type_classes_mapping( class _NodeDataModelMixin[NodeDataT: BaseNodeData]: """Typed node-data hydration helpers.""" + @classmethod + def node_data_from_mapping( + cls: type[Node[NodeDataT]], + node_data: Mapping[str, Any], + ) -> NodeDataT: + """Build the concrete node-data instance from a Python mapping. + + This convenience wrapper keeps direct node construction ergonomic for + callers that naturally start from plain dictionaries while preserving the + stricter `Node.__init__(..., config=NodeDataT, ...)` contract. + + Returns: + The validated node data instance for the concrete node subclass. + + """ + return cls.validate_node_data(node_data) + @classmethod def validate_node_data( cls: type[Node[NodeDataT]], diff --git a/tests/graph/test_graph_validation.py b/tests/graph/test_graph_validation.py index dd3be52..d3c2d3f 100644 --- a/tests/graph/test_graph_validation.py +++ b/tests/graph/test_graph_validation.py @@ -132,6 +132,20 @@ def test_graph_initialization_runs_default_validators( assert "answer" in graph.nodes +def test_node_data_from_mapping_returns_typed_node_data() -> None: + node_data = _TestNode.node_data_from_mapping( + { + "type": BuiltinNodeTypes.ANSWER, + "title": "Answer", + "execution_type": NodeExecutionType.EXECUTABLE, + }, + ) + + assert isinstance(node_data, _TestNodeData) + assert node_data.type == BuiltinNodeTypes.ANSWER + assert node_data.execution_type == NodeExecutionType.EXECUTABLE + + def test_graph_validation_fails_for_unknown_edge_targets( graph_init_dependencies: tuple[_SimpleNodeFactory, dict[str, object]], ) -> None: