feat: Enhance AI Workflow with Variable Contracts and Input Mapping
- Added ConfigSchema, InputSchema, OutputSchema, and DefaultInputs to AIWorkflowNodeSpecResponse. - Implemented BuildAIWorkflowNodeSpecs to include variable contracts for start and send_reply nodes. - Introduced applyAutoInputMappings to automatically map inputs based on node connections. - Enhanced validation to check for required input mappings in workflows. - Updated workflow editor to support variable selection for node inputs. - Translated node names and labels to Chinese for better localization. - Added tests for variable mapping and validation logic.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package registry
|
||||
|
||||
import "agent-desk/internal/ai/workflow/dsl"
|
||||
|
||||
const (
|
||||
NodeTypeStart = "start"
|
||||
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
|
||||
@@ -17,17 +19,173 @@ const (
|
||||
|
||||
func DefaultRegistry() *Registry {
|
||||
return NewRegistry(
|
||||
NodeSpec{Type: NodeTypeStart, Title: "Start", Description: "Conversation workflow entry.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypeKnowledgeRetrieve, Title: "Knowledge Retrieve", Description: "Retrieve knowledge for the current user message.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypeAnswerabilityGate, Title: "Answerability Gate", Description: "Decide whether retrieved knowledge is enough to answer.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypeLLMReply, Title: "LLM Reply", Description: "Generate a reply or structured analysis with the configured model.", RiskLevel: NodeRiskLevelMedium},
|
||||
NodeSpec{Type: NodeTypeCondition, Title: "Condition", Description: "Route by controlled workflow variables.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypeAnalyzeConversation, Title: "Analyze Conversation", Description: "Analyze intent, risk, and recommended next action.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypePrepareTicketDraft, Title: "Prepare Ticket Draft", Description: "Build a ticket draft from conversation context.", RiskLevel: NodeRiskLevelMedium},
|
||||
NodeSpec{Type: NodeTypeHumanConfirm, Title: "Human Confirm", Description: "Interrupt and wait for explicit user confirmation.", RiskLevel: NodeRiskLevelMedium, Interruptible: true},
|
||||
NodeSpec{Type: NodeTypeCreateTicket, Title: "Create Ticket", Description: "Create a ticket from a confirmed draft.", RiskLevel: NodeRiskLevelHigh, RequiresConfirmationPredecessor: true},
|
||||
NodeSpec{Type: NodeTypeHandoffToHuman, Title: "Handoff To Human", Description: "Transfer the conversation to human support.", RiskLevel: NodeRiskLevelHigh, RequiresConfirmationPredecessor: true},
|
||||
NodeSpec{Type: NodeTypeSendReply, Title: "Send Reply", Description: "Return or commit customer-visible reply text.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{Type: NodeTypeEnd, Title: "End", Description: "End workflow execution.", RiskLevel: NodeRiskLevelLow},
|
||||
NodeSpec{
|
||||
Type: NodeTypeStart,
|
||||
Title: "Start",
|
||||
Description: "Conversation workflow entry.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
OutputSchema: []VariableSpec{
|
||||
output("conversationId", VariableTypeInteger, "Conversation ID."),
|
||||
output("messageId", VariableTypeInteger, "Current user message ID."),
|
||||
output("aiAgentId", VariableTypeInteger, "AI Agent ID."),
|
||||
output("userMessage", VariableTypeString, "Current user message content."),
|
||||
output("knowledgeBaseIds", VariableTypeIntegerArray, "Knowledge bases bound to the AI Agent."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeKnowledgeRetrieve,
|
||||
Title: "Knowledge Retrieve",
|
||||
Description: "Retrieve knowledge for the current user message.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("query", VariableTypeString, "Search query."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("items", VariableTypeObjectArray, "Retrieved knowledge items."),
|
||||
output("summary", VariableTypeString, "Short retrieval summary."),
|
||||
},
|
||||
DefaultInputs: map[string]dsl.VariableSelector{
|
||||
"query": {NodeID: "start_1", Field: "userMessage"},
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeAnswerabilityGate,
|
||||
Title: "Answerability Gate",
|
||||
Description: "Decide whether retrieved knowledge is enough to answer.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("userMessage", VariableTypeString, "Current user message content."),
|
||||
requiredInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("answerability", VariableTypeString, "Answerability decision."),
|
||||
output("reason", VariableTypeString, "Decision reason."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeLLMReply,
|
||||
Title: "LLM Reply",
|
||||
Description: "Generate a reply or structured analysis with the configured model.",
|
||||
RiskLevel: NodeRiskLevelMedium,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("userMessage", VariableTypeString, "Current user message content."),
|
||||
optionalInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("replyText", VariableTypeString, "Generated reply text."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeCondition,
|
||||
Title: "Condition",
|
||||
Description: "Route by controlled workflow variables.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
OutputSchema: []VariableSpec{
|
||||
output("matched", VariableTypeBoolean, "Whether the condition matched."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeAnalyzeConversation,
|
||||
Title: "Analyze Conversation",
|
||||
Description: "Analyze intent, risk, and recommended next action.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("userMessage", VariableTypeString, "Current user message content."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("intent", VariableTypeString, "Detected user intent."),
|
||||
output("riskLevel", VariableTypeString, "Detected risk level."),
|
||||
output("needTicket", VariableTypeBoolean, "Whether a ticket is recommended."),
|
||||
output("needHumanHandoff", VariableTypeBoolean, "Whether human handoff is recommended."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypePrepareTicketDraft,
|
||||
Title: "Prepare Ticket Draft",
|
||||
Description: "Build a ticket draft from conversation context.",
|
||||
RiskLevel: NodeRiskLevelMedium,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("issue", VariableTypeString, "Issue summary."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("ticketDraft", VariableTypeObject, "Draft ticket payload."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeHumanConfirm,
|
||||
Title: "Human Confirm",
|
||||
Description: "Interrupt and wait for explicit user confirmation.",
|
||||
RiskLevel: NodeRiskLevelMedium,
|
||||
Interruptible: true,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("prompt", VariableTypeString, "Confirmation prompt."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("confirmed", VariableTypeBoolean, "Whether the user confirmed."),
|
||||
output("responseText", VariableTypeString, "Confirmation response text."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeCreateTicket,
|
||||
Title: "Create Ticket",
|
||||
Description: "Create a ticket from a confirmed draft.",
|
||||
RiskLevel: NodeRiskLevelHigh,
|
||||
RequiresConfirmationPredecessor: true,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("ticketDraft", VariableTypeObject, "Confirmed draft ticket payload."),
|
||||
requiredInput("confirmed", VariableTypeBoolean, "Confirmation result."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("ticketId", VariableTypeInteger, "Created ticket ID."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeHandoffToHuman,
|
||||
Title: "Handoff To Human",
|
||||
Description: "Transfer the conversation to human support.",
|
||||
RiskLevel: NodeRiskLevelHigh,
|
||||
RequiresConfirmationPredecessor: true,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("reason", VariableTypeString, "Handoff reason."),
|
||||
requiredInput("confirmed", VariableTypeBoolean, "Confirmation result."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("handoffId", VariableTypeInteger, "Handoff operation ID."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeSendReply,
|
||||
Title: "Send Reply",
|
||||
Description: "Return or commit customer-visible reply text.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
InputSchema: []VariableSpec{
|
||||
requiredInput("replyText", VariableTypeString, "Customer-visible reply text."),
|
||||
},
|
||||
OutputSchema: []VariableSpec{
|
||||
output("sent", VariableTypeBoolean, "Whether the reply was sent."),
|
||||
output("replyMessageId", VariableTypeInteger, "Reply message ID."),
|
||||
},
|
||||
},
|
||||
NodeSpec{
|
||||
Type: NodeTypeEnd,
|
||||
Title: "End",
|
||||
Description: "End workflow execution.",
|
||||
RiskLevel: NodeRiskLevelLow,
|
||||
OutputSchema: []VariableSpec{
|
||||
output("status", VariableTypeString, "Workflow terminal status."),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func requiredInput(name string, variableType VariableType, description string) VariableSpec {
|
||||
return VariableSpec{Name: name, Type: variableType, Required: true, Description: description}
|
||||
}
|
||||
|
||||
func optionalInput(name string, variableType VariableType, description string) VariableSpec {
|
||||
return VariableSpec{Name: name, Type: variableType, Description: description}
|
||||
}
|
||||
|
||||
func output(name string, variableType VariableType, description string) VariableSpec {
|
||||
return VariableSpec{Name: name, Type: variableType, Description: description}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package registry
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultRegistryExposesStartOutputs(t *testing.T) {
|
||||
spec, ok := DefaultRegistry().Get(NodeTypeStart)
|
||||
if !ok {
|
||||
t.Fatalf("start node spec not found")
|
||||
}
|
||||
if !hasVariable(spec.OutputSchema, "userMessage", VariableTypeString) {
|
||||
t.Fatalf("expected start output userMessage:string, got %#v", spec.OutputSchema)
|
||||
}
|
||||
if !hasVariable(spec.OutputSchema, "knowledgeBaseIds", VariableTypeIntegerArray) {
|
||||
t.Fatalf("expected start output knowledgeBaseIds:array<int>, got %#v", spec.OutputSchema)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRegistryExposesKnowledgeRetrieveInputsAndOutputs(t *testing.T) {
|
||||
spec, ok := DefaultRegistry().Get(NodeTypeKnowledgeRetrieve)
|
||||
if !ok {
|
||||
t.Fatalf("knowledge_retrieve node spec not found")
|
||||
}
|
||||
if !hasRequiredVariable(spec.InputSchema, "query", VariableTypeString) {
|
||||
t.Fatalf("expected knowledge_retrieve required input query:string, got %#v", spec.InputSchema)
|
||||
}
|
||||
if !hasVariable(spec.OutputSchema, "items", VariableTypeObjectArray) {
|
||||
t.Fatalf("expected knowledge_retrieve output items:array<object>, got %#v", spec.OutputSchema)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRegistryExposesSendReplyRequiredInput(t *testing.T) {
|
||||
spec, ok := DefaultRegistry().Get(NodeTypeSendReply)
|
||||
if !ok {
|
||||
t.Fatalf("send_reply node spec not found")
|
||||
}
|
||||
if !hasRequiredVariable(spec.InputSchema, "replyText", VariableTypeString) {
|
||||
t.Fatalf("expected send_reply required input replyText:string, got %#v", spec.InputSchema)
|
||||
}
|
||||
if !hasVariable(spec.OutputSchema, "sent", VariableTypeBoolean) {
|
||||
t.Fatalf("expected send_reply output sent:boolean, got %#v", spec.OutputSchema)
|
||||
}
|
||||
}
|
||||
|
||||
func hasRequiredVariable(items []VariableSpec, name string, variableType VariableType) bool {
|
||||
for _, item := range items {
|
||||
if item.Name == name && item.Type == variableType && item.Required {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasVariable(items []VariableSpec, name string, variableType VariableType) bool {
|
||||
for _, item := range items {
|
||||
if item.Name == name && item.Type == variableType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package registry
|
||||
|
||||
import "agent-desk/internal/ai/workflow/dsl"
|
||||
|
||||
type NodeRiskLevel string
|
||||
|
||||
const (
|
||||
@@ -8,13 +10,37 @@ const (
|
||||
NodeRiskLevelHigh NodeRiskLevel = "high"
|
||||
)
|
||||
|
||||
type VariableType string
|
||||
|
||||
const (
|
||||
VariableTypeString VariableType = "string"
|
||||
VariableTypeInteger VariableType = "integer"
|
||||
VariableTypeBoolean VariableType = "boolean"
|
||||
VariableTypeObject VariableType = "object"
|
||||
VariableTypeStringArray VariableType = "array<string>"
|
||||
VariableTypeIntegerArray VariableType = "array<int>"
|
||||
VariableTypeObjectArray VariableType = "array<object>"
|
||||
VariableTypeAny VariableType = "any"
|
||||
)
|
||||
|
||||
type VariableSpec struct {
|
||||
Name string `json:"name"`
|
||||
Type VariableType `json:"type"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type NodeSpec struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
RiskLevel NodeRiskLevel `json:"riskLevel"`
|
||||
Interruptible bool `json:"interruptible"`
|
||||
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
RiskLevel NodeRiskLevel `json:"riskLevel"`
|
||||
Interruptible bool `json:"interruptible"`
|
||||
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
|
||||
ConfigSchema any `json:"configSchema,omitempty"`
|
||||
InputSchema []VariableSpec `json:"inputSchema,omitempty"`
|
||||
OutputSchema []VariableSpec `json:"outputSchema,omitempty"`
|
||||
DefaultInputs map[string]dsl.VariableSelector `json:"defaultInputs,omitempty"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
|
||||
Reference in New Issue
Block a user