feat: add support for Graph Tools in AIAgent model and related services

This commit is contained in:
mlogclub
2026-04-14 15:10:50 +08:00
parent 8d560bb6bd
commit aa7f94e08f
9 changed files with 213 additions and 19 deletions
@@ -73,16 +73,23 @@ func (c *toolCatalog) parseSkillAllowedToolCodes(skill *models.SkillDefinition)
}
func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" {
if aiAgent == nil {
return nil
}
items, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools)
if err != nil {
return nil
ret := make([]string, 0)
if raw := strings.TrimSpace(aiAgent.AllowedMCPTools); raw != "" {
items, err := toolx.ParseAgentMCPToolsJSON(raw)
if err == nil {
for _, item := range items {
ret = append(ret, item.ToolCode)
}
}
}
ret := make([]string, 0, len(items))
for _, item := range items {
ret = append(ret, item.ToolCode)
if raw := strings.TrimSpace(aiAgent.AllowedGraphTools); raw != "" {
var graphTools []string
if err := json.Unmarshal([]byte(raw), &graphTools); err == nil {
ret = append(ret, graphTools...)
}
}
return toolx.NormalizeToolCodes(ret)
}
@@ -131,6 +131,7 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
Skills: make([]response.AIAgentSkillResponse, 0),
Teams: make([]response.AIAgentTeamResponse, 0),
DirectTools: make([]response.AIAgentMCPToolResponse, 0),
GraphTools: make([]string, 0),
SortNo: item.SortNo,
Remark: item.Remark,
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -175,6 +176,10 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
if toolx.IsAutoInjectedToolCode(toolCode) {
continue
}
if toolx.IsAgentDirectGraphToolCode(toolCode) {
ret.GraphTools = appendGraphToolCodeIfMissing(ret.GraphTools, toolCode)
continue
}
serverCode := strings.TrimSpace(tool.ServerCode)
toolName := strings.TrimSpace(tool.ToolName)
if registeredServerCode, registeredToolName, ok := toolx.GetRegisteredToolIdentity(toolCode); ok {
@@ -207,5 +212,30 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
}
}
}
if raw := strings.TrimSpace(item.AllowedGraphTools); raw != "" {
var graphTools []string
if err := json.Unmarshal([]byte(raw), &graphTools); err == nil {
for _, toolCode := range graphTools {
toolCode = toolx.NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
if !toolx.IsAgentDirectGraphToolCode(toolCode) {
continue
}
ret.GraphTools = appendGraphToolCodeIfMissing(ret.GraphTools, toolCode)
}
}
}
return ret
}
func appendGraphToolCodeIfMissing(items []string, toolCode string) []string {
toolCode = strings.TrimSpace(toolCode)
if toolCode == "" {
return items
}
for _, item := range items {
if strings.TrimSpace(item) == toolCode {
return items
}
}
return append(items, toolCode)
}
+1
View File
@@ -514,6 +514,7 @@ type AIAgent struct {
KnowledgeIDs string `gorm:"type:varchar(500);not null;default:''"` // KnowledgeIDs 为绑定的知识库ID列表,按顺序表示优先级。
SkillIDs string `gorm:"type:varchar(500);not null;default:''"` // SkillIDs 为绑定的技能ID列表,按顺序表示允许路由的范围。
AllowedMCPTools string `gorm:"type:text"` // AllowedMCPTools 为允许 direct tool 路由的 MCP 工具白名单配置JSON。
AllowedGraphTools string `gorm:"type:text"` // AllowedGraphTools 为允许 Graph Tool 的白名单配置JSON。
SortNo int `gorm:"type:int;not null;default:0;index"` // SortNo 为后台展示排序号。
Remark string `gorm:"type:text"` // Remark 为备注。
AuditFields
+1
View File
@@ -56,6 +56,7 @@ type CreateAIAgentRequest struct {
KnowledgeIDs []int64 `json:"knowledgeIds"`
SkillIDs []int64 `json:"skillIds"`
DirectTools []AIAgentMCPToolRequest `json:"directTools"`
GraphTools []string `json:"graphTools"`
Remark string `json:"remark"`
}
+1
View File
@@ -89,6 +89,7 @@ type AIAgentResponse struct {
SkillIDs []int64 `json:"skillIds"`
Skills []AIAgentSkillResponse `json:"skills"`
DirectTools []AIAgentMCPToolResponse `json:"directTools"`
GraphTools []string `json:"graphTools"`
SortNo int `json:"sortNo"`
Remark string `json:"remark"`
CreatedAt string `json:"createdAt"`
+6
View File
@@ -281,6 +281,12 @@ func IsAgentDirectToolCode(toolCode string) bool {
return ok && spec.DirectAccess
}
func IsAgentDirectGraphToolCode(toolCode string) bool {
toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
spec, ok := GetRegisteredToolSpec(toolCode)
return ok && spec.DirectAccess && spec.SourceType == enums.ToolSourceTypeGraph
}
func NormalizeToolCodeAlias(toolCode string) string {
toolCode = strings.TrimSpace(toolCode)
if canonical, ok := toolAliasToCanonical[toolCode]; ok {
+40
View File
@@ -105,6 +105,7 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
"knowledge_ids": item.KnowledgeIDs,
"skill_ids": item.SkillIDs,
"allowed_mcp_tools": item.AllowedMCPTools,
"allowed_graph_tools": item.AllowedGraphTools,
"remark": item.Remark,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
@@ -179,6 +180,10 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
if err != nil {
return nil, err
}
graphTools, err := s.normalizeGraphTools(req.GraphTools)
if err != nil {
return nil, err
}
directToolsJSON := ""
if len(directTools) > 0 {
buf, marshalErr := json.Marshal(directTools)
@@ -187,6 +192,14 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
}
directToolsJSON = string(buf)
}
graphToolsJSON := ""
if len(graphTools) > 0 {
buf, marshalErr := json.Marshal(graphTools)
if marshalErr != nil {
return nil, errorsx.InvalidParam("Graph Tools 配置格式不合法")
}
graphToolsJSON = string(buf)
}
return &models.AIAgent{
Name: name,
Description: strings.TrimSpace(req.Description),
@@ -201,6 +214,7 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
KnowledgeIDs: utils.JoinInt64s(knowledgeIDs),
SkillIDs: utils.JoinInt64s(skillIDs),
AllowedMCPTools: directToolsJSON,
AllowedGraphTools: graphToolsJSON,
Remark: strings.TrimSpace(req.Remark),
}, nil
}
@@ -289,6 +303,9 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
if toolx.IsAutoInjectedToolCode(strings.TrimSpace(normalized.ToolCode)) {
continue
}
if toolx.ResolveToolSourceType(normalized.ToolCode) != enums.ToolSourceTypeMCP {
return nil, errorsx.InvalidParam("Direct Tools 仅允许配置 MCP 工具")
}
if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil {
return nil, err
}
@@ -302,6 +319,29 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
return ret, nil
}
func (s *aIAgentService) normalizeGraphTools(input []string) ([]string, error) {
if len(input) == 0 {
return nil, nil
}
ret := make([]string, 0, len(input))
seen := make(map[string]struct{})
for _, item := range input {
toolCode := toolx.NormalizeToolCodeAlias(strings.TrimSpace(item))
if toolCode == "" {
continue
}
if !toolx.IsAgentDirectGraphToolCode(toolCode) {
return nil, errorsx.InvalidParam("Graph Tools 仅允许配置 Graph Tool")
}
if _, exists := seen[toolCode]; exists {
continue
}
seen[toolCode] = struct{}{}
ret = append(ret, toolCode)
}
return ret, nil
}
func (s *aIAgentService) UpdateSort(ids []int64) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
for i, id := range ids {
+118 -12
View File
@@ -69,6 +69,11 @@ type DirectToolOption = {
groupLabel: string;
};
type GraphToolOption = {
value: string;
label: string;
};
type EditDialogProps = {
open: boolean;
saving: boolean;
@@ -150,6 +155,7 @@ function buildPayload(
teamIds: number[],
skillIds: number[],
directTools: CreateAIAgentPayload["directTools"],
graphTools: CreateAIAgentPayload["graphTools"],
): CreateAIAgentPayload {
return {
name: form.name.trim(),
@@ -165,6 +171,7 @@ function buildPayload(
knowledgeIds,
skillIds,
directTools,
graphTools,
remark: form.remark.trim(),
};
}
@@ -226,14 +233,19 @@ function EditDialogBody({
const [skillToAdd, setSkillToAdd] = useState("");
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("");
const [directToolToAdd, setDirectToolToAdd] = useState("");
const [graphToolToAdd, setGraphToolToAdd] = useState("");
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]);
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]);
const [skills, setSkills] = useState<SkillDefinition[]>([]);
const [directTools, setDirectTools] = useState<DirectToolItem[]>([]);
const [graphTools, setGraphTools] = useState<string[]>([]);
const [directToolOptions, setDirectToolOptions] = useState<DirectToolOption[]>(
[],
);
const [graphToolOptions, setGraphToolOptions] = useState<GraphToolOption[]>(
[],
);
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]);
useEffect(() => {
@@ -244,11 +256,13 @@ function EditDialogBody({
setSelectedTeamIds([]);
setSelectedSkillIds([]);
setDirectTools([]);
setGraphTools([]);
setKnowledgeToAdd("");
setTeamToAdd("");
setSkillToAdd("");
setDirectToolGroupToAdd("");
setDirectToolToAdd("");
setGraphToolToAdd("");
return;
}
setLoading(true);
@@ -259,11 +273,13 @@ function EditDialogBody({
setSelectedTeamIds((data.teams ?? []).map((team) => team.id));
setSelectedSkillIds(data.skillIds ?? []);
setDirectTools(data.directTools ?? []);
setGraphTools(data.graphTools ?? []);
setKnowledgeToAdd("");
setTeamToAdd("");
setSkillToAdd("");
setDirectToolGroupToAdd("");
setDirectToolToAdd("");
setGraphToolToAdd("");
} catch (error) {
toast.error(
error instanceof Error ? error.message : "加载 AI Agent 详情失败",
@@ -338,7 +354,7 @@ function EditDialogBody({
setToolCatalog(catalog);
setDirectToolOptions(
catalog
.filter((tool) => !tool.autoInjected)
.filter((tool) => !tool.autoInjected && tool.sourceType === "mcp")
.map((tool) => ({
value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
@@ -358,6 +374,14 @@ function EditDialogBody({
},
})),
);
setGraphToolOptions(
catalog
.filter((tool) => tool.sourceType === "graph")
.map((tool) => ({
value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
})),
);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "加载 Direct Tools 失败",
@@ -470,10 +494,7 @@ function EditDialogBody({
const directToolsGrouped = useMemo(() => {
const groups = new Map<string, DirectToolItem[]>();
for (const tool of directTools) {
const groupLabel =
tool.serverCode === "builtin" || tool.toolCode.startsWith("builtin/")
? "内置工具"
: tool.serverCode || "未分组";
const groupLabel = tool.serverCode || "未分组";
const current = groups.get(groupLabel) ?? [];
current.push(tool);
groups.set(groupLabel, current);
@@ -481,6 +502,14 @@ function EditDialogBody({
return Array.from(groups.entries());
}, [directTools]);
const addableGraphToolOptions = useMemo(
() =>
graphToolOptions.filter(
(option) => !graphTools.includes(option.value),
),
[graphToolOptions, graphTools],
);
const addableTeamOptions = useMemo(
() =>
teamOptions.filter(
@@ -512,6 +541,7 @@ function EditDialogBody({
selectedTeamIds,
selectedSkillIds,
directTools,
graphTools,
),
);
}
@@ -593,6 +623,18 @@ function EditDialogBody({
setDirectTools((prev) => prev.filter((item) => item.toolCode !== value));
}
function handleAddGraphTool(value: string) {
if (!value || graphTools.includes(value)) {
return;
}
setGraphTools((prev) => [...prev, value]);
setGraphToolToAdd("");
}
function handleRemoveGraphTool(value: string) {
setGraphTools((prev) => prev.filter((item) => item !== value));
}
return (
<ProjectDialog
open={open}
@@ -700,9 +742,9 @@ function EditDialogBody({
<SectionCard
title="能力配置"
description="知识库用于 RAGSkills 用于业务流程,Direct Tools 用于低风险实时查询。"
description="知识库用于 RAGSkills 用于业务流程,Direct Tools 用于外部 MCP 查询,Graph Tools 用于内置业务流程。"
>
<div className="grid gap-4 xl:grid-cols-3">
<div className="grid gap-4 xl:grid-cols-4">
<div className="rounded-xl border bg-muted/10 p-4">
<div className="mb-1 text-sm font-medium"></div>
<div className="mb-4 text-xs text-muted-foreground">
@@ -854,8 +896,7 @@ function EditDialogBody({
<div className="rounded-xl border bg-muted/10 p-4">
<div className="mb-1 text-sm font-medium">Direct Tools</div>
<div className="mb-4 text-xs text-muted-foreground">
MCP
`tool_search` Runtime
MCP
</div>
<Field>
<FieldContent className="space-y-3">
@@ -923,9 +964,7 @@ function EditDialogBody({
>
{tool.title || catalogItem?.title || value}
<span className="text-[10px] text-muted-foreground/80">
{catalogItem?.sourceType === "builtin"
? "内置"
: tool.serverCode || "MCP"}
{tool.serverCode || "MCP"}
</span>
<Button
type="button"
@@ -950,6 +989,73 @@ function EditDialogBody({
</FieldContent>
</Field>
</div>
<div className="rounded-xl border bg-muted/10 p-4">
<div className="mb-1 text-sm font-medium">Graph Tools</div>
<div className="mb-4 text-xs text-muted-foreground">
Direct Tools
</div>
<Field>
<FieldContent className="space-y-3">
<div className="flex items-center gap-2">
<div className="flex-1">
<OptionCombobox
value={graphToolToAdd}
options={addableGraphToolOptions}
placeholder="选择 Graph Tool"
searchPlaceholder="搜索 Graph Tool"
emptyText="没有可添加的 Graph Tool"
onChange={handleAddGraphTool}
/>
</div>
<Button
type="button"
variant="outline"
disabled={!graphToolToAdd}
onClick={() => handleAddGraphTool(graphToolToAdd)}
>
<PlusIcon />
</Button>
</div>
<div className="flex flex-wrap gap-2">
{graphTools.length === 0 ? (
<span className="text-sm text-muted-foreground">
Graph Tool Agent /
</span>
) : (
graphTools.map((toolCode) => {
const catalogItem = toolCatalog.find(
(item) => item.toolCode === toolCode,
);
return (
<Badge
key={toolCode}
variant="secondary"
className="gap-1 pr-1"
>
{catalogItem?.title || toolCode}
<span className="text-[10px] text-muted-foreground/80">
graph
</span>
<Button
type="button"
variant="ghost"
size="icon"
className="size-5"
onClick={() => handleRemoveGraphTool(toolCode)}
aria-label={`移除 Graph Tool ${toolCode}`}
>
<Trash2Icon className="size-3" />
</Button>
</Badge>
);
})
)}
</div>
</FieldContent>
</Field>
</div>
</div>
</SectionCard>
+2
View File
@@ -220,6 +220,7 @@ export type AIAgent = {
description: string
arguments?: Record<string, string>
}[]
graphTools: string[]
sortNo: number
remark: string
createdAt: string
@@ -249,6 +250,7 @@ export type CreateAIAgentPayload = {
description: string
arguments?: Record<string, string>
}[]
graphTools: string[]
remark: string
}