Skip to content

Commit 8a39091

Browse files
committed
✨ file to text tool
1 parent 43b16c0 commit 8a39091

File tree

2 files changed

+259
-24
lines changed

2 files changed

+259
-24
lines changed

test/backend/agents/test_create_agent_info.py

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
import pytest
22
import sys
3+
import types
34
from unittest.mock import AsyncMock, MagicMock, patch, Mock, PropertyMock
45

5-
# Mock consts module first to avoid ModuleNotFoundError
6-
consts_mock = MagicMock()
7-
consts_mock.const = MagicMock()
8-
# Set required constants in consts.const
9-
consts_mock.const.MINIO_ENDPOINT = "http://localhost:9000"
10-
consts_mock.const.MINIO_ACCESS_KEY = "test_access_key"
11-
consts_mock.const.MINIO_SECRET_KEY = "test_secret_key"
12-
consts_mock.const.MINIO_REGION = "us-east-1"
13-
consts_mock.const.MINIO_DEFAULT_BUCKET = "test-bucket"
14-
consts_mock.const.POSTGRES_HOST = "localhost"
15-
consts_mock.const.POSTGRES_USER = "test_user"
16-
consts_mock.const.NEXENT_POSTGRES_PASSWORD = "test_password"
17-
consts_mock.const.POSTGRES_DB = "test_db"
18-
consts_mock.const.POSTGRES_PORT = 5432
19-
consts_mock.const.DEFAULT_TENANT_ID = "default_tenant"
20-
consts_mock.const.LOCAL_MCP_SERVER = "http://localhost:5011"
21-
consts_mock.const.MODEL_CONFIG_MAPPING = {"llm": "llm_config"}
22-
consts_mock.const.LANGUAGE = {"ZH": "zh"}
23-
24-
# Add the mocked consts module to sys.modules
25-
sys.modules['consts'] = consts_mock
26-
sys.modules['consts.const'] = consts_mock.const
6+
from test.common.env_test_utils import bootstrap_env
7+
8+
env_state = bootstrap_env()
9+
consts_const = env_state["mock_const"]
10+
11+
# Utilities ---------------------------------------------------------------
12+
def _create_stub_module(name: str, **attrs):
13+
"""Return a lightweight module stub with the provided attributes."""
14+
module = types.ModuleType(name)
15+
for attr_name, attr_value in attrs.items():
16+
setattr(module, attr_name, attr_value)
17+
return module
18+
19+
20+
# Configure required constants via shared bootstrap env
21+
consts_const.MINIO_ENDPOINT = "http://localhost:9000"
22+
consts_const.MINIO_ACCESS_KEY = "test_access_key"
23+
consts_const.MINIO_SECRET_KEY = "test_secret_key"
24+
consts_const.MINIO_REGION = "us-east-1"
25+
consts_const.MINIO_DEFAULT_BUCKET = "test-bucket"
26+
consts_const.POSTGRES_HOST = "localhost"
27+
consts_const.POSTGRES_USER = "test_user"
28+
consts_const.NEXENT_POSTGRES_PASSWORD = "test_password"
29+
consts_const.POSTGRES_DB = "test_db"
30+
consts_const.POSTGRES_PORT = 5432
31+
consts_const.DEFAULT_TENANT_ID = "default_tenant"
32+
consts_const.LOCAL_MCP_SERVER = "http://localhost:5011"
33+
consts_const.MODEL_CONFIG_MAPPING = {"llm": "llm_config"}
34+
consts_const.LANGUAGE = {"ZH": "zh"}
2735

2836
# Mock utils module
2937
utils_mock = MagicMock()
@@ -49,13 +57,24 @@
4957

5058
# Add the mocked client module to sys.modules
5159
sys.modules['backend.database.client'] = client_mock
60+
sys.modules['database.client'] = _create_stub_module(
61+
"database.client",
62+
minio_client=MagicMock(),
63+
postgres_client=MagicMock(),
64+
db_client=MagicMock(),
65+
get_db_session=MagicMock(),
66+
as_dict=MagicMock(),
67+
)
5268

