Skip to content

Commit 574c1ca

Browse files
authored
🐛 When integrating the deep thinking model, the content of the think label in the final answer should be removed. #1254
2 parents 1f83d34 + 472b858 commit 574c1ca

File tree

3 files changed

+241
-16
lines changed

3 files changed

+241
-16
lines changed

sdk/nexent/core/agents/nexent_agent.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from threading import Event
23
from typing import List
34

@@ -6,6 +7,7 @@
67

78
from ..models.openai_llm import OpenAIModel
89
from ..tools import * # Used for tool creation, do not delete!!!
10+
from ..utils.constants import THINK_TAG_PATTERN
911
from ..utils.observer import MessageObserver, ProcessType
1012
from .agent_model import AgentConfig, AgentHistory, ModelConfig, ToolConfig
1113
from .core_agent import CoreAgent, convert_code_format
@@ -192,11 +194,11 @@ def agent_run_with_observer(self, query: str, reset=True):
192194

193195
if isinstance(final_answer, AgentText):
194196
final_answer_str = convert_code_format(final_answer.to_string())
195-
observer.add_message(self.agent.agent_name, ProcessType.FINAL_ANSWER, final_answer_str)
196197
else:
197198
# prepare for multi-modal final_answer
198199
final_answer_str = convert_code_format(str(final_answer))
199-
observer.add_message(self.agent.agent_name, ProcessType.FINAL_ANSWER, final_answer_str)
200+
final_answer_str = re.sub(THINK_TAG_PATTERN, "", final_answer_str, flags=re.DOTALL | re.IGNORECASE)
201+
observer.add_message(self.agent.agent_name, ProcessType.FINAL_ANSWER, final_answer_str)
200202

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

sdk/nexent/core/utils/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
THINK_TAG_PATTERN = r"<think>.*?</think>"

test/sdk/core/agents/test_nexent_agent.py

Lines changed: 236 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,30 @@
99

1010
# Mock for smolagents and its sub-modules
1111
mock_smolagents = MagicMock()
12-
mock_smolagents.ActionStep = MagicMock()
13-
mock_smolagents.TaskStep = MagicMock()
14-
mock_smolagents.AgentText = MagicMock()
12+
13+
# Define lightweight classes to support isinstance checks in source code
14+
15+
16+
class _ActionStep:
17+
pass
18+
19+
20+
class _TaskStep:
21+
pass
22+
23+
24+
class _AgentText:
25+
def __init__(self, content: str = ""):
26+
self._content = content
27+
28+
def to_string(self):
29+
return self._content
30+
31+
32+
# Expose these classes on the mocked smolagents module
33+
mock_smolagents.ActionStep = _ActionStep
34+
mock_smolagents.TaskStep = _TaskStep
35+
mock_smolagents.AgentText = _AgentText
1536
mock_smolagents.handle_agent_output_types = MagicMock()
1637

1738
# Mock for smolagents.tools.Tool with a configurable from_langchain method
@@ -36,6 +57,15 @@
3657
mock_openai_model = MagicMock()
3758
mock_openai_model_class = MagicMock(return_value=mock_openai_model)
3859

60+
# Mock for CoreAgent
61+
62+
63+
class _TestCoreAgent:
64+
pass
65+
66+
67+
mock_core_agent_class = _TestCoreAgent
68+
3969
# Very lightweight mock for openai path required by internal OpenAIModel import
4070
mock_openai_chat_completion_message = MagicMock()
4171
module_mocks = {
@@ -58,14 +88,19 @@
5888
"exa_py": MagicMock(Exa=MagicMock()),
5989
# Mock the OpenAIModel import
6090
"sdk.nexent.core.models.openai_llm": MagicMock(OpenAIModel=mock_openai_model_class),
91+
# Mock CoreAgent import
92+
"sdk.nexent.core.agents.core_agent": MagicMock(
93+
CoreAgent=mock_core_agent_class,
94+
convert_code_format=lambda s: s if isinstance(s, str) else str(s),
95+
),
6196
}
6297

6398
# ---------------------------------------------------------------------------
6499
# Import the classes under test with patched dependencies in place
65100
# ---------------------------------------------------------------------------
66101
with patch.dict("sys.modules", module_mocks):
67-
from sdk.nexent.core.utils.observer import MessageObserver
68-
from sdk.nexent.core.agents.nexent_agent import NexentAgent
102+
from sdk.nexent.core.utils.observer import MessageObserver, ProcessType
103+
from sdk.nexent.core.agents.nexent_agent import NexentAgent, ActionStep, TaskStep
69104
from sdk.nexent.core.agents.agent_model import ToolConfig, ModelConfig, AgentConfig
70105

