You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

workflow.py 39KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. import json
  2. import logging
  3. from collections.abc import Sequence
  4. from typing import cast
  5. from flask import abort, request
  6. from flask_restx import Resource, fields, inputs, marshal_with, reqparse
  7. from sqlalchemy.orm import Session
  8. from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
  9. import services
  10. from configs import dify_config
  11. from controllers.console import api, console_ns
  12. from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
  13. from controllers.console.app.wraps import get_app_model
  14. from controllers.console.wraps import account_initialization_required, setup_required
  15. from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
  16. from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
  17. from core.app.apps.base_app_queue_manager import AppQueueManager
  18. from core.app.entities.app_invoke_entities import InvokeFrom
  19. from core.file.models import File
  20. from core.helper.trace_id_helper import get_external_trace_id
  21. from extensions.ext_database import db
  22. from factories import file_factory, variable_factory
  23. from fields.workflow_fields import workflow_fields, workflow_pagination_fields
  24. from fields.workflow_run_fields import workflow_run_node_execution_fields
  25. from libs import helper
  26. from libs.helper import TimestampField, uuid_value
  27. from libs.login import current_user, login_required
  28. from models import App
  29. from models.account import Account
  30. from models.model import AppMode
  31. from models.workflow import Workflow
  32. from services.app_generate_service import AppGenerateService
  33. from services.errors.app import WorkflowHashNotEqualError
  34. from services.errors.llm import InvokeRateLimitError
  35. from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
  36. logger = logging.getLogger(__name__)
  37. # TODO(QuantumGhost): Refactor existing node run API to handle file parameter parsing
  38. # at the controller level rather than in the workflow logic. This would improve separation
  39. # of concerns and make the code more maintainable.
  40. def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence[File]:
  41. files = files or []
  42. file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
  43. file_objs: Sequence[File] = []
  44. if file_extra_config is None:
  45. return file_objs
  46. file_objs = file_factory.build_from_mappings(
  47. mappings=files,
  48. tenant_id=workflow.tenant_id,
  49. config=file_extra_config,
  50. )
  51. return file_objs
  52. @console_ns.route("/apps/<uuid:app_id>/workflows/draft")
  53. class DraftWorkflowApi(Resource):
  54. @api.doc("get_draft_workflow")
  55. @api.doc(description="Get draft workflow for an application")
  56. @api.doc(params={"app_id": "Application ID"})
  57. @api.response(200, "Draft workflow retrieved successfully", workflow_fields)
  58. @api.response(404, "Draft workflow not found")
  59. @setup_required
  60. @login_required
  61. @account_initialization_required
  62. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  63. @marshal_with(workflow_fields)
  64. def get(self, app_model: App):
  65. """
  66. Get draft workflow
  67. """
  68. # The role of the current user in the ta table must be admin, owner, or editor
  69. assert isinstance(current_user, Account)
  70. if not current_user.has_edit_permission:
  71. raise Forbidden()
  72. # fetch draft workflow by app_model
  73. workflow_service = WorkflowService()
  74. workflow = workflow_service.get_draft_workflow(app_model=app_model)
  75. if not workflow:
  76. raise DraftWorkflowNotExist()
  77. # return workflow, if not found, return None (initiate graph by frontend)
  78. return workflow
  79. @setup_required
  80. @login_required
  81. @account_initialization_required
  82. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  83. @api.doc("sync_draft_workflow")
  84. @api.doc(description="Sync draft workflow configuration")
  85. @api.expect(
  86. api.model(
  87. "SyncDraftWorkflowRequest",
  88. {
  89. "graph": fields.Raw(required=True, description="Workflow graph configuration"),
  90. "features": fields.Raw(required=True, description="Workflow features configuration"),
  91. "hash": fields.String(description="Workflow hash for validation"),
  92. "environment_variables": fields.List(fields.Raw, required=True, description="Environment variables"),
  93. "conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
  94. },
  95. )
  96. )
  97. @api.response(200, "Draft workflow synced successfully", workflow_fields)
  98. @api.response(400, "Invalid workflow configuration")
  99. @api.response(403, "Permission denied")
  100. def post(self, app_model: App):
  101. """
  102. Sync draft workflow
  103. """
  104. # The role of the current user in the ta table must be admin, owner, or editor
  105. assert isinstance(current_user, Account)
  106. if not current_user.has_edit_permission:
  107. raise Forbidden()
  108. content_type = request.headers.get("Content-Type", "")
  109. if "application/json" in content_type:
  110. parser = reqparse.RequestParser()
  111. parser.add_argument("graph", type=dict, required=True, nullable=False, location="json")
  112. parser.add_argument("features", type=dict, required=True, nullable=False, location="json")
  113. parser.add_argument("hash", type=str, required=False, location="json")
  114. parser.add_argument("environment_variables", type=list, required=True, location="json")
  115. parser.add_argument("conversation_variables", type=list, required=False, location="json")
  116. args = parser.parse_args()
  117. elif "text/plain" in content_type:
  118. try:
  119. data = json.loads(request.data.decode("utf-8"))
  120. if "graph" not in data or "features" not in data:
  121. raise ValueError("graph or features not found in data")
  122. if not isinstance(data.get("graph"), dict) or not isinstance(data.get("features"), dict):
  123. raise ValueError("graph or features is not a dict")
  124. args = {
  125. "graph": data.get("graph"),
  126. "features": data.get("features"),
  127. "hash": data.get("hash"),
  128. "environment_variables": data.get("environment_variables"),
  129. "conversation_variables": data.get("conversation_variables"),
  130. }
  131. except json.JSONDecodeError:
  132. return {"message": "Invalid JSON data"}, 400
  133. else:
  134. abort(415)
  135. if not isinstance(current_user, Account):
  136. raise Forbidden()
  137. workflow_service = WorkflowService()
  138. try:
  139. environment_variables_list = args.get("environment_variables") or []
  140. environment_variables = [
  141. variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
  142. ]
  143. conversation_variables_list = args.get("conversation_variables") or []
  144. conversation_variables = [
  145. variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
  146. ]
  147. workflow = workflow_service.sync_draft_workflow(
  148. app_model=app_model,
  149. graph=args["graph"],
  150. features=args["features"],
  151. unique_hash=args.get("hash"),
  152. account=current_user,
  153. environment_variables=environment_variables,
  154. conversation_variables=conversation_variables,
  155. )
  156. except WorkflowHashNotEqualError:
  157. raise DraftWorkflowNotSync()
  158. return {
  159. "result": "success",
  160. "hash": workflow.unique_hash,
  161. "updated_at": TimestampField().format(workflow.updated_at or workflow.created_at),
  162. }
  163. @console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
  164. class AdvancedChatDraftWorkflowRunApi(Resource):
  165. @api.doc("run_advanced_chat_draft_workflow")
  166. @api.doc(description="Run draft workflow for advanced chat application")
  167. @api.doc(params={"app_id": "Application ID"})
  168. @api.expect(
  169. api.model(
  170. "AdvancedChatWorkflowRunRequest",
  171. {
  172. "query": fields.String(required=True, description="User query"),
  173. "inputs": fields.Raw(description="Input variables"),
  174. "files": fields.List(fields.Raw, description="File uploads"),
  175. "conversation_id": fields.String(description="Conversation ID"),
  176. },
  177. )
  178. )
  179. @api.response(200, "Workflow run started successfully")
  180. @api.response(400, "Invalid request parameters")
  181. @api.response(403, "Permission denied")
  182. @setup_required
  183. @login_required
  184. @account_initialization_required
  185. @get_app_model(mode=[AppMode.ADVANCED_CHAT])
  186. def post(self, app_model: App):
  187. """
  188. Run draft workflow
  189. """
  190. # The role of the current user in the ta table must be admin, owner, or editor
  191. assert isinstance(current_user, Account)
  192. if not current_user.has_edit_permission:
  193. raise Forbidden()
  194. if not isinstance(current_user, Account):
  195. raise Forbidden()
  196. parser = reqparse.RequestParser()
  197. parser.add_argument("inputs", type=dict, location="json")
  198. parser.add_argument("query", type=str, required=True, location="json", default="")
  199. parser.add_argument("files", type=list, location="json")
  200. parser.add_argument("conversation_id", type=uuid_value, location="json")
  201. parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json")
  202. args = parser.parse_args()
  203. external_trace_id = get_external_trace_id(request)
  204. if external_trace_id:
  205. args["external_trace_id"] = external_trace_id
  206. try:
  207. response = AppGenerateService.generate(
  208. app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=True
  209. )
  210. return helper.compact_generate_response(response)
  211. except services.errors.conversation.ConversationNotExistsError:
  212. raise NotFound("Conversation Not Exists.")
  213. except services.errors.conversation.ConversationCompletedError:
  214. raise ConversationCompletedError()
  215. except InvokeRateLimitError as ex:
  216. raise InvokeRateLimitHttpError(ex.description)
  217. except ValueError as e:
  218. raise e
  219. except Exception:
  220. logger.exception("internal server error.")
  221. raise InternalServerError()
  222. @console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run")
  223. class AdvancedChatDraftRunIterationNodeApi(Resource):
  224. @api.doc("run_advanced_chat_draft_iteration_node")
  225. @api.doc(description="Run draft workflow iteration node for advanced chat")
  226. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  227. @api.expect(
  228. api.model(
  229. "IterationNodeRunRequest",
  230. {
  231. "task_id": fields.String(required=True, description="Task ID"),
  232. "inputs": fields.Raw(description="Input variables"),
  233. },
  234. )
  235. )
  236. @api.response(200, "Iteration node run started successfully")
  237. @api.response(403, "Permission denied")
  238. @api.response(404, "Node not found")
  239. @setup_required
  240. @login_required
  241. @account_initialization_required
  242. @get_app_model(mode=[AppMode.ADVANCED_CHAT])
  243. def post(self, app_model: App, node_id: str):
  244. """
  245. Run draft workflow iteration node
  246. """
  247. if not isinstance(current_user, Account):
  248. raise Forbidden()
  249. # The role of the current user in the ta table must be admin, owner, or editor
  250. if not current_user.has_edit_permission:
  251. raise Forbidden()
  252. parser = reqparse.RequestParser()
  253. parser.add_argument("inputs", type=dict, location="json")
  254. args = parser.parse_args()
  255. try:
  256. response = AppGenerateService.generate_single_iteration(
  257. app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
  258. )
  259. return helper.compact_generate_response(response)
  260. except services.errors.conversation.ConversationNotExistsError:
  261. raise NotFound("Conversation Not Exists.")
  262. except services.errors.conversation.ConversationCompletedError:
  263. raise ConversationCompletedError()
  264. except ValueError as e:
  265. raise e
  266. except Exception:
  267. logger.exception("internal server error.")
  268. raise InternalServerError()
  269. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run")
  270. class WorkflowDraftRunIterationNodeApi(Resource):
  271. @api.doc("run_workflow_draft_iteration_node")
  272. @api.doc(description="Run draft workflow iteration node")
  273. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  274. @api.expect(
  275. api.model(
  276. "WorkflowIterationNodeRunRequest",
  277. {
  278. "task_id": fields.String(required=True, description="Task ID"),
  279. "inputs": fields.Raw(description="Input variables"),
  280. },
  281. )
  282. )
  283. @api.response(200, "Workflow iteration node run started successfully")
  284. @api.response(403, "Permission denied")
  285. @api.response(404, "Node not found")
  286. @setup_required
  287. @login_required
  288. @account_initialization_required
  289. @get_app_model(mode=[AppMode.WORKFLOW])
  290. def post(self, app_model: App, node_id: str):
  291. """
  292. Run draft workflow iteration node
  293. """
  294. # The role of the current user in the ta table must be admin, owner, or editor
  295. if not isinstance(current_user, Account):
  296. raise Forbidden()
  297. if not current_user.has_edit_permission:
  298. raise Forbidden()
  299. parser = reqparse.RequestParser()
  300. parser.add_argument("inputs", type=dict, location="json")
  301. args = parser.parse_args()
  302. try:
  303. response = AppGenerateService.generate_single_iteration(
  304. app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
  305. )
  306. return helper.compact_generate_response(response)
  307. except services.errors.conversation.ConversationNotExistsError:
  308. raise NotFound("Conversation Not Exists.")
  309. except services.errors.conversation.ConversationCompletedError:
  310. raise ConversationCompletedError()
  311. except ValueError as e:
  312. raise e
  313. except Exception:
  314. logger.exception("internal server error.")
  315. raise InternalServerError()
  316. @console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run")
  317. class AdvancedChatDraftRunLoopNodeApi(Resource):
  318. @api.doc("run_advanced_chat_draft_loop_node")
  319. @api.doc(description="Run draft workflow loop node for advanced chat")
  320. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  321. @api.expect(
  322. api.model(
  323. "LoopNodeRunRequest",
  324. {
  325. "task_id": fields.String(required=True, description="Task ID"),
  326. "inputs": fields.Raw(description="Input variables"),
  327. },
  328. )
  329. )
  330. @api.response(200, "Loop node run started successfully")
  331. @api.response(403, "Permission denied")
  332. @api.response(404, "Node not found")
  333. @setup_required
  334. @login_required
  335. @account_initialization_required
  336. @get_app_model(mode=[AppMode.ADVANCED_CHAT])
  337. def post(self, app_model: App, node_id: str):
  338. """
  339. Run draft workflow loop node
  340. """
  341. if not isinstance(current_user, Account):
  342. raise Forbidden()
  343. # The role of the current user in the ta table must be admin, owner, or editor
  344. if not current_user.has_edit_permission:
  345. raise Forbidden()
  346. parser = reqparse.RequestParser()
  347. parser.add_argument("inputs", type=dict, location="json")
  348. args = parser.parse_args()
  349. try:
  350. response = AppGenerateService.generate_single_loop(
  351. app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
  352. )
  353. return helper.compact_generate_response(response)
  354. except services.errors.conversation.ConversationNotExistsError:
  355. raise NotFound("Conversation Not Exists.")
  356. except services.errors.conversation.ConversationCompletedError:
  357. raise ConversationCompletedError()
  358. except ValueError as e:
  359. raise e
  360. except Exception:
  361. logger.exception("internal server error.")
  362. raise InternalServerError()
  363. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run")
  364. class WorkflowDraftRunLoopNodeApi(Resource):
  365. @api.doc("run_workflow_draft_loop_node")
  366. @api.doc(description="Run draft workflow loop node")
  367. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  368. @api.expect(
  369. api.model(
  370. "WorkflowLoopNodeRunRequest",
  371. {
  372. "task_id": fields.String(required=True, description="Task ID"),
  373. "inputs": fields.Raw(description="Input variables"),
  374. },
  375. )
  376. )
  377. @api.response(200, "Workflow loop node run started successfully")
  378. @api.response(403, "Permission denied")
  379. @api.response(404, "Node not found")
  380. @setup_required
  381. @login_required
  382. @account_initialization_required
  383. @get_app_model(mode=[AppMode.WORKFLOW])
  384. def post(self, app_model: App, node_id: str):
  385. """
  386. Run draft workflow loop node
  387. """
  388. if not isinstance(current_user, Account):
  389. raise Forbidden()
  390. # The role of the current user in the ta table must be admin, owner, or editor
  391. if not current_user.has_edit_permission:
  392. raise Forbidden()
  393. parser = reqparse.RequestParser()
  394. parser.add_argument("inputs", type=dict, location="json")
  395. args = parser.parse_args()
  396. try:
  397. response = AppGenerateService.generate_single_loop(
  398. app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
  399. )
  400. return helper.compact_generate_response(response)
  401. except services.errors.conversation.ConversationNotExistsError:
  402. raise NotFound("Conversation Not Exists.")
  403. except services.errors.conversation.ConversationCompletedError:
  404. raise ConversationCompletedError()
  405. except ValueError as e:
  406. raise e
  407. except Exception:
  408. logger.exception("internal server error.")
  409. raise InternalServerError()
  410. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/run")
  411. class DraftWorkflowRunApi(Resource):
  412. @api.doc("run_draft_workflow")
  413. @api.doc(description="Run draft workflow")
  414. @api.doc(params={"app_id": "Application ID"})
  415. @api.expect(
  416. api.model(
  417. "DraftWorkflowRunRequest",
  418. {
  419. "inputs": fields.Raw(required=True, description="Input variables"),
  420. "files": fields.List(fields.Raw, description="File uploads"),
  421. },
  422. )
  423. )
  424. @api.response(200, "Draft workflow run started successfully")
  425. @api.response(403, "Permission denied")
  426. @setup_required
  427. @login_required
  428. @account_initialization_required
  429. @get_app_model(mode=[AppMode.WORKFLOW])
  430. def post(self, app_model: App):
  431. """
  432. Run draft workflow
  433. """
  434. if not isinstance(current_user, Account):
  435. raise Forbidden()
  436. # The role of the current user in the ta table must be admin, owner, or editor
  437. if not current_user.has_edit_permission:
  438. raise Forbidden()
  439. parser = reqparse.RequestParser()
  440. parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
  441. parser.add_argument("files", type=list, required=False, location="json")
  442. args = parser.parse_args()
  443. external_trace_id = get_external_trace_id(request)
  444. if external_trace_id:
  445. args["external_trace_id"] = external_trace_id
  446. try:
  447. response = AppGenerateService.generate(
  448. app_model=app_model,
  449. user=current_user,
  450. args=args,
  451. invoke_from=InvokeFrom.DEBUGGER,
  452. streaming=True,
  453. )
  454. return helper.compact_generate_response(response)
  455. except InvokeRateLimitError as ex:
  456. raise InvokeRateLimitHttpError(ex.description)
  457. @console_ns.route("/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
  458. class WorkflowTaskStopApi(Resource):
  459. @api.doc("stop_workflow_task")
  460. @api.doc(description="Stop running workflow task")
  461. @api.doc(params={"app_id": "Application ID", "task_id": "Task ID"})
  462. @api.response(200, "Task stopped successfully")
  463. @api.response(404, "Task not found")
  464. @api.response(403, "Permission denied")
  465. @setup_required
  466. @login_required
  467. @account_initialization_required
  468. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  469. def post(self, app_model: App, task_id: str):
  470. """
  471. Stop workflow task
  472. """
  473. if not isinstance(current_user, Account):
  474. raise Forbidden()
  475. # The role of the current user in the ta table must be admin, owner, or editor
  476. if not current_user.has_edit_permission:
  477. raise Forbidden()
  478. AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
  479. return {"result": "success"}
  480. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run")
  481. class DraftWorkflowNodeRunApi(Resource):
  482. @api.doc("run_draft_workflow_node")
  483. @api.doc(description="Run draft workflow node")
  484. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  485. @api.expect(
  486. api.model(
  487. "DraftWorkflowNodeRunRequest",
  488. {
  489. "inputs": fields.Raw(description="Input variables"),
  490. },
  491. )
  492. )
  493. @api.response(200, "Node run started successfully", workflow_run_node_execution_fields)
  494. @api.response(403, "Permission denied")
  495. @api.response(404, "Node not found")
  496. @setup_required
  497. @login_required
  498. @account_initialization_required
  499. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  500. @marshal_with(workflow_run_node_execution_fields)
  501. def post(self, app_model: App, node_id: str):
  502. """
  503. Run draft workflow node
  504. """
  505. if not isinstance(current_user, Account):
  506. raise Forbidden()
  507. # The role of the current user in the ta table must be admin, owner, or editor
  508. if not current_user.has_edit_permission:
  509. raise Forbidden()
  510. parser = reqparse.RequestParser()
  511. parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
  512. parser.add_argument("query", type=str, required=False, location="json", default="")
  513. parser.add_argument("files", type=list, location="json", default=[])
  514. args = parser.parse_args()
  515. user_inputs = args.get("inputs")
  516. if user_inputs is None:
  517. raise ValueError("missing inputs")
  518. workflow_srv = WorkflowService()
  519. # fetch draft workflow by app_model
  520. draft_workflow = workflow_srv.get_draft_workflow(app_model=app_model)
  521. if not draft_workflow:
  522. raise ValueError("Workflow not initialized")
  523. files = _parse_file(draft_workflow, args.get("files"))
  524. workflow_service = WorkflowService()
  525. workflow_node_execution = workflow_service.run_draft_workflow_node(
  526. app_model=app_model,
  527. draft_workflow=draft_workflow,
  528. node_id=node_id,
  529. user_inputs=user_inputs,
  530. account=current_user,
  531. query=args.get("query", ""),
  532. files=files,
  533. )
  534. return workflow_node_execution
  535. @console_ns.route("/apps/<uuid:app_id>/workflows/publish")
  536. class PublishedWorkflowApi(Resource):
  537. @api.doc("get_published_workflow")
  538. @api.doc(description="Get published workflow for an application")
  539. @api.doc(params={"app_id": "Application ID"})
  540. @api.response(200, "Published workflow retrieved successfully", workflow_fields)
  541. @api.response(404, "Published workflow not found")
  542. @setup_required
  543. @login_required
  544. @account_initialization_required
  545. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  546. @marshal_with(workflow_fields)
  547. def get(self, app_model: App):
  548. """
  549. Get published workflow
  550. """
  551. if not isinstance(current_user, Account):
  552. raise Forbidden()
  553. # The role of the current user in the ta table must be admin, owner, or editor
  554. if not current_user.has_edit_permission:
  555. raise Forbidden()
  556. # fetch published workflow by app_model
  557. workflow_service = WorkflowService()
  558. workflow = workflow_service.get_published_workflow(app_model=app_model)
  559. # return workflow, if not found, return None
  560. return workflow
  561. @setup_required
  562. @login_required
  563. @account_initialization_required
  564. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  565. def post(self, app_model: App):
  566. """
  567. Publish workflow
  568. """
  569. if not isinstance(current_user, Account):
  570. raise Forbidden()
  571. # The role of the current user in the ta table must be admin, owner, or editor
  572. if not current_user.has_edit_permission:
  573. raise Forbidden()
  574. parser = reqparse.RequestParser()
  575. parser.add_argument("marked_name", type=str, required=False, default="", location="json")
  576. parser.add_argument("marked_comment", type=str, required=False, default="", location="json")
  577. args = parser.parse_args()
  578. # Validate name and comment length
  579. if args.marked_name and len(args.marked_name) > 20:
  580. raise ValueError("Marked name cannot exceed 20 characters")
  581. if args.marked_comment and len(args.marked_comment) > 100:
  582. raise ValueError("Marked comment cannot exceed 100 characters")
  583. workflow_service = WorkflowService()
  584. with Session(db.engine) as session:
  585. workflow = workflow_service.publish_workflow(
  586. session=session,
  587. app_model=app_model,
  588. account=current_user,
  589. marked_name=args.marked_name or "",
  590. marked_comment=args.marked_comment or "",
  591. )
  592. app_model.workflow_id = workflow.id
  593. db.session.commit() # NOTE: this is necessary for update app_model.workflow_id
  594. workflow_created_at = TimestampField().format(workflow.created_at)
  595. session.commit()
  596. return {
  597. "result": "success",
  598. "created_at": workflow_created_at,
  599. }
  600. @console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
  601. class DefaultBlockConfigsApi(Resource):
  602. @api.doc("get_default_block_configs")
  603. @api.doc(description="Get default block configurations for workflow")
  604. @api.doc(params={"app_id": "Application ID"})
  605. @api.response(200, "Default block configurations retrieved successfully")
  606. @setup_required
  607. @login_required
  608. @account_initialization_required
  609. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  610. def get(self, app_model: App):
  611. """
  612. Get default block config
  613. """
  614. if not isinstance(current_user, Account):
  615. raise Forbidden()
  616. # The role of the current user in the ta table must be admin, owner, or editor
  617. if not current_user.has_edit_permission:
  618. raise Forbidden()
  619. # Get default block configs
  620. workflow_service = WorkflowService()
  621. return workflow_service.get_default_block_configs()
  622. @console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>")
  623. class DefaultBlockConfigApi(Resource):
  624. @api.doc("get_default_block_config")
  625. @api.doc(description="Get default block configuration by type")
  626. @api.doc(params={"app_id": "Application ID", "block_type": "Block type"})
  627. @api.response(200, "Default block configuration retrieved successfully")
  628. @api.response(404, "Block type not found")
  629. @setup_required
  630. @login_required
  631. @account_initialization_required
  632. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  633. def get(self, app_model: App, block_type: str):
  634. """
  635. Get default block config
  636. """
  637. if not isinstance(current_user, Account):
  638. raise Forbidden()
  639. # The role of the current user in the ta table must be admin, owner, or editor
  640. if not current_user.has_edit_permission:
  641. raise Forbidden()
  642. parser = reqparse.RequestParser()
  643. parser.add_argument("q", type=str, location="args")
  644. args = parser.parse_args()
  645. q = args.get("q")
  646. filters = None
  647. if q:
  648. try:
  649. filters = json.loads(args.get("q", ""))
  650. except json.JSONDecodeError:
  651. raise ValueError("Invalid filters")
  652. # Get default block configs
  653. workflow_service = WorkflowService()
  654. return workflow_service.get_default_block_config(node_type=block_type, filters=filters)
  655. @console_ns.route("/apps/<uuid:app_id>/convert-to-workflow")
  656. class ConvertToWorkflowApi(Resource):
  657. @api.doc("convert_to_workflow")
  658. @api.doc(description="Convert application to workflow mode")
  659. @api.doc(params={"app_id": "Application ID"})
  660. @api.response(200, "Application converted to workflow successfully")
  661. @api.response(400, "Application cannot be converted")
  662. @api.response(403, "Permission denied")
  663. @setup_required
  664. @login_required
  665. @account_initialization_required
  666. @get_app_model(mode=[AppMode.CHAT, AppMode.COMPLETION])
  667. def post(self, app_model: App):
  668. """
  669. Convert basic mode of chatbot app to workflow mode
  670. Convert expert mode of chatbot app to workflow mode
  671. Convert Completion App to Workflow App
  672. """
  673. if not isinstance(current_user, Account):
  674. raise Forbidden()
  675. # The role of the current user in the ta table must be admin, owner, or editor
  676. if not current_user.has_edit_permission:
  677. raise Forbidden()
  678. if request.data:
  679. parser = reqparse.RequestParser()
  680. parser.add_argument("name", type=str, required=False, nullable=True, location="json")
  681. parser.add_argument("icon_type", type=str, required=False, nullable=True, location="json")
  682. parser.add_argument("icon", type=str, required=False, nullable=True, location="json")
  683. parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json")
  684. args = parser.parse_args()
  685. else:
  686. args = {}
  687. # convert to workflow mode
  688. workflow_service = WorkflowService()
  689. new_app_model = workflow_service.convert_to_workflow(app_model=app_model, account=current_user, args=args)
  690. # return app id
  691. return {
  692. "new_app_id": new_app_model.id,
  693. }
  694. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/config")
  695. class WorkflowConfigApi(Resource):
  696. """Resource for workflow configuration."""
  697. @api.doc("get_workflow_config")
  698. @api.doc(description="Get workflow configuration")
  699. @api.doc(params={"app_id": "Application ID"})
  700. @api.response(200, "Workflow configuration retrieved successfully")
  701. @setup_required
  702. @login_required
  703. @account_initialization_required
  704. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  705. def get(self, app_model: App):
  706. return {
  707. "parallel_depth_limit": dify_config.WORKFLOW_PARALLEL_DEPTH_LIMIT,
  708. }
  709. @console_ns.route("/apps/<uuid:app_id>/workflows")
  710. class PublishedAllWorkflowApi(Resource):
  711. @api.doc("get_all_published_workflows")
  712. @api.doc(description="Get all published workflows for an application")
  713. @api.doc(params={"app_id": "Application ID"})
  714. @api.response(200, "Published workflows retrieved successfully", workflow_pagination_fields)
  715. @setup_required
  716. @login_required
  717. @account_initialization_required
  718. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  719. @marshal_with(workflow_pagination_fields)
  720. def get(self, app_model: App):
  721. """
  722. Get published workflows
  723. """
  724. if not isinstance(current_user, Account):
  725. raise Forbidden()
  726. if not current_user.has_edit_permission:
  727. raise Forbidden()
  728. parser = reqparse.RequestParser()
  729. parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
  730. parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
  731. parser.add_argument("user_id", type=str, required=False, location="args")
  732. parser.add_argument("named_only", type=inputs.boolean, required=False, default=False, location="args")
  733. args = parser.parse_args()
  734. page = int(args.get("page", 1))
  735. limit = int(args.get("limit", 10))
  736. user_id = args.get("user_id")
  737. named_only = args.get("named_only", False)
  738. if user_id:
  739. if user_id != current_user.id:
  740. raise Forbidden()
  741. user_id = cast(str, user_id)
  742. workflow_service = WorkflowService()
  743. with Session(db.engine) as session:
  744. workflows, has_more = workflow_service.get_all_published_workflow(
  745. session=session,
  746. app_model=app_model,
  747. page=page,
  748. limit=limit,
  749. user_id=user_id,
  750. named_only=named_only,
  751. )
  752. return {
  753. "items": workflows,
  754. "page": page,
  755. "limit": limit,
  756. "has_more": has_more,
  757. }
  758. @console_ns.route("/apps/<uuid:app_id>/workflows/<string:workflow_id>")
  759. class WorkflowByIdApi(Resource):
  760. @api.doc("update_workflow_by_id")
  761. @api.doc(description="Update workflow by ID")
  762. @api.doc(params={"app_id": "Application ID", "workflow_id": "Workflow ID"})
  763. @api.expect(
  764. api.model(
  765. "UpdateWorkflowRequest",
  766. {
  767. "environment_variables": fields.List(fields.Raw, description="Environment variables"),
  768. "conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
  769. },
  770. )
  771. )
  772. @api.response(200, "Workflow updated successfully", workflow_fields)
  773. @api.response(404, "Workflow not found")
  774. @api.response(403, "Permission denied")
  775. @setup_required
  776. @login_required
  777. @account_initialization_required
  778. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  779. @marshal_with(workflow_fields)
  780. def patch(self, app_model: App, workflow_id: str):
  781. """
  782. Update workflow attributes
  783. """
  784. if not isinstance(current_user, Account):
  785. raise Forbidden()
  786. # Check permission
  787. if not current_user.has_edit_permission:
  788. raise Forbidden()
  789. parser = reqparse.RequestParser()
  790. parser.add_argument("marked_name", type=str, required=False, location="json")
  791. parser.add_argument("marked_comment", type=str, required=False, location="json")
  792. args = parser.parse_args()
  793. # Validate name and comment length
  794. if args.marked_name and len(args.marked_name) > 20:
  795. raise ValueError("Marked name cannot exceed 20 characters")
  796. if args.marked_comment and len(args.marked_comment) > 100:
  797. raise ValueError("Marked comment cannot exceed 100 characters")
  798. # Prepare update data
  799. update_data = {}
  800. if args.get("marked_name") is not None:
  801. update_data["marked_name"] = args["marked_name"]
  802. if args.get("marked_comment") is not None:
  803. update_data["marked_comment"] = args["marked_comment"]
  804. if not update_data:
  805. return {"message": "No valid fields to update"}, 400
  806. workflow_service = WorkflowService()
  807. # Create a session and manage the transaction
  808. with Session(db.engine, expire_on_commit=False) as session:
  809. workflow = workflow_service.update_workflow(
  810. session=session,
  811. workflow_id=workflow_id,
  812. tenant_id=app_model.tenant_id,
  813. account_id=current_user.id,
  814. data=update_data,
  815. )
  816. if not workflow:
  817. raise NotFound("Workflow not found")
  818. # Commit the transaction in the controller
  819. session.commit()
  820. return workflow
  821. @setup_required
  822. @login_required
  823. @account_initialization_required
  824. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  825. def delete(self, app_model: App, workflow_id: str):
  826. """
  827. Delete workflow
  828. """
  829. if not isinstance(current_user, Account):
  830. raise Forbidden()
  831. # Check permission
  832. if not current_user.has_edit_permission:
  833. raise Forbidden()
  834. workflow_service = WorkflowService()
  835. # Create a session and manage the transaction
  836. with Session(db.engine) as session:
  837. try:
  838. workflow_service.delete_workflow(
  839. session=session, workflow_id=workflow_id, tenant_id=app_model.tenant_id
  840. )
  841. # Commit the transaction in the controller
  842. session.commit()
  843. except WorkflowInUseError as e:
  844. abort(400, description=str(e))
  845. except DraftWorkflowDeletionError as e:
  846. abort(400, description=str(e))
  847. except ValueError as e:
  848. raise NotFound(str(e))
  849. return None, 204
  850. @console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/last-run")
  851. class DraftWorkflowNodeLastRunApi(Resource):
  852. @api.doc("get_draft_workflow_node_last_run")
  853. @api.doc(description="Get last run result for draft workflow node")
  854. @api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
  855. @api.response(200, "Node last run retrieved successfully", workflow_run_node_execution_fields)
  856. @api.response(404, "Node last run not found")
  857. @api.response(403, "Permission denied")
  858. @setup_required
  859. @login_required
  860. @account_initialization_required
  861. @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
  862. @marshal_with(workflow_run_node_execution_fields)
  863. def get(self, app_model: App, node_id: str):
  864. srv = WorkflowService()
  865. workflow = srv.get_draft_workflow(app_model)
  866. if not workflow:
  867. raise NotFound("Workflow not found")
  868. node_exec = srv.get_node_last_run(
  869. app_model=app_model,
  870. workflow=workflow,
  871. node_id=node_id,
  872. )
  873. if node_exec is None:
  874. raise NotFound("last run not found")
  875. return node_exec