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.

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