71106

@@ -80,6 +115,23 @@ def reset_mocks():
80115
return None
81116

82117

118+
@pytest.fixture(autouse=True)
119+
def patch_convert_code_format():
120+
"""Ensure convert_code_format returns a plain string for downstream re.sub."""
121+
import sys
122+
module = sys.modules.get("sdk.nexent.core.agents.nexent_agent")
123+
if module is None:
124+
# If the module is not imported yet, skip patching to avoid triggering imports
125+
yield
126+
return
127+
with patch.object(
128+
module,
129+
"convert_code_format",
130+
new=lambda s: s if isinstance(s, str) else str(s),
131+
):
132+
yield
133+
134+
83135
@pytest.fixture
84136
def mock_observer():
85137
"""Return a mocked MessageObserver instance."""
@@ -90,7 +142,8 @@ def mock_observer():
90142
@pytest.fixture
91143
def nexent_agent_instance(mock_observer):
92144
"""Create a NexentAgent instance with minimal initialisation."""
93-
agent = NexentAgent(observer=mock_observer, model_config_list=[], stop_event=Event())
145+
agent = NexentAgent(observer=mock_observer,
146+
model_config_list=[], stop_event=Event())
94147
return agent
95148

96149

@@ -124,7 +177,8 @@ def mock_deep_thinking_model_config():
124177
def nexent_agent_with_models(mock_observer, mock_model_config, mock_deep_thinking_model_config):
125178
"""Create a NexentAgent instance with model configurations."""
126179
model_config_list = [mock_model_config, mock_deep_thinking_model_config]
127-
agent = NexentAgent(observer=mock_observer, model_config_list=model_config_list, stop_event=Event())
180+
agent = NexentAgent(observer=mock_observer,
181+
model_config_list=model_config_list, stop_event=Event())
128182
return agent
129183

130184

@@ -146,13 +200,14 @@ def mock_agent_config():
146200
@pytest.fixture
147201
def mock_core_agent():
148202
"""Create a mock CoreAgent instance for testing."""
149-
agent = MagicMock()
203+
agent = mock_core_agent_class()
150204
agent.agent_name = "test_agent"
151205
agent.memory = MagicMock()
152206
agent.memory.steps = []
153207
agent.memory.reset = MagicMock()
154208
agent.observer = MagicMock()
155209
agent.stop_event = MagicMock()
210+
agent.run = MagicMock() # Ensure .run exists and is mockable
156211
return agent
157212

158213

@@ -163,7 +218,8 @@ def mock_core_agent():
163218
def test_nexent_agent_initialization_success(mock_observer):
164219
"""Test successful NexentAgent initialization."""
165220
stop_event = Event()
166-
agent = NexentAgent(observer=mock_observer, model_config_list=[], stop_event=stop_event)
221+
agent = NexentAgent(observer=mock_observer,
222+
model_config_list=[], stop_event=stop_event)
167223

168224
assert agent.observer == mock_observer
169225
assert agent.model_config_list == []
@@ -188,7 +244,8 @@ def test_nexent_agent_initialization_invalid_observer():
188244
invalid_observer = "not_a_message_observer"
189245

190246
with pytest.raises(TypeError, match="Create Observer Object with MessageObserver"):
191-
NexentAgent(observer=invalid_observer, model_config_list=[], stop_event=stop_event)
247+
NexentAgent(observer=invalid_observer,
248+
model_config_list=[], stop_event=stop_event)
192249

193250

194251
# ----------------------------------------------------------------------------
@@ -255,15 +312,17 @@ def test_create_model_not_found(nexent_agent_with_models):
255312

256313
def test_create_model_empty_config_list(mock_observer):
257314
"""Test create_model raises ValueError when model_config_list is empty."""
258-
agent = NexentAgent(observer=mock_observer, model_config_list=[], stop_event=Event())
315+
agent = NexentAgent(observer=mock_observer,
316+
model_config_list=[], stop_event=Event())
259317

260318
with pytest.raises(ValueError, match="Model test_model not found"):
261319
agent.create_model("test_model")
262320

263321