5369
# Mock external dependencies before imports
54-
sys.modules['nexent.core.utils.observer'] = MagicMock()
70+
mock_message_observer = MagicMock()
71+
sys.modules['nexent.core.utils.observer'] = MagicMock(MessageObserver=mock_message_observer)
5572
sys.modules['nexent.core.agents.agent_model'] = MagicMock()
5673
sys.modules['smolagents.agents'] = MagicMock()
5774
sys.modules['smolagents.utils'] = MagicMock()
5875
sys.modules['services.remote_mcp_service'] = MagicMock()
76+
database_module = _create_stub_module("database")
77+
sys.modules['database'] = database_module
5978
sys.modules['database.agent_db'] = MagicMock()
6079
sys.modules['database.tool_db'] = MagicMock()
6180
sys.modules['database.model_management_db'] = MagicMock()
@@ -67,14 +86,30 @@
6786
sys.modules['utils.model_name_utils'] = MagicMock()
6887
sys.modules['langchain_core.tools'] = MagicMock()
6988
sys.modules['services.memory_config_service'] = MagicMock()
89+
# Build services module hierarchy with minimal functionality
90+
services_module = _create_stub_module("services")
91+
sys.modules['services'] = services_module
92+
sys.modules['services.image_service'] = _create_stub_module(
93+
"services.image_service", get_vlm_model=MagicMock(return_value="stub_vlm")
94+
)
95+
# Build top-level nexent module to avoid importing the real package
96+
nexent_module = _create_stub_module(
97+
"nexent",
98+
MessageObserver=mock_message_observer,
99+
)
100+
sys.modules['nexent'] = nexent_module
101+
102+
# Create nested modules for nexent.core to satisfy imports safely
103+
sys.modules['nexent.core'] = _create_stub_module("nexent.core")
104+
sys.modules['nexent.core.agents'] = _create_stub_module("nexent.core.agents")
105+
sys.modules['nexent.core.utils'] = _create_stub_module("nexent.core.utils")
70106
sys.modules['nexent.memory.memory_service'] = MagicMock()
71107

72108
# Create mock classes that might be imported
73109
mock_agent_config = MagicMock()
74110
mock_model_config = MagicMock()
75111
mock_tool_config = MagicMock()
76112
mock_agent_run_info = MagicMock()
77-
mock_message_observer = MagicMock()
78113

79114
sys.modules['nexent.core.agents.agent_model'].AgentConfig = mock_agent_config
80115
sys.modules['nexent.core.agents.agent_model'].ModelConfig = mock_model_config
@@ -85,6 +120,13 @@
85120
# Mock BASE_BUILTIN_MODULES
86121
sys.modules['smolagents.utils'].BASE_BUILTIN_MODULES = ["os", "sys", "json"]
87122

123+
# Provide lightweight smolagents package to prevent circular imports
124+
smolagents_module = _create_stub_module("smolagents")
125+
smolagents_tools_module = _create_stub_module("smolagents.tools", Tool=MagicMock())
126+
smolagents_module.tools = smolagents_tools_module
127+
sys.modules['smolagents'] = smolagents_module
128+
sys.modules['smolagents.tools'] = smolagents_tools_module
129+
88130
# Now import the module under test
89131
from backend.agents.create_agent_info import (
90132
discover_langchain_tools,
@@ -282,6 +324,43 @@ async def test_create_tool_config_list_with_knowledge_base_tool(self):
282324
last_call = mock_tool_config.call_args_list[-1]
283325
assert last_call[1]['class_name'] == "KnowledgeBaseSearchTool"
284326

327+
@pytest.mark.asyncio
328+
async def test_create_tool_config_list_with_analyze_text_file_tool(self):
329+
"""Ensure AnalyzeTextFileTool receives text-specific metadata."""
330+
mock_tool_instance = MagicMock()
331+
mock_tool_instance.class_name = "AnalyzeTextFileTool"
332+
mock_tool_config.return_value = mock_tool_instance
333+
334+
with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \
335+
patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \
336+
patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \
337+
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock) as mock_minio_client:
338+
339+
mock_search_tools.return_value = [
340+
{
341+
"class_name": "AnalyzeTextFileTool",
342+
"name": "analyze_text_file",
343+
"description": "Analyze text file tool",
344+
"inputs": "string",
345+
"output_type": "array",
346+
"params": [{"name": "prompt", "default": "describe"}],
347+
"source": "local",
348+
"usage": None
349+
}
350+
]
351+
mock_get_llm_model.return_value = "mock_llm_model"
352+
353+
result = await create_tool_config_list("agent_1", "tenant_1", "user_1")
354+
355+
assert len(result) == 1
356+
assert result[0] is mock_tool_instance
357+
mock_get_llm_model.assert_called_once_with(tenant_id="tenant_1")
358+
assert mock_tool_instance.metadata == {
359+
"llm_model": "mock_llm_model",
360+
"storage_client": mock_minio_client,
361+
"data_process_service_url": consts_const.DATA_PROCESS_SERVICE,
362+
}
363+
285364

286365
class TestCreateAgentConfig:
287366
"""Tests for the create_agent_config function"""

test/sdk/core/agents/test_nexent_agent.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1+
import sys
2+
import types
3+
from pathlib import Path
14
from threading import Event
25
from unittest.mock import MagicMock, patch
36

47
import pytest
58

