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

annotation_service.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import datetime
  2. import uuid
  3. from typing import cast
  4. import pandas as pd
  5. from flask_login import current_user
  6. from sqlalchemy import or_, select
  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. .where(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).where(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 = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  58. if annotation_setting:
  59. add_annotation_to_index_task.delay(
  60. annotation.id,
  61. args["question"],
  62. current_user.current_tenant_id,
  63. app_id,
  64. annotation_setting.collection_binding_id,
  65. )
  66. return cast(MessageAnnotation, annotation)
  67. @classmethod
  68. def enable_app_annotation(cls, args: dict, app_id: str) -> dict:
  69. enable_app_annotation_key = f"enable_app_annotation_{str(app_id)}"
  70. cache_result = redis_client.get(enable_app_annotation_key)
  71. if cache_result is not None:
  72. return {"job_id": cache_result, "job_status": "processing"}
  73. # async job
  74. job_id = str(uuid.uuid4())
  75. enable_app_annotation_job_key = f"enable_app_annotation_job_{str(job_id)}"
  76. # send batch add segments task
  77. redis_client.setnx(enable_app_annotation_job_key, "waiting")
  78. enable_annotation_reply_task.delay(
  79. str(job_id),
  80. app_id,
  81. current_user.id,
  82. current_user.current_tenant_id,
  83. args["score_threshold"],
  84. args["embedding_provider_name"],
  85. args["embedding_model_name"],
  86. )
  87. return {"job_id": job_id, "job_status": "waiting"}
  88. @classmethod
  89. def disable_app_annotation(cls, app_id: str) -> dict:
  90. disable_app_annotation_key = f"disable_app_annotation_{str(app_id)}"
  91. cache_result = redis_client.get(disable_app_annotation_key)
  92. if cache_result is not None:
  93. return {"job_id": cache_result, "job_status": "processing"}
  94. # async job
  95. job_id = str(uuid.uuid4())
  96. disable_app_annotation_job_key = f"disable_app_annotation_job_{str(job_id)}"
  97. # send batch add segments task
  98. redis_client.setnx(disable_app_annotation_job_key, "waiting")
  99. disable_annotation_reply_task.delay(str(job_id), app_id, current_user.current_tenant_id)
  100. return {"job_id": job_id, "job_status": "waiting"}
  101. @classmethod
  102. def get_annotation_list_by_app_id(cls, app_id: str, page: int, limit: int, keyword: str):
  103. # get app info
  104. app = (
  105. db.session.query(App)
  106. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  107. .first()
  108. )
  109. if not app:
  110. raise NotFound("App not found")
  111. if keyword:
  112. stmt = (
  113. select(MessageAnnotation)
  114. .where(MessageAnnotation.app_id == app_id)
  115. .where(
  116. or_(
  117. MessageAnnotation.question.ilike(f"%{keyword}%"),
  118. MessageAnnotation.content.ilike(f"%{keyword}%"),
  119. )
  120. )
  121. .order_by(MessageAnnotation.created_at.desc(), MessageAnnotation.id.desc())
  122. )
  123. else:
  124. stmt = (
  125. select(MessageAnnotation)
  126. .where(MessageAnnotation.app_id == app_id)
  127. .order_by(MessageAnnotation.created_at.desc(), MessageAnnotation.id.desc())
  128. )
  129. annotations = db.paginate(select=stmt, page=page, per_page=limit, max_per_page=100, error_out=False)
  130. return annotations.items, annotations.total
  131. @classmethod
  132. def export_annotation_list_by_app_id(cls, app_id: str):
  133. # get app info
  134. app = (
  135. db.session.query(App)
  136. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  137. .first()
  138. )
  139. if not app:
  140. raise NotFound("App not found")
  141. annotations = (
  142. db.session.query(MessageAnnotation)
  143. .where(MessageAnnotation.app_id == app_id)
  144. .order_by(MessageAnnotation.created_at.desc())
  145. .all()
  146. )
  147. return annotations
  148. @classmethod
  149. def insert_app_annotation_directly(cls, args: dict, app_id: str) -> MessageAnnotation:
  150. # get app info
  151. app = (
  152. db.session.query(App)
  153. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  154. .first()
  155. )
  156. if not app:
  157. raise NotFound("App not found")
  158. annotation = MessageAnnotation(
  159. app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id
  160. )
  161. db.session.add(annotation)
  162. db.session.commit()
  163. # if annotation reply is enabled , add annotation to index
  164. annotation_setting = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  165. if annotation_setting:
  166. add_annotation_to_index_task.delay(
  167. annotation.id,
  168. args["question"],
  169. current_user.current_tenant_id,
  170. app_id,
  171. annotation_setting.collection_binding_id,
  172. )
  173. return annotation
  174. @classmethod
  175. def update_app_annotation_directly(cls, args: dict, app_id: str, annotation_id: str):
  176. # get app info
  177. app = (
  178. db.session.query(App)
  179. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  180. .first()
  181. )
  182. if not app:
  183. raise NotFound("App not found")
  184. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  185. if not annotation:
  186. raise NotFound("Annotation not found")
  187. annotation.content = args["answer"]
  188. annotation.question = args["question"]
  189. db.session.commit()
  190. # if annotation reply is enabled , add annotation to index
  191. app_annotation_setting = (
  192. db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  193. )
  194. if app_annotation_setting:
  195. update_annotation_to_index_task.delay(
  196. annotation.id,
  197. annotation.question,
  198. current_user.current_tenant_id,
  199. app_id,
  200. app_annotation_setting.collection_binding_id,
  201. )
  202. return annotation
  203. @classmethod
  204. def delete_app_annotation(cls, app_id: str, annotation_id: str):
  205. # get app info
  206. app = (
  207. db.session.query(App)
  208. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  209. .first()
  210. )
  211. if not app:
  212. raise NotFound("App not found")
  213. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  214. if not annotation:
  215. raise NotFound("Annotation not found")
  216. db.session.delete(annotation)
  217. annotation_hit_histories = (
  218. db.session.query(AppAnnotationHitHistory)
  219. .where(AppAnnotationHitHistory.annotation_id == annotation_id)
  220. .all()
  221. )
  222. if annotation_hit_histories:
  223. for annotation_hit_history in annotation_hit_histories:
  224. db.session.delete(annotation_hit_history)
  225. db.session.commit()
  226. # if annotation reply is enabled , delete annotation index
  227. app_annotation_setting = (
  228. db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  229. )
  230. if app_annotation_setting:
  231. delete_annotation_index_task.delay(
  232. annotation.id, app_id, current_user.current_tenant_id, app_annotation_setting.collection_binding_id
  233. )
  234. @classmethod
  235. def delete_app_annotations_in_batch(cls, app_id: str, annotation_ids: list[str]):
  236. # get app info
  237. app = (
  238. db.session.query(App)
  239. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  240. .first()
  241. )
  242. if not app:
  243. raise NotFound("App not found")
  244. # Fetch annotations and their settings in a single query
  245. annotations_to_delete = (
  246. db.session.query(MessageAnnotation, AppAnnotationSetting)
  247. .outerjoin(AppAnnotationSetting, MessageAnnotation.app_id == AppAnnotationSetting.app_id)
  248. .filter(MessageAnnotation.id.in_(annotation_ids))
  249. .all()
  250. )
  251. if not annotations_to_delete:
  252. return {"deleted_count": 0}
  253. # Step 1: Extract IDs for bulk operations
  254. annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete]
  255. # Step 2: Bulk delete hit histories in a single query
  256. db.session.query(AppAnnotationHitHistory).where(
  257. AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete)
  258. ).delete(synchronize_session=False)
  259. # Step 3: Trigger async tasks for search index deletion
  260. for annotation, annotation_setting in annotations_to_delete:
  261. if annotation_setting:
  262. delete_annotation_index_task.delay(
  263. annotation.id, app_id, current_user.current_tenant_id, annotation_setting.collection_binding_id
  264. )
  265. # Step 4: Bulk delete annotations in a single query
  266. deleted_count = (
  267. db.session.query(MessageAnnotation)
  268. .where(MessageAnnotation.id.in_(annotation_ids_to_delete))
  269. .delete(synchronize_session=False)
  270. )
  271. db.session.commit()
  272. return {"deleted_count": deleted_count}
  273. @classmethod
  274. def batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
  275. # get app info
  276. app = (
  277. db.session.query(App)
  278. .where(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. try:
  284. # Skip the first row
  285. df = pd.read_csv(file, dtype=str)
  286. result = []
  287. for index, row in df.iterrows():
  288. content = {"question": row.iloc[0], "answer": row.iloc[1]}
  289. result.append(content)
  290. if len(result) == 0:
  291. raise ValueError("The CSV file is empty.")
  292. # check annotation limit
  293. features = FeatureService.get_features(current_user.current_tenant_id)
  294. if features.billing.enabled:
  295. annotation_quota_limit = features.annotation_quota_limit
  296. if annotation_quota_limit.limit < len(result) + annotation_quota_limit.size:
  297. raise ValueError("The number of annotations exceeds the limit of your subscription.")
  298. # async job
  299. job_id = str(uuid.uuid4())
  300. indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
  301. # send batch add segments task
  302. redis_client.setnx(indexing_cache_key, "waiting")
  303. batch_import_annotations_task.delay(
  304. str(job_id), result, app_id, current_user.current_tenant_id, current_user.id
  305. )
  306. except Exception as e:
  307. return {"error_msg": str(e)}
  308. return {"job_id": job_id, "job_status": "waiting"}
  309. @classmethod
  310. def get_annotation_hit_histories(cls, app_id: str, annotation_id: str, page, limit):
  311. # get app info
  312. app = (
  313. db.session.query(App)
  314. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  315. .first()
  316. )
  317. if not app:
  318. raise NotFound("App not found")
  319. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  320. if not annotation:
  321. raise NotFound("Annotation not found")
  322. stmt = (
  323. select(AppAnnotationHitHistory)
  324. .where(
  325. AppAnnotationHitHistory.app_id == app_id,
  326. AppAnnotationHitHistory.annotation_id == annotation_id,
  327. )
  328. .order_by(AppAnnotationHitHistory.created_at.desc())
  329. )
  330. annotation_hit_histories = db.paginate(
  331. select=stmt, page=page, per_page=limit, max_per_page=100, error_out=False
  332. )
  333. return annotation_hit_histories.items, annotation_hit_histories.total
  334. @classmethod
  335. def get_annotation_by_id(cls, annotation_id: str) -> MessageAnnotation | None:
  336. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  337. if not annotation:
  338. return None
  339. return annotation
  340. @classmethod
  341. def add_annotation_history(
  342. cls,
  343. annotation_id: str,
  344. app_id: str,
  345. annotation_question: str,
  346. annotation_content: str,
  347. query: str,
  348. user_id: str,
  349. message_id: str,
  350. from_source: str,
  351. score: float,
  352. ):
  353. # add hit count to annotation
  354. db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).update(
  355. {MessageAnnotation.hit_count: MessageAnnotation.hit_count + 1}, synchronize_session=False
  356. )
  357. annotation_hit_history = AppAnnotationHitHistory(
  358. annotation_id=annotation_id,
  359. app_id=app_id,
  360. account_id=user_id,
  361. question=query,
  362. source=from_source,
  363. score=score,
  364. message_id=message_id,
  365. annotation_question=annotation_question,
  366. annotation_content=annotation_content,
  367. )
  368. db.session.add(annotation_hit_history)
  369. db.session.commit()
  370. @classmethod
  371. def get_app_annotation_setting_by_app_id(cls, app_id: str):
  372. # get app info
  373. app = (
  374. db.session.query(App)
  375. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  376. .first()
  377. )
  378. if not app:
  379. raise NotFound("App not found")
  380. annotation_setting = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  381. if annotation_setting:
  382. collection_binding_detail = annotation_setting.collection_binding_detail
  383. return {
  384. "id": annotation_setting.id,
  385. "enabled": True,
  386. "score_threshold": annotation_setting.score_threshold,
  387. "embedding_model": {
  388. "embedding_provider_name": collection_binding_detail.provider_name,
  389. "embedding_model_name": collection_binding_detail.model_name,
  390. },
  391. }
  392. return {"enabled": False}
  393. @classmethod
  394. def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
  395. # get app info
  396. app = (
  397. db.session.query(App)
  398. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  399. .first()
  400. )
  401. if not app:
  402. raise NotFound("App not found")
  403. annotation_setting = (
  404. db.session.query(AppAnnotationSetting)
  405. .where(
  406. AppAnnotationSetting.app_id == app_id,
  407. AppAnnotationSetting.id == annotation_setting_id,
  408. )
  409. .first()
  410. )
  411. if not annotation_setting:
  412. raise NotFound("App annotation not found")
  413. annotation_setting.score_threshold = args["score_threshold"]
  414. annotation_setting.updated_user_id = current_user.id
  415. annotation_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
  416. db.session.add(annotation_setting)
  417. db.session.commit()
  418. collection_binding_detail = annotation_setting.collection_binding_detail
  419. return {
  420. "id": annotation_setting.id,
  421. "enabled": True,
  422. "score_threshold": annotation_setting.score_threshold,
  423. "embedding_model": {
  424. "embedding_provider_name": collection_binding_detail.provider_name,
  425. "embedding_model_name": collection_binding_detail.model_name,
  426. },
  427. }
  428. @classmethod
  429. def clear_all_annotations(cls, app_id: str) -> dict:
  430. app = (
  431. db.session.query(App)
  432. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  433. .first()
  434. )
  435. if not app:
  436. raise NotFound("App not found")
  437. # if annotation reply is enabled, delete annotation index
  438. app_annotation_setting = (
  439. db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  440. )
  441. annotations_query = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_id)
  442. for annotation in annotations_query.yield_per(100):
  443. annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).where(
  444. AppAnnotationHitHistory.annotation_id == annotation.id
  445. )
  446. for annotation_hit_history in annotation_hit_histories_query.yield_per(100):
  447. db.session.delete(annotation_hit_history)
  448. # if annotation reply is enabled, delete annotation index
  449. if app_annotation_setting:
  450. delete_annotation_index_task.delay(
  451. annotation.id, app_id, current_user.current_tenant_id, app_annotation_setting.collection_binding_id
  452. )
  453. db.session.delete(annotation)
  454. db.session.commit()
  455. return {"result": "success"}