Skip to content

Commit 395d10b

Browse files
committed
🐛 Bugfix: agent tools params cannot initialize correctly
1 parent bc22cac commit 395d10b

File tree

2 files changed

+252
-5
lines changed

2 files changed

+252
-5
lines changed

sdk/nexent/core/agents/nexent_agent.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,23 @@ def create_local_tool(self, tool_config: ToolConfig):
6666
raise ValueError(f"{class_name} not found in local")
6767
else:
6868
if class_name == "KnowledgeBaseSearchTool":
69-
tools_obj = tool_class(index_names=tool_config.metadata.get("index_names", []),
70-
observer=self.observer,
71-
vdb_core=tool_config.metadata.get("vdb_core", []),
72-
embedding_model=tool_config.metadata.get("embedding_model", []),
73-
**params)
69+
# Filter out conflicting parameters from params to avoid conflicts
70+
# These parameters have exclude=True and cannot be passed to __init__
71+
# due to smolagents.tools.Tool wrapper restrictions
72+
filtered_params = {k: v for k, v in params.items()
73+
if k not in ["index_names", "vdb_core", "embedding_model", "observer"]}
74+
# Create instance with only non-excluded parameters
75+
tools_obj = tool_class(**filtered_params)
76+
# Set excluded parameters directly as attributes after instantiation
77+
# This bypasses smolagents wrapper restrictions
78+
tools_obj.observer = self.observer
79+
index_names = tool_config.metadata.get(
80+
"index_names", None) if tool_config.metadata else None
81+
tools_obj.index_names = [] if index_names is None else index_names
82+
tools_obj.vdb_core = tool_config.metadata.get(
83+
"vdb_core", None) if tool_config.metadata else None
84+
tools_obj.embedding_model = tool_config.metadata.get(
85+
"embedding_model", None) if tool_config.metadata else None
7486
else:
7587
tools_obj = tool_class(**params)
7688
if hasattr(tools_obj, 'observer'):

test/sdk/core/agents/test_nexent_agent.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,241 @@ def test_create_tool_with_local_source(nexent_agent_instance):
447447
assert result == "local_tool"
448448

449449

