Files
Agent-System-Backend/internal/intentgate/gate.go
T
weichen 45b011af9f
ci / vet-and-test (pull_request) Failing after 34s
Wire the intent slow lane behind settings flags (I2 WP3)
Why: I2 increments 1-2 shipped the agent-intent ports (SemanticScorer,
CommandGenerator); the backend still had no implementation, so the slow
lane could not be enabled in the product.
Evidence: go test ./... green; lint-boundaries ok; the two new
runtime.intentSemantic / runtime.intentGenerator flags are registered
with default false and the gate defaults to the I1 recognizer when both
are off (TestIntentSlowLaneFlagsDefaultOff, TestGate*).
Impact: no behavior change by default; enabling runtime.intentSemantic
routes ambiguous rule matches through embedding cosine scoring, and
runtime.intentGenerator lets the lightweight model propose commands on
fallback with library-side fail-closed validation. Nested agent-intent
pin moves to 343b1e6 (I2 increments 1-2).
Refs: I2 increment 3
2026-10-07 11:20:41 +00:00

192 lines
5.4 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 []Command
}
// 命令类型经闸门投影(别名):生态库按边界规则只许本包导入,
// Router/runmgr 等消费方只依赖本包表面。
type (
Command = agentintent.Command
ActionRef = agentintent.ActionRef
)
const (
CommandStart = agentintent.CommandStart
ActionAuto = agentintent.ActionAuto
ActionCommand = agentintent.ActionCommand
ActionWorkflow = agentintent.ActionWorkflow
ActionBlueprint = agentintent.ActionBlueprint
ActionHandler = agentintent.ActionHandler
)
type Gate struct {
mu sync.RWMutex
reg *agentintent.Registry
rec *agentintent.FunnelRecognizer
specs int
specsByID map[string]agentintent.IntentSpec
// 慢通道(I2):nil = 不启用(默认,行为与 I1 一致)。
sem agentintent.SemanticScorer
semThresh float64
gen agentintent.CommandGenerator
}
func New() *Gate {
return &Gate{rec: agentintent.NewRecognizer(agentintent.Options{}, nil, nil)}
}
// SetSemantic 接线语义打分器(慢通道中位)。识别器随之重建;
// 传 nil 关闭语义层。阈值 ≤0 用库默认(0.8)。
func (g *Gate) SetSemantic(s agentintent.SemanticScorer, threshold float64) {
g.mu.Lock()
defer g.mu.Unlock()
g.sem, g.semThresh = s, threshold
g.rebuildRecognizerLocked()
}
// SetGenerator 接线模型命令生成器(慢通道末位)。识别器随之重建;
// 传 nil 关闭生成层。
func (g *Gate) SetGenerator(gen agentintent.CommandGenerator) {
g.mu.Lock()
defer g.mu.Unlock()
g.gen = gen
g.rebuildRecognizerLocked()
}
// rebuildRecognizerLocked 按当前慢通道配置重建识别器(调用方持写锁)。
func (g *Gate) rebuildRecognizerLocked() {
if g.sem == nil && g.gen == nil {
g.rec = agentintent.NewRecognizer(agentintent.Options{}, nil, nil)
return
}
g.rec = agentintent.NewRecognizer(agentintent.Options{
Semantic: g.sem,
SemanticThreshold: g.semThresh,
Generator: g.gen,
}, 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
}