Add AnalyzeConversation node execution and corresponding test case
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"agent-desk/internal/ai"
|
"agent-desk/internal/ai"
|
||||||
|
"agent-desk/internal/ai/runtime/graphs"
|
||||||
"agent-desk/internal/ai/runtime/internal/impl/retrievers"
|
"agent-desk/internal/ai/runtime/internal/impl/retrievers"
|
||||||
"agent-desk/internal/ai/workflow/dsl"
|
"agent-desk/internal/ai/workflow/dsl"
|
||||||
workflowregistry "agent-desk/internal/ai/workflow/registry"
|
workflowregistry "agent-desk/internal/ai/workflow/registry"
|
||||||
@@ -124,6 +125,8 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
|||||||
return e.executeAnswerabilityGate(state, node)
|
return e.executeAnswerabilityGate(state, node)
|
||||||
case workflowregistry.NodeTypeCondition:
|
case workflowregistry.NodeTypeCondition:
|
||||||
state.setNodeVars(node.ID, map[string]any{"matched": true})
|
state.setNodeVars(node.ID, map[string]any{"matched": true})
|
||||||
|
case workflowregistry.NodeTypeAnalyzeConversation:
|
||||||
|
return e.executeAnalyzeConversation(ctx, state, node)
|
||||||
case workflowregistry.NodeTypeLLMReply:
|
case workflowregistry.NodeTypeLLMReply:
|
||||||
return e.executeLLMReply(ctx, state, node)
|
return e.executeLLMReply(ctx, state, node)
|
||||||
case workflowregistry.NodeTypeSendReply:
|
case workflowregistry.NodeTypeSendReply:
|
||||||
@@ -143,6 +146,48 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Executor) executeAnalyzeConversation(ctx context.Context, state *runState, node dsl.Node) error {
|
||||||
|
userMessage := strings.TrimSpace(toString(state.resolveInput(node, "userMessage")))
|
||||||
|
input := graphs.AnalyzeConversationInput{
|
||||||
|
ObservedIssue: userMessage,
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(readStringConfig(node.Config, "goal")) != "" {
|
||||||
|
input.Goal = strings.TrimSpace(readStringConfig(node.Config, "goal"))
|
||||||
|
}
|
||||||
|
if readBoolConfig(node.Config, "needTicket") {
|
||||||
|
input.NeedTicket = true
|
||||||
|
}
|
||||||
|
if readBoolConfig(node.Config, "needHumanHandoff") {
|
||||||
|
input.NeedHumanHandoff = true
|
||||||
|
}
|
||||||
|
if readBoolConfig(node.Config, "needQualityCheck") {
|
||||||
|
input.NeedQualityCheck = true
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(readStringConfig(node.Config, "additionalContext")) != "" {
|
||||||
|
input.AdditionalContext = strings.TrimSpace(readStringConfig(node.Config, "additionalContext"))
|
||||||
|
}
|
||||||
|
args, err := json.Marshal(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := graphs.NewAnalyzeConversationGraph(state.input.Conversation).Run(ctx, string(args))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var result graphs.AnalyzeConversationResult
|
||||||
|
if err := json.Unmarshal([]byte(raw), &result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nextAction := strings.TrimSpace(result.RecommendedNextAction)
|
||||||
|
state.setNodeVars(node.ID, map[string]any{
|
||||||
|
"intent": strings.TrimSpace(result.UserIntent),
|
||||||
|
"riskLevel": strings.TrimSpace(result.RiskLevel),
|
||||||
|
"needTicket": nextAction == "prepare_ticket",
|
||||||
|
"needHumanHandoff": nextAction == "handoff_to_human",
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Executor) executeHandoffToHuman(state *runState, node dsl.Node) error {
|
func (e *Executor) executeHandoffToHuman(state *runState, node dsl.Node) error {
|
||||||
reason := strings.TrimSpace(toString(state.resolveInput(node, "reason")))
|
reason := strings.TrimSpace(toString(state.resolveInput(node, "reason")))
|
||||||
result, err := services.ConversationHumanDispatchService.HandoffByAIWithRequestID(
|
result, err := services.ConversationHumanDispatchService.HandoffByAIWithRequestID(
|
||||||
@@ -335,6 +380,17 @@ func readStringConfig(raw json.RawMessage, key string) string {
|
|||||||
return toString(cfg[key])
|
return toString(cfg[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readBoolConfig(raw json.RawMessage, key string) bool {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var cfg map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return truthy(cfg[key])
|
||||||
|
}
|
||||||
|
|
||||||
func compareString(left any, right any) int {
|
func compareString(left any, right any) int {
|
||||||
return strings.Compare(toString(left), toString(right))
|
return strings.Compare(toString(left), toString(right))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,24 @@ func TestExecutorHandoffToHumanRunsRealDispatchAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecutorAnalyzeConversationOutputsBranchVariables(t *testing.T) {
|
||||||
|
db := setupWorkflowExecutorHandoffDB(t)
|
||||||
|
aiAgent := createWorkflowExecutorHandoffAIAgent(t, db, "1")
|
||||||
|
conversation := createWorkflowExecutorHandoffConversation(t, db, aiAgent.ID)
|
||||||
|
userMessage := createWorkflowExecutorCustomerMessage(t, db, conversation.ID, "你们重复扣费了,我要投诉并转人工")
|
||||||
|
|
||||||
|
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||||
|
Definition: analyzeConversationWorkflowDefinition(),
|
||||||
|
Conversation: conversation,
|
||||||
|
UserMessage: userMessage,
|
||||||
|
AIAgent: aiAgent,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("execute workflow: %v", err)
|
||||||
|
}
|
||||||
|
assertPath(t, result.NodePath, []string{"start_1", "analyze_1", "handoff_end"})
|
||||||
|
}
|
||||||
|
|
||||||
func conditionalReplyDefinition() dsl.Definition {
|
func conditionalReplyDefinition() dsl.Definition {
|
||||||
return dsl.Definition{
|
return dsl.Definition{
|
||||||
SchemaVersion: 1,
|
SchemaVersion: 1,
|
||||||
@@ -130,6 +148,34 @@ func conditionalReplyDefinition() dsl.Definition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func analyzeConversationWorkflowDefinition() dsl.Definition {
|
||||||
|
return dsl.Definition{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
EntryNodeID: "start_1",
|
||||||
|
Nodes: []dsl.Node{
|
||||||
|
{ID: "start_1", Type: workflowregistry.NodeTypeStart, Name: "Start"},
|
||||||
|
{ID: "analyze_1", Type: workflowregistry.NodeTypeAnalyzeConversation, Name: "Analyze", Inputs: map[string]dsl.VariableSelector{
|
||||||
|
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||||
|
}},
|
||||||
|
{ID: "handoff_end", Type: workflowregistry.NodeTypeEnd, Name: "Handoff"},
|
||||||
|
{ID: "default_end", Type: workflowregistry.NodeTypeEnd, Name: "Default"},
|
||||||
|
},
|
||||||
|
Edges: []dsl.Edge{
|
||||||
|
{ID: "edge_start_analyze", Source: "start_1", Target: "analyze_1"},
|
||||||
|
{
|
||||||
|
ID: "edge_analyze_handoff",
|
||||||
|
Source: "analyze_1",
|
||||||
|
Target: "handoff_end",
|
||||||
|
Condition: &dsl.Condition{
|
||||||
|
Left: &dsl.VariableSelector{NodeID: "analyze_1", Field: "needHumanHandoff"},
|
||||||
|
Operator: "is_true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "edge_analyze_default", Source: "analyze_1", Target: "default_end"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handoffWorkflowDefinition() dsl.Definition {
|
func handoffWorkflowDefinition() dsl.Definition {
|
||||||
return dsl.Definition{
|
return dsl.Definition{
|
||||||
SchemaVersion: 1,
|
SchemaVersion: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user