Skip to content

Commit bb8bbdb

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

File tree

7 files changed

+749
-30
lines changed

7 files changed

+749
-30
lines changed

sdk/nexent/core/utils/prompt_template_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
import yaml
66

7-
from consts.const import LANGUAGE
7+
LANGUAGE = {
8+
"ZH": "zh",
9+
"EN": "en"
10+
}
811

912
logger = logging.getLogger("prompt_template_utils")
1013

test/backend/agents/test_create_agent_info.py

Lines changed: 148 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
11
import pytest
22
import sys
3+
import types
4+
import importlib.util
5+
from pathlib import Path
36
from unittest.mock import AsyncMock, MagicMock, patch, Mock, PropertyMock
47

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
8+
TEST_ROOT = Path(__file__).resolve().parents[2]
9+
PROJECT_ROOT = TEST_ROOT.parent
10+
11+
# Ensure project backend package is found before test/backend
12+
for _path in (str(PROJECT_ROOT), str(TEST_ROOT)):
13+
if _path not in sys.path:
14+
sys.path.insert(0, _path)
15+
16+
from test.common.env_test_utils import bootstrap_env
17+
18+
env_state = bootstrap_env()
19+
consts_const = env_state["mock_const"]
20+
21+
# Utilities ---------------------------------------------------------------
22+
def _create_stub_module(name: str, **attrs):
23+
"""Return a lightweight module stub with the provided attributes."""
24+
module = types.ModuleType(name)
25+
for attr_name, attr_value in attrs.items():
26+
setattr(module, attr_name, attr_value)
27+
return module
28+
29+
30+
# Configure required constants via shared bootstrap env
31+
consts_const.MINIO_ENDPOINT = "http://localhost:9000"
32+
consts_const.MINIO_ACCESS_KEY = "test_access_key"
33+
consts_const.MINIO_SECRET_KEY = "test_secret_key"
34+
consts_const.MINIO_REGION = "us-east-1"
35+
consts_const.MINIO_DEFAULT_BUCKET = "test-bucket"
36+
consts_const.POSTGRES_HOST = "localhost"
37+
consts_const.POSTGRES_USER = "test_user"
38+
consts_const.NEXENT_POSTGRES_PASSWORD = "test_password"
39+
consts_const.POSTGRES_DB = "test_db"
40+
consts_const.POSTGRES_PORT = 5432
41+
consts_const.DEFAULT_TENANT_ID = "default_tenant"
42+
consts_const.LOCAL_MCP_SERVER = "http://localhost:5011"
43+
consts_const.MODEL_CONFIG_MAPPING = {"llm": "llm_config"}
44+
consts_const.LANGUAGE = {"ZH": "zh"}
45+
consts_const.DATA_PROCESS_SERVICE = "https://example.com/data-process"
2746

2847
# Mock utils module
2948
utils_mock = MagicMock()
@@ -38,6 +57,7 @@
3857
# if the testing environment does not have it available.
3958
boto3_mock = MagicMock()
4059
sys.modules['boto3'] = boto3_mock
60+
sys.modules['dotenv'] = MagicMock(load_dotenv=MagicMock())
4161

4262
# Mock the entire client module
4363
client_mock = MagicMock()
@@ -49,13 +69,24 @@
4969

5070
# Add the mocked client module to sys.modules
5171
sys.modules['backend.database.client'] = client_mock
72+
sys.modules['database.client'] = _create_stub_module(
73+
"database.client",
74+
minio_client=MagicMock(),
75+
postgres_client=MagicMock(),
76+
db_client=MagicMock(),
77+
get_db_session=MagicMock(),
78+
as_dict=MagicMock(),
79+
)
5280

