test_agent_service.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  1. import json
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from faker import Faker
  5. from core.plugin.impl.exc import PluginDaemonClientSideError
  6. from models.model import AppModelConfig, Conversation, EndUser, Message, MessageAgentThought
  7. from services.account_service import AccountService, TenantService
  8. from services.agent_service import AgentService
  9. from services.app_service import AppService
  10. class TestAgentService:
  11. """Integration tests for AgentService using testcontainers."""
  12. @pytest.fixture
  13. def mock_external_service_dependencies(self):
  14. """Mock setup for external service dependencies."""
  15. with (
  16. patch("services.agent_service.PluginAgentClient") as mock_plugin_agent_client,
  17. patch("services.agent_service.ToolManager") as mock_tool_manager,
  18. patch("services.agent_service.AgentConfigManager") as mock_agent_config_manager,
  19. patch("services.agent_service.current_user") as mock_current_user,
  20. patch("services.app_service.FeatureService") as mock_feature_service,
  21. patch("services.app_service.EnterpriseService") as mock_enterprise_service,
  22. patch("services.app_service.ModelManager") as mock_model_manager,
  23. patch("services.account_service.FeatureService") as mock_account_feature_service,
  24. ):
  25. # Setup default mock returns for agent service
  26. mock_plugin_agent_client_instance = mock_plugin_agent_client.return_value
  27. mock_plugin_agent_client_instance.fetch_agent_strategy_providers.return_value = [
  28. MagicMock(
  29. plugin_id="test_plugin",
  30. declaration=MagicMock(
  31. identity=MagicMock(name="test_provider"),
  32. strategies=[MagicMock(identity=MagicMock(name="test_strategy"))],
  33. ),
  34. )
  35. ]
  36. mock_plugin_agent_client_instance.fetch_agent_strategy_provider.return_value = MagicMock(
  37. plugin_id="test_plugin",
  38. declaration=MagicMock(
  39. identity=MagicMock(name="test_provider"),
  40. strategies=[MagicMock(identity=MagicMock(name="test_strategy"))],
  41. ),
  42. )
  43. # Setup ToolManager mocks
  44. mock_tool_manager.get_tool_icon.return_value = "test_icon"
  45. mock_tool_manager.get_tool_label.return_value = MagicMock(
  46. to_dict=lambda: {"en_US": "Test Tool", "zh_Hans": "测试工具"}
  47. )
  48. # Setup AgentConfigManager mocks
  49. mock_agent_config = MagicMock()
  50. mock_agent_config.tools = [
  51. MagicMock(tool_name="test_tool", provider_type="test_provider", provider_id="test_id")
  52. ]
  53. mock_agent_config_manager.convert.return_value = mock_agent_config
  54. # Setup current_user mock
  55. mock_current_user.timezone = "UTC"
  56. # Setup default mock returns for app service
  57. mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
  58. mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
  59. mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
  60. # Setup default mock returns for account service
  61. mock_account_feature_service.get_system_features.return_value.is_allow_register = True
  62. # Mock ModelManager for model configuration
  63. mock_model_instance = mock_model_manager.return_value
  64. mock_model_instance.get_default_model_instance.return_value = None
  65. mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo")
  66. yield {
  67. "plugin_agent_client": mock_plugin_agent_client,
  68. "tool_manager": mock_tool_manager,
  69. "agent_config_manager": mock_agent_config_manager,
  70. "current_user": mock_current_user,
  71. "feature_service": mock_feature_service,
  72. "enterprise_service": mock_enterprise_service,
  73. "model_manager": mock_model_manager,
  74. "account_feature_service": mock_account_feature_service,
  75. }
  76. def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies):
  77. """
  78. Helper method to create a test app and account for testing.
  79. Args:
  80. db_session_with_containers: Database session from testcontainers infrastructure
  81. mock_external_service_dependencies: Mock dependencies
  82. Returns:
  83. tuple: (app, account) - Created app and account instances
  84. """
  85. fake = Faker()
  86. # Setup mocks for account creation
  87. mock_external_service_dependencies[
  88. "account_feature_service"
  89. ].get_system_features.return_value.is_allow_register = True
  90. # Create account and tenant
  91. account = AccountService.create_account(
  92. email=fake.email(),
  93. name=fake.name(),
  94. interface_language="en-US",
  95. password=fake.password(length=12),
  96. )
  97. TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
  98. tenant = account.current_tenant
  99. # Create app with realistic data
  100. app_args = {
  101. "name": fake.company(),
  102. "description": fake.text(max_nb_chars=100),
  103. "mode": "agent-chat",
  104. "icon_type": "emoji",
  105. "icon": "📚",
  106. "icon_background": "#FF6B6B",
  107. "api_rph": 100,
  108. "api_rpm": 10,
  109. }
  110. app_service = AppService()
  111. app = app_service.create_app(tenant.id, app_args, account)
  112. # Update the app model config to set agent_mode for agent-chat mode
  113. if app.mode == "agent-chat" and app.app_model_config:
  114. app.app_model_config.agent_mode = json.dumps({"enabled": True, "strategy": "react", "tools": []})
  115. from extensions.ext_database import db
  116. db.session.commit()
  117. return app, account
  118. def _create_test_conversation_and_message(self, db_session_with_containers, app, account):
  119. """
  120. Helper method to create a test conversation and message with agent thoughts.
  121. Args:
  122. db_session_with_containers: Database session from testcontainers infrastructure
  123. app: App instance
  124. account: Account instance
  125. Returns:
  126. tuple: (conversation, message) - Created conversation and message instances
  127. """
  128. fake = Faker()
  129. from extensions.ext_database import db
  130. # Create conversation
  131. conversation = Conversation(
  132. id=fake.uuid4(),
  133. app_id=app.id,
  134. from_account_id=account.id,
  135. from_end_user_id=None,
  136. name=fake.sentence(),
  137. inputs={},
  138. status="normal",
  139. mode="chat",
  140. from_source="api",
  141. )
  142. db.session.add(conversation)
  143. db.session.commit()
  144. # Create app model config
  145. app_model_config = AppModelConfig(
  146. id=fake.uuid4(),
  147. app_id=app.id,
  148. provider="openai",
  149. model_id="gpt-3.5-turbo",
  150. configs={},
  151. model="gpt-3.5-turbo",
  152. agent_mode=json.dumps({"enabled": True, "strategy": "react", "tools": []}),
  153. )
  154. db.session.add(app_model_config)
  155. db.session.commit()
  156. # Update conversation with app model config
  157. conversation.app_model_config_id = app_model_config.id
  158. db.session.commit()
  159. # Create message
  160. message = Message(
  161. id=fake.uuid4(),
  162. conversation_id=conversation.id,
  163. app_id=app.id,
  164. from_account_id=account.id,
  165. from_end_user_id=None,
  166. inputs={},
  167. query=fake.text(max_nb_chars=100),
  168. message=[{"role": "user", "text": fake.text(max_nb_chars=100)}],
  169. answer=fake.text(max_nb_chars=200),
  170. message_tokens=100,
  171. message_unit_price=0.001,
  172. answer_tokens=200,
  173. answer_unit_price=0.001,
  174. provider_response_latency=1.5,
  175. currency="USD",
  176. from_source="api",
  177. )
  178. db.session.add(message)
  179. db.session.commit()
  180. return conversation, message
  181. def _create_test_agent_thoughts(self, db_session_with_containers, message):
  182. """
  183. Helper method to create test agent thoughts for a message.
  184. Args:
  185. db_session_with_containers: Database session from testcontainers infrastructure
  186. message: Message instance
  187. Returns:
  188. list: Created agent thoughts
  189. """
  190. fake = Faker()
  191. from extensions.ext_database import db
  192. agent_thoughts = []
  193. # Create first agent thought
  194. thought1 = MessageAgentThought(
  195. id=fake.uuid4(),
  196. message_id=message.id,
  197. position=1,
  198. thought="I need to analyze the user's request",
  199. tool="test_tool",
  200. tool_labels_str=json.dumps({"test_tool": {"en_US": "Test Tool", "zh_Hans": "测试工具"}}),
  201. tool_meta_str=json.dumps(
  202. {
  203. "test_tool": {
  204. "error": None,
  205. "time_cost": 0.5,
  206. "tool_config": {"tool_provider_type": "test_provider", "tool_provider": "test_id"},
  207. "tool_parameters": {},
  208. }
  209. }
  210. ),
  211. tool_input=json.dumps({"test_tool": {"input": "test_input"}}),
  212. observation=json.dumps({"test_tool": {"output": "test_output"}}),
  213. tokens=50,
  214. created_by_role="account",
  215. created_by=message.from_account_id,
  216. )
  217. db.session.add(thought1)
  218. agent_thoughts.append(thought1)
  219. # Create second agent thought
  220. thought2 = MessageAgentThought(
  221. id=fake.uuid4(),
  222. message_id=message.id,
  223. position=2,
  224. thought="Based on the analysis, I can provide a response",
  225. tool="dataset_tool",
  226. tool_labels_str=json.dumps({"dataset_tool": {"en_US": "Dataset Tool", "zh_Hans": "数据集工具"}}),
  227. tool_meta_str=json.dumps(
  228. {
  229. "dataset_tool": {
  230. "error": None,
  231. "time_cost": 0.3,
  232. "tool_config": {"tool_provider_type": "dataset-retrieval", "tool_provider": "dataset_id"},
  233. "tool_parameters": {},
  234. }
  235. }
  236. ),
  237. tool_input=json.dumps({"dataset_tool": {"query": "test_query"}}),
  238. observation=json.dumps({"dataset_tool": {"results": "test_results"}}),
  239. tokens=30,
  240. created_by_role="account",
  241. created_by=message.from_account_id,
  242. )
  243. db.session.add(thought2)
  244. agent_thoughts.append(thought2)
  245. db.session.commit()
  246. return agent_thoughts
  247. def test_get_agent_logs_success(self, db_session_with_containers, mock_external_service_dependencies):
  248. """
  249. Test successful retrieval of agent logs with complete data.
  250. """
  251. fake = Faker()
  252. # Create test data
  253. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  254. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  255. agent_thoughts = self._create_test_agent_thoughts(db_session_with_containers, message)
  256. # Execute the method under test
  257. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  258. # Verify the result structure
  259. assert result is not None
  260. assert "meta" in result
  261. assert "iterations" in result
  262. assert "files" in result
  263. # Verify meta information
  264. meta = result["meta"]
  265. assert meta["status"] == "success"
  266. assert meta["executor"] == account.name
  267. assert meta["iterations"] == 2
  268. assert meta["agent_mode"] == "react"
  269. assert meta["total_tokens"] == 300 # 100 + 200
  270. assert meta["elapsed_time"] == 1.5
  271. # Verify iterations
  272. iterations = result["iterations"]
  273. assert len(iterations) == 2
  274. # Verify first iteration
  275. first_iteration = iterations[0]
  276. assert first_iteration["tokens"] == 50
  277. assert first_iteration["thought"] == "I need to analyze the user's request"
  278. assert len(first_iteration["tool_calls"]) == 1
  279. tool_call = first_iteration["tool_calls"][0]
  280. assert tool_call["tool_name"] == "test_tool"
  281. assert tool_call["tool_label"] == {"en_US": "Test Tool", "zh_Hans": "测试工具"}
  282. assert tool_call["status"] == "success"
  283. assert tool_call["time_cost"] == 0.5
  284. assert tool_call["tool_icon"] == "test_icon"
  285. # Verify second iteration
  286. second_iteration = iterations[1]
  287. assert second_iteration["tokens"] == 30
  288. assert second_iteration["thought"] == "Based on the analysis, I can provide a response"
  289. assert len(second_iteration["tool_calls"]) == 1
  290. dataset_tool_call = second_iteration["tool_calls"][0]
  291. assert dataset_tool_call["tool_name"] == "dataset_tool"
  292. assert dataset_tool_call["tool_label"] == {"en_US": "Dataset Tool", "zh_Hans": "数据集工具"}
  293. assert dataset_tool_call["status"] == "success"
  294. assert dataset_tool_call["time_cost"] == 0.3
  295. assert dataset_tool_call["tool_icon"] == "" # dataset-retrieval tools have empty icon
  296. def test_get_agent_logs_conversation_not_found(
  297. self, db_session_with_containers, mock_external_service_dependencies
  298. ):
  299. """
  300. Test error handling when conversation is not found.
  301. """
  302. fake = Faker()
  303. # Create test data
  304. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  305. # Execute the method under test with non-existent conversation
  306. with pytest.raises(ValueError, match="Conversation not found"):
  307. AgentService.get_agent_logs(app, fake.uuid4(), fake.uuid4())
  308. def test_get_agent_logs_message_not_found(self, db_session_with_containers, mock_external_service_dependencies):
  309. """
  310. Test error handling when message is not found.
  311. """
  312. fake = Faker()
  313. # Create test data
  314. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  315. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  316. # Execute the method under test with non-existent message
  317. with pytest.raises(ValueError, match="Message not found"):
  318. AgentService.get_agent_logs(app, str(conversation.id), fake.uuid4())
  319. def test_get_agent_logs_with_end_user(self, db_session_with_containers, mock_external_service_dependencies):
  320. """
  321. Test agent logs retrieval when conversation is from end user.
  322. """
  323. fake = Faker()
  324. # Create test data
  325. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  326. from extensions.ext_database import db
  327. # Create end user
  328. end_user = EndUser(
  329. id=fake.uuid4(),
  330. tenant_id=app.tenant_id,
  331. app_id=app.id,
  332. type="web_app",
  333. is_anonymous=False,
  334. session_id=fake.uuid4(),
  335. name=fake.name(),
  336. )
  337. db.session.add(end_user)
  338. db.session.commit()
  339. # Create conversation with end user
  340. conversation = Conversation(
  341. id=fake.uuid4(),
  342. app_id=app.id,
  343. from_account_id=None,
  344. from_end_user_id=end_user.id,
  345. name=fake.sentence(),
  346. inputs={},
  347. status="normal",
  348. mode="chat",
  349. from_source="api",
  350. )
  351. db.session.add(conversation)
  352. db.session.commit()
  353. # Create app model config
  354. app_model_config = AppModelConfig(
  355. id=fake.uuid4(),
  356. app_id=app.id,
  357. provider="openai",
  358. model_id="gpt-3.5-turbo",
  359. configs={},
  360. model="gpt-3.5-turbo",
  361. agent_mode=json.dumps({"enabled": True, "strategy": "react", "tools": []}),
  362. )
  363. db.session.add(app_model_config)
  364. db.session.commit()
  365. # Update conversation with app model config
  366. conversation.app_model_config_id = app_model_config.id
  367. db.session.commit()
  368. # Create message
  369. message = Message(
  370. id=fake.uuid4(),
  371. conversation_id=conversation.id,
  372. app_id=app.id,
  373. from_account_id=None,
  374. from_end_user_id=end_user.id,
  375. inputs={},
  376. query=fake.text(max_nb_chars=100),
  377. message=[{"role": "user", "text": fake.text(max_nb_chars=100)}],
  378. answer=fake.text(max_nb_chars=200),
  379. message_tokens=100,
  380. message_unit_price=0.001,
  381. answer_tokens=200,
  382. answer_unit_price=0.001,
  383. provider_response_latency=1.5,
  384. currency="USD",
  385. from_source="api",
  386. )
  387. db.session.add(message)
  388. db.session.commit()
  389. # Execute the method under test
  390. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  391. # Verify the result
  392. assert result is not None
  393. assert result["meta"]["executor"] == end_user.name
  394. def test_get_agent_logs_with_unknown_executor(self, db_session_with_containers, mock_external_service_dependencies):
  395. """
  396. Test agent logs retrieval when executor is unknown.
  397. """
  398. fake = Faker()
  399. # Create test data
  400. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  401. from extensions.ext_database import db
  402. # Create conversation with non-existent account
  403. conversation = Conversation(
  404. id=fake.uuid4(),
  405. app_id=app.id,
  406. from_account_id=fake.uuid4(), # Non-existent account
  407. from_end_user_id=None,
  408. name=fake.sentence(),
  409. inputs={},
  410. status="normal",
  411. mode="chat",
  412. from_source="api",
  413. )
  414. db.session.add(conversation)
  415. db.session.commit()
  416. # Create app model config
  417. app_model_config = AppModelConfig(
  418. id=fake.uuid4(),
  419. app_id=app.id,
  420. provider="openai",
  421. model_id="gpt-3.5-turbo",
  422. configs={},
  423. model="gpt-3.5-turbo",
  424. agent_mode=json.dumps({"enabled": True, "strategy": "react", "tools": []}),
  425. )
  426. db.session.add(app_model_config)
  427. db.session.commit()
  428. # Update conversation with app model config
  429. conversation.app_model_config_id = app_model_config.id
  430. db.session.commit()
  431. # Create message
  432. message = Message(
  433. id=fake.uuid4(),
  434. conversation_id=conversation.id,
  435. app_id=app.id,
  436. from_account_id=fake.uuid4(), # Non-existent account
  437. from_end_user_id=None,
  438. inputs={},
  439. query=fake.text(max_nb_chars=100),
  440. message=[{"role": "user", "text": fake.text(max_nb_chars=100)}],
  441. answer=fake.text(max_nb_chars=200),
  442. message_tokens=100,
  443. message_unit_price=0.001,
  444. answer_tokens=200,
  445. answer_unit_price=0.001,
  446. provider_response_latency=1.5,
  447. currency="USD",
  448. from_source="api",
  449. )
  450. db.session.add(message)
  451. db.session.commit()
  452. # Execute the method under test
  453. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  454. # Verify the result
  455. assert result is not None
  456. assert result["meta"]["executor"] == "Unknown"
  457. def test_get_agent_logs_with_tool_error(self, db_session_with_containers, mock_external_service_dependencies):
  458. """
  459. Test agent logs retrieval with tool errors.
  460. """
  461. fake = Faker()
  462. # Create test data
  463. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  464. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  465. from extensions.ext_database import db
  466. # Create agent thought with tool error
  467. thought_with_error = MessageAgentThought(
  468. id=fake.uuid4(),
  469. message_id=message.id,
  470. position=1,
  471. thought="I need to analyze the user's request",
  472. tool="error_tool",
  473. tool_labels_str=json.dumps({"error_tool": {"en_US": "Error Tool", "zh_Hans": "错误工具"}}),
  474. tool_meta_str=json.dumps(
  475. {
  476. "error_tool": {
  477. "error": "Tool execution failed",
  478. "time_cost": 0.5,
  479. "tool_config": {"tool_provider_type": "test_provider", "tool_provider": "test_id"},
  480. "tool_parameters": {},
  481. }
  482. }
  483. ),
  484. tool_input=json.dumps({"error_tool": {"input": "test_input"}}),
  485. observation=json.dumps({"error_tool": {"output": "error_output"}}),
  486. tokens=50,
  487. created_by_role="account",
  488. created_by=message.from_account_id,
  489. )
  490. db.session.add(thought_with_error)
  491. db.session.commit()
  492. # Execute the method under test
  493. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  494. # Verify the result
  495. assert result is not None
  496. iterations = result["iterations"]
  497. assert len(iterations) == 1
  498. tool_call = iterations[0]["tool_calls"][0]
  499. assert tool_call["status"] == "error"
  500. assert tool_call["error"] == "Tool execution failed"
  501. def test_get_agent_logs_without_agent_thoughts(
  502. self, db_session_with_containers, mock_external_service_dependencies
  503. ):
  504. """
  505. Test agent logs retrieval when message has no agent thoughts.
  506. """
  507. fake = Faker()
  508. # Create test data
  509. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  510. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  511. # Execute the method under test
  512. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  513. # Verify the result
  514. assert result is not None
  515. assert result["meta"]["iterations"] == 0
  516. assert len(result["iterations"]) == 0
  517. def test_get_agent_logs_app_model_config_not_found(
  518. self, db_session_with_containers, mock_external_service_dependencies
  519. ):
  520. """
  521. Test error handling when app model config is not found.
  522. """
  523. fake = Faker()
  524. # Create test data
  525. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  526. from extensions.ext_database import db
  527. # Remove app model config to test error handling
  528. app.app_model_config_id = None
  529. db.session.commit()
  530. # Create conversation without app model config
  531. conversation = Conversation(
  532. id=fake.uuid4(),
  533. app_id=app.id,
  534. from_account_id=account.id,
  535. from_end_user_id=None,
  536. name=fake.sentence(),
  537. inputs={},
  538. status="normal",
  539. mode="chat",
  540. from_source="api",
  541. app_model_config_id=None, # Explicitly set to None
  542. )
  543. db.session.add(conversation)
  544. db.session.commit()
  545. # Create message
  546. message = Message(
  547. id=fake.uuid4(),
  548. conversation_id=conversation.id,
  549. app_id=app.id,
  550. from_account_id=account.id,
  551. from_end_user_id=None,
  552. inputs={},
  553. query=fake.text(max_nb_chars=100),
  554. message=[{"role": "user", "text": fake.text(max_nb_chars=100)}],
  555. answer=fake.text(max_nb_chars=200),
  556. message_tokens=100,
  557. message_unit_price=0.001,
  558. answer_tokens=200,
  559. answer_unit_price=0.001,
  560. provider_response_latency=1.5,
  561. currency="USD",
  562. from_source="api",
  563. )
  564. db.session.add(message)
  565. db.session.commit()
  566. # Execute the method under test
  567. with pytest.raises(ValueError, match="App model config not found"):
  568. AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  569. def test_get_agent_logs_agent_config_not_found(
  570. self, db_session_with_containers, mock_external_service_dependencies
  571. ):
  572. """
  573. Test error handling when agent config is not found.
  574. """
  575. fake = Faker()
  576. # Create test data
  577. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  578. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  579. # Mock AgentConfigManager to return None
  580. mock_external_service_dependencies["agent_config_manager"].convert.return_value = None
  581. # Execute the method under test
  582. with pytest.raises(ValueError, match="Agent config not found"):
  583. AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  584. def test_list_agent_providers_success(self, db_session_with_containers, mock_external_service_dependencies):
  585. """
  586. Test successful listing of agent providers.
  587. """
  588. fake = Faker()
  589. # Create test data
  590. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  591. # Execute the method under test
  592. result = AgentService.list_agent_providers(str(account.id), str(app.tenant_id))
  593. # Verify the result
  594. assert result is not None
  595. assert len(result) == 1
  596. assert result[0].plugin_id == "test_plugin"
  597. # Verify the mock was called correctly
  598. mock_plugin_client = mock_external_service_dependencies["plugin_agent_client"].return_value
  599. mock_plugin_client.fetch_agent_strategy_providers.assert_called_once_with(str(app.tenant_id))
  600. def test_get_agent_provider_success(self, db_session_with_containers, mock_external_service_dependencies):
  601. """
  602. Test successful retrieval of specific agent provider.
  603. """
  604. fake = Faker()
  605. # Create test data
  606. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  607. provider_name = "test_provider"
  608. # Execute the method under test
  609. result = AgentService.get_agent_provider(str(account.id), str(app.tenant_id), provider_name)
  610. # Verify the result
  611. assert result is not None
  612. assert result.plugin_id == "test_plugin"
  613. # Verify the mock was called correctly
  614. mock_plugin_client = mock_external_service_dependencies["plugin_agent_client"].return_value
  615. mock_plugin_client.fetch_agent_strategy_provider.assert_called_once_with(str(app.tenant_id), provider_name)
  616. def test_get_agent_provider_plugin_error(self, db_session_with_containers, mock_external_service_dependencies):
  617. """
  618. Test error handling when plugin daemon client raises an error.
  619. """
  620. fake = Faker()
  621. # Create test data
  622. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  623. provider_name = "test_provider"
  624. error_message = "Plugin not found"
  625. # Mock PluginAgentClient to raise an error
  626. mock_plugin_client = mock_external_service_dependencies["plugin_agent_client"].return_value
  627. mock_plugin_client.fetch_agent_strategy_provider.side_effect = PluginDaemonClientSideError(error_message)
  628. # Execute the method under test
  629. with pytest.raises(ValueError, match=error_message):
  630. AgentService.get_agent_provider(str(account.id), str(app.tenant_id), provider_name)
  631. def test_get_agent_logs_with_complex_tool_data(
  632. self, db_session_with_containers, mock_external_service_dependencies
  633. ):
  634. """
  635. Test agent logs retrieval with complex tool data and multiple tools.
  636. """
  637. fake = Faker()
  638. # Create test data
  639. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  640. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  641. from extensions.ext_database import db
  642. # Create agent thought with multiple tools
  643. complex_thought = MessageAgentThought(
  644. id=fake.uuid4(),
  645. message_id=message.id,
  646. position=1,
  647. thought="I need to use multiple tools to complete this task",
  648. tool="tool1;tool2;tool3",
  649. tool_labels_str=json.dumps(
  650. {
  651. "tool1": {"en_US": "First Tool", "zh_Hans": "第一个工具"},
  652. "tool2": {"en_US": "Second Tool", "zh_Hans": "第二个工具"},
  653. "tool3": {"en_US": "Third Tool", "zh_Hans": "第三个工具"},
  654. }
  655. ),
  656. tool_meta_str=json.dumps(
  657. {
  658. "tool1": {
  659. "error": None,
  660. "time_cost": 0.5,
  661. "tool_config": {"tool_provider_type": "test_provider", "tool_provider": "test_id"},
  662. "tool_parameters": {"param1": "value1"},
  663. },
  664. "tool2": {
  665. "error": "Tool 2 failed",
  666. "time_cost": 0.3,
  667. "tool_config": {"tool_provider_type": "another_provider", "tool_provider": "another_id"},
  668. "tool_parameters": {"param2": "value2"},
  669. },
  670. "tool3": {
  671. "error": None,
  672. "time_cost": 0.7,
  673. "tool_config": {"tool_provider_type": "dataset-retrieval", "tool_provider": "dataset_id"},
  674. "tool_parameters": {"param3": "value3"},
  675. },
  676. }
  677. ),
  678. tool_input=json.dumps(
  679. {"tool1": {"input1": "data1"}, "tool2": {"input2": "data2"}, "tool3": {"input3": "data3"}}
  680. ),
  681. observation=json.dumps(
  682. {"tool1": {"output1": "result1"}, "tool2": {"output2": "result2"}, "tool3": {"output3": "result3"}}
  683. ),
  684. tokens=100,
  685. created_by_role="account",
  686. created_by=message.from_account_id,
  687. )
  688. db.session.add(complex_thought)
  689. db.session.commit()
  690. # Execute the method under test
  691. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  692. # Verify the result
  693. assert result is not None
  694. iterations = result["iterations"]
  695. assert len(iterations) == 1
  696. tool_calls = iterations[0]["tool_calls"]
  697. assert len(tool_calls) == 3
  698. # Verify first tool
  699. assert tool_calls[0]["tool_name"] == "tool1"
  700. assert tool_calls[0]["tool_label"] == {"en_US": "First Tool", "zh_Hans": "第一个工具"}
  701. assert tool_calls[0]["status"] == "success"
  702. assert tool_calls[0]["tool_parameters"] == {"param1": "value1"}
  703. # Verify second tool (with error)
  704. assert tool_calls[1]["tool_name"] == "tool2"
  705. assert tool_calls[1]["tool_label"] == {"en_US": "Second Tool", "zh_Hans": "第二个工具"}
  706. assert tool_calls[1]["status"] == "error"
  707. assert tool_calls[1]["error"] == "Tool 2 failed"
  708. # Verify third tool (dataset tool)
  709. assert tool_calls[2]["tool_name"] == "tool3"
  710. assert tool_calls[2]["tool_label"] == {"en_US": "Third Tool", "zh_Hans": "第三个工具"}
  711. assert tool_calls[2]["status"] == "success"
  712. assert tool_calls[2]["tool_icon"] == "" # dataset-retrieval tools have empty icon
  713. def test_get_agent_logs_with_files(self, db_session_with_containers, mock_external_service_dependencies):
  714. """
  715. Test agent logs retrieval with message files and agent thought files.
  716. """
  717. fake = Faker()
  718. # Create test data
  719. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  720. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  721. from core.file import FileTransferMethod, FileType
  722. from extensions.ext_database import db
  723. from models.enums import CreatorUserRole
  724. # Add files to message
  725. from models.model import MessageFile
  726. message_file1 = MessageFile(
  727. message_id=message.id,
  728. type=FileType.IMAGE,
  729. transfer_method=FileTransferMethod.REMOTE_URL,
  730. url="http://example.com/file1.jpg",
  731. belongs_to="user",
  732. created_by_role=CreatorUserRole.ACCOUNT,
  733. created_by=message.from_account_id,
  734. )
  735. message_file2 = MessageFile(
  736. message_id=message.id,
  737. type=FileType.IMAGE,
  738. transfer_method=FileTransferMethod.REMOTE_URL,
  739. url="http://example.com/file2.png",
  740. belongs_to="user",
  741. created_by_role=CreatorUserRole.ACCOUNT,
  742. created_by=message.from_account_id,
  743. )
  744. db.session.add(message_file1)
  745. db.session.add(message_file2)
  746. db.session.commit()
  747. # Create agent thought with files
  748. thought_with_files = MessageAgentThought(
  749. id=fake.uuid4(),
  750. message_id=message.id,
  751. position=1,
  752. thought="I need to process some files",
  753. tool="file_tool",
  754. tool_labels_str=json.dumps({"file_tool": {"en_US": "File Tool", "zh_Hans": "文件工具"}}),
  755. tool_meta_str=json.dumps(
  756. {
  757. "file_tool": {
  758. "error": None,
  759. "time_cost": 0.5,
  760. "tool_config": {"tool_provider_type": "test_provider", "tool_provider": "test_id"},
  761. "tool_parameters": {},
  762. }
  763. }
  764. ),
  765. tool_input=json.dumps({"file_tool": {"input": "test_input"}}),
  766. observation=json.dumps({"file_tool": {"output": "test_output"}}),
  767. message_files=json.dumps(["file1", "file2"]),
  768. tokens=50,
  769. created_by_role="account",
  770. created_by=message.from_account_id,
  771. )
  772. db.session.add(thought_with_files)
  773. db.session.commit()
  774. # Execute the method under test
  775. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  776. # Verify the result
  777. assert result is not None
  778. assert len(result["files"]) == 2
  779. iterations = result["iterations"]
  780. assert len(iterations) == 1
  781. assert len(iterations[0]["files"]) == 2
  782. assert "file1" in iterations[0]["files"]
  783. assert "file2" in iterations[0]["files"]
  784. def test_get_agent_logs_with_different_timezone(
  785. self, db_session_with_containers, mock_external_service_dependencies
  786. ):
  787. """
  788. Test agent logs retrieval with different timezone settings.
  789. """
  790. fake = Faker()
  791. # Create test data
  792. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  793. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  794. # Mock current_user with different timezone
  795. mock_external_service_dependencies["current_user"].timezone = "Asia/Shanghai"
  796. # Execute the method under test
  797. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  798. # Verify the result
  799. assert result is not None
  800. assert "start_time" in result["meta"]
  801. # Verify the timezone conversion
  802. start_time = result["meta"]["start_time"]
  803. assert "T" in start_time # ISO format
  804. assert "+08:00" in start_time or "Z" in start_time # Timezone offset
  805. def test_get_agent_logs_with_empty_tool_data(self, db_session_with_containers, mock_external_service_dependencies):
  806. """
  807. Test agent logs retrieval with empty tool data.
  808. """
  809. fake = Faker()
  810. # Create test data
  811. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  812. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  813. from extensions.ext_database import db
  814. # Create agent thought with empty tool data
  815. empty_thought = MessageAgentThought(
  816. id=fake.uuid4(),
  817. message_id=message.id,
  818. position=1,
  819. thought="I need to analyze the user's request",
  820. tool="", # Empty tool
  821. tool_labels_str="{}", # Empty labels
  822. tool_meta_str="{}", # Empty meta
  823. tool_input="", # Empty input
  824. observation="", # Empty observation
  825. tokens=50,
  826. created_by_role="account",
  827. created_by=message.from_account_id,
  828. )
  829. db.session.add(empty_thought)
  830. db.session.commit()
  831. # Execute the method under test
  832. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  833. # Verify the result
  834. assert result is not None
  835. iterations = result["iterations"]
  836. assert len(iterations) == 1
  837. # Verify empty tool calls
  838. tool_calls = iterations[0]["tool_calls"]
  839. assert len(tool_calls) == 0 # No tools to process
  840. def test_get_agent_logs_with_malformed_json(self, db_session_with_containers, mock_external_service_dependencies):
  841. """
  842. Test agent logs retrieval with malformed JSON data in tool fields.
  843. """
  844. fake = Faker()
  845. # Create test data
  846. app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
  847. conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account)
  848. from extensions.ext_database import db
  849. # Create agent thought with malformed JSON
  850. malformed_thought = MessageAgentThought(
  851. id=fake.uuid4(),
  852. message_id=message.id,
  853. position=1,
  854. thought="I need to analyze the user's request",
  855. tool="test_tool",
  856. tool_labels_str="invalid json", # Malformed JSON
  857. tool_meta_str="invalid json", # Malformed JSON
  858. tool_input="invalid json", # Malformed JSON
  859. observation="invalid json", # Malformed JSON
  860. tokens=50,
  861. created_by_role="account",
  862. created_by=message.from_account_id,
  863. )
  864. db.session.add(malformed_thought)
  865. db.session.commit()
  866. # Execute the method under test
  867. result = AgentService.get_agent_logs(app, str(conversation.id), str(message.id))
  868. # Verify the result - should handle malformed JSON gracefully
  869. assert result is not None
  870. iterations = result["iterations"]
  871. assert len(iterations) == 1
  872. tool_calls = iterations[0]["tool_calls"]
  873. assert len(tool_calls) == 1
  874. # Verify default values for malformed JSON
  875. tool_call = tool_calls[0]
  876. assert tool_call["tool_name"] == "test_tool"
  877. assert tool_call["tool_label"] == "test_tool" # Default to tool name
  878. assert tool_call["tool_input"] == {}
  879. assert tool_call["tool_output"] == "invalid json" # Raw observation value
  880. assert tool_call["tool_parameters"] == {}