Add typed jevdecide HTTP client (circuit, metrics) and coordinator DecideAssist wiring behind JEV_DECIDE_MODE (default off). Offline eval and synthetic soak evidence gate realtime/profile before assist rollout.
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package coordinator
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/jevdecide"
|
|
)
|
|
|
|
// DecideAssist is the narrow port for typed jev decisions (nil-safe).
|
|
// Implemented by *jevdecide.Client; injected from the app composition root.
|
|
type DecideAssist interface {
|
|
DecideRealtime(ctx context.Context, userInput string) (yes bool, ans jevdecide.Answer, err error)
|
|
DecideProfileBucket(ctx context.Context, userInput string) (bucket string, ans jevdecide.Answer, err error)
|
|
}
|
|
|
|
// SetDecideAssist injects the optional jev decide client; nil disables product integration.
|
|
func (e *CoordinatorEngine) SetDecideAssist(d DecideAssist) {
|
|
if e != nil {
|
|
e.decideAssist = d
|
|
}
|
|
}
|
|
|
|
// resolveRealtimeQuery applies rule + optional jev shadow/assist.
|
|
// Rules always run. In assist mode, high-confidence jev "yes" can only ADD recall
|
|
// (rule || jevYes); it never turns a rule-true into false.
|
|
func (e *CoordinatorEngine) resolveRealtimeQuery(ctx context.Context, userInput string) bool {
|
|
rule := IsRealtimeInformationQuery(userInput)
|
|
mode := jevdecide.CurrentMode()
|
|
if mode == jevdecide.ModeOff || e == nil || e.decideAssist == nil {
|
|
return rule
|
|
}
|
|
yes, ans, err := e.decideAssist.DecideRealtime(ctx, userInput)
|
|
if err != nil {
|
|
jevdecide.Global.JevFail.Add(1)
|
|
slog.Info("jevdecide realtime fail-open", "err", err.Error(), "rule", rule)
|
|
return rule
|
|
}
|
|
agree := yes == rule
|
|
if agree {
|
|
jevdecide.Global.Agree.Add(1)
|
|
} else {
|
|
jevdecide.Global.Disagree.Add(1)
|
|
}
|
|
slog.Info("jevdecide realtime shadow",
|
|
"mode", string(mode), "rule", rule, "jev", yes,
|
|
"confidence", ans.Confidence, "margin", ans.Margin(), "agree", agree)
|
|
|
|
if mode != jevdecide.ModeAssist {
|
|
return rule
|
|
}
|
|
// assist: conservative synthesis — rules are the floor
|
|
if yes && ans.Reliable(jevdecide.MinAssistConf, jevdecide.MinAssistMargin) {
|
|
if !rule {
|
|
jevdecide.Global.AssistHit.Add(1)
|
|
}
|
|
return true
|
|
}
|
|
return rule
|
|
}
|
|
|
|
// shadowProfile observes jev complexity bucket without changing routing.
|
|
func (e *CoordinatorEngine) shadowProfile(ctx context.Context, userInput string, profile TaskProfile) {
|
|
mode := jevdecide.CurrentMode()
|
|
if mode == jevdecide.ModeOff || e == nil || e.decideAssist == nil {
|
|
return
|
|
}
|
|
start := time.Now()
|
|
bucket, ans, err := e.decideAssist.DecideProfileBucket(ctx, userInput)
|
|
_ = start
|
|
ruleBucket := jevdecide.ProfileBucket(profile.ComplexityScore)
|
|
if err != nil {
|
|
jevdecide.Global.JevFail.Add(1)
|
|
slog.Info("jevdecide profile fail-open", "err", err.Error(), "rule_bucket", ruleBucket)
|
|
return
|
|
}
|
|
agree := bucket == ruleBucket
|
|
if agree {
|
|
jevdecide.Global.Agree.Add(1)
|
|
} else {
|
|
jevdecide.Global.Disagree.Add(1)
|
|
}
|
|
slog.Info("jevdecide profile shadow",
|
|
"mode", string(mode), "rule_bucket", ruleBucket, "jev_bucket", bucket,
|
|
"score", profile.ComplexityScore, "confidence", ans.Confidence, "agree", agree)
|
|
}
|