|
@@ -0,0 +1,407 @@
|
|
|
+import logging
|
|
|
+
|
|
|
+import httpx
|
|
|
+import yaml # type: ignore
|
|
|
+
|
|
|
+from events.app_event import app_model_config_was_updated, app_was_created
|
|
|
+from extensions.ext_database import db
|
|
|
+from models.account import Account
|
|
|
+from models.model import App, AppMode, AppModelConfig
|
|
|
+from models.workflow import Workflow
|
|
|
+from services.workflow_service import WorkflowService
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+current_dsl_version = "0.1.0"
|
|
|
+dsl_to_dify_version_mapping: dict[str, str] = {
|
|
|
+ "0.1.0": "0.6.0", # dsl version -> from dify version
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+class AppDslService:
|
|
|
+ @classmethod
|
|
|
+ def import_and_create_new_app_from_url(cls, tenant_id: str, url: str, args: dict, account: Account) -> App:
|
|
|
+ """
|
|
|
+ Import app dsl from url and create new app
|
|
|
+ :param tenant_id: tenant id
|
|
|
+ :param url: import url
|
|
|
+ :param args: request args
|
|
|
+ :param account: Account instance
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ max_size = 10 * 1024 * 1024 # 10MB
|
|
|
+ timeout = httpx.Timeout(10.0)
|
|
|
+ with httpx.stream("GET", url.strip(), follow_redirects=True, timeout=timeout) as response:
|
|
|
+ response.raise_for_status()
|
|
|
+ total_size = 0
|
|
|
+ content = b""
|
|
|
+ for chunk in response.iter_bytes():
|
|
|
+ total_size += len(chunk)
|
|
|
+ if total_size > max_size:
|
|
|
+ raise ValueError("File size exceeds the limit of 10MB")
|
|
|
+ content += chunk
|
|
|
+ except httpx.HTTPStatusError as http_err:
|
|
|
+ raise ValueError(f"HTTP error occurred: {http_err}")
|
|
|
+ except httpx.RequestError as req_err:
|
|
|
+ raise ValueError(f"Request error occurred: {req_err}")
|
|
|
+ except Exception as e:
|
|
|
+ raise ValueError(f"Failed to fetch DSL from URL: {e}")
|
|
|
+
|
|
|
+ if not content:
|
|
|
+ raise ValueError("Empty content from url")
|
|
|
+
|
|
|
+ try:
|
|
|
+ data = content.decode("utf-8")
|
|
|
+ except UnicodeDecodeError as e:
|
|
|
+ raise ValueError(f"Error decoding content: {e}")
|
|
|
+
|
|
|
+ return cls.import_and_create_new_app(tenant_id, data, args, account)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, account: Account) -> App:
|
|
|
+ """
|
|
|
+ Import app dsl and create new app
|
|
|
+ :param tenant_id: tenant id
|
|
|
+ :param data: import data
|
|
|
+ :param args: request args
|
|
|
+ :param account: Account instance
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ import_data = yaml.safe_load(data)
|
|
|
+ except yaml.YAMLError:
|
|
|
+ raise ValueError("Invalid YAML format in data argument.")
|
|
|
+
|
|
|
+ # check or repair dsl version
|
|
|
+ import_data = cls._check_or_fix_dsl(import_data)
|
|
|
+
|
|
|
+ app_data = import_data.get('app')
|
|
|
+ if not app_data:
|
|
|
+ raise ValueError("Missing app in data argument")
|
|
|
+
|
|
|
+ # get app basic info
|
|
|
+ name = args.get("name") if args.get("name") else app_data.get('name')
|
|
|
+ description = args.get("description") if args.get("description") else app_data.get('description', '')
|
|
|
+ icon = args.get("icon") if args.get("icon") else app_data.get('icon')
|
|
|
+ icon_background = args.get("icon_background") if args.get("icon_background") \
|
|
|
+ else app_data.get('icon_background')
|
|
|
+
|
|
|
+ # import dsl and create app
|
|
|
+ app_mode = AppMode.value_of(app_data.get('mode'))
|
|
|
+ if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
|
|
|
+ app = cls._import_and_create_new_workflow_based_app(
|
|
|
+ tenant_id=tenant_id,
|
|
|
+ app_mode=app_mode,
|
|
|
+ workflow_data=import_data.get('workflow'),
|
|
|
+ account=account,
|
|
|
+ name=name,
|
|
|
+ description=description,
|
|
|
+ icon=icon,
|
|
|
+ icon_background=icon_background
|
|
|
+ )
|
|
|
+ elif app_mode in [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]:
|
|
|
+ app = cls._import_and_create_new_model_config_based_app(
|
|
|
+ tenant_id=tenant_id,
|
|
|
+ app_mode=app_mode,
|
|
|
+ model_config_data=import_data.get('model_config'),
|
|
|
+ account=account,
|
|
|
+ name=name,
|
|
|
+ description=description,
|
|
|
+ icon=icon,
|
|
|
+ icon_background=icon_background
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ raise ValueError("Invalid app mode")
|
|
|
+
|
|
|
+ return app
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def import_and_overwrite_workflow(cls, app_model: App, data: str, account: Account) -> Workflow:
|
|
|
+ """
|
|
|
+ Import app dsl and overwrite workflow
|
|
|
+ :param app_model: App instance
|
|
|
+ :param data: import data
|
|
|
+ :param account: Account instance
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ import_data = yaml.safe_load(data)
|
|
|
+ except yaml.YAMLError:
|
|
|
+ raise ValueError("Invalid YAML format in data argument.")
|
|
|
+
|
|
|
+ # check or repair dsl version
|
|
|
+ import_data = cls._check_or_fix_dsl(import_data)
|
|
|
+
|
|
|
+ app_data = import_data.get('app')
|
|
|
+ if not app_data:
|
|
|
+ raise ValueError("Missing app in data argument")
|
|
|
+
|
|
|
+ # import dsl and overwrite app
|
|
|
+ app_mode = AppMode.value_of(app_data.get('mode'))
|
|
|
+ if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
|
|
|
+ raise ValueError("Only support import workflow in advanced-chat or workflow app.")
|
|
|
+
|
|
|
+ if app_data.get('mode') != app_model.mode:
|
|
|
+ raise ValueError(
|
|
|
+ f"App mode {app_data.get('mode')} is not matched with current app mode {app_mode.value}")
|
|
|
+
|
|
|
+ return cls._import_and_overwrite_workflow_based_app(
|
|
|
+ app_model=app_model,
|
|
|
+ workflow_data=import_data.get('workflow'),
|
|
|
+ account=account,
|
|
|
+ )
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def export_dsl(cls, app_model: App) -> str:
|
|
|
+ """
|
|
|
+ Export app
|
|
|
+ :param app_model: App instance
|
|
|
+ :return:
|
|
|
+ """
|
|
|
+ app_mode = AppMode.value_of(app_model.mode)
|
|
|
+
|
|
|
+ export_data = {
|
|
|
+ "version": current_dsl_version,
|
|
|
+ "kind": "app",
|
|
|
+ "app": {
|
|
|
+ "name": app_model.name,
|
|
|
+ "mode": app_model.mode,
|
|
|
+ "icon": app_model.icon,
|
|
|
+ "icon_background": app_model.icon_background,
|
|
|
+ "description": app_model.description
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
|
|
|
+ cls._append_workflow_export_data(export_data, app_model)
|
|
|
+ else:
|
|
|
+ cls._append_model_config_export_data(export_data, app_model)
|
|
|
+
|
|
|
+ return yaml.dump(export_data)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _check_or_fix_dsl(cls, import_data: dict) -> dict:
|
|
|
+ """
|
|
|
+ Check or fix dsl
|
|
|
+
|
|
|
+ :param import_data: import data
|
|
|
+ """
|
|
|
+ if not import_data.get('version'):
|
|
|
+ import_data['version'] = "0.1.0"
|
|
|
+
|
|
|
+ if not import_data.get('kind') or import_data.get('kind') != "app":
|
|
|
+ import_data['kind'] = "app"
|
|
|
+
|
|
|
+ if import_data.get('version') != current_dsl_version:
|
|
|
+ # Currently only one DSL version, so no difference checks or compatibility fixes will be performed.
|
|
|
+ logger.warning(f"DSL version {import_data.get('version')} is not compatible "
|
|
|
+ f"with current version {current_dsl_version}, related to "
|
|
|
+ f"Dify version {dsl_to_dify_version_mapping.get(current_dsl_version)}.")
|
|
|
+
|
|
|
+ return import_data
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _import_and_create_new_workflow_based_app(cls,
|
|
|
+ tenant_id: str,
|
|
|
+ app_mode: AppMode,
|
|
|
+ workflow_data: dict,
|
|
|
+ account: Account,
|
|
|
+ name: str,
|
|
|
+ description: str,
|
|
|
+ icon: str,
|
|
|
+ icon_background: str) -> App:
|
|
|
+ """
|
|
|
+ Import app dsl and create new workflow based app
|
|
|
+
|
|
|
+ :param tenant_id: tenant id
|
|
|
+ :param app_mode: app mode
|
|
|
+ :param workflow_data: workflow data
|
|
|
+ :param account: Account instance
|
|
|
+ :param name: app name
|
|
|
+ :param description: app description
|
|
|
+ :param icon: app icon
|
|
|
+ :param icon_background: app icon background
|
|
|
+ """
|
|
|
+ if not workflow_data:
|
|
|
+ raise ValueError("Missing workflow in data argument "
|
|
|
+ "when app mode is advanced-chat or workflow")
|
|
|
+
|
|
|
+ app = cls._create_app(
|
|
|
+ tenant_id=tenant_id,
|
|
|
+ app_mode=app_mode,
|
|
|
+ account=account,
|
|
|
+ name=name,
|
|
|
+ description=description,
|
|
|
+ icon=icon,
|
|
|
+ icon_background=icon_background
|
|
|
+ )
|
|
|
+
|
|
|
+ # init draft workflow
|
|
|
+ workflow_service = WorkflowService()
|
|
|
+ draft_workflow = workflow_service.sync_draft_workflow(
|
|
|
+ app_model=app,
|
|
|
+ graph=workflow_data.get('graph', {}),
|
|
|
+ features=workflow_data.get('../core/app/features', {}),
|
|
|
+ unique_hash=None,
|
|
|
+ account=account
|
|
|
+ )
|
|
|
+ workflow_service.publish_workflow(
|
|
|
+ app_model=app,
|
|
|
+ account=account,
|
|
|
+ draft_workflow=draft_workflow
|
|
|
+ )
|
|
|
+
|
|
|
+ return app
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _import_and_overwrite_workflow_based_app(cls,
|
|
|
+ app_model: App,
|
|
|
+ workflow_data: dict,
|
|
|
+ account: Account) -> Workflow:
|
|
|
+ """
|
|
|
+ Import app dsl and overwrite workflow based app
|
|
|
+
|
|
|
+ :param app_model: App instance
|
|
|
+ :param workflow_data: workflow data
|
|
|
+ :param account: Account instance
|
|
|
+ """
|
|
|
+ if not workflow_data:
|
|
|
+ raise ValueError("Missing workflow in data argument "
|
|
|
+ "when app mode is advanced-chat or workflow")
|
|
|
+
|
|
|
+ # fetch draft workflow by app_model
|
|
|
+ workflow_service = WorkflowService()
|
|
|
+ current_draft_workflow = workflow_service.get_draft_workflow(app_model=app_model)
|
|
|
+ if current_draft_workflow:
|
|
|
+ unique_hash = current_draft_workflow.unique_hash
|
|
|
+ else:
|
|
|
+ unique_hash = None
|
|
|
+
|
|
|
+ # sync draft workflow
|
|
|
+ draft_workflow = workflow_service.sync_draft_workflow(
|
|
|
+ app_model=app_model,
|
|
|
+ graph=workflow_data.get('graph', {}),
|
|
|
+ features=workflow_data.get('features', {}),
|
|
|
+ unique_hash=unique_hash,
|
|
|
+ account=account
|
|
|
+ )
|
|
|
+
|
|
|
+ return draft_workflow
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _import_and_create_new_model_config_based_app(cls,
|
|
|
+ tenant_id: str,
|
|
|
+ app_mode: AppMode,
|
|
|
+ model_config_data: dict,
|
|
|
+ account: Account,
|
|
|
+ name: str,
|
|
|
+ description: str,
|
|
|
+ icon: str,
|
|
|
+ icon_background: str) -> App:
|
|
|
+ """
|
|
|
+ Import app dsl and create new model config based app
|
|
|
+
|
|
|
+ :param tenant_id: tenant id
|
|
|
+ :param app_mode: app mode
|
|
|
+ :param model_config_data: model config data
|
|
|
+ :param account: Account instance
|
|
|
+ :param name: app name
|
|
|
+ :param description: app description
|
|
|
+ :param icon: app icon
|
|
|
+ :param icon_background: app icon background
|
|
|
+ """
|
|
|
+ if not model_config_data:
|
|
|
+ raise ValueError("Missing model_config in data argument "
|
|
|
+ "when app mode is chat, agent-chat or completion")
|
|
|
+
|
|
|
+ app = cls._create_app(
|
|
|
+ tenant_id=tenant_id,
|
|
|
+ app_mode=app_mode,
|
|
|
+ account=account,
|
|
|
+ name=name,
|
|
|
+ description=description,
|
|
|
+ icon=icon,
|
|
|
+ icon_background=icon_background
|
|
|
+ )
|
|
|
+
|
|
|
+ app_model_config = AppModelConfig()
|
|
|
+ app_model_config = app_model_config.from_model_config_dict(model_config_data)
|
|
|
+ app_model_config.app_id = app.id
|
|
|
+
|
|
|
+ db.session.add(app_model_config)
|
|
|
+ db.session.commit()
|
|
|
+
|
|
|
+ app.app_model_config_id = app_model_config.id
|
|
|
+
|
|
|
+ app_model_config_was_updated.send(
|
|
|
+ app,
|
|
|
+ app_model_config=app_model_config
|
|
|
+ )
|
|
|
+
|
|
|
+ return app
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _create_app(cls,
|
|
|
+ tenant_id: str,
|
|
|
+ app_mode: AppMode,
|
|
|
+ account: Account,
|
|
|
+ name: str,
|
|
|
+ description: str,
|
|
|
+ icon: str,
|
|
|
+ icon_background: str) -> App:
|
|
|
+ """
|
|
|
+ Create new app
|
|
|
+
|
|
|
+ :param tenant_id: tenant id
|
|
|
+ :param app_mode: app mode
|
|
|
+ :param account: Account instance
|
|
|
+ :param name: app name
|
|
|
+ :param description: app description
|
|
|
+ :param icon: app icon
|
|
|
+ :param icon_background: app icon background
|
|
|
+ """
|
|
|
+ app = App(
|
|
|
+ tenant_id=tenant_id,
|
|
|
+ mode=app_mode.value,
|
|
|
+ name=name,
|
|
|
+ description=description,
|
|
|
+ icon=icon,
|
|
|
+ icon_background=icon_background,
|
|
|
+ enable_site=True,
|
|
|
+ enable_api=True
|
|
|
+ )
|
|
|
+
|
|
|
+ db.session.add(app)
|
|
|
+ db.session.commit()
|
|
|
+
|
|
|
+ app_was_created.send(app, account=account)
|
|
|
+
|
|
|
+ return app
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _append_workflow_export_data(cls, export_data: dict, app_model: App) -> None:
|
|
|
+ """
|
|
|
+ Append workflow export data
|
|
|
+ :param export_data: export data
|
|
|
+ :param app_model: App instance
|
|
|
+ """
|
|
|
+ workflow_service = WorkflowService()
|
|
|
+ workflow = workflow_service.get_draft_workflow(app_model)
|
|
|
+ if not workflow:
|
|
|
+ raise ValueError("Missing draft workflow configuration, please check.")
|
|
|
+
|
|
|
+ export_data['workflow'] = {
|
|
|
+ "graph": workflow.graph_dict,
|
|
|
+ "features": workflow.features_dict
|
|
|
+ }
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
|
|
|
+ """
|
|
|
+ Append model config export data
|
|
|
+ :param export_data: export data
|
|
|
+ :param app_model: App instance
|
|
|
+ """
|
|
|
+ app_model_config = app_model.app_model_config
|
|
|
+ if not app_model_config:
|
|
|
+ raise ValueError("Missing app configuration, please check.")
|
|
|
+
|
|
|
+ export_data['model_config'] = app_model_config.to_dict()
|