Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

workflow.py 30KB

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