Skip to content

Commit 3cd9e45

Browse files
authored
🐛 knowledgebase search tools cannot initialize correctly
2 parents bc22cac + 8d9edb9 commit 3cd9e45

File tree

2 files changed

+257
-5
lines changed

2 files changed

+257
-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: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ class _TestCoreAgent:
8686
"openai.types.chat.chat_completion_message_param": MagicMock(),
8787
# Mock exa_py to avoid importing the real package when sdk.nexent.core.tools imports it
8888
"exa_py": MagicMock(Exa=MagicMock()),
89+
# Mock paramiko and cryptography to avoid PyO3 import issues in tests
90+
"paramiko": MagicMock(),
91+
"cryptography": MagicMock(),
92+
"cryptography.hazmat": MagicMock(),
93+
"cryptography.hazmat.primitives": MagicMock(),
94+
"cryptography.hazmat.primitives.ciphers": MagicMock(),
95+
"cryptography.hazmat.primitives.ciphers.base": MagicMock(),
96+
"cryptography.hazmat.bindings": MagicMock(),
97+
"cryptography.hazmat.bindings._rust": MagicMock(),
8998
# Mock the OpenAIModel import
9099
"sdk.nexent.core.models.openai_llm": MagicMock(OpenAIModel=mock_openai_model_class),
91100
# Mock CoreAgent import
@@ -100,6 +109,7 @@ class _TestCoreAgent:
100109
# ---------------------------------------------------------------------------
101110
with patch.dict("sys.modules", module_mocks):
102111
from sdk.nexent.core.utils.observer import MessageObserver, ProcessType
112+
from sdk.nexent.core.agents import nexent_agent
103113
from sdk.nexent.core.agents.nexent_agent import NexentAgent, ActionStep, TaskStep
104114
from sdk.nexent.core.agents.agent_model import ToolConfig, ModelConfig, AgentConfig
105115

@@ -447,6 +457,236 @@ def test_create_tool_with_local_source(nexent_agent_instance):
447457
assert result == "local_tool"
448458

449459

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

0 commit comments

Comments
 (0)