Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

annotation_service.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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. .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. stmt = (
  115. select(MessageAnnotation)
  116. .filter(MessageAnnotation.app_id == app_id)
  117. .filter(
  118. or_(
  119. MessageAnnotation.question.ilike("%{}%".format(keyword)),
  120. MessageAnnotation.content.ilike("%{}%".format(keyword)),
  121. )
  122. )
  123. .order_by(MessageAnnotation.created_at.desc(), MessageAnnotation.id.desc())
  124. )
  125. else:
  126. stmt = (
  127. select(MessageAnnotation)
  128. .filter(MessageAnnotation.app_id == app_id)
  129. .order_by(MessageAnnotation.created_at.desc(), MessageAnnotation.id.desc())
  130. )
  131. annotations = db.paginate(select=stmt, page=page, per_page=limit, max_per_page=100, error_out=False)
  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.iloc[0], "answer": row.iloc[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. stmt = (
  288. select(AppAnnotationHitHistory)
  289. .filter(
  290. AppAnnotationHitHistory.app_id == app_id,
  291. AppAnnotationHitHistory.annotation_id == annotation_id,
  292. )
  293. .order_by(AppAnnotationHitHistory.created_at.desc())
  294. )
  295. annotation_hit_histories = db.paginate(
  296. select=stmt, page=page, per_page=limit, max_per_page=100, error_out=False
  297. )
  298. return annotation_hit_histories.items, annotation_hit_histories.total
  299. @classmethod
  300. def get_annotation_by_id(cls, annotation_id: str) -> MessageAnnotation | None:
  301. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
  302. if not annotation:
  303. return None
  304. return annotation
  305. @classmethod
  306. def add_annotation_history(
  307. cls,
  308. annotation_id: str,
  309. app_id: str,
  310. annotation_question: str,
  311. annotation_content: str,
  312. query: str,
  313. user_id: str,
  314. message_id: str,
  315. from_source: str,
  316. score: float,
  317. ):
  318. # add hit count to annotation
  319. db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).update(
  320. {MessageAnnotation.hit_count: MessageAnnotation.hit_count + 1}, synchronize_session=False
  321. )
  322. annotation_hit_history = AppAnnotationHitHistory(
  323. annotation_id=annotation_id,
  324. app_id=app_id,
  325. account_id=user_id,
  326. question=query,
  327. source=from_source,
  328. score=score,
  329. message_id=message_id,
  330. annotation_question=annotation_question,
  331. annotation_content=annotation_content,
  332. )
  333. db.session.add(annotation_hit_history)
  334. db.session.commit()
  335. @classmethod
  336. def get_app_annotation_setting_by_app_id(cls, app_id: str):
  337. # get app info
  338. app = (
  339. db.session.query(App)
  340. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  341. .first()
  342. )
  343. if not app:
  344. raise NotFound("App not found")
  345. annotation_setting = (
  346. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).first()
  347. )
  348. if annotation_setting:
  349. collection_binding_detail = annotation_setting.collection_binding_detail
  350. return {
  351. "id": annotation_setting.id,
  352. "enabled": True,
  353. "score_threshold": annotation_setting.score_threshold,
  354. "embedding_model": {
  355. "embedding_provider_name": collection_binding_detail.provider_name,
  356. "embedding_model_name": collection_binding_detail.model_name,
  357. },
  358. }
  359. return {"enabled": False}
  360. @classmethod
  361. def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
  362. # get app info
  363. app = (
  364. db.session.query(App)
  365. .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
  366. .first()
  367. )
  368. if not app:
  369. raise NotFound("App not found")
  370. annotation_setting = (
  371. db.session.query(AppAnnotationSetting)
  372. .filter(
  373. AppAnnotationSetting.app_id == app_id,
  374. AppAnnotationSetting.id == annotation_setting_id,
  375. )
  376. .first()
  377. )
  378. if not annotation_setting:
  379. raise NotFound("App annotation not found")
  380. annotation_setting.score_threshold = args["score_threshold"]
  381. annotation_setting.updated_user_id = current_user.id
  382. annotation_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
  383. db.session.add(annotation_setting)
  384. db.session.commit()
  385. collection_binding_detail = annotation_setting.collection_binding_detail
  386. return {
  387. "id": annotation_setting.id,
  388. "enabled": True,
  389. "score_threshold": annotation_setting.score_threshold,
  390. "embedding_model": {
  391. "embedding_provider_name": collection_binding_detail.provider_name,
  392. "embedding_model_name": collection_binding_detail.model_name,
  393. },
  394. }