annotation_service.py 17 KB

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