app_dsl_service.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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. use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
  78. # import dsl and create app
  79. app_mode = AppMode.value_of(app_data.get("mode"))
  80. if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  81. app = cls._import_and_create_new_workflow_based_app(
  82. tenant_id=tenant_id,
  83. app_mode=app_mode,
  84. workflow_data=import_data.get("workflow"),
  85. account=account,
  86. name=name,
  87. description=description,
  88. icon_type=icon_type,
  89. icon=icon,
  90. icon_background=icon_background,
  91. use_icon_as_answer_icon=use_icon_as_answer_icon,
  92. )
  93. elif app_mode in [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]:
  94. app = cls._import_and_create_new_model_config_based_app(
  95. tenant_id=tenant_id,
  96. app_mode=app_mode,
  97. model_config_data=import_data.get("model_config"),
  98. account=account,
  99. name=name,
  100. description=description,
  101. icon_type=icon_type,
  102. icon=icon,
  103. icon_background=icon_background,
  104. use_icon_as_answer_icon=use_icon_as_answer_icon,
  105. )
  106. else:
  107. raise ValueError("Invalid app mode")
  108. return app
  109. @classmethod
  110. def import_and_overwrite_workflow(cls, app_model: App, data: str, account: Account) -> Workflow:
  111. """
  112. Import app dsl and overwrite workflow
  113. :param app_model: App instance
  114. :param data: import data
  115. :param account: Account instance
  116. """
  117. try:
  118. import_data = yaml.safe_load(data)
  119. except yaml.YAMLError:
  120. raise ValueError("Invalid YAML format in data argument.")
  121. # check or repair dsl version
  122. import_data = cls._check_or_fix_dsl(import_data)
  123. app_data = import_data.get("app")
  124. if not app_data:
  125. raise ValueError("Missing app in data argument")
  126. # import dsl and overwrite app
  127. app_mode = AppMode.value_of(app_data.get("mode"))
  128. if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  129. raise ValueError("Only support import workflow in advanced-chat or workflow app.")
  130. if app_data.get("mode") != app_model.mode:
  131. raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_mode.value}")
  132. return cls._import_and_overwrite_workflow_based_app(
  133. app_model=app_model,
  134. workflow_data=import_data.get("workflow"),
  135. account=account,
  136. )
  137. @classmethod
  138. def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
  139. """
  140. Export app
  141. :param app_model: App instance
  142. :return:
  143. """
  144. app_mode = AppMode.value_of(app_model.mode)
  145. export_data = {
  146. "version": current_dsl_version,
  147. "kind": "app",
  148. "app": {
  149. "name": app_model.name,
  150. "mode": app_model.mode,
  151. "icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
  152. "icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
  153. "description": app_model.description,
  154. "use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
  155. },
  156. }
  157. if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
  158. cls._append_workflow_export_data(
  159. export_data=export_data, app_model=app_model, include_secret=include_secret
  160. )
  161. else:
  162. cls._append_model_config_export_data(export_data, app_model)
  163. return yaml.dump(export_data, allow_unicode=True)
  164. @classmethod
  165. def _check_or_fix_dsl(cls, import_data: dict) -> dict:
  166. """
  167. Check or fix dsl
  168. :param import_data: import data
  169. """
  170. if not import_data.get("version"):
  171. import_data["version"] = "0.1.0"
  172. if not import_data.get("kind") or import_data.get("kind") != "app":
  173. import_data["kind"] = "app"
  174. if import_data.get("version") != current_dsl_version:
  175. # Currently only one DSL version, so no difference checks or compatibility fixes will be performed.
  176. logger.warning(
  177. f"DSL version {import_data.get('version')} is not compatible "
  178. f"with current version {current_dsl_version}, related to "
  179. f"Dify version {dsl_to_dify_version_mapping.get(current_dsl_version)}."
  180. )
  181. return import_data
  182. @classmethod
  183. def _import_and_create_new_workflow_based_app(
  184. cls,
  185. tenant_id: str,
  186. app_mode: AppMode,
  187. workflow_data: dict,
  188. account: Account,
  189. name: str,
  190. description: str,
  191. icon_type: str,
  192. icon: str,
  193. icon_background: str,
  194. use_icon_as_answer_icon: bool,
  195. ) -> App:
  196. """
  197. Import app dsl and create new workflow based app
  198. :param tenant_id: tenant id
  199. :param app_mode: app mode
  200. :param workflow_data: workflow data
  201. :param account: Account instance
  202. :param name: app name
  203. :param description: app description
  204. :param icon_type: app icon type, "emoji" or "image"
  205. :param icon: app icon
  206. :param icon_background: app icon background
  207. :param use_icon_as_answer_icon: use app icon as answer icon
  208. """
  209. if not workflow_data:
  210. raise ValueError("Missing workflow in data argument " "when app mode is advanced-chat or workflow")
  211. app = cls._create_app(
  212. tenant_id=tenant_id,
  213. app_mode=app_mode,
  214. account=account,
  215. name=name,
  216. description=description,
  217. icon_type=icon_type,
  218. icon=icon,
  219. icon_background=icon_background,
  220. use_icon_as_answer_icon=use_icon_as_answer_icon,
  221. )
  222. # init draft workflow
  223. environment_variables_list = workflow_data.get("environment_variables") or []
  224. environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
  225. conversation_variables_list = workflow_data.get("conversation_variables") or []
  226. conversation_variables = [factory.build_variable_from_mapping(obj) for obj in conversation_variables_list]
  227. workflow_service = WorkflowService()
  228. draft_workflow = workflow_service.sync_draft_workflow(
  229. app_model=app,
  230. graph=workflow_data.get("graph", {}),
  231. features=workflow_data.get("../core/app/features", {}),
  232. unique_hash=None,
  233. account=account,
  234. environment_variables=environment_variables,
  235. conversation_variables=conversation_variables,
  236. )
  237. workflow_service.publish_workflow(app_model=app, account=account, draft_workflow=draft_workflow)
  238. return app
  239. @classmethod
  240. def _import_and_overwrite_workflow_based_app(
  241. cls, app_model: App, workflow_data: dict, account: Account
  242. ) -> Workflow:
  243. """
  244. Import app dsl and overwrite workflow based app
  245. :param app_model: App instance
  246. :param workflow_data: workflow data
  247. :param account: Account instance
  248. """
  249. if not workflow_data:
  250. raise ValueError("Missing workflow in data argument " "when app mode is advanced-chat or workflow")
  251. # fetch draft workflow by app_model
  252. workflow_service = WorkflowService()
  253. current_draft_workflow = workflow_service.get_draft_workflow(app_model=app_model)
  254. if current_draft_workflow:
  255. unique_hash = current_draft_workflow.unique_hash
  256. else:
  257. unique_hash = None
  258. # sync draft workflow
  259. environment_variables_list = workflow_data.get("environment_variables") or []
  260. environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
  261. conversation_variables_list = workflow_data.get("conversation_variables") or []
  262. conversation_variables = [factory.build_variable_from_mapping(obj) for obj in conversation_variables_list]
  263. draft_workflow = workflow_service.sync_draft_workflow(
  264. app_model=app_model,
  265. graph=workflow_data.get("graph", {}),
  266. features=workflow_data.get("features", {}),
  267. unique_hash=unique_hash,
  268. account=account,
  269. environment_variables=environment_variables,
  270. conversation_variables=conversation_variables,
  271. )
  272. return draft_workflow
  273. @classmethod
  274. def _import_and_create_new_model_config_based_app(
  275. cls,
  276. tenant_id: str,
  277. app_mode: AppMode,
  278. model_config_data: dict,
  279. account: Account,
  280. name: str,
  281. description: str,
  282. icon_type: str,
  283. icon: str,
  284. icon_background: str,
  285. use_icon_as_answer_icon: bool,
  286. ) -> App:
  287. """
  288. Import app dsl and create new model config based app
  289. :param tenant_id: tenant id
  290. :param app_mode: app mode
  291. :param model_config_data: model config data
  292. :param account: Account instance
  293. :param name: app name
  294. :param description: app description
  295. :param icon: app icon
  296. :param icon_background: app icon background
  297. """
  298. if not model_config_data:
  299. raise ValueError("Missing model_config in data argument " "when app mode is chat, agent-chat or completion")
  300. app = cls._create_app(
  301. tenant_id=tenant_id,
  302. app_mode=app_mode,
  303. account=account,
  304. name=name,
  305. description=description,
  306. icon_type=icon_type,
  307. icon=icon,
  308. icon_background=icon_background,
  309. use_icon_as_answer_icon=use_icon_as_answer_icon,
  310. )
  311. app_model_config = AppModelConfig()
  312. app_model_config = app_model_config.from_model_config_dict(model_config_data)
  313. app_model_config.app_id = app.id
  314. app_model_config.created_by = account.id
  315. app_model_config.updated_by = account.id
  316. db.session.add(app_model_config)
  317. db.session.commit()
  318. app.app_model_config_id = app_model_config.id
  319. app_model_config_was_updated.send(app, app_model_config=app_model_config)
  320. return app
  321. @classmethod
  322. def _create_app(
  323. cls,
  324. tenant_id: str,
  325. app_mode: AppMode,
  326. account: Account,
  327. name: str,
  328. description: str,
  329. icon_type: str,
  330. icon: str,
  331. icon_background: str,
  332. use_icon_as_answer_icon: bool,
  333. ) -> App:
  334. """
  335. Create new app
  336. :param tenant_id: tenant id
  337. :param app_mode: app mode
  338. :param account: Account instance
  339. :param name: app name
  340. :param description: app description
  341. :param icon_type: app icon type, "emoji" or "image"
  342. :param icon: app icon
  343. :param icon_background: app icon background
  344. :param use_icon_as_answer_icon: use app icon as answer icon
  345. """
  346. app = App(
  347. tenant_id=tenant_id,
  348. mode=app_mode.value,
  349. name=name,
  350. description=description,
  351. icon_type=icon_type,
  352. icon=icon,
  353. icon_background=icon_background,
  354. enable_site=True,
  355. enable_api=True,
  356. use_icon_as_answer_icon=use_icon_as_answer_icon,
  357. created_by=account.id,
  358. updated_by=account.id,
  359. )
  360. db.session.add(app)
  361. db.session.commit()
  362. app_was_created.send(app, account=account)
  363. return app
  364. @classmethod
  365. def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
  366. """
  367. Append workflow export data
  368. :param export_data: export data
  369. :param app_model: App instance
  370. """
  371. workflow_service = WorkflowService()
  372. workflow = workflow_service.get_draft_workflow(app_model)
  373. if not workflow:
  374. raise ValueError("Missing draft workflow configuration, please check.")
  375. export_data["workflow"] = workflow.to_dict(include_secret=include_secret)
  376. @classmethod
  377. def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
  378. """
  379. Append model config export data
  380. :param export_data: export data
  381. :param app_model: App instance
  382. """
  383. app_model_config = app_model.app_model_config
  384. if not app_model_config:
  385. raise ValueError("Missing app configuration, please check.")
  386. export_data["model_config"] = app_model_config.to_dict()