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:
@@ -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 {
|
||||
|
||||
@@ -14,6 +14,7 @@ type VariableType string
|
||||
|
||||
const (
|
||||
VariableTypeString VariableType = "string"
|
||||
VariableTypeNumber VariableType = "number"
|
||||
VariableTypeInteger VariableType = "integer"
|
||||
VariableTypeBoolean VariableType = "boolean"
|
||||
VariableTypeObject VariableType = "object"
|
||||
|
||||
Reference in New Issue
Block a user