Skip to content

Commit 7cd2fa8

Browse files
author
jiangpeiling
committed
✨ Add tool category feature. #1362
✨ Add tool category feature. #1362
1 parent 87d6a9c commit 7cd2fa8

35 files changed

+2151
-34
lines changed

backend/apps/tool_config_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi import APIRouter, Header, HTTPException
66
from fastapi.responses import JSONResponse
77

8-
from consts.exceptions import MCPConnectionError, TimeoutException, NotFoundException
8+
from consts.exceptions import MCPConnectionError, NotFoundException
99
from consts.model import ToolInstanceInfoRequest, ToolInstanceSearchRequest, ToolValidateRequest
1010
from services.tool_configuration_service import (
1111
search_tool_info_impl,
@@ -132,5 +132,5 @@ async def validate_tool(
132132
logger.error(f"Failed to validate tool: {e}")
133133
raise HTTPException(
134134
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
135-
detail=f"Failed to validate tool: {str(e)}"
135+
detail=str(e)
136136
)

backend/consts/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ class ToolInfo(BaseModel):
245245
class_name: str
246246
usage: Optional[str]
247247
origin_name: Optional[str] = None
248+
category: Optional[str] = None
248249

249250

250251
# used in Knowledge Summary request

backend/database/db_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class ToolInfo(TableBase):
177177
params = Column(JSON, doc="Tool parameter information (json)")
178178
inputs = Column(String(2048), doc="Prompt tool inputs description")
179179
output_type = Column(String(100), doc="Prompt tool output description")
180+
category = Column(String(100), doc="Tool category description")
180181
is_available = Column(
181182
Boolean, doc="Whether the tool can be used under the current main service")
182183

backend/services/tool_configuration_service.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import importlib
33
import inspect
44
import json
5-
import keyword
65
import logging
76
from typing import Any, List, Optional, Dict
87
from urllib.parse import urljoin
@@ -102,6 +101,7 @@ def get_local_tools() -> List[ToolInfo]:
102101
inputs=json.dumps(getattr(tool_class, 'inputs'),
103102
ensure_ascii=False),
104103
output_type=getattr(tool_class, 'output_type'),
104+
category=getattr(tool_class, 'category'),
105105
class_name=tool_class.__name__,
106106
usage=None,
107107
origin_name=getattr(tool_class, 'name')
@@ -162,7 +162,8 @@ def _build_tool_info_from_langchain(obj) -> ToolInfo:
162162
output_type=output_type,
163163
class_name=tool_name,
164164
usage=None,
165-
origin_name=tool_name
165+
origin_name=tool_name,
166+
category=None
166167
)
167168
return tool_info
168169

@@ -298,7 +299,8 @@ async def get_tool_from_remote_mcp_server(mcp_server_name: str, remote_mcp_serve
298299
output_type="string",
299300
class_name=sanitized_tool_name,
300301
usage=mcp_server_name,
301-
origin_name=tool.name)
302+
origin_name=tool.name,
303+
category=None)
302304
tools_info.append(tool_info)
303305
return tools_info
304306
except Exception as e:
@@ -351,7 +353,8 @@ async def list_all_tools(tenant_id: str):
351353
"create_time": tool.get("create_time"),
352354
"usage": tool.get("usage"),
353355
"params": tool.get("params", []),
354-
"inputs": tool.get("inputs", {})
356+
"inputs": tool.get("inputs", {}),
357+
"category": tool.get("category")
355358
}
356359
formatted_tools.append(formatted_tool)
357360

