99
1010# Mock for smolagents and its sub-modules
1111mock_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
1536mock_smolagents .handle_agent_output_types = MagicMock ()
1637
1738# Mock for smolagents.tools.Tool with a configurable from_langchain method
3657mock_openai_model = MagicMock ()
3758mock_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
4070mock_openai_chat_completion_message = MagicMock ()
4171module_mocks = {
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# ---------------------------------------------------------------------------
66101with 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
84136def mock_observer ():
85137 """Return a mocked MessageObserver instance."""
@@ -90,7 +142,8 @@ def mock_observer():
90142@pytest .fixture
91143def 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():
124177def 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
147201def 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():
163218def 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
256313def 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
264322def 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+
504726if __name__ == "__main__" :
505727 pytest .main ([__file__ ])
0 commit comments