- 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/<uuid:app_id>")
- 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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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"}
|