diff --git a/sdk/nexent/core/agents/nexent_agent.py b/sdk/nexent/core/agents/nexent_agent.py
index 4f2c38d07..5ee09fdc0 100644
--- a/sdk/nexent/core/agents/nexent_agent.py
+++ b/sdk/nexent/core/agents/nexent_agent.py
@@ -8,7 +8,7 @@
from ..models.openai_llm import OpenAIModel
from ..tools import * # Used for tool creation, do not delete!!!
-from ..utils.constants import THINK_TAG_PATTERN
+from ..utils.constants import THINK_TAG_PATTERN, THINK_PREFIX_PATTERN
from ..utils.observer import MessageObserver, ProcessType
from .agent_model import AgentConfig, AgentHistory, ModelConfig, ToolConfig
from .core_agent import CoreAgent, convert_code_format
@@ -225,8 +225,13 @@ def agent_run_with_observer(self, query: str, reset=True):
else:
# prepare for multi-modal final_answer
final_answer_str = convert_code_format(str(final_answer))
- final_answer_str = re.sub(THINK_TAG_PATTERN, "", final_answer_str, flags=re.DOTALL | re.IGNORECASE)
- observer.add_message(self.agent.agent_name, ProcessType.FINAL_ANSWER, final_answer_str)
+ final_answer_str = re.sub(
+ THINK_TAG_PATTERN, "", final_answer_str, flags=re.DOTALL | re.IGNORECASE)
+ # Remove "思考:" or "思考:" prefix content (until two newlines)
+ final_answer_str = re.sub(
+ THINK_PREFIX_PATTERN, "", final_answer_str, flags=re.DOTALL)
+ observer.add_message(self.agent.agent_name,
+ ProcessType.FINAL_ANSWER, final_answer_str)
# Check if we need to stop from external stop_event
if self.agent.stop_event.is_set():
diff --git a/sdk/nexent/core/utils/constants.py b/sdk/nexent/core/utils/constants.py
index f956687b6..ff297b2ca 100644
--- a/sdk/nexent/core/utils/constants.py
+++ b/sdk/nexent/core/utils/constants.py
@@ -1 +1,3 @@
THINK_TAG_PATTERN = r"(?:)?.*?"
+# Pattern to match "思考:" or "思考:" followed by content until two newlines
+THINK_PREFIX_PATTERN = r"思考[::].*?\n\n"
diff --git a/test/sdk/core/agents/test_nexent_agent.py b/test/sdk/core/agents/test_nexent_agent.py
index 1d50c5aa3..b24c12ad7 100644
--- a/test/sdk/core/agents/test_nexent_agent.py
+++ b/test/sdk/core/agents/test_nexent_agent.py
@@ -111,12 +111,14 @@ class _MockProcessType:
FINAL_ANSWER = "final_answer"
ERROR = "error"
+
MessageObserver = _MockMessageObserver
ProcessType = _MockProcessType
mock_nexent_core_utils_module = types.ModuleType("nexent.core.utils")
-mock_nexent_core_utils_observer_module = types.ModuleType("nexent.core.utils.observer")
+mock_nexent_core_utils_observer_module = types.ModuleType(
+ "nexent.core.utils.observer")
mock_nexent_core_utils_observer_module.MessageObserver = _MockMessageObserver
mock_nexent_core_utils_observer_module.ProcessType = _MockProcessType
@@ -133,17 +135,20 @@ class _MockProcessType:
mock_sdk_module.__path__ = [str(SDK_SOURCE_ROOT)]
mock_sdk_nexent_module.__path__ = [str(SDK_SOURCE_ROOT / "nexent")]
-mock_sdk_nexent_core_module.__path__ = [str(SDK_SOURCE_ROOT / "nexent" / "core")]
+mock_sdk_nexent_core_module.__path__ = [
+ str(SDK_SOURCE_ROOT / "nexent" / "core")]
mock_sdk_nexent_core_agents_module.__path__ = [
str(SDK_SOURCE_ROOT / "nexent" / "core" / "agents")
]
-mock_sdk_nexent_core_utils_module.__path__ = [str(SDK_SOURCE_ROOT / "nexent" / "core" / "utils")]
+mock_sdk_nexent_core_utils_module.__path__ = [
+ str(SDK_SOURCE_ROOT / "nexent" / "core" / "utils")]
mock_sdk_nexent_core_utils_observer_module.__path__ = []
mock_prompt_template_utils_module = types.ModuleType(
"nexent.core.utils.prompt_template_utils"
)
-mock_prompt_template_utils_module.get_prompt_template = MagicMock(return_value="")
+mock_prompt_template_utils_module.get_prompt_template = MagicMock(
+ return_value="")
mock_tools_common_message_module = types.ModuleType(
"nexent.core.utils.tools_common_message"
@@ -199,7 +204,8 @@ class _MockToolSign:
mock_nexent_storage_module.MinIOStorageClient = MagicMock()
mock_nexent_module.storage = mock_nexent_storage_module
mock_nexent_multi_modal_module = types.ModuleType("nexent.multi_modal")
-mock_nexent_load_save_module = types.ModuleType("nexent.multi_modal.load_save_object")
+mock_nexent_load_save_module = types.ModuleType(
+ "nexent.multi_modal.load_save_object")
mock_nexent_load_save_module.LoadSaveObjectManager = MagicMock()
mock_nexent_module.multi_modal = mock_nexent_multi_modal_module
module_mocks = {
@@ -679,7 +685,7 @@ def test_create_local_tool_analyze_text_file_tool(nexent_agent_instance):
metadata={
"llm_model": "llm_model_obj",
"storage_client": "storage_client_obj",
- "data_process_service_url": "https://example.com",
+ "data_process_service_url": "https://example.com",
},
)
@@ -785,14 +791,16 @@ def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params(ne
output_type="string",
params={
"top_k": 10,
- "index_names": ["conflicting_index"], # This should be filtered out
+ # This should be filtered out
+ "index_names": ["conflicting_index"],
"vdb_core": "conflicting_vdb", # This should be filtered out
"embedding_model": "conflicting_model", # This should be filtered out
"observer": "conflicting_observer", # This should be filtered out
},
source="local",
metadata={
- "index_names": ["index1", "index2"], # These should be used instead
+ # These should be used instead
+ "index_names": ["index1", "index2"],
"vdb_core": mock_vdb_core,
"embedding_model": mock_embedding_model,
},
@@ -814,13 +822,15 @@ def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params(ne
# Only non-excluded params should be passed to __init__ due to smolagents wrapper restrictions
mock_kb_tool_class.assert_called_once_with(
top_k=10, # From filtered_params (not in conflict list)
- index_names=["conflicting_index"], # Not excluded by current implementation
+ # Not excluded by current implementation
+ index_names=["conflicting_index"],
)
# Verify excluded parameters were set directly as attributes after instantiation
assert result == mock_kb_tool_instance
assert mock_kb_tool_instance.observer == nexent_agent_instance.observer
assert mock_kb_tool_instance.vdb_core == mock_vdb_core # From metadata, not params
- assert mock_kb_tool_instance.embedding_model == mock_embedding_model # From metadata, not params
+ # From metadata, not params
+ assert mock_kb_tool_instance.embedding_model == mock_embedding_model
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_
assert mock_kb_tool_instance.embedding_model is None
assert result == mock_kb_tool_instance
+
def test_create_local_tool_analyze_text_file_tool(nexent_agent_instance):
"""Test AnalyzeTextFileTool creation injects observer and metadata."""
mock_analyze_tool_class = MagicMock()
@@ -1345,6 +1356,215 @@ def test_agent_run_with_observer_with_reset_false(nexent_agent_instance, mock_co
mock_core_agent.run.assert_called_once_with(
"test query", stream=True, reset=False)
+
+def test_agent_run_with_observer_removes_think_prefix_chinese_colon(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer removes '思考:' prefix content until two newlines."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with Chinese colon "思考:" followed by content and two newlines
+ final_answer_with_think = (
+ "思考:用户需要一份营养早餐的搭配建议。作为健康饮食搭配助手,我需要基于营养学知识,提供一份科学、均衡、易于准备的早餐方案。由于没有可用工具,我将直接给出建议,包括食物种类、分量和营养说明。\n\n"
+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。以下是我的推荐:"
+ )
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_with_think
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the "思考:" prefix content was removed
+ expected_final_answer = (
+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。以下是我的推荐:"
+ )
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
+def test_agent_run_with_observer_removes_think_prefix_english_colon(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer removes '思考:' prefix content until two newlines."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with English colon "思考:" followed by content and two newlines
+ final_answer_with_think = (
+ "思考:This is a thinking process about the user's question.\n\n"
+ "Here is the actual answer to the question."
+ )
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_with_think
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the "思考:" prefix content was removed
+ expected_final_answer = "Here is the actual answer to the question."
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
+def test_agent_run_with_observer_preserves_think_prefix_without_two_newlines(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer preserves '思考:' content when not followed by two newlines."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with "思考:" but only one newline (should not be removed)
+ final_answer_with_think = (
+ "思考:This is thinking content.\n"
+ "Here is the actual answer."
+ )
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_with_think
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the content was preserved (not removed because no \n\n)
+ expected_final_answer = (
+ "思考:This is thinking content.\n"
+ "Here is the actual answer."
+ )
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
+def test_agent_run_with_observer_removes_both_think_tag_and_think_prefix(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer removes both THINK_TAG_PATTERN and THINK_PREFIX_PATTERN."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with both tags and "思考:" prefix
+ final_answer_with_both = (
+ "Some reasoning content"
+ "思考:用户需要一份营养早餐的搭配建议。\n\n"
+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
+ )
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_with_both
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify both patterns were removed
+ expected_final_answer = "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
+def test_agent_run_with_observer_think_prefix_in_middle(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer removes '思考:' even when it appears in the middle of text."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with "思考:" in the middle of the text
+ final_answer_with_think = (
+ "Some initial content. "
+ "思考:This is thinking content in the middle.\n\n"
+ "Here is the rest of the answer."
+ )
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_with_think
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the "思考:" content was removed
+ expected_final_answer = "Some initial content. Here is the rest of the answer."
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
+def test_agent_run_with_observer_no_think_prefix(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer handles content without '思考:' prefix normally."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with normal content without "思考:" prefix
+ final_answer_normal = "This is a normal final answer without any thinking prefix."
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = final_answer_normal
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the content was preserved as-is
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, final_answer_normal
+ )
+
+
+def test_agent_run_with_observer_think_prefix_with_agent_text(nexent_agent_instance, mock_core_agent):
+ """Test agent_run_with_observer removes '思考:' prefix when final answer is AgentText."""
+ # Setup
+ nexent_agent_instance.agent = mock_core_agent
+ mock_core_agent.stop_event.is_set.return_value = False
+
+ # Mock step logs
+ mock_action_step = MagicMock(spec=ActionStep)
+ mock_action_step.duration = 1.0
+ mock_action_step.error = None
+
+ # Test with AgentText containing "思考:" prefix
+ final_answer_with_think = (
+ "思考:用户需要一份营养早餐的搭配建议。\n\n"
+ "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
+ )
+ mock_final_answer = _AgentText(final_answer_with_think)
+
+ mock_core_agent.run.return_value = [mock_action_step]
+ mock_core_agent.run.return_value[-1].output = mock_final_answer
+
+ # Execute
+ nexent_agent_instance.agent_run_with_observer("test query")
+
+ # Verify the "思考:" prefix content was removed
+ expected_final_answer = "一份营养均衡的早餐应包含碳水化合物、蛋白质、健康脂肪、维生素和矿物质。"
+ mock_core_agent.observer.add_message.assert_any_call(
+ "test_agent", ProcessType.FINAL_ANSWER, expected_final_answer
+ )
+
+
def test_create_local_tool_datamate_search_tool_success(nexent_agent_instance):
"""Test successful creation of DataMateSearchTool with metadata."""
mock_datamate_tool_class = MagicMock()
@@ -1385,7 +1605,6 @@ def test_create_local_tool_datamate_search_tool_success(nexent_agent_instance):
assert mock_datamate_tool_instance.observer == nexent_agent_instance.observer
-
def test_create_local_tool_datamate_search_tool_with_none_defaults(nexent_agent_instance):
"""Test DataMateSearchTool creation with None defaults when metadata is missing."""
mock_datamate_tool_class = MagicMock()