Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

annotation.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. from typing import Literal
  2. from flask import request
  3. from flask_login import current_user
  4. from flask_restx import Resource, fields, marshal, marshal_with, reqparse
  5. from werkzeug.exceptions import Forbidden
  6. from controllers.common.errors import NoFileUploadedError, TooManyFilesError
  7. from controllers.console import api, console_ns
  8. from controllers.console.wraps import (
  9. account_initialization_required,
  10. cloud_edition_billing_resource_check,
  11. setup_required,
  12. )
  13. from extensions.ext_redis import redis_client
  14. from fields.annotation_fields import (
  15. annotation_fields,
  16. annotation_hit_history_fields,
  17. )
  18. from libs.login import login_required
  19. from services.annotation_service import AppAnnotationService
  20. @console_ns.route("/apps/<uuid:app_id>/annotation-reply/<string:action>")
  21. class AnnotationReplyActionApi(Resource):
  22. @api.doc("annotation_reply_action")
  23. @api.doc(description="Enable or disable annotation reply for an app")
  24. @api.doc(params={"app_id": "Application ID", "action": "Action to perform (enable/disable)"})
  25. @api.expect(
  26. api.model(
  27. "AnnotationReplyActionRequest",
  28. {
  29. "score_threshold": fields.Float(required=True, description="Score threshold for annotation matching"),
  30. "embedding_provider_name": fields.String(required=True, description="Embedding provider name"),
  31. "embedding_model_name": fields.String(required=True, description="Embedding model name"),
  32. },
  33. )
  34. )
  35. @api.response(200, "Action completed successfully")
  36. @api.response(403, "Insufficient permissions")
  37. @setup_required
  38. @login_required
  39. @account_initialization_required
  40. @cloud_edition_billing_resource_check("annotation")
  41. def post(self, app_id, action: Literal["enable", "disable"]):
  42. if not current_user.is_editor:
  43. raise Forbidden()
  44. app_id = str(app_id)
  45. parser = reqparse.RequestParser()
  46. parser.add_argument("score_threshold", required=True, type=float, location="json")
  47. parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
  48. parser.add_argument("embedding_model_name", required=True, type=str, location="json")
  49. args = parser.parse_args()
  50. if action == "enable":
  51. result = AppAnnotationService.enable_app_annotation(args, app_id)
  52. elif action == "disable":
  53. result = AppAnnotationService.disable_app_annotation(app_id)
  54. return result, 200
  55. @console_ns.route("/apps/<uuid:app_id>/annotation-setting")
  56. class AppAnnotationSettingDetailApi(Resource):
  57. @api.doc("get_annotation_setting")
  58. @api.doc(description="Get annotation settings for an app")
  59. @api.doc(params={"app_id": "Application ID"})
  60. @api.response(200, "Annotation settings retrieved successfully")
  61. @api.response(403, "Insufficient permissions")
  62. @setup_required
  63. @login_required
  64. @account_initialization_required
  65. def get(self, app_id):
  66. if not current_user.is_editor:
  67. raise Forbidden()
  68. app_id = str(app_id)
  69. result = AppAnnotationService.get_app_annotation_setting_by_app_id(app_id)
  70. return result, 200
  71. @console_ns.route("/apps/<uuid:app_id>/annotation-settings/<uuid:annotation_setting_id>")
  72. class AppAnnotationSettingUpdateApi(Resource):
  73. @api.doc("update_annotation_setting")
  74. @api.doc(description="Update annotation settings for an app")
  75. @api.doc(params={"app_id": "Application ID", "annotation_setting_id": "Annotation setting ID"})
  76. @api.expect(
  77. api.model(
  78. "AnnotationSettingUpdateRequest",
  79. {
  80. "score_threshold": fields.Float(required=True, description="Score threshold"),
  81. "embedding_provider_name": fields.String(required=True, description="Embedding provider"),
  82. "embedding_model_name": fields.String(required=True, description="Embedding model"),
  83. },
  84. )
  85. )
  86. @api.response(200, "Settings updated successfully")
  87. @api.response(403, "Insufficient permissions")
  88. @setup_required
  89. @login_required
  90. @account_initialization_required
  91. def post(self, app_id, annotation_setting_id):
  92. if not current_user.is_editor:
  93. raise Forbidden()
  94. app_id = str(app_id)
  95. annotation_setting_id = str(annotation_setting_id)
  96. parser = reqparse.RequestParser()
  97. parser.add_argument("score_threshold", required=True, type=float, location="json")
  98. args = parser.parse_args()
  99. result = AppAnnotationService.update_app_annotation_setting(app_id, annotation_setting_id, args)
  100. return result, 200
  101. @console_ns.route("/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>")
  102. class AnnotationReplyActionStatusApi(Resource):
  103. @api.doc("get_annotation_reply_action_status")
  104. @api.doc(description="Get status of annotation reply action job")
  105. @api.doc(params={"app_id": "Application ID", "job_id": "Job ID", "action": "Action type"})
  106. @api.response(200, "Job status retrieved successfully")
  107. @api.response(403, "Insufficient permissions")
  108. @setup_required
  109. @login_required
  110. @account_initialization_required
  111. @cloud_edition_billing_resource_check("annotation")
  112. def get(self, app_id, job_id, action):
  113. if not current_user.is_editor:
  114. raise Forbidden()
  115. job_id = str(job_id)
  116. app_annotation_job_key = f"{action}_app_annotation_job_{str(job_id)}"
  117. cache_result = redis_client.get(app_annotation_job_key)
  118. if cache_result is None:
  119. raise ValueError("The job does not exist.")
  120. job_status = cache_result.decode()
  121. error_msg = ""
  122. if job_status == "error":
  123. app_annotation_error_key = f"{action}_app_annotation_error_{str(job_id)}"
  124. error_msg = redis_client.get(app_annotation_error_key).decode()
  125. return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
  126. @console_ns.route("/apps/<uuid:app_id>/annotations")
  127. class AnnotationApi(Resource):
  128. @api.doc("list_annotations")
  129. @api.doc(description="Get annotations for an app with pagination")
  130. @api.doc(params={"app_id": "Application ID"})
  131. @api.expect(
  132. api.parser()
  133. .add_argument("page", type=int, location="args", default=1, help="Page number")
  134. .add_argument("limit", type=int, location="args", default=20, help="Page size")
  135. .add_argument("keyword", type=str, location="args", default="", help="Search keyword")
  136. )
  137. @api.response(200, "Annotations retrieved successfully")
  138. @api.response(403, "Insufficient permissions")
  139. @setup_required
  140. @login_required
  141. @account_initialization_required
  142. def get(self, app_id):
  143. if not current_user.is_editor:
  144. raise Forbidden()
  145. page = request.args.get("page", default=1, type=int)
  146. limit = request.args.get("limit", default=20, type=int)
  147. keyword = request.args.get("keyword", default="", type=str)
  148. app_id = str(app_id)
  149. annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)
  150. response = {
  151. "data": marshal(annotation_list, annotation_fields),
  152. "has_more": len(annotation_list) == limit,
  153. "limit": limit,
  154. "total": total,
  155. "page": page,
  156. }
  157. return response, 200
  158. @api.doc("create_annotation")
  159. @api.doc(description="Create a new annotation for an app")
  160. @api.doc(params={"app_id": "Application ID"})
  161. @api.expect(
  162. api.model(
  163. "CreateAnnotationRequest",
  164. {
  165. "question": fields.String(required=True, description="Question text"),
  166. "answer": fields.String(required=True, description="Answer text"),
  167. "annotation_reply": fields.Raw(description="Annotation reply data"),
  168. },
  169. )
  170. )
  171. @api.response(201, "Annotation created successfully", annotation_fields)
  172. @api.response(403, "Insufficient permissions")
  173. @setup_required
  174. @login_required
  175. @account_initialization_required
  176. @cloud_edition_billing_resource_check("annotation")
  177. @marshal_with(annotation_fields)
  178. def post(self, app_id):
  179. if not current_user.is_editor:
  180. raise Forbidden()
  181. app_id = str(app_id)
  182. parser = reqparse.RequestParser()
  183. parser.add_argument("question", required=True, type=str, location="json")
  184. parser.add_argument("answer", required=True, type=str, location="json")
  185. args = parser.parse_args()
  186. annotation = AppAnnotationService.insert_app_annotation_directly(args, app_id)
  187. return annotation
  188. @setup_required
  189. @login_required
  190. @account_initialization_required
  191. def delete(self, app_id):
  192. if not current_user.is_editor:
  193. raise Forbidden()
  194. app_id = str(app_id)
  195. # Use request.args.getlist to get annotation_ids array directly
  196. annotation_ids = request.args.getlist("annotation_id")
  197. # If annotation_ids are provided, handle batch deletion
  198. if annotation_ids:
  199. # Check if any annotation_ids contain empty strings or invalid values
  200. if not all(annotation_id.strip() for annotation_id in annotation_ids if annotation_id):
  201. return {
  202. "code": "bad_request",
  203. "message": "annotation_ids are required if the parameter is provided.",
  204. }, 400
  205. result = AppAnnotationService.delete_app_annotations_in_batch(app_id, annotation_ids)
  206. return result, 204
  207. # If no annotation_ids are provided, handle clearing all annotations
  208. else:
  209. AppAnnotationService.clear_all_annotations(app_id)
  210. return {"result": "success"}, 204
  211. @console_ns.route("/apps/<uuid:app_id>/annotations/export")
  212. class AnnotationExportApi(Resource):
  213. @api.doc("export_annotations")
  214. @api.doc(description="Export all annotations for an app")
  215. @api.doc(params={"app_id": "Application ID"})
  216. @api.response(200, "Annotations exported successfully", fields.List(fields.Nested(annotation_fields)))
  217. @api.response(403, "Insufficient permissions")
  218. @setup_required
  219. @login_required
  220. @account_initialization_required
  221. def get(self, app_id):
  222. if not current_user.is_editor:
  223. raise Forbidden()
  224. app_id = str(app_id)
  225. annotation_list = AppAnnotationService.export_annotation_list_by_app_id(app_id)
  226. response = {"data": marshal(annotation_list, annotation_fields)}
  227. return response, 200
  228. @console_ns.route("/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
  229. class AnnotationUpdateDeleteApi(Resource):
  230. @api.doc("update_delete_annotation")
  231. @api.doc(description="Update or delete an annotation")
  232. @api.doc(params={"app_id": "Application ID", "annotation_id": "Annotation ID"})
  233. @api.response(200, "Annotation updated successfully", annotation_fields)
  234. @api.response(204, "Annotation deleted successfully")
  235. @api.response(403, "Insufficient permissions")
  236. @setup_required
  237. @login_required
  238. @account_initialization_required
  239. @cloud_edition_billing_resource_check("annotation")
  240. @marshal_with(annotation_fields)
  241. def post(self, app_id, annotation_id):
  242. if not current_user.is_editor:
  243. raise Forbidden()
  244. app_id = str(app_id)
  245. annotation_id = str(annotation_id)
  246. parser = reqparse.RequestParser()
  247. parser.add_argument("question", required=True, type=str, location="json")
  248. parser.add_argument("answer", required=True, type=str, location="json")
  249. args = parser.parse_args()
  250. annotation = AppAnnotationService.update_app_annotation_directly(args, app_id, annotation_id)
  251. return annotation
  252. @setup_required
  253. @login_required
  254. @account_initialization_required
  255. def delete(self, app_id, annotation_id):
  256. if not current_user.is_editor:
  257. raise Forbidden()
  258. app_id = str(app_id)
  259. annotation_id = str(annotation_id)
  260. AppAnnotationService.delete_app_annotation(app_id, annotation_id)
  261. return {"result": "success"}, 204
  262. @console_ns.route("/apps/<uuid:app_id>/annotations/batch-import")
  263. class AnnotationBatchImportApi(Resource):
  264. @api.doc("batch_import_annotations")
  265. @api.doc(description="Batch import annotations from CSV file")
  266. @api.doc(params={"app_id": "Application ID"})
  267. @api.response(200, "Batch import started successfully")
  268. @api.response(403, "Insufficient permissions")
  269. @api.response(400, "No file uploaded or too many files")
  270. @setup_required
  271. @login_required
  272. @account_initialization_required
  273. @cloud_edition_billing_resource_check("annotation")
  274. def post(self, app_id):
  275. if not current_user.is_editor:
  276. raise Forbidden()
  277. app_id = str(app_id)
  278. # check file
  279. if "file" not in request.files:
  280. raise NoFileUploadedError()
  281. if len(request.files) > 1:
  282. raise TooManyFilesError()
  283. # get file from request
  284. file = request.files["file"]
  285. # check file type
  286. if not file.filename or not file.filename.lower().endswith(".csv"):
  287. raise ValueError("Invalid file type. Only CSV files are allowed")
  288. return AppAnnotationService.batch_import_app_annotations(app_id, file)
  289. @console_ns.route("/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
  290. class AnnotationBatchImportStatusApi(Resource):
  291. @api.doc("get_batch_import_status")
  292. @api.doc(description="Get status of batch import job")
  293. @api.doc(params={"app_id": "Application ID", "job_id": "Job ID"})
  294. @api.response(200, "Job status retrieved successfully")
  295. @api.response(403, "Insufficient permissions")
  296. @setup_required
  297. @login_required
  298. @account_initialization_required
  299. @cloud_edition_billing_resource_check("annotation")
  300. def get(self, app_id, job_id):
  301. if not current_user.is_editor:
  302. raise Forbidden()
  303. job_id = str(job_id)
  304. indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
  305. cache_result = redis_client.get(indexing_cache_key)
  306. if cache_result is None:
  307. raise ValueError("The job does not exist.")
  308. job_status = cache_result.decode()
  309. error_msg = ""
  310. if job_status == "error":
  311. indexing_error_msg_key = f"app_annotation_batch_import_error_msg_{str(job_id)}"
  312. error_msg = redis_client.get(indexing_error_msg_key).decode()
  313. return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
  314. @console_ns.route("/apps/<uuid:app_id>/annotations/<uuid:annotation_id>/hit-histories")
  315. class AnnotationHitHistoryListApi(Resource):
  316. @api.doc("list_annotation_hit_histories")
  317. @api.doc(description="Get hit histories for an annotation")
  318. @api.doc(params={"app_id": "Application ID", "annotation_id": "Annotation ID"})
  319. @api.expect(
  320. api.parser()
  321. .add_argument("page", type=int, location="args", default=1, help="Page number")
  322. .add_argument("limit", type=int, location="args", default=20, help="Page size")
  323. )
  324. @api.response(
  325. 200, "Hit histories retrieved successfully", fields.List(fields.Nested(annotation_hit_history_fields))
  326. )
  327. @api.response(403, "Insufficient permissions")
  328. @setup_required
  329. @login_required
  330. @account_initialization_required
  331. def get(self, app_id, annotation_id):
  332. if not current_user.is_editor:
  333. raise Forbidden()
  334. page = request.args.get("page", default=1, type=int)
  335. limit = request.args.get("limit", default=20, type=int)
  336. app_id = str(app_id)
  337. annotation_id = str(annotation_id)
  338. annotation_hit_history_list, total = AppAnnotationService.get_annotation_hit_histories(
  339. app_id, annotation_id, page, limit
  340. )
  341. response = {
  342. "data": marshal(annotation_hit_history_list, annotation_hit_history_fields),
  343. "has_more": len(annotation_hit_history_list) == limit,
  344. "limit": limit,
  345. "total": total,
  346. "page": page,
  347. }
  348. return response