refactor: remove knowledge base references from AI agent model and related services
- Removed KnowledgeIDs from CreateAIAgentRequest and AIAgentResponse. - Updated buildAIAgentResponseWithLocale to eliminate knowledge base name retrieval. - Refactored AI agent repository and service to remove knowledge base handling. - Adjusted dashboard service to no longer track AI agents without knowledge bases. - Modified knowledge base service to check for workflow references instead of AI agent references. - Updated frontend components to remove knowledge base selection and display. - Enhanced workflow validation to ensure knowledge retrieve nodes have associated knowledge bases.
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
BotMessageSquareIcon,
|
||||
GitBranchIcon,
|
||||
HistoryIcon,
|
||||
@@ -44,7 +42,6 @@ import {
|
||||
fetchAIWorkflowNodeSpecs,
|
||||
fetchAIWorkflowVersions,
|
||||
fetchAgentTeamsAll,
|
||||
fetchKnowledgeBasesAll,
|
||||
fetchMCPCatalog,
|
||||
fetchSkillDefinitionsAll,
|
||||
publishAIAgentWorkflow,
|
||||
@@ -58,7 +55,6 @@ import {
|
||||
type AIWorkflowVersion,
|
||||
type AdminAgentTeam,
|
||||
type CreateAIAgentPayload,
|
||||
type KnowledgeBase,
|
||||
type MCPToolCatalogItem,
|
||||
type MCPToolSourceType,
|
||||
type SkillDefinition,
|
||||
@@ -148,7 +144,6 @@ export function AIAgentConfigWorkbench({
|
||||
const [handoffMode, setHandoffMode] = useState(String(AIAgentHandoffMode.WaitPool))
|
||||
const [fallbackMode, setFallbackMode] = useState(String(AIAgentFallbackMode.NoAnswer))
|
||||
const [fallbackMessage, setFallbackMessage] = useState("")
|
||||
const [selectedKnowledgeIds, setSelectedKnowledgeIds] = useState<number[]>([])
|
||||
const [selectedTeamIds, setSelectedTeamIds] = useState<number[]>([])
|
||||
const [selectedSkillIds, setSelectedSkillIds] = useState<number[]>([])
|
||||
const [directTools, setDirectTools] = useState<DirectToolItem[]>([])
|
||||
@@ -157,11 +152,9 @@ export function AIAgentConfigWorkbench({
|
||||
const [workflowRevision, setWorkflowRevision] = useState(0)
|
||||
|
||||
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([])
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([])
|
||||
const [skills, setSkills] = useState<SkillDefinition[]>([])
|
||||
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([])
|
||||
const [knowledgeToAdd, setKnowledgeToAdd] = useState("")
|
||||
const [teamToAdd, setTeamToAdd] = useState("")
|
||||
const [skillToAdd, setSkillToAdd] = useState("")
|
||||
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
|
||||
@@ -183,7 +176,6 @@ export function AIAgentConfigWorkbench({
|
||||
specs,
|
||||
defaultDefinition,
|
||||
configs,
|
||||
bases,
|
||||
teams,
|
||||
skillList,
|
||||
catalog,
|
||||
@@ -191,7 +183,6 @@ export function AIAgentConfigWorkbench({
|
||||
fetchAIWorkflowNodeSpecs(),
|
||||
fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition),
|
||||
fetchAIConfigsAll({ modelType: AIModelType.LLM }),
|
||||
fetchKnowledgeBasesAll({ status: Status.Ok }),
|
||||
fetchAgentTeamsAll(),
|
||||
fetchSkillDefinitionsAll({ status: Status.Ok }),
|
||||
fetchMCPCatalog(),
|
||||
@@ -199,7 +190,6 @@ export function AIAgentConfigWorkbench({
|
||||
|
||||
setNodeSpecs(specs ?? [])
|
||||
setAIConfigs(configs ?? [])
|
||||
setKnowledgeBases(bases ?? [])
|
||||
setAgentTeams(teams ?? [])
|
||||
setSkills(skillList ?? [])
|
||||
setToolCatalog(catalog ?? [])
|
||||
@@ -217,7 +207,6 @@ export function AIAgentConfigWorkbench({
|
||||
setHandoffMode(String(AIAgentHandoffMode.WaitPool))
|
||||
setFallbackMode(String(AIAgentFallbackMode.NoAnswer))
|
||||
setFallbackMessage("")
|
||||
setSelectedKnowledgeIds([])
|
||||
setSelectedTeamIds([])
|
||||
setSelectedSkillIds([])
|
||||
setDirectTools([])
|
||||
@@ -247,7 +236,6 @@ export function AIAgentConfigWorkbench({
|
||||
setHandoffMode(String(agentDetail.handoffMode || AIAgentHandoffMode.WaitPool))
|
||||
setFallbackMode(String(agentDetail.fallbackMode || AIAgentFallbackMode.NoAnswer))
|
||||
setFallbackMessage(agentDetail.fallbackMessage || "")
|
||||
setSelectedKnowledgeIds(agentDetail.knowledgeIds ?? [])
|
||||
setSelectedTeamIds((agentDetail.teams ?? []).map((team) => team.id))
|
||||
setSelectedSkillIds(agentDetail.skillIds ?? [])
|
||||
setDirectTools(agentDetail.directTools ?? [])
|
||||
@@ -290,10 +278,6 @@ export function AIAgentConfigWorkbench({
|
||||
() => aiConfigs.map((item) => ({ value: String(item.id), label: `${item.name} · ${item.modelName}` })),
|
||||
[aiConfigs]
|
||||
)
|
||||
const knowledgeOptions = useMemo(
|
||||
() => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })),
|
||||
[knowledgeBases]
|
||||
)
|
||||
const teamOptions = useMemo(
|
||||
() => agentTeams.map((item) => ({ value: String(item.id), label: item.name })),
|
||||
[agentTeams]
|
||||
@@ -356,16 +340,6 @@ export function AIAgentConfigWorkbench({
|
||||
setNext([...current, id])
|
||||
}
|
||||
|
||||
function moveKnowledge(index: number, direction: -1 | 1) {
|
||||
const targetIndex = index + direction
|
||||
if (targetIndex < 0 || targetIndex >= selectedKnowledgeIds.length) return
|
||||
const next = [...selectedKnowledgeIds]
|
||||
const current = next[index]
|
||||
next[index] = next[targetIndex]
|
||||
next[targetIndex] = current
|
||||
setSelectedKnowledgeIds(next)
|
||||
}
|
||||
|
||||
function addDirectTool(value: string) {
|
||||
const option = directToolOptions.find((item) => item.value === value)
|
||||
if (!option) return
|
||||
@@ -390,7 +364,6 @@ export function AIAgentConfigWorkbench({
|
||||
handoffMode: Number(handoffMode),
|
||||
fallbackMode: Number(fallbackMode),
|
||||
fallbackMessage: fallbackMessage.trim(),
|
||||
knowledgeIds: uniqueNumbers(selectedKnowledgeIds),
|
||||
skillIds: uniqueNumbers(selectedSkillIds),
|
||||
directTools,
|
||||
}
|
||||
@@ -505,7 +478,6 @@ export function AIAgentConfigWorkbench({
|
||||
{ key: "workflow", title: "会话流程", icon: <GitBranchIcon /> },
|
||||
]
|
||||
|
||||
const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions)
|
||||
const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions)
|
||||
const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions)
|
||||
const workflowPublished = isWorkflowPublished(agent)
|
||||
@@ -683,59 +655,6 @@ export function AIAgentConfigWorkbench({
|
||||
</ConfigSection>
|
||||
) : null}
|
||||
|
||||
{activeSection === "capabilities" ? (
|
||||
<ConfigSection>
|
||||
<AddRow
|
||||
value={knowledgeToAdd}
|
||||
options={knowledgeOptions.filter((option) => !selectedKnowledgeIds.includes(Number(option.value)))}
|
||||
placeholder="选择知识库"
|
||||
onValueChange={setKnowledgeToAdd}
|
||||
onAdd={() => {
|
||||
addSelected(knowledgeToAdd, selectedKnowledgeIds, setSelectedKnowledgeIds)
|
||||
setKnowledgeToAdd("")
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-2 rounded-md border p-3">
|
||||
{selectedKnowledgeOptions.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">至少选择一个知识库。</div>
|
||||
) : (
|
||||
selectedKnowledgeOptions.map((option, index) => (
|
||||
<div key={option.value} className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="min-w-8 justify-center">{index + 1}</Badge>
|
||||
<div className="flex-1 text-sm">{option.label}</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
disabled={index === 0}
|
||||
onClick={() => moveKnowledge(index, -1)}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
disabled={index === selectedKnowledgeOptions.length - 1}
|
||||
onClick={() => moveKnowledge(index, 1)}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => setSelectedKnowledgeIds((current) => current.filter((id) => id !== Number(option.value)))}
|
||||
>
|
||||
<Trash2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ConfigSection>
|
||||
) : null}
|
||||
|
||||
{activeSection === "capabilities" ? (
|
||||
<ConfigSection>
|
||||
<AddRow
|
||||
|
||||
@@ -150,32 +150,6 @@ export default function DashboardAIAgentsPage() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "knowledge",
|
||||
label: t("aiAgent.columnKnowledge"),
|
||||
render: (item) => {
|
||||
const knowledgeIds = item.knowledgeIds ?? [];
|
||||
const knowledgeBaseNames = item.knowledgeBaseNames ?? [];
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{knowledgeIds.length === 0 ? (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("aiAgent.notConfigured")}
|
||||
</span>
|
||||
) : (
|
||||
knowledgeBaseNames.map((name, index) => (
|
||||
<Badge
|
||||
key={knowledgeIds[index] ?? `${item.id}-${index}`}
|
||||
variant="secondary"
|
||||
>
|
||||
{name}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "skills",
|
||||
label: t("aiAgent.columnSkills"),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { Trash2Icon } from "lucide-react"
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react"
|
||||
import { ArrowDownIcon, ArrowUpIcon, Trash2Icon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { OptionCombobox } from "@/components/option-combobox"
|
||||
import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
import { fetchKnowledgeBasesAll, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type KnowledgeBase } from "@/lib/api/admin"
|
||||
import { Status } from "@/lib/generated/enums"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { VariableSelector } from "./variable-selector"
|
||||
@@ -177,6 +178,13 @@ export function NodeConfigPanel({
|
||||
outputContent={outputFields}
|
||||
/>
|
||||
|
||||
{node.type === "knowledge_retrieve" ? (
|
||||
<KnowledgeRetrieveConfigPanel
|
||||
config={config}
|
||||
onChange={(nextConfig) => updateConfig(nextConfig)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showConditionBranches && (node.type === "condition" || branches.length > 0) ? (
|
||||
<ConditionBranchesEditor
|
||||
branches={branches}
|
||||
@@ -277,6 +285,136 @@ export function ConditionBranchConfigPanel({
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeRetrieveConfigPanel({
|
||||
config,
|
||||
onChange,
|
||||
}: {
|
||||
config: Record<string, unknown>
|
||||
onChange: (config: Record<string, unknown>) => void
|
||||
}) {
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [knowledgeToAdd, setKnowledgeToAdd] = useState("")
|
||||
const selectedKnowledgeIds = normalizeKnowledgeBaseIds(config.knowledgeBaseIds)
|
||||
const knowledgeOptions = useMemo(
|
||||
() => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })),
|
||||
[knowledgeBases]
|
||||
)
|
||||
const selectedKnowledgeOptions = selectedKnowledgeIds
|
||||
.map((id) => knowledgeOptions.find((option) => Number(option.value) === id))
|
||||
.filter((option): option is { value: string; label: string } => Boolean(option))
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
fetchKnowledgeBasesAll({ status: Status.Ok })
|
||||
.then((items) => {
|
||||
if (!cancelled) {
|
||||
setKnowledgeBases(items ?? [])
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setKnowledgeBases([])
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const updateKnowledgeBaseIds = (ids: number[]) => {
|
||||
onChange({ ...config, knowledgeBaseIds: uniquePositiveNumbers(ids) })
|
||||
}
|
||||
const addKnowledgeBase = () => {
|
||||
const id = Number(knowledgeToAdd)
|
||||
if (!Number.isFinite(id) || id <= 0 || selectedKnowledgeIds.includes(id)) return
|
||||
updateKnowledgeBaseIds([...selectedKnowledgeIds, id])
|
||||
setKnowledgeToAdd("")
|
||||
}
|
||||
const moveKnowledgeBase = (index: number, direction: -1 | 1) => {
|
||||
const targetIndex = index + direction
|
||||
if (targetIndex < 0 || targetIndex >= selectedKnowledgeIds.length) return
|
||||
const next = [...selectedKnowledgeIds]
|
||||
const current = next[index]
|
||||
next[index] = next[targetIndex]
|
||||
next[targetIndex] = current
|
||||
updateKnowledgeBaseIds(next)
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectorSection title="节点配置" meta={`${selectedKnowledgeIds.length} 个知识库`}>
|
||||
<InspectorRow label="知识库" required>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||
<OptionCombobox
|
||||
value={knowledgeToAdd}
|
||||
options={knowledgeOptions.filter((option) => !selectedKnowledgeIds.includes(Number(option.value)))}
|
||||
placeholder="选择知识库"
|
||||
searchPlaceholder="搜索知识库"
|
||||
emptyText="没有可用知识库"
|
||||
triggerClassName={inspectorComboboxClassName}
|
||||
onChange={setKnowledgeToAdd}
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" className="h-8" onClick={addKnowledgeBase}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100 rounded-md border border-slate-200">
|
||||
{selectedKnowledgeOptions.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-slate-500">
|
||||
未选择知识库,流程发布校验不会通过。
|
||||
</div>
|
||||
) : (
|
||||
selectedKnowledgeOptions.map((option, index) => (
|
||||
<div key={option.value} className="grid grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-2 px-2 py-1.5">
|
||||
<span className="inline-flex h-5 items-center justify-center rounded-sm border border-slate-200 bg-slate-50 font-mono text-xs text-slate-500">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="truncate text-sm text-slate-700">{option.label}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
disabled={index === 0}
|
||||
onClick={() => moveKnowledgeBase(index, -1)}
|
||||
aria-label="上移知识库"
|
||||
>
|
||||
<ArrowUpIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
disabled={index === selectedKnowledgeOptions.length - 1}
|
||||
onClick={() => moveKnowledgeBase(index, 1)}
|
||||
aria-label="下移知识库"
|
||||
>
|
||||
<ArrowDownIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-slate-500 hover:text-destructive"
|
||||
onClick={() => updateKnowledgeBaseIds(selectedKnowledgeIds.filter((id) => id !== Number(option.value)))}
|
||||
aria-label="移除知识库"
|
||||
>
|
||||
<Trash2Icon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</InspectorRow>
|
||||
</InspectorSection>
|
||||
)
|
||||
}
|
||||
|
||||
function ConditionBranchesEditor({
|
||||
branches,
|
||||
nodes,
|
||||
@@ -639,3 +777,18 @@ function stringifyConditionRight(value: unknown) {
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function normalizeKnowledgeBaseIds(value: unknown) {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
return uniquePositiveNumbers(
|
||||
value
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isFinite(item))
|
||||
)
|
||||
}
|
||||
|
||||
function uniquePositiveNumbers(input: number[]) {
|
||||
return Array.from(new Set(input.filter((item) => item > 0)))
|
||||
}
|
||||
|
||||
@@ -140,6 +140,27 @@ describe("validateWorkflowDefinition", () => {
|
||||
|
||||
assert.deepEqual(plain(result), { valid: true, errors: [] })
|
||||
})
|
||||
|
||||
it("rejects knowledge retrieve nodes without node knowledge bases", async () => {
|
||||
const { createRefValue, validateWorkflowDefinition } = await loadModule()
|
||||
|
||||
const result = validateWorkflowDefinition({
|
||||
schemaVersion: 2,
|
||||
nodes: [
|
||||
workflowNode("start_1", "start"),
|
||||
workflowNode("retrieve_1", "knowledge_retrieve", { x: 240, y: 0 }, {
|
||||
title: "知识检索",
|
||||
inputsValues: { query: createRefValue("start_1", "userMessage") },
|
||||
config: { knowledgeBaseIds: [] },
|
||||
}),
|
||||
workflowNode("end_1", "end", { x: 480, y: 0 }),
|
||||
],
|
||||
edges: [workflowEdge("start_1", "retrieve_1"), workflowEdge("retrieve_1", "end_1")],
|
||||
})
|
||||
|
||||
assert.equal(result.valid, false)
|
||||
assert.match(result.errors.join("\n"), /需要选择至少一个知识库/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createWorkflowNodeFromSpec", () => {
|
||||
|
||||
@@ -179,6 +179,15 @@ export function validateWorkflowDefinition(
|
||||
if (!node.type?.trim()) {
|
||||
errors.push(`node type is required: ${node.id}`)
|
||||
}
|
||||
if (node.type === "knowledge_retrieve") {
|
||||
const config = normalizeNodeConfig(node.data?.config)
|
||||
const knowledgeBaseIds = Array.isArray(config.knowledgeBaseIds) ? config.knowledgeBaseIds : []
|
||||
if (knowledgeBaseIds.length === 0) {
|
||||
errors.push(`${getNodeTitle(node, nodeSpecs)} 需要选择至少一个知识库`)
|
||||
} else if (knowledgeBaseIds.some((id) => Number(id) <= 0)) {
|
||||
errors.push(`${getNodeTitle(node, nodeSpecs)} 知识库 ID 必须大于 0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
|
||||
@@ -227,8 +227,6 @@ export type AIAgent = {
|
||||
fallbackMode: number
|
||||
fallbackModeName: string
|
||||
fallbackMessage: string
|
||||
knowledgeIds: number[]
|
||||
knowledgeBaseNames: string[]
|
||||
skillIds: number[]
|
||||
skills: { id: number; name: string }[]
|
||||
directTools: {
|
||||
@@ -262,7 +260,6 @@ export type CreateAIAgentPayload = {
|
||||
handoffMode: number
|
||||
fallbackMode: number
|
||||
fallbackMessage: string
|
||||
knowledgeIds: number[]
|
||||
skillIds: number[]
|
||||
directTools: {
|
||||
toolCode: string
|
||||
|
||||
Reference in New Issue
Block a user