workflow_entry.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import logging
  2. import time
  3. import uuid
  4. from collections.abc import Generator, Mapping, Sequence
  5. from typing import Any, Optional, cast
  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. node_cls = cast(type[BaseNode], node_cls)
  242. # init workflow run state
  243. node: BaseNode = node_cls(
  244. id=str(uuid.uuid4()),
  245. config=node_config,
  246. graph_init_params=GraphInitParams(
  247. tenant_id=tenant_id,
  248. app_id="",
  249. workflow_type=WorkflowType.WORKFLOW,
  250. workflow_id="",
  251. graph_config=graph_dict,
  252. user_id=user_id,
  253. user_from=UserFrom.ACCOUNT,
  254. invoke_from=InvokeFrom.DEBUGGER,
  255. call_depth=0,
  256. ),
  257. graph=graph,
  258. graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()),
  259. )
  260. node.init_node_data(node_data)
  261. try:
  262. # variable selector to variable mapping
  263. try:
  264. variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
  265. graph_config=graph_dict, config=node_config
  266. )
  267. except NotImplementedError:
  268. variable_mapping = {}
  269. cls.mapping_user_inputs_to_variable_pool(
  270. variable_mapping=variable_mapping,
  271. user_inputs=user_inputs,
  272. variable_pool=variable_pool,
  273. tenant_id=tenant_id,
  274. )
  275. # run node
  276. generator = node.run()
  277. return node, generator
  278. except Exception as e:
  279. logger.exception(
  280. "error while running node, node_id=%s, node_type=%s, node_version=%s",
  281. node.id,
  282. node.type_,
  283. node.version(),
  284. )
  285. raise WorkflowNodeRunFailedError(node=node, err_msg=str(e))
  286. @staticmethod
  287. def handle_special_values(value: Optional[Mapping[str, Any]]) -> Mapping[str, Any] | None:
  288. # NOTE(QuantumGhost): Avoid using this function in new code.
  289. # Keep values structured as long as possible and only convert to dict
  290. # immediately before serialization (e.g., JSON serialization) to maintain
  291. # data integrity and type information.
  292. result = WorkflowEntry._handle_special_values(value)
  293. return result if isinstance(result, Mapping) or result is None else dict(result)
  294. @staticmethod
  295. def _handle_special_values(value: Any) -> Any:
  296. if value is None:
  297. return value
  298. if isinstance(value, dict):
  299. res = {}
  300. for k, v in value.items():
  301. res[k] = WorkflowEntry._handle_special_values(v)
  302. return res
  303. if isinstance(value, list):
  304. res_list = []
  305. for item in value:
  306. res_list.append(WorkflowEntry._handle_special_values(item))
  307. return res_list
  308. if isinstance(value, File):
  309. return value.to_dict()
  310. return value
  311. @classmethod
  312. def mapping_user_inputs_to_variable_pool(
  313. cls,
  314. *,
  315. variable_mapping: Mapping[str, Sequence[str]],
  316. user_inputs: Mapping[str, Any],
  317. variable_pool: VariablePool,
  318. tenant_id: str,
  319. ) -> None:
  320. # NOTE(QuantumGhost): This logic should remain synchronized with
  321. # the implementation of `load_into_variable_pool`, specifically the logic about
  322. # variable existence checking.
  323. # WARNING(QuantumGhost): The semantics of this method are not clearly defined,
  324. # and multiple parts of the codebase depend on its current behavior.
  325. # Modify with caution.
  326. for node_variable, variable_selector in variable_mapping.items():
  327. # fetch node id and variable key from node_variable
  328. node_variable_list = node_variable.split(".")
  329. if len(node_variable_list) < 1:
  330. raise ValueError(f"Invalid node variable {node_variable}")
  331. node_variable_key = ".".join(node_variable_list[1:])
  332. if (node_variable_key not in user_inputs and node_variable not in user_inputs) and not variable_pool.get(
  333. variable_selector
  334. ):
  335. raise ValueError(f"Variable key {node_variable} not found in user inputs.")
  336. # environment variable already exist in variable pool, not from user inputs
  337. if variable_pool.get(variable_selector):
  338. continue
  339. # fetch variable node id from variable selector
  340. variable_node_id = variable_selector[0]
  341. variable_key_list = variable_selector[1:]
  342. variable_key_list = list(variable_key_list)
  343. # get input value
  344. input_value = user_inputs.get(node_variable)
  345. if not input_value:
  346. input_value = user_inputs.get(node_variable_key)
  347. if isinstance(input_value, dict) and "type" in input_value and "transfer_method" in input_value:
  348. input_value = file_factory.build_from_mapping(mapping=input_value, tenant_id=tenant_id)
  349. if (
  350. isinstance(input_value, list)
  351. and all(isinstance(item, dict) for item in input_value)
  352. and all("type" in item and "transfer_method" in item for item in input_value)
  353. ):
  354. input_value = file_factory.build_from_mappings(mappings=input_value, tenant_id=tenant_id)
  355. # append variable and value to variable pool
  356. if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID:
  357. variable_pool.add([variable_node_id] + variable_key_list, input_value)