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:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -24,6 +26,8 @@ import (
|
||||
|
||||
const maxWorkflowSteps = 128
|
||||
|
||||
var workflowHTMLTagPattern = regexp.MustCompile(`<[^>]+>`)
|
||||
|
||||
type Input struct {
|
||||
Definition dsl.Definition
|
||||
Conversation models.Conversation
|
||||
@@ -259,6 +263,10 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
||||
"knowledgeBaseIds": utils.SplitInt64s(state.input.AIAgent.KnowledgeIDs),
|
||||
"conversationState": state.input.Conversation.Status,
|
||||
})
|
||||
case workflowregistry.NodeTypeConversationUnderstanding:
|
||||
return e.executeConversationUnderstanding(state, node)
|
||||
case workflowregistry.NodeTypeReplyPolicy:
|
||||
return e.executeReplyPolicy(state, node)
|
||||
case workflowregistry.NodeTypeKnowledgeRetrieve:
|
||||
return e.executeKnowledgeRetrieve(ctx, state, node)
|
||||
case workflowregistry.NodeTypeAnswerabilityGate:
|
||||
@@ -292,6 +300,44 @@ func (e *Executor) executeNode(ctx context.Context, state *runState, node dsl.No
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeConversationUnderstanding(state *runState, node dsl.Node) error {
|
||||
rawMessage := strings.TrimSpace(toString(state.resolveInput(node, "userMessage")))
|
||||
if rawMessage == "" {
|
||||
rawMessage = state.input.UserMessage.Content
|
||||
}
|
||||
understanding := understandConversationMessage(rawMessage)
|
||||
state.setNodeVars(node.ID, map[string]any{
|
||||
"normalizedMessage": understanding.NormalizedMessage,
|
||||
"messageIntent": understanding.MessageIntent,
|
||||
"answerScope": understanding.AnswerScope,
|
||||
"confidence": understanding.Confidence,
|
||||
"riskSignals": understanding.RiskSignals,
|
||||
"reason": understanding.Reason,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeReplyPolicy(state *runState, node dsl.Node) error {
|
||||
intent := strings.TrimSpace(toString(state.resolveInput(node, "messageIntent")))
|
||||
scope := strings.TrimSpace(toString(state.resolveInput(node, "answerScope")))
|
||||
userMessage := normalizeWorkflowUserMessage(toString(state.resolveInput(node, "userMessage")))
|
||||
decision := decideWorkflowReplyPolicy(state.input.AIAgent, workflowReplyPolicyInput{
|
||||
MessageIntent: intent,
|
||||
AnswerScope: scope,
|
||||
UserMessage: userMessage,
|
||||
Answerability: strings.TrimSpace(toString(state.resolveInput(node, "answerability"))),
|
||||
})
|
||||
state.setNodeVars(node.ID, map[string]any{
|
||||
"action": decision.Action,
|
||||
"replyText": decision.ReplyText,
|
||||
"reason": decision.Reason,
|
||||
"requiresFlow": decision.RequiresFlow,
|
||||
"targetFlow": decision.TargetFlow,
|
||||
"finalReplySource": decision.FinalReplySource,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeCreateTicket(state *runState, node dsl.Node) error {
|
||||
confirmed := truthy(state.resolveInput(node, "confirmed"))
|
||||
if !confirmed {
|
||||
@@ -344,6 +390,164 @@ func workflowAIPrincipal(aiAgent models.AIAgent) *dto.AuthPrincipal {
|
||||
}
|
||||
}
|
||||
|
||||
type workflowConversationUnderstanding struct {
|
||||
NormalizedMessage string
|
||||
MessageIntent string
|
||||
AnswerScope string
|
||||
Confidence float64
|
||||
RiskSignals []string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type workflowReplyPolicyInput struct {
|
||||
MessageIntent string
|
||||
AnswerScope string
|
||||
UserMessage string
|
||||
Answerability string
|
||||
}
|
||||
|
||||
type workflowReplyPolicyDecision struct {
|
||||
Action string
|
||||
ReplyText string
|
||||
Reason string
|
||||
RequiresFlow bool
|
||||
TargetFlow string
|
||||
FinalReplySource string
|
||||
}
|
||||
|
||||
func understandConversationMessage(rawMessage string) workflowConversationUnderstanding {
|
||||
message := normalizeWorkflowUserMessage(rawMessage)
|
||||
ret := workflowConversationUnderstanding{
|
||||
NormalizedMessage: message,
|
||||
MessageIntent: "unknown",
|
||||
AnswerScope: "needs_clarification",
|
||||
Confidence: 0.5,
|
||||
Reason: "message intent is unclear",
|
||||
}
|
||||
if message == "" {
|
||||
ret.MessageIntent = "unknown"
|
||||
ret.AnswerScope = "needs_clarification"
|
||||
ret.Confidence = 0.9
|
||||
ret.Reason = "empty message"
|
||||
return ret
|
||||
}
|
||||
lower := strings.ToLower(message)
|
||||
switch {
|
||||
case isGreetingMessage(lower):
|
||||
ret.MessageIntent = "greeting"
|
||||
ret.AnswerScope = "direct_reply"
|
||||
ret.Confidence = 0.98
|
||||
ret.Reason = "matched greeting phrase"
|
||||
case containsAnyWorkflowText(lower, "谢谢", "感谢", "多谢", "辛苦了", "thank"):
|
||||
ret.MessageIntent = "thanks"
|
||||
ret.AnswerScope = "direct_reply"
|
||||
ret.Confidence = 0.95
|
||||
ret.Reason = "matched thanks phrase"
|
||||
case containsAnyWorkflowText(lower, "再见", "拜拜", "不用了", "没事了", "结束"):
|
||||
ret.MessageIntent = "end_conversation"
|
||||
ret.AnswerScope = "direct_reply"
|
||||
ret.Confidence = 0.9
|
||||
ret.Reason = "matched ending phrase"
|
||||
case containsAnyWorkflowText(lower, "人工", "转人工", "真人", "客服"):
|
||||
ret.MessageIntent = "handoff_request"
|
||||
ret.AnswerScope = "needs_handoff"
|
||||
ret.Confidence = 0.95
|
||||
ret.RiskSignals = append(ret.RiskSignals, "handoff_requested")
|
||||
ret.Reason = "matched handoff phrase"
|
||||
case containsAnyWorkflowText(lower, "投诉", "举报", "差评", "曝光", "起诉", "律师", "12315"):
|
||||
ret.MessageIntent = "complaint"
|
||||
ret.AnswerScope = "needs_handoff"
|
||||
ret.Confidence = 0.92
|
||||
ret.RiskSignals = append(ret.RiskSignals, "complaint_escalation")
|
||||
ret.Reason = "matched complaint phrase"
|
||||
case containsAnyWorkflowText(lower, "工单", "报障", "售后", "登记问题", "记录问题"):
|
||||
ret.MessageIntent = "ticket_request"
|
||||
ret.AnswerScope = "needs_ticket"
|
||||
ret.Confidence = 0.9
|
||||
ret.RiskSignals = append(ret.RiskSignals, "ticket_expected")
|
||||
ret.Reason = "matched ticket phrase"
|
||||
case containsAnyWorkflowText(lower, "确认", "可以", "好的", "好", "是的", "取消"):
|
||||
ret.MessageIntent = "confirmation"
|
||||
ret.AnswerScope = "direct_reply"
|
||||
ret.Confidence = 0.8
|
||||
ret.Reason = "matched confirmation phrase"
|
||||
case isAmbiguousWorkflowQuestion(lower):
|
||||
ret.MessageIntent = "ambiguous_question"
|
||||
ret.AnswerScope = "needs_clarification"
|
||||
ret.Confidence = 0.82
|
||||
ret.Reason = "message lacks a concrete business object"
|
||||
default:
|
||||
ret.MessageIntent = "business_question"
|
||||
ret.AnswerScope = "needs_knowledge"
|
||||
ret.Confidence = 0.7
|
||||
ret.Reason = "default business question policy"
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func decideWorkflowReplyPolicy(aiAgent models.AIAgent, input workflowReplyPolicyInput) workflowReplyPolicyDecision {
|
||||
intent := strings.TrimSpace(input.MessageIntent)
|
||||
scope := strings.TrimSpace(input.AnswerScope)
|
||||
if answerability := strings.TrimSpace(input.Answerability); answerability != "" && answerability != "answerable" {
|
||||
return workflowReplyPolicyDecision{
|
||||
Action: "knowledge_fallback",
|
||||
ReplyText: workflowKnowledgeFallbackReply(aiAgent),
|
||||
Reason: "knowledge is not sufficient for business answer",
|
||||
FinalReplySource: "knowledge_fallback",
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case intent == "greeting":
|
||||
return workflowReplyPolicyDecision{Action: "direct_reply", ReplyText: "您好,请问有什么可以帮您?", Reason: "greeting can be answered directly", FinalReplySource: "direct_reply"}
|
||||
case intent == "thanks":
|
||||
return workflowReplyPolicyDecision{Action: "direct_reply", ReplyText: "不客气,如有其他问题可以继续告诉我。", Reason: "thanks can be answered directly", FinalReplySource: "direct_reply"}
|
||||
case intent == "end_conversation":
|
||||
return workflowReplyPolicyDecision{Action: "end_conversation", ReplyText: "好的,如后续还有问题可以随时联系。", Reason: "conversation ending phrase", FinalReplySource: "direct_reply"}
|
||||
case intent == "confirmation":
|
||||
return workflowReplyPolicyDecision{Action: "direct_reply", ReplyText: "好的,请继续补充需要处理的问题。", Reason: "confirmation without pending interrupt", FinalReplySource: "direct_reply"}
|
||||
case intent == "handoff_request" || scope == "needs_handoff":
|
||||
return workflowReplyPolicyDecision{Action: "handoff_to_human", Reason: "user requested human support or risk requires handoff", RequiresFlow: true, TargetFlow: "handoff_to_human", FinalReplySource: "handoff_notice"}
|
||||
case intent == "ticket_request" || scope == "needs_ticket":
|
||||
return workflowReplyPolicyDecision{Action: "prepare_ticket", Reason: "user requested ticket handling", RequiresFlow: true, TargetFlow: "prepare_ticket", FinalReplySource: "ticket_result"}
|
||||
case intent == "ambiguous_question" || scope == "needs_clarification":
|
||||
return workflowReplyPolicyDecision{Action: "clarify", ReplyText: "请补充具体的产品、场景、报错信息或你希望处理的结果,我再继续帮你确认。", Reason: "message needs clarification", FinalReplySource: "clarification"}
|
||||
case scope == "needs_knowledge":
|
||||
return workflowReplyPolicyDecision{Action: "retrieve_knowledge", Reason: "business question should be answered with knowledge evidence", RequiresFlow: true, TargetFlow: "knowledge", FinalReplySource: "knowledge_answer"}
|
||||
default:
|
||||
return workflowReplyPolicyDecision{Action: "clarify", ReplyText: "请补充更具体的问题,我再继续帮你处理。", Reason: "fallback to clarification for unclear policy input", FinalReplySource: "clarification"}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWorkflowUserMessage(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
value = workflowHTMLTagPattern.ReplaceAllString(value, " ")
|
||||
value = html.UnescapeString(value)
|
||||
value = strings.Join(strings.Fields(value), " ")
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func isGreetingMessage(value string) bool {
|
||||
trimmed := strings.Trim(value, " \r\n。.!!??~~")
|
||||
return containsAnyWorkflowText(trimmed, "你好", "您好", "在吗", "在不在") || trimmed == "hello" || trimmed == "hi"
|
||||
}
|
||||
|
||||
func isAmbiguousWorkflowQuestion(value string) bool {
|
||||
trimmed := strings.Trim(value, " \r\n。.!!??~~")
|
||||
return containsAnyWorkflowText(trimmed, "怎么弄", "怎么办", "怎么处理", "帮我看看", "有问题") || len([]rune(trimmed)) <= 3
|
||||
}
|
||||
|
||||
func containsAnyWorkflowText(value string, needles ...string) bool {
|
||||
for _, needle := range needles {
|
||||
if strings.Contains(value, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *Executor) executeHumanConfirm(state *runState, node dsl.Node) error {
|
||||
prompt := strings.TrimSpace(toString(state.resolveInput(node, "prompt")))
|
||||
if prompt == "" {
|
||||
|
||||
@@ -239,6 +239,55 @@ func TestExecutorPrepareTicketDraftOutputsDraftVariable(t *testing.T) {
|
||||
assertPath(t, result.NodePath, []string{"start_1", "draft_1", "draft_route_1", "ready_end"})
|
||||
}
|
||||
|
||||
func TestExecutorPolicyFirstWorkflowRoutesGreetingToDirectReply(t *testing.T) {
|
||||
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: policyFirstWorkflowDefinition(),
|
||||
UserMessage: models.Message{
|
||||
Content: "<p>你好。</p>",
|
||||
},
|
||||
AIAgent: models.AIAgent{
|
||||
KnowledgeIDs: "1",
|
||||
FallbackMessage: "我暂时没有找到足够准确的信息。",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute workflow: %v", err)
|
||||
}
|
||||
if result.ReplyText != "您好,请问有什么可以帮您?" {
|
||||
t.Fatalf("expected greeting reply, got %q", result.ReplyText)
|
||||
}
|
||||
if result.RetrieverCount != 0 {
|
||||
t.Fatalf("expected greeting to skip retrieval, got retriever count %d", result.RetrieverCount)
|
||||
}
|
||||
assertPath(t, result.NodePath, []string{"start_1", "understanding_1", "policy_1", "policy_route_1", "send_direct_1", "end_1"})
|
||||
|
||||
understandingTrace := findNodeTrace(result.NodeTraces, "understanding_1")
|
||||
if understandingTrace == nil || !strings.Contains(understandingTrace.OutputPreview, `"messageIntent":"greeting"`) || !strings.Contains(understandingTrace.OutputPreview, `"answerScope":"direct_reply"`) {
|
||||
t.Fatalf("expected understanding trace to audit greeting/direct_reply, got %#v", understandingTrace)
|
||||
}
|
||||
policyTrace := findNodeTrace(result.NodeTraces, "policy_1")
|
||||
if policyTrace == nil || !strings.Contains(policyTrace.OutputPreview, `"action":"direct_reply"`) || !strings.Contains(policyTrace.OutputPreview, `"finalReplySource":"direct_reply"`) {
|
||||
t.Fatalf("expected policy trace to audit direct reply, got %#v", policyTrace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutorPolicyFirstWorkflowRoutesBusinessQuestionToKnowledge(t *testing.T) {
|
||||
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: policyFirstWorkflowDefinition(),
|
||||
UserMessage: models.Message{
|
||||
Content: "你们价格是多少?",
|
||||
},
|
||||
AIAgent: models.AIAgent{
|
||||
KnowledgeIDs: "1",
|
||||
FallbackMessage: "我暂时没有找到足够准确的信息。",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute workflow: %v", err)
|
||||
}
|
||||
assertPath(t, result.NodePath, []string{"start_1", "understanding_1", "policy_1", "policy_route_1", "retrieve_end"})
|
||||
}
|
||||
|
||||
func TestExecutorLLMReplyUsesAgentFallbackWhenDeclaredKnowledgeIsEmpty(t *testing.T) {
|
||||
result, err := NewExecutor().Execute(context.Background(), Input{
|
||||
Definition: emptyKnowledgeReplyDefinition(),
|
||||
@@ -408,6 +457,53 @@ func emptyKnowledgeReplyDefinition() dsl.Definition {
|
||||
}
|
||||
}
|
||||
|
||||
func policyFirstWorkflowDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: workflowregistry.NodeTypeStart, Name: "Start"},
|
||||
{ID: "understanding_1", Type: workflowregistry.NodeTypeConversationUnderstanding, Name: "Understanding", Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "policy_1", Type: workflowregistry.NodeTypeReplyPolicy, Name: "Policy", Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
"messageIntent": {NodeID: "understanding_1", Field: "messageIntent"},
|
||||
"answerScope": {NodeID: "understanding_1", Field: "answerScope"},
|
||||
"riskSignals": {NodeID: "understanding_1", Field: "riskSignals"},
|
||||
"knowledgeItems": {NodeID: "retrieve_1", Field: "items"},
|
||||
}},
|
||||
{ID: "policy_route_1", Type: workflowregistry.NodeTypeCondition, Name: "Policy Route", Config: mustMarshalWorkflowTestConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "direct", Name: "Direct", TargetNodeID: "send_direct_1", Condition: &dsl.Condition{
|
||||
Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"},
|
||||
Operator: "eq",
|
||||
Right: "direct_reply",
|
||||
}},
|
||||
{ID: "knowledge", Name: "Knowledge", TargetNodeID: "retrieve_end", Condition: &dsl.Condition{
|
||||
Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"},
|
||||
Operator: "eq",
|
||||
Right: "retrieve_knowledge",
|
||||
}},
|
||||
{ID: "default", Name: "Default", TargetNodeID: "end_1", Default: true},
|
||||
}})},
|
||||
{ID: "send_direct_1", Type: workflowregistry.NodeTypeSendReply, Name: "Send Direct", Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "policy_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "retrieve_end", Type: workflowregistry.NodeTypeEnd, Name: "Retrieve"},
|
||||
{ID: "end_1", Type: workflowregistry.NodeTypeEnd, Name: "End"},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "edge_start_understanding", Source: "start_1", Target: "understanding_1"},
|
||||
{ID: "edge_understanding_policy", Source: "understanding_1", Target: "policy_1"},
|
||||
{ID: "edge_policy_route", Source: "policy_1", Target: "policy_route_1"},
|
||||
{ID: "edge_policy_direct", Source: "policy_route_1", Target: "send_direct_1"},
|
||||
{ID: "edge_policy_knowledge", Source: "policy_route_1", Target: "retrieve_end"},
|
||||
{ID: "edge_policy_default", Source: "policy_route_1", Target: "end_1"},
|
||||
{ID: "edge_send_direct_end", Source: "send_direct_1", Target: "end_1"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func conditionalReplyDefinition() dsl.Definition {
|
||||
return dsl.Definition{
|
||||
SchemaVersion: 1,
|
||||
|
||||
@@ -4,6 +4,8 @@ import "agent-desk/internal/ai/workflow/dsl"
|
||||
|
||||
const (
|
||||
NodeTypeStart = "start"
|
||||
NodeTypeConversationUnderstanding = "conversation_understanding"
|
||||
NodeTypeReplyPolicy = "reply_policy"
|
||||
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
|
||||
NodeTypeAnswerabilityGate = "answerability_gate"
|
||||
NodeTypeLLMReply = "llm_reply"
|
||||
@@ -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"
|
||||
|
||||
@@ -56,10 +56,18 @@ func TestAIAgentServiceCreatesDefaultWorkflow(t *testing.T) {
|
||||
if !validation.Valid {
|
||||
t.Fatalf("expected default workflow to be valid, got %#v", validation.Errors)
|
||||
}
|
||||
if nodeTypeByID(stored, "route_intent_1") != workflowregistry.NodeTypeCondition {
|
||||
t.Fatalf("expected default workflow to start with a clear intent router, got nodes: %#v", stored.Nodes)
|
||||
if nodeTypeByID(stored, "understanding_1") != workflowregistry.NodeTypeConversationUnderstanding {
|
||||
t.Fatalf("expected default workflow to include conversation understanding, got nodes: %#v", stored.Nodes)
|
||||
}
|
||||
if nodeTypeByID(stored, "policy_1") != workflowregistry.NodeTypeReplyPolicy {
|
||||
t.Fatalf("expected default workflow to include reply policy, got nodes: %#v", stored.Nodes)
|
||||
}
|
||||
if !workflowEdgeExists(stored, "start_1", "understanding_1") || !workflowEdgeExists(stored, "understanding_1", "policy_1") {
|
||||
t.Fatalf("expected default workflow to start with policy-first understanding flow, got edges: %#v", stored.Edges)
|
||||
}
|
||||
for _, nodeType := range []string{
|
||||
workflowregistry.NodeTypeConversationUnderstanding,
|
||||
workflowregistry.NodeTypeReplyPolicy,
|
||||
workflowregistry.NodeTypeHandoffToHuman,
|
||||
workflowregistry.NodeTypePrepareTicketDraft,
|
||||
workflowregistry.NodeTypeHumanConfirm,
|
||||
@@ -73,8 +81,9 @@ func TestAIAgentServiceCreatesDefaultWorkflow(t *testing.T) {
|
||||
t.Fatalf("expected default workflow to include %s node: %#v", nodeType, stored.Nodes)
|
||||
}
|
||||
}
|
||||
assertConditionBranchToNodeType(t, stored, "route_intent_1", workflowregistry.NodeTypeHandoffToHuman, "contains", "人工")
|
||||
assertConditionBranchToNodeType(t, stored, "route_intent_1", workflowregistry.NodeTypePrepareTicketDraft, "contains", "工单")
|
||||
assertConditionBranchToNodeType(t, stored, "policy_route_1", workflowregistry.NodeTypeSendReply, "eq", "direct_reply")
|
||||
assertConditionBranchToNodeType(t, stored, "policy_route_1", workflowregistry.NodeTypeHandoffToHuman, "eq", "handoff_to_human")
|
||||
assertConditionBranchToNodeType(t, stored, "policy_route_1", workflowregistry.NodeTypePrepareTicketDraft, "eq", "prepare_ticket")
|
||||
assertConditionBranchToNodeID(t, stored, "answerability_route_1", "reply_1", "eq", "answerable")
|
||||
assertDefaultBranchToNodeID(t, stored, "answerability_route_1", "fallback_reply_1")
|
||||
if !workflowEdgeExists(stored, "create_ticket_1", "ticket_result_reply_1") {
|
||||
@@ -91,8 +100,11 @@ func TestAIWorkflowServiceDefaultAgentWorkflowDefinitionIsValid(t *testing.T) {
|
||||
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 nodeTypeByID(definition, "understanding_1") != workflowregistry.NodeTypeConversationUnderstanding {
|
||||
t.Fatalf("expected default workflow to include conversation understanding, got nodes: %#v", definition.Nodes)
|
||||
}
|
||||
if nodeTypeByID(definition, "policy_1") != workflowregistry.NodeTypeReplyPolicy {
|
||||
t.Fatalf("expected default workflow to include reply policy, got nodes: %#v", definition.Nodes)
|
||||
}
|
||||
if !workflowHasNodeType(definition, workflowregistry.NodeTypeHandoffToHuman) {
|
||||
t.Fatalf("expected default workflow to include human handoff node")
|
||||
|
||||
@@ -426,76 +426,94 @@ func defaultAgentWorkflowDefinition() dsl.Definition {
|
||||
SchemaVersion: 1,
|
||||
EntryNodeID: "start_1",
|
||||
Nodes: []dsl.Node{
|
||||
{ID: "start_1", Type: workflowregistry.NodeTypeStart, Name: "开始", Position: dsl.Position{X: 0, Y: 260}},
|
||||
{ID: "route_intent_1", Type: workflowregistry.NodeTypeCondition, Name: "意图分流", Position: dsl.Position{X: 260, Y: 260}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "handoff", Name: "需要转人工", TargetNodeID: "handoff_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"}, Operator: "contains", Right: "人工"}},
|
||||
{ID: "ticket", Name: "需要建单", TargetNodeID: "draft_ticket_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"}, Operator: "contains", Right: "工单"}},
|
||||
{ID: "complaint", Name: "投诉建单", TargetNodeID: "draft_ticket_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"}, Operator: "contains", Right: "投诉"}},
|
||||
{ID: "incident", Name: "报障建单", TargetNodeID: "draft_ticket_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"}, Operator: "contains", Right: "报障"}},
|
||||
{ID: "default", Name: "默认知识库回复", TargetNodeID: "retrieve_1", Default: true},
|
||||
}})},
|
||||
{ID: "handoff_1", Type: workflowregistry.NodeTypeHandoffToHuman, Name: "转人工", Position: dsl.Position{X: 560, Y: 80}, Inputs: map[string]dsl.VariableSelector{
|
||||
"reason": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "handoff_end_1", Type: workflowregistry.NodeTypeEnd, Name: "结束", Position: dsl.Position{X: 860, Y: 80}},
|
||||
{ID: "draft_ticket_1", Type: workflowregistry.NodeTypePrepareTicketDraft, Name: "整理工单草稿", Position: dsl.Position{X: 560, Y: 240}, Inputs: map[string]dsl.VariableSelector{
|
||||
"issue": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "ticket_confirm_prompt_1", Type: workflowregistry.NodeTypeLLMReply, Name: "建单确认文案", Position: dsl.Position{X: 860, Y: 240}, Config: json.RawMessage(`{"staticReply":"我已整理工单草稿。请回复“确认”创建工单,或回复“取消”放弃。"}`), Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "start_1", Type: workflowregistry.NodeTypeStart, Name: "开始", Position: dsl.Position{X: 0, Y: 320}},
|
||||
{ID: "understanding_1", Type: workflowregistry.NodeTypeConversationUnderstanding, Name: "会话理解", Position: dsl.Position{X: 260, Y: 320}, Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "ticket_confirm_1", Type: workflowregistry.NodeTypeHumanConfirm, Name: "确认建单", Position: dsl.Position{X: 1160, Y: 240}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "policy_1", Type: workflowregistry.NodeTypeReplyPolicy, Name: "回复策略", Position: dsl.Position{X: 520, Y: 320}, Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
"messageIntent": {NodeID: "understanding_1", Field: "messageIntent"},
|
||||
"answerScope": {NodeID: "understanding_1", Field: "answerScope"},
|
||||
"riskSignals": {NodeID: "understanding_1", Field: "riskSignals"},
|
||||
}},
|
||||
{ID: "policy_route_1", Type: workflowregistry.NodeTypeCondition, Name: "策略分流", Position: dsl.Position{X: 780, Y: 320}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "direct", Name: "直接回复", TargetNodeID: "policy_reply_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "direct_reply"}},
|
||||
{ID: "clarify", Name: "追问澄清", TargetNodeID: "policy_reply_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "clarify"}},
|
||||
{ID: "end_conversation", Name: "结束语", TargetNodeID: "policy_reply_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "end_conversation"}},
|
||||
{ID: "handoff", Name: "转人工", TargetNodeID: "handoff_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "handoff_to_human"}},
|
||||
{ID: "ticket", Name: "创建工单", TargetNodeID: "draft_ticket_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "prepare_ticket"}},
|
||||
{ID: "knowledge", Name: "知识库回复", TargetNodeID: "retrieve_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"}, Operator: "eq", Right: "retrieve_knowledge"}},
|
||||
{ID: "default", Name: "默认澄清", TargetNodeID: "policy_reply_1", Default: true},
|
||||
}})},
|
||||
{ID: "policy_reply_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送策略回复", Position: dsl.Position{X: 1080, Y: 60}, Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "policy_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "handoff_1", Type: workflowregistry.NodeTypeHandoffToHuman, Name: "转人工", Position: dsl.Position{X: 1080, Y: 180}, Inputs: map[string]dsl.VariableSelector{
|
||||
"reason": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "handoff_end_1", Type: workflowregistry.NodeTypeEnd, Name: "结束", Position: dsl.Position{X: 1380, Y: 180}},
|
||||
{ID: "draft_ticket_1", Type: workflowregistry.NodeTypePrepareTicketDraft, Name: "整理工单草稿", Position: dsl.Position{X: 1080, Y: 320}, Inputs: map[string]dsl.VariableSelector{
|
||||
"issue": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "ticket_confirm_prompt_1", Type: workflowregistry.NodeTypeLLMReply, Name: "建单确认文案", Position: dsl.Position{X: 1380, Y: 320}, Config: json.RawMessage(`{"staticReply":"我已整理工单草稿。请回复“确认”创建工单,或回复“取消”放弃。"}`), Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "ticket_confirm_1", Type: workflowregistry.NodeTypeHumanConfirm, Name: "确认建单", Position: dsl.Position{X: 1680, Y: 320}, Inputs: map[string]dsl.VariableSelector{
|
||||
"prompt": {NodeID: "ticket_confirm_prompt_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "ticket_confirm_route_1", Type: workflowregistry.NodeTypeCondition, Name: "建单确认分流", Position: dsl.Position{X: 1310, Y: 240}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "ticket_confirm_route_1", Type: workflowregistry.NodeTypeCondition, Name: "建单确认分流", Position: dsl.Position{X: 1830, Y: 320}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "confirmed", Name: "已确认", TargetNodeID: "create_ticket_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "ticket_confirm_1", Field: "confirmed"}, Operator: "is_true"}},
|
||||
{ID: "default", Name: "取消或未确认", TargetNodeID: "ticket_cancel_reply_1", Default: true},
|
||||
}})},
|
||||
{ID: "create_ticket_1", Type: workflowregistry.NodeTypeCreateTicket, Name: "创建工单", Position: dsl.Position{X: 1460, Y: 180}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "create_ticket_1", Type: workflowregistry.NodeTypeCreateTicket, Name: "创建工单", Position: dsl.Position{X: 1980, Y: 260}, Inputs: map[string]dsl.VariableSelector{
|
||||
"ticketDraft": {NodeID: "draft_ticket_1", Field: "ticketDraft"},
|
||||
"confirmed": {NodeID: "ticket_confirm_1", Field: "confirmed"},
|
||||
}},
|
||||
{ID: "ticket_result_reply_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送建单结果", Position: dsl.Position{X: 1760, Y: 180}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "ticket_result_reply_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送建单结果", Position: dsl.Position{X: 2280, Y: 260}, Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "create_ticket_1", Field: "message"},
|
||||
}},
|
||||
{ID: "ticket_cancel_reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "取消建单提示", Position: dsl.Position{X: 1460, Y: 320}, Config: json.RawMessage(`{"staticReply":"已取消创建工单。你可以继续补充问题,我会继续帮你处理。"}`), Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "ticket_cancel_reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "取消建单提示", Position: dsl.Position{X: 1980, Y: 380}, Config: json.RawMessage(`{"staticReply":"已取消创建工单。你可以继续补充问题,我会继续帮你处理。"}`), Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "send_ticket_cancel_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送取消提示", Position: dsl.Position{X: 1760, Y: 320}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "send_ticket_cancel_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送取消提示", Position: dsl.Position{X: 2280, Y: 380}, Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "ticket_cancel_reply_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "retrieve_1", Type: workflowregistry.NodeTypeKnowledgeRetrieve, Name: "知识检索", Position: dsl.Position{X: 560, Y: 500}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "retrieve_1", Type: workflowregistry.NodeTypeKnowledgeRetrieve, Name: "知识检索", Position: dsl.Position{X: 1080, Y: 560}, Inputs: map[string]dsl.VariableSelector{
|
||||
"query": {NodeID: "start_1", Field: "userMessage"},
|
||||
}},
|
||||
{ID: "answerability_1", Type: workflowregistry.NodeTypeAnswerabilityGate, Name: "可回答判断", Position: dsl.Position{X: 860, Y: 500}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "answerability_1", Type: workflowregistry.NodeTypeAnswerabilityGate, Name: "可回答判断", Position: dsl.Position{X: 1380, Y: 560}, Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
"knowledgeItems": {NodeID: "retrieve_1", Field: "items"},
|
||||
}},
|
||||
{ID: "answerability_route_1", Type: workflowregistry.NodeTypeCondition, Name: "可回答分流", Position: dsl.Position{X: 1010, Y: 500}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "answerability_route_1", Type: workflowregistry.NodeTypeCondition, Name: "可回答分流", Position: dsl.Position{X: 1530, Y: 560}, Config: mustMarshalWorkflowConfig(dsl.ConditionConfig{Branches: []dsl.ConditionBranch{
|
||||
{ID: "answerable", Name: "可以回答", TargetNodeID: "reply_1", Condition: &dsl.Condition{Left: &dsl.VariableSelector{NodeID: "answerability_1", Field: "answerability"}, Operator: "eq", Right: "answerable"}},
|
||||
{ID: "default", Name: "兜底追问", TargetNodeID: "fallback_reply_1", Default: true},
|
||||
}})},
|
||||
{ID: "reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "AI 回复", Position: dsl.Position{X: 1160, Y: 440}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "AI 回复", Position: dsl.Position{X: 1680, Y: 500}, Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
"knowledgeItems": {NodeID: "retrieve_1", Field: "items"},
|
||||
}},
|
||||
{ID: "send_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送回复", Position: dsl.Position{X: 1460, Y: 440}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "send_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送回复", Position: dsl.Position{X: 1980, Y: 500}, Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "reply_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "fallback_reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "兜底追问", Position: dsl.Position{X: 1160, Y: 600}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "fallback_reply_1", Type: workflowregistry.NodeTypeLLMReply, Name: "兜底追问", Position: dsl.Position{X: 1680, Y: 620}, Inputs: map[string]dsl.VariableSelector{
|
||||
"userMessage": {NodeID: "start_1", Field: "userMessage"},
|
||||
"knowledgeItems": {NodeID: "retrieve_1", Field: "items"},
|
||||
}},
|
||||
{ID: "send_fallback_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送兜底", Position: dsl.Position{X: 1460, Y: 600}, Inputs: map[string]dsl.VariableSelector{
|
||||
{ID: "send_fallback_1", Type: workflowregistry.NodeTypeSendReply, Name: "发送兜底", Position: dsl.Position{X: 1980, Y: 620}, Inputs: map[string]dsl.VariableSelector{
|
||||
"replyText": {NodeID: "fallback_reply_1", Field: "replyText"},
|
||||
}},
|
||||
{ID: "end_1", Type: workflowregistry.NodeTypeEnd, Name: "结束", Position: dsl.Position{X: 2060, Y: 440}},
|
||||
{ID: "end_1", Type: workflowregistry.NodeTypeEnd, Name: "结束", Position: dsl.Position{X: 2580, Y: 500}},
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
{ID: "edge_start_route_intent", Source: "start_1", Target: "route_intent_1"},
|
||||
{ID: "edge_intent_handoff", Source: "route_intent_1", Target: "handoff_1"},
|
||||
{ID: "edge_intent_ticket", Source: "route_intent_1", Target: "draft_ticket_1"},
|
||||
{ID: "edge_intent_knowledge_default", Source: "route_intent_1", Target: "retrieve_1"},
|
||||
{ID: "edge_start_understanding", Source: "start_1", Target: "understanding_1"},
|
||||
{ID: "edge_understanding_policy", Source: "understanding_1", Target: "policy_1"},
|
||||
{ID: "edge_policy_route", Source: "policy_1", Target: "policy_route_1"},
|
||||
{ID: "edge_policy_reply", Source: "policy_route_1", Target: "policy_reply_1"},
|
||||
{ID: "edge_policy_handoff", Source: "policy_route_1", Target: "handoff_1"},
|
||||
{ID: "edge_policy_ticket", Source: "policy_route_1", Target: "draft_ticket_1"},
|
||||
{ID: "edge_policy_knowledge", Source: "policy_route_1", Target: "retrieve_1"},
|
||||
{ID: "edge_policy_reply_end", Source: "policy_reply_1", Target: "end_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"},
|
||||
|
||||
@@ -94,6 +94,7 @@ export type WorkflowDefinition = {
|
||||
|
||||
export type WorkflowVariableType =
|
||||
| "string"
|
||||
| "number"
|
||||
| "integer"
|
||||
| "boolean"
|
||||
| "object"
|
||||
|
||||
@@ -287,6 +287,7 @@ export type AIWorkflowPosition = {
|
||||
|
||||
export type AIWorkflowVariableType =
|
||||
| "string"
|
||||
| "number"
|
||||
| "integer"
|
||||
| "boolean"
|
||||
| "object"
|
||||
|
||||
Reference in New Issue
Block a user