@@ -665,10 +668,10 @@ async def validate_tool_impl(
665668

666669
except NotFoundException as e:
667670
logger.error(f"Tool not found: {e}")
668-
raise NotFoundException(f"Tool not found: {str(e)}")
671+
raise NotFoundException(str(e))
669672
except MCPConnectionError as e:
670673
logger.error(f"MCP connection failed: {e}")
671-
raise MCPConnectionError(f"MCP connection failed: {str(e)}")
674+
raise MCPConnectionError(str(e))
672675
except Exception as e:
673676
logger.error(f"Validate Tool failed: {e}")
674-
raise ToolExecutionException(f"Validate Tool failed: {str(e)}")
677+
raise ToolExecutionException(str(e))

docker/init.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ CREATE TABLE IF NOT EXISTS nexent.ag_tool_info_t (
237237
params JSON,
238238
inputs VARCHAR,
239239
output_type VARCHAR(100),
240+
category VARCHAR(100),
240241
is_available BOOLEAN DEFAULT FALSE,
241242
create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
242243
update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Add category column to ag_tool_info_t table
2+
-- This field stores the tool category information (search, file, email, terminal)
3+
4+
ALTER TABLE nexent.ag_tool_info_t
5+
ADD COLUMN IF NOT EXISTS category VARCHAR(100);
6+
7+
-- Add comment to document the purpose of this field
8+
COMMENT ON COLUMN nexent.ag_tool_info_t.category IS 'Tool category information';

frontend/app/[locale]/setup/agents/components/tool/ToolConfigModal.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export default function ToolConfigModal({
5151
const [parsedInputs, setParsedInputs] = useState<Record<string, any>>({});
5252
const [paramValues, setParamValues] = useState<Record<string, string>>({});
5353
const [dynamicInputParams, setDynamicInputParams] = useState<string[]>([]);
54+
const [windowWidth, setWindowWidth] = useState<number>(0);
55+
const [mainModalTop, setMainModalTop] = useState<number>(0);
56+
const [mainModalRight, setMainModalRight] = useState<number>(0);
5457

5558
// load tool config
5659
useEffect(() => {
@@ -118,6 +121,55 @@ export default function ToolConfigModal({
118121
}
119122
}, [isOpen, tool, mainAgentId, t]);
120123

124+
// Monitor window width for responsive positioning
125+
useEffect(() => {
126+
const handleResize = () => {
127+
setWindowWidth(window.innerWidth);
128+
};
129+
130+
// Set initial width
131+
setWindowWidth(window.innerWidth);
132+
133+
window.addEventListener("resize", handleResize);
134+
return () => window.removeEventListener("resize", handleResize);
135+
}, []);
136+
137+
// Calculate main modal position for tool test panel alignment
138+
useEffect(() => {
139+
if (!isOpen) return;
140+
141+
const calculateMainModalPosition = () => {
142+
const modalElement = document.querySelector(".ant-modal");
143+
if (modalElement) {
144+
const rect = modalElement.getBoundingClientRect();
145+
setMainModalTop(rect.top);
146+
setMainModalRight(rect.right);
147+
}
148+
};
149+
150+
// Delay calculation to ensure Modal is rendered
151+
const timeoutId = setTimeout(calculateMainModalPosition, 100);
152+
153+
// Use ResizeObserver to track modal size changes
154+
const observer = new ResizeObserver((entries) => {
155+
for (let entry of entries) {
156+
const rect = entry.target.getBoundingClientRect();
157+
setMainModalTop(rect.top);
158+
setMainModalRight(rect.right);
159+
}
160+
});
161+
162+
const modalElement = document.querySelector(".ant-modal");
163+
if (modalElement) {
164+
observer.observe(modalElement);
165+
}
166+
167+
return () => {
168+
clearTimeout(timeoutId);
169+
observer.disconnect();
170+
};
171+
}, [isOpen]);
172+
121173
// check required fields
122174
const checkRequiredFields = () => {
123175
if (!tool) return false;
@@ -572,8 +624,11 @@ export default function ToolConfigModal({
572624
className="tool-test-panel"
573625
style={{
574626
position: "fixed",
575-
top: "10vh",
576-
right: "5vw",
627+
top: mainModalTop > 0 ? `${mainModalTop}px` : "10vh", // Align with main modal top or fallback to 10vh
628+
left:
629+
mainModalRight > 0
630+
? `${mainModalRight + windowWidth * 0.05}px`
631+
: "calc(50% + 300px + 5vw)", // Position to the right of main modal with 5% viewport width gap
577632
width: "500px",
578633
height: "auto",
579634
maxHeight: "80vh",
@@ -759,4 +814,4 @@ export default function ToolConfigModal({
759814
)}
760815
</>
761816
);
762-
}
817+
}

frontend/app/[locale]/setup/agents/components/tool/ToolPool.tsx

Lines changed: 137 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import {
1212
} from "@ant-design/icons";
1313

1414
import { TOOL_SOURCE_TYPES } from "@/const/agentConfig";
15-
import {
16-
Tooltip,
17-
TooltipTrigger,
18-
TooltipContent,
19-
} from "@/components/ui/tooltip";
2015
import log from "@/lib/logger";
21-
import { Tool, ToolPoolProps, ToolGroup } from "@/types/agentConfig";
16+
import {
17+
Tool,
18+
ToolPoolProps,
19+
ToolGroup,
20+
ToolSubGroup,
21+
} from "@/types/agentConfig";
2222
import {
2323
fetchTools,
2424
searchToolConfig,
@@ -56,6 +56,7 @@ function ToolPool({
5656
const [isMcpModalOpen, setIsMcpModalOpen] = useState(false);
5757
const [isRefreshing, setIsRefreshing] = useState(false);
5858
const [activeTabKey, setActiveTabKey] = useState<string>("");
59+
const [selectedCategory, setSelectedCategory] = useState<string>("");
5960

6061
// Use useMemo to cache tool grouping
6162
const toolGroups = useMemo(() => {
@@ -100,6 +101,37 @@ function ToolPool({
100101
return a.create_time.localeCompare(b.create_time);
101102
});
102103

104+
// Create secondary grouping for local tools
105+
let subGroups: ToolSubGroup[] | undefined;
106+
if (key === TOOL_SOURCE_TYPES.LOCAL) {
107+
const categoryMap = new Map<string, Tool[]>();
108+
109+
sortedTools.forEach((tool) => {
110+
const category =
111+
tool.category && tool.category.trim() !== ""
112+
? tool.category
113+
: t("toolPool.category.other");
114+
if (!categoryMap.has(category)) {
115+
categoryMap.set(category, []);
116+
}
117+
categoryMap.get(category)!.push(tool);
118+
});
119+
120+
subGroups = Array.from(categoryMap.entries())
121+
.map(([category, categoryTools]) => ({
122+
key: category,
123+
label: category,
124+
tools: categoryTools.sort((a, b) => a.name.localeCompare(b.name)), // Sort by name alphabetically
125+
}))
126+
.sort((a, b) => {
127+
// Put "Other" category at the end
128+
const otherKey = t("toolPool.category.other");
129+
if (a.key === otherKey) return 1;
130+
if (b.key === otherKey) return -1;
131+
return a.label.localeCompare(b.label); // Sort other categories alphabetically
132+
});
133+
}
134+
103135
groups.push({
104136
key,
105137
label: key.startsWith("mcp-")
@@ -110,6 +142,7 @@ function ToolPool({
110142
? t("toolPool.group.langchain")
111143
: key,
112144
tools: sortedTools,
145+
subGroups,
113146
});
114147
});
115148

@@ -132,6 +165,20 @@ function ToolPool({
132165
}
133166
}, [toolGroups, activeTabKey]);
134167

168+
// Set default category selection for local tools
169+
useEffect(() => {
170+
const localGroup = toolGroups.find(
171+
(group) => group.key === TOOL_SOURCE_TYPES.LOCAL
172+
);
173+
if (
174+
localGroup?.subGroups &&
175+
localGroup.subGroups.length > 0 &&
176+
!selectedCategory
177+
) {
178+
setSelectedCategory(localGroup.subGroups[0].key);
179+
}
180+
}, [toolGroups, selectedCategory]);
181+
135182
// Use useMemo to cache the selected tool ID set to improve lookup efficiency
136183
const selectedToolIds = useMemo(() => {
137184
return new Set(selectedTools.map((tool) => tool.id));
@@ -511,17 +558,94 @@ function ToolPool({
511558
),
512559
children: (
513560
<div
514-
className="flex flex-col gap-3 pr-2"
561+
className="flex h-full flex-col sm:flex-row"
515562
style={{
516563
height: "100%",
517-
overflowY: "auto",
518-
padding: "8px 0",
519-
maxHeight: "100%",
564+
overflow: "hidden",
520565
}}
521566
>
522-
{group.tools.map((tool) => (
523-
<ToolItem key={tool.id} tool={tool} />
524-
))}
567+
{group.subGroups ? (
568+
<>
569+
{/* Left sidebar - Category navigation */}
570+
<div className="w-auto min-w-fit border-r border-gray-200 flex flex-col hidden sm:flex">
571+
<div className="flex-1 overflow-y-auto">
572+
<div className="px-2 py-2">
573+
{/* Individual categories */}
574+
{group.subGroups.map((subGroup) => (
575+
<div key={subGroup.key}>
576+
<div
577+
className={`h-14 flex items-center px-2 cursor-pointer transition-colors ${
578+
selectedCategory === subGroup.key
579+
? "text-blue-600 font-medium"
580+
: "text-gray-700 font-normal"
581+
}`}
582+
onClick={() => setSelectedCategory(subGroup.key)}
583+
>
584+
<div className="whitespace-nowrap">
585+
{subGroup.label}
586+
</div>
587+
</div>
588+
<div className="border-b border-gray-200 -mx-2"></div>
589+
</div>
590+
))}
591+
</div>
592+
</div>
593+
</div>
594+
595+
{/* Mobile category selector */}
596+
<div className="sm:hidden w-full mb-2">
597+
<select
598+
value={selectedCategory}
599+
onChange={(e) => setSelectedCategory(e.target.value)}
600+
className="w-full p-2 text-sm border border-gray-300 rounded-md bg-white"
601+
>
602+
{group.subGroups.map((subGroup) => (
603+
<option key={subGroup.key} value={subGroup.key}>
604+
{subGroup.label}
605+
</option>
606+
))}
607+
</select>
608+
</div>
609+
610+
{/* Right content - Tool list */}
611+
<div className="flex-1 overflow-hidden">
612+
<div
613+
className="h-full overflow-y-auto p-2"
614+
style={{
615+
maxHeight: "100%",
616+
}}
617+
>
618+
{(() => {
619+
const selectedSubGroup = group.subGroups.find(
620+
(sg) => sg.key === selectedCategory
621+
);
622+
return selectedSubGroup ? (
623+
<div className="space-y-2">
624+
{selectedSubGroup.tools.map((tool) => (
625+
<ToolItem key={tool.id} tool={tool} />
626+
))}
627+
</div>
628+
) : null;
629+
})()}
630+
</div>
631+
</div>
632+
</>
633+
) : (
634+
// Regular layout for non-local tools
635+
<div
636+
className="flex flex-col gap-3 pr-2 flex-1"
637+
style={{
638+
height: "100%",
639+
overflowY: "auto",
640+
padding: "8px 0",
641+
maxHeight: "100%",
642+
}}
643+
>
644+
{group.tools.map((tool) => (
645+
<ToolItem key={tool.id} tool={tool} />
646+
))}
647+
</div>
648+
)}
525649
</div>
526650
),
527651
};

0 commit comments

Comments
 (0)