app_dsl_service.py 15 KB

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