450+
def test_create_local_tool_success(nexent_agent_instance):
451+
"""Test successful creation of a local tool."""
452+
mock_tool_class = MagicMock()
453+
mock_tool_instance = MagicMock()
454+
mock_tool_class.return_value = mock_tool_instance
455+
456+
tool_config = ToolConfig(
457+
class_name="DummyTool",
458+
name="dummy",
459+
description="desc",
460+
inputs="{}",
461+
output_type="string",
462+
params={"param1": "value1", "param2": 42},
463+
source="local",
464+
metadata={},
465+
)
466+
467+
# Patch the module's globals to include our mock tool class
468+
import sdk.nexent.core.agents.nexent_agent as nexent_agent_module
469+
original_value = nexent_agent_module.__dict__.get("DummyTool")
470+
nexent_agent_module.__dict__["DummyTool"] = mock_tool_class
471+
472+
try:
473+
result = nexent_agent_instance.create_local_tool(tool_config)
474+
finally:
475+
# Restore original value
476+
if original_value is not None:
477+
nexent_agent_module.__dict__["DummyTool"] = original_value
478+
elif "DummyTool" in nexent_agent_module.__dict__:
479+
del nexent_agent_module.__dict__["DummyTool"]
480+
481+
mock_tool_class.assert_called_once_with(param1="value1", param2=42)
482+
assert result == mock_tool_instance
483+
484+
485+
def test_create_local_tool_class_not_found(nexent_agent_instance):
486+
"""Test create_local_tool raises ValueError when class is not found."""
487+
tool_config = ToolConfig(
488+
class_name="NonExistentTool",
489+
name="dummy",
490+
description="desc",
491+
inputs="{}",
492+
output_type="string",
493+
params={},
494+
source="local",
495+
metadata={},
496+
)
497+
498+
with pytest.raises(ValueError, match="NonExistentTool not found in local"):
499+
nexent_agent_instance.create_local_tool(tool_config)
500+
501+
502+
def test_create_local_tool_knowledge_base_search_tool_success(nexent_agent_instance):
503+
"""Test successful creation of KnowledgeBaseSearchTool with metadata."""
504+
mock_kb_tool_class = MagicMock()
505+
mock_kb_tool_instance = MagicMock()
506+
mock_kb_tool_class.return_value = mock_kb_tool_instance
507+
508+
mock_vdb_core = MagicMock()
509+
mock_embedding_model = MagicMock()
510+
511+
tool_config = ToolConfig(
512+
class_name="KnowledgeBaseSearchTool",
513+
name="knowledge_base_search",
514+
description="desc",
515+
inputs="{}",
516+
output_type="string",
517+
params={"top_k": 10},
518+
source="local",
519+
metadata={
520+
"index_names": ["index1", "index2"],
521+
"vdb_core": mock_vdb_core,
522+
"embedding_model": mock_embedding_model,
523+
},
524+
)
525+
526+
import sdk.nexent.core.agents.nexent_agent as nexent_agent_module
527+
original_value = nexent_agent_module.__dict__.get("KnowledgeBaseSearchTool")
528+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = mock_kb_tool_class
529+
530+
try:
531+
result = nexent_agent_instance.create_local_tool(tool_config)
532+
finally:
533+
# Restore original value
534+
if original_value is not None:
535+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = original_value
536+
elif "KnowledgeBaseSearchTool" in nexent_agent_module.__dict__:
537+
del nexent_agent_module.__dict__["KnowledgeBaseSearchTool"]
538+
539+
# Verify only non-excluded params are passed to __init__
540+
mock_kb_tool_class.assert_called_once_with(
541+
top_k=10, # Only non-excluded params passed to __init__
542+
)
543+
# Verify excluded parameters were set directly as attributes after instantiation
544+
assert result == mock_kb_tool_instance
545+
assert mock_kb_tool_instance.observer == nexent_agent_instance.observer
546+
assert mock_kb_tool_instance.index_names == ["index1", "index2"]
547+
assert mock_kb_tool_instance.vdb_core == mock_vdb_core
548+
assert mock_kb_tool_instance.embedding_model == mock_embedding_model
549+
550+
551+
def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params(nexent_agent_instance):
552+
"""Test KnowledgeBaseSearchTool creation filters out conflicting params from params dict."""
553+
mock_kb_tool_class = MagicMock()
554+
mock_kb_tool_instance = MagicMock()
555+
mock_kb_tool_class.return_value = mock_kb_tool_instance
556+
557+
mock_vdb_core = MagicMock()
558+
mock_embedding_model = MagicMock()
559+
560+
tool_config = ToolConfig(
561+
class_name="KnowledgeBaseSearchTool",
562+
name="knowledge_base_search",
563+
description="desc",
564+
inputs="{}",
565+
output_type="string",
566+
params={
567+
"top_k": 10,
568+
"index_names": ["conflicting_index"], # This should be filtered out
569+
"vdb_core": "conflicting_vdb", # This should be filtered out
570+
"embedding_model": "conflicting_model", # This should be filtered out
571+
"observer": "conflicting_observer", # This should be filtered out
572+
},
573+
source="local",
574+
metadata={
575+
"index_names": ["index1", "index2"], # These should be used instead
576+
"vdb_core": mock_vdb_core,
577+
"embedding_model": mock_embedding_model,
578+
},
579+
)
580+
581+
import sdk.nexent.core.agents.nexent_agent as nexent_agent_module
582+
original_value = nexent_agent_module.__dict__.get("KnowledgeBaseSearchTool")
583+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = mock_kb_tool_class
584+
585+
try:
586+
result = nexent_agent_instance.create_local_tool(tool_config)
587+
finally:
588+
# Restore original value
589+
if original_value is not None:
590+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = original_value
591+
elif "KnowledgeBaseSearchTool" in nexent_agent_module.__dict__:
592+
del nexent_agent_module.__dict__["KnowledgeBaseSearchTool"]
593+
594+
# Verify conflicting params were filtered out from __init__ call
595+
# Only non-excluded params should be passed to __init__ due to smolagents wrapper restrictions
596+
mock_kb_tool_class.assert_called_once_with(
597+
top_k=10, # From filtered_params (not in conflict list)
598+
)
599+
# Verify excluded parameters were set directly as attributes after instantiation
600+
assert result == mock_kb_tool_instance
601+
assert mock_kb_tool_instance.observer == nexent_agent_instance.observer
602+
assert mock_kb_tool_instance.index_names == ["index1", "index2"] # From metadata, not params
603+
assert mock_kb_tool_instance.vdb_core == mock_vdb_core # From metadata, not params
604+
assert mock_kb_tool_instance.embedding_model == mock_embedding_model # From metadata, not params
605+
606+
607+
def test_create_local_tool_knowledge_base_search_tool_with_none_defaults(nexent_agent_instance):
608+
"""Test KnowledgeBaseSearchTool creation with None defaults when metadata is missing."""
609+
mock_kb_tool_class = MagicMock()
610+
mock_kb_tool_instance = MagicMock()
611+
mock_kb_tool_class.return_value = mock_kb_tool_instance
612+
613+
tool_config = ToolConfig(
614+
class_name="KnowledgeBaseSearchTool",
615+
name="knowledge_base_search",
616+
description="desc",
617+
inputs="{}",
618+
output_type="string",
619+
params={"top_k": 5},
620+
source="local",
621+
metadata={}, # No metadata provided
622+
)
623+
624+
import sdk.nexent.core.agents.nexent_agent as nexent_agent_module
625+
original_value = nexent_agent_module.__dict__.get("KnowledgeBaseSearchTool")
626+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = mock_kb_tool_class
627+
628+
try:
629+
result = nexent_agent_instance.create_local_tool(tool_config)
630+
finally:
631+
# Restore original value
632+
if original_value is not None:
633+
nexent_agent_module.__dict__["KnowledgeBaseSearchTool"] = original_value
634+
elif "KnowledgeBaseSearchTool" in nexent_agent_module.__dict__:
635+
del nexent_agent_module.__dict__["KnowledgeBaseSearchTool"]
636+
637+
# Verify only non-excluded params are passed to __init__
638+
mock_kb_tool_class.assert_called_once_with(
639+
top_k=5,
640+
)
641+
# Verify excluded parameters were set directly as attributes with None defaults when metadata is missing
642+
assert result == mock_kb_tool_instance
643+
assert mock_kb_tool_instance.observer == nexent_agent_instance.observer
644+
assert mock_kb_tool_instance.index_names == [] # Empty list when None
645+
assert mock_kb_tool_instance.vdb_core is None
646+
assert mock_kb_tool_instance.embedding_model is None
647+
assert result == mock_kb_tool_instance
648+
649+
650+
def test_create_local_tool_with_observer_attribute(nexent_agent_instance):
651+
"""Test create_local_tool sets observer attribute on tool if it exists."""
652+
mock_tool_class = MagicMock()
653+
mock_tool_instance = MagicMock()
654+
mock_tool_instance.observer = None # Initially no observer
655+
mock_tool_class.return_value = mock_tool_instance
656+
657+
tool_config = ToolConfig(
658+
class_name="ToolWithObserver",
659+
name="tool",
660+
description="desc",
661+
inputs="{}",
662+
output_type="string",
663+
params={},
664+
source="local",
665+
metadata={},
666+
)
667+
668+
import sdk.nexent.core.agents.nexent_agent as nexent_agent_module
669+
original_value = nexent_agent_module.__dict__.get("ToolWithObserver")
670+
nexent_agent_module.__dict__["ToolWithObserver"] = mock_tool_class
671+
672+
try:
673+
result = nexent_agent_instance.create_local_tool(tool_config)
674+
finally:
675+
# Restore original value
676+
if original_value is not None:
677+
nexent_agent_module.__dict__["ToolWithObserver"] = original_value
678+
elif "ToolWithObserver" in nexent_agent_module.__dict__:
679+
del nexent_agent_module.__dict__["ToolWithObserver"]
680+
681+
# Verify observer was set on the tool instance
682+
assert result.observer == nexent_agent_instance.observer
683+
684+
450685
def test_create_tool_with_mcp_source(nexent_agent_instance):
451686
"""Ensure create_tool dispatches to create_mcp_tool for mcp source."""
452687
tool_config = ToolConfig(

0 commit comments

Comments
 (0)