Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

workflow_service.py 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. import json
  2. import time
  3. import uuid
  4. from collections.abc import Callable, Generator, Mapping, Sequence
  5. from typing import Any, Optional, cast
  6. from uuid import uuid4
  7. from sqlalchemy import exists, select
  8. from sqlalchemy.orm import Session, sessionmaker
  9. from core.app.app_config.entities import VariableEntityType
  10. from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
  11. from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
  12. from core.file import File
  13. from core.repositories import DifyCoreRepositoryFactory
  14. from core.variables import Variable
  15. from core.variables.variables import VariableUnion
  16. from core.workflow.entities.node_entities import NodeRunResult
  17. from core.workflow.entities.variable_pool import VariablePool
  18. from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution, WorkflowNodeExecutionStatus
  19. from core.workflow.errors import WorkflowNodeRunFailedError
  20. from core.workflow.graph_engine.entities.event import InNodeEvent
  21. from core.workflow.nodes import NodeType
  22. from core.workflow.nodes.base.node import BaseNode
  23. from core.workflow.nodes.enums import ErrorStrategy
  24. from core.workflow.nodes.event import RunCompletedEvent
  25. from core.workflow.nodes.event.types import NodeEvent
  26. from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
  27. from core.workflow.nodes.start.entities import StartNodeData
  28. from core.workflow.system_variable import SystemVariable
  29. from core.workflow.workflow_entry import WorkflowEntry
  30. from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated
  31. from extensions.ext_database import db
  32. from factories.file_factory import build_from_mapping, build_from_mappings
  33. from libs.datetime_utils import naive_utc_now
  34. from models.account import Account
  35. from models.model import App, AppMode
  36. from models.tools import WorkflowToolProvider
  37. from models.workflow import (
  38. Workflow,
  39. WorkflowNodeExecutionModel,
  40. WorkflowNodeExecutionTriggeredFrom,
  41. WorkflowType,
  42. )
  43. from repositories.factory import DifyAPIRepositoryFactory
  44. from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError
  45. from services.workflow.workflow_converter import WorkflowConverter
  46. from .errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError
  47. from .workflow_draft_variable_service import (
  48. DraftVariableSaver,
  49. DraftVarLoader,
  50. WorkflowDraftVariableService,
  51. )
  52. class WorkflowService:
  53. """
  54. Workflow Service
  55. """
  56. def __init__(self, session_maker: sessionmaker | None = None):
  57. """Initialize WorkflowService with repository dependencies."""
  58. if session_maker is None:
  59. session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
  60. self._node_execution_service_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository(
  61. session_maker
  62. )
  63. def get_node_last_run(self, app_model: App, workflow: Workflow, node_id: str) -> WorkflowNodeExecutionModel | None:
  64. """
  65. Get the most recent execution for a specific node.
  66. Args:
  67. app_model: The application model
  68. workflow: The workflow model
  69. node_id: The node identifier
  70. Returns:
  71. The most recent WorkflowNodeExecutionModel for the node, or None if not found
  72. """
  73. return self._node_execution_service_repo.get_node_last_execution(
  74. tenant_id=app_model.tenant_id,
  75. app_id=app_model.id,
  76. workflow_id=workflow.id,
  77. node_id=node_id,
  78. )
  79. def is_workflow_exist(self, app_model: App) -> bool:
  80. stmt = select(
  81. exists().where(
  82. Workflow.tenant_id == app_model.tenant_id,
  83. Workflow.app_id == app_model.id,
  84. Workflow.version == Workflow.VERSION_DRAFT,
  85. )
  86. )
  87. return db.session.execute(stmt).scalar_one()
  88. def get_draft_workflow(self, app_model: App, workflow_id: Optional[str] = None) -> Optional[Workflow]:
  89. """
  90. Get draft workflow
  91. """
  92. if workflow_id:
  93. return self.get_published_workflow_by_id(app_model, workflow_id)
  94. # fetch draft workflow by app_model
  95. workflow = (
  96. db.session.query(Workflow)
  97. .where(
  98. Workflow.tenant_id == app_model.tenant_id,
  99. Workflow.app_id == app_model.id,
  100. Workflow.version == Workflow.VERSION_DRAFT,
  101. )
  102. .first()
  103. )
  104. # return draft workflow
  105. return workflow
  106. def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
  107. """
  108. fetch published workflow by workflow_id
  109. """
  110. workflow = (
  111. db.session.query(Workflow)
  112. .where(
  113. Workflow.tenant_id == app_model.tenant_id,
  114. Workflow.app_id == app_model.id,
  115. Workflow.id == workflow_id,
  116. )
  117. .first()
  118. )
  119. if not workflow:
  120. return None
  121. if workflow.version == Workflow.VERSION_DRAFT:
  122. raise IsDraftWorkflowError(
  123. f"Cannot use draft workflow version. Workflow ID: {workflow_id}. "
  124. f"Please use a published workflow version or leave workflow_id empty."
  125. )
  126. return workflow
  127. def get_published_workflow(self, app_model: App) -> Optional[Workflow]:
  128. """
  129. Get published workflow
  130. """
  131. if not app_model.workflow_id:
  132. return None
  133. # fetch published workflow by workflow_id
  134. workflow = (
  135. db.session.query(Workflow)
  136. .where(
  137. Workflow.tenant_id == app_model.tenant_id,
  138. Workflow.app_id == app_model.id,
  139. Workflow.id == app_model.workflow_id,
  140. )
  141. .first()
  142. )
  143. return workflow
  144. def get_all_published_workflow(
  145. self,
  146. *,
  147. session: Session,
  148. app_model: App,
  149. page: int,
  150. limit: int,
  151. user_id: str | None,
  152. named_only: bool = False,
  153. ) -> tuple[Sequence[Workflow], bool]:
  154. """
  155. Get published workflow with pagination
  156. """
  157. if not app_model.workflow_id:
  158. return [], False
  159. stmt = (
  160. select(Workflow)
  161. .where(Workflow.app_id == app_model.id)
  162. .order_by(Workflow.version.desc())
  163. .limit(limit + 1)
  164. .offset((page - 1) * limit)
  165. )
  166. if user_id:
  167. stmt = stmt.where(Workflow.created_by == user_id)
  168. if named_only:
  169. stmt = stmt.where(Workflow.marked_name != "")
  170. workflows = session.scalars(stmt).all()
  171. has_more = len(workflows) > limit
  172. if has_more:
  173. workflows = workflows[:-1]
  174. return workflows, has_more
  175. def sync_draft_workflow(
  176. self,
  177. *,
  178. app_model: App,
  179. graph: dict,
  180. features: dict,
  181. unique_hash: Optional[str],
  182. account: Account,
  183. environment_variables: Sequence[Variable],
  184. conversation_variables: Sequence[Variable],
  185. ) -> Workflow:
  186. """
  187. Sync draft workflow
  188. :raises WorkflowHashNotEqualError
  189. """
  190. # fetch draft workflow by app_model
  191. workflow = self.get_draft_workflow(app_model=app_model)
  192. if workflow and workflow.unique_hash != unique_hash:
  193. raise WorkflowHashNotEqualError()
  194. # validate features structure
  195. self.validate_features_structure(app_model=app_model, features=features)
  196. # create draft workflow if not found
  197. if not workflow:
  198. workflow = Workflow(
  199. tenant_id=app_model.tenant_id,
  200. app_id=app_model.id,
  201. type=WorkflowType.from_app_mode(app_model.mode).value,
  202. version=Workflow.VERSION_DRAFT,
  203. graph=json.dumps(graph),
  204. features=json.dumps(features),
  205. created_by=account.id,
  206. environment_variables=environment_variables,
  207. conversation_variables=conversation_variables,
  208. )
  209. db.session.add(workflow)
  210. # update draft workflow if found
  211. else:
  212. workflow.graph = json.dumps(graph)
  213. workflow.features = json.dumps(features)
  214. workflow.updated_by = account.id
  215. workflow.updated_at = naive_utc_now()
  216. workflow.environment_variables = environment_variables
  217. workflow.conversation_variables = conversation_variables
  218. # commit db session changes
  219. db.session.commit()
  220. # trigger app workflow events
  221. app_draft_workflow_was_synced.send(app_model, synced_draft_workflow=workflow)
  222. # return draft workflow
  223. return workflow
  224. def publish_workflow(
  225. self,
  226. *,
  227. session: Session,
  228. app_model: App,
  229. account: Account,
  230. marked_name: str = "",
  231. marked_comment: str = "",
  232. ) -> Workflow:
  233. draft_workflow_stmt = select(Workflow).where(
  234. Workflow.tenant_id == app_model.tenant_id,
  235. Workflow.app_id == app_model.id,
  236. Workflow.version == Workflow.VERSION_DRAFT,
  237. )
  238. draft_workflow = session.scalar(draft_workflow_stmt)
  239. if not draft_workflow:
  240. raise ValueError("No valid workflow found.")
  241. # create new workflow
  242. workflow = Workflow.new(
  243. tenant_id=app_model.tenant_id,
  244. app_id=app_model.id,
  245. type=draft_workflow.type,
  246. version=Workflow.version_from_datetime(naive_utc_now()),
  247. graph=draft_workflow.graph,
  248. features=draft_workflow.features,
  249. created_by=account.id,
  250. environment_variables=draft_workflow.environment_variables,
  251. conversation_variables=draft_workflow.conversation_variables,
  252. marked_name=marked_name,
  253. marked_comment=marked_comment,
  254. )
  255. # commit db session changes
  256. session.add(workflow)
  257. # trigger app workflow events
  258. app_published_workflow_was_updated.send(app_model, published_workflow=workflow)
  259. # return new workflow
  260. return workflow
  261. def get_default_block_configs(self) -> list[dict]:
  262. """
  263. Get default block configs
  264. """
  265. # return default block config
  266. default_block_configs = []
  267. for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values():
  268. node_class = node_class_mapping[LATEST_VERSION]
  269. default_config = node_class.get_default_config()
  270. if default_config:
  271. default_block_configs.append(default_config)
  272. return default_block_configs
  273. def get_default_block_config(self, node_type: str, filters: Optional[dict] = None) -> Optional[dict]:
  274. """
  275. Get default config of node.
  276. :param node_type: node type
  277. :param filters: filter by node config parameters.
  278. :return:
  279. """
  280. node_type_enum = NodeType(node_type)
  281. # return default block config
  282. if node_type_enum not in NODE_TYPE_CLASSES_MAPPING:
  283. return None
  284. node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION]
  285. default_config = node_class.get_default_config(filters=filters)
  286. if not default_config:
  287. return None
  288. return default_config
  289. def run_draft_workflow_node(
  290. self,
  291. app_model: App,
  292. draft_workflow: Workflow,
  293. node_id: str,
  294. user_inputs: Mapping[str, Any],
  295. account: Account,
  296. query: str = "",
  297. files: Sequence[File] | None = None,
  298. ) -> WorkflowNodeExecutionModel:
  299. """
  300. Run draft workflow node
  301. """
  302. files = files or []
  303. with Session(bind=db.engine, expire_on_commit=False) as session, session.begin():
  304. draft_var_srv = WorkflowDraftVariableService(session)
  305. draft_var_srv.prefill_conversation_variable_default_values(draft_workflow)
  306. node_config = draft_workflow.get_node_config_by_id(node_id)
  307. node_type = Workflow.get_node_type_from_node_config(node_config)
  308. node_data = node_config.get("data", {})
  309. if node_type == NodeType.START:
  310. with Session(bind=db.engine) as session, session.begin():
  311. draft_var_srv = WorkflowDraftVariableService(session)
  312. conversation_id = draft_var_srv.get_or_create_conversation(
  313. account_id=account.id,
  314. app=app_model,
  315. workflow=draft_workflow,
  316. )
  317. start_data = StartNodeData.model_validate(node_data)
  318. user_inputs = _rebuild_file_for_user_inputs_in_start_node(
  319. tenant_id=draft_workflow.tenant_id, start_node_data=start_data, user_inputs=user_inputs
  320. )
  321. # init variable pool
  322. variable_pool = _setup_variable_pool(
  323. query=query,
  324. files=files or [],
  325. user_id=account.id,
  326. user_inputs=user_inputs,
  327. workflow=draft_workflow,
  328. # NOTE(QuantumGhost): We rely on `DraftVarLoader` to load conversation variables.
  329. conversation_variables=[],
  330. node_type=node_type,
  331. conversation_id=conversation_id,
  332. )
  333. else:
  334. variable_pool = VariablePool(
  335. system_variables=SystemVariable.empty(),
  336. user_inputs=user_inputs,
  337. environment_variables=draft_workflow.environment_variables,
  338. conversation_variables=[],
  339. )
  340. variable_loader = DraftVarLoader(
  341. engine=db.engine,
  342. app_id=app_model.id,
  343. tenant_id=app_model.tenant_id,
  344. )
  345. enclosing_node_type_and_id = draft_workflow.get_enclosing_node_type_and_id(node_config)
  346. if enclosing_node_type_and_id:
  347. _, enclosing_node_id = enclosing_node_type_and_id
  348. else:
  349. enclosing_node_id = None
  350. run = WorkflowEntry.single_step_run(
  351. workflow=draft_workflow,
  352. node_id=node_id,
  353. user_inputs=user_inputs,
  354. user_id=account.id,
  355. variable_pool=variable_pool,
  356. variable_loader=variable_loader,
  357. )
  358. # run draft workflow node
  359. start_at = time.perf_counter()
  360. node_execution = self._handle_node_run_result(
  361. invoke_node_fn=lambda: run,
  362. start_at=start_at,
  363. node_id=node_id,
  364. )
  365. # Set workflow_id on the NodeExecution
  366. node_execution.workflow_id = draft_workflow.id
  367. # Create repository and save the node execution
  368. repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
  369. session_factory=db.engine,
  370. user=account,
  371. app_id=app_model.id,
  372. triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP,
  373. )
  374. repository.save(node_execution)
  375. workflow_node_execution = self._node_execution_service_repo.get_execution_by_id(node_execution.id)
  376. if workflow_node_execution is None:
  377. raise ValueError(f"WorkflowNodeExecution with id {node_execution.id} not found after saving")
  378. with Session(bind=db.engine) as session, session.begin():
  379. draft_var_saver = DraftVariableSaver(
  380. session=session,
  381. app_id=app_model.id,
  382. node_id=workflow_node_execution.node_id,
  383. node_type=NodeType(workflow_node_execution.node_type),
  384. enclosing_node_id=enclosing_node_id,
  385. node_execution_id=node_execution.id,
  386. )
  387. draft_var_saver.save(process_data=node_execution.process_data, outputs=node_execution.outputs)
  388. session.commit()
  389. return workflow_node_execution
  390. def run_free_workflow_node(
  391. self, node_data: dict, tenant_id: str, user_id: str, node_id: str, user_inputs: dict[str, Any]
  392. ) -> WorkflowNodeExecution:
  393. """
  394. Run free workflow node
  395. """
  396. # run free workflow node
  397. start_at = time.perf_counter()
  398. node_execution = self._handle_node_run_result(
  399. invoke_node_fn=lambda: WorkflowEntry.run_free_node(
  400. node_id=node_id,
  401. node_data=node_data,
  402. tenant_id=tenant_id,
  403. user_id=user_id,
  404. user_inputs=user_inputs,
  405. ),
  406. start_at=start_at,
  407. node_id=node_id,
  408. )
  409. return node_execution
  410. def _handle_node_run_result(
  411. self,
  412. invoke_node_fn: Callable[[], tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]],
  413. start_at: float,
  414. node_id: str,
  415. ) -> WorkflowNodeExecution:
  416. try:
  417. node, node_events = invoke_node_fn()
  418. node_run_result: NodeRunResult | None = None
  419. for event in node_events:
  420. if isinstance(event, RunCompletedEvent):
  421. node_run_result = event.run_result
  422. # sign output files
  423. # node_run_result.outputs = WorkflowEntry.handle_special_values(node_run_result.outputs)
  424. break
  425. if not node_run_result:
  426. raise ValueError("Node run failed with no run result")
  427. # single step debug mode error handling return
  428. if node_run_result.status == WorkflowNodeExecutionStatus.FAILED and node.continue_on_error:
  429. node_error_args: dict[str, Any] = {
  430. "status": WorkflowNodeExecutionStatus.EXCEPTION,
  431. "error": node_run_result.error,
  432. "inputs": node_run_result.inputs,
  433. "metadata": {"error_strategy": node.error_strategy},
  434. }
  435. if node.error_strategy is ErrorStrategy.DEFAULT_VALUE:
  436. node_run_result = NodeRunResult(
  437. **node_error_args,
  438. outputs={
  439. **node.default_value_dict,
  440. "error_message": node_run_result.error,
  441. "error_type": node_run_result.error_type,
  442. },
  443. )
  444. else:
  445. node_run_result = NodeRunResult(
  446. **node_error_args,
  447. outputs={
  448. "error_message": node_run_result.error,
  449. "error_type": node_run_result.error_type,
  450. },
  451. )
  452. run_succeeded = node_run_result.status in (
  453. WorkflowNodeExecutionStatus.SUCCEEDED,
  454. WorkflowNodeExecutionStatus.EXCEPTION,
  455. )
  456. error = node_run_result.error if not run_succeeded else None
  457. except WorkflowNodeRunFailedError as e:
  458. node = e._node
  459. run_succeeded = False
  460. node_run_result = None
  461. error = e._error
  462. # Create a NodeExecution domain model
  463. node_execution = WorkflowNodeExecution(
  464. id=str(uuid4()),
  465. workflow_id="", # This is a single-step execution, so no workflow ID
  466. index=1,
  467. node_id=node_id,
  468. node_type=node.type_,
  469. title=node.title,
  470. elapsed_time=time.perf_counter() - start_at,
  471. created_at=naive_utc_now(),
  472. finished_at=naive_utc_now(),
  473. )
  474. if run_succeeded and node_run_result:
  475. # Set inputs, process_data, and outputs as dictionaries (not JSON strings)
  476. inputs = WorkflowEntry.handle_special_values(node_run_result.inputs) if node_run_result.inputs else None
  477. process_data = (
  478. WorkflowEntry.handle_special_values(node_run_result.process_data)
  479. if node_run_result.process_data
  480. else None
  481. )
  482. outputs = node_run_result.outputs
  483. node_execution.inputs = inputs
  484. node_execution.process_data = process_data
  485. node_execution.outputs = outputs
  486. node_execution.metadata = node_run_result.metadata
  487. # Map status from WorkflowNodeExecutionStatus to NodeExecutionStatus
  488. if node_run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED:
  489. node_execution.status = WorkflowNodeExecutionStatus.SUCCEEDED
  490. elif node_run_result.status == WorkflowNodeExecutionStatus.EXCEPTION:
  491. node_execution.status = WorkflowNodeExecutionStatus.EXCEPTION
  492. node_execution.error = node_run_result.error
  493. else:
  494. # Set failed status and error
  495. node_execution.status = WorkflowNodeExecutionStatus.FAILED
  496. node_execution.error = error
  497. return node_execution
  498. def convert_to_workflow(self, app_model: App, account: Account, args: dict) -> App:
  499. """
  500. Basic mode of chatbot app(expert mode) to workflow
  501. Completion App to Workflow App
  502. :param app_model: App instance
  503. :param account: Account instance
  504. :param args: dict
  505. :return:
  506. """
  507. # chatbot convert to workflow mode
  508. workflow_converter = WorkflowConverter()
  509. if app_model.mode not in {AppMode.CHAT.value, AppMode.COMPLETION.value}:
  510. raise ValueError(f"Current App mode: {app_model.mode} is not supported convert to workflow.")
  511. # convert to workflow
  512. new_app: App = workflow_converter.convert_to_workflow(
  513. app_model=app_model,
  514. account=account,
  515. name=args.get("name", "Default Name"),
  516. icon_type=args.get("icon_type", "emoji"),
  517. icon=args.get("icon", "🤖"),
  518. icon_background=args.get("icon_background", "#FFEAD5"),
  519. )
  520. return new_app
  521. def validate_features_structure(self, app_model: App, features: dict) -> dict:
  522. if app_model.mode == AppMode.ADVANCED_CHAT.value:
  523. return AdvancedChatAppConfigManager.config_validate(
  524. tenant_id=app_model.tenant_id, config=features, only_structure_validate=True
  525. )
  526. elif app_model.mode == AppMode.WORKFLOW.value:
  527. return WorkflowAppConfigManager.config_validate(
  528. tenant_id=app_model.tenant_id, config=features, only_structure_validate=True
  529. )
  530. else:
  531. raise ValueError(f"Invalid app mode: {app_model.mode}")
  532. def update_workflow(
  533. self, *, session: Session, workflow_id: str, tenant_id: str, account_id: str, data: dict
  534. ) -> Optional[Workflow]:
  535. """
  536. Update workflow attributes
  537. :param session: SQLAlchemy database session
  538. :param workflow_id: Workflow ID
  539. :param tenant_id: Tenant ID
  540. :param account_id: Account ID (for permission check)
  541. :param data: Dictionary containing fields to update
  542. :return: Updated workflow or None if not found
  543. """
  544. stmt = select(Workflow).where(Workflow.id == workflow_id, Workflow.tenant_id == tenant_id)
  545. workflow = session.scalar(stmt)
  546. if not workflow:
  547. return None
  548. allowed_fields = ["marked_name", "marked_comment"]
  549. for field, value in data.items():
  550. if field in allowed_fields:
  551. setattr(workflow, field, value)
  552. workflow.updated_by = account_id
  553. workflow.updated_at = naive_utc_now()
  554. return workflow
  555. def delete_workflow(self, *, session: Session, workflow_id: str, tenant_id: str) -> bool:
  556. """
  557. Delete a workflow
  558. :param session: SQLAlchemy database session
  559. :param workflow_id: Workflow ID
  560. :param tenant_id: Tenant ID
  561. :return: True if successful
  562. :raises: ValueError if workflow not found
  563. :raises: WorkflowInUseError if workflow is in use
  564. :raises: DraftWorkflowDeletionError if workflow is a draft version
  565. """
  566. stmt = select(Workflow).where(Workflow.id == workflow_id, Workflow.tenant_id == tenant_id)
  567. workflow = session.scalar(stmt)
  568. if not workflow:
  569. raise ValueError(f"Workflow with ID {workflow_id} not found")
  570. # Check if workflow is a draft version
  571. if workflow.version == Workflow.VERSION_DRAFT:
  572. raise DraftWorkflowDeletionError("Cannot delete draft workflow versions")
  573. # Check if this workflow is currently referenced by an app
  574. app_stmt = select(App).where(App.workflow_id == workflow_id)
  575. app = session.scalar(app_stmt)
  576. if app:
  577. # Cannot delete a workflow that's currently in use by an app
  578. raise WorkflowInUseError(f"Cannot delete workflow that is currently in use by app '{app.id}'")
  579. # Don't use workflow.tool_published as it's not accurate for specific workflow versions
  580. # Check if there's a tool provider using this specific workflow version
  581. tool_provider = (
  582. session.query(WorkflowToolProvider)
  583. .where(
  584. WorkflowToolProvider.tenant_id == workflow.tenant_id,
  585. WorkflowToolProvider.app_id == workflow.app_id,
  586. WorkflowToolProvider.version == workflow.version,
  587. )
  588. .first()
  589. )
  590. if tool_provider:
  591. # Cannot delete a workflow that's published as a tool
  592. raise WorkflowInUseError("Cannot delete workflow that is published as a tool")
  593. session.delete(workflow)
  594. return True
  595. def _setup_variable_pool(
  596. query: str,
  597. files: Sequence[File],
  598. user_id: str,
  599. user_inputs: Mapping[str, Any],
  600. workflow: Workflow,
  601. node_type: NodeType,
  602. conversation_id: str,
  603. conversation_variables: list[Variable],
  604. ):
  605. # Only inject system variables for START node type.
  606. if node_type == NodeType.START:
  607. system_variable = SystemVariable(
  608. user_id=user_id,
  609. app_id=workflow.app_id,
  610. workflow_id=workflow.id,
  611. files=files or [],
  612. workflow_execution_id=str(uuid.uuid4()),
  613. )
  614. # Only add chatflow-specific variables for non-workflow types
  615. if workflow.type != WorkflowType.WORKFLOW.value:
  616. system_variable.query = query
  617. system_variable.conversation_id = conversation_id
  618. system_variable.dialogue_count = 0
  619. else:
  620. system_variable = SystemVariable.empty()
  621. # init variable pool
  622. variable_pool = VariablePool(
  623. system_variables=system_variable,
  624. user_inputs=user_inputs,
  625. environment_variables=workflow.environment_variables,
  626. # Based on the definition of `VariableUnion`,
  627. # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible.
  628. conversation_variables=cast(list[VariableUnion], conversation_variables), #
  629. )
  630. return variable_pool
  631. def _rebuild_file_for_user_inputs_in_start_node(
  632. tenant_id: str, start_node_data: StartNodeData, user_inputs: Mapping[str, Any]
  633. ) -> Mapping[str, Any]:
  634. inputs_copy = dict(user_inputs)
  635. for variable in start_node_data.variables:
  636. if variable.type not in (VariableEntityType.FILE, VariableEntityType.FILE_LIST):
  637. continue
  638. if variable.variable not in user_inputs:
  639. continue
  640. value = user_inputs[variable.variable]
  641. file = _rebuild_single_file(tenant_id=tenant_id, value=value, variable_entity_type=variable.type)
  642. inputs_copy[variable.variable] = file
  643. return inputs_copy
  644. def _rebuild_single_file(tenant_id: str, value: Any, variable_entity_type: VariableEntityType) -> File | Sequence[File]:
  645. if variable_entity_type == VariableEntityType.FILE:
  646. if not isinstance(value, dict):
  647. raise ValueError(f"expected dict for file object, got {type(value)}")
  648. return build_from_mapping(mapping=value, tenant_id=tenant_id)
  649. elif variable_entity_type == VariableEntityType.FILE_LIST:
  650. if not isinstance(value, list):
  651. raise ValueError(f"expected list for file list object, got {type(value)}")
  652. if len(value) == 0:
  653. return []
  654. if not isinstance(value[0], dict):
  655. raise ValueError(f"expected dict for first element in the file list, got {type(value)}")
  656. return build_from_mappings(mappings=value, tenant_id=tenant_id)
  657. else:
  658. raise Exception("unreachable")