You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import uuid
  2. from typing import cast
  3. from flask_login import current_user
  4. from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqparse
  5. from sqlalchemy import select
  6. from sqlalchemy.orm import Session
  7. from werkzeug.exceptions import BadRequest, Forbidden, abort
  8. from controllers.console import api, console_ns
  9. from controllers.console.app.wraps import get_app_model
  10. from controllers.console.wraps import (
  11. account_initialization_required,
  12. cloud_edition_billing_resource_check,
  13. enterprise_license_required,
  14. setup_required,
  15. )
  16. from core.ops.ops_trace_manager import OpsTraceManager
  17. from extensions.ext_database import db
  18. from fields.app_fields import app_detail_fields, app_detail_fields_with_site, app_pagination_fields
  19. from libs.login import login_required
  20. from models import Account, App
  21. from services.app_dsl_service import AppDslService, ImportMode
  22. from services.app_service import AppService
  23. from services.enterprise.enterprise_service import EnterpriseService
  24. from services.feature_service import FeatureService
  25. ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
  26. def _validate_description_length(description):
  27. if description and len(description) > 400:
  28. raise ValueError("Description cannot exceed 400 characters.")
  29. return description
  30. @console_ns.route("/apps")
  31. class AppListApi(Resource):
  32. @api.doc("list_apps")
  33. @api.doc(description="Get list of applications with pagination and filtering")
  34. @api.expect(
  35. api.parser()
  36. .add_argument("page", type=int, location="args", help="Page number (1-99999)", default=1)
  37. .add_argument("limit", type=int, location="args", help="Page size (1-100)", default=20)
  38. .add_argument(
  39. "mode",
  40. type=str,
  41. location="args",
  42. choices=["completion", "chat", "advanced-chat", "workflow", "agent-chat", "channel", "all"],
  43. default="all",
  44. help="App mode filter",
  45. )
  46. .add_argument("name", type=str, location="args", help="Filter by app name")
  47. .add_argument("tag_ids", type=str, location="args", help="Comma-separated tag IDs")
  48. .add_argument("is_created_by_me", type=bool, location="args", help="Filter by creator")
  49. )
  50. @api.response(200, "Success", app_pagination_fields)
  51. @setup_required
  52. @login_required
  53. @account_initialization_required
  54. @enterprise_license_required
  55. def get(self):
  56. """Get app list"""
  57. def uuid_list(value):
  58. try:
  59. return [str(uuid.UUID(v)) for v in value.split(",")]
  60. except ValueError:
  61. abort(400, message="Invalid UUID format in tag_ids.")
  62. parser = reqparse.RequestParser()
  63. parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
  64. parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
  65. parser.add_argument(
  66. "mode",
  67. type=str,
  68. choices=[
  69. "completion",
  70. "chat",
  71. "advanced-chat",
  72. "workflow",
  73. "agent-chat",
  74. "channel",
  75. "all",
  76. ],
  77. default="all",
  78. location="args",
  79. required=False,
  80. )
  81. parser.add_argument("name", type=str, location="args", required=False)
  82. parser.add_argument("tag_ids", type=uuid_list, location="args", required=False)
  83. parser.add_argument("is_created_by_me", type=inputs.boolean, location="args", required=False)
  84. args = parser.parse_args()
  85. # get app list
  86. app_service = AppService()
  87. app_pagination = app_service.get_paginate_apps(current_user.id, current_user.current_tenant_id, args)
  88. if not app_pagination:
  89. return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
  90. if FeatureService.get_system_features().webapp_auth.enabled:
  91. app_ids = [str(app.id) for app in app_pagination.items]
  92. res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
  93. if len(res) != len(app_ids):
  94. raise BadRequest("Invalid app id in webapp auth")
  95. for app in app_pagination.items:
  96. if str(app.id) in res:
  97. app.access_mode = res[str(app.id)].access_mode
  98. return marshal(app_pagination, app_pagination_fields), 200
  99. @api.doc("create_app")
  100. @api.doc(description="Create a new application")
  101. @api.expect(
  102. api.model(
  103. "CreateAppRequest",
  104. {
  105. "name": fields.String(required=True, description="App name"),
  106. "description": fields.String(description="App description (max 400 chars)"),
  107. "mode": fields.String(required=True, enum=ALLOW_CREATE_APP_MODES, description="App mode"),
  108. "icon_type": fields.String(description="Icon type"),
  109. "icon": fields.String(description="Icon"),
  110. "icon_background": fields.String(description="Icon background color"),
  111. },
  112. )
  113. )
  114. @api.response(201, "App created successfully", app_detail_fields)
  115. @api.response(403, "Insufficient permissions")
  116. @api.response(400, "Invalid request parameters")
  117. @setup_required
  118. @login_required
  119. @account_initialization_required
  120. @marshal_with(app_detail_fields)
  121. @cloud_edition_billing_resource_check("apps")
  122. def post(self):
  123. """Create app"""
  124. parser = reqparse.RequestParser()
  125. parser.add_argument("name", type=str, required=True, location="json")
  126. parser.add_argument("description", type=_validate_description_length, location="json")
  127. parser.add_argument("mode", type=str, choices=ALLOW_CREATE_APP_MODES, location="json")
  128. parser.add_argument("icon_type", type=str, location="json")
  129. parser.add_argument("icon", type=str, location="json")
  130. parser.add_argument("icon_background", type=str, location="json")
  131. args = parser.parse_args()
  132. # The role of the current user in the ta table must be admin, owner, or editor
  133. if not current_user.is_editor:
  134. raise Forbidden()
  135. if "mode" not in args or args["mode"] is None:
  136. raise BadRequest("mode is required")
  137. app_service = AppService()
  138. if not isinstance(current_user, Account):
  139. raise ValueError("current_user must be an Account instance")
  140. if current_user.current_tenant_id is None:
  141. raise ValueError("current_user.current_tenant_id cannot be None")
  142. app = app_service.create_app(current_user.current_tenant_id, args, current_user)
  143. return app, 201
  144. @console_ns.route("/apps/<uuid:app_id>")
  145. class AppApi(Resource):
  146. @api.doc("get_app_detail")
  147. @api.doc(description="Get application details")
  148. @api.doc(params={"app_id": "Application ID"})
  149. @api.response(200, "Success", app_detail_fields_with_site)
  150. @setup_required
  151. @login_required
  152. @account_initialization_required
  153. @enterprise_license_required
  154. @get_app_model
  155. @marshal_with(app_detail_fields_with_site)
  156. def get(self, app_model):
  157. """Get app detail"""
  158. app_service = AppService()
  159. app_model = app_service.get_app(app_model)
  160. if FeatureService.get_system_features().webapp_auth.enabled:
  161. app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
  162. app_model.access_mode = app_setting.access_mode
  163. return app_model
  164. @api.doc("update_app")
  165. @api.doc(description="Update application details")
  166. @api.doc(params={"app_id": "Application ID"})
  167. @api.expect(
  168. api.model(
  169. "UpdateAppRequest",
  170. {
  171. "name": fields.String(required=True, description="App name"),
  172. "description": fields.String(description="App description (max 400 chars)"),
  173. "icon_type": fields.String(description="Icon type"),
  174. "icon": fields.String(description="Icon"),
  175. "icon_background": fields.String(description="Icon background color"),
  176. "use_icon_as_answer_icon": fields.Boolean(description="Use icon as answer icon"),
  177. "max_active_requests": fields.Integer(description="Maximum active requests"),
  178. },
  179. )
  180. )
  181. @api.response(200, "App updated successfully", app_detail_fields_with_site)
  182. @api.response(403, "Insufficient permissions")
  183. @api.response(400, "Invalid request parameters")
  184. @setup_required
  185. @login_required
  186. @account_initialization_required
  187. @get_app_model
  188. @marshal_with(app_detail_fields_with_site)
  189. def put(self, app_model):
  190. """Update app"""
  191. # The role of the current user in the ta table must be admin, owner, or editor
  192. if not current_user.is_editor:
  193. raise Forbidden()
  194. parser = reqparse.RequestParser()
  195. parser.add_argument("name", type=str, required=True, nullable=False, location="json")
  196. parser.add_argument("description", type=_validate_description_length, location="json")
  197. parser.add_argument("icon_type", type=str, location="json")
  198. parser.add_argument("icon", type=str, location="json")
  199. parser.add_argument("icon_background", type=str, location="json")
  200. parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
  201. parser.add_argument("max_active_requests", type=int, location="json")
  202. args = parser.parse_args()
  203. app_service = AppService()
  204. # Construct ArgsDict from parsed arguments
  205. from services.app_service import AppService as AppServiceType
  206. args_dict: AppServiceType.ArgsDict = {
  207. "name": args["name"],
  208. "description": args.get("description", ""),
  209. "icon_type": args.get("icon_type", ""),
  210. "icon": args.get("icon", ""),
  211. "icon_background": args.get("icon_background", ""),
  212. "use_icon_as_answer_icon": args.get("use_icon_as_answer_icon", False),
  213. "max_active_requests": args.get("max_active_requests", 0),
  214. }
  215. app_model = app_service.update_app(app_model, args_dict)
  216. return app_model
  217. @api.doc("delete_app")
  218. @api.doc(description="Delete application")
  219. @api.doc(params={"app_id": "Application ID"})
  220. @api.response(204, "App deleted successfully")
  221. @api.response(403, "Insufficient permissions")
  222. @get_app_model
  223. @setup_required
  224. @login_required
  225. @account_initialization_required
  226. def delete(self, app_model):
  227. """Delete app"""
  228. # The role of the current user in the ta table must be admin, owner, or editor
  229. if not current_user.is_editor:
  230. raise Forbidden()
  231. app_service = AppService()
  232. app_service.delete_app(app_model)
  233. return {"result": "success"}, 204
  234. @console_ns.route("/apps/<uuid:app_id>/copy")
  235. class AppCopyApi(Resource):
  236. @api.doc("copy_app")
  237. @api.doc(description="Create a copy of an existing application")
  238. @api.doc(params={"app_id": "Application ID to copy"})
  239. @api.expect(
  240. api.model(
  241. "CopyAppRequest",
  242. {
  243. "name": fields.String(description="Name for the copied app"),
  244. "description": fields.String(description="Description for the copied app"),
  245. "icon_type": fields.String(description="Icon type"),
  246. "icon": fields.String(description="Icon"),
  247. "icon_background": fields.String(description="Icon background color"),
  248. },
  249. )
  250. )
  251. @api.response(201, "App copied successfully", app_detail_fields_with_site)
  252. @api.response(403, "Insufficient permissions")
  253. @setup_required
  254. @login_required
  255. @account_initialization_required
  256. @get_app_model
  257. @marshal_with(app_detail_fields_with_site)
  258. def post(self, app_model):
  259. """Copy app"""
  260. # The role of the current user in the ta table must be admin, owner, or editor
  261. if not current_user.is_editor:
  262. raise Forbidden()
  263. parser = reqparse.RequestParser()
  264. parser.add_argument("name", type=str, location="json")
  265. parser.add_argument("description", type=_validate_description_length, location="json")
  266. parser.add_argument("icon_type", type=str, location="json")
  267. parser.add_argument("icon", type=str, location="json")
  268. parser.add_argument("icon_background", type=str, location="json")
  269. args = parser.parse_args()
  270. with Session(db.engine) as session:
  271. import_service = AppDslService(session)
  272. yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
  273. account = cast(Account, current_user)
  274. result = import_service.import_app(
  275. account=account,
  276. import_mode=ImportMode.YAML_CONTENT.value,
  277. yaml_content=yaml_content,
  278. name=args.get("name"),
  279. description=args.get("description"),
  280. icon_type=args.get("icon_type"),
  281. icon=args.get("icon"),
  282. icon_background=args.get("icon_background"),
  283. )
  284. session.commit()
  285. stmt = select(App).where(App.id == result.app_id)
  286. app = session.scalar(stmt)
  287. return app, 201
  288. @console_ns.route("/apps/<uuid:app_id>/export")
  289. class AppExportApi(Resource):
  290. @api.doc("export_app")
  291. @api.doc(description="Export application configuration as DSL")
  292. @api.doc(params={"app_id": "Application ID to export"})
  293. @api.expect(
  294. api.parser()
  295. .add_argument("include_secret", type=bool, location="args", default=False, help="Include secrets in export")
  296. .add_argument("workflow_id", type=str, location="args", help="Specific workflow ID to export")
  297. )
  298. @api.response(
  299. 200,
  300. "App exported successfully",
  301. api.model("AppExportResponse", {"data": fields.String(description="DSL export data")}),
  302. )
  303. @api.response(403, "Insufficient permissions")
  304. @get_app_model
  305. @setup_required
  306. @login_required
  307. @account_initialization_required
  308. def get(self, app_model):
  309. """Export app"""
  310. # The role of the current user in the ta table must be admin, owner, or editor
  311. if not current_user.is_editor:
  312. raise Forbidden()
  313. # Add include_secret params
  314. parser = reqparse.RequestParser()
  315. parser.add_argument("include_secret", type=inputs.boolean, default=False, location="args")
  316. parser.add_argument("workflow_id", type=str, location="args")
  317. args = parser.parse_args()
  318. return {
  319. "data": AppDslService.export_dsl(
  320. app_model=app_model, include_secret=args["include_secret"], workflow_id=args.get("workflow_id")
  321. )
  322. }
  323. @console_ns.route("/apps/<uuid:app_id>/name")
  324. class AppNameApi(Resource):
  325. @api.doc("check_app_name")
  326. @api.doc(description="Check if app name is available")
  327. @api.doc(params={"app_id": "Application ID"})
  328. @api.expect(api.parser().add_argument("name", type=str, required=True, location="args", help="Name to check"))
  329. @api.response(200, "Name availability checked")
  330. @setup_required
  331. @login_required
  332. @account_initialization_required
  333. @get_app_model
  334. @marshal_with(app_detail_fields)
  335. def post(self, app_model):
  336. # The role of the current user in the ta table must be admin, owner, or editor
  337. if not current_user.is_editor:
  338. raise Forbidden()
  339. parser = reqparse.RequestParser()
  340. parser.add_argument("name", type=str, required=True, location="json")
  341. args = parser.parse_args()
  342. app_service = AppService()
  343. app_model = app_service.update_app_name(app_model, args["name"])
  344. return app_model
  345. @console_ns.route("/apps/<uuid:app_id>/icon")
  346. class AppIconApi(Resource):
  347. @api.doc("update_app_icon")
  348. @api.doc(description="Update application icon")
  349. @api.doc(params={"app_id": "Application ID"})
  350. @api.expect(
  351. api.model(
  352. "AppIconRequest",
  353. {
  354. "icon": fields.String(required=True, description="Icon data"),
  355. "icon_type": fields.String(description="Icon type"),
  356. "icon_background": fields.String(description="Icon background color"),
  357. },
  358. )
  359. )
  360. @api.response(200, "Icon updated successfully")
  361. @api.response(403, "Insufficient permissions")
  362. @setup_required
  363. @login_required
  364. @account_initialization_required
  365. @get_app_model
  366. @marshal_with(app_detail_fields)
  367. def post(self, app_model):
  368. # The role of the current user in the ta table must be admin, owner, or editor
  369. if not current_user.is_editor:
  370. raise Forbidden()
  371. parser = reqparse.RequestParser()
  372. parser.add_argument("icon", type=str, location="json")
  373. parser.add_argument("icon_background", type=str, location="json")
  374. args = parser.parse_args()
  375. app_service = AppService()
  376. app_model = app_service.update_app_icon(app_model, args.get("icon") or "", args.get("icon_background") or "")
  377. return app_model
  378. @console_ns.route("/apps/<uuid:app_id>/site-enable")
  379. class AppSiteStatus(Resource):
  380. @api.doc("update_app_site_status")
  381. @api.doc(description="Enable or disable app site")
  382. @api.doc(params={"app_id": "Application ID"})
  383. @api.expect(
  384. api.model(
  385. "AppSiteStatusRequest", {"enable_site": fields.Boolean(required=True, description="Enable or disable site")}
  386. )
  387. )
  388. @api.response(200, "Site status updated successfully", app_detail_fields)
  389. @api.response(403, "Insufficient permissions")
  390. @setup_required
  391. @login_required
  392. @account_initialization_required
  393. @get_app_model
  394. @marshal_with(app_detail_fields)
  395. def post(self, app_model):
  396. # The role of the current user in the ta table must be admin, owner, or editor
  397. if not current_user.is_editor:
  398. raise Forbidden()
  399. parser = reqparse.RequestParser()
  400. parser.add_argument("enable_site", type=bool, required=True, location="json")
  401. args = parser.parse_args()
  402. app_service = AppService()
  403. app_model = app_service.update_app_site_status(app_model, args["enable_site"])
  404. return app_model
  405. @console_ns.route("/apps/<uuid:app_id>/api-enable")
  406. class AppApiStatus(Resource):
  407. @api.doc("update_app_api_status")
  408. @api.doc(description="Enable or disable app API")
  409. @api.doc(params={"app_id": "Application ID"})
  410. @api.expect(
  411. api.model(
  412. "AppApiStatusRequest", {"enable_api": fields.Boolean(required=True, description="Enable or disable API")}
  413. )
  414. )
  415. @api.response(200, "API status updated successfully", app_detail_fields)
  416. @api.response(403, "Insufficient permissions")
  417. @setup_required
  418. @login_required
  419. @account_initialization_required
  420. @get_app_model
  421. @marshal_with(app_detail_fields)
  422. def post(self, app_model):
  423. # The role of the current user in the ta table must be admin or owner
  424. if not current_user.is_admin_or_owner:
  425. raise Forbidden()
  426. parser = reqparse.RequestParser()
  427. parser.add_argument("enable_api", type=bool, required=True, location="json")
  428. args = parser.parse_args()
  429. app_service = AppService()
  430. app_model = app_service.update_app_api_status(app_model, args["enable_api"])
  431. return app_model
  432. @console_ns.route("/apps/<uuid:app_id>/trace")
  433. class AppTraceApi(Resource):
  434. @api.doc("get_app_trace")
  435. @api.doc(description="Get app tracing configuration")
  436. @api.doc(params={"app_id": "Application ID"})
  437. @api.response(200, "Trace configuration retrieved successfully")
  438. @setup_required
  439. @login_required
  440. @account_initialization_required
  441. def get(self, app_id):
  442. """Get app trace"""
  443. app_trace_config = OpsTraceManager.get_app_tracing_config(app_id=app_id)
  444. return app_trace_config
  445. @api.doc("update_app_trace")
  446. @api.doc(description="Update app tracing configuration")
  447. @api.doc(params={"app_id": "Application ID"})
  448. @api.expect(
  449. api.model(
  450. "AppTraceRequest",
  451. {
  452. "enabled": fields.Boolean(required=True, description="Enable or disable tracing"),
  453. "tracing_provider": fields.String(required=True, description="Tracing provider"),
  454. },
  455. )
  456. )
  457. @api.response(200, "Trace configuration updated successfully")
  458. @api.response(403, "Insufficient permissions")
  459. @setup_required
  460. @login_required
  461. @account_initialization_required
  462. def post(self, app_id):
  463. # add app trace
  464. if not current_user.is_editor:
  465. raise Forbidden()
  466. parser = reqparse.RequestParser()
  467. parser.add_argument("enabled", type=bool, required=True, location="json")
  468. parser.add_argument("tracing_provider", type=str, required=True, location="json")
  469. args = parser.parse_args()
  470. OpsTraceManager.update_app_tracing_config(
  471. app_id=app_id,
  472. enabled=args["enabled"],
  473. tracing_provider=args["tracing_provider"],
  474. )
  475. return {"result": "success"}