Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

annotation_service.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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 batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
  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. try:
  245. # Skip the first row
  246. df = pd.read_csv(file)
  247. result = []
  248. for index, row in df.iterrows():
  249. content = {"question": row.iloc[0], "answer": row.iloc[1]}
  250. result.append(content)
  251. if len(result) == 0:
  252. raise ValueError("The CSV file is empty.")
  253. # check annotation limit
  254. features = FeatureService.get_features(current_user.current_tenant_id)
  255. if features.billing.enabled:
  256. annotation_quota_limit = features.annotation_quota_limit
  257. if annotation_quota_limit.limit < len(result) + annotation_quota_limit.size:
  258. raise ValueError("The number of annotations exceeds the limit of your subscription.")
  259. # async job
  260. job_id = str(uuid.uuid4())
  261. indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
  262. # send batch add segments task
  263. redis_client.setnx(indexing_cache_key, "waiting")
  264. batch_import_annotations_task.delay(
  265. str(job_id), result, app_id, current_user.current_tenant_id, current_user.id
  266. )
  267. except Exception as e:
  268. return {"error_msg": str(e)}
  269. return {"job_id": job_id, "job_status": "waiting"}
  270. @classmethod
  271. def get_annotation_hit_histories(cls, app_id: str, annotation_id: str, page, limit):
  272. # get app info
  273. app = (
  274. db.session.query(App)
  275. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  276. .first()
  277. )
  278. if not app:
  279. raise NotFound("App not found")
  280. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  281. if not annotation:
  282. raise NotFound("Annotation not found")
  283. stmt = (
  284. select(AppAnnotationHitHistory)
  285. .where(
  286. AppAnnotationHitHistory.app_id == app_id,
  287. AppAnnotationHitHistory.annotation_id == annotation_id,
  288. )
  289. .order_by(AppAnnotationHitHistory.created_at.desc())
  290. )
  291. annotation_hit_histories = db.paginate(
  292. select=stmt, 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).where(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).where(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. .where(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 = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
  342. if annotation_setting:
  343. collection_binding_detail = annotation_setting.collection_binding_detail
  344. return {
  345. "id": annotation_setting.id,
  346. "enabled": True,
  347. "score_threshold": annotation_setting.score_threshold,
  348. "embedding_model": {
  349. "embedding_provider_name": collection_binding_detail.provider_name,
  350. "embedding_model_name": collection_binding_detail.model_name,
  351. },
  352. }
  353. return {"enabled": False}
  354. @classmethod
  355. def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
  356. # get app info
  357. app = (
  358. db.session.query(App)
  359. .where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  360. .first()
  361. )
  362. if not app:
  363. raise NotFound("App not found")
  364. annotation_setting = (
  365. db.session.query(AppAnnotationSetting)
  366. .where(
  367. AppAnnotationSetting.app_id == app_id,
  368. AppAnnotationSetting.id == annotation_setting_id,
  369. )
  370. .first()
  371. )
  372. if not annotation_setting:
  373. raise NotFound("App annotation not found")
  374. annotation_setting.score_threshold = args["score_threshold"]
  375. annotation_setting.updated_user_id = current_user.id
  376. annotation_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
  377. db.session.add(annotation_setting)
  378. db.session.commit()
  379. collection_binding_detail = annotation_setting.collection_binding_detail
  380. return {
  381. "id": annotation_setting.id,
  382. "enabled": True,
  383. "score_threshold": annotation_setting.score_threshold,
  384. "embedding_model": {
  385. "embedding_provider_name": collection_binding_detail.provider_name,
  386. "embedding_model_name": collection_binding_detail.model_name,
  387. },
  388. }
  389. @classmethod
  390. def clear_all_annotations(cls, app_id: str) -> dict:
  391. app = (
  392. db.session.query(App)
  393. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  394. .first()
  395. )
  396. if not app:
  397. raise NotFound("App not found")
  398. annotations_query = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app_id)
  399. for annotation in annotations_query.yield_per(100):
  400. annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).filter(
  401. AppAnnotationHitHistory.annotation_id == annotation.id
  402. )
  403. for annotation_hit_history in annotation_hit_histories_query.yield_per(100):
  404. db.session.delete(annotation_hit_history)
  405. db.session.delete(annotation)
  406. db.session.commit()
  407. return {"result": "success"}