5381
# Mock external dependencies before imports
54-
sys.modules['nexent.core.utils.observer'] = MagicMock()
82+
mock_message_observer = MagicMock()
83+
sys.modules['nexent.core.utils.observer'] = MagicMock(MessageObserver=mock_message_observer)
5584
sys.modules['nexent.core.agents.agent_model'] = MagicMock()
5685
sys.modules['smolagents.agents'] = MagicMock()
5786
sys.modules['smolagents.utils'] = MagicMock()
5887
sys.modules['services.remote_mcp_service'] = MagicMock()
88+
database_module = _create_stub_module("database")
89+
sys.modules['database'] = database_module
5990
sys.modules['database.agent_db'] = MagicMock()
6091
sys.modules['database.tool_db'] = MagicMock()
6192
sys.modules['database.model_management_db'] = MagicMock()
@@ -67,14 +98,38 @@
6798
sys.modules['utils.model_name_utils'] = MagicMock()
6899
sys.modules['langchain_core.tools'] = MagicMock()
69100
sys.modules['services.memory_config_service'] = MagicMock()
101+
# Build services module hierarchy with minimal functionality
102+
services_module = _create_stub_module("services")
103+
sys.modules['services'] = services_module
104+
sys.modules['services.image_service'] = _create_stub_module(
105+
"services.image_service", get_vlm_model=MagicMock(return_value="stub_vlm")
106+
)
107+
sys.modules['services.file_management_service'] = _create_stub_module(
108+
"services.file_management_service",
109+
get_llm_model=MagicMock(return_value="stub_llm_model"),
110+
)
111+
sys.modules['services.tool_configuration_service'] = _create_stub_module(
112+
"services.tool_configuration_service",
113+
initialize_tools_on_startup=AsyncMock(),
114+
)
115+
# Build top-level nexent module to avoid importing the real package
116+
nexent_module = _create_stub_module(
117+
"nexent",
118+
MessageObserver=mock_message_observer,
119+
)
120+
sys.modules['nexent'] = nexent_module
121+
122+
# Create nested modules for nexent.core to satisfy imports safely
123+
sys.modules['nexent.core'] = _create_stub_module("nexent.core")
124+
sys.modules['nexent.core.agents'] = _create_stub_module("nexent.core.agents")
125+
sys.modules['nexent.core.utils'] = _create_stub_module("nexent.core.utils")
70126
sys.modules['nexent.memory.memory_service'] = MagicMock()
71127

72128
# Create mock classes that might be imported
73129
mock_agent_config = MagicMock()
74130
mock_model_config = MagicMock()
75131
mock_tool_config = MagicMock()
76132
mock_agent_run_info = MagicMock()
77-
mock_message_observer = MagicMock()
78133

79134
sys.modules['nexent.core.agents.agent_model'].AgentConfig = mock_agent_config
80135
sys.modules['nexent.core.agents.agent_model'].ModelConfig = mock_model_config
@@ -85,7 +140,38 @@
85140
# Mock BASE_BUILTIN_MODULES
86141
sys.modules['smolagents.utils'].BASE_BUILTIN_MODULES = ["os", "sys", "json"]
87142

