workflow_entry.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import logging
  2. import time
  3. import uuid
  4. from collections.abc import Generator, Mapping, Sequence
  5. from typing import Any, Optional
  6. from configs import dify_config
  7. from core.app.apps.exc import GenerateTaskStoppedError
  8. from core.app.entities.app_invoke_entities import InvokeFrom
  9. from core.file.models import File
  10. from core.workflow.callbacks import WorkflowCallback
  11. from core.workflow.constants import ENVIRONMENT_VARIABLE_NODE_ID
  12. from core.workflow.entities.variable_pool import VariablePool
  13. from core.workflow.errors import WorkflowNodeRunFailedError
  14. from core.workflow.graph_engine.entities.event import GraphEngineEvent, GraphRunFailedEvent, InNodeEvent
  15. from core.workflow.graph_engine.entities.graph import Graph
  16. from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams
  17. from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
  18. from core.workflow.graph_engine.graph_engine import GraphEngine
  19. from core.workflow.nodes import NodeType
  20. from core.workflow.nodes.base import BaseNode
  21. from core.workflow.nodes.event import NodeEvent
  22. from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING
  23. from core.workflow.system_variable import SystemVariable
  24. from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool
  25. from factories import file_factory
  26. from models.enums import UserFrom
  27. from models.workflow import (
  28. Workflow,
  29. WorkflowType,
  30. )
  31. logger = logging.getLogger(__name__)
  32. class WorkflowEntry:
  33. def __init__(
  34. self,
  35. tenant_id: str,
  36. app_id: str,
  37. workflow_id: str,
  38. workflow_type: WorkflowType,
  39. graph_config: Mapping[str, Any],
  40. graph: Graph,
  41. user_id: str,
  42. user_from: UserFrom,
  43. invoke_from: InvokeFrom,
  44. call_depth: int,
  45. variable_pool: VariablePool,
  46. thread_pool_id: Optional[str] = None,
  47. ) -> None:
  48. """
  49. Init workflow entry
  50. :param tenant_id: tenant id
  51. :param app_id: app id
  52. :param workflow_id: workflow id
  53. :param workflow_type: workflow type
  54. :param graph_config: workflow graph config
  55. :param graph: workflow graph
  56. :param user_id: user id
  57. :param user_from: user from
  58. :param invoke_from: invoke from
  59. :param call_depth: call depth
  60. :param variable_pool: variable pool
  61. :param thread_pool_id: thread pool id
  62. """
  63. # check call depth
  64. workflow_call_max_depth = dify_config.WORKFLOW_CALL_MAX_DEPTH
  65. if call_depth > workflow_call_max_depth:
  66. raise ValueError(f"Max workflow call depth {workflow_call_max_depth} reached.")
  67. # init workflow run state
  68. graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
  69. self.graph_engine = GraphEngine(
  70. tenant_id=tenant_id,
  71. app_id=app_id,
  72. workflow_type=workflow_type,
  73. workflow_id=workflow_id,
  74. user_id=user_id,
  75. user_from=user_from,
  76. invoke_from=invoke_from,
  77. call_depth=call_depth,
  78. graph=graph,
  79. graph_config=graph_config,
  80. graph_runtime_state=graph_runtime_state,
  81. max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS,
  82. max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME,
  83. thread_pool_id=thread_pool_id,
  84. )
  85. def run(
  86. self,
  87. *,
  88. callbacks: Sequence[WorkflowCallback],
  89. ) -> Generator[GraphEngineEvent, None, None]:
  90. """
  91. :param callbacks: workflow callbacks
  92. """
  93. graph_engine = self.graph_engine
  94. try:
  95. # run workflow
  96. generator = graph_engine.run()
  97. for event in generator:
  98. if callbacks:
  99. for callback in callbacks:
  100. callback.on_event(event=event)
  101. yield event
  102. except GenerateTaskStoppedError:
  103. pass
  104. except Exception as e:
  105. logger.exception("Unknown Error when workflow entry running")
  106. if callbacks:
  107. for callback in callbacks:
  108. callback.on_event(event=GraphRunFailedEvent(error=str(e)))
  109. return
  110. @classmethod
  111. def single_step_run(
  112. cls,
  113. *,
  114. workflow: Workflow,
  115. node_id: str,
  116. user_id: str,
  117. user_inputs: Mapping[str, Any],
  118. variable_pool: VariablePool,
  119. variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
  120. ) -> tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]:
  121. """
  122. Single step run workflow node
  123. :param workflow: Workflow instance
  124. :param node_id: node id
  125. :param user_id: user id
  126. :param user_inputs: user inputs
  127. :return:
  128. """
  129. node_config = workflow.get_node_config_by_id(node_id)
  130. node_config_data = node_config.get("data", {})
  131. # Get node class
  132. node_type = NodeType(node_config_data.get("type"))
  133. node_version = node_config_data.get("version", "1")
  134. node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version]
  135. # init graph
  136. graph = Graph.init(graph_config=workflow.graph_dict)
  137. # init workflow run state
  138. node = node_cls(
  139. id=str(uuid.uuid4()),
  140. config=node_config,
  141. graph_init_params=GraphInitParams(
  142. tenant_id=workflow.tenant_id,
  143. app_id=workflow.app_id,
  144. workflow_type=WorkflowType.value_of(workflow.type),
  145. workflow_id=workflow.id,
  146. graph_config=workflow.graph_dict,
  147. user_id=user_id,
  148. user_from=UserFrom.ACCOUNT,
  149. invoke_from=InvokeFrom.DEBUGGER,
  150. call_depth=0,
  151. ),
  152. graph=graph,
  153. graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
  154. )
  155. node.init_node_data(node_config_data)
  156. try:
  157. # variable selector to variable mapping
  158. variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
  159. graph_config=workflow.graph_dict, config=node_config
  160. )
  161. except NotImplementedError:
  162. variable_mapping = {}
  163. # Loading missing variable from draft var here, and set it into
  164. # variable_pool.
  165. load_into_variable_pool(
  166. variable_loader=variable_loader,
  167. variable_pool=variable_pool,
  168. variable_mapping=variable_mapping,
  169. user_inputs=user_inputs,
  170. )
  171. cls.mapping_user_inputs_to_variable_pool(
  172. variable_mapping=variable_mapping,
  173. user_inputs=user_inputs,
  174. variable_pool=variable_pool,
  175. tenant_id=workflow.tenant_id,
  176. )
  177. try:
  178. # run node
  179. generator = node.run()
  180. except Exception as e:
  181. logger.exception(
  182. "error while running node, workflow_id=%s, node_id=%s, node_type=%s, node_version=%s",
  183. workflow.id,
  184. node.id,
  185. node.type_,
  186. node.version(),
  187. )
  188. raise WorkflowNodeRunFailedError(node=node, err_msg=str(e))
  189. return node, generator
  190. @classmethod
  191. def run_free_node(
  192. cls, node_data: dict, node_id: str, tenant_id: str, user_id: str, user_inputs: dict[str, Any]
  193. ) -> tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]:
  194. """
  195. Run free node
  196. NOTE: only parameter_extractor/question_classifier are supported
  197. :param node_data: node data
  198. :param node_id: node id
  199. :param tenant_id: tenant id
  200. :param user_id: user id
  201. :param user_inputs: user inputs
  202. :return:
  203. """
  204. # generate a fake graph
  205. node_config = {"id": node_id, "width": 114, "height": 514, "type": "custom", "data": node_data}
  206. start_node_config = {
  207. "id": "start",
  208. "width": 114,
  209. "height": 514,
  210. "type": "custom",
  211. "data": {
  212. "type": NodeType.START.value,
  213. "title": "Start",
  214. "desc": "Start",
  215. },
  216. }
  217. graph_dict = {
  218. "nodes": [start_node_config, node_config],
  219. "edges": [
  220. {
  221. "source": "start",
  222. "target": node_id,
  223. "sourceHandle": "source",
  224. "targetHandle": "target",
  225. }
  226. ],
  227. }
  228. node_type = NodeType(node_data.get("type", ""))
  229. if node_type not in {NodeType.PARAMETER_EXTRACTOR, NodeType.QUESTION_CLASSIFIER}:
  230. raise ValueError(f"Node type {node_type} not supported")
  231. node_cls = NODE_TYPE_CLASSES_MAPPING[node_type]["1"]
  232. if not node_cls:
  233. raise ValueError(f"Node class not found for node type {node_type}")
  234. graph = Graph.init(graph_config=graph_dict)
  235. # init variable pool
  236. variable_pool = VariablePool(
  237. system_variables=SystemVariable.empty(),
  238. user_inputs={},
  239. environment_variables=[],
  240. )
  241. # init workflow run state
  242. node: BaseNode = node_cls(
  243. id=str(uuid.uuid4()),
  244. config=node_config,
  245. graph_init_params=GraphInitParams(
  246. tenant_id=tenant_id,
  247. app_id="",
  248. workflow_type=WorkflowType.WORKFLOW,
  249. workflow_id="",
  250. graph_config=graph_dict,
  251. user_id=user_id,
  252. user_from=UserFrom.ACCOUNT,
  253. invoke_from=InvokeFrom.DEBUGGER,
  254. call_depth=0,
  255. ),
  256. graph=graph,
  257. graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
  258. )
  259. node.init_node_data(node_data)
  260. try:
  261. # variable selector to variable mapping
  262. try:
  263. variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
  264. graph_config=graph_dict, config=node_config
  265. )
  266. except NotImplementedError:
  267. variable_mapping = {}
  268. cls.mapping_user_inputs_to_variable_pool(
  269. variable_mapping=variable_mapping,
  270. user_inputs=user_inputs,
  271. variable_pool=variable_pool,
  272. tenant_id=tenant_id,
  273. )
  274. # run node
  275. generator = node.run()
  276. return node, generator
  277. except Exception as e:
  278. logger.exception(
  279. "error while running node, node_id=%s, node_type=%s, node_version=%s",
  280. node.id,
  281. node.type_,
  282. node.version(),
  283. )
  284. raise WorkflowNodeRunFailedError(node=node, err_msg=str(e))
  285. @staticmethod
  286. def handle_special_values(value: Optional[Mapping[str, Any]]) -> Mapping[str, Any] | None:
  287. # NOTE(QuantumGhost): Avoid using this function in new code.
  288. # Keep values structured as long as possible and only convert to dict
  289. # immediately before serialization (e.g., JSON serialization) to maintain
  290. # data integrity and type information.
  291. result = WorkflowEntry._handle_special_values(value)
  292. return result if isinstance(result, Mapping) or result is None else dict(result)
  293. @staticmethod
  294. def _handle_special_values(value: Any) -> Any:
  295. if value is None:
  296. return value
  297. if isinstance(value, dict):
  298. res = {}
  299. for k, v in value.items():
  300. res[k] = WorkflowEntry._handle_special_values(v)
  301. return res
  302. if isinstance(value, list):
  303. res_list = []
  304. for item in value:
  305. res_list.append(WorkflowEntry._handle_special_values(item))
  306. return res_list
  307. if isinstance(value, File):
  308. return value.to_dict()
  309. return value
  310. @classmethod
  311. def mapping_user_inputs_to_variable_pool(
  312. cls,
  313. *,
  314. variable_mapping: Mapping[str, Sequence[str]],
  315. user_inputs: Mapping[str, Any],
  316. variable_pool: VariablePool,
  317. tenant_id: str,
  318. ) -> None:
  319. # NOTE(QuantumGhost): This logic should remain synchronized with
  320. # the implementation of `load_into_variable_pool`, specifically the logic about
  321. # variable existence checking.
  322. # WARNING(QuantumGhost): The semantics of this method are not clearly defined,
  323. # and multiple parts of the codebase depend on its current behavior.
  324. # Modify with caution.
  325. for node_variable, variable_selector in variable_mapping.items():
  326. # fetch node id and variable key from node_variable
  327. node_variable_list = node_variable.split(".")
  328. if len(node_variable_list) < 1:
  329. raise ValueError(f"Invalid node variable {node_variable}")
  330. node_variable_key = ".".join(node_variable_list[1:])
  331. if (node_variable_key not in user_inputs and node_variable not in user_inputs) and not variable_pool.get(
  332. variable_selector
  333. ):
  334. raise ValueError(f"Variable key {node_variable} not found in user inputs.")
  335. # environment variable already exist in variable pool, not from user inputs
  336. if variable_pool.get(variable_selector):
  337. continue
  338. # fetch variable node id from variable selector
  339. variable_node_id = variable_selector[0]
  340. variable_key_list = variable_selector[1:]
  341. variable_key_list = list(variable_key_list)
  342. # get input value
  343. input_value = user_inputs.get(node_variable)
  344. if not input_value:
  345. input_value = user_inputs.get(node_variable_key)
  346. if isinstance(input_value, dict) and "type" in input_value and "transfer_method" in input_value:
  347. input_value = file_factory.build_from_mapping(mapping=input_value, tenant_id=tenant_id)
  348. if (
  349. isinstance(input_value, list)
  350. and all(isinstance(item, dict) for item in input_value)
  351. and all("type" in item and "transfer_method" in item for item in input_value)
  352. ):
  353. input_value = file_factory.build_from_mappings(mappings=input_value, tenant_id=tenant_id)
  354. # append variable and value to variable pool
  355. if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID:
  356. variable_pool.add([variable_node_id] + variable_key_list, input_value)