package intent import ( "sort" "strings" ) // CommandKind 是一条路由指令的类型。 type CommandKind string const ( // CommandStart 启动一个动作(流程/命令/蓝图/处理器),携带已抽取槽位。 CommandStart CommandKind = "start" // CommandSetSlot 记录一个槽位值(多轮澄清/预期回答的落槽)。 CommandSetSlot CommandKind = "set_slot" // CommandChangeSlot 改写一个已有槽位值。 CommandChangeSlot CommandKind = "change_slot" // CommandCancel 取消某个进行中的动作(否定语义的命令层表达)。 CommandCancel CommandKind = "cancel" // CommandClarify 向用户澄清:缺槽位或候选消歧。 CommandClarify CommandKind = "clarify" // CommandChitchat 闲聊(不由规则漏斗产出;留给语义层与宿主映射)。 CommandChitchat CommandKind = "chitchat" // CommandHandoff 转人工(不由规则漏斗产出;留给语义层与宿主映射)。 CommandHandoff CommandKind = "handoff" ) // ActionRef 指向一个可执行动作。auto 表示由宿主在命令总线与工作流之间 // 裁决(与现网「意图 ID 同时是命令 ID 与 workflow IntentID」的现状兼容); // handler 与 blueprint 来自 spec 字段的显式声明;command/workflow 预留给 // spec.action 显式声明(WP2)。 type ActionRef struct { Kind string `json:"kind"` ID string `json:"id"` } const ( ActionAuto = "auto" ActionHandler = "handler" ActionBlueprint = "blueprint" ActionCommand = "command" ActionWorkflow = "workflow" ) // Command 是一条对宿主路由层的指令。一次识别可产出多条命令 // (如先 SetSlot 后 Start),宿主按序消费。 type Command struct { Kind CommandKind `json:"kind"` Action ActionRef `json:"action,omitempty"` Slots map[string]string `json:"slots,omitempty"` Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` Missing []string `json:"missing,omitempty"` Question string `json:"question,omitempty"` Candidates []string `json:"candidates,omitempty"` Scope string `json:"scope,omitempty"` Target string `json:"target,omitempty"` } // actionOf 从候选推导动作引用:spec.action 显式声明优先, // 其后是 blueprint 键、Handler,都没有时回落 auto(宿主裁决)。 func actionOf(c Candidate) ActionRef { if c.Action != nil { return *c.Action } if bp := strings.TrimSpace(c.Metadata["blueprint"]); bp != "" { return ActionRef{Kind: ActionBlueprint, ID: bp} } if h := strings.TrimSpace(c.Handler); h != "" { return ActionRef{Kind: ActionHandler, ID: h} } return ActionRef{Kind: ActionAuto, ID: c.ID} } // deriveCommands 把识别结果展开为命令列表(纯函数、确定性)。 // 规则: // - guard_hit:不产出命令(守卫语义是「识别即拒绝」,由宿主处理; // Cancel 命令留给声明 action 的意图与语义层,不在规则漏斗内代为表达)。 // - expectation_hit:每个已填槽位一条 SetSlot;槽位齐全且合法追加 Start, // 否则追加 Clarify(缺槽/非法)。 // - matched:槽位齐全且合法 → 单条 Start;有缺失/非法 → Start(携带已抽槽, // 宿主可带槽启动并追问,与现网行为一致)+ Clarify。 // - ambiguous:单条 Clarify(候选消歧,按现有候选序)。 // - fallback:不产出命令(交给宿主默认通道;Chitchat/Handoff 不由规则漏斗产出)。 func deriveCommands(res Result, snap *Snapshot) []Command { if res.Top == nil { if res.Branch == BranchAmbiguous && len(res.Candidates) > 1 { ids := make([]string, 0, len(res.Candidates)) for _, c := range res.Candidates { ids = append(ids, c.ID) } return []Command{{Kind: CommandClarify, Candidates: ids}} } return nil } top := res.Top complete := len(top.MissingRequiredSlots) == 0 && len(top.InvalidSlots) == 0 switch res.Branch { case BranchExpectationHit: var cmds []Command for _, k := range sortedKeys(top.Slots) { cmds = append(cmds, Command{Kind: CommandSetSlot, Name: k, Value: top.Slots[k]}) } if complete { cmds = append(cmds, Command{Kind: CommandStart, Action: actionOf(*top), Slots: top.Slots}) } else { cmds = append(cmds, clarifyOf(*top, snap)) } return cmds case BranchMatched: start := Command{Kind: CommandStart, Action: actionOf(*top), Slots: top.Slots} if complete { return []Command{start} } return []Command{start, clarifyOf(*top, snap)} } return nil } // clarifyOf 组装澄清命令:Missing 为必填缺失与非法槽位的合并(确定性排序); // Question 取第一个有 Question 配置的缺失/非法槽位的建议提问(查得到 spec 时)。 func clarifyOf(top Candidate, snap *Snapshot) Command { invalid := sortedKeys(top.InvalidSlots) missing := append(append([]string(nil), top.MissingRequiredSlots...), invalid...) var question string if snap != nil { if spec, ok := snap.SpecByID(top.ID); ok { asked := map[string]bool{} for _, k := range missing { if asked[k] { continue } asked[k] = true for _, s := range spec.Slots { if s.Key == k && strings.TrimSpace(s.Question) != "" { question = s.Question break } } if question != "" { break } } } } return Command{Kind: CommandClarify, Missing: missing, Question: question} } func sortedKeys(m map[string]string) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys }