Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca9ed62
feat: add Chinese localization for tool descriptions
Feb 14, 2026
0cb51f8
feat: add init_param_descriptions with i18n for exa_search_tool
Feb 14, 2026
c842f96
fix: add services module mock to fix test_add_tool_field test
Feb 14, 2026
762019b
fix: fix TypeScript type error in ToolTestPanel.tsx
Feb 14, 2026
8a7cf06
test: add tests for get_local_tools_description_zh function
Feb 14, 2026
774a32b
test: add tests for get_local_tools_description_zh i18n function
Feb 14, 2026
f9c1dbe
test: add tests for add_tool_field description_zh i18n merge logic
Feb 14, 2026
8601b99
test: fix test imports for get_local_tools_description_zh
Feb 14, 2026
958335c
Pre-download tiktoken cl100k_base model
WMC001 Feb 28, 2026
6c3e4d2
🐛 Bugfix: The agent_run process cannot invoke the MCP service with au…
WMC001 Feb 28, 2026
fa516a7
Merge remote-tracking branch 'origin/release/v1.8.0.1' into gerui-bugfix
Mar 10, 2026
9992be2
Merge remote-tracking branch 'origin/develop' into gerui-bugfix
Mar 10, 2026
ec3aa12
fix: 解决循环导入问题并清理工具参数描述
Mar 10, 2026
8447f08
fix: remove duplicate period in get_email_tool description_zh
Mar 10, 2026
a240575
fix: restore MCP transport functions and authorization support
Mar 11, 2026
b37c5a3
test: fix mock paths for get_local_tools_classes and get_local_tools_…
Mar 11, 2026
f8e8d4e
fix: restore MCP tool unique key logic and fix test mock paths
Mar 11, 2026
54eba07
fix: add missing ToolSourceEnum import and restore create_or_update_t…
Mar 11, 2026
41efd51
test: add tests for description_zh coverage
Mar 11, 2026
2d234aa
test: fix mock paths and async tests for description_zh coverage
Mar 11, 2026
fe803fb
fix: add init_param_descriptions fallback for param description_zh
Mar 11, 2026
3eddd99
update docs and bugfix
Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ class ToolSourceEnum(Enum):
class ToolInfo(BaseModel):
name: str
description: str
description_zh: Optional[str] = None
params: List
source: str
inputs: str
Expand Down
52 changes: 40 additions & 12 deletions backend/database/tool_db.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import re
import json
from typing import List

from database.agent_db import logger
from database.client import get_db_session, filter_property, as_dict
from database.db_models import ToolInstance, ToolInfo
from consts.model import ToolSourceEnum
from utils.tool_utils import get_local_tools_description_zh


def create_tool(tool_info, version_no: int = 0):
Expand Down Expand Up @@ -225,15 +226,15 @@
is_available = True if re.match(
r'^[a-zA-Z_][a-zA-Z0-9_]*$', tool.name) is not None else False

# Use same key generation logic as above
# Build key for lookup - same logic as existing_tool_dict
if tool.source == ToolSourceEnum.MCP.value:
tool_key = f"{tool.name}&{tool.source}&{tool.usage or ''}"
key = f"{tool.name}&{tool.source}&{tool.usage or ''}"
else:
tool_key = f"{tool.name}&{tool.source}"
key = f"{tool.name}&{tool.source}"

if tool_key in existing_tool_dict:
# by tool name, source, and usage (for MCP) to update the existing tool
existing_tool = existing_tool_dict[tool_key]
if key in existing_tool_dict:
# by tool name and source to update the existing tool
existing_tool = existing_tool_dict[key]
for key, value in filtered_tool_data.items():
setattr(existing_tool, key, value)
existing_tool.updated_by = user_id
Expand All @@ -247,22 +248,50 @@
logger.info("Updated tool table in PG database")


def add_tool_field(tool_info):

Check failure on line 251 in backend/database/tool_db.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 47 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ0tJRsBJp73I62kbFF1&open=AZ0tJRsBJp73I62kbFF1&pullRequest=2527
with get_db_session() as session:
# Query if there is an existing ToolInstance
query = session.query(ToolInfo).filter(
ToolInfo.tool_id == tool_info["tool_id"])
tool = query.first()

# add tool params
tool_params = tool.params
for ele in tool_params:
param_name = ele["name"]
ele["default"] = tool_info["params"].get(param_name)

tool_dict = as_dict(tool)
tool_dict["params"] = tool_params


# Merge description_zh from SDK for local tools
tool_name = tool_dict.get("name")
if tool_dict.get("source") == "local":
local_tool_descriptions = get_local_tools_description_zh()
if tool_name in local_tool_descriptions:
sdk_info = local_tool_descriptions[tool_name]
tool_dict["description_zh"] = sdk_info.get("description_zh")

# Merge params description_zh from SDK
for param in tool_params:
if not param.get("description_zh"):
for sdk_param in sdk_info.get("params", []):
if sdk_param.get("name") == param.get("name"):
param["description_zh"] = sdk_param.get("description_zh")
break

