node.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import logging
  2. from abc import abstractmethod
  3. from collections.abc import Generator, Mapping, Sequence
  4. from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
  5. from core.workflow.entities.node_entities import NodeRunResult
  6. from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
  7. from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
  8. from core.workflow.nodes.enums import ErrorStrategy, NodeType
  9. from core.workflow.nodes.event import NodeEvent, RunCompletedEvent
  10. if TYPE_CHECKING:
  11. from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
  12. from core.workflow.graph_engine.entities.event import InNodeEvent
  13. logger = logging.getLogger(__name__)
  14. class BaseNode:
  15. _node_type: ClassVar[NodeType]
  16. def __init__(
  17. self,
  18. id: str,
  19. config: Mapping[str, Any],
  20. graph_init_params: "GraphInitParams",
  21. graph: "Graph",
  22. graph_runtime_state: "GraphRuntimeState",
  23. previous_node_id: Optional[str] = None,
  24. thread_pool_id: Optional[str] = None,
  25. ) -> None:
  26. self.id = id
  27. self.tenant_id = graph_init_params.tenant_id
  28. self.app_id = graph_init_params.app_id
  29. self.workflow_type = graph_init_params.workflow_type
  30. self.workflow_id = graph_init_params.workflow_id
  31. self.graph_config = graph_init_params.graph_config
  32. self.user_id = graph_init_params.user_id
  33. self.user_from = graph_init_params.user_from
  34. self.invoke_from = graph_init_params.invoke_from
  35. self.workflow_call_depth = graph_init_params.call_depth
  36. self.graph = graph
  37. self.graph_runtime_state = graph_runtime_state
  38. self.previous_node_id = previous_node_id
  39. self.thread_pool_id = thread_pool_id
  40. node_id = config.get("id")
  41. if not node_id:
  42. raise ValueError("Node ID is required.")
  43. self.node_id = node_id
  44. @abstractmethod
  45. def init_node_data(self, data: Mapping[str, Any]) -> None: ...
  46. @abstractmethod
  47. def _run(self) -> NodeRunResult | Generator[Union[NodeEvent, "InNodeEvent"], None, None]:
  48. """
  49. Run node
  50. :return:
  51. """
  52. raise NotImplementedError
  53. def run(self) -> Generator[Union[NodeEvent, "InNodeEvent"], None, None]:
  54. try:
  55. result = self._run()
  56. except Exception as e:
  57. logger.exception("Node %s failed to run", self.node_id)
  58. result = NodeRunResult(
  59. status=WorkflowNodeExecutionStatus.FAILED,
  60. error=str(e),
  61. error_type="WorkflowNodeError",
  62. )
  63. if isinstance(result, NodeRunResult):
  64. yield RunCompletedEvent(run_result=result)
  65. else:
  66. yield from result
  67. @classmethod
  68. def extract_variable_selector_to_variable_mapping(
  69. cls,
  70. *,
  71. graph_config: Mapping[str, Any],
  72. config: Mapping[str, Any],
  73. ) -> Mapping[str, Sequence[str]]:
  74. """Extracts references variable selectors from node configuration.
  75. The `config` parameter represents the configuration for a specific node type and corresponds
  76. to the `data` field in the node definition object.
  77. The returned mapping has the following structure:
  78. {'1747829548239.#1747829667553.result#': ['1747829667553', 'result']}
  79. For loop and iteration nodes, the mapping may look like this:
  80. {
  81. "1748332301644.input_selector": ["1748332363630", "result"],
  82. "1748332325079.1748332325079.#sys.workflow_id#": ["sys", "workflow_id"],
  83. }
  84. where `1748332301644` is the ID of the loop / iteration node,
  85. and `1748332325079` is the ID of the node inside the loop or iteration node.
  86. Here, the key consists of two parts: the current node ID (provided as the `node_id`
  87. parameter to `_extract_variable_selector_to_variable_mapping`) and the variable selector,
  88. enclosed in `#` symbols. These two parts are separated by a dot (`.`).
  89. The value is a list of string representing the variable selector, where the first element is the node ID
  90. of the referenced variable, and the second element is the variable name within that node.
  91. The meaning of the above response is:
  92. The node with ID `1747829548239` references the variable `result` from the node with
  93. ID `1747829667553`. For example, if `1747829548239` is a LLM node, its prompt may contain a
  94. reference to the `result` output variable of node `1747829667553`.
  95. :param graph_config: graph config
  96. :param config: node config
  97. :return:
  98. """
  99. node_id = config.get("id")
  100. if not node_id:
  101. raise ValueError("Node ID is required when extracting variable selector to variable mapping.")
  102. # Pass raw dict data instead of creating NodeData instance
  103. data = cls._extract_variable_selector_to_variable_mapping(
  104. graph_config=graph_config, node_id=node_id, node_data=config.get("data", {})
  105. )
  106. return data
  107. @classmethod
  108. def _extract_variable_selector_to_variable_mapping(
  109. cls,
  110. *,
  111. graph_config: Mapping[str, Any],
  112. node_id: str,
  113. node_data: Mapping[str, Any],
  114. ) -> Mapping[str, Sequence[str]]:
  115. return {}
  116. @classmethod
  117. def get_default_config(cls, filters: Optional[dict] = None) -> dict:
  118. return {}
  119. @property
  120. def type_(self) -> NodeType:
  121. return self._node_type
  122. @classmethod
  123. @abstractmethod
  124. def version(cls) -> str:
  125. """`node_version` returns the version of current node type."""
  126. # NOTE(QuantumGhost): This should be in sync with `NODE_TYPE_CLASSES_MAPPING`.
  127. #
  128. # If you have introduced a new node type, please add it to `NODE_TYPE_CLASSES_MAPPING`
  129. # in `api/core/workflow/nodes/__init__.py`.
  130. raise NotImplementedError("subclasses of BaseNode must implement `version` method.")
  131. @property
  132. def continue_on_error(self) -> bool:
  133. return False
  134. @property
  135. def retry(self) -> bool:
  136. return False
  137. # Abstract methods that subclasses must implement to provide access
  138. # to BaseNodeData properties in a type-safe way
  139. @abstractmethod
  140. def _get_error_strategy(self) -> Optional[ErrorStrategy]:
  141. """Get the error strategy for this node."""
  142. ...
  143. @abstractmethod
  144. def _get_retry_config(self) -> RetryConfig:
  145. """Get the retry configuration for this node."""
  146. ...
  147. @abstractmethod
  148. def _get_title(self) -> str:
  149. """Get the node title."""
  150. ...
  151. @abstractmethod
  152. def _get_description(self) -> Optional[str]:
  153. """Get the node description."""
  154. ...
  155. @abstractmethod
  156. def _get_default_value_dict(self) -> dict[str, Any]:
  157. """Get the default values dictionary for this node."""
  158. ...
  159. @abstractmethod
  160. def get_base_node_data(self) -> BaseNodeData:
  161. """Get the BaseNodeData object for this node."""
  162. ...
  163. # Public interface properties that delegate to abstract methods
  164. @property
  165. def error_strategy(self) -> Optional[ErrorStrategy]:
  166. """Get the error strategy for this node."""
  167. return self._get_error_strategy()
  168. @property
  169. def retry_config(self) -> RetryConfig:
  170. """Get the retry configuration for this node."""
  171. return self._get_retry_config()
  172. @property
  173. def title(self) -> str:
  174. """Get the node title."""
  175. return self._get_title()
  176. @property
  177. def description(self) -> Optional[str]:
  178. """Get the node description."""
  179. return self._get_description()
  180. @property
  181. def default_value_dict(self) -> dict[str, Any]:
  182. """Get the default values dictionary for this node."""
  183. return self._get_default_value_dict()