From b7056b59b530b76ecb64d9593397729241f7f80b Mon Sep 17 00:00:00 2001 From: mlogclub Date: Thu, 25 Jun 2026 18:50:38 +0800 Subject: [PATCH] feat: add default workflow definition endpoint and related functionality --- internal/bootstrap/routes.go | 1 + internal/bootstrap/server_route_test.go | 1 + .../handlers/dashboard/ai_workflow_handler.go | 8 + .../ai_agent_workflow_service_test.go | 20 ++ internal/services/ai_workflow_service.go | 4 + .../_components/config-workbench.tsx | 255 +----------------- web/lib/api/admin.ts | 4 + 7 files changed, 48 insertions(+), 245 deletions(-) diff --git a/internal/bootstrap/routes.go b/internal/bootstrap/routes.go index 5f427ac..1712958 100644 --- a/internal/bootstrap/routes.go +++ b/internal/bootstrap/routes.go @@ -230,6 +230,7 @@ func registerDashboardAIAgentRoutes(group *gin.RouterGroup) { func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) { group.GET("/node-spec/list", dashboard.AIWorkflowGetNodeSpecList) + group.GET("/default-definition", dashboard.AIWorkflowGetDefaultDefinition) group.POST("/validate", dashboard.AIWorkflowPostValidate) group.Any("/run/list", dashboard.AIWorkflowAnyRunList) group.GET("/run/:id", dashboard.AIWorkflowGetRunBy) diff --git a/internal/bootstrap/server_route_test.go b/internal/bootstrap/server_route_test.go index 44e954e..94b554a 100644 --- a/internal/bootstrap/server_route_test.go +++ b/internal/bootstrap/server_route_test.go @@ -41,6 +41,7 @@ func TestNewServerRegistersGinRoutes(t *testing.T) { http.MethodGet + " /api/dashboard/user/:id", http.MethodPost + " /api/dashboard/user/create", http.MethodPost + " /api/dashboard/conversation/send_message", + http.MethodGet + " /api/dashboard/ai-workflow/default-definition", http.MethodGet + " /api/dashboard/ai-workflow/run/list", http.MethodGet + " /api/dashboard/ai-workflow/run/:id", http.MethodGet + " /api/ws/dashboard", diff --git a/internal/handlers/dashboard/ai_workflow_handler.go b/internal/handlers/dashboard/ai_workflow_handler.go index 3fff14b..536002d 100644 --- a/internal/handlers/dashboard/ai_workflow_handler.go +++ b/internal/handlers/dashboard/ai_workflow_handler.go @@ -108,6 +108,14 @@ func AIWorkflowGetNodeSpecList(ctx *gin.Context) { httpx.WriteJSON(ctx, builders.BuildAIWorkflowNodeSpecs(services.AIWorkflowService.ListNodeSpecs())) } +func AIWorkflowGetDefaultDefinition(ctx *gin.Context) { + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, services.AIWorkflowService.DefaultAgentWorkflowDefinition()) +} + func AIWorkflowPostValidate(ctx *gin.Context) { if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { httpx.WriteJSON(ctx, err) diff --git a/internal/services/ai_agent_workflow_service_test.go b/internal/services/ai_agent_workflow_service_test.go index d1c32fb..902683e 100644 --- a/internal/services/ai_agent_workflow_service_test.go +++ b/internal/services/ai_agent_workflow_service_test.go @@ -82,6 +82,26 @@ func TestAIAgentServiceCreatesDefaultWorkflow(t *testing.T) { } } +func TestAIWorkflowServiceDefaultAgentWorkflowDefinitionIsValid(t *testing.T) { + definition := AIWorkflowService.DefaultAgentWorkflowDefinition() + if definition.EntryNodeID == "" { + t.Fatalf("expected default workflow definition") + } + validation := workflowvalidator.ValidateDefinition(definition, workflowregistry.DefaultRegistry()) + if !validation.Valid { + t.Fatalf("expected default workflow definition to be valid, got %#v", validation.Errors) + } + if nodeTypeByID(definition, "route_intent_1") != workflowregistry.NodeTypeCondition { + t.Fatalf("expected default workflow to include intent router, got nodes: %#v", definition.Nodes) + } + if !workflowHasNodeType(definition, workflowregistry.NodeTypeHandoffToHuman) { + t.Fatalf("expected default workflow to include human handoff node") + } + if !workflowHasNodeType(definition, workflowregistry.NodeTypeCreateTicket) { + t.Fatalf("expected default workflow to include ticket creation node") + } +} + func TestAIWorkflowServicePublishAgentWorkflowBindsAgentVersion(t *testing.T) { setupAIAgentWorkflowTestDB(t) operator := aiAgentWorkflowTestOperator() diff --git a/internal/services/ai_workflow_service.go b/internal/services/ai_workflow_service.go index be4a465..9193910 100644 --- a/internal/services/ai_workflow_service.go +++ b/internal/services/ai_workflow_service.go @@ -190,6 +190,10 @@ func (s *aiWorkflowService) ListNodeSpecs() []workflowregistry.NodeSpec { return s.registry.List() } +func (s *aiWorkflowService) DefaultAgentWorkflowDefinition() dsl.Definition { + return defaultAgentWorkflowDefinition() +} + func (s *aiWorkflowService) ValidateDefinition(def dsl.Definition) workflowvalidator.Result { return workflowvalidator.ValidateDefinition(def, s.registry) } diff --git a/web/app/dashboard/ai-agents/_components/config-workbench.tsx b/web/app/dashboard/ai-agents/_components/config-workbench.tsx index d06f376..4858ac3 100644 --- a/web/app/dashboard/ai-agents/_components/config-workbench.tsx +++ b/web/app/dashboard/ai-agents/_components/config-workbench.tsx @@ -40,6 +40,7 @@ import { fetchAIAgent, fetchAIAgentWorkflow, fetchAIConfigsAll, + fetchAIWorkflowDefaultDefinition, fetchAIWorkflowNodeSpecs, fetchAIWorkflowVersions, fetchAgentTeamsAll, @@ -93,7 +94,7 @@ type SectionKey = | "handoff" | "versions" -const emptyDefinition: AIWorkflowDefinition = { +const fallbackDefinition: AIWorkflowDefinition = { schemaVersion: 1, entryNodeId: "start_1", nodes: [ @@ -101,256 +102,18 @@ const emptyDefinition: AIWorkflowDefinition = { id: "start_1", type: "start", name: "开始", - position: { x: 0, y: 260 }, + position: { x: 0, y: 80 }, config: {}, }, - { - id: "route_intent_1", - type: "condition", - name: "意图分流", - position: { x: 260, y: 260 }, - config: {}, - }, - { - id: "handoff_1", - type: "handoff_to_human", - name: "转人工", - position: { x: 560, y: 80 }, - config: {}, - inputs: { - reason: { nodeId: "start_1", field: "userMessage" }, - }, - }, - { - id: "handoff_end_1", - type: "end", - name: "结束", - position: { x: 860, y: 80 }, - config: {}, - }, - { - id: "draft_ticket_1", - type: "prepare_ticket_draft", - name: "整理工单草稿", - position: { x: 560, y: 240 }, - config: {}, - inputs: { - issue: { nodeId: "start_1", field: "userMessage" }, - }, - }, - { - id: "ticket_confirm_prompt_1", - type: "llm_reply", - name: "建单确认文案", - position: { x: 860, y: 240 }, - config: { - staticReply: "我已整理工单草稿。请回复“确认”创建工单,或回复“取消”放弃。", - }, - inputs: { - userMessage: { nodeId: "start_1", field: "userMessage" }, - }, - }, - { - id: "ticket_confirm_1", - type: "human_confirm", - name: "确认建单", - position: { x: 1160, y: 240 }, - config: {}, - inputs: { - prompt: { nodeId: "ticket_confirm_prompt_1", field: "replyText" }, - }, - }, - { - id: "create_ticket_1", - type: "create_ticket", - name: "创建工单", - position: { x: 1460, y: 180 }, - config: {}, - inputs: { - ticketDraft: { nodeId: "draft_ticket_1", field: "ticketDraft" }, - confirmed: { nodeId: "ticket_confirm_1", field: "confirmed" }, - }, - }, - { - id: "ticket_result_reply_1", - type: "send_reply", - name: "发送建单结果", - position: { x: 1760, y: 180 }, - config: {}, - inputs: { - replyText: { nodeId: "create_ticket_1", field: "message" }, - }, - }, - { - id: "ticket_cancel_reply_1", - type: "llm_reply", - name: "取消建单提示", - position: { x: 1460, y: 320 }, - config: { - staticReply: "已取消创建工单。你可以继续补充问题,我会继续帮你处理。", - }, - inputs: { - userMessage: { nodeId: "start_1", field: "userMessage" }, - }, - }, - { - id: "send_ticket_cancel_1", - type: "send_reply", - name: "发送取消提示", - position: { x: 1760, y: 320 }, - config: {}, - inputs: { - replyText: { nodeId: "ticket_cancel_reply_1", field: "replyText" }, - }, - }, - { - id: "retrieve_1", - type: "knowledge_retrieve", - name: "知识检索", - position: { x: 560, y: 500 }, - config: {}, - inputs: { - query: { nodeId: "start_1", field: "userMessage" }, - }, - }, - { - id: "answerability_1", - type: "answerability_gate", - name: "可回答判断", - position: { x: 860, y: 500 }, - config: {}, - inputs: { - userMessage: { nodeId: "start_1", field: "userMessage" }, - knowledgeItems: { nodeId: "retrieve_1", field: "items" }, - }, - }, - { - id: "reply_1", - type: "llm_reply", - name: "AI 回复", - position: { x: 1160, y: 440 }, - config: {}, - inputs: { - userMessage: { nodeId: "start_1", field: "userMessage" }, - knowledgeItems: { nodeId: "retrieve_1", field: "items" }, - }, - }, - { - id: "send_1", - type: "send_reply", - name: "发送回复", - position: { x: 1460, y: 440 }, - config: {}, - inputs: { - replyText: { nodeId: "reply_1", field: "replyText" }, - }, - }, - { - id: "fallback_reply_1", - type: "llm_reply", - name: "兜底追问", - position: { x: 1160, y: 600 }, - config: {}, - inputs: { - userMessage: { nodeId: "start_1", field: "userMessage" }, - knowledgeItems: { nodeId: "retrieve_1", field: "items" }, - }, - }, - { - id: "send_fallback_1", - type: "send_reply", - name: "发送兜底", - position: { x: 1460, y: 600 }, - config: {}, - inputs: { - replyText: { nodeId: "fallback_reply_1", field: "replyText" }, - }, - }, { id: "end_1", type: "end", name: "结束", - position: { x: 2060, y: 440 }, + position: { x: 260, y: 80 }, config: {}, }, ], - edges: [ - { id: "edge_start_route_intent", source: "start_1", target: "route_intent_1" }, - { - id: "edge_intent_handoff", - source: "route_intent_1", - target: "handoff_1", - condition: { - left: { nodeId: "start_1", field: "userMessage" }, - operator: "contains", - right: "人工", - }, - }, - { - id: "edge_intent_ticket", - source: "route_intent_1", - target: "draft_ticket_1", - condition: { - left: { nodeId: "start_1", field: "userMessage" }, - operator: "contains", - right: "工单", - }, - }, - { - id: "edge_intent_complaint", - source: "route_intent_1", - target: "draft_ticket_1", - condition: { - left: { nodeId: "start_1", field: "userMessage" }, - operator: "contains", - right: "投诉", - }, - }, - { - id: "edge_intent_incident", - source: "route_intent_1", - target: "draft_ticket_1", - condition: { - left: { nodeId: "start_1", field: "userMessage" }, - operator: "contains", - right: "报障", - }, - }, - { id: "edge_intent_knowledge_default", source: "route_intent_1", target: "retrieve_1" }, - { id: "edge_handoff_end", source: "handoff_1", target: "handoff_end_1" }, - { id: "edge_draft_ticket_confirm_prompt", source: "draft_ticket_1", target: "ticket_confirm_prompt_1" }, - { id: "edge_ticket_prompt_confirm", source: "ticket_confirm_prompt_1", target: "ticket_confirm_1" }, - { - id: "edge_ticket_confirm_create", - source: "ticket_confirm_1", - target: "create_ticket_1", - condition: { - left: { nodeId: "ticket_confirm_1", field: "confirmed" }, - operator: "is_true", - }, - }, - { id: "edge_ticket_confirm_cancel", source: "ticket_confirm_1", target: "ticket_cancel_reply_1" }, - { id: "edge_create_ticket_result", source: "create_ticket_1", target: "ticket_result_reply_1" }, - { id: "edge_ticket_result_end", source: "ticket_result_reply_1", target: "end_1" }, - { id: "edge_ticket_cancel_send", source: "ticket_cancel_reply_1", target: "send_ticket_cancel_1" }, - { id: "edge_ticket_cancel_end", source: "send_ticket_cancel_1", target: "end_1" }, - { id: "edge_retrieve_answerability", source: "retrieve_1", target: "answerability_1" }, - { - id: "edge_answerability_reply", - source: "answerability_1", - target: "reply_1", - condition: { - left: { nodeId: "answerability_1", field: "answerability" }, - operator: "eq", - right: "answerable", - }, - }, - { id: "edge_answerability_fallback", source: "answerability_1", target: "fallback_reply_1" }, - { id: "edge_reply_send", source: "reply_1", target: "send_1" }, - { id: "edge_fallback_send", source: "fallback_reply_1", target: "send_fallback_1" }, - { id: "edge_send_end", source: "send_1", target: "end_1" }, - { id: "edge_send_fallback_end", source: "send_fallback_1", target: "end_1" }, - ], + edges: [{ id: "edge_start_end", source: "start_1", target: "end_1" }], } function toText(value: string | number | undefined | null) { @@ -401,7 +164,7 @@ export function AIAgentConfigWorkbench({ const [selectedSkillIds, setSelectedSkillIds] = useState([]) const [directTools, setDirectTools] = useState([]) - const [definition, setDefinition] = useState(emptyDefinition) + const [definition, setDefinition] = useState(fallbackDefinition) const [aiConfigs, setAIConfigs] = useState([]) const [knowledgeBases, setKnowledgeBases] = useState([]) @@ -423,6 +186,7 @@ export function AIAgentConfigWorkbench({ try { const [ specs, + defaultDefinition, configs, bases, teams, @@ -430,6 +194,7 @@ export function AIAgentConfigWorkbench({ catalog, ] = await Promise.all([ fetchAIWorkflowNodeSpecs(), + fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition), fetchAIConfigsAll({ modelType: AIModelType.LLM }), fetchKnowledgeBasesAll({ status: Status.Ok }), fetchAgentTeamsAll(), @@ -462,7 +227,7 @@ export function AIAgentConfigWorkbench({ setSelectedTeamIds([]) setSelectedSkillIds([]) setDirectTools([]) - setDefinition(emptyDefinition) + setDefinition(defaultDefinition ?? fallbackDefinition) setValidation(null) return } @@ -494,7 +259,7 @@ export function AIAgentConfigWorkbench({ setSelectedTeamIds((agentDetail.teams ?? []).map((team) => team.id)) setSelectedSkillIds(agentDetail.skillIds ?? []) setDirectTools(agentDetail.directTools ?? []) - setDefinition(workflowDetail.draftDefinition ?? emptyDefinition) + setDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition) setValidation(null) } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to load Agent config") diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index 8a181d0..fe185fe 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -821,6 +821,10 @@ export function fetchAIWorkflowNodeSpecs() { return request("/api/dashboard/ai-workflow/node-spec/list") } +export function fetchAIWorkflowDefaultDefinition() { + return request("/api/dashboard/ai-workflow/default-definition") +} + export function fetchAIWorkflowVersions(query?: Record) { return request>( `/api/dashboard/ai-workflow/version/list${toQueryString(query)}`