feat: add conversation understanding and reply policy nodes to workflow executor

- Implemented conversation understanding and reply policy execution in the workflow executor.
- Added new node types: NodeTypeConversationUnderstanding and NodeTypeReplyPolicy.
- Enhanced input and output schemas for the new nodes.
- Updated workflow registry to include new node specifications.
- Created tests for the new workflow routes and behaviors.
- Modified existing workflows to integrate the new conversation understanding and reply policy logic.
This commit is contained in:
mlogclub
2026-06-25 22:38:34 +08:00
parent 7ba7deea96
commit 21fd119b27
9 changed files with 473 additions and 52 deletions
+55 -12
View File
@@ -3,18 +3,20 @@ package registry
import "agent-desk/internal/ai/workflow/dsl"
const (
NodeTypeStart = "start"
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
NodeTypeAnswerabilityGate = "answerability_gate"
NodeTypeLLMReply = "llm_reply"
NodeTypeCondition = "condition"
NodeTypeAnalyzeConversation = "analyze_conversation"
NodeTypePrepareTicketDraft = "prepare_ticket_draft"
NodeTypeHumanConfirm = "human_confirm"
NodeTypeCreateTicket = "create_ticket"
NodeTypeHandoffToHuman = "handoff_to_human"
NodeTypeSendReply = "send_reply"
NodeTypeEnd = "end"
NodeTypeStart = "start"
NodeTypeConversationUnderstanding = "conversation_understanding"
NodeTypeReplyPolicy = "reply_policy"
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
NodeTypeAnswerabilityGate = "answerability_gate"
NodeTypeLLMReply = "llm_reply"
NodeTypeCondition = "condition"
NodeTypeAnalyzeConversation = "analyze_conversation"
NodeTypePrepareTicketDraft = "prepare_ticket_draft"
NodeTypeHumanConfirm = "human_confirm"
NodeTypeCreateTicket = "create_ticket"
NodeTypeHandoffToHuman = "handoff_to_human"
NodeTypeSendReply = "send_reply"
NodeTypeEnd = "end"
)
func DefaultRegistry() *Registry {
@@ -32,6 +34,47 @@ func DefaultRegistry() *Registry {
output("knowledgeBaseIds", VariableTypeIntegerArray, "Knowledge bases bound to the AI Agent."),
},
},
NodeSpec{
Type: NodeTypeConversationUnderstanding,
Title: "Conversation Understanding",
Description: "Classify customer message intent and answer scope before retrieval.",
RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{
requiredInput("userMessage", VariableTypeString, "Current user message content."),
},
OutputSchema: []VariableSpec{
output("normalizedMessage", VariableTypeString, "Normalized customer message."),
output("messageIntent", VariableTypeString, "Detected customer message intent."),
output("answerScope", VariableTypeString, "Recommended answer scope."),
output("confidence", VariableTypeNumber, "Classifier confidence."),
output("riskSignals", VariableTypeStringArray, "Detected risk signals."),
output("reason", VariableTypeString, "Decision reason."),
},
DefaultInputs: map[string]dsl.VariableSelector{
"userMessage": {NodeID: "start_1", Field: "userMessage"},
},
},
NodeSpec{
Type: NodeTypeReplyPolicy,
Title: "Reply Policy",
Description: "Decide the next customer-service action from understanding output and agent policy.",
RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{
requiredInput("messageIntent", VariableTypeString, "Detected customer message intent."),
requiredInput("answerScope", VariableTypeString, "Recommended answer scope."),
optionalInput("userMessage", VariableTypeString, "Current user message content."),
optionalInput("riskSignals", VariableTypeStringArray, "Detected risk signals."),
optionalInput("answerability", VariableTypeString, "Knowledge answerability decision."),
},
OutputSchema: []VariableSpec{
output("action", VariableTypeString, "Selected policy action."),
output("replyText", VariableTypeString, "Customer-visible reply text when the policy can answer directly."),
output("reason", VariableTypeString, "Policy decision reason."),
output("requiresFlow", VariableTypeBoolean, "Whether the decision should continue into workflow actions."),
output("targetFlow", VariableTypeString, "Suggested target flow."),
output("finalReplySource", VariableTypeString, "Source category for the final reply."),
},
},
NodeSpec{
Type: NodeTypeKnowledgeRetrieve,
Title: "Knowledge Retrieve",
@@ -41,6 +41,42 @@ func TestDefaultRegistryExposesSendReplyRequiredInput(t *testing.T) {
}
}
func TestDefaultRegistryExposesConversationUnderstandingOutputs(t *testing.T) {
spec, ok := DefaultRegistry().Get(NodeTypeConversationUnderstanding)
if !ok {
t.Fatalf("conversation_understanding node spec not found")
}
if !hasRequiredVariable(spec.InputSchema, "userMessage", VariableTypeString) {
t.Fatalf("expected conversation_understanding required input userMessage:string, got %#v", spec.InputSchema)
}
for _, want := range []string{"messageIntent", "answerScope", "riskSignals", "reason"} {
if !hasVariableName(spec.OutputSchema, want) {
t.Fatalf("expected conversation_understanding output %s, got %#v", want, spec.OutputSchema)
}
}
if !hasVariable(spec.OutputSchema, "confidence", VariableTypeNumber) {
t.Fatalf("expected conversation_understanding output confidence:number, got %#v", spec.OutputSchema)
}
}
func TestDefaultRegistryExposesReplyPolicyOutputs(t *testing.T) {
spec, ok := DefaultRegistry().Get(NodeTypeReplyPolicy)
if !ok {
t.Fatalf("reply_policy node spec not found")
}
if !hasRequiredVariable(spec.InputSchema, "messageIntent", VariableTypeString) {
t.Fatalf("expected reply_policy required input messageIntent:string, got %#v", spec.InputSchema)
}
if !hasRequiredVariable(spec.InputSchema, "answerScope", VariableTypeString) {
t.Fatalf("expected reply_policy required input answerScope:string, got %#v", spec.InputSchema)
}
for _, want := range []string{"action", "replyText", "reason", "finalReplySource"} {
if !hasVariableName(spec.OutputSchema, want) {
t.Fatalf("expected reply_policy output %s, got %#v", want, spec.OutputSchema)
}
}
}
func hasRequiredVariable(items []VariableSpec, name string, variableType VariableType) bool {
for _, item := range items {
if item.Name == name && item.Type == variableType && item.Required {
@@ -50,6 +86,15 @@ func hasRequiredVariable(items []VariableSpec, name string, variableType Variabl
return false
}
func hasVariableName(items []VariableSpec, name string) bool {
for _, item := range items {
if item.Name == name {
return true
}
}
return false
}
func hasVariable(items []VariableSpec, name string, variableType VariableType) bool {
for _, item := range items {
if item.Name == name && item.Type == variableType {
+1
View File
@@ -14,6 +14,7 @@ type VariableType string
const (
VariableTypeString VariableType = "string"
VariableTypeNumber VariableType = "number"
VariableTypeInteger VariableType = "integer"
VariableTypeBoolean VariableType = "boolean"
VariableTypeObject VariableType = "object"