9+
TEST_ROOT = Path(__file__).resolve().parents[3]
10+
PROJECT_ROOT = TEST_ROOT.parent
11+
for _path in (str(PROJECT_ROOT), str(TEST_ROOT)):
12+
if _path not in sys.path:
13+
sys.path.insert(0, _path)
14+
15+
SDK_SOURCE_ROOT = PROJECT_ROOT / "sdk"
16+
sdk_namespace_module = types.ModuleType("sdk")
17+
sdk_namespace_module.__path__ = [str(SDK_SOURCE_ROOT)]
18+
619
# ---------------------------------------------------------------------------
720
# Prepare mocks for external dependencies that are not required for this test
821
# ---------------------------------------------------------------------------
@@ -68,7 +81,91 @@ class _TestCoreAgent:
6881

6982
# Very lightweight mock for openai path required by internal OpenAIModel import
7083
mock_openai_chat_completion_message = MagicMock()
84+
85+
mock_botocore_module = types.ModuleType("botocore")
86+
mock_botocore_exceptions = types.ModuleType("botocore.exceptions")
87+
mock_botocore_exceptions.ClientError = MagicMock()
88+
mock_botocore_module.exceptions = mock_botocore_exceptions
89+
mock_botocore_client = types.ModuleType("botocore.client")
90+
mock_botocore_client.Config = MagicMock()
91+
mock_botocore_args = types.ModuleType("botocore.args")
92+
mock_botocore_args.ClientArgsCreator = MagicMock()
93+
mock_botocore_regions = types.ModuleType("botocore.regions")
94+
mock_botocore_regions.EndpointResolverBuiltins = MagicMock()
95+
mock_botocore_crt = types.ModuleType("botocore.crt")
96+
mock_botocore_crt.CRT_SUPPORTED_AUTH_TYPES = []
97+
98+
99+
class _MockMessageObserver:
100+
def add_message(self, *args, **kwargs):
101+
return None
102+
103+
104+
class _MockProcessType:
105+
TOKEN_COUNT = "token_count"
106+
FINAL_ANSWER = "final_answer"
107+
ERROR = "error"
108+
109+
110+
mock_nexent_core_utils_module = types.ModuleType("nexent.core.utils")
111+
mock_nexent_core_utils_observer_module = types.ModuleType("nexent.core.utils.observer")
112+
mock_nexent_core_utils_observer_module.MessageObserver = _MockMessageObserver
113+
mock_nexent_core_utils_observer_module.ProcessType = _MockProcessType
114+
115+
mock_prompt_template_utils_module = types.ModuleType(
116+
"nexent.core.utils.prompt_template_utils"
117+
)
118+
mock_prompt_template_utils_module.get_prompt_template = MagicMock(return_value="")
119+
120+
mock_tools_common_message_module = types.ModuleType(
121+
"nexent.core.utils.tools_common_message"
122+
)
123+
124+
125+
class _EnumStub:
126+
def __init__(self, value):
127+
self.value = value
128+
129+
130+
class _MockToolCategory:
131+
SEARCH = _EnumStub("search")
132+
FILE = _EnumStub("file")
133+
EMAIL = _EnumStub("email")
134+
TERMINAL = _EnumStub("terminal")
135+
MULTIMODAL = _EnumStub("multimodal")
136+
137+
138+
class _MockToolSign:
139+
KNOWLEDGE_BASE = _EnumStub("a")
140+
EXA_SEARCH = _EnumStub("b")
141+
LINKUP_SEARCH = _EnumStub("c")
142+
TAVILY_SEARCH = _EnumStub("d")
143+
FILE_OPERATION = _EnumStub("f")
144+
TERMINAL_OPERATION = _EnumStub("t")
145+
MULTIMODAL_OPERATION = _EnumStub("m")
146+
147+
148+
mock_tools_common_message_module.ToolCategory = _MockToolCategory
149+
mock_tools_common_message_module.ToolSign = _MockToolSign
150+
151+
mock_nexent_core_utils_module.observer = mock_nexent_core_utils_observer_module
152+
mock_nexent_core_utils_module.prompt_template_utils = mock_prompt_template_utils_module
153+
mock_nexent_core_utils_module.tools_common_message = mock_tools_common_message_module
154+
155+
mock_nexent_core_module = types.ModuleType("nexent.core")
156+
mock_nexent_core_module.utils = mock_nexent_core_utils_module
157+
mock_nexent_core_module.MessageObserver = _MockMessageObserver
158+
mock_nexent_module = types.ModuleType("nexent")
159+
mock_nexent_module.core = mock_nexent_core_module
160+
mock_nexent_storage_module = types.ModuleType("nexent.storage")
161+
mock_nexent_storage_module.MinIOStorageClient = MagicMock()
162+
mock_nexent_module.storage = mock_nexent_storage_module
163+
mock_nexent_multi_modal_module = types.ModuleType("nexent.multi_modal")
164+
mock_nexent_load_save_module = types.ModuleType("nexent.multi_modal.load_save_object")
165+
mock_nexent_load_save_module.LoadSaveObjectManager = MagicMock()
166+
mock_nexent_module.multi_modal = mock_nexent_multi_modal_module
71167
module_mocks = {
168+
"sdk": sdk_namespace_module,
72169
"smolagents": mock_smolagents,
73170
"smolagents.tools": mock_smolagents_tools,
74171
"smolagents.agents": MagicMock(),
@@ -95,6 +192,22 @@ class _TestCoreAgent:
95192
"cryptography.hazmat.primitives.ciphers.base": MagicMock(),
96193
"cryptography.hazmat.bindings": MagicMock(),
97194
"cryptography.hazmat.bindings._rust": MagicMock(),
195+
"boto3": MagicMock(),
196+
"botocore": mock_botocore_module,
197+
"botocore.client": mock_botocore_client,
198+
"botocore.exceptions": mock_botocore_exceptions,
199+
"botocore.args": mock_botocore_args,
200+
"botocore.regions": mock_botocore_regions,
201+
"botocore.crt": mock_botocore_crt,
202+
"nexent": mock_nexent_module,
203+
"nexent.core": mock_nexent_core_module,
204+
"nexent.core.utils": mock_nexent_core_utils_module,
205+
"nexent.core.utils.observer": mock_nexent_core_utils_observer_module,
206+
"nexent.core.utils.prompt_template_utils": mock_prompt_template_utils_module,
207+
"nexent.core.utils.tools_common_message": mock_tools_common_message_module,
208+
"nexent.storage": mock_nexent_storage_module,
209+
"nexent.multi_modal": mock_nexent_multi_modal_module,
210+
"nexent.multi_modal.load_save_object": mock_nexent_load_save_module,
98211
# Mock the OpenAIModel import
99212
"sdk.nexent.core.models.openai_llm": MagicMock(OpenAIModel=mock_openai_model_class),
100213
# Mock CoreAgent import
@@ -508,6 +621,7 @@ def test_create_local_tool_analyze_text_file_tool(nexent_agent_instance):
508621
metadata={
509622
"llm_model": "llm_model_obj",
510623
"storage_client": "storage_client_obj",
624+
"data_process_service_url": "https://example.com",
511625
},
512626
)
513627

