app_dsl_service.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import logging
  2. import httpx
  3. import yaml # type: ignore
  4. from events.app_event import app_model_config_was_updated, app_was_created
  5. from extensions.ext_database import db
  6. from models.account import Account
  7. from models.model import App, AppMode, AppModelConfig
  8. from models.workflow import Workflow
  9. from services.workflow_service import WorkflowService
  10. logger = logging.getLogger(__name__)
  11. current_dsl_version = "0.1.0"
  12. dsl_to_dify_version_mapping: dict[str, str] = {
  13. "0.1.0": "0.6.0", # dsl version -> from dify version
  14. }
  15. class AppDslService:
  16. @classmethod
  17. def import_and_create_new_app_from_url(cls, tenant_id: str, url: str, args: dict, account: Account) -> App:
  18. """
  19. Import app dsl from url and create new app
  20. :param tenant_id: tenant id
  21. :param url: import url
  22. :param args: request args
  23. :param account: Account instance
  24. """
  25. try:
  26. max_size = 10 * 1024 * 1024 # 10MB
  27. timeout = httpx.Timeout(10.0)
  28. with httpx.stream("GET", url.strip(), follow_redirects=True, timeout=timeout) as response:
  29. response.raise_for_status()
  30. total_size = 0
  31. content = b""
  32. for chunk in response.iter_bytes():
  33. total_size += len(chunk)
  34. if total_size > max_size:
  35. raise ValueError("File size exceeds the limit of 10MB")
  36. content += chunk
  37. except httpx.HTTPStatusError as http_err:
  38. raise ValueError(f"HTTP error occurred: {http_err}")
  39. except httpx.RequestError as req_err:
  40. raise ValueError(f"Request error occurred: {req_err}")
  41. except Exception as e:
  42. raise ValueError(f"Failed to fetch DSL from URL: {e}")
  43. if not content:
  44. raise ValueError("Empty content from url")
  45. try:
  46. data = content.decode("utf-8")
  47. except UnicodeDecodeError as e:
  48. raise ValueError(f"Error decoding content: {e}")
  49. return cls.import_and_create_new_app(tenant_id, data, args, account)
  50. @classmethod
  51. def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, account: Account) -> App:
  52. """
  53. Import app dsl and create new app
  54. :param tenant_id: tenant id
  55. :param data: import data
  56. :param args: request args
  57. :param account: Account instance
  58. """
  59. try:
  60. import_data = yaml.safe_load(data)
  61. except yaml.YAMLError:
  62. raise ValueError("Invalid YAML format in data argument.")
  63. # check or repair dsl version
  64. import_data = cls._check_or_fix_dsl(import_data)
  65. app_data = import_data.get('app')
  66. if not app_data:
  67. raise ValueError("Missing app in data argument")
  68. # get app basic info
  69. name = args.get("name") if args.get("name") else app_data.get('name')
  70. description = args.get("description") if args.get("description") else app_data.get('description', '')
  71. icon = args.get("icon") if args.get("icon") else app_data.get('icon')
  72. icon_background = args.get("icon_background") if args.get("icon_background") \
  73. else app_data.get('icon_background')
  74. # import dsl and create app
  75. app_mode = AppMode.value_of(app_data.get('mode'))
  76. if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  77. app = cls._import_and_create_new_workflow_based_app(
  78. tenant_id=tenant_id,
  79. app_mode=app_mode,
  80. workflow_data=import_data.get('workflow'),
  81. account=account,
  82. name=name,
  83. description=description,
  84. icon=icon,
  85. icon_background=icon_background
  86. )
  87. elif app_mode in [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]:
  88. app = cls._import_and_create_new_model_config_based_app(
  89. tenant_id=tenant_id,
  90. app_mode=app_mode,
  91. model_config_data=import_data.get('model_config'),
  92. account=account,
  93. name=name,
  94. description=description,
  95. icon=icon,
  96. icon_background=icon_background
  97. )
  98. else:
  99. raise ValueError("Invalid app mode")
  100. return app
  101. @classmethod
  102. def import_and_overwrite_workflow(cls, app_model: App, data: str, account: Account) -> Workflow:
  103. """
  104. Import app dsl and overwrite workflow
  105. :param app_model: App instance
  106. :param data: import data
  107. :param account: Account instance
  108. """
  109. try:
  110. import_data = yaml.safe_load(data)
  111. except yaml.YAMLError:
  112. raise ValueError("Invalid YAML format in data argument.")
  113. # check or repair dsl version
  114. import_data = cls._check_or_fix_dsl(import_data)
  115. app_data = import_data.get('app')
  116. if not app_data:
  117. raise ValueError("Missing app in data argument")
  118. # import dsl and overwrite app
  119. app_mode = AppMode.value_of(app_data.get('mode'))
  120. if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  121. raise ValueError("Only support import workflow in advanced-chat or workflow app.")
  122. if app_data.get('mode') != app_model.mode:
  123. raise ValueError(
  124. f"App mode {app_data.get('mode')} is not matched with current app mode {app_mode.value}")
  125. return cls._import_and_overwrite_workflow_based_app(
  126. app_model=app_model,
  127. workflow_data=import_data.get('workflow'),
  128. account=account,
  129. )
  130. @classmethod
  131. def export_dsl(cls, app_model: App) -> str:
  132. """
  133. Export app
  134. :param app_model: App instance
  135. :return:
  136. """
  137. app_mode = AppMode.value_of(app_model.mode)
  138. export_data = {
  139. "version": current_dsl_version,
  140. "kind": "app",
  141. "app": {
  142. "name": app_model.name,
  143. "mode": app_model.mode,
  144. "icon": app_model.icon,
  145. "icon_background": app_model.icon_background,
  146. "description": app_model.description
  147. }
  148. }
  149. if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  150. cls._append_workflow_export_data(export_data, app_model)
  151. else:
  152. cls._append_model_config_export_data(export_data, app_model)
  153. return yaml.dump(export_data)
  154. @classmethod
  155. def _check_or_fix_dsl(cls, import_data: dict) -> dict:
  156. """
  157. Check or fix dsl
  158. :param import_data: import data
  159. """
  160. if not import_data.get('version'):
  161. import_data['version'] = "0.1.0"
  162. if not import_data.get('kind') or import_data.get('kind') != "app":
  163. import_data['kind'] = "app"
  164. if import_data.get('version') != current_dsl_version:
  165. # Currently only one DSL version, so no difference checks or compatibility fixes will be performed.
  166. logger.warning(f"DSL version {import_data.get('version')} is not compatible "
  167. f"with current version {current_dsl_version}, related to "
  168. f"Dify version {dsl_to_dify_version_mapping.get(current_dsl_version)}.")
  169. return import_data
  170. @classmethod
  171. def _import_and_create_new_workflow_based_app(cls,
  172. tenant_id: str,
  173. app_mode: AppMode,
  174. workflow_data: dict,
  175. account: Account,
  176. name: str,
  177. description: str,
  178. icon: str,
  179. icon_background: str) -> App:
  180. """
  181. Import app dsl and create new workflow based app
  182. :param tenant_id: tenant id
  183. :param app_mode: app mode
  184. :param workflow_data: workflow data
  185. :param account: Account instance
  186. :param name: app name
  187. :param description: app description
  188. :param icon: app icon
  189. :param icon_background: app icon background
  190. """
  191. if not workflow_data:
  192. raise ValueError("Missing workflow in data argument "
  193. "when app mode is advanced-chat or workflow")
  194. app = cls._create_app(
  195. tenant_id=tenant_id,
  196. app_mode=app_mode,
  197. account=account,
  198. name=name,
  199. description=description,
  200. icon=icon,
  201. icon_background=icon_background
  202. )
  203. # init draft workflow
  204. workflow_service = WorkflowService()
  205. draft_workflow = workflow_service.sync_draft_workflow(
  206. app_model=app,
  207. graph=workflow_data.get('graph', {}),
  208. features=workflow_data.get('../core/app/features', {}),
  209. unique_hash=None,
  210. account=account
  211. )
  212. workflow_service.publish_workflow(
  213. app_model=app,
  214. account=account,
  215. draft_workflow=draft_workflow
  216. )
  217. return app
  218. @classmethod
  219. def _import_and_overwrite_workflow_based_app(cls,
  220. app_model: App,
  221. workflow_data: dict,
  222. account: Account) -> Workflow:
  223. """
  224. Import app dsl and overwrite workflow based app
  225. :param app_model: App instance
  226. :param workflow_data: workflow data
  227. :param account: Account instance
  228. """
  229. if not workflow_data:
  230. raise ValueError("Missing workflow in data argument "
  231. "when app mode is advanced-chat or workflow")
  232. # fetch draft workflow by app_model
  233. workflow_service = WorkflowService()
  234. current_draft_workflow = workflow_service.get_draft_workflow(app_model=app_model)
  235. if current_draft_workflow:
  236. unique_hash = current_draft_workflow.unique_hash
  237. else:
  238. unique_hash = None
  239. # sync draft workflow
  240. draft_workflow = workflow_service.sync_draft_workflow(
  241. app_model=app_model,
  242. graph=workflow_data.get('graph', {}),
  243. features=workflow_data.get('features', {}),
  244. unique_hash=unique_hash,
  245. account=account
  246. )
  247. return draft_workflow
  248. @classmethod
  249. def _import_and_create_new_model_config_based_app(cls,
  250. tenant_id: str,
  251. app_mode: AppMode,
  252. model_config_data: dict,
  253. account: Account,
  254. name: str,
  255. description: str,
  256. icon: str,
  257. icon_background: str) -> App:
  258. """
  259. Import app dsl and create new model config based app
  260. :param tenant_id: tenant id
  261. :param app_mode: app mode
  262. :param model_config_data: model config data
  263. :param account: Account instance
  264. :param name: app name
  265. :param description: app description
  266. :param icon: app icon
  267. :param icon_background: app icon background
  268. """
  269. if not model_config_data:
  270. raise ValueError("Missing model_config in data argument "
  271. "when app mode is chat, agent-chat or completion")
  272. app = cls._create_app(
  273. tenant_id=tenant_id,
  274. app_mode=app_mode,
  275. account=account,
  276. name=name,
  277. description=description,
  278. icon=icon,
  279. icon_background=icon_background
  280. )
  281. app_model_config = AppModelConfig()
  282. app_model_config = app_model_config.from_model_config_dict(model_config_data)
  283. app_model_config.app_id = app.id
  284. db.session.add(app_model_config)
  285. db.session.commit()
  286. app.app_model_config_id = app_model_config.id
  287. app_model_config_was_updated.send(
  288. app,
  289. app_model_config=app_model_config
  290. )
  291. return app
  292. @classmethod
  293. def _create_app(cls,
  294. tenant_id: str,
  295. app_mode: AppMode,
  296. account: Account,
  297. name: str,
  298. description: str,
  299. icon: str,
  300. icon_background: str) -> App:
  301. """
  302. Create new app
  303. :param tenant_id: tenant id
  304. :param app_mode: app mode
  305. :param account: Account instance
  306. :param name: app name
  307. :param description: app description
  308. :param icon: app icon
  309. :param icon_background: app icon background
  310. """
  311. app = App(
  312. tenant_id=tenant_id,
  313. mode=app_mode.value,
  314. name=name,
  315. description=description,
  316. icon=icon,
  317. icon_background=icon_background,
  318. enable_site=True,
  319. enable_api=True
  320. )
  321. db.session.add(app)
  322. db.session.commit()
  323. app_was_created.send(app, account=account)
  324. return app
  325. @classmethod
  326. def _append_workflow_export_data(cls, export_data: dict, app_model: App) -> None:
  327. """
  328. Append workflow export data
  329. :param export_data: export data
  330. :param app_model: App instance
  331. """
  332. workflow_service = WorkflowService()
  333. workflow = workflow_service.get_draft_workflow(app_model)
  334. if not workflow:
  335. raise ValueError("Missing draft workflow configuration, please check.")
  336. export_data['workflow'] = {
  337. "graph": workflow.graph_dict,
  338. "features": workflow.features_dict
  339. }
  340. @classmethod
  341. def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
  342. """
  343. Append model config export data
  344. :param export_data: export data
  345. :param app_model: App instance
  346. """
  347. app_model_config = app_model.app_model_config
  348. if not app_model_config:
  349. raise ValueError("Missing app configuration, please check.")
  350. export_data['model_config'] = app_model_config.to_dict()