app_dsl_service.py 15 KB

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