# Merge inputs description_zh from SDK
inputs_str = tool_dict.get("inputs", "{}")
try:
inputs = json.loads(inputs_str) if isinstance(inputs_str, str) else inputs_str
if isinstance(inputs, dict):
for key, value in inputs.items():
if isinstance(value, dict) and not value.get("description_zh"):
sdk_inputs = sdk_info.get("inputs", {})
if key in sdk_inputs:
value["description_zh"] = sdk_inputs[key].get("description_zh")
tool_dict["inputs"] = json.dumps(inputs, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
pass

# combine tool_info and tool_dict
tool_info.update(tool_dict)
return tool_info
Expand Down Expand Up @@ -331,7 +360,6 @@
ToolInstance.delete_flag: 'Y', 'updated_by': user_id
})


def search_last_tool_instance_by_tool_id(tool_id: int, tenant_id: str, user_id: str, version_no: int = 0):
"""
Query the latest ToolInstance by tool_id.
Expand All @@ -355,4 +383,4 @@
ToolInstance.delete_flag != 'Y'
).order_by(ToolInstance.update_time.desc())
tool_instance = query.first()
return as_dict(tool_instance) if tool_instance else None
return as_dict(tool_instance) if tool_instance else None
106 changes: 83 additions & 23 deletions backend/services/tool_configuration_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from services.vectordatabase_service import get_embedding_model, get_vector_db_core
from database.client import minio_client
from services.image_service import get_vlm_model
from utils.tool_utils import get_local_tools_classes, get_local_tools_description_zh

logger = logging.getLogger("tool_configuration_service")

Expand Down Expand Up @@ -94,7 +95,7 @@
return type_mapping.get(type_name, type_name)


def get_local_tools() -> List[ToolInfo]:

Check failure on line 98 in backend/services/tool_configuration_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 38 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ0tJRsUJp73I62kbFF2&open=AZ0tJRsUJp73I62kbFF2&pullRequest=2527
"""
Get metadata for all locally available tools

Expand All @@ -104,16 +105,35 @@
tools_info = []
tools_classes = get_local_tools_classes()
for tool_class in tools_classes:
# Get class-level init_param_descriptions for fallback
init_param_descriptions = getattr(tool_class, 'init_param_descriptions', {})

init_params_list = []
sig = inspect.signature(tool_class.__init__)
for param_name, param in sig.parameters.items():
if param_name == "self" or param.default.exclude:
if param_name == "self":
continue

# Check if parameter has a default value and if it should be excluded
if param.default != inspect.Parameter.empty:
if hasattr(param.default, 'exclude') and param.default.exclude:
continue

# Get description in both languages
param_description = param.default.description if hasattr(param.default, 'description') else ""

# First try to get from param.default.description_zh (FieldInfo)
param_description_zh = param.default.description_zh if hasattr(param.default, 'description_zh') else None

# Fallback to init_param_descriptions if not found
if param_description_zh is None and param_name in init_param_descriptions:
param_description_zh = init_param_descriptions[param_name].get('description_zh')

param_info = {
"type": python_type_to_json_schema(param.annotation),
"name": param_name,
"description": param.default.description
"description": param_description,
"description_zh": param_description_zh
}
if param.default.default is PydanticUndefined:
param_info["optional"] = False
Expand All @@ -123,14 +143,29 @@

init_params_list.append(param_info)

# get tool fixed attributes
# Get tool fixed attributes with bilingual support
tool_description_zh = getattr(tool_class, 'description_zh', None)
tool_inputs = getattr(tool_class, 'inputs', {})

# Process inputs to add bilingual descriptions
processed_inputs = {}
if isinstance(tool_inputs, dict):
for key, value in tool_inputs.items():
if isinstance(value, dict):
processed_inputs[key] = {
**value,
"description_zh": value.get("description_zh")
}
else:
processed_inputs[key] = value

tool_info = ToolInfo(
name=getattr(tool_class, 'name'),
description=getattr(tool_class, 'description'),
description_zh=tool_description_zh,
params=init_params_list,
Comment thread
geruihappy-creator marked this conversation as resolved.
source=ToolSourceEnum.LOCAL.value,
inputs=json.dumps(getattr(tool_class, 'inputs'),
ensure_ascii=False),
inputs=json.dumps(processed_inputs, ensure_ascii=False),
output_type=getattr(tool_class, 'output_type'),
category=getattr(tool_class, 'category'),
class_name=tool_class.__name__,
Expand All @@ -141,22 +176,6 @@
return tools_info


def get_local_tools_classes() -> List[type]:
"""
Get all tool classes from the nexent.core.tools package

Returns:
List of tool class objects
"""
tools_package = importlib.import_module('nexent.core.tools')
tools_classes = []
for name in dir(tools_package):
obj = getattr(tools_package, name)
if inspect.isclass(obj):
tools_classes.append(obj)
return tools_classes


# --------------------------------------------------
# LangChain tools discovery (functions decorated with @tool)
# --------------------------------------------------
Expand Down Expand Up @@ -422,25 +441,66 @@
tool_list=local_tools+mcp_tools+langchain_tools)


