Skip to content

Commit 7a51074

Browse files
authored
🐛 Bugfix: Fix the issue that the Q&A results show the thinking process
🐛 Bugfix: Fix the issue that the Q&A results show the thinking process #2509
2 parents b2a7cc6 + 82ebcde commit 7a51074

File tree

3 files changed

+240
-14
lines changed

3 files changed

+240
-14
lines changed

sdk/nexent/core/agents/nexent_agent.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from ..models.openai_llm import OpenAIModel
1010
from ..tools import * # Used for tool creation, do not delete!!!
11-
from ..utils.constants import THINK_TAG_PATTERN
11+
from ..utils.constants import THINK_TAG_PATTERN, THINK_PREFIX_PATTERN
1212
from ..utils.observer import MessageObserver, ProcessType
1313
from .agent_model import AgentConfig, AgentHistory, ModelConfig, ToolConfig
1414
from .core_agent import CoreAgent, convert_code_format
@@ -225,8 +225,13 @@ def agent_run_with_observer(self, query: str, reset=True):
225225
else:
226226
# prepare for multi-modal final_answer
227227
final_answer_str = convert_code_format(str(final_answer))
228-
final_answer_str = re.sub(THINK_TAG_PATTERN, "", final_answer_str, flags=re.DOTALL | re.IGNORECASE)
229-
observer.add_message(self.agent.agent_name, ProcessType.FINAL_ANSWER, final_answer_str)
228+
final_answer_str = re.sub(
229+
THINK_TAG_PATTERN, "", final_answer_str, flags=re.DOTALL | re.IGNORECASE)
230+
# Remove "思考:" or "思考:" prefix content (until two newlines)
231+
final_answer_str = re.sub(
232+
THINK_PREFIX_PATTERN, "", final_answer_str, flags=re.DOTALL)
233+
observer.add_message(self.agent.agent_name,
234+
ProcessType.FINAL_ANSWER, final_answer_str)
230235

231236
# Check if we need to stop from external stop_event
232237
if self.agent.stop_event.is_set():

sdk/nexent/core/utils/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
THINK_TAG_PATTERN = r"(?:<think>)?.*?</think>"
2+
# Pattern to match "思考:" or "思考:" followed by content until two newlines
3+
THINK_PREFIX_PATTERN = r"思考[::].*?\n\n"

test/sdk/core/agents/test_nexent_agent.py

Lines changed: 230 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,14 @@ class _MockProcessType:
111111
FINAL_ANSWER = "final_answer"
112112
ERROR = "error"
113113

114+
114115
MessageObserver = _MockMessageObserver
115116
ProcessType = _MockProcessType
116117

117118

118119
mock_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")
120122
mock_nexent_core_utils_observer_module.MessageObserver = _MockMessageObserver
121123
mock_nexent_core_utils_observer_module.ProcessType = _MockProcessType
122124

@@ -133,17 +135,20 @@ class _MockProcessType:
133135

134136
mock_sdk_module.__path__ = [str(SDK_SOURCE_ROOT)]
135137
mock_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")]
137140
mock_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")]
141145
mock_sdk_nexent_core_utils_observer_module.__path__ = []
142146

143147
mock_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

148153
mock_tools_common_message_module = types.ModuleType(
149154
"nexent.core.utils.tools_common_message"
@@ -199,7 +204,8 @@ class _MockToolSign:
199204
mock_nexent_storage_module.MinIOStorageClient = MagicMock()
200205
mock_nexent_module.storage = mock_nexent_storage_module
201206
mock_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")
203209
mock_nexent_load_save_module.LoadSaveObjectManager = MagicMock()
204210
mock_nexent_module.multi_modal = mock_nexent_multi_modal_module
205211
module_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

826836
def 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+
866877
def 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+
13481568
def 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-
13891608
def 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

Comments
 (0)