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.
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
// Package jevdecide is an HTTP client for the local jev-service typed decide API.
|
||
// Product wiring is fail-open: timeouts, auth errors, and circuit opens fall back
|
||
// to existing deterministic rules. See .doc/ops/short-term/2026-09-24-jev-product-shadow.md.
|
||
package jevdecide
|
||
|
||
import "strings"
|
||
|
||
// Mode controls product-side integration.
|
||
type Mode string
|
||
|
||
const (
|
||
ModeOff Mode = "off" // zero behavior change (default)
|
||
ModeShadow Mode = "shadow" // rules route; jev recorded for comparison
|
||
ModeAssist Mode = "assist" // jev may augment selected decisions (with rule floor)
|
||
)
|
||
|
||
// Question is one typed decide question (choice / noul / numeric).
|
||
type Question struct {
|
||
Type string `json:"type"`
|
||
Question string `json:"question"`
|
||
Options []string `json:"options,omitempty"`
|
||
Min *float64 `json:"min,omitempty"`
|
||
Max *float64 `json:"max,omitempty"`
|
||
}
|
||
|
||
// Answer is one typed decide answer.
|
||
type Answer struct {
|
||
Type string `json:"type"`
|
||
Choice string `json:"choice,omitempty"`
|
||
Confidence float64 `json:"confidence"`
|
||
Probabilities map[string]float64 `json:"probabilities,omitempty"`
|
||
Yes bool `json:"yes,omitempty"`
|
||
Noul float64 `json:"noul,omitempty"`
|
||
Value float64 `json:"value,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// Response is the /v1/decide envelope.
|
||
type Response struct {
|
||
Model string `json:"model"`
|
||
Answers map[string]Answer `json:"answers"`
|
||
}
|
||
|
||
// Margin returns top1−top2 probability gap (0 if fewer than two options).
|
||
func (a Answer) Margin() float64 {
|
||
if len(a.Probabilities) < 2 {
|
||
return a.Confidence
|
||
}
|
||
var top1, top2 float64
|
||
for _, p := range a.Probabilities {
|
||
if p >= top1 {
|
||
top2 = top1
|
||
top1 = p
|
||
} else if p > top2 {
|
||
top2 = p
|
||
}
|
||
}
|
||
return top1 - top2
|
||
}
|
||
|
||
// Reliable reports whether the answer is usable for assist routing.
|
||
func (a Answer) Reliable(minConf, minMargin float64) bool {
|
||
if a.Error != "" || strings.TrimSpace(a.Choice) == "" {
|
||
return false
|
||
}
|
||
return a.Confidence >= minConf && a.Margin() >= minMargin
|
||
}
|
||
|
||
// ProfileBucket maps a complexity score to a closed enum for jev choice.
|
||
func ProfileBucket(score int) string {
|
||
switch {
|
||
case score >= 60:
|
||
return "high"
|
||
case score >= 30:
|
||
return "mid"
|
||
default:
|
||
return "low"
|
||
}
|
||
}
|