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 { func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" { if aiAgent == nil {
return nil return nil
} }
items, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools) ret := make([]string, 0)
if err != nil { if raw := strings.TrimSpace(aiAgent.AllowedMCPTools); raw != "" {
return nil items, err := toolx.ParseAgentMCPToolsJSON(raw)
if err == nil {
for _, item := range items {
ret = append(ret, item.ToolCode)
}
}
} }
ret := make([]string, 0, len(items)) if raw := strings.TrimSpace(aiAgent.AllowedGraphTools); raw != "" {
for _, item := range items { var graphTools []string
ret = append(ret, item.ToolCode) if err := json.Unmarshal([]byte(raw), &graphTools); err == nil {
ret = append(ret, graphTools...)
}
} }
return toolx.NormalizeToolCodes(ret) return toolx.NormalizeToolCodes(ret)
} }
@@ -131,6 +131,7 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
Skills: make([]response.AIAgentSkillResponse, 0), Skills: make([]response.AIAgentSkillResponse, 0),
Teams: make([]response.AIAgentTeamResponse, 0), Teams: make([]response.AIAgentTeamResponse, 0),
DirectTools: make([]response.AIAgentMCPToolResponse, 0), DirectTools: make([]response.AIAgentMCPToolResponse, 0),
GraphTools: make([]string, 0),
SortNo: item.SortNo, SortNo: item.SortNo,
Remark: item.Remark, Remark: item.Remark,
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"), 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) { if toolx.IsAutoInjectedToolCode(toolCode) {
continue continue
} }
if toolx.IsAgentDirectGraphToolCode(toolCode) {
ret.GraphTools = appendGraphToolCodeIfMissing(ret.GraphTools, toolCode)
continue
}
serverCode := strings.TrimSpace(tool.ServerCode) serverCode := strings.TrimSpace(tool.ServerCode)
toolName := strings.TrimSpace(tool.ToolName) toolName := strings.TrimSpace(tool.ToolName)
if registeredServerCode, registeredToolName, ok := toolx.GetRegisteredToolIdentity(toolCode); ok { 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 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列表,按顺序表示优先级。 KnowledgeIDs string `gorm:"type:varchar(500);not null;default:''"` // KnowledgeIDs 为绑定的知识库ID列表,按顺序表示优先级。
SkillIDs string `gorm:"type:varchar(500);not null;default:''"` // SkillIDs 为绑定的技能ID列表,按顺序表示允许路由的范围。 SkillIDs string `gorm:"type:varchar(500);not null;default:''"` // SkillIDs 为绑定的技能ID列表,按顺序表示允许路由的范围。
AllowedMCPTools string `gorm:"type:text"` // AllowedMCPTools 为允许 direct tool 路由的 MCP 工具白名单配置JSON。 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 为后台展示排序号。 SortNo int `gorm:"type:int;not null;default:0;index"` // SortNo 为后台展示排序号。
Remark string `gorm:"type:text"` // Remark 为备注。 Remark string `gorm:"type:text"` // Remark 为备注。
AuditFields AuditFields
+1
View File
@@ -56,6 +56,7 @@ type CreateAIAgentRequest struct {
KnowledgeIDs []int64 `json:"knowledgeIds"` KnowledgeIDs []int64 `json:"knowledgeIds"`
SkillIDs []int64 `json:"skillIds"` SkillIDs []int64 `json:"skillIds"`
DirectTools []AIAgentMCPToolRequest `json:"directTools"` DirectTools []AIAgentMCPToolRequest `json:"directTools"`
GraphTools []string `json:"graphTools"`
Remark string `json:"remark"` Remark string `json:"remark"`
} }
+1
View File
@@ -89,6 +89,7 @@ type AIAgentResponse struct {
SkillIDs []int64 `json:"skillIds"` SkillIDs []int64 `json:"skillIds"`
Skills []AIAgentSkillResponse `json:"skills"` Skills []AIAgentSkillResponse `json:"skills"`
DirectTools []AIAgentMCPToolResponse `json:"directTools"` DirectTools []AIAgentMCPToolResponse `json:"directTools"`
GraphTools []string `json:"graphTools"`
SortNo int `json:"sortNo"` SortNo int `json:"sortNo"`
Remark string `json:"remark"` Remark string `json:"remark"`
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`
+6
View File
@@ -281,6 +281,12 @@ func IsAgentDirectToolCode(toolCode string) bool {
return ok && spec.DirectAccess 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 { func NormalizeToolCodeAlias(toolCode string) string {
toolCode = strings.TrimSpace(toolCode) toolCode = strings.TrimSpace(toolCode)
if canonical, ok := toolAliasToCanonical[toolCode]; ok { 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, "knowledge_ids": item.KnowledgeIDs,
"skill_ids": item.SkillIDs, "skill_ids": item.SkillIDs,
"allowed_mcp_tools": item.AllowedMCPTools, "allowed_mcp_tools": item.AllowedMCPTools,
"allowed_graph_tools": item.AllowedGraphTools,
"remark": item.Remark, "remark": item.Remark,
"update_user_id": operator.UserID, "update_user_id": operator.UserID,
"update_user_name": operator.Username, "update_user_name": operator.Username,
@@ -179,6 +180,10 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
if err != nil { if err != nil {
return nil, err return nil, err
} }
graphTools, err := s.normalizeGraphTools(req.GraphTools)
if err != nil {
return nil, err
}
directToolsJSON := "" directToolsJSON := ""
if len(directTools) > 0 { if len(directTools) > 0 {
buf, marshalErr := json.Marshal(directTools) buf, marshalErr := json.Marshal(directTools)
@@ -187,6 +192,14 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
} }
directToolsJSON = string(buf) 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{ return &models.AIAgent{
Name: name, Name: name,
Description: strings.TrimSpace(req.Description), Description: strings.TrimSpace(req.Description),
@@ -201,6 +214,7 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
KnowledgeIDs: utils.JoinInt64s(knowledgeIDs), KnowledgeIDs: utils.JoinInt64s(knowledgeIDs),
SkillIDs: utils.JoinInt64s(skillIDs), SkillIDs: utils.JoinInt64s(skillIDs),
AllowedMCPTools: directToolsJSON, AllowedMCPTools: directToolsJSON,
AllowedGraphTools: graphToolsJSON,
Remark: strings.TrimSpace(req.Remark), Remark: strings.TrimSpace(req.Remark),
}, nil }, nil
} }
@@ -289,6 +303,9 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
if toolx.IsAutoInjectedToolCode(strings.TrimSpace(normalized.ToolCode)) { if toolx.IsAutoInjectedToolCode(strings.TrimSpace(normalized.ToolCode)) {
continue continue
} }
if toolx.ResolveToolSourceType(normalized.ToolCode) != enums.ToolSourceTypeMCP {
return nil, errorsx.InvalidParam("Direct Tools 仅允许配置 MCP 工具")
}
if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil { if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil {
return nil, err return nil, err
} }
@@ -302,6 +319,29 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
return ret, nil 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 { func (s *aIAgentService) UpdateSort(ids []int64) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error { return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
for i, id := range ids { for i, id := range ids {
+118 -12
View File
@@ -69,6 +69,11 @@ type DirectToolOption = {
groupLabel: string; groupLabel: string;
}; };
type GraphToolOption = {
value: string;
label: string;
};
type EditDialogProps = { type EditDialogProps = {
open: boolean; open: boolean;
saving: boolean; saving: boolean;
@@ -150,6 +155,7 @@ function buildPayload(
teamIds: number[], teamIds: number[],
skillIds: number[], skillIds: number[],
directTools: CreateAIAgentPayload["directTools"], directTools: CreateAIAgentPayload["directTools"],
graphTools: CreateAIAgentPayload["graphTools"],
): CreateAIAgentPayload { ): CreateAIAgentPayload {
return { return {
name: form.name.trim(), name: form.name.trim(),
@@ -165,6 +171,7 @@ function buildPayload(
knowledgeIds, knowledgeIds,
skillIds, skillIds,
directTools, directTools,
graphTools,
remark: form.remark.trim(), remark: form.remark.trim(),
}; };
} }
@@ -226,14 +233,19 @@ function EditDialogBody({
const [skillToAdd, setSkillToAdd] = useState(""); const [skillToAdd, setSkillToAdd] = useState("");
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState(""); const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("");
const [directToolToAdd, setDirectToolToAdd] = useState(""); const [directToolToAdd, setDirectToolToAdd] = useState("");
const [graphToolToAdd, setGraphToolToAdd] = useState("");
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]); const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]);
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]); const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]); const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]);
const [skills, setSkills] = useState<SkillDefinition[]>([]); const [skills, setSkills] = useState<SkillDefinition[]>([]);
const [directTools, setDirectTools] = useState<DirectToolItem[]>([]); const [directTools, setDirectTools] = useState<DirectToolItem[]>([]);
const [graphTools, setGraphTools] = useState<string[]>([]);
const [directToolOptions, setDirectToolOptions] = useState<DirectToolOption[]>( const [directToolOptions, setDirectToolOptions] = useState<DirectToolOption[]>(
[], [],
); );
const [graphToolOptions, setGraphToolOptions] = useState<GraphToolOption[]>(
[],
);
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]); const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]);
useEffect(() => { useEffect(() => {
@@ -244,11 +256,13 @@ function EditDialogBody({
setSelectedTeamIds([]); setSelectedTeamIds([]);
setSelectedSkillIds([]); setSelectedSkillIds([]);
setDirectTools([]); setDirectTools([]);
setGraphTools([]);
setKnowledgeToAdd(""); setKnowledgeToAdd("");
setTeamToAdd(""); setTeamToAdd("");
setSkillToAdd(""); setSkillToAdd("");
setDirectToolGroupToAdd(""); setDirectToolGroupToAdd("");
setDirectToolToAdd(""); setDirectToolToAdd("");
setGraphToolToAdd("");
return; return;
} }
setLoading(true); setLoading(true);
@@ -259,11 +273,13 @@ function EditDialogBody({
setSelectedTeamIds((data.teams ?? []).map((team) => team.id)); setSelectedTeamIds((data.teams ?? []).map((team) => team.id));
setSelectedSkillIds(data.skillIds ?? []); setSelectedSkillIds(data.skillIds ?? []);
setDirectTools(data.directTools ?? []); setDirectTools(data.directTools ?? []);
setGraphTools(data.graphTools ?? []);
setKnowledgeToAdd(""); setKnowledgeToAdd("");
setTeamToAdd(""); setTeamToAdd("");
setSkillToAdd(""); setSkillToAdd("");
setDirectToolGroupToAdd(""); setDirectToolGroupToAdd("");
setDirectToolToAdd(""); setDirectToolToAdd("");
setGraphToolToAdd("");
} catch (error) { } catch (error) {
toast.error( toast.error(
error instanceof Error ? error.message : "加载 AI Agent 详情失败", error instanceof Error ? error.message : "加载 AI Agent 详情失败",
@@ -338,7 +354,7 @@ function EditDialogBody({
setToolCatalog(catalog); setToolCatalog(catalog);
setDirectToolOptions( setDirectToolOptions(
catalog catalog
.filter((tool) => !tool.autoInjected) .filter((tool) => !tool.autoInjected && tool.sourceType === "mcp")
.map((tool) => ({ .map((tool) => ({
value: tool.toolCode, value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${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) { } catch (error) {
toast.error( toast.error(
error instanceof Error ? error.message : "加载 Direct Tools 失败", error instanceof Error ? error.message : "加载 Direct Tools 失败",
@@ -470,10 +494,7 @@ function EditDialogBody({
const directToolsGrouped = useMemo(() => { const directToolsGrouped = useMemo(() => {
const groups = new Map<string, DirectToolItem[]>(); const groups = new Map<string, DirectToolItem[]>();
for (const tool of directTools) { for (const tool of directTools) {
const groupLabel = const groupLabel = tool.serverCode || "未分组";
tool.serverCode === "builtin" || tool.toolCode.startsWith("builtin/")
? "内置工具"
: tool.serverCode || "未分组";
const current = groups.get(groupLabel) ?? []; const current = groups.get(groupLabel) ?? [];
current.push(tool); current.push(tool);
groups.set(groupLabel, current); groups.set(groupLabel, current);
@@ -481,6 +502,14 @@ function EditDialogBody({
return Array.from(groups.entries()); return Array.from(groups.entries());
}, [directTools]); }, [directTools]);
const addableGraphToolOptions = useMemo(
() =>
graphToolOptions.filter(
(option) => !graphTools.includes(option.value),
),
[graphToolOptions, graphTools],
);
const addableTeamOptions = useMemo( const addableTeamOptions = useMemo(
() => () =>
teamOptions.filter( teamOptions.filter(
@@ -512,6 +541,7 @@ function EditDialogBody({
selectedTeamIds, selectedTeamIds,
selectedSkillIds, selectedSkillIds,
directTools, directTools,
graphTools,
), ),
); );
} }
@@ -593,6 +623,18 @@ function EditDialogBody({
setDirectTools((prev) => prev.filter((item) => item.toolCode !== value)); 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 ( return (
<ProjectDialog <ProjectDialog
open={open} open={open}
@@ -700,9 +742,9 @@ function EditDialogBody({
<SectionCard <SectionCard
title="能力配置" 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="rounded-xl border bg-muted/10 p-4">
<div className="mb-1 text-sm font-medium"></div> <div className="mb-1 text-sm font-medium"></div>
<div className="mb-4 text-xs text-muted-foreground"> <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="rounded-xl border bg-muted/10 p-4">
<div className="mb-1 text-sm font-medium">Direct Tools</div> <div className="mb-1 text-sm font-medium">Direct Tools</div>
<div className="mb-4 text-xs text-muted-foreground"> <div className="mb-4 text-xs text-muted-foreground">
MCP MCP
`tool_search` Runtime
</div> </div>
<Field> <Field>
<FieldContent className="space-y-3"> <FieldContent className="space-y-3">
@@ -923,9 +964,7 @@ function EditDialogBody({
> >
{tool.title || catalogItem?.title || value} {tool.title || catalogItem?.title || value}
<span className="text-[10px] text-muted-foreground/80"> <span className="text-[10px] text-muted-foreground/80">
{catalogItem?.sourceType === "builtin" {tool.serverCode || "MCP"}
? "内置"
: tool.serverCode || "MCP"}
</span> </span>
<Button <Button
type="button" type="button"
@@ -950,6 +989,73 @@ function EditDialogBody({
</FieldContent> </FieldContent>
</Field> </Field>
</div> </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> </div>
</SectionCard> </SectionCard>
+2
View File
@@ -220,6 +220,7 @@ export type AIAgent = {
description: string description: string
arguments?: Record<string, string> arguments?: Record<string, string>
}[] }[]
graphTools: string[]
sortNo: number sortNo: number
remark: string remark: string
createdAt: string createdAt: string
@@ -249,6 +250,7 @@ export type CreateAIAgentPayload = {
description: string description: string
arguments?: Record<string, string> arguments?: Record<string, string>
}[] }[]
graphTools: string[]
remark: string remark: string
} }