Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
47 changes: 41 additions & 6 deletions backend/apps/tool_config_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logging
from http import HTTPStatus
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, List

from fastapi import APIRouter, Header, HTTPException, Body
from fastapi import APIRouter, Header, HTTPException, Body, Query
from fastapi.responses import JSONResponse

from consts.exceptions import MCPConnectionError, NotFoundException
Expand All @@ -26,14 +26,17 @@


@router.get("/list")
async def list_tools_api(authorization: Optional[str] = Header(None)):
async def list_tools_api(
authorization: Optional[str] = Header(None),

Check warning on line 30 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKwIHStKnsFKB3A&open=AZ8SStKwIHStKnsFKB3A&pullRequest=3326
labels: Optional[str] = Query(None, description="Comma-separated label strings to filter tools (OR match)")

Check warning on line 31 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKwIHStKnsFKB3B&open=AZ8SStKwIHStKnsFKB3B&pullRequest=3326
):
"""
List all system tools from PG dataset
List all system tools from PG dataset, optionally filtered by labels.
"""
try:
_, tenant_id = get_current_user_id(authorization)
# now only admin can modify the tool, user_id is not used
return await list_all_tools(tenant_id=tenant_id)
label_list = [lbl.strip() for lbl in labels.split(",") if lbl.strip()] if labels else None
return await list_all_tools(tenant_id=tenant_id, labels=label_list)
except Exception as e:
logging.error(f"Failed to get tool info, error in: {str(e)}")
raise HTTPException(
Expand Down Expand Up @@ -278,3 +281,35 @@
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to delete OpenAPI service: {str(e)}"
)


@router.put("/labels")
async def update_tool_labels_api(
tool_id: int = Body(..., embed=True),

Check warning on line 288 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKwIHStKnsFKB3C&open=AZ8SStKwIHStKnsFKB3C&pullRequest=3326
labels: List[str] = Body(..., embed=True),

Check warning on line 289 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKwIHStKnsFKB3D&open=AZ8SStKwIHStKnsFKB3D&pullRequest=3326
authorization: Optional[str] = Header(None)

Check warning on line 290 in backend/apps/tool_config_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKwIHStKnsFKB3E&open=AZ8SStKwIHStKnsFKB3E&pullRequest=3326
):
"""
Update labels for a specific tool. Replaces all labels with the provided list.
"""
try:
user_id, tenant_id = get_current_user_id(authorization)
from database.tool_db import update_tool_labels
updated = update_tool_labels(tool_id, tenant_id, labels, user_id)
if not updated:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Tool not found or access denied"
)
return JSONResponse(
status_code=HTTPStatus.OK,
content={"message": "Labels updated successfully", "status": "success", "labels": labels}
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"Failed to update tool labels: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Failed to update tool labels: {str(e)}"
)
1 change: 1 addition & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ class ToolInfo(BaseModel):
usage: Optional[str]
origin_name: Optional[str] = None
category: Optional[str] = None
labels: Optional[List[str]] = None


# used in Knowledge Summary request
Expand Down
54 changes: 54 additions & 0 deletions backend/consts/tool_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Built-in labels for well-known local tools.

These are applied when a tool is first created for a tenant via
update_tool_table_from_scan_tool_list() — which runs during the first
API call for a new tenant (init_tool_list_for_tenant).

Why not in SQL?
- init.sql runs before the backend starts, so the ag_tool_info_t table
is empty and UPDATE statements would hit zero rows.
- Migration SQL (docker/sql/) covers the upgrade path from v2.2.x,
but cannot cover fresh v2.3.0+ installs where tools don't exist yet.
- This module is the only hook that fires at the exact moment tools are
inserted — the earliest lifecycle point where the data exists.