264322
def test_create_model_with_none_config_list(mock_observer):
265323
"""Test create_model raises ValueError when model_config_list contains None."""
266-
agent = NexentAgent(observer=mock_observer, model_config_list=[None], stop_event=Event())
324+
agent = NexentAgent(observer=mock_observer, model_config_list=[
325+
None], stop_event=Event())
267326

268327
with pytest.raises(ValueError, match="Model test_model not found"):
269328
agent.create_model("test_model")
@@ -289,7 +348,8 @@ def test_create_model_with_multiple_configs(mock_observer):
289348
)
290349

291350
stop_event = Event()
292-
agent = NexentAgent(observer=mock_observer, model_config_list=[config1, config2], stop_event=stop_event)
351+
agent = NexentAgent(observer=mock_observer, model_config_list=[
352+
config1, config2], stop_event=stop_event)
293353

294354
# Use the existing mock that was set up at the top of the file
295355
mock_model = MagicMock()
@@ -332,7 +392,8 @@ def test_create_langchain_tool_success(nexent_agent_instance):
332392
result = nexent_agent_instance.create_langchain_tool(tool_config)
333393

334394
# Assertions
335-
mock_from_langchain.assert_called_once_with({"inner_tool": mock_langchain_tool_obj})
395+
mock_from_langchain.assert_called_once_with(
396+
{"inner_tool": mock_langchain_tool_obj})
336397
assert result == "converted_tool"
337398

338399

