import uuid from typing import cast from flask_login import current_user from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqparse from sqlalchemy import select from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest, Forbidden, abort from controllers.console import api, console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( account_initialization_required, cloud_edition_billing_resource_check, enterprise_license_required, setup_required, ) from core.ops.ops_trace_manager import OpsTraceManager from extensions.ext_database import db from fields.app_fields import app_detail_fields, app_detail_fields_with_site, app_pagination_fields from libs.login import login_required from models import Account, App from services.app_dsl_service import AppDslService, ImportMode from services.app_service import AppService from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"] def _validate_description_length(description): if description and len(description) > 400: raise ValueError("Description cannot exceed 400 characters.") return description @console_ns.route("/apps") class AppListApi(Resource): @api.doc("list_apps") @api.doc(description="Get list of applications with pagination and filtering") @api.expect( api.parser() .add_argument("page", type=int, location="args", help="Page number (1-99999)", default=1) .add_argument("limit", type=int, location="args", help="Page size (1-100)", default=20) .add_argument( "mode", type=str, location="args", choices=["completion", "chat", "advanced-chat", "workflow", "agent-chat", "channel", "all"], default="all", help="App mode filter", ) .add_argument("name", type=str, location="args", help="Filter by app name") .add_argument("tag_ids", type=str, location="args", help="Comma-separated tag IDs") .add_argument("is_created_by_me", type=bool, location="args", help="Filter by creator") ) @api.response(200, "Success", app_pagination_fields) @setup_required @login_required @account_initialization_required @enterprise_license_required def get(self): """Get app list""" def uuid_list(value): try: return [str(uuid.UUID(v)) for v in value.split(",")] except ValueError: abort(400, message="Invalid UUID format in tag_ids.") parser = reqparse.RequestParser() parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args") parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args") parser.add_argument( "mode", type=str, choices=[ "completion", "chat", "advanced-chat", "workflow", "agent-chat", "channel", "all", ], default="all", location="args", required=False, ) parser.add_argument("name", type=str, location="args", required=False) parser.add_argument("tag_ids", type=uuid_list, location="args", required=False) parser.add_argument("is_created_by_me", type=inputs.boolean, location="args", required=False) args = parser.parse_args() # get app list app_service = AppService() app_pagination = app_service.get_paginate_apps(current_user.id, current_user.current_tenant_id, args) if not app_pagination: return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False} if FeatureService.get_system_features().webapp_auth.enabled: app_ids = [str(app.id) for app in app_pagination.items] res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids) if len(res) != len(app_ids): raise BadRequest("Invalid app id in webapp auth") for app in app_pagination.items: if str(app.id) in res: app.access_mode = res[str(app.id)].access_mode return marshal(app_pagination, app_pagination_fields), 200 @api.doc("create_app") @api.doc(description="Create a new application") @api.expect( api.model( "CreateAppRequest", { "name": fields.String(required=True, description="App name"), "description": fields.String(description="App description (max 400 chars)"), "mode": fields.String(required=True, enum=ALLOW_CREATE_APP_MODES, description="App mode"), "icon_type": fields.String(description="Icon type"), "icon": fields.String(description="Icon"), "icon_background": fields.String(description="Icon background color"), }, ) ) @api.response(201, "App created successfully", app_detail_fields) @api.response(403, "Insufficient permissions") @api.response(400, "Invalid request parameters") @setup_required @login_required @account_initialization_required @marshal_with(app_detail_fields) @cloud_edition_billing_resource_check("apps") def post(self): """Create app""" parser = reqparse.RequestParser() parser.add_argument("name", type=str, required=True, location="json") parser.add_argument("description", type=_validate_description_length, location="json") parser.add_argument("mode", type=str, choices=ALLOW_CREATE_APP_MODES, location="json") parser.add_argument("icon_type", type=str, location="json") parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") args = parser.parse_args() # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() if "mode" not in args or args["mode"] is None: raise BadRequest("mode is required") app_service = AppService() if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") if current_user.current_tenant_id is None: raise ValueError("current_user.current_tenant_id cannot be None") app = app_service.create_app(current_user.current_tenant_id, args, current_user) return app, 201 @console_ns.route("/apps/") class AppApi(Resource): @api.doc("get_app_detail") @api.doc(description="Get application details") @api.doc(params={"app_id": "Application ID"}) @api.response(200, "Success", app_detail_fields_with_site) @setup_required @login_required @account_initialization_required @enterprise_license_required @get_app_model @marshal_with(app_detail_fields_with_site) def get(self, app_model): """Get app detail""" app_service = AppService() app_model = app_service.get_app(app_model) if FeatureService.get_system_features().webapp_auth.enabled: app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) app_model.access_mode = app_setting.access_mode return app_model @api.doc("update_app") @api.doc(description="Update application details") @api.doc(params={"app_id": "Application ID"}) @api.expect( api.model( "UpdateAppRequest", { "name": fields.String(required=True, description="App name"), "description": fields.String(description="App description (max 400 chars)"), "icon_type": fields.String(description="Icon type"), "icon": fields.String(description="Icon"), "icon_background": fields.String(description="Icon background color"), "use_icon_as_answer_icon": fields.Boolean(description="Use icon as answer icon"), "max_active_requests": fields.Integer(description="Maximum active requests"), }, ) ) @api.response(200, "App updated successfully", app_detail_fields_with_site) @api.response(403, "Insufficient permissions") @api.response(400, "Invalid request parameters") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields_with_site) def put(self, app_model): """Update app""" # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("name", type=str, required=True, nullable=False, location="json") parser.add_argument("description", type=_validate_description_length, location="json") parser.add_argument("icon_type", type=str, location="json") parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") parser.add_argument("max_active_requests", type=int, location="json") args = parser.parse_args() app_service = AppService() # Construct ArgsDict from parsed arguments from services.app_service import AppService as AppServiceType args_dict: AppServiceType.ArgsDict = { "name": args["name"], "description": args.get("description", ""), "icon_type": args.get("icon_type", ""), "icon": args.get("icon", ""), "icon_background": args.get("icon_background", ""), "use_icon_as_answer_icon": args.get("use_icon_as_answer_icon", False), "max_active_requests": args.get("max_active_requests", 0), } app_model = app_service.update_app(app_model, args_dict) return app_model @api.doc("delete_app") @api.doc(description="Delete application") @api.doc(params={"app_id": "Application ID"}) @api.response(204, "App deleted successfully") @api.response(403, "Insufficient permissions") @get_app_model @setup_required @login_required @account_initialization_required def delete(self, app_model): """Delete app""" # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() app_service = AppService() app_service.delete_app(app_model) return {"result": "success"}, 204 @console_ns.route("/apps//copy") class AppCopyApi(Resource): @api.doc("copy_app") @api.doc(description="Create a copy of an existing application") @api.doc(params={"app_id": "Application ID to copy"}) @api.expect( api.model( "CopyAppRequest", { "name": fields.String(description="Name for the copied app"), "description": fields.String(description="Description for the copied app"), "icon_type": fields.String(description="Icon type"), "icon": fields.String(description="Icon"), "icon_background": fields.String(description="Icon background color"), }, ) ) @api.response(201, "App copied successfully", app_detail_fields_with_site) @api.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields_with_site) def post(self, app_model): """Copy app""" # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("name", type=str, location="json") parser.add_argument("description", type=_validate_description_length, location="json") parser.add_argument("icon_type", type=str, location="json") parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") args = parser.parse_args() with Session(db.engine) as session: import_service = AppDslService(session) yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True) account = cast(Account, current_user) result = import_service.import_app( account=account, import_mode=ImportMode.YAML_CONTENT.value, yaml_content=yaml_content, name=args.get("name"), description=args.get("description"), icon_type=args.get("icon_type"), icon=args.get("icon"), icon_background=args.get("icon_background"), ) session.commit() stmt = select(App).where(App.id == result.app_id) app = session.scalar(stmt) return app, 201 @console_ns.route("/apps//export") class AppExportApi(Resource): @api.doc("export_app") @api.doc(description="Export application configuration as DSL") @api.doc(params={"app_id": "Application ID to export"}) @api.expect( api.parser() .add_argument("include_secret", type=bool, location="args", default=False, help="Include secrets in export") .add_argument("workflow_id", type=str, location="args", help="Specific workflow ID to export") ) @api.response( 200, "App exported successfully", api.model("AppExportResponse", {"data": fields.String(description="DSL export data")}), ) @api.response(403, "Insufficient permissions") @get_app_model @setup_required @login_required @account_initialization_required def get(self, app_model): """Export app""" # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() # Add include_secret params parser = reqparse.RequestParser() parser.add_argument("include_secret", type=inputs.boolean, default=False, location="args") parser.add_argument("workflow_id", type=str, location="args") args = parser.parse_args() return { "data": AppDslService.export_dsl( app_model=app_model, include_secret=args["include_secret"], workflow_id=args.get("workflow_id") ) } @console_ns.route("/apps//name") class AppNameApi(Resource): @api.doc("check_app_name") @api.doc(description="Check if app name is available") @api.doc(params={"app_id": "Application ID"}) @api.expect(api.parser().add_argument("name", type=str, required=True, location="args", help="Name to check")) @api.response(200, "Name availability checked") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("name", type=str, required=True, location="json") args = parser.parse_args() app_service = AppService() app_model = app_service.update_app_name(app_model, args["name"]) return app_model @console_ns.route("/apps//icon") class AppIconApi(Resource): @api.doc("update_app_icon") @api.doc(description="Update application icon") @api.doc(params={"app_id": "Application ID"}) @api.expect( api.model( "AppIconRequest", { "icon": fields.String(required=True, description="Icon data"), "icon_type": fields.String(description="Icon type"), "icon_background": fields.String(description="Icon background color"), }, ) ) @api.response(200, "Icon updated successfully") @api.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") args = parser.parse_args() app_service = AppService() app_model = app_service.update_app_icon(app_model, args.get("icon") or "", args.get("icon_background") or "") return app_model @console_ns.route("/apps//site-enable") class AppSiteStatus(Resource): @api.doc("update_app_site_status") @api.doc(description="Enable or disable app site") @api.doc(params={"app_id": "Application ID"}) @api.expect( api.model( "AppSiteStatusRequest", {"enable_site": fields.Boolean(required=True, description="Enable or disable site")} ) ) @api.response(200, "Site status updated successfully", app_detail_fields) @api.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("enable_site", type=bool, required=True, location="json") args = parser.parse_args() app_service = AppService() app_model = app_service.update_app_site_status(app_model, args["enable_site"]) return app_model @console_ns.route("/apps//api-enable") class AppApiStatus(Resource): @api.doc("update_app_api_status") @api.doc(description="Enable or disable app API") @api.doc(params={"app_id": "Application ID"}) @api.expect( api.model( "AppApiStatusRequest", {"enable_api": fields.Boolean(required=True, description="Enable or disable API")} ) ) @api.response(200, "API status updated successfully", app_detail_fields) @api.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): # The role of the current user in the ta table must be admin or owner if not current_user.is_admin_or_owner: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("enable_api", type=bool, required=True, location="json") args = parser.parse_args() app_service = AppService() app_model = app_service.update_app_api_status(app_model, args["enable_api"]) return app_model @console_ns.route("/apps//trace") class AppTraceApi(Resource): @api.doc("get_app_trace") @api.doc(description="Get app tracing configuration") @api.doc(params={"app_id": "Application ID"}) @api.response(200, "Trace configuration retrieved successfully") @setup_required @login_required @account_initialization_required def get(self, app_id): """Get app trace""" app_trace_config = OpsTraceManager.get_app_tracing_config(app_id=app_id) return app_trace_config @api.doc("update_app_trace") @api.doc(description="Update app tracing configuration") @api.doc(params={"app_id": "Application ID"}) @api.expect( api.model( "AppTraceRequest", { "enabled": fields.Boolean(required=True, description="Enable or disable tracing"), "tracing_provider": fields.String(required=True, description="Tracing provider"), }, ) ) @api.response(200, "Trace configuration updated successfully") @api.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required def post(self, app_id): # add app trace if not current_user.is_editor: raise Forbidden() parser = reqparse.RequestParser() parser.add_argument("enabled", type=bool, required=True, location="json") parser.add_argument("tracing_provider", type=str, required=True, location="json") args = parser.parse_args() OpsTraceManager.update_app_tracing_config( app_id=app_id, enabled=args["enabled"], tracing_provider=args["tracing_provider"], ) return {"result": "success"}