Keep in sync with: deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql
"""

# tool_name → [label, ...]
# Built per-category to avoid cross-file duplication with the matching SQL seed data.
_category_database = {
"mysql_database": ["database"], "postgres_database": ["database"], "mssql_database": ["database"],
}
_category_file = {
"read_file": ["file"], "create_file": ["file"], "delete_file": ["file"],
"create_directory": ["file"], "delete_directory": ["file"],
"list_directory": ["file"], "move_item": ["file"],
}
_category_search = {
"tavily_search": ["search"], "exa_search": ["search"], "linkup_search": ["search"],
"search_memory": ["search"], "knowledge_base_search": ["search"],
}
_category_kb = {
"dify_search": ["knowledge-base"], "datamate_search": ["knowledge-base"],
"idata_search": ["knowledge-base"], "haotian_search": ["knowledge-base"],
"aidp_search": ["knowledge-base"],
}
_category_multimodal = {
"analyze_image": ["multimodal"], "analyze_audio": ["multimodal"],
"analyze_video": ["multimodal"], "analyze_text_file": ["multimodal"],
}
_category_email = {"get_email": ["email"], "send_email": ["email"]}
_category_memory = {"store_memory": ["memory"]}
_category_terminal = {"terminal": ["terminal"]}

BUILTIN_LABEL_MAP: dict[str, list[str]] = {}
BUILTIN_LABEL_MAP.update(_category_database)
BUILTIN_LABEL_MAP.update(_category_file)
BUILTIN_LABEL_MAP.update(_category_search)
BUILTIN_LABEL_MAP.update(_category_kb)
BUILTIN_LABEL_MAP.update(_category_multimodal)
BUILTIN_LABEL_MAP.update(_category_email)
BUILTIN_LABEL_MAP.update(_category_memory)
BUILTIN_LABEL_MAP.update(_category_terminal)
1 change: 1 addition & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ class ToolInfo(TableBase):
inputs = Column(String(2048), doc="Prompt tool inputs description")
output_type = Column(String(100), doc="Prompt tool output description")
category = Column(String(100), doc="Tool category description")
labels = Column(JSONB, default=[], doc="JSON array of label strings for filtering/grouping tools")
is_available = Column(
Boolean, doc="Whether the tool can be used under the current main service")

Expand Down
59 changes: 57 additions & 2 deletions backend/database/tool_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from database.client import get_db_session, filter_property, as_dict
from database.db_models import ToolInstance, ToolInfo
from consts.model import ToolSourceEnum
from consts.tool_labels import BUILTIN_LABEL_MAP
from utils.tool_utils import get_local_tools_description_zh


Expand Down Expand Up @@ -136,6 +137,54 @@ def query_tools_by_ids(tool_id_list: List[int]):
return [as_dict(tool) for tool in tools]


def query_tools_by_labels(tenant_id: str, labels: List[str]):
"""
Query ToolInfo by labels using OR match (tool has ANY of the requested labels).

Args:
tenant_id: Tenant ID for filtering
labels: List of label strings to filter by

Returns:
List of ToolInfo dicts matching any of the given labels
"""
with get_db_session() as session:
query = session.query(ToolInfo).filter(
ToolInfo.delete_flag != 'Y',
ToolInfo.author == tenant_id,
ToolInfo.labels.op('?|')(labels)
)
tools = query.all()
return [as_dict(tool) for tool in tools]


def update_tool_labels(tool_id: int, tenant_id: str, labels: List[str], user_id: str) -> bool:
"""
Update labels for a specific tool. Replaces all existing labels.

Args:
tool_id: Tool ID to update
tenant_id: Tenant ID for access control
labels: New list of label strings
user_id: User performing the update

Returns:
True if updated, False if tool not found or access denied
"""
with get_db_session() as session:
tool = session.query(ToolInfo).filter(
ToolInfo.tool_id == tool_id,
ToolInfo.author == tenant_id,
ToolInfo.delete_flag != 'Y'
).first()
if not tool:
return False
tool.labels = labels
tool.updated_by = user_id
session.flush()
return True


def query_all_enabled_tool_instances(agent_id: int, tenant_id: str, version_no: int = 0):
"""
Query enabled ToolInstance in the database.
Expand Down Expand Up @@ -243,11 +292,17 @@ def update_tool_table_from_scan_tool_list(tenant_id: str, user_id: str, tool_lis
# by tool name and source to update the existing tool
existing_tool = existing_tool_dict[key]
for key, value in filtered_tool_data.items():
# Preserve user-set labels; only overwrite if new labels are explicitly provided
if key == "labels" and not value:
continue
setattr(existing_tool, key, value)
existing_tool.updated_by = user_id
existing_tool.is_available = is_available
else:
# create new tool
# create new tool — apply built-in labels for fresh installs
builtin_labels = BUILTIN_LABEL_MAP.get(tool.name, [])
if builtin_labels:
filtered_tool_data["labels"] = builtin_labels
filtered_tool_data.update(
{"created_by": user_id, "updated_by": user_id, "author": tenant_id, "is_available": is_available})
new_tool = ToolInfo(**filtered_tool_data)
Expand Down Expand Up @@ -394,4 +449,4 @@ def search_last_tool_instance_by_tool_id(tool_id: int, tenant_id: str, user_id:
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
18 changes: 13 additions & 5 deletions backend/services/tool_configuration_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
check_tool_list_initialized,
create_or_update_tool_by_tool_info,
query_all_tools,
query_tools_by_labels,
query_tool_instances_by_id,
search_last_tool_instance_by_tool_id,
update_tool_table_from_scan_tool_list,
Expand Down Expand Up @@ -200,6 +201,7 @@
inputs=json.dumps(processed_inputs, ensure_ascii=False),
output_type=getattr(tool_class, 'output_type'),
category=getattr(tool_class, 'category'),
labels=getattr(tool_class, 'labels', None),
class_name=tool_class.__name__,
usage=None,
origin_name=getattr(tool_class, 'name')
Expand Down Expand Up @@ -245,7 +247,8 @@
class_name=tool_name,
usage=None,
origin_name=tool_name,
category=None
category=None,
labels=None
)
return tool_info

Expand Down Expand Up @@ -486,11 +489,14 @@
tool_list=local_tools+mcp_tools+langchain_tools)


