@@ -111,12 +111,14 @@ class _MockProcessType:
111111 FINAL_ANSWER = "final_answer"
112112 ERROR = "error"
113113
114+
114115MessageObserver = _MockMessageObserver
115116ProcessType = _MockProcessType
116117
117118
118119mock_nexent_core_utils_module = types .ModuleType ("nexent.core.utils" )
119- mock_nexent_core_utils_observer_module = types .ModuleType ("nexent.core.utils.observer" )
120+ mock_nexent_core_utils_observer_module = types .ModuleType (
121+ "nexent.core.utils.observer" )
120122mock_nexent_core_utils_observer_module .MessageObserver = _MockMessageObserver
121123mock_nexent_core_utils_observer_module .ProcessType = _MockProcessType
122124
@@ -133,17 +135,20 @@ class _MockProcessType:
133135
134136mock_sdk_module .__path__ = [str (SDK_SOURCE_ROOT )]
135137mock_sdk_nexent_module .__path__ = [str (SDK_SOURCE_ROOT / "nexent" )]
136- mock_sdk_nexent_core_module .__path__ = [str (SDK_SOURCE_ROOT / "nexent" / "core" )]
138+ mock_sdk_nexent_core_module .__path__ = [
139+ str (SDK_SOURCE_ROOT / "nexent" / "core" )]
137140mock_sdk_nexent_core_agents_module .__path__ = [
138141 str (SDK_SOURCE_ROOT / "nexent" / "core" / "agents" )
139142]
140- mock_sdk_nexent_core_utils_module .__path__ = [str (SDK_SOURCE_ROOT / "nexent" / "core" / "utils" )]
143+ mock_sdk_nexent_core_utils_module .__path__ = [
144+ str (SDK_SOURCE_ROOT / "nexent" / "core" / "utils" )]
141145mock_sdk_nexent_core_utils_observer_module .__path__ = []
142146
143147mock_prompt_template_utils_module = types .ModuleType (
144148 "nexent.core.utils.prompt_template_utils"
145149)
146- mock_prompt_template_utils_module .get_prompt_template = MagicMock (return_value = "" )
150+ mock_prompt_template_utils_module .get_prompt_template = MagicMock (
151+ return_value = "" )
147152
148153mock_tools_common_message_module = types .ModuleType (
149154 "nexent.core.utils.tools_common_message"
@@ -199,7 +204,8 @@ class _MockToolSign:
199204mock_nexent_storage_module .MinIOStorageClient = MagicMock ()
200205mock_nexent_module .storage = mock_nexent_storage_module
201206mock_nexent_multi_modal_module = types .ModuleType ("nexent.multi_modal" )
202- mock_nexent_load_save_module = types .ModuleType ("nexent.multi_modal.load_save_object" )
207+ mock_nexent_load_save_module = types .ModuleType (
208+ "nexent.multi_modal.load_save_object" )
203209mock_nexent_load_save_module .LoadSaveObjectManager = MagicMock ()
204210mock_nexent_module .multi_modal = mock_nexent_multi_modal_module
205211module_mocks = {
@@ -679,7 +685,7 @@ def test_create_local_tool_analyze_text_file_tool(nexent_agent_instance):
679685 metadata = {
680686 "llm_model" : "llm_model_obj" ,
681687 "storage_client" : "storage_client_obj" ,
682- "data_process_service_url" : "https://example.com" ,
688+ "data_process_service_url" : "https://example.com" ,
683689 },
684690 )
685691
@@ -785,14 +791,16 @@ def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params(ne
785791 output_type = "string" ,
786792 params = {
787793 "top_k" : 10 ,
788- "index_names" : ["conflicting_index" ], # This should be filtered out
794+ # This should be filtered out
795+ "index_names" : ["conflicting_index" ],
789796 "vdb_core" : "conflicting_vdb" , # This should be filtered out
790797 "embedding_model" : "conflicting_model" , # This should be filtered out
791798 "observer" : "conflicting_observer" , # This should be filtered out
792799 },
793800 source = "local" ,
794801 metadata = {
795- "index_names" : ["index1" , "index2" ], # These should be used instead
802+ # These should be used instead
803+ "index_names" : ["index1" , "index2" ],
796804 "vdb_core" : mock_vdb_core ,
797805 "embedding_model" : mock_embedding_model ,
798806 },
@@ -814,13 +822,15 @@ def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params(ne
814822 # Only non-excluded params should be passed to __init__ due to smolagents wrapper restrictions
815823 mock_kb_tool_class .assert_called_once_with (
816824 top_k = 10 , # From filtered_params (not in conflict list)
817- index_names = ["conflicting_index" ], # Not excluded by current implementation
825+ # Not excluded by current implementation
826+ index_names = ["conflicting_index" ],
818827 )
819828 # Verify excluded parameters were set directly as attributes after instantiation
820829 assert result == mock_kb_tool_instance
821830 assert mock_kb_tool_instance .observer == nexent_agent_instance .observer
822831 assert mock_kb_tool_instance .vdb_core == mock_vdb_core # From metadata, not params
823- assert mock_kb_tool_instance .embedding_model == mock_embedding_model # From metadata, not params
832+ # From metadata, not params
833+ assert mock_kb_tool_instance .embedding_model == mock_embedding_model
824834
825835
826836def test_create_local_tool_knowledge_base_search_tool_with_none_defaults (nexent_agent_instance ):
@@ -863,6 +873,7 @@ def test_create_local_tool_knowledge_base_search_tool_with_none_defaults(nexent_
863873 assert mock_kb_tool_instance .embedding_model is None
864874 assert result == mock_kb_tool_instance
865875
876+
866877def test_create_local_tool_analyze_text_file_tool (nexent_agent_instance ):
867878 """Test AnalyzeTextFileTool creation injects observer and metadata."""
868879 mock_analyze_tool_class = MagicMock ()
@@ -1345,6 +1356,215 @@ def test_agent_run_with_observer_with_reset_false(nexent_agent_instance, mock_co
13451356 mock_core_agent .run .assert_called_once_with (
13461357 "test query" , stream = True , reset = False )
13471358
1359+
1360+ def test_agent_run_with_observer_removes_think_prefix_chinese_colon (nexent_agent_instance , mock_core_agent ):
1361+ """Test agent_run_with_observer removes '思考:' prefix content until two newlines."""
1362+ # Setup
1363+ nexent_agent_instance .agent = mock_core_agent
1364+ mock_core_agent .stop_event .is_set .return_value = False
1365+
1366+ # Mock step logs
1367+ mock_action_step = MagicMock (spec = ActionStep )
1368+ mock_action_step .duration = 1.0
1369+ mock_action_step .error = None
1370+
1371+ # Test with Chinese colon "思考:" followed by content and two newlines
1372+ final_answer_with_think = (
1373+ "思考:用户需要一份营养早餐的搭配建议。作为健康饮食搭配助手,我需要基于营养学知识,提供一份科学、均衡、易于准备的早餐方案。由于没有可用工具,我将直接给出建议,包括食物种类、分量和营养说明。\n \n "
1374+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。以下是我的推荐:"
1375+ )
1376+ mock_core_agent .run .return_value = [mock_action_step ]
1377+ mock_core_agent .run .return_value [- 1 ].output = final_answer_with_think
1378+
1379+ # Execute
1380+ nexent_agent_instance .agent_run_with_observer ("test query" )
1381+
1382+ # Verify the "思考:" prefix content was removed
1383+ expected_final_answer = (
1384+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。以下是我的推荐:"
1385+ )
1386+ mock_core_agent .observer .add_message .assert_any_call (
1387+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1388+ )
1389+
1390+
1391+ def test_agent_run_with_observer_removes_think_prefix_english_colon (nexent_agent_instance , mock_core_agent ):
1392+ """Test agent_run_with_observer removes '思考:' prefix content until two newlines."""
1393+ # Setup
1394+ nexent_agent_instance .agent = mock_core_agent
1395+ mock_core_agent .stop_event .is_set .return_value = False
1396+
1397+ # Mock step logs
1398+ mock_action_step = MagicMock (spec = ActionStep )
1399+ mock_action_step .duration = 1.0
1400+ mock_action_step .error = None
1401+
1402+ # Test with English colon "思考:" followed by content and two newlines
1403+ final_answer_with_think = (
1404+ "思考:This is a thinking process about the user's question.\n \n "
1405+ "Here is the actual answer to the question."
1406+ )
1407+ mock_core_agent .run .return_value = [mock_action_step ]
1408+ mock_core_agent .run .return_value [- 1 ].output = final_answer_with_think
1409+
1410+ # Execute
1411+ nexent_agent_instance .agent_run_with_observer ("test query" )
1412+
1413+ # Verify the "思考:" prefix content was removed
1414+ expected_final_answer = "Here is the actual answer to the question."
1415+ mock_core_agent .observer .add_message .assert_any_call (
1416+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1417+ )
1418+
1419+
1420+ def test_agent_run_with_observer_preserves_think_prefix_without_two_newlines (nexent_agent_instance , mock_core_agent ):
1421+ """Test agent_run_with_observer preserves '思考:' content when not followed by two newlines."""
1422+ # Setup
1423+ nexent_agent_instance .agent = mock_core_agent
1424+ mock_core_agent .stop_event .is_set .return_value = False
1425+
1426+ # Mock step logs
1427+ mock_action_step = MagicMock (spec = ActionStep )
1428+ mock_action_step .duration = 1.0
1429+ mock_action_step .error = None
1430+
1431+ # Test with "思考:" but only one newline (should not be removed)
1432+ final_answer_with_think = (
1433+ "思考:This is thinking content.\n "
1434+ "Here is the actual answer."
1435+ )
1436+ mock_core_agent .run .return_value = [mock_action_step ]
1437+ mock_core_agent .run .return_value [- 1 ].output = final_answer_with_think
1438+
1439+ # Execute
1440+ nexent_agent_instance .agent_run_with_observer ("test query" )
1441+
1442+ # Verify the content was preserved (not removed because no \n\n)
1443+ expected_final_answer = (
1444+ "思考:This is thinking content.\n "
1445+ "Here is the actual answer."
1446+ )
1447+ mock_core_agent .observer .add_message .assert_any_call (
1448+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1449+ )
1450+
1451+
1452+ def test_agent_run_with_observer_removes_both_think_tag_and_think_prefix (nexent_agent_instance , mock_core_agent ):
1453+ """Test agent_run_with_observer removes both THINK_TAG_PATTERN and THINK_PREFIX_PATTERN."""
1454+ # Setup
1455+ nexent_agent_instance .agent = mock_core_agent
1456+ mock_core_agent .stop_event .is_set .return_value = False
1457+
1458+ # Mock step logs
1459+ mock_action_step = MagicMock (spec = ActionStep )
1460+ mock_action_step .duration = 1.0
1461+ mock_action_step .error = None
1462+
1463+ # Test with both <think> tags and "思考:" prefix
1464+ final_answer_with_both = (
1465+ "<think>Some reasoning content</think>"
1466+ "思考:用户需要一份营养早餐的搭配建议。\n \n "
1467+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
1468+ )
1469+ mock_core_agent .run .return_value = [mock_action_step ]
1470+ mock_core_agent .run .return_value [- 1 ].output = final_answer_with_both
1471+
1472+ # Execute
1473+ nexent_agent_instance .agent_run_with_observer ("test query" )
1474+
1475+ # Verify both patterns were removed
1476+ expected_final_answer = "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
1477+ mock_core_agent .observer .add_message .assert_any_call (
1478+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1479+ )
1480+
1481+
1482+ def test_agent_run_with_observer_think_prefix_in_middle (nexent_agent_instance , mock_core_agent ):
1483+ """Test agent_run_with_observer removes '思考:' even when it appears in the middle of text."""
1484+ # Setup
1485+ nexent_agent_instance .agent = mock_core_agent
1486+ mock_core_agent .stop_event .is_set .return_value = False
1487+
1488+ # Mock step logs
1489+ mock_action_step = MagicMock (spec = ActionStep )
1490+ mock_action_step .duration = 1.0
1491+ mock_action_step .error = None
1492+
1493+ # Test with "思考:" in the middle of the text
1494+ final_answer_with_think = (
1495+ "Some initial content. "
1496+ "思考:This is thinking content in the middle.\n \n "
1497+ "Here is the rest of the answer."
1498+ )
1499+ mock_core_agent .run .return_value = [mock_action_step ]
1500+ mock_core_agent .run .return_value [- 1 ].output = final_answer_with_think
1501+
1502+ # Execute
1503+ nexent_agent_instance .agent_run_with_observer ("test query" )
1504+
1505+ # Verify the "思考:" content was removed
1506+ expected_final_answer = "Some initial content. Here is the rest of the answer."
1507+ mock_core_agent .observer .add_message .assert_any_call (
1508+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1509+ )
1510+
1511+
1512+ def test_agent_run_with_observer_no_think_prefix (nexent_agent_instance , mock_core_agent ):
1513+ """Test agent_run_with_observer handles content without '思考:' prefix normally."""
1514+ # Setup
1515+ nexent_agent_instance .agent = mock_core_agent
1516+ mock_core_agent .stop_event .is_set .return_value = False
1517+
1518+ # Mock step logs
1519+ mock_action_step = MagicMock (spec = ActionStep )
1520+ mock_action_step .duration = 1.0
1521+ mock_action_step .error = None
1522+
1523+ # Test with normal content without "思考:" prefix
1524+ final_answer_normal = "This is a normal final answer without any thinking prefix."
1525+ mock_core_agent .run .return_value = [mock_action_step ]
1526+ mock_core_agent .run .return_value [- 1 ].output = final_answer_normal
1527+
1528+ # Execute
1529+ nexent_agent_instance .agent_run_with_observer ("test query" )
1530+
1531+ # Verify the content was preserved as-is
1532+ mock_core_agent .observer .add_message .assert_any_call (
1533+ "test_agent" , ProcessType .FINAL_ANSWER , final_answer_normal
1534+ )
1535+
1536+
1537+ def test_agent_run_with_observer_think_prefix_with_agent_text (nexent_agent_instance , mock_core_agent ):
1538+ """Test agent_run_with_observer removes '思考:' prefix when final answer is AgentText."""
1539+ # Setup
1540+ nexent_agent_instance .agent = mock_core_agent
1541+ mock_core_agent .stop_event .is_set .return_value = False
1542+
1543+ # Mock step logs
1544+ mock_action_step = MagicMock (spec = ActionStep )
1545+ mock_action_step .duration = 1.0
1546+ mock_action_step .error = None
1547+
1548+ # Test with AgentText containing "思考:" prefix
1549+ final_answer_with_think = (
1550+ "思考:用户需要一份营养早餐的搭配建议。\n \n "
1551+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
1552+ )
1553+ mock_final_answer = _AgentText (final_answer_with_think )
1554+
1555+ mock_core_agent .run .return_value = [mock_action_step ]
1556+ mock_core_agent .run .return_value [- 1 ].output = mock_final_answer
1557+
1558+ # Execute
1559+ nexent_agent_instance .agent_run_with_observer ("test query" )
1560+
1561+ # Verify the "思考:" prefix content was removed
1562+ expected_final_answer = "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
1563+ mock_core_agent .observer .add_message .assert_any_call (
1564+ "test_agent" , ProcessType .FINAL_ANSWER , expected_final_answer
1565+ )
1566+
1567+
13481568def test_create_local_tool_datamate_search_tool_success (nexent_agent_instance ):
13491569 """Test successful creation of DataMateSearchTool with metadata."""
13501570 mock_datamate_tool_class = MagicMock ()
@@ -1385,7 +1605,6 @@ def test_create_local_tool_datamate_search_tool_success(nexent_agent_instance):
13851605 assert mock_datamate_tool_instance .observer == nexent_agent_instance .observer
13861606
13871607
1388-
13891608def test_create_local_tool_datamate_search_tool_with_none_defaults (nexent_agent_instance ):
13901609 """Test DataMateSearchTool creation with None defaults when metadata is missing."""
13911610 mock_datamate_tool_class = MagicMock ()
0 commit comments