refactor: remove graph tools references and related logic from AIAgent and workflows

This commit is contained in:
mlogclub
2026-06-26 18:56:09 +08:00
parent 015b57bfe7
commit d4ff0980c4
14 changed files with 48 additions and 199 deletions
@@ -2,9 +2,7 @@ package runtime
import (
"encoding/json"
"strings"
"agent-desk/internal/ai/workflow/compiler"
"agent-desk/internal/ai/workflow/dsl"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
@@ -16,7 +14,6 @@ import (
type resolvedWorkflow struct {
Definition dsl.Definition
Compiled compiler.Result
WorkflowID int64
VersionID int64
}
@@ -35,7 +32,6 @@ func resolveAgentWorkflow(aiAgent models.AIAgent) (resolvedWorkflow, error) {
}
return resolvedWorkflow{
Definition: def,
Compiled: compiler.Compile(def),
WorkflowID: version.WorkflowID,
VersionID: version.ID,
}, nil
@@ -46,15 +42,5 @@ func prepareWorkflowAgent(aiAgent models.AIAgent) (models.AIAgent, resolvedWorkf
if err != nil {
return aiAgent, resolvedWorkflow{}, err
}
if strings.TrimSpace(workflow.Compiled.Appendix) == "" {
return aiAgent, workflow, nil
}
prompt := strings.TrimSpace(aiAgent.SystemPrompt)
appendix := strings.TrimSpace(workflow.Compiled.Appendix)
if prompt == "" {
aiAgent.SystemPrompt = appendix
return aiAgent, workflow, nil
}
aiAgent.SystemPrompt = prompt + "\n\n" + appendix
return aiAgent, workflow, nil
}
@@ -46,6 +46,40 @@ func TestToWorkflowSummaryPreservesInterruptCheckpoint(t *testing.T) {
}
}
func TestPrepareWorkflowAgentDoesNotInjectWorkflowAppendix(t *testing.T) {
db := setupWorkflowResumeTestDB(t)
definitionJSON := mustMarshalDefinition(t, dsl.Definition{
SchemaVersion: 1,
EntryNodeID: "start",
Nodes: []dsl.Node{
{ID: "start", Type: workflowregistry.NodeTypeStart, Name: "Start"},
{ID: "handoff", Type: workflowregistry.NodeTypeHandoffToHuman, Name: "Handoff"},
},
Edges: []dsl.Edge{{ID: "edge_start_handoff", Source: "start", Target: "handoff"}},
})
version := models.AIWorkflowVersion{
WorkflowID: 1,
Version: 1,
Status: enums.StatusOk,
Definition: definitionJSON,
}
if err := db.Create(&version).Error; err != nil {
t.Fatalf("create workflow version: %v", err)
}
agent, _, err := prepareWorkflowAgent(models.AIAgent{
ID: 1,
SystemPrompt: "保持简洁回答。",
WorkflowVersionID: version.ID,
})
if err != nil {
t.Fatalf("prepareWorkflowAgent() error = %v", err)
}
if agent.SystemPrompt != "保持简洁回答。" {
t.Fatalf("expected system prompt to stay unchanged, got %q", agent.SystemPrompt)
}
}
func TestServiceResumeUsesWorkflowCheckpointData(t *testing.T) {
db := setupWorkflowResumeTestDB(t)
def := runtimeHumanConfirmDefinition()
-60
View File
@@ -1,60 +0,0 @@
package compiler
import (
"fmt"
"strings"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
"agent-desk/internal/pkg/toolx"
)
type Result struct {
ToolCodes []string
Appendix string
}
func Compile(def dsl.Definition) Result {
toolCodes := make([]string, 0)
lines := make([]string, 0, len(def.Nodes)+2)
if strings.TrimSpace(def.EntryNodeID) != "" {
lines = append(lines, fmt.Sprintf("Workflow entry node: %s.", strings.TrimSpace(def.EntryNodeID)))
}
for _, node := range def.Nodes {
nodeType := strings.TrimSpace(node.Type)
if code := graphToolCodeForNodeType(nodeType); code != "" {
toolCodes = append(toolCodes, code)
}
nodeName := strings.TrimSpace(node.Name)
if nodeName == "" {
nodeName = strings.TrimSpace(node.ID)
}
if nodeName == "" {
continue
}
lines = append(lines, fmt.Sprintf("- %s: %s", nodeName, nodeType))
}
appendix := ""
if len(lines) > 0 {
appendix = "Published customer-service workflow:\n" + strings.Join(lines, "\n")
}
return Result{
ToolCodes: toolx.NormalizeToolCodes(toolCodes),
Appendix: appendix,
}
}
func graphToolCodeForNodeType(nodeType string) string {
switch strings.TrimSpace(nodeType) {
case workflowregistry.NodeTypeAnalyzeConversation:
return toolx.GraphAnalyzeConversation.Code
case workflowregistry.NodeTypePrepareTicketDraft:
return toolx.GraphPrepareTicketDraft.Code
case workflowregistry.NodeTypeCreateTicket:
return toolx.GraphCreateTicketConfirm.Code
case workflowregistry.NodeTypeHandoffToHuman:
return toolx.GraphHandoffConversation.Code
default:
return ""
}
}
@@ -1,39 +0,0 @@
package compiler
import (
"testing"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
"agent-desk/internal/pkg/toolx"
)
func TestCompileMapsWorkflowNodesToGraphTools(t *testing.T) {
result := Compile(dsl.Definition{
EntryNodeID: "start",
Nodes: []dsl.Node{
{ID: "start", Type: workflowregistry.NodeTypeStart, Name: "Start"},
{ID: "analyze", Type: workflowregistry.NodeTypeAnalyzeConversation, Name: "Analyze"},
{ID: "draft", Type: workflowregistry.NodeTypePrepareTicketDraft, Name: "Draft"},
{ID: "create", Type: workflowregistry.NodeTypeCreateTicket, Name: "Create"},
{ID: "handoff", Type: workflowregistry.NodeTypeHandoffToHuman, Name: "Handoff"},
},
})
want := []string{
toolx.GraphAnalyzeConversation.Code,
toolx.GraphPrepareTicketDraft.Code,
toolx.GraphCreateTicketConfirm.Code,
toolx.GraphHandoffConversation.Code,
}
if len(result.ToolCodes) != len(want) {
t.Fatalf("expected %d tool codes, got %d: %#v", len(want), len(result.ToolCodes), result.ToolCodes)
}
for i, item := range want {
if result.ToolCodes[i] != item {
t.Fatalf("tool code[%d] = %s, want %s", i, result.ToolCodes[i], item)
}
}
if result.Appendix == "" {
t.Fatalf("expected workflow appendix")
}
}
@@ -188,7 +188,6 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
Skills: make([]response.AIAgentSkillResponse, 0),
Teams: make([]response.AIAgentTeamResponse, 0),
DirectTools: make([]response.AIAgentMCPToolResponse, 0),
GraphTools: make([]string, 0),
WorkflowVersionID: item.WorkflowVersionID,
WorkflowPublished: item.WorkflowVersionID > 0,
WorkflowState: aiAgentWorkflowState(item.WorkflowVersionID),
@@ -236,7 +235,6 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
continue
}
if toolx.IsAgentDirectGraphToolCode(toolCode) {
ret.GraphTools = appendGraphToolCodeIfMissing(ret.GraphTools, toolCode)
continue
}
serverCode := strings.TrimSpace(tool.ServerCode)
@@ -271,18 +269,6 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
}
}
}
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
}
@@ -299,16 +285,3 @@ func aiAgentWorkflowStateText(workflowVersionID int64) string {
}
return "未发布"
}
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
@@ -524,7 +524,6 @@ 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。
WorkflowVersionID int64 `gorm:"type:bigint;not null;default:0;index"` // WorkflowVersionID 为绑定的已发布会话流程版本ID。
SortNo int `gorm:"type:int;not null;default:0;index"` // SortNo 为后台展示排序号。
AuditFields
-1
View File
@@ -57,7 +57,6 @@ type CreateAIAgentRequest struct {
KnowledgeIDs []int64 `json:"knowledgeIds"`
SkillIDs []int64 `json:"skillIds"`
DirectTools []AIAgentMCPToolRequest `json:"directTools"`
GraphTools []string `json:"graphTools"`
}
type UpdateAIAgentRequest struct {
-1
View File
@@ -90,7 +90,6 @@ type AIAgentResponse struct {
SkillIDs []int64 `json:"skillIds"`
Skills []AIAgentSkillResponse `json:"skills"`
DirectTools []AIAgentMCPToolResponse `json:"directTools"`
GraphTools []string `json:"graphTools"`
WorkflowVersionID int64 `json:"workflowVersionId"`
WorkflowPublished bool `json:"workflowPublished"`
WorkflowState string `json:"workflowState"`
-37
View File
@@ -113,7 +113,6 @@ 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,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
@@ -193,10 +192,6 @@ 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)
@@ -205,14 +200,6 @@ 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.InvalidParamI18n("error.e0028")
}
graphToolsJSON = string(buf)
}
return &models.AIAgent{
Name: name,
Description: strings.TrimSpace(req.Description),
@@ -228,7 +215,6 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
KnowledgeIDs: utils.JoinInt64s(knowledgeIDs),
SkillIDs: utils.JoinInt64s(skillIDs),
AllowedMCPTools: directToolsJSON,
AllowedGraphTools: graphToolsJSON,
WorkflowVersionID: 0,
}, nil
}
@@ -333,29 +319,6 @@ 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.InvalidParamI18n("error.e0027")
}
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 {
@@ -4,6 +4,8 @@ import { readFile } from "node:fs/promises"
const configWorkbenchSource = await readFile(new URL("./config-workbench.tsx", import.meta.url), "utf8")
const zhMessagesSource = await readFile(new URL("../../../../messages/zh-CN.json", import.meta.url), "utf8")
const adminApiSource = await readFile(new URL("../../../../lib/api/admin.ts", import.meta.url), "utf8")
const zhMessages = JSON.parse(zhMessagesSource)
test("AI Agent workflow-era policy copy separates handoff execution from knowledge fallback", () => {
const combinedSource = `${configWorkbenchSource}\n${zhMessagesSource}`
@@ -19,3 +21,13 @@ test("AI Agent workflow-era policy copy separates handoff execution from knowled
assert.doesNotMatch(configWorkbenchSource, /兜底策略/)
assert.doesNotMatch(configWorkbenchSource, /兜底文案/)
})
test("AI Agent config no longer exposes legacy graph tool routing knobs", () => {
const aiAgentMessages = JSON.stringify(zhMessages.aiAgent ?? {})
assert.doesNotMatch(configWorkbenchSource, /graphTools/)
assert.doesNotMatch(adminApiSource, /graphTools/)
assert.doesNotMatch(aiAgentMessages, /graphTools/)
assert.doesNotMatch(aiAgentMessages, /Graph Tool/)
assert.doesNotMatch(aiAgentMessages, /内置流程/)
})
@@ -396,7 +396,6 @@ export function AIAgentConfigWorkbench({
knowledgeIds: uniqueNumbers(selectedKnowledgeIds),
skillIds: uniqueNumbers(selectedSkillIds),
directTools,
graphTools: [],
}
}
-2
View File
@@ -239,7 +239,6 @@ export type AIAgent = {
description: string
arguments?: Record<string, string>
}[]
graphTools: string[]
workflowVersionId: number
workflowPublished: boolean
workflowState: string
@@ -273,7 +272,6 @@ export type CreateAIAgentPayload = {
description: string
arguments?: Record<string, string>
}[]
graphTools: string[]
}
export type UpdateAIAgentPayload = CreateAIAgentPayload & {
+1 -8
View File
@@ -1090,7 +1090,6 @@
"knowledgeTab": "Knowledge",
"skillsTab": "Skills",
"directToolsTab": "MCP tools",
"graphToolsTab": "Graph Tools",
"knowledgeHint": "Select at least one knowledge base. You can adjust priority order.",
"selectKnowledge": "Select and add knowledge base",
"searchKnowledge": "Search knowledge bases",
@@ -1117,13 +1116,7 @@
"searchDirectTool": "Search Direct Tools",
"emptyDirectTool": "No Direct Tools available",
"noDirectToolsHint": "Without Direct Tools, the agent will not call external or built-in tools directly. It will rely on knowledge bases, skills, and normal replies.",
"removeDirectTool": "Remove Direct Tool {name}",
"graphToolsHint": "Use for built-in workflows such as ticket creation and human handoff. These are kept separate from Direct Tools.",
"selectGraphTool": "Select Graph Tool",
"searchGraphTool": "Search Graph Tools",
"emptyGraphTool": "No Graph Tools available",
"noGraphToolsHint": "Without Graph Tools, the agent will not expose built-in workflow tools such as ticket creation or human handoff.",
"removeGraphTool": "Remove Graph Tool {name}"
"removeDirectTool": "Remove Direct Tool {name}"
},
"aiConfig": {
"allStatuses": "All statuses",
+1 -8
View File
@@ -1090,7 +1090,6 @@
"knowledgeTab": "知识库",
"skillsTab": "Skills",
"directToolsTab": "MCP tools",
"graphToolsTab": "内置流程",
"knowledgeHint": "至少选择一个知识库,可调整知识库优先级。",
"selectKnowledge": "选择并添加知识库",
"searchKnowledge": "搜索知识库",
@@ -1117,13 +1116,7 @@
"searchDirectTool": "搜索 Direct Tool",
"emptyDirectTool": "没有可添加的 Direct Tool",
"noDirectToolsHint": "不配置 Direct Tool 时,Agent 不会直接调用外部或内置工具,只会依赖知识库、Skill 和普通回复。",
"removeDirectTool": "移除 Direct Tool {name}",
"graphToolsHint": "用于建单、转人工等系统内置流程,不再混放到 Direct Tools 中。",
"selectGraphTool": "选择 Graph Tool",
"searchGraphTool": "搜索 Graph Tool",
"emptyGraphTool": "没有可添加的 Graph Tool",
"noGraphToolsHint": "不配置 Graph Tool 时,Agent 不会暴露建单/转人工等内置流程工具。",
"removeGraphTool": "移除 Graph Tool {name}"
"removeDirectTool": "移除 Direct Tool {name}"
},
"aiConfig": {
"allStatuses": "全部状态",