Skip to content

Commit 04277b6

Browse files
committed
✨Supports the import and use of LangChain tools.
1 parent 5e18057 commit 04277b6

File tree

8 files changed

+1071
-11
lines changed

8 files changed

+1071
-11
lines changed

backend/mcp_service/langchain/compute_tool.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@
22

33
@tool
44
def multiply(a: int, b: int) -> int:
5-
"""Calculate the product of two integers."""
5+
"""
6+
Calculate the product of two integers.
7+
8+
Args:
9+
a (int): The first integer.
10+
b (int): The second integer.
11+
12+
Returns:
13+
int: The product of a and b.
14+
"""
615
return a * b
716

817
@tool
918
def add(a: int, b: int) -> int:
10-
"""Calculate the sum of two numbers"""
19+
"""
20+
Calculate the sum of two numbers.
21+
22+
Args:
23+
a (int): The first addend.
24+
b (int): The second addend.
25+
26+
Returns:
27+
int: The sum of a and b.
28+
"""
1129
return a+b

backend/services/tool_configuration_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def _build_tool_info_from_langchain(obj) -> ToolInfo:
141141
param_info = {
142142
"name": param_name,
143143
"type": python_type_to_json_schema(param.annotation),
144-
"description": "", # LangChain doesn’t store per-arg descriptions by default
144+
"description": "see tool description", # LangChain doesn’t store per-arg descriptions by default
145145
"optional": param.default is not inspect._empty,
146146
}
147147

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import unittest
2+
import logging
3+
import sys
4+
from unittest.mock import AsyncMock, MagicMock, patch, Mock
5+
import pytest
6+
7+
# 创建一个模块模拟函数
8+
def mock_module(name):
9+
mock = MagicMock()
10+
sys.modules[name] = mock
11+
return mock
12+
13+
# 模拟主要依赖
14+
mock_module('nexent.core.utils.observer')
15+
mock_module('nexent.core.agents.agent_model')
16+
mock_module('smolagents.agents')
17+
mock_module('smolagents.utils')
18+
mock_module('smolagents.tools')
19+
mock_module('services.remote_mcp_service')
20+
mock_module('database.remote_mcp_db')
21+
mock_module('database.client')
22+
mock_module('utils.config_utils')
23+
mock_module('utils.prompt_template_utils')
24+
mock_module('database.agent_db')
25+
26+
# 预先模拟consts.model以避免Pydantic错误
27+
mock_model = mock_module('consts.model')
28+
mock_tool_config = MagicMock()
29+
sys.modules['nexent.core.agents.agent_model'].ToolConfig = mock_tool_config
30+
31+
# 模拟logger
32+
logger_mock = MagicMock()
33+
34+
35+
class MockDiscoverLangchainTools:
36+
@staticmethod
37+
async def discover_langchain_tools():
38+
"""模拟discover_langchain_tools函数以便于测试"""
39+
return []
40+
41+
42+
class TestDiscoverLangchainTools:
43+
"""测试discover_langchain_tools函数"""
44+
45+
def setup_method(self):
46+
"""每个测试方法前的设置"""
47+
# 为create_agent_info创建一个模拟模块
48+
self.agent_info_mock = MagicMock()
49+
# 添加logger
50+
self.agent_info_mock.logger = logger_mock
51+
# 将模拟模块设置为系统模块
52+
sys.modules['backend.agents.create_agent_info'] = self.agent_info_mock
53+
54+
# 为discover_langchain_modules创建模拟
55+
self.discover_modules_mock = MagicMock()
56+
self.agent_info_mock.discover_langchain_modules = self.discover_modules_mock
57+
58+
@pytest.mark.asyncio
59+
async def test_discover_langchain_tools_success(self):
60+
"""测试成功发现LangChain工具的情况"""
61+
# 创建模拟的工具对象
62+
mock_tool1 = Mock()
63+
mock_tool1.name = "test_tool1"
64+
65+
mock_tool2 = Mock()
66+
mock_tool2.name = "test_tool2"
67+
68+
# 设置discover_langchain_modules的返回值
69+
self.discover_modules_mock.return_value = [
70+
(mock_tool1, "tool1.py"),
71+
(mock_tool2, "tool2.py")
72+
]
73+
74+
# 复原discover_langchain_tools函数的实现
75+
self.agent_info_mock.discover_langchain_tools = Mock(wraps=async_wrapper(self.agent_info_mock))
76+
77+
# 执行函数
78+
result = await self.agent_info_mock.discover_langchain_tools()
79+
80+
# 验证结果
81+
assert len(result) == 2
82+
assert result[0] == mock_tool1
83+
assert result[1] == mock_tool2
84+
assert self.discover_modules_mock.called
85+
86+
@pytest.mark.asyncio
87+
async def test_discover_langchain_tools_empty(self):
88+
"""测试未发现任何工具的情况"""
89+
# 设置discover_langchain_modules返回空列表
90+
self.discover_modules_mock.return_value = []
91+
92+
# 复原discover_langchain_tools函数的实现
93+
self.agent_info_mock.discover_langchain_tools = Mock(wraps=async_wrapper(self.agent_info_mock))
94+
95+
# 执行函数
96+
result = await self.agent_info_mock.discover_langchain_tools()
97+
98+
# 验证结果
99+
assert len(result) == 0
100+
assert result == []
101+
assert self.discover_modules_mock.called
102+
103+
@pytest.mark.asyncio
104+
async def test_discover_langchain_tools_module_exception(self):
105+
"""测试discover_langchain_modules抛出异常的情况"""
106+
# 设置discover_langchain_modules抛出异常
107+
self.discover_modules_mock.side_effect = Exception("模块发现错误")
108+
109+
# 复原discover_langchain_tools函数的实现
110+
self.agent_info_mock.discover_langchain_tools = Mock(wraps=async_wrapper(self.agent_info_mock))
111+
112+
# 执行函数 - 应该捕获异常并返回空列表
113+
result = await self.agent_info_mock.discover_langchain_tools()
114+
115+
# 验证结果
116+
assert len(result) == 0
117+
assert result == []
118+
assert self.discover_modules_mock.called
119+
120+
@pytest.mark.asyncio
121+
async def test_discover_langchain_tools_processing_exception(self):
122+
"""测试处理单个工具出错的情况"""
123+
# 创建一个正常的工具
124+
mock_good_tool = Mock()
125+
mock_good_tool.name = "good_tool"
126+
127+
# 创建一个会引发异常的工具,使用真实类而不是Mock
128+
class ErrorTool:
129+
@property
130+
def name(self):
131+
raise Exception("工具处理错误")
132+
133+
error_tool = ErrorTool()
134+
135+
# 设置discover_langchain_modules的返回值
136+
self.discover_modules_mock.return_value = [
137+
(mock_good_tool, "good_tool.py"),
138+
(error_tool, "error_tool.py")
139+
]
140+
141+
# 复原discover_langchain_tools函数的实现
142+
self.agent_info_mock.discover_langchain_tools = Mock(wraps=async_wrapper(self.agent_info_mock))
143+
144+
# 执行函数 - 应该正确处理一个工具并忽略错误的工具
145+
result = await self.agent_info_mock.discover_langchain_tools()
146+
147+
# 验证结果
148+
assert len(result) == 1
149+
assert result[0] == mock_good_tool
150+
151+
152+
def async_wrapper(mock):
153+
"""创建一个简单的异步wrapper来模拟discover_langchain_tools函数的行为"""
154+
async def discover_langchain_tools():
155+
langchain_tools = []
156+
try:
157+
discovered_tools = mock.discover_langchain_modules()
158+
for obj, filename in discovered_tools:
159+
try:
160+
# 先尝试访问name属性 - 如果这里抛出异常,则对象不会被添加
161+
tool_name = obj.name
162+
logger_mock.info(f"Loaded LangChain tool '{tool_name}' from {filename}")
163+
langchain_tools.append(obj)
164+
except Exception as e:
165+
logger_mock.error(f"Error processing LangChain tool from {filename}: {e}")
166+
except Exception as e:
167+
logger_mock.error(f"Unexpected error scanning LangChain tools directory: {e}")
168+
return langchain_tools
169+
return discover_langchain_tools
170+
171+
172+
if __name__ == "__main__":
173+
unittest.main()

test/backend/services/test_nexent_mcp_service.py

Whitespace-only changes.

0 commit comments

Comments
 (0)