您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

annotation.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. from typing import Literal
  2. from flask import request
  3. from flask_login import current_user
  4. from flask_restful import Resource, marshal, marshal_with, reqparse
  5. from werkzeug.exceptions import Forbidden
  6. from controllers.common.errors import NoFileUploadedError, TooManyFilesError
  7. from controllers.console import api
  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. class AnnotationReplyActionApi(Resource):
  21. @setup_required
  22. @login_required
  23. @account_initialization_required
  24. @cloud_edition_billing_resource_check("annotation")
  25. def post(self, app_id, action: Literal["enable", "disable"]):
  26. if not current_user.is_editor:
  27. raise Forbidden()
  28. app_id = str(app_id)
  29. parser = reqparse.RequestParser()
  30. parser.add_argument("score_threshold", required=True, type=float, location="json")
  31. parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
  32. parser.add_argument("embedding_model_name", required=True, type=str, location="json")
  33. args = parser.parse_args()
  34. if action == "enable":
  35. result = AppAnnotationService.enable_app_annotation(args, app_id)
  36. elif action == "disable":
  37. result = AppAnnotationService.disable_app_annotation(app_id)
  38. return result, 200
  39. class AppAnnotationSettingDetailApi(Resource):
  40. @setup_required
  41. @login_required
  42. @account_initialization_required
  43. def get(self, app_id):
  44. if not current_user.is_editor:
  45. raise Forbidden()
  46. app_id = str(app_id)
  47. result = AppAnnotationService.get_app_annotation_setting_by_app_id(app_id)
  48. return result, 200
  49. class AppAnnotationSettingUpdateApi(Resource):
  50. @setup_required
  51. @login_required
  52. @account_initialization_required
  53. def post(self, app_id, annotation_setting_id):
  54. if not current_user.is_editor:
  55. raise Forbidden()
  56. app_id = str(app_id)
  57. annotation_setting_id = str(annotation_setting_id)
  58. parser = reqparse.RequestParser()
  59. parser.add_argument("score_threshold", required=True, type=float, location="json")
  60. args = parser.parse_args()
  61. result = AppAnnotationService.update_app_annotation_setting(app_id, annotation_setting_id, args)
  62. return result, 200
  63. class AnnotationReplyActionStatusApi(Resource):
  64. @setup_required
  65. @login_required
  66. @account_initialization_required
  67. @cloud_edition_billing_resource_check("annotation")
  68. def get(self, app_id, job_id, action):
  69. if not current_user.is_editor:
  70. raise Forbidden()
  71. job_id = str(job_id)
  72. app_annotation_job_key = f"{action}_app_annotation_job_{str(job_id)}"
  73. cache_result = redis_client.get(app_annotation_job_key)
  74. if cache_result is None:
  75. raise ValueError("The job does not exist.")
  76. job_status = cache_result.decode()
  77. error_msg = ""
  78. if job_status == "error":
  79. app_annotation_error_key = f"{action}_app_annotation_error_{str(job_id)}"
  80. error_msg = redis_client.get(app_annotation_error_key).decode()
  81. return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
  82. class AnnotationApi(Resource):
  83. @setup_required
  84. @login_required
  85. @account_initialization_required
  86. def get(self, app_id):
  87. if not current_user.is_editor:
  88. raise Forbidden()
  89. page = request.args.get("page", default=1, type=int)
  90. limit = request.args.get("limit", default=20, type=int)
  91. keyword = request.args.get("keyword", default="", type=str)
  92. app_id = str(app_id)
  93. annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)
  94. response = {
  95. "data": marshal(annotation_list, annotation_fields),
  96. "has_more": len(annotation_list) == limit,
  97. "limit": limit,
  98. "total": total,
  99. "page": page,
  100. }
  101. return response, 200
  102. @setup_required
  103. @login_required
  104. @account_initialization_required
  105. @cloud_edition_billing_resource_check("annotation")
  106. @marshal_with(annotation_fields)
  107. def post(self, app_id):
  108. if not current_user.is_editor:
  109. raise Forbidden()
  110. app_id = str(app_id)
  111. parser = reqparse.RequestParser()
  112. parser.add_argument("question", required=True, type=str, location="json")
  113. parser.add_argument("answer", required=True, type=str, location="json")
  114. args = parser.parse_args()
  115. annotation = AppAnnotationService.insert_app_annotation_directly(args, app_id)
  116. return annotation
  117. @setup_required
  118. @login_required
  119. @account_initialization_required
  120. def delete(self, app_id):
  121. if not current_user.is_editor:
  122. raise Forbidden()
  123. app_id = str(app_id)
  124. # Use request.args.getlist to get annotation_ids array directly
  125. annotation_ids = request.args.getlist("annotation_id")
  126. # If annotation_ids are provided, handle batch deletion
  127. if annotation_ids:
  128. # Check if any annotation_ids contain empty strings or invalid values
  129. if not all(annotation_id.strip() for annotation_id in annotation_ids if annotation_id):
  130. return {
  131. "code": "bad_request",
  132. "message": "annotation_ids are required if the parameter is provided.",
  133. }, 400
  134. result = AppAnnotationService.delete_app_annotations_in_batch(app_id, annotation_ids)
  135. return result, 204
  136. # If no annotation_ids are provided, handle clearing all annotations
  137. else:
  138. AppAnnotationService.clear_all_annotations(app_id)
  139. return {"result": "success"}, 204
  140. class AnnotationExportApi(Resource):
  141. @setup_required
  142. @login_required
  143. @account_initialization_required
  144. def get(self, app_id):
  145. if not current_user.is_editor:
  146. raise Forbidden()
  147. app_id = str(app_id)
  148. annotation_list = AppAnnotationService.export_annotation_list_by_app_id(app_id)
  149. response = {"data": marshal(annotation_list, annotation_fields)}
  150. return response, 200
  151. class AnnotationUpdateDeleteApi(Resource):
  152. @setup_required
  153. @login_required
  154. @account_initialization_required
  155. @cloud_edition_billing_resource_check("annotation")
  156. @marshal_with(annotation_fields)
  157. def post(self, app_id, annotation_id):
  158. if not current_user.is_editor:
  159. raise Forbidden()
  160. app_id = str(app_id)
  161. annotation_id = str(annotation_id)
  162. parser = reqparse.RequestParser()
  163. parser.add_argument("question", required=True, type=str, location="json")
  164. parser.add_argument("answer", required=True, type=str, location="json")
  165. args = parser.parse_args()
  166. annotation = AppAnnotationService.update_app_annotation_directly(args, app_id, annotation_id)
  167. return annotation
  168. @setup_required
  169. @login_required
  170. @account_initialization_required
  171. def delete(self, app_id, annotation_id):
  172. if not current_user.is_editor:
  173. raise Forbidden()
  174. app_id = str(app_id)
  175. annotation_id = str(annotation_id)
  176. AppAnnotationService.delete_app_annotation(app_id, annotation_id)
  177. return {"result": "success"}, 204
  178. class AnnotationBatchImportApi(Resource):
  179. @setup_required
  180. @login_required
  181. @account_initialization_required
  182. @cloud_edition_billing_resource_check("annotation")
  183. def post(self, app_id):
  184. if not current_user.is_editor:
  185. raise Forbidden()
  186. app_id = str(app_id)
  187. # check file
  188. if "file" not in request.files:
  189. raise NoFileUploadedError()
  190. if len(request.files) > 1:
  191. raise TooManyFilesError()
  192. # get file from request
  193. file = request.files["file"]
  194. # check file type
  195. if not file.filename or not file.filename.lower().endswith(".csv"):
  196. raise ValueError("Invalid file type. Only CSV files are allowed")
  197. return AppAnnotationService.batch_import_app_annotations(app_id, file)
  198. class AnnotationBatchImportStatusApi(Resource):
  199. @setup_required
  200. @login_required
  201. @account_initialization_required
  202. @cloud_edition_billing_resource_check("annotation")
  203. def get(self, app_id, job_id):
  204. if not current_user.is_editor:
  205. raise Forbidden()
  206. job_id = str(job_id)
  207. indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
  208. cache_result = redis_client.get(indexing_cache_key)
  209. if cache_result is None:
  210. raise ValueError("The job does not exist.")
  211. job_status = cache_result.decode()
  212. error_msg = ""
  213. if job_status == "error":
  214. indexing_error_msg_key = f"app_annotation_batch_import_error_msg_{str(job_id)}"
  215. error_msg = redis_client.get(indexing_error_msg_key).decode()
  216. return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
  217. class AnnotationHitHistoryListApi(Resource):
  218. @setup_required
  219. @login_required
  220. @account_initialization_required
  221. def get(self, app_id, annotation_id):
  222. if not current_user.is_editor:
  223. raise Forbidden()
  224. page = request.args.get("page", default=1, type=int)
  225. limit = request.args.get("limit", default=20, type=int)
  226. app_id = str(app_id)
  227. annotation_id = str(annotation_id)
  228. annotation_hit_history_list, total = AppAnnotationService.get_annotation_hit_histories(
  229. app_id, annotation_id, page, limit
  230. )
  231. response = {
  232. "data": marshal(annotation_hit_history_list, annotation_hit_history_fields),
  233. "has_more": len(annotation_hit_history_list) == limit,
  234. "limit": limit,
  235. "total": total,
  236. "page": page,
  237. }
  238. return response
  239. api.add_resource(AnnotationReplyActionApi, "/apps/<uuid:app_id>/annotation-reply/<string:action>")
  240. api.add_resource(
  241. AnnotationReplyActionStatusApi, "/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>"
  242. )
  243. api.add_resource(AnnotationApi, "/apps/<uuid:app_id>/annotations")
  244. api.add_resource(AnnotationExportApi, "/apps/<uuid:app_id>/annotations/export")
  245. api.add_resource(AnnotationUpdateDeleteApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
  246. api.add_resource(AnnotationBatchImportApi, "/apps/<uuid:app_id>/annotations/batch-import")
  247. api.add_resource(AnnotationBatchImportStatusApi, "/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
  248. api.add_resource(AnnotationHitHistoryListApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>/hit-histories")
  249. api.add_resource(AppAnnotationSettingDetailApi, "/apps/<uuid:app_id>/annotation-setting")
  250. api.add_resource(AppAnnotationSettingUpdateApi, "/apps/<uuid:app_id>/annotation-settings/<uuid:annotation_setting_id>")