@@ -501,5 +562,166 @@ def test_add_history_to_agent_none_history(nexent_agent_instance, mock_core_agen
501562
assert len(mock_core_agent.memory.steps) == 0
502563

503564

565+
def test_agent_run_with_observer_success_with_agent_text(nexent_agent_instance, mock_core_agent):
566+
"""Test successful agent_run_with_observer with AgentText final answer."""
567+
# Setup
568+
nexent_agent_instance.agent = mock_core_agent
569+
mock_core_agent.stop_event.is_set.return_value = False
570+
571+
# Mock step logs
572+
mock_action_step = MagicMock(spec=ActionStep)
573+
mock_action_step.duration = 1.5
574+
mock_action_step.error = None
575+
576+
# Use an instance of our _AgentText so isinstance(..., AgentText) is valid
577+
mock_final_answer = _AgentText(
578+
"Final answer with <think>thinking</think> content")
579+
580+
mock_core_agent.run.return_value = [mock_action_step]
581+
mock_core_agent.run.return_value[-1].final_answer = mock_final_answer
582+
583+
# Execute
584+
nexent_agent_instance.agent_run_with_observer("test query")
585+
586+
# Verify
587+
mock_core_agent.run.assert_called_once_with(
588+
"test query", stream=True, reset=True)
589+
mock_core_agent.observer.add_message.assert_any_call(
590+
"", ProcessType.TOKEN_COUNT, "1.5")
591+
mock_core_agent.observer.add_message.assert_any_call(
592+
"test_agent", ProcessType.FINAL_ANSWER, "Final answer with content")
593+
594+
595+
def test_agent_run_with_observer_success_with_string_final_answer(nexent_agent_instance, mock_core_agent):
596+
"""Test successful agent_run_with_observer with string final answer."""
597+
# Setup
598+
nexent_agent_instance.agent = mock_core_agent
599+
mock_core_agent.stop_event.is_set.return_value = False
600+
601+
# Mock step logs
602+
mock_action_step = MagicMock(spec=ActionStep)
603+
mock_action_step.duration = 2.0
604+
mock_action_step.error = None
605+
606+
mock_core_agent.run.return_value = [mock_action_step]
607+
mock_core_agent.run.return_value[-1].final_answer = "String final answer with <think>thinking</think>"
608+
609+
# Execute
610+
nexent_agent_instance.agent_run_with_observer("test query")
611+
612+
# Verify
613+
mock_core_agent.observer.add_message.assert_any_call(
614+
"", ProcessType.TOKEN_COUNT, "2.0")
615+
mock_core_agent.observer.add_message.assert_any_call(
616+
"test_agent", ProcessType.FINAL_ANSWER, "String final answer with ")
617+
618+
619+
def test_agent_run_with_observer_with_error_in_step(nexent_agent_instance, mock_core_agent):
620+
"""Test agent_run_with_observer handles error in step log."""
621+
# Setup
622+
nexent_agent_instance.agent = mock_core_agent
623+
mock_core_agent.stop_event.is_set.return_value = False
624+
625+
# Mock step logs with error
626+
mock_action_step = MagicMock(spec=ActionStep)
627+
mock_action_step.duration = 1.0
628+
mock_action_step.error = "Test error occurred"
629+
630+
mock_core_agent.run.return_value = [mock_action_step]
631+
mock_core_agent.run.return_value[-1].final_answer = "Final answer"
632+
633+
# Execute
634+
nexent_agent_instance.agent_run_with_observer("test query")
635+
636+
# Verify error message was added
637+
mock_core_agent.observer.add_message.assert_any_call(
638+
"", ProcessType.ERROR, "Test error occurred")
639+
640+
641+
def test_agent_run_with_observer_skips_non_action_step(nexent_agent_instance, mock_core_agent):
642+
"""Test agent_run_with_observer skips non-ActionStep logs."""
643+
# Setup
644+
nexent_agent_instance.agent = mock_core_agent
645+
mock_core_agent.stop_event.is_set.return_value = False
646+
647+
# Mock step logs with non-ActionStep
648+
mock_task_step = MagicMock(spec=TaskStep)
649+
mock_action_step = MagicMock(spec=ActionStep)
650+
mock_action_step.duration = 1.0
651+
mock_action_step.error = None
652+
653+
mock_core_agent.run.return_value = [mock_task_step, mock_action_step]
654+
mock_core_agent.run.return_value[-1].final_answer = "Final answer"
655+
656+
# Execute
657+
nexent_agent_instance.agent_run_with_observer("test query")
658+
659+
# Verify only ActionStep was processed
660+
mock_core_agent.observer.add_message.assert_any_call(
661+
"", ProcessType.TOKEN_COUNT, "1.0")
662+
# Should not process TaskStep
663+
664+
665+
def test_agent_run_with_observer_with_stop_event_set(nexent_agent_instance, mock_core_agent):
666+
"""Test agent_run_with_observer handles stop event being set."""
667+
# Setup
668+
nexent_agent_instance.agent = mock_core_agent
669+
mock_core_agent.stop_event.is_set.return_value = True
670+
671+
# Mock step logs
672+
mock_action_step = MagicMock(spec=ActionStep)
673+
mock_action_step.duration = 1.0
674+
mock_action_step.error = None
675+
676+
mock_core_agent.run.return_value = [mock_action_step]
677+
mock_core_agent.run.return_value[-1].final_answer = "Final answer"
678+
679+
# Execute
680+
nexent_agent_instance.agent_run_with_observer("test query")
681+
682+
# Verify stop event message was added
683+
mock_core_agent.observer.add_message.assert_any_call(
684+
"test_agent", ProcessType.ERROR, "Agent execution interrupted by external stop signal"
685+
)
686+
687+
688+
def test_agent_run_with_observer_with_exception(nexent_agent_instance, mock_core_agent):
689+
"""Test agent_run_with_observer handles exceptions during execution."""
690+
# Setup
691+
nexent_agent_instance.agent = mock_core_agent
692+
mock_core_agent.run.side_effect = Exception("Test execution error")
693+
694+
# Execute and verify exception is raised
695+
with pytest.raises(ValueError, match="Error in interaction: Test execution error"):
696+
nexent_agent_instance.agent_run_with_observer("test query")
697+
698+
# Verify error message was added to observer
699+
mock_core_agent.observer.add_message.assert_called_once_with(
700+
agent_name="test_agent", process_type=ProcessType.ERROR, content="Error in interaction: Test execution error"
701+
)
702+
703+
704+
def test_agent_run_with_observer_with_reset_false(nexent_agent_instance, mock_core_agent):
705+
"""Test agent_run_with_observer with reset=False parameter."""
706+
# Setup
707+
nexent_agent_instance.agent = mock_core_agent
708+
mock_core_agent.stop_event.is_set.return_value = False
709+
710+
# Mock step logs
711+
mock_action_step = MagicMock(spec=ActionStep)
712+
mock_action_step.duration = 1.0
713+
mock_action_step.error = None
714+
715+
mock_core_agent.run.return_value = [mock_action_step]
716+
mock_core_agent.run.return_value[-1].final_answer = "Final answer"
717+
718+
# Execute with reset=False
719+
nexent_agent_instance.agent_run_with_observer("test query", reset=False)
720+
721+
# Verify run was called with reset=False
722+
mock_core_agent.run.assert_called_once_with(
723+
"test query", stream=True, reset=False)
724+
725+
504726
if __name__ == "__main__":
505727
pytest.main([__file__])

0 commit comments

Comments
 (0)