@@ -693,6 +807,48 @@ def test_create_local_tool_knowledge_base_search_tool_with_none_defaults(nexent_
693807
assert mock_kb_tool_instance.embedding_model is None
694808
assert result == mock_kb_tool_instance
695809

810+
def test_create_local_tool_analyze_text_file_tool(nexent_agent_instance):
811+
"""Test AnalyzeTextFileTool creation injects observer and metadata."""
812+
mock_analyze_tool_class = MagicMock()
813+
mock_analyze_tool_instance = MagicMock()
814+
mock_analyze_tool_class.return_value = mock_analyze_tool_instance
815+
816+
tool_config = ToolConfig(
817+
class_name="AnalyzeTextFileTool",
818+
name="analyze_text_file",
819+
description="desc",
820+
inputs="{}",
821+
output_type="string",
822+
params={"prompt": "describe this"},
823+
source="local",
824+
metadata={
825+
"llm_model": "llm_model_obj",
826+
"storage_client": "storage_client_obj",
827+
"data_process_service_url": "DATA_PROCESS_SERVICE",
828+
829+
},
830+
)
831+
832+
original_value = nexent_agent.__dict__.get("AnalyzeTextFileTool")
833+
nexent_agent.__dict__["AnalyzeTextFileTool"] = mock_analyze_tool_class
834+
835+
try:
836+
result = nexent_agent_instance.create_local_tool(tool_config)
837+
finally:
838+
if original_value is not None:
839+
nexent_agent.__dict__["AnalyzeTextFileTool"] = original_value
840+
elif "AnalyzeTextFileTool" in nexent_agent.__dict__:
841+
del nexent_agent.__dict__["AnalyzeTextFileTool"]
842+
843+
mock_analyze_tool_class.assert_called_once_with(
844+
observer=nexent_agent_instance.observer,
845+
llm_model="llm_model_obj",
846+
storage_client="storage_client_obj",
847+
data_process_service_url="DATA_PROCESS_SERVICE",
848+
prompt="describe this",
849+
)
850+
assert result == mock_analyze_tool_instance
851+
696852

697853
def test_create_local_tool_with_observer_attribute(nexent_agent_instance):
698854
"""Test create_local_tool sets observer attribute on tool if it exists."""

0 commit comments

Comments
 (0)