app_dsl_service.py 16 KB

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