88-
# Now import the module under test
143+
# Provide lightweight smolagents package to prevent circular imports
144+
smolagents_module = _create_stub_module("smolagents")
145+
smolagents_tools_module = _create_stub_module("smolagents.tools", Tool=MagicMock())
146+
smolagents_module.tools = smolagents_tools_module
147+
sys.modules['smolagents'] = smolagents_module
148+
sys.modules['smolagents.tools'] = smolagents_tools_module
149+
150+
# Ensure real backend.agents.create_agent_info is available and uses our stubs
151+
backend_pkg = sys.modules.get("backend")
152+
if backend_pkg is None:
153+
backend_pkg = types.ModuleType("backend")
154+
backend_pkg.__path__ = [str((TEST_ROOT.parent) / "backend")]
155+
sys.modules["backend"] = backend_pkg
156+
157+
agents_pkg = sys.modules.get("backend.agents")
158+
if agents_pkg is None:
159+
agents_pkg = types.ModuleType("backend.agents")
160+
agents_pkg.__path__ = [str((TEST_ROOT.parent) / "backend" / "agents")]
161+
sys.modules["backend.agents"] = agents_pkg
162+
setattr(backend_pkg, "agents", agents_pkg)
163+
164+
create_agent_info_path = (TEST_ROOT.parent / "backend" / "agents" / "create_agent_info.py")
165+
spec = importlib.util.spec_from_file_location(
166+
"backend.agents.create_agent_info", create_agent_info_path
167+
)
168+
create_agent_info_module = importlib.util.module_from_spec(spec)
169+
sys.modules["backend.agents.create_agent_info"] = create_agent_info_module
170+
assert spec.loader is not None
171+
spec.loader.exec_module(create_agent_info_module)
172+
setattr(agents_pkg, "create_agent_info", create_agent_info_module)
173+
174+
# Now import the symbols under test
89175
from backend.agents.create_agent_info import (
90176
discover_langchain_tools,
91177
create_tool_config_list,
@@ -282,6 +368,43 @@ async def test_create_tool_config_list_with_knowledge_base_tool(self):
282368
last_call = mock_tool_config.call_args_list[-1]
283369
assert last_call[1]['class_name'] == "KnowledgeBaseSearchTool"
284370

371+
@pytest.mark.asyncio
372+
async def test_create_tool_config_list_with_analyze_text_file_tool(self):
373+
"""Ensure AnalyzeTextFileTool receives text-specific metadata."""
374+
mock_tool_instance = MagicMock()
375+
mock_tool_instance.class_name = "AnalyzeTextFileTool"
376+
mock_tool_config.return_value = mock_tool_instance
377+
378+
with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \
379+
patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \
380+
patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \
381+
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock) as mock_minio_client:
382+
383+
mock_search_tools.return_value = [
384+
{
385+
"class_name": "AnalyzeTextFileTool",
386+
"name": "analyze_text_file",
387+
"description": "Analyze text file tool",
388+
"inputs": "string",
389+
"output_type": "array",
390+
"params": [{"name": "prompt", "default": "describe"}],
391+
"source": "local",
392+
"usage": None
393+
}
394+
]
395+
mock_get_llm_model.return_value = "mock_llm_model"
396+
397+
result = await create_tool_config_list("agent_1", "tenant_1", "user_1")
398+
399+
assert len(result) == 1
400+
assert result[0] is mock_tool_instance
401+
mock_get_llm_model.assert_called_once_with(tenant_id="tenant_1")
402+
assert mock_tool_instance.metadata == {
403+
"llm_model": "mock_llm_model",
404+
"storage_client": mock_minio_client,
405+
"data_process_service_url": consts_const.DATA_PROCESS_SERVICE,
406+
}
407+
285408

286409
class TestCreateAgentConfig:
287410
"""Tests for the create_agent_config function"""

test/backend/services/test_file_management_service.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
import types
1111
import pytest
12-
from unittest.mock import patch, MagicMock, AsyncMock
12+
from unittest.mock import patch, MagicMock, AsyncMock, Mock
1313
from pathlib import Path
1414
from io import BytesIO
1515

