Details (original message): Add deterministic slot constraints with labels and questions, and the planner-facing capability catalog.
293 lines
8.2 KiB
Go
293 lines
8.2 KiB
Go
// intentctl 是意图配置的只读诊断 CLI:lint 校验配置并报告冲突,
|
||
// explain 输出单条输入的裁决轨迹,eval 跑评测集。所有命令不改配置、不写文件。
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
|
||
"agent-intent/intent"
|
||
)
|
||
|
||
func main() {
|
||
if err := run(os.Args[1:]); err != nil {
|
||
fmt.Fprintln(os.Stderr, "intentctl:", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func run(args []string) error {
|
||
if len(args) < 1 {
|
||
return usage()
|
||
}
|
||
var err error
|
||
switch args[0] {
|
||
case "lint":
|
||
err = cmdLint(args[1:])
|
||
case "explain":
|
||
err = cmdExplain(args[1:])
|
||
case "eval":
|
||
err = cmdEval(args[1:])
|
||
case "catalog":
|
||
err = cmdCatalog(args[1:])
|
||
default:
|
||
err = usage()
|
||
}
|
||
return err
|
||
}
|
||
|
||
func usage() error {
|
||
return fmt.Errorf("用法:intentctl <lint|explain|eval|catalog> ...\n lint -root <dir> [-protected a,b] [-guard a,b]\n explain -root <dir> [-protected a,b] [-guard a,b] <input>\n eval -root <dir> [-protected a,b] [-guard a,b] <corpus.json>\n catalog -root <dir> [-protected a,b] [-guard a,b] 输出能力目录(注入规划者/领导提示词用)")
|
||
}
|
||
|
||
type commonOpts struct {
|
||
root string
|
||
protected string
|
||
guard string
|
||
}
|
||
|
||
func parseCommon(fs *flag.FlagSet) *commonOpts {
|
||
c := &commonOpts{}
|
||
fs.StringVar(&c.root, "root", "", "配置目录(*.json,必填)")
|
||
fs.StringVar(&c.protected, "protected", "guard", "保护域,逗号分隔")
|
||
fs.StringVar(&c.guard, "guard", "guard", "守卫域,逗号分隔")
|
||
return c
|
||
}
|
||
|
||
func buildRegistry(c *commonOpts) (*intent.Registry, error) {
|
||
if c.root == "" {
|
||
return nil, fmt.Errorf("-root 必填")
|
||
}
|
||
b := intent.NewRegistryBuilder(intent.Options{
|
||
ProtectedDomains: splitDomains(c.protected),
|
||
GuardDomains: splitDomains(c.guard),
|
||
})
|
||
return b.BuildFS(context.Background(), os.DirFS(c.root), ".")
|
||
}
|
||
|
||
func splitDomains(s string) []string {
|
||
if strings.TrimSpace(s) == "" {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for _, p := range strings.Split(s, ",") {
|
||
if p = strings.TrimSpace(p); p != "" {
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cmdLint(args []string) error {
|
||
fs := flag.NewFlagSet("lint", flag.ContinueOnError)
|
||
c := parseCommon(fs)
|
||
if err := fs.Parse(args); err != nil {
|
||
return err
|
||
}
|
||
reg, err := buildRegistry(c)
|
||
if err != nil {
|
||
return fmt.Errorf("配置校验失败:%w", err)
|
||
}
|
||
snap := reg.Snapshot()
|
||
fmt.Printf("快照 revision=%d 启用=%d 禁用=%d\n", snap.Revision(), len(snap.SpecIDs()), len(snap.DisabledIDs()))
|
||
for _, id := range snap.SpecIDs() {
|
||
src, _ := snap.Source(id)
|
||
fmt.Printf(" spec %s (%s)\n", id, src)
|
||
}
|
||
for _, id := range snap.DisabledIDs() {
|
||
src, _ := snap.Source(id)
|
||
fmt.Printf(" disabled %s (%s)\n", id, src)
|
||
}
|
||
conflicts := snap.Conflicts()
|
||
if len(conflicts) == 0 {
|
||
fmt.Println("共享词条提示:无")
|
||
return nil
|
||
}
|
||
fmt.Printf("共享词条提示(%d 条,非错误,计分与确定性次序会裁决):\n", len(conflicts))
|
||
for _, cf := range conflicts {
|
||
fmt.Printf(" %s 与 %s 共享词条 %q\n", cf.A, cf.B, cf.Term)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cmdExplain(args []string) error {
|
||
fs := flag.NewFlagSet("explain", flag.ContinueOnError)
|
||
c := parseCommon(fs)
|
||
if err := fs.Parse(args); err != nil {
|
||
return err
|
||
}
|
||
if fs.NArg() < 1 {
|
||
return fmt.Errorf("explain 需要一个输入参数")
|
||
}
|
||
input := fs.Arg(0)
|
||
reg, err := buildRegistry(c)
|
||
if err != nil {
|
||
return fmt.Errorf("配置校验失败:%w", err)
|
||
}
|
||
snap := reg.Snapshot()
|
||
rec := intent.NewRecognizer(intent.Options{}, nil, nil)
|
||
res, err := rec.Recognize(context.Background(), input, snap)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Printf("输入:%q(%d 字节)\n分支:%s\n", input, len(input), res.Branch)
|
||
if res.Top != nil {
|
||
fmt.Printf("Top:%s 置信度=%.4f 风险=%s 确认=%v handler=%s\n", res.Top.ID, res.Top.Confidence, res.Top.Risk, res.Top.RequiresConfirm, res.Top.Handler)
|
||
if len(res.Top.Slots) > 0 {
|
||
keys := make([]string, 0, len(res.Top.Slots))
|
||
for k := range res.Top.Slots {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
for _, k := range keys {
|
||
fmt.Printf(" 槽位 %s=%q\n", k, res.Top.Slots[k])
|
||
}
|
||
}
|
||
if len(res.Top.MissingRequiredSlots) > 0 {
|
||
fmt.Printf(" 缺失必填槽位:%s\n", strings.Join(res.Top.MissingRequiredSlots, ","))
|
||
}
|
||
if len(res.Top.InvalidSlots) > 0 {
|
||
fmt.Printf(" 非法槽位:")
|
||
for _, k := range sortedKeys(res.Top.InvalidSlots) {
|
||
fmt.Printf("%s(%s) ", k, res.Top.InvalidSlots[k])
|
||
}
|
||
fmt.Println()
|
||
}
|
||
}
|
||
if len(res.Candidates) > 0 {
|
||
fmt.Printf("候选(%d):\n", len(res.Candidates))
|
||
for _, cand := range res.Candidates {
|
||
fmt.Printf(" %.4f %-28s %s\n", cand.Confidence, cand.ID, cand.Reason)
|
||
}
|
||
}
|
||
fmt.Println("裁决轨迹:")
|
||
trace := snap.Explain(input)
|
||
fmt.Printf(" margin=%.4f\n", trace.AmbiguityMargin)
|
||
printStage(" 守卫", trace.Guard, trace.GuardRejected)
|
||
if trace.Expectation != nil {
|
||
fmt.Printf(" 挂起预期:%v 命中=%s\n", trace.Expectation.PendingIDs, trace.Expectation.HitID)
|
||
}
|
||
printStage(" 规则", trace.Rules, trace.RulesRejected)
|
||
return nil
|
||
}
|
||
|
||
func printStage(name string, scored []intent.ScoredSpec, rejected []intent.RejectedSpec) {
|
||
if len(scored) == 0 && len(rejected) == 0 {
|
||
fmt.Printf("%s:未评估\n", name)
|
||
return
|
||
}
|
||
for _, s := range scored {
|
||
fmt.Printf("%s 命中 %-28s %.4f %s %s\n", name, s.ID, s.Score, strings.Join(s.HitTerms, ","), strings.Join(s.HitPatterns, ","))
|
||
}
|
||
for _, r := range rejected {
|
||
fmt.Printf("%s 排除 %-28s %s\n", name, r.ID, r.Reason)
|
||
}
|
||
}
|
||
|
||
func cmdCatalog(args []string) error {
|
||
fs := flag.NewFlagSet("catalog", flag.ContinueOnError)
|
||
c := parseCommon(fs)
|
||
if err := fs.Parse(args); err != nil {
|
||
return err
|
||
}
|
||
reg, err := buildRegistry(c)
|
||
if err != nil {
|
||
return fmt.Errorf("配置校验失败:%w", err)
|
||
}
|
||
fmt.Print(reg.Snapshot().CatalogMarkdown())
|
||
return nil
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// evalCase 与 intent 包评测集(testdata/corpus.json)同格式。
|
||
type evalCase struct {
|
||
Name string `json:"name"`
|
||
Input string `json:"input"`
|
||
ExpectBranch string `json:"expect_branch"`
|
||
ExpectTop string `json:"expect_top"`
|
||
ExpectSlots map[string]string `json:"expect_slots"`
|
||
ExpectMissingSlots []string `json:"expect_missing_slots"`
|
||
}
|
||
|
||
type evalCorpus struct {
|
||
Cases []evalCase `json:"cases"`
|
||
}
|
||
|
||
func cmdEval(args []string) error {
|
||
fs := flag.NewFlagSet("eval", flag.ContinueOnError)
|
||
c := parseCommon(fs)
|
||
if err := fs.Parse(args); err != nil {
|
||
return err
|
||
}
|
||
if fs.NArg() < 1 {
|
||
return fmt.Errorf("eval 需要评测集路径参数")
|
||
}
|
||
data, err := os.ReadFile(fs.Arg(0))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var corpus evalCorpus
|
||
if err := json.Unmarshal(data, &corpus); err != nil {
|
||
return fmt.Errorf("评测集解析失败:%w", err)
|
||
}
|
||
if len(corpus.Cases) == 0 {
|
||
return fmt.Errorf("评测集为空")
|
||
}
|
||
reg, err := buildRegistry(c)
|
||
if err != nil {
|
||
return fmt.Errorf("配置校验失败:%w", err)
|
||
}
|
||
snap := reg.Snapshot()
|
||
rec := intent.NewRecognizer(intent.Options{}, nil, nil)
|
||
|
||
failed := 0
|
||
for _, tc := range corpus.Cases {
|
||
res, err := rec.Recognize(context.Background(), tc.Input, snap)
|
||
if err != nil {
|
||
fmt.Printf("FAIL %-28s 识别错误:%v\n", tc.Name, err)
|
||
failed++
|
||
continue
|
||
}
|
||
if string(res.Branch) != tc.ExpectBranch || (tc.ExpectTop != "" && (res.Top == nil || res.Top.ID != tc.ExpectTop)) {
|
||
gotTop := "(none)"
|
||
if res.Top != nil {
|
||
gotTop = res.Top.ID
|
||
}
|
||
fmt.Printf("FAIL %-28s 输入=%q 分支=%s(期望 %s) top=%s(期望 %s)\n", tc.Name, tc.Input, res.Branch, tc.ExpectBranch, gotTop, tc.ExpectTop)
|
||
failed++
|
||
continue
|
||
}
|
||
slotOK := true
|
||
for k, v := range tc.ExpectSlots {
|
||
if res.Top == nil || res.Top.Slots[k] != v {
|
||
slotOK = false
|
||
break
|
||
}
|
||
}
|
||
if !slotOK {
|
||
fmt.Printf("FAIL %-28s 槽位不符:%v\n", tc.Name, res.Top.Slots)
|
||
failed++
|
||
continue
|
||
}
|
||
fmt.Printf("PASS %-28s %s\n", tc.Name, res.Branch)
|
||
}
|
||
fmt.Printf("总计 %d 用例:%d 通过,%d 失败\n", len(corpus.Cases), len(corpus.Cases)-failed, failed)
|
||
if failed > 0 {
|
||
return fmt.Errorf("%d/%d 用例失败", failed, len(corpus.Cases))
|
||
}
|
||
return nil
|
||
}
|