annotation_service.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import datetime
  2. import uuid
  3. import pandas as pd
  4. from flask_login import current_user
  5. from sqlalchemy import or_
  6. from werkzeug.datastructures import FileStorage
  7. from werkzeug.exceptions import NotFound
  8. from extensions.ext_database import db
  9. from extensions.ext_redis import redis_client
  10. from models.model import App, AppAnnotationHitHistory, AppAnnotationSetting, Message, MessageAnnotation
  11. from services.feature_service import FeatureService
  12. from tasks.annotation.add_annotation_to_index_task import add_annotation_to_index_task
  13. from tasks.annotation.batch_import_annotations_task import batch_import_annotations_task
  14. from tasks.annotation.delete_annotation_index_task import delete_annotation_index_task
  15. from tasks.annotation.disable_annotation_reply_task import disable_annotation_reply_task
  16. from tasks.annotation.enable_annotation_reply_task import enable_annotation_reply_task
  17. from tasks.annotation.update_annotation_to_index_task import update_annotation_to_index_task
  18. class AppAnnotationService:
  19. @classmethod
  20. def up_insert_app_annotation_from_message(cls, args: dict, app_id: str) -> MessageAnnotation:
  21. # get app info
  22. app = (
  23. db.session.query(App)
  24. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  25. .first()
  26. )
  27. if not app:
  28. raise NotFound("App not found")
  29. if args.get("message_id"):
  30. message_id = str(args["message_id"])
  31. # get message info
  32. message = db.session.query(Message).filter(Message.id == message_id, Message.app_id == app.id).first()
  33. if not message:
  34. raise NotFound("Message Not Exists.")
  35. annotation = message.annotation
  36. # save the message annotation
  37. if annotation:
  38. annotation.content = args["answer"]
  39. annotation.question = args["question"]
  40. else:
  41. annotation = MessageAnnotation(
  42. app_id=app.id,
  43. conversation_id=message.conversation_id,
  44. message_id=message.id,
  45. content=args["answer"],
  46. question=args["question"],
  47. account_id=current_user.id,
  48. )
  49. else:
  50. annotation = MessageAnnotation(
  51. app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id
  52. )
  53. db.session.add(annotation)
  54. db.session.commit()
  55. # if annotation reply is enabled , add annotation to index
  56. annotation_setting = (
  57. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  58. )
  59. if annotation_setting:
  60. add_annotation_to_index_task.delay(
  61. annotation.id,
  62. args["question"],
  63. current_user.current_tenant_id,
  64. app_id,
  65. annotation_setting.collection_binding_id,
  66. )
  67. return annotation
  68. @classmethod
  69. def enable_app_annotation(cls, args: dict, app_id: str) -> dict:
  70. enable_app_annotation_key = "enable_app_annotation_{}".format(str(app_id))
  71. cache_result = redis_client.get(enable_app_annotation_key)
  72. if cache_result is not None:
  73. return {"job_id": cache_result, "job_status": "processing"}
  74. # async job
  75. job_id = str(uuid.uuid4())
  76. enable_app_annotation_job_key = "enable_app_annotation_job_{}".format(str(job_id))
  77. # send batch add segments task
  78. redis_client.setnx(enable_app_annotation_job_key, "waiting")
  79. enable_annotation_reply_task.delay(
  80. str(job_id),
  81. app_id,
  82. current_user.id,
  83. current_user.current_tenant_id,
  84. args["score_threshold"],
  85. args["embedding_provider_name"],
  86. args["embedding_model_name"],
  87. )
  88. return {"job_id": job_id, "job_status": "waiting"}
  89. @classmethod
  90. def disable_app_annotation(cls, app_id: str) -> dict:
  91. disable_app_annotation_key = "disable_app_annotation_{}".format(str(app_id))
  92. cache_result = redis_client.get(disable_app_annotation_key)
  93. if cache_result is not None:
  94. return {"job_id": cache_result, "job_status": "processing"}
  95. # async job
  96. job_id = str(uuid.uuid4())
  97. disable_app_annotation_job_key = "disable_app_annotation_job_{}".format(str(job_id))
  98. # send batch add segments task
  99. redis_client.setnx(disable_app_annotation_job_key, "waiting")
  100. disable_annotation_reply_task.delay(str(job_id), app_id, current_user.current_tenant_id)
  101. return {"job_id": job_id, "job_status": "waiting"}
  102. @classmethod
  103. def get_annotation_list_by_app_id(cls, app_id: str, page: int, limit: int, keyword: str):
  104. # get app info
  105. app = (
  106. db.session.query(App)
  107. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  108. .first()
  109. )
  110. if not app:
  111. raise NotFound("App not found")
  112. if keyword:
  113. annotations = (
  114. db.session.query(MessageAnnotation)
  115. .filter(MessageAnnotation.app_id == app_id)
  116. .filter(
  117. or_(
  118. MessageAnnotation.question.ilike("%{}%".format(keyword)),
  119. MessageAnnotation.content.ilike("%{}%".format(keyword)),
  120. )
  121. )
  122. .order_by(MessageAnnotation.created_at.desc())
  123. .paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
  124. )
  125. else:
  126. annotations = (
  127. db.session.query(MessageAnnotation)
  128. .filter(MessageAnnotation.app_id == app_id)
  129. .order_by(MessageAnnotation.created_at.desc())
  130. .paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
  131. )
  132. return annotations.items, annotations.total
  133. @classmethod
  134. def export_annotation_list_by_app_id(cls, app_id: str):
  135. # get app info
  136. app = (
  137. db.session.query(App)
  138. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  139. .first()
  140. )
  141. if not app:
  142. raise NotFound("App not found")
  143. annotations = (
  144. db.session.query(MessageAnnotation)
  145. .filter(MessageAnnotation.app_id == app_id)
  146. .order_by(MessageAnnotation.created_at.desc())
  147. .all()
  148. )
  149. return annotations
  150. @classmethod
  151. def insert_app_annotation_directly(cls, args: dict, app_id: str) -> MessageAnnotation:
  152. # get app info
  153. app = (
  154. db.session.query(App)
  155. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  156. .first()
  157. )
  158. if not app:
  159. raise NotFound("App not found")
  160. annotation = MessageAnnotation(
  161. app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id
  162. )
  163. db.session.add(annotation)
  164. db.session.commit()
  165. # if annotation reply is enabled , add annotation to index
  166. annotation_setting = (
  167. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  168. )
  169. if annotation_setting:
  170. add_annotation_to_index_task.delay(
  171. annotation.id,
  172. args["question"],
  173. current_user.current_tenant_id,
  174. app_id,
  175. annotation_setting.collection_binding_id,
  176. )
  177. return annotation
  178. @classmethod
  179. def update_app_annotation_directly(cls, args: dict, app_id: str, annotation_id: str):
  180. # get app info
  181. app = (
  182. db.session.query(App)
  183. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  184. .first()
  185. )
  186. if not app:
  187. raise NotFound("App not found")
  188. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
  189. if not annotation:
  190. raise NotFound("Annotation not found")
  191. annotation.content = args["answer"]
  192. annotation.question = args["question"]
  193. db.session.commit()
  194. # if annotation reply is enabled , add annotation to index
  195. app_annotation_setting = (
  196. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  197. )
  198. if app_annotation_setting:
  199. update_annotation_to_index_task.delay(
  200. annotation.id,
  201. annotation.question,
  202. current_user.current_tenant_id,
  203. app_id,
  204. app_annotation_setting.collection_binding_id,
  205. )
  206. return annotation
  207. @classmethod
  208. def delete_app_annotation(cls, app_id: str, annotation_id: str):
  209. # get app info
  210. app = (
  211. db.session.query(App)
  212. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  213. .first()
  214. )
  215. if not app:
  216. raise NotFound("App not found")
  217. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
  218. if not annotation:
  219. raise NotFound("Annotation not found")
  220. db.session.delete(annotation)
  221. annotation_hit_histories = (
  222. db.session.query(AppAnnotationHitHistory)
  223. .filter(AppAnnotationHitHistory.annotation_id == annotation_id)
  224. .all()
  225. )
  226. if annotation_hit_histories:
  227. for annotation_hit_history in annotation_hit_histories:
  228. db.session.delete(annotation_hit_history)
  229. db.session.commit()
  230. # if annotation reply is enabled , delete annotation index
  231. app_annotation_setting = (
  232. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  233. )
  234. if app_annotation_setting:
  235. delete_annotation_index_task.delay(
  236. annotation.id, app_id, current_user.current_tenant_id, app_annotation_setting.collection_binding_id
  237. )
  238. @classmethod
  239. def batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
  240. # get app info
  241. app = (
  242. db.session.query(App)
  243. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  244. .first()
  245. )
  246. if not app:
  247. raise NotFound("App not found")
  248. try:
  249. # Skip the first row
  250. df = pd.read_csv(file)
  251. result = []
  252. for index, row in df.iterrows():
  253. content = {"question": row[0], "answer": row[1]}
  254. result.append(content)
  255. if len(result) == 0:
  256. raise ValueError("The CSV file is empty.")
  257. # check annotation limit
  258. features = FeatureService.get_features(current_user.current_tenant_id)
  259. if features.billing.enabled:
  260. annotation_quota_limit = features.annotation_quota_limit
  261. if annotation_quota_limit.limit < len(result) + annotation_quota_limit.size:
  262. raise ValueError("The number of annotations exceeds the limit of your subscription.")
  263. # async job
  264. job_id = str(uuid.uuid4())
  265. indexing_cache_key = "app_annotation_batch_import_{}".format(str(job_id))
  266. # send batch add segments task
  267. redis_client.setnx(indexing_cache_key, "waiting")
  268. batch_import_annotations_task.delay(
  269. str(job_id), result, app_id, current_user.current_tenant_id, current_user.id
  270. )
  271. except Exception as e:
  272. return {"error_msg": str(e)}
  273. return {"job_id": job_id, "job_status": "waiting"}
  274. @classmethod
  275. def get_annotation_hit_histories(cls, app_id: str, annotation_id: str, page, limit):
  276. # get app info
  277. app = (
  278. db.session.query(App)
  279. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  280. .first()
  281. )
  282. if not app:
  283. raise NotFound("App not found")
  284. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
  285. if not annotation:
  286. raise NotFound("Annotation not found")
  287. annotation_hit_histories = (
  288. db.session.query(AppAnnotationHitHistory)
  289. .filter(
  290. AppAnnotationHitHistory.app_id == app_id,
  291. AppAnnotationHitHistory.annotation_id == annotation_id,
  292. )
  293. .order_by(AppAnnotationHitHistory.created_at.desc())
  294. .paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
  295. )
  296. return annotation_hit_histories.items, annotation_hit_histories.total
  297. @classmethod
  298. def get_annotation_by_id(cls, annotation_id: str) -> MessageAnnotation | None:
  299. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
  300. if not annotation:
  301. return None
  302. return annotation
  303. @classmethod
  304. def add_annotation_history(
  305. cls,
  306. annotation_id: str,
  307. app_id: str,
  308. annotation_question: str,
  309. annotation_content: str,
  310. query: str,
  311. user_id: str,
  312. message_id: str,
  313. from_source: str,
  314. score: float,
  315. ):
  316. # add hit count to annotation
  317. db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).update(
  318. {MessageAnnotation.hit_count: MessageAnnotation.hit_count + 1}, synchronize_session=False
  319. )
  320. annotation_hit_history = AppAnnotationHitHistory(
  321. annotation_id=annotation_id,
  322. app_id=app_id,
  323. account_id=user_id,
  324. question=query,
  325. source=from_source,
  326. score=score,
  327. message_id=message_id,
  328. annotation_question=annotation_question,
  329. annotation_content=annotation_content,
  330. )
  331. db.session.add(annotation_hit_history)
  332. db.session.commit()
  333. @classmethod
  334. def get_app_annotation_setting_by_app_id(cls, app_id: str):
  335. # get app info
  336. app = (
  337. db.session.query(App)
  338. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  339. .first()
  340. )
  341. if not app:
  342. raise NotFound("App not found")
  343. annotation_setting = (
  344. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  345. )
  346. if annotation_setting:
  347. collection_binding_detail = annotation_setting.collection_binding_detail
  348. return {
  349. "id": annotation_setting.id,
  350. "enabled": True,
  351. "score_threshold": annotation_setting.score_threshold,
  352. "embedding_model": {
  353. "embedding_provider_name": collection_binding_detail.provider_name,
  354. "embedding_model_name": collection_binding_detail.model_name,
  355. },
  356. }
  357. return {"enabled": False}
  358. @classmethod
  359. def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
  360. # get app info
  361. app = (
  362. db.session.query(App)
  363. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  364. .first()
  365. )
  366. if not app:
  367. raise NotFound("App not found")
  368. annotation_setting = (
  369. db.session.query(AppAnnotationSetting)
  370. .filter(
  371. AppAnnotationSetting.app_id == app_id,
  372. AppAnnotationSetting.id == annotation_setting_id,
  373. )
  374. .first()
  375. )
  376. if not annotation_setting:
  377. raise NotFound("App annotation not found")
  378. annotation_setting.score_threshold = args["score_threshold"]
  379. annotation_setting.updated_user_id = current_user.id
  380. annotation_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
  381. db.session.add(annotation_setting)
  382. db.session.commit()
  383. collection_binding_detail = annotation_setting.collection_binding_detail
  384. return {
  385. "id": annotation_setting.id,
  386. "enabled": True,
  387. "score_threshold": annotation_setting.score_threshold,
  388. "embedding_model": {
  389. "embedding_provider_name": collection_binding_detail.provider_name,
  390. "embedding_model_name": collection_binding_detail.model_name,
  391. },
  392. }