Files
Agent-Intent/intent/command.go
T
weichen fa512ec03c
ci / vet-and-test (pull_request) Successful in 17s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add command model, priority tie-break, and boundary matching
Why: I1 increment one - recognition output becomes a command list
for the host router, same-score ties resolve by priority (I5), and
CJK substring false positives get an opt-in boundary mode (I3).

Evidence: go vet clean; go test ./... all ok, including new command
(6), boundary (3), and tie-break tests; intentctl lint smoke run
shows boundary hints on short unbounded terms. Existing classifier
and ambiguity fixtures repointed to an equal-priority pair; the
greet corpus case now expects the I5 tie-break outcome.

Impact: Result gains Commands; recognizer derives them at one point
(guard/fallback stay empty); MatchSpec gains Boundary (default off,
legacy behavior unchanged); lint gains boundary hints. Top/Branch
stay for compatibility.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP1/WP4/WP5
2026-10-07 07:21:55 +00:00

154 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 从候选推导动作引用:显式 blueprint 键优先于 Handler,
// 都没有时回落 auto(宿主裁决)。
func actionOf(c Candidate) ActionRef {
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
}