Files
Agent-Intent/intent/spec.go
T
weichen b58595fb38 Implement the intent core
Details (original message):
Implement the intent core, conformance suite, catalog alignment, examples, and the intentctl diagnostics CLI.
2026-09-29 15:35:18 +00:00

181 lines
7.3 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 (
"fmt"
"regexp"
"strings"
)
// 风险等级。透传给宿主做授权与确认判断,本模块不解释。
const (
RiskLow = "low"
RiskMedium = "medium"
RiskHigh = "high"
)
// ConfidenceSpec 各字段为零值时使用包默认值。
const (
DefaultBase = 0.7
DefaultPatternBonus = 0.2
DefaultThreshold = 0.6
)
// MatchSpec 声明式匹配规则。
// 计分:score = base + (任一 pattern 命中 ? pattern_bonus : 0),上限 1.0。
// 命中条件:excludes_any 全不命中 && includes_all 全命中 &&(includes_any 至少一词命中
// 或任一 pattern 命中;仅配置 includes_all 时以全命中为准)。
// case_sensitive 只作用于 includes/excludes 词表;pattern 自行用 (?i) 表达大小写。
type MatchSpec struct {
IncludesAny []string `json:"includes_any,omitempty"`
IncludesAll []string `json:"includes_all,omitempty"`
ExcludesAny []string `json:"excludes_any,omitempty"`
Patterns []string `json:"patterns,omitempty"`
CaseSensitive bool `json:"case_sensitive,omitempty"`
}
// SlotSpec 槽位抽取规则。group 未填默认取 1(第一个捕获组)。
type SlotSpec struct {
Key string `json:"key"`
Required bool `json:"required,omitempty"`
Pattern string `json:"pattern"`
Group int `json:"group,omitempty"`
}
// ConfidenceSpec 置信度参数。Base 初始分;PatternBonus 模式命中加成;
// Threshold 为该 spec 自身的过阈线,score ≥ threshold 才进入候选。
type ConfidenceSpec struct {
Base float64 `json:"base,omitempty"`
PatternBonus float64 `json:"pattern_bonus,omitempty"`
Threshold float64 `json:"threshold,omitempty"`
}
// IntentSpec 意图定义,Go 注册与 JSON overlay 共用同一结构。
// Handler 与 Metadata 由宿主解释(路由通道、动作键等),本模块只透传。
type IntentSpec struct {
ID string `json:"id"`
Domain string `json:"domain"`
Version int `json:"version"`
Description string `json:"description,omitempty"`
Priority int `json:"priority,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Matcher MatchSpec `json:"matcher"`
Slots []SlotSpec `json:"slots,omitempty"`
Confidence ConfidenceSpec `json:"confidence,omitempty"`
Risk string `json:"risk,omitempty"`
RequiresConfirm bool `json:"requires_confirm,omitempty"`
Handler string `json:"handler,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
var (
domainRe = regexp.MustCompile(`^[a-z][a-z0-9-]{1,31}$`)
segmentRe = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`)
slotKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
)
// limits 在 Options 中声明,防止配置膨胀拖垮识别与构建。
type limits struct {
maxSpecs int
maxPatternsPerSpec int
maxSlotsPerSpec int
}
// validateSpec 校验单条 spec 的结构合法性,错误可定位到字段。limits 为零值时跳过数量上限。
func validateSpec(spec IntentSpec, lim limits) error {
if spec.Domain == "" {
return newError(InvalidSpec, "domain 为空", "field", "domain")
}
if !domainRe.MatchString(spec.Domain) {
return newError(InvalidSpec, "domain 格式不合法(^[a-z][a-z0-9-]{1,31}$)", "field", "domain", "domain", spec.Domain)
}
if spec.ID == "" {
return newError(InvalidSpec, "id 为空", "field", "id")
}
// id 形如 <domain>.<mnemonic>,mnemonic 可继续分级(demo.run.status_query)。
segments := strings.Split(spec.ID, ".")
if len(segments) < 2 {
return newError(InvalidSpec, "id 至少包含 domain 与记忆名两段", "field", "id", "id", spec.ID)
}
if segments[0] != spec.Domain {
return newError(InvalidSpec, "id 首段必须等于 domain", "field", "id", "id", spec.ID, "domain", spec.Domain)
}
for _, seg := range segments[1:] {
if !segmentRe.MatchString(seg) {
return newError(InvalidSpec, "id 段格式不合法(^[a-z][a-z0-9_-]{0,63}$)", "field", "id", "id", spec.ID)
}
}
if spec.Version < 1 {
return newError(InvalidSpec, "version 必须 ≥ 1", "field", "version", "version", spec.Version)
}
switch spec.Risk {
case "", RiskLow, RiskMedium, RiskHigh:
default:
return newError(InvalidSpec, "risk 只能是 low/medium/high 或留空", "field", "risk", "risk", spec.Risk)
}
m := spec.Matcher
for _, terms := range []struct {
name string
value []string
}{{"includes_any", m.IncludesAny}, {"includes_all", m.IncludesAll}, {"excludes_any", m.ExcludesAny}} {
for _, t := range terms.value {
if strings.TrimSpace(t) == "" {
return newError(InvalidSpec, "matcher 词条为空", "field", "matcher."+terms.name)
}
}
}
if len(m.IncludesAny) == 0 && len(m.IncludesAll) == 0 && len(m.Patterns) == 0 {
return newError(InvalidSpec, "matcher 缺少正向条件(includes_any / includes_all / patterns 至少一项)", "field", "matcher", "id", spec.ID)
}
if lim.maxPatternsPerSpec > 0 && len(m.Patterns) > lim.maxPatternsPerSpec {
return newError(InvalidSpec, "patterns 数量超过上限", "field", "matcher.patterns", "count", len(m.Patterns), "max", lim.maxPatternsPerSpec, "id", spec.ID)
}
for i, p := range m.Patterns {
if _, err := regexp.Compile(p); err != nil {
return newError(InvalidSpec, "pattern 编译失败", "field", fmt.Sprintf("matcher.patterns[%d]", i), "pattern", p, "id", spec.ID, "error", err.Error())
}
}
if lim.maxSlotsPerSpec > 0 && len(spec.Slots) > lim.maxSlotsPerSpec {
return newError(InvalidSpec, "slots 数量超过上限", "field", "slots", "count", len(spec.Slots), "max", lim.maxSlotsPerSpec, "id", spec.ID)
}
for i, s := range spec.Slots {
if !slotKeyRe.MatchString(s.Key) {
return newError(InvalidSpec, "slot key 格式不合法(^[a-z][a-z0-9_]{0,63}$)", "field", fmt.Sprintf("slots[%d].key", i), "key", s.Key)
}
re, err := regexp.Compile(s.Pattern)
if err != nil {
return newError(InvalidSpec, "slot pattern 编译失败", "field", fmt.Sprintf("slots[%d].pattern", i), "key", s.Key, "error", err.Error())
}
// group 约定:未填默认取 1(第一个捕获组);需整段匹配时在 pattern 里写捕获组即可。
g := effectiveGroup(s.Group)
if g < 1 || g > re.NumSubexp() {
return newError(InvalidSpec, "slot group 超出捕获组范围", "field", fmt.Sprintf("slots[%d].group", i), "key", s.Key, "group", s.Group, "groups", re.NumSubexp())
}
}
c := spec.Confidence
if c.Base < 0 || c.Base > 1 {
return newError(InvalidSpec, "confidence.base 须在 (0,1],0/留空取默认", "field", "confidence.base", "id", spec.ID)
}
if c.PatternBonus < 0 || c.PatternBonus >= 1 {
return newError(InvalidSpec, "confidence.pattern_bonus 须在 [0,1),留空取默认", "field", "confidence.pattern_bonus", "id", spec.ID)
}
if c.Threshold < 0 || c.Threshold > 1 {
return newError(InvalidSpec, "confidence.threshold 须在 (0,1],0/留空取默认", "field", "confidence.threshold", "id", spec.ID)
}
for k := range spec.Metadata {
if strings.TrimSpace(k) == "" {
return newError(InvalidSpec, "metadata 键为空", "field", "metadata", "id", spec.ID)
}
}
return nil
}
// effectiveGroup 槽位 group 的缺省规则:未填(0)默认取 1。
func effectiveGroup(g int) int {
if g == 0 {
return 1
}
return g
}