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 31KB

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