async def list_all_tools(tenant_id: str):

Check failure on line 444 in backend/services/tool_configuration_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 55 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ0tJRsUJp73I62kbFF3&open=AZ0tJRsUJp73I62kbFF3&pullRequest=2527
"""
List all tools for a given tenant
"""
tools_info = query_all_tools(tenant_id)

# Get description_zh from SDK for local tools (not persisted to DB)
local_tool_descriptions = get_local_tools_description_zh()

# only return the fields needed
formatted_tools = []
for tool in tools_info:
tool_name = tool.get("name")

# Merge description_zh from SDK for local tools
if tool.get("source") == "local" and tool_name in local_tool_descriptions:
sdk_info = local_tool_descriptions[tool_name]
description_zh = sdk_info.get("description_zh")

# Merge params description_zh from SDK (independent of tool-level description_zh)
params = tool.get("params", [])
if params:
for param in params:
if not param.get("description_zh"):
# Find matching param in SDK
for sdk_param in sdk_info.get("params", []):
if sdk_param.get("name") == param.get("name"):
param["description_zh"] = sdk_param.get("description_zh")
break

# Merge inputs description_zh from SDK
inputs_str = tool.get("inputs", "{}")
try:
inputs = json.loads(inputs_str) if isinstance(inputs_str, str) else inputs_str
if isinstance(inputs, dict):
for key, value in inputs.items():
if isinstance(value, dict) and not value.get("description_zh"):
# Find matching input in SDK
sdk_inputs = sdk_info.get("inputs", {})
if key in sdk_inputs:
value["description_zh"] = sdk_inputs[key].get("description_zh")
inputs_str = json.dumps(inputs, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
pass
else:
description_zh = tool.get("description_zh")
inputs_str = tool.get("inputs", "{}")

formatted_tool = {
"tool_id": tool.get("tool_id"),
"name": tool.get("name"),
"name": tool_name,
"origin_name": tool.get("origin_name"),
"description": tool.get("description"),
"description_zh": description_zh,
"source": tool.get("source"),
"is_available": tool.get("is_available"),
"create_time": tool.get("create_time"),
"usage": tool.get("usage"),
"params": tool.get("params", []),
"inputs": tool.get("inputs", {}),
"inputs": inputs_str,
"category": tool.get("category")
}
formatted_tools.append(formatted_tool)
Expand Down
73 changes: 73 additions & 0 deletions backend/utils/tool_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import importlib
import inspect
from typing import List, Dict


def get_local_tools_classes() -> List[type]:
"""
Get all tool classes from the nexent.core.tools package

Returns:
List of tool class objects
"""
tools_package = importlib.import_module('nexent.core.tools')
tools_classes = []
for name in dir(tools_package):
obj = getattr(tools_package, name)
if inspect.isclass(obj):
tools_classes.append(obj)
return tools_classes


def get_local_tools_description_zh() -> Dict[str, Dict]:

Check failure on line 22 in backend/utils/tool_utils.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 31 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ0tJRsdJp73I62kbFF4&open=AZ0tJRsdJp73I62kbFF4&pullRequest=2527
Comment thread
WMC001 marked this conversation as resolved.
"""
Get description_zh for all local tools from SDK (not persisted to DB).

Returns:
Dict mapping tool name to {"description_zh": ..., "params": [...], "inputs": {...}}
"""
tools_classes = get_local_tools_classes()
result = {}
for tool_class in tools_classes:
tool_name = getattr(tool_class, 'name')

description_zh = getattr(tool_class, 'description_zh', None)

init_param_descriptions = getattr(tool_class, 'init_param_descriptions', {})

init_params_list = []
sig = inspect.signature(tool_class.__init__)
for param_name, param in sig.parameters.items():
if param_name == "self":
continue

# Check if parameter has a default value and if it should be excluded
if param.default != inspect.Parameter.empty:
if hasattr(param.default, 'exclude') and param.default.exclude:
continue

param_description_zh = param.default.description_zh if hasattr(param.default, 'description_zh') else None

if param_description_zh is None and param_name in init_param_descriptions:
param_description_zh = init_param_descriptions[param_name].get('description_zh')

init_params_list.append({
"name": param_name,
"description_zh": param_description_zh
})

tool_inputs = getattr(tool_class, 'inputs', {})
inputs_description_zh = {}
if isinstance(tool_inputs, dict):
for key, value in tool_inputs.items():
if isinstance(value, dict) and value.get("description_zh"):
inputs_description_zh[key] = {
"description_zh": value.get("description_zh")
}

result[tool_name] = {
"description_zh": description_zh,
"params": init_params_list,
"inputs": inputs_description_zh
}
return result
Loading
Loading