updates on the ai and agent and all thse things awith onboarding
This commit is contained in:
152
internal/ai/registry/thresholds.go
Normal file
152
internal/ai/registry/thresholds.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ThresholdSpec is one tunable number on a skill: its default and the range an
|
||||
// operator may move it within. The ranges are the product's safety rails — a
|
||||
// stall timeout of 0 minutes or a cash cap of ₹10 lakh is a typo, not a policy.
|
||||
type ThresholdSpec struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Default float64 `json:"default"`
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
Step float64 `json:"step"`
|
||||
}
|
||||
|
||||
// onStep reports whether v sits on the spec's step grid, measured from Min.
|
||||
// Tolerant of float noise: 0.7 must pass a 0.05 step starting at 0.5.
|
||||
func (s ThresholdSpec) onStep(v float64) bool {
|
||||
if s.Step <= 0 {
|
||||
return true
|
||||
}
|
||||
n := (v - s.Min) / s.Step
|
||||
return math.Abs(n-math.Round(n)) < 1e-6
|
||||
}
|
||||
|
||||
func (s ThresholdSpec) check(v float64) error {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Errorf("%s must be a number", s.Key)
|
||||
}
|
||||
if v < s.Min || v > s.Max {
|
||||
return fmt.Errorf("%s must be between %s and %s", s.Key, fmtNum(s.Min), fmtNum(s.Max))
|
||||
}
|
||||
if !s.onStep(v) {
|
||||
return fmt.Errorf("%s must move in steps of %s from %s", s.Key, fmtNum(s.Step), fmtNum(s.Min))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtNum(v float64) string {
|
||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4f", v), "0"), ".")
|
||||
}
|
||||
|
||||
// EffectiveThresholds is what a skill actually runs with: the stored value for
|
||||
// every key the schema still defines and that is still in range, the default
|
||||
// for everything else. Keys the schema no longer has are dropped. A schema
|
||||
// change in code therefore never strands a skill on a value it can no longer
|
||||
// validate, and never needs a data migration.
|
||||
func EffectiveThresholds(schema []ThresholdSpec, stored map[string]float64) map[string]float64 {
|
||||
out := make(map[string]float64, len(schema))
|
||||
for _, s := range schema {
|
||||
v, ok := stored[s.Key]
|
||||
if !ok || s.check(v) != nil {
|
||||
v = s.Default
|
||||
}
|
||||
out[s.Key] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ApplyThresholdPatch validates an operator's patch against the schema and
|
||||
// returns the full resulting set. Unknown keys and non-numbers are refused, not
|
||||
// ignored: a misspelt key silently doing nothing is exactly the failure an
|
||||
// operator cannot see. All keys are checked before any error is returned, so
|
||||
// the message lists every problem at once.
|
||||
func ApplyThresholdPatch(schema []ThresholdSpec, current map[string]float64, patch map[string]any) (map[string]float64, error) {
|
||||
byKey := make(map[string]ThresholdSpec, len(schema))
|
||||
for _, s := range schema {
|
||||
byKey[s.Key] = s
|
||||
}
|
||||
|
||||
next := EffectiveThresholds(schema, current)
|
||||
var problems []string
|
||||
|
||||
keys := make([]string, 0, len(patch))
|
||||
for k := range patch {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
spec, known := byKey[k]
|
||||
if !known {
|
||||
problems = append(problems, fmt.Sprintf("%s is not a threshold of this skill", k))
|
||||
continue
|
||||
}
|
||||
v, isNum := patch[k].(float64)
|
||||
if !isNum {
|
||||
problems = append(problems, fmt.Sprintf("%s must be a number", k))
|
||||
continue
|
||||
}
|
||||
if err := spec.check(v); err != nil {
|
||||
problems = append(problems, err.Error())
|
||||
continue
|
||||
}
|
||||
next[k] = v
|
||||
}
|
||||
|
||||
if len(problems) > 0 {
|
||||
return nil, &ValidationError{Msg: strings.Join(problems, "; ")}
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// ParseThresholds reads a stored jsonb thresholds document. Empty, null or
|
||||
// malformed reads as "nothing stored", which EffectiveThresholds turns into
|
||||
// the defaults — a bad row degrades to defaults rather than failing a read.
|
||||
func ParseThresholds(raw string) map[string]float64 {
|
||||
out := map[string]float64{}
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return out
|
||||
}
|
||||
_ = json.Unmarshal([]byte(raw), &out)
|
||||
if out == nil {
|
||||
out = map[string]float64{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseSchema reads a stored thresholds schema. Malformed reads as no schema.
|
||||
func ParseSchema(raw string) []ThresholdSpec {
|
||||
var out []ThresholdSpec
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return []ThresholdSpec{}
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &out); err != nil || out == nil {
|
||||
return []ThresholdSpec{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DefaultThresholds is the value set a freshly seeded skill starts with.
|
||||
func DefaultThresholds(schema []ThresholdSpec) map[string]float64 {
|
||||
return EffectiveThresholds(schema, nil)
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
// Only ever called on values built in this package; a failure here is
|
||||
// a programming error, and the seed tests exercise every one.
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user