app_dsl_service.py 16 KB

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