async def list_all_tools(tenant_id: str):
async def list_all_tools(tenant_id: str, labels: Optional[List[str]] = None):

Check failure on line 492 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 69 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKRIHStKnsFKB2_&open=AZ8SStKRIHStKnsFKB2_&pullRequest=3326

Check warning on line 492 in backend/services/tool_configuration_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use asynchronous features in this function or remove the `async` keyword.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8SStKRIHStKnsFKB2-&open=AZ8SStKRIHStKnsFKB2-&pullRequest=3326
"""
List all tools for a given tenant
List all tools for a given tenant, optionally filtered by labels (OR match).
"""
tools_info = query_all_tools(tenant_id)
if labels:
tools_info = query_tools_by_labels(tenant_id, labels)
else:
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()
Expand Down Expand Up @@ -555,7 +561,9 @@
"usage": tool.get("usage"),
"params": tool.get("params", []),
"inputs": inputs_str,
"category": tool.get("category")
"category": tool.get("category"),
"labels": tool.get("labels", []),
"updated_by": tool.get("updated_by", "")
}
formatted_tools.append(formatted_tool)
return formatted_tools
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- Add labels column to ag_tool_info_t table for tool filtering/grouping
ALTER TABLE nexent.ag_tool_info_t
ADD COLUMN IF NOT EXISTS labels JSONB DEFAULT '[]'::jsonb;

COMMENT ON COLUMN nexent.ag_tool_info_t.labels IS 'JSON array of label strings for filtering/grouping tools';

-- Seed built-in labels for well-known local tools.
-- These labels serve as suggested defaults and can be modified by users.
-- Keep in sync with: backend/consts/tool_labels.py

WITH label_map AS (
SELECT key AS tool_name, value AS label FROM jsonb_each_text('{

Check failure on line 12 in deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

An illegal character with code point 10 was found in this literal.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8S32Du-W1iVJbxa6kQ&open=AZ8S32Du-W1iVJbxa6kQ&pullRequest=3326
"mysql_database": "database", "postgres_database": "database", "mssql_database": "database",
"read_file": "file", "create_file": "file", "delete_file": "file",
"create_directory": "file", "delete_directory": "file", "list_directory": "file",
"move_item": "file",
"tavily_search": "search", "exa_search": "search", "linkup_search": "search",
"search_memory": "search", "knowledge_base_search": "search",
"dify_search": "knowledge-base", "datamate_search": "knowledge-base",
"idata_search": "knowledge-base", "haotian_search": "knowledge-base",
"aidp_search": "knowledge-base",
"analyze_image": "multimodal", "analyze_audio": "multimodal",
"analyze_video": "multimodal", "analyze_text_file": "multimodal",
"get_email": "email", "send_email": "email",
"store_memory": "memory",
"terminal": "terminal"
}'::jsonb)
)
UPDATE nexent.ag_tool_info_t t
SET labels = to_jsonb(ARRAY[m.label])
FROM label_map m
WHERE t.name = m.tool_name AND t.labels = '[]'::jsonb;
Loading
Loading