Files
Agent-System-Backend/internal/intentgate/gate.go
T
weichen ff43c7c9aa
ci / vet-and-test (pull_request) Successful in 41s
Unify intent routing behind the route package and enable NFKC
Why: I1 WP3 - chat routing and the HITL free-text pivot now share one
router consuming the library's command list, and both intent hosts
fold compatibility-equivalent input.

Evidence: go vet clean; go test ./... 30 packages ok, including the
existing workflow-route and server e2e suites unchanged (behavior
parity) and the new route package tests.

Impact: new internal/route package (unique router with explicit
priority rules); intentgate carries Commands and folds NFKC;
server.go and runmgr.go route through it; nested pins at intent
2554b50 and squad 6b7a991. Blueprint actions resolve to a squad
decision at the router level; per-run squad entry stays a recorded
follow-up (no silent misrouting).

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP3/WP4
2026-10-07 07:44:31 +00:00

141 lines
3.8 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 intentgate 接入 agent-intent(生态仓):插件意图 overlay → 注册表 →
// HITL 自由输入路由的确定性识别(设计 4.5 三分支之「命中其他意图→转轨」)。
// 未配置/未装载意图 = 行为回退(T0:直接按澄清回填),识别失败不阻断。
package intentgate
import (
"context"
"encoding/json"
"fmt"
"sync"
agentintent "agent-intent/intent"
)
// SlotQuestion 是命中意图后仍缺的必填槽位,以及建议的提问。
type SlotQuestion struct {
Key string
Question string
}
// MatchResult 是一次识别。Branch 与 Match 相同。
type MatchResult struct {
Branch string
ID string
Slots map[string]string
Missing []SlotQuestion
// Commands 是库层展开的命令列表(I1);Router 消费它做唯一裁决。
// guard_hit/expectation_hit 被折叠为 fallback 时为空。
Commands []agentintent.Command
}
type Gate struct {
mu sync.RWMutex
reg *agentintent.Registry
rec *agentintent.FunnelRecognizer
specs int
specsByID map[string]agentintent.IntentSpec
}
func New() *Gate {
return &Gate{rec: agentintent.NewRecognizer(agentintent.Options{}, nil, nil)}
}
// Rebuild 用插件意图 overlay 重建注册表;失败保留上一快照(fail-closed)并返回错误。
func (g *Gate) Rebuild(ctx context.Context, overlay []json.RawMessage) error {
if len(overlay) == 0 {
g.mu.Lock()
g.reg, g.specs, g.specsByID = nil, 0, nil
g.mu.Unlock()
return nil
}
specs := make([]agentintent.IntentSpec, 0, len(overlay))
for i, raw := range overlay {
var s agentintent.IntentSpec
if err := json.Unmarshal(raw, &s); err != nil {
return fmt.Errorf("intentgate: spec[%d]: %w", i, err)
}
specs = append(specs, s)
}
b := agentintent.NewRegistryBuilder(agentintent.Options{TopN: 3, NFKC: true})
if err := b.Register(specs...); err != nil {
return fmt.Errorf("intentgate: register: %w", err)
}
reg, err := b.Build()
if err != nil {
return fmt.Errorf("intentgate: build: %w", err)
}
byID := make(map[string]agentintent.IntentSpec, len(specs))
for _, s := range specs {
byID[s.ID] = s
}
g.mu.Lock()
g.reg, g.specs, g.specsByID = reg, len(specs), byID
g.mu.Unlock()
return nil
}
// Match 识别自由输入;返回 (branch, topID)。
// branch:matched | ambiguous | fallback | ""(未配置/系统级错误 → 回退通道)。
func (g *Gate) Match(ctx context.Context, input string) (string, string) {
res := g.Recognize(ctx, input)
return res.Branch, res.ID
}
// Recognize 在 Match 的结果上带上槽位和缺失提问。
func (g *Gate) Recognize(ctx context.Context, input string) MatchResult {
g.mu.RLock()
reg, rec, byID := g.reg, g.rec, g.specsByID
g.mu.RUnlock()
if reg == nil {
return MatchResult{}
}
res, err := rec.Recognize(ctx, input, reg.Snapshot())
if err != nil {
return MatchResult{}
}
out := MatchResult{Branch: "fallback"}
switch res.Branch {
case agentintent.BranchMatched:
out.Branch = "matched"
out.Commands = res.Commands
case agentintent.BranchAmbiguous:
out.Branch = "ambiguous"
}
if res.Top == nil {
return out
}
out.ID = res.Top.ID
if len(res.Top.Slots) > 0 {
out.Slots = map[string]string{}
for k, v := range res.Top.Slots {
out.Slots[k] = v
}
}
spec := byID[out.ID]
for _, key := range res.Top.MissingRequiredSlots {
q := key
for _, sl := range spec.Slots {
if sl.Key == key && sl.Question != "" {
q = sl.Question
}
}
out.Missing = append(out.Missing, SlotQuestion{Key: key, Question: q})
}
return out
}
// Enabled 报告是否装载了意图(诊断/设置展示用)。
func (g *Gate) Enabled() bool {
g.mu.RLock()
defer g.mu.RUnlock()
return g.reg != nil && g.specs > 0
}
// SpecCount 返回已装载意图数。
func (g *Gate) SpecCount() int {
g.mu.RLock()
defer g.mu.RUnlock()
return g.specs
}