Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

workflow_service.py 27KB

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