test_annotation_service.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252
  1. from unittest.mock import patch
  2. import pytest
  3. from faker import Faker
  4. from werkzeug.exceptions import NotFound
  5. from models.model import MessageAnnotation
  6. from services.annotation_service import AppAnnotationService
  7. from services.app_service import AppService
  8. class TestAnnotationService:
  9. """Integration tests for AnnotationService using testcontainers."""
  10. @pytest.fixture
  11. def mock_external_service_dependencies(self):
  12. """Mock setup for external service dependencies."""
  13. with (
  14. patch("services.account_service.FeatureService") as mock_account_feature_service,
  15. patch("services.annotation_service.FeatureService") as mock_feature_service,
  16. patch("services.annotation_service.add_annotation_to_index_task") as mock_add_task,
  17. patch("services.annotation_service.update_annotation_to_index_task") as mock_update_task,
  18. patch("services.annotation_service.delete_annotation_index_task") as mock_delete_task,
  19. patch("services.annotation_service.enable_annotation_reply_task") as mock_enable_task,
  20. patch("services.annotation_service.disable_annotation_reply_task") as mock_disable_task,
  21. patch("services.annotation_service.batch_import_annotations_task") as mock_batch_import_task,
  22. patch("services.annotation_service.current_user") as mock_current_user,
  23. ):
  24. # Setup default mock returns
  25. mock_account_feature_service.get_features.return_value.billing.enabled = False
  26. mock_add_task.delay.return_value = None
  27. mock_update_task.delay.return_value = None
  28. mock_delete_task.delay.return_value = None
  29. mock_enable_task.delay.return_value = None
  30. mock_disable_task.delay.return_value = None
  31. mock_batch_import_task.delay.return_value = None
  32. yield {
  33. "account_feature_service": mock_account_feature_service,
  34. "feature_service": mock_feature_service,
  35. "add_task": mock_add_task,
  36. "update_task": mock_update_task,
  37. "delete_task": mock_delete_task,
  38. "enable_task": mock_enable_task,
  39. "disable_task": mock_disable_task,
  40. "batch_import_task": mock_batch_import_task,
  41. "current_user": mock_current_user,
  42. }
  43. def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies):
  44. """
  45. Helper method to create a test app and account for testing.
  46. Args:
  47. db_session_with_containers: Database session from testcontainers infrastructure
  48. mock_external_service_dependencies: Mock dependencies
  49. Returns:
  50. tuple: (app, account) - Created app and account instances
  51. """
  52. fake = Faker()
  53. # Setup mocks for account creation
  54. mock_external_service_dependencies[
  55. "account_feature_service"
  56. ].get_system_features.return_value.is_allow_register = True
  57. # Create account and tenant first
  58. from services.account_service import AccountService, TenantService
  59. account = AccountService.create_account(
  60. email=fake.email(),
  61. name=fake.name(),
  62. interface_language="en-US",
  63. password=fake.password(length=12),
  64. )
  65. TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
  66. tenant = account.current_tenant
  67. # Setup app creation arguments
  68. app_args = {
  69. "name": fake.company(),
  70. "description": fake.text(max_nb_chars=100),
  71. "mode": "chat",
  72. "icon_type": "emoji",
  73. "icon": "📚",
  74. "icon_background": "#FF6B6B",
  75. "api_rph": 100,
  76. "api_rpm": 10,
  77. }
  78. # Create app
  79. app_service = AppService()
  80. app = app_service.create_app(tenant.id, app_args, account)
  81. # Setup current_user mock
  82. self._mock_current_user(mock_external_service_dependencies, account.id, tenant.id)
  83. return app, account
  84. def _mock_current_user(self, mock_external_service_dependencies, account_id, tenant_id):
  85. """
  86. Helper method to mock the current user for testing.
  87. """
  88. mock_external_service_dependencies["current_user"].id = account_id
  89. mock_external_service_dependencies["current_user"].current_tenant_id = tenant_id
  90. def _create_test_conversation(self, app, account, fake):
  91. """
  92. Helper method to create a test conversation with all required fields.
  93. """
  94. from extensions.ext_database import db
  95. from models.model import Conversation
  96. conversation = Conversation(
  97. app_id=app.id,
  98. app_model_config_id=None,
  99. model_provider=None,
  100. model_id="",
  101. override_model_configs=None,
  102. mode=app.mode,
  103. name=fake.sentence(),
  104. inputs={},
  105. introduction="",
  106. system_instruction="",
  107. system_instruction_tokens=0,
  108. status="normal",
  109. invoke_from="console",
  110. from_source="console",
  111. from_end_user_id=None,
  112. from_account_id=account.id,
  113. )
  114. db.session.add(conversation)
  115. db.session.flush()
  116. return conversation
  117. def _create_test_message(self, app, conversation, account, fake):
  118. """
  119. Helper method to create a test message with all required fields.
  120. """
  121. import json
  122. from extensions.ext_database import db
  123. from models.model import Message
  124. message = Message(
  125. app_id=app.id,
  126. model_provider=None,
  127. model_id="",
  128. override_model_configs=None,
  129. conversation_id=conversation.id,
  130. inputs={},
  131. query=fake.sentence(),
  132. message=json.dumps([{"role": "user", "text": fake.sentence()}]),
  133. message_tokens=0,
  134. message_unit_price=0,
  135. message_price_unit=0.001,
  136. answer=fake.text(max_nb_chars=200),
  137. answer_tokens=0,
  138. answer_unit_price=0,
  139. answer_price_unit=0.001,
  140. parent_message_id=None,
  141. provider_response_latency=0,
  142. total_price=0,
  143. currency="USD",
  144. invoke_from="console",
  145. from_source="console",
  146. from_end_user_id=None,
  147. from_account_id=account.id,
  148. )
  149. db.session.add(message)
  150. db.session.commit()
  151. return message
  152. def test_insert_app_annotation_directly_success(
  153. self, db_session_with_containers, mock_external_service_dependencies
  154. ):
  155. """
  156. Test successful direct insertion of app annotation.
  157. """
  158. fake = Faker()
  159. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  160. # Setup annotation data
  161. annotation_args = {
  162. "question": fake.sentence(),
  163. "answer": fake.text(max_nb_chars=200),
  164. }
  165. # Insert annotation directly
  166. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  167. # Verify annotation was created correctly
  168. assert annotation.app_id == app.id
  169. assert annotation.question == annotation_args["question"]
  170. assert annotation.content == annotation_args["answer"]
  171. assert annotation.account_id == account.id
  172. assert annotation.hit_count == 0
  173. assert annotation.id is not None
  174. # Verify annotation was saved to database
  175. from extensions.ext_database import db
  176. db.session.refresh(annotation)
  177. assert annotation.id is not None
  178. # Verify add_annotation_to_index_task was called (when annotation setting exists)
  179. # Note: In this test, no annotation setting exists, so task should not be called
  180. mock_external_service_dependencies["add_task"].delay.assert_not_called()
  181. def test_insert_app_annotation_directly_app_not_found(
  182. self, db_session_with_containers, mock_external_service_dependencies
  183. ):
  184. """
  185. Test direct insertion of app annotation when app is not found.
  186. """
  187. fake = Faker()
  188. non_existent_app_id = fake.uuid4()
  189. # Mock random current user to avoid dependency issues
  190. self._mock_current_user(mock_external_service_dependencies, fake.uuid4(), fake.uuid4())
  191. # Setup annotation data
  192. annotation_args = {
  193. "question": fake.sentence(),
  194. "answer": fake.text(max_nb_chars=200),
  195. }
  196. # Try to insert annotation with non-existent app
  197. with pytest.raises(NotFound, match="App not found"):
  198. AppAnnotationService.insert_app_annotation_directly(annotation_args, non_existent_app_id)
  199. def test_update_app_annotation_directly_success(
  200. self, db_session_with_containers, mock_external_service_dependencies
  201. ):
  202. """
  203. Test successful direct update of app annotation.
  204. """
  205. fake = Faker()
  206. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  207. # First, create an annotation
  208. original_args = {
  209. "question": fake.sentence(),
  210. "answer": fake.text(max_nb_chars=200),
  211. }
  212. annotation = AppAnnotationService.insert_app_annotation_directly(original_args, app.id)
  213. # Update the annotation
  214. updated_args = {
  215. "question": fake.sentence(),
  216. "answer": fake.text(max_nb_chars=200),
  217. }
  218. updated_annotation = AppAnnotationService.update_app_annotation_directly(updated_args, app.id, annotation.id)
  219. # Verify annotation was updated correctly
  220. assert updated_annotation.id == annotation.id
  221. assert updated_annotation.app_id == app.id
  222. assert updated_annotation.question == updated_args["question"]
  223. assert updated_annotation.content == updated_args["answer"]
  224. assert updated_annotation.account_id == account.id
  225. # Verify original values were changed
  226. assert updated_annotation.question != original_args["question"]
  227. assert updated_annotation.content != original_args["answer"]
  228. # Verify update_annotation_to_index_task was called (when annotation setting exists)
  229. # Note: In this test, no annotation setting exists, so task should not be called
  230. mock_external_service_dependencies["update_task"].delay.assert_not_called()
  231. def test_up_insert_app_annotation_from_message_new(
  232. self, db_session_with_containers, mock_external_service_dependencies
  233. ):
  234. """
  235. Test creating new annotation from message.
  236. """
  237. fake = Faker()
  238. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  239. # Create a conversation and message first
  240. conversation = self._create_test_conversation(app, account, fake)
  241. message = self._create_test_message(app, conversation, account, fake)
  242. # Setup annotation data with message_id
  243. annotation_args = {
  244. "message_id": message.id,
  245. "question": fake.sentence(),
  246. "answer": fake.text(max_nb_chars=200),
  247. }
  248. # Insert annotation from message
  249. annotation = AppAnnotationService.up_insert_app_annotation_from_message(annotation_args, app.id)
  250. # Verify annotation was created correctly
  251. assert annotation.app_id == app.id
  252. assert annotation.conversation_id == conversation.id
  253. assert annotation.message_id == message.id
  254. assert annotation.question == annotation_args["question"]
  255. assert annotation.content == annotation_args["answer"]
  256. assert annotation.account_id == account.id
  257. # Verify add_annotation_to_index_task was called (when annotation setting exists)
  258. # Note: In this test, no annotation setting exists, so task should not be called
  259. mock_external_service_dependencies["add_task"].delay.assert_not_called()
  260. def test_up_insert_app_annotation_from_message_update(
  261. self, db_session_with_containers, mock_external_service_dependencies
  262. ):
  263. """
  264. Test updating existing annotation from message.
  265. """
  266. fake = Faker()
  267. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  268. # Create a conversation and message first
  269. conversation = self._create_test_conversation(app, account, fake)
  270. message = self._create_test_message(app, conversation, account, fake)
  271. # Create initial annotation
  272. initial_args = {
  273. "message_id": message.id,
  274. "question": fake.sentence(),
  275. "answer": fake.text(max_nb_chars=200),
  276. }
  277. initial_annotation = AppAnnotationService.up_insert_app_annotation_from_message(initial_args, app.id)
  278. # Update the annotation
  279. updated_args = {
  280. "message_id": message.id,
  281. "question": fake.sentence(),
  282. "answer": fake.text(max_nb_chars=200),
  283. }
  284. updated_annotation = AppAnnotationService.up_insert_app_annotation_from_message(updated_args, app.id)
  285. # Verify annotation was updated correctly (same ID)
  286. assert updated_annotation.id == initial_annotation.id
  287. assert updated_annotation.question == updated_args["question"]
  288. assert updated_annotation.content == updated_args["answer"]
  289. assert updated_annotation.question != initial_args["question"]
  290. assert updated_annotation.content != initial_args["answer"]
  291. # Verify add_annotation_to_index_task was called (when annotation setting exists)
  292. # Note: In this test, no annotation setting exists, so task should not be called
  293. mock_external_service_dependencies["add_task"].delay.assert_not_called()
  294. def test_up_insert_app_annotation_from_message_app_not_found(
  295. self, db_session_with_containers, mock_external_service_dependencies
  296. ):
  297. """
  298. Test creating annotation from message when app is not found.
  299. """
  300. fake = Faker()
  301. non_existent_app_id = fake.uuid4()
  302. # Mock random current user to avoid dependency issues
  303. self._mock_current_user(mock_external_service_dependencies, fake.uuid4(), fake.uuid4())
  304. # Setup annotation data
  305. annotation_args = {
  306. "question": fake.sentence(),
  307. "answer": fake.text(max_nb_chars=200),
  308. }
  309. # Try to insert annotation with non-existent app
  310. with pytest.raises(NotFound, match="App not found"):
  311. AppAnnotationService.up_insert_app_annotation_from_message(annotation_args, non_existent_app_id)
  312. def test_get_annotation_list_by_app_id_success(
  313. self, db_session_with_containers, mock_external_service_dependencies
  314. ):
  315. """
  316. Test successful retrieval of annotation list by app ID.
  317. """
  318. fake = Faker()
  319. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  320. # Create multiple annotations
  321. annotations = []
  322. for i in range(3):
  323. annotation_args = {
  324. "question": f"Question {i}: {fake.sentence()}",
  325. "answer": f"Answer {i}: {fake.text(max_nb_chars=200)}",
  326. }
  327. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  328. annotations.append(annotation)
  329. # Get annotation list
  330. annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
  331. app.id, page=1, limit=10, keyword=""
  332. )
  333. # Verify results
  334. assert len(annotation_list) == 3
  335. assert total == 3
  336. # Verify all annotations belong to the correct app
  337. for annotation in annotation_list:
  338. assert annotation.app_id == app.id
  339. assert annotation.account_id == account.id
  340. def test_get_annotation_list_by_app_id_with_keyword(
  341. self, db_session_with_containers, mock_external_service_dependencies
  342. ):
  343. """
  344. Test retrieval of annotation list with keyword search.
  345. """
  346. fake = Faker()
  347. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  348. # Create annotations with specific keywords
  349. unique_keyword = f"unique_{fake.uuid4()[:8]}"
  350. annotation_args = {
  351. "question": f"Question with {unique_keyword} keyword",
  352. "answer": f"Answer with {unique_keyword} keyword",
  353. }
  354. AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  355. # Create another annotation without the keyword
  356. other_args = {
  357. "question": "Different question without special term",
  358. "answer": "Different answer without special content",
  359. }
  360. AppAnnotationService.insert_app_annotation_directly(other_args, app.id)
  361. # Search with keyword
  362. annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
  363. app.id, page=1, limit=10, keyword=unique_keyword
  364. )
  365. # Verify only matching annotations are returned
  366. assert len(annotation_list) == 1
  367. assert total == 1
  368. assert unique_keyword in annotation_list[0].question or unique_keyword in annotation_list[0].content
  369. def test_get_annotation_list_by_app_id_app_not_found(
  370. self, db_session_with_containers, mock_external_service_dependencies
  371. ):
  372. """
  373. Test retrieval of annotation list when app is not found.
  374. """
  375. fake = Faker()
  376. non_existent_app_id = fake.uuid4()
  377. # Mock random current user to avoid dependency issues
  378. self._mock_current_user(mock_external_service_dependencies, fake.uuid4(), fake.uuid4())
  379. # Try to get annotation list with non-existent app
  380. with pytest.raises(NotFound, match="App not found"):
  381. AppAnnotationService.get_annotation_list_by_app_id(non_existent_app_id, page=1, limit=10, keyword="")
  382. def test_delete_app_annotation_success(self, db_session_with_containers, mock_external_service_dependencies):
  383. """
  384. Test successful deletion of app annotation.
  385. """
  386. fake = Faker()
  387. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  388. # Create an annotation first
  389. annotation_args = {
  390. "question": fake.sentence(),
  391. "answer": fake.text(max_nb_chars=200),
  392. }
  393. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  394. annotation_id = annotation.id
  395. # Delete the annotation
  396. AppAnnotationService.delete_app_annotation(app.id, annotation_id)
  397. # Verify annotation was deleted
  398. from extensions.ext_database import db
  399. deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  400. assert deleted_annotation is None
  401. # Verify delete_annotation_index_task was called (when annotation setting exists)
  402. # Note: In this test, no annotation setting exists, so task should not be called
  403. mock_external_service_dependencies["delete_task"].delay.assert_not_called()
  404. def test_delete_app_annotation_app_not_found(self, db_session_with_containers, mock_external_service_dependencies):
  405. """
  406. Test deletion of app annotation when app is not found.
  407. """
  408. fake = Faker()
  409. non_existent_app_id = fake.uuid4()
  410. annotation_id = fake.uuid4()
  411. # Mock random current user to avoid dependency issues
  412. self._mock_current_user(mock_external_service_dependencies, fake.uuid4(), fake.uuid4())
  413. # Try to delete annotation with non-existent app
  414. with pytest.raises(NotFound, match="App not found"):
  415. AppAnnotationService.delete_app_annotation(non_existent_app_id, annotation_id)
  416. def test_delete_app_annotation_annotation_not_found(
  417. self, db_session_with_containers, mock_external_service_dependencies
  418. ):
  419. """
  420. Test deletion of app annotation when annotation is not found.
  421. """
  422. fake = Faker()
  423. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  424. non_existent_annotation_id = fake.uuid4()
  425. # Try to delete non-existent annotation
  426. with pytest.raises(NotFound, match="Annotation not found"):
  427. AppAnnotationService.delete_app_annotation(app.id, non_existent_annotation_id)
  428. def test_enable_app_annotation_success(self, db_session_with_containers, mock_external_service_dependencies):
  429. """
  430. Test successful enabling of app annotation.
  431. """
  432. fake = Faker()
  433. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  434. # Setup enable arguments
  435. enable_args = {
  436. "score_threshold": 0.8,
  437. "embedding_provider_name": "openai",
  438. "embedding_model_name": "text-embedding-ada-002",
  439. }
  440. # Enable annotation
  441. result = AppAnnotationService.enable_app_annotation(enable_args, app.id)
  442. # Verify result structure
  443. assert "job_id" in result
  444. assert "job_status" in result
  445. assert result["job_status"] == "waiting"
  446. assert result["job_id"] is not None
  447. # Verify task was called
  448. mock_external_service_dependencies["enable_task"].delay.assert_called_once()
  449. def test_disable_app_annotation_success(self, db_session_with_containers, mock_external_service_dependencies):
  450. """
  451. Test successful disabling of app annotation.
  452. """
  453. fake = Faker()
  454. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  455. # Disable annotation
  456. result = AppAnnotationService.disable_app_annotation(app.id)
  457. # Verify result structure
  458. assert "job_id" in result
  459. assert "job_status" in result
  460. assert result["job_status"] == "waiting"
  461. assert result["job_id"] is not None
  462. # Verify task was called
  463. mock_external_service_dependencies["disable_task"].delay.assert_called_once()
  464. def test_enable_app_annotation_cached_job(self, db_session_with_containers, mock_external_service_dependencies):
  465. """
  466. Test enabling app annotation when job is already cached.
  467. """
  468. fake = Faker()
  469. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  470. # Mock Redis to return cached job
  471. from extensions.ext_redis import redis_client
  472. cached_job_id = fake.uuid4()
  473. enable_app_annotation_key = f"enable_app_annotation_{app.id}"
  474. redis_client.set(enable_app_annotation_key, cached_job_id)
  475. # Setup enable arguments
  476. enable_args = {
  477. "score_threshold": 0.8,
  478. "embedding_provider_name": "openai",
  479. "embedding_model_name": "text-embedding-ada-002",
  480. }
  481. # Enable annotation (should return cached job)
  482. result = AppAnnotationService.enable_app_annotation(enable_args, app.id)
  483. # Verify cached result
  484. assert cached_job_id == result["job_id"].decode("utf-8")
  485. assert result["job_status"] == "processing"
  486. # Verify task was not called again
  487. mock_external_service_dependencies["enable_task"].delay.assert_not_called()
  488. # Clean up
  489. redis_client.delete(enable_app_annotation_key)
  490. def test_get_annotation_hit_histories_success(self, db_session_with_containers, mock_external_service_dependencies):
  491. """
  492. Test successful retrieval of annotation hit histories.
  493. """
  494. fake = Faker()
  495. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  496. # Create an annotation first
  497. annotation_args = {
  498. "question": fake.sentence(),
  499. "answer": fake.text(max_nb_chars=200),
  500. }
  501. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  502. # Add some hit histories
  503. for i in range(3):
  504. AppAnnotationService.add_annotation_history(
  505. annotation_id=annotation.id,
  506. app_id=app.id,
  507. annotation_question=annotation.question,
  508. annotation_content=annotation.content,
  509. query=f"Query {i}: {fake.sentence()}",
  510. user_id=account.id,
  511. message_id=fake.uuid4(),
  512. from_source="console",
  513. score=0.8 + (i * 0.1),
  514. )
  515. # Get hit histories
  516. hit_histories, total = AppAnnotationService.get_annotation_hit_histories(
  517. app.id, annotation.id, page=1, limit=10
  518. )
  519. # Verify results
  520. assert len(hit_histories) == 3
  521. assert total == 3
  522. # Verify all histories belong to the correct annotation
  523. for history in hit_histories:
  524. assert history.annotation_id == annotation.id
  525. assert history.app_id == app.id
  526. assert history.account_id == account.id
  527. def test_add_annotation_history_success(self, db_session_with_containers, mock_external_service_dependencies):
  528. """
  529. Test successful addition of annotation history.
  530. """
  531. fake = Faker()
  532. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  533. # Create an annotation first
  534. annotation_args = {
  535. "question": fake.sentence(),
  536. "answer": fake.text(max_nb_chars=200),
  537. }
  538. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  539. # Get initial hit count
  540. initial_hit_count = annotation.hit_count
  541. # Add annotation history
  542. query = fake.sentence()
  543. message_id = fake.uuid4()
  544. score = 0.85
  545. AppAnnotationService.add_annotation_history(
  546. annotation_id=annotation.id,
  547. app_id=app.id,
  548. annotation_question=annotation.question,
  549. annotation_content=annotation.content,
  550. query=query,
  551. user_id=account.id,
  552. message_id=message_id,
  553. from_source="console",
  554. score=score,
  555. )
  556. # Verify hit count was incremented
  557. from extensions.ext_database import db
  558. db.session.refresh(annotation)
  559. assert annotation.hit_count == initial_hit_count + 1
  560. # Verify history was created
  561. from models.model import AppAnnotationHitHistory
  562. history = (
  563. db.session.query(AppAnnotationHitHistory)
  564. .where(
  565. AppAnnotationHitHistory.annotation_id == annotation.id, AppAnnotationHitHistory.message_id == message_id
  566. )
  567. .first()
  568. )
  569. assert history is not None
  570. assert history.app_id == app.id
  571. assert history.account_id == account.id
  572. assert history.question == query
  573. assert history.score == score
  574. assert history.source == "console"
  575. def test_get_annotation_by_id_success(self, db_session_with_containers, mock_external_service_dependencies):
  576. """
  577. Test successful retrieval of annotation by ID.
  578. """
  579. fake = Faker()
  580. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  581. # Create an annotation
  582. annotation_args = {
  583. "question": fake.sentence(),
  584. "answer": fake.text(max_nb_chars=200),
  585. }
  586. created_annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  587. # Get annotation by ID
  588. retrieved_annotation = AppAnnotationService.get_annotation_by_id(created_annotation.id)
  589. # Verify annotation was retrieved correctly
  590. assert retrieved_annotation is not None
  591. assert retrieved_annotation.id == created_annotation.id
  592. assert retrieved_annotation.app_id == app.id
  593. assert retrieved_annotation.question == annotation_args["question"]
  594. assert retrieved_annotation.content == annotation_args["answer"]
  595. assert retrieved_annotation.account_id == account.id
  596. def test_batch_import_app_annotations_success(self, db_session_with_containers, mock_external_service_dependencies):
  597. """
  598. Test successful batch import of app annotations.
  599. """
  600. fake = Faker()
  601. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  602. # Create CSV content
  603. csv_content = "Question 1,Answer 1\nQuestion 2,Answer 2\nQuestion 3,Answer 3"
  604. # Mock FileStorage
  605. from io import BytesIO
  606. from werkzeug.datastructures import FileStorage
  607. file_storage = FileStorage(
  608. stream=BytesIO(csv_content.encode("utf-8")), filename="annotations.csv", content_type="text/csv"
  609. )
  610. mock_external_service_dependencies["feature_service"].get_features.return_value.billing.enabled = False
  611. # Mock pandas to return expected DataFrame
  612. import pandas as pd
  613. with patch("services.annotation_service.pd") as mock_pd:
  614. mock_df = pd.DataFrame(
  615. {0: ["Question 1", "Question 2", "Question 3"], 1: ["Answer 1", "Answer 2", "Answer 3"]}
  616. )
  617. mock_pd.read_csv.return_value = mock_df
  618. # Batch import annotations
  619. result = AppAnnotationService.batch_import_app_annotations(app.id, file_storage)
  620. # Verify result structure
  621. assert "job_id" in result
  622. assert "job_status" in result
  623. assert result["job_status"] == "waiting"
  624. assert result["job_id"] is not None
  625. # Verify task was called
  626. mock_external_service_dependencies["batch_import_task"].delay.assert_called_once()
  627. def test_batch_import_app_annotations_empty_file(
  628. self, db_session_with_containers, mock_external_service_dependencies
  629. ):
  630. """
  631. Test batch import with empty CSV file.
  632. """
  633. fake = Faker()
  634. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  635. # Create empty CSV content
  636. csv_content = ""
  637. # Mock FileStorage
  638. from io import BytesIO
  639. from werkzeug.datastructures import FileStorage
  640. file_storage = FileStorage(
  641. stream=BytesIO(csv_content.encode("utf-8")), filename="annotations.csv", content_type="text/csv"
  642. )
  643. # Mock pandas to return empty DataFrame
  644. import pandas as pd
  645. with patch("services.annotation_service.pd") as mock_pd:
  646. mock_df = pd.DataFrame()
  647. mock_pd.read_csv.return_value = mock_df
  648. # Batch import annotations
  649. result = AppAnnotationService.batch_import_app_annotations(app.id, file_storage)
  650. # Verify error result
  651. assert "error_msg" in result
  652. assert "empty" in result["error_msg"].lower()
  653. def test_batch_import_app_annotations_quota_exceeded(
  654. self, db_session_with_containers, mock_external_service_dependencies
  655. ):
  656. """
  657. Test batch import when quota is exceeded.
  658. """
  659. fake = Faker()
  660. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  661. # Create CSV content
  662. csv_content = "Question 1,Answer 1\nQuestion 2,Answer 2\nQuestion 3,Answer 3"
  663. # Mock FileStorage
  664. from io import BytesIO
  665. from werkzeug.datastructures import FileStorage
  666. file_storage = FileStorage(
  667. stream=BytesIO(csv_content.encode("utf-8")), filename="annotations.csv", content_type="text/csv"
  668. )
  669. # Mock pandas to return DataFrame
  670. import pandas as pd
  671. with patch("services.annotation_service.pd") as mock_pd:
  672. mock_df = pd.DataFrame(
  673. {0: ["Question 1", "Question 2", "Question 3"], 1: ["Answer 1", "Answer 2", "Answer 3"]}
  674. )
  675. mock_pd.read_csv.return_value = mock_df
  676. # Mock FeatureService to return billing enabled with quota exceeded
  677. mock_external_service_dependencies["feature_service"].get_features.return_value.billing.enabled = True
  678. mock_external_service_dependencies[
  679. "feature_service"
  680. ].get_features.return_value.annotation_quota_limit.limit = 1
  681. mock_external_service_dependencies[
  682. "feature_service"
  683. ].get_features.return_value.annotation_quota_limit.size = 0
  684. # Batch import annotations
  685. result = AppAnnotationService.batch_import_app_annotations(app.id, file_storage)
  686. # Verify error result
  687. assert "error_msg" in result
  688. assert "limit" in result["error_msg"].lower()
  689. def test_get_app_annotation_setting_by_app_id_enabled(
  690. self, db_session_with_containers, mock_external_service_dependencies
  691. ):
  692. """
  693. Test getting enabled app annotation setting by app ID.
  694. """
  695. fake = Faker()
  696. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  697. # Create annotation setting
  698. from extensions.ext_database import db
  699. from models.dataset import DatasetCollectionBinding
  700. from models.model import AppAnnotationSetting
  701. # Create a collection binding first
  702. collection_binding = DatasetCollectionBinding()
  703. collection_binding.id = fake.uuid4()
  704. collection_binding.provider_name = "openai"
  705. collection_binding.model_name = "text-embedding-ada-002"
  706. collection_binding.type = "annotation"
  707. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  708. db.session.add(collection_binding)
  709. db.session.flush()
  710. # Create annotation setting
  711. annotation_setting = AppAnnotationSetting()
  712. annotation_setting.app_id = app.id
  713. annotation_setting.score_threshold = 0.8
  714. annotation_setting.collection_binding_id = collection_binding.id
  715. annotation_setting.created_user_id = account.id
  716. annotation_setting.updated_user_id = account.id
  717. db.session.add(annotation_setting)
  718. db.session.commit()
  719. # Get annotation setting
  720. result = AppAnnotationService.get_app_annotation_setting_by_app_id(app.id)
  721. # Verify result structure
  722. assert result["enabled"] is True
  723. assert result["id"] == annotation_setting.id
  724. assert result["score_threshold"] == 0.8
  725. assert result["embedding_model"]["embedding_provider_name"] == "openai"
  726. assert result["embedding_model"]["embedding_model_name"] == "text-embedding-ada-002"
  727. def test_get_app_annotation_setting_by_app_id_disabled(
  728. self, db_session_with_containers, mock_external_service_dependencies
  729. ):
  730. """
  731. Test getting disabled app annotation setting by app ID.
  732. """
  733. fake = Faker()
  734. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  735. # Get annotation setting (no setting exists)
  736. result = AppAnnotationService.get_app_annotation_setting_by_app_id(app.id)
  737. # Verify result structure
  738. assert result["enabled"] is False
  739. def test_update_app_annotation_setting_success(
  740. self, db_session_with_containers, mock_external_service_dependencies
  741. ):
  742. """
  743. Test successful update of app annotation setting.
  744. """
  745. fake = Faker()
  746. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  747. # Create annotation setting first
  748. from extensions.ext_database import db
  749. from models.dataset import DatasetCollectionBinding
  750. from models.model import AppAnnotationSetting
  751. # Create a collection binding first
  752. collection_binding = DatasetCollectionBinding()
  753. collection_binding.id = fake.uuid4()
  754. collection_binding.provider_name = "openai"
  755. collection_binding.model_name = "text-embedding-ada-002"
  756. collection_binding.type = "annotation"
  757. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  758. db.session.add(collection_binding)
  759. db.session.flush()
  760. # Create annotation setting
  761. annotation_setting = AppAnnotationSetting()
  762. annotation_setting.app_id = app.id
  763. annotation_setting.score_threshold = 0.8
  764. annotation_setting.collection_binding_id = collection_binding.id
  765. annotation_setting.created_user_id = account.id
  766. annotation_setting.updated_user_id = account.id
  767. db.session.add(annotation_setting)
  768. db.session.commit()
  769. # Update annotation setting
  770. update_args = {
  771. "score_threshold": 0.9,
  772. }
  773. result = AppAnnotationService.update_app_annotation_setting(app.id, annotation_setting.id, update_args)
  774. # Verify result structure
  775. assert result["enabled"] is True
  776. assert result["id"] == annotation_setting.id
  777. assert result["score_threshold"] == 0.9
  778. assert result["embedding_model"]["embedding_provider_name"] == "openai"
  779. assert result["embedding_model"]["embedding_model_name"] == "text-embedding-ada-002"
  780. # Verify database was updated
  781. db.session.refresh(annotation_setting)
  782. assert annotation_setting.score_threshold == 0.9
  783. def test_export_annotation_list_by_app_id_success(
  784. self, db_session_with_containers, mock_external_service_dependencies
  785. ):
  786. """
  787. Test successful export of annotation list by app ID.
  788. """
  789. fake = Faker()
  790. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  791. # Create multiple annotations
  792. annotations = []
  793. for i in range(3):
  794. annotation_args = {
  795. "question": f"Question {i}: {fake.sentence()}",
  796. "answer": f"Answer {i}: {fake.text(max_nb_chars=200)}",
  797. }
  798. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  799. annotations.append(annotation)
  800. # Export annotation list
  801. exported_annotations = AppAnnotationService.export_annotation_list_by_app_id(app.id)
  802. # Verify results
  803. assert len(exported_annotations) == 3
  804. # Verify all annotations belong to the correct app and are ordered by created_at desc
  805. for i, annotation in enumerate(exported_annotations):
  806. assert annotation.app_id == app.id
  807. assert annotation.account_id == account.id
  808. if i > 0:
  809. # Verify descending order (newer first)
  810. assert annotation.created_at <= exported_annotations[i - 1].created_at
  811. def test_export_annotation_list_by_app_id_app_not_found(
  812. self, db_session_with_containers, mock_external_service_dependencies
  813. ):
  814. """
  815. Test export of annotation list when app is not found.
  816. """
  817. fake = Faker()
  818. non_existent_app_id = fake.uuid4()
  819. # Mock random current user to avoid dependency issues
  820. self._mock_current_user(mock_external_service_dependencies, fake.uuid4(), fake.uuid4())
  821. # Try to export annotation list with non-existent app
  822. with pytest.raises(NotFound, match="App not found"):
  823. AppAnnotationService.export_annotation_list_by_app_id(non_existent_app_id)
  824. def test_insert_app_annotation_directly_with_setting_success(
  825. self, db_session_with_containers, mock_external_service_dependencies
  826. ):
  827. """
  828. Test successful direct insertion of app annotation with annotation setting enabled.
  829. """
  830. fake = Faker()
  831. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  832. # Create annotation setting first
  833. from extensions.ext_database import db
  834. from models.dataset import DatasetCollectionBinding
  835. from models.model import AppAnnotationSetting
  836. # Create a collection binding first
  837. collection_binding = DatasetCollectionBinding()
  838. collection_binding.id = fake.uuid4()
  839. collection_binding.provider_name = "openai"
  840. collection_binding.model_name = "text-embedding-ada-002"
  841. collection_binding.type = "annotation"
  842. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  843. db.session.add(collection_binding)
  844. db.session.flush()
  845. # Create annotation setting
  846. annotation_setting = AppAnnotationSetting()
  847. annotation_setting.app_id = app.id
  848. annotation_setting.score_threshold = 0.8
  849. annotation_setting.collection_binding_id = collection_binding.id
  850. annotation_setting.created_user_id = account.id
  851. annotation_setting.updated_user_id = account.id
  852. db.session.add(annotation_setting)
  853. db.session.commit()
  854. # Setup annotation data
  855. annotation_args = {
  856. "question": fake.sentence(),
  857. "answer": fake.text(max_nb_chars=200),
  858. }
  859. # Insert annotation directly
  860. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  861. # Verify annotation was created correctly
  862. assert annotation.app_id == app.id
  863. assert annotation.question == annotation_args["question"]
  864. assert annotation.content == annotation_args["answer"]
  865. assert annotation.account_id == account.id
  866. assert annotation.hit_count == 0
  867. assert annotation.id is not None
  868. # Verify add_annotation_to_index_task was called
  869. mock_external_service_dependencies["add_task"].delay.assert_called_once()
  870. call_args = mock_external_service_dependencies["add_task"].delay.call_args[0]
  871. assert call_args[0] == annotation.id # annotation_id
  872. assert call_args[1] == annotation_args["question"] # question
  873. assert call_args[2] == account.current_tenant_id # tenant_id
  874. assert call_args[3] == app.id # app_id
  875. assert call_args[4] == collection_binding.id # collection_binding_id
  876. def test_update_app_annotation_directly_with_setting_success(
  877. self, db_session_with_containers, mock_external_service_dependencies
  878. ):
  879. """
  880. Test successful direct update of app annotation with annotation setting enabled.
  881. """
  882. fake = Faker()
  883. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  884. # Create annotation setting first
  885. from extensions.ext_database import db
  886. from models.dataset import DatasetCollectionBinding
  887. from models.model import AppAnnotationSetting
  888. # Create a collection binding first
  889. collection_binding = DatasetCollectionBinding()
  890. collection_binding.id = fake.uuid4()
  891. collection_binding.provider_name = "openai"
  892. collection_binding.model_name = "text-embedding-ada-002"
  893. collection_binding.type = "annotation"
  894. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  895. db.session.add(collection_binding)
  896. db.session.flush()
  897. # Create annotation setting
  898. annotation_setting = AppAnnotationSetting()
  899. annotation_setting.app_id = app.id
  900. annotation_setting.score_threshold = 0.8
  901. annotation_setting.collection_binding_id = collection_binding.id
  902. annotation_setting.created_user_id = account.id
  903. annotation_setting.updated_user_id = account.id
  904. db.session.add(annotation_setting)
  905. db.session.commit()
  906. # First, create an annotation
  907. original_args = {
  908. "question": fake.sentence(),
  909. "answer": fake.text(max_nb_chars=200),
  910. }
  911. annotation = AppAnnotationService.insert_app_annotation_directly(original_args, app.id)
  912. # Reset mock to clear previous calls
  913. mock_external_service_dependencies["update_task"].delay.reset_mock()
  914. # Update the annotation
  915. updated_args = {
  916. "question": fake.sentence(),
  917. "answer": fake.text(max_nb_chars=200),
  918. }
  919. updated_annotation = AppAnnotationService.update_app_annotation_directly(updated_args, app.id, annotation.id)
  920. # Verify annotation was updated correctly
  921. assert updated_annotation.id == annotation.id
  922. assert updated_annotation.app_id == app.id
  923. assert updated_annotation.question == updated_args["question"]
  924. assert updated_annotation.content == updated_args["answer"]
  925. assert updated_annotation.account_id == account.id
  926. # Verify original values were changed
  927. assert updated_annotation.question != original_args["question"]
  928. assert updated_annotation.content != original_args["answer"]
  929. # Verify update_annotation_to_index_task was called
  930. mock_external_service_dependencies["update_task"].delay.assert_called_once()
  931. call_args = mock_external_service_dependencies["update_task"].delay.call_args[0]
  932. assert call_args[0] == annotation.id # annotation_id
  933. assert call_args[1] == updated_args["question"] # question
  934. assert call_args[2] == account.current_tenant_id # tenant_id
  935. assert call_args[3] == app.id # app_id
  936. assert call_args[4] == collection_binding.id # collection_binding_id
  937. def test_delete_app_annotation_with_setting_success(
  938. self, db_session_with_containers, mock_external_service_dependencies
  939. ):
  940. """
  941. Test successful deletion of app annotation with annotation setting enabled.
  942. """
  943. fake = Faker()
  944. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  945. # Create annotation setting first
  946. from extensions.ext_database import db
  947. from models.dataset import DatasetCollectionBinding
  948. from models.model import AppAnnotationSetting
  949. # Create a collection binding first
  950. collection_binding = DatasetCollectionBinding()
  951. collection_binding.id = fake.uuid4()
  952. collection_binding.provider_name = "openai"
  953. collection_binding.model_name = "text-embedding-ada-002"
  954. collection_binding.type = "annotation"
  955. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  956. db.session.add(collection_binding)
  957. db.session.flush()
  958. # Create annotation setting
  959. annotation_setting = AppAnnotationSetting()
  960. annotation_setting.app_id = app.id
  961. annotation_setting.score_threshold = 0.8
  962. annotation_setting.collection_binding_id = collection_binding.id
  963. annotation_setting.created_user_id = account.id
  964. annotation_setting.updated_user_id = account.id
  965. db.session.add(annotation_setting)
  966. db.session.commit()
  967. # Create an annotation first
  968. annotation_args = {
  969. "question": fake.sentence(),
  970. "answer": fake.text(max_nb_chars=200),
  971. }
  972. annotation = AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
  973. annotation_id = annotation.id
  974. # Reset mock to clear previous calls
  975. mock_external_service_dependencies["delete_task"].delay.reset_mock()
  976. # Delete the annotation
  977. AppAnnotationService.delete_app_annotation(app.id, annotation_id)
  978. # Verify annotation was deleted
  979. deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
  980. assert deleted_annotation is None
  981. # Verify delete_annotation_index_task was called
  982. mock_external_service_dependencies["delete_task"].delay.assert_called_once()
  983. call_args = mock_external_service_dependencies["delete_task"].delay.call_args[0]
  984. assert call_args[0] == annotation_id # annotation_id
  985. assert call_args[1] == app.id # app_id
  986. assert call_args[2] == account.current_tenant_id # tenant_id
  987. assert call_args[3] == collection_binding.id # collection_binding_id
  988. def test_up_insert_app_annotation_from_message_with_setting_success(
  989. self, db_session_with_containers, mock_external_service_dependencies
  990. ):
  991. """
  992. Test creating annotation from message with annotation setting enabled.
  993. """
  994. fake = Faker()
  995. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  996. # Create annotation setting first
  997. from extensions.ext_database import db
  998. from models.dataset import DatasetCollectionBinding
  999. from models.model import AppAnnotationSetting
  1000. # Create a collection binding first
  1001. collection_binding = DatasetCollectionBinding()
  1002. collection_binding.id = fake.uuid4()
  1003. collection_binding.provider_name = "openai"
  1004. collection_binding.model_name = "text-embedding-ada-002"
  1005. collection_binding.type = "annotation"
  1006. collection_binding.collection_name = f"annotation_collection_{fake.uuid4()}"
  1007. db.session.add(collection_binding)
  1008. db.session.flush()
  1009. # Create annotation setting
  1010. annotation_setting = AppAnnotationSetting()
  1011. annotation_setting.app_id = app.id
  1012. annotation_setting.score_threshold = 0.8
  1013. annotation_setting.collection_binding_id = collection_binding.id
  1014. annotation_setting.created_user_id = account.id
  1015. annotation_setting.updated_user_id = account.id
  1016. db.session.add(annotation_setting)
  1017. db.session.commit()
  1018. # Create a conversation and message first
  1019. conversation = self._create_test_conversation(app, account, fake)
  1020. message = self._create_test_message(app, conversation, account, fake)
  1021. # Setup annotation data with message_id
  1022. annotation_args = {
  1023. "message_id": message.id,
  1024. "question": fake.sentence(),
  1025. "answer": fake.text(max_nb_chars=200),
  1026. }
  1027. # Insert annotation from message
  1028. annotation = AppAnnotationService.up_insert_app_annotation_from_message(annotation_args, app.id)
  1029. # Verify annotation was created correctly
  1030. assert annotation.app_id == app.id
  1031. assert annotation.conversation_id == conversation.id
  1032. assert annotation.message_id == message.id
  1033. assert annotation.question == annotation_args["question"]
  1034. assert annotation.content == annotation_args["answer"]
  1035. assert annotation.account_id == account.id
  1036. # Verify add_annotation_to_index_task was called
  1037. mock_external_service_dependencies["add_task"].delay.assert_called_once()
  1038. call_args = mock_external_service_dependencies["add_task"].delay.call_args[0]
  1039. assert call_args[0] == annotation.id # annotation_id
  1040. assert call_args[1] == annotation_args["question"] # question
  1041. assert call_args[2] == account.current_tenant_id # tenant_id
  1042. assert call_args[3] == app.id # app_id
  1043. assert call_args[4] == collection_binding.id # collection_binding_id