feat: add support for Graph Tools in AIAgent model and related services
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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="知识库用于 RAG,Skills 用于业务流程,Direct Tools 用于低风险实时查询。"
|
||||
description="知识库用于 RAG,Skills 用于业务流程,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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user