@@ -1583,3 +1583,130 @@ async def test_upload_to_minio_with_empty_folder(self):
15831583
# Verify that empty string was passed as prefix
15841584
call_args = mock_upload.call_args
15851585
assert call_args[1]["prefix"] == ""
1586+
1587+
1588+
class TestGetLlmModel:
1589+
"""Test cases for get_llm_model function"""
1590+
1591+
@patch('backend.services.file_management_service.MODEL_CONFIG_MAPPING', {"llm": "llm_config_key"})
1592+
@patch('backend.services.file_management_service.MessageObserver')
1593+
@patch('backend.services.file_management_service.OpenAILongContextModel')
1594+
@patch('backend.services.file_management_service.get_model_name_from_config')
1595+
@patch('backend.services.file_management_service.tenant_config_manager')
1596+
def test_get_llm_model_success(self, mock_tenant_config, mock_get_model_name, mock_openai_model, mock_message_observer):
1597+
"""Test successful LLM model retrieval"""
1598+
from backend.services.file_management_service import get_llm_model
1599+
1600+
# Mock tenant config manager
1601+
mock_config = {
1602+
"base_url": "http://api.example.com",
1603+
"api_key": "test_api_key",
1604+
"max_tokens": 4096
1605+
}
1606+
mock_tenant_config.get_model_config.return_value = mock_config
1607+
1608+
# Mock model name
1609+
mock_get_model_name.return_value = "gpt-4"
1610+
1611+
# Mock MessageObserver
1612+
mock_observer_instance = Mock()
1613+
mock_message_observer.return_value = mock_observer_instance
1614+
1615+
# Mock OpenAILongContextModel
1616+
mock_model_instance = Mock()
1617+
mock_openai_model.return_value = mock_model_instance
1618+
1619+
# Execute
1620+
result = get_llm_model("tenant123")
1621+
1622+
# Assertions
1623+
assert result == mock_model_instance
1624+
mock_tenant_config.get_model_config.assert_called_once_with(
1625+
key="llm_config_key", tenant_id="tenant123")
1626+
mock_get_model_name.assert_called_once_with(mock_config)
1627+
mock_message_observer.assert_called_once()
1628+
mock_openai_model.assert_called_once_with(
1629+
observer=mock_observer_instance,
1630+
model_id="gpt-4",
1631+
api_base="http://api.example.com",
1632+
api_key="test_api_key",
1633+
max_context_tokens=4096
1634+
)
1635+
1636+
@patch('backend.services.file_management_service.MODEL_CONFIG_MAPPING', {"llm": "llm_config_key"})
1637+
@patch('backend.services.file_management_service.MessageObserver')
1638+
@patch('backend.services.file_management_service.OpenAILongContextModel')
1639+
@patch('backend.services.file_management_service.get_model_name_from_config')
1640+
@patch('backend.services.file_management_service.tenant_config_manager')
1641+
def test_get_llm_model_with_missing_config_values(self, mock_tenant_config, mock_get_model_name, mock_openai_model, mock_message_observer):
1642+
"""Test get_llm_model with missing config values"""
1643+
from backend.services.file_management_service import get_llm_model
1644+
1645+
# Mock tenant config manager with missing values
1646+
mock_config = {
1647+
"base_url": "http://api.example.com"
1648+
# Missing api_key and max_tokens
1649+
}
1650+
mock_tenant_config.get_model_config.return_value = mock_config
1651+
1652+
# Mock model name
1653+
mock_get_model_name.return_value = "gpt-4"
1654+
1655+
# Mock MessageObserver
1656+
mock_observer_instance = Mock()
1657+
mock_message_observer.return_value = mock_observer_instance
1658+
1659+
# Mock OpenAILongContextModel
1660+
mock_model_instance = Mock()
1661+
mock_openai_model.return_value = mock_model_instance
1662+
1663+
# Execute
1664+
result = get_llm_model("tenant123")
1665+
1666+
# Assertions
1667+
assert result == mock_model_instance
1668+
# Verify that get() is used for missing values (returns None)
1669+
mock_openai_model.assert_called_once()
1670+
call_kwargs = mock_openai_model.call_args[1]
1671+
assert call_kwargs["api_key"] is None
1672+
assert call_kwargs["max_context_tokens"] is None
1673+
1674+
@patch('backend.services.file_management_service.MODEL_CONFIG_MAPPING', {"llm": "llm_config_key"})
1675+
@patch('backend.services.file_management_service.MessageObserver')
1676+
@patch('backend.services.file_management_service.OpenAILongContextModel')
1677+
@patch('backend.services.file_management_service.get_model_name_from_config')
1678+
@patch('backend.services.file_management_service.tenant_config_manager')
1679+
def test_get_llm_model_with_different_tenant_ids(self, mock_tenant_config, mock_get_model_name, mock_openai_model, mock_message_observer):
1680+
"""Test get_llm_model with different tenant IDs"""
1681+
from backend.services.file_management_service import get_llm_model
1682+
1683+
# Mock tenant config manager
1684+
mock_config = {
1685+
"base_url": "http://api.example.com",
1686+
"api_key": "test_api_key",
1687+
"max_tokens": 4096
1688+
}
1689+
mock_tenant_config.get_model_config.return_value = mock_config
1690+
1691+
# Mock model name
1692+
mock_get_model_name.return_value = "gpt-4"
1693+
1694+
# Mock MessageObserver
1695+
mock_observer_instance = Mock()
1696+
mock_message_observer.return_value = mock_observer_instance
1697+
1698+
# Mock OpenAILongContextModel
1699+
mock_model_instance = Mock()
1700+
mock_openai_model.return_value = mock_model_instance
1701+
1702+
# Execute with different tenant IDs
1703+
result1 = get_llm_model("tenant1")
1704+
result2 = get_llm_model("tenant2")
1705+
1706+
# Assertions
1707+
assert result1 == mock_model_instance
1708+
assert result2 == mock_model_instance
1709+
# Verify tenant config was called with different tenant IDs
1710+
assert mock_tenant_config.get_model_config.call_count == 2
1711+
assert mock_tenant_config.get_model_config.call_args_list[0][1]["tenant_id"] == "tenant1"
1712+
assert mock_tenant_config.get_model_config.call_args_list[1][1]["tenant_id"] == "tenant2"

0 commit comments

Comments
 (0)