cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / functions / jev.go
jev.go 7.5K
1package functions23import (4 "strings"56 "src.rest/cat/012/internal/formula"7 "src.rest/cat/012/internal/value"8)910// JEV functions ask TypeSafe's hosted model (jev) typed questions about a11// value. They share one shape, like Sheets' own functions: the value to12// ask about (a cell, a range or text), the question in plain words, then13// what the answers mean. Each maps to a familiar spreadsheet or dataframe14// idea:15//16// JEV.TEST yes/no, like IF's condition or a boolean mask17// JEV.PROB the probability of yes, like predict_proba18// JEV.CLASSIFY one label from a list, like SWITCH or pd.cut19// JEV.SCORE a score on an ordered rubric, like a rating scale2021func init() {22 define(23 &FuncDef{Name: "JEV.TEST", Args: "value, question, [yes_means], [no_means]",24 Desc: "TRUE or FALSE, answered by the JEV model", Min: 2, Max: 4, Volatile: true,25 remote: yesNoCall, eval: remoteEval(yesNoCall, func(a RemoteAnswer) Value { return boolean(a.Noul >= 0.5) })},26 &FuncDef{Name: "JEV.PROB", Args: "value, question, [yes_means], [no_means]",27 Desc: "The probability the answer is yes, from the JEV model", Min: 2, Max: 4, Volatile: true,28 remote: yesNoCall, eval: remoteEval(yesNoCall, func(a RemoteAnswer) Value { return num(a.Noul) }),29 format: func([]Node, func(Node) Format) Format { return Format{Kind: value.FmtPercent} }},30 &FuncDef{Name: "JEV.CLASSIFY", Args: "value, question, labels, [descriptions]",31 Desc: "The label that fits best, chosen by the JEV model", Min: 3, Max: 4, Volatile: true,32 remote: choiceCall, eval: remoteEval(choiceCall, func(a RemoteAnswer) Value { return Value{Kind: value.Text, Str: a.Choice} })},33 &FuncDef{Name: "JEV.SCORE", Args: "value, question, levels",34 Desc: "A score on an ordered rubric, from the JEV model", Min: 3, Max: 3, Volatile: true,35 remote: scoreCall, eval: remoteEval(scoreCall, func(a RemoteAnswer) Value { return num(a.Score) })},36 )37}3839// Limits the JEV service enforces (typesafe.MaxChoiceOptions and40// MaxScoreLevels; internal/jev tests keep them in step). Checking them here41// shows #VALUE! at once instead of a request that is bound to fail.42const (43 maxChoiceLabels = 25544 maxScoreLevels = 1045)4647// ErrRemote is a question the model couldn't answer, e.g. a network48// failure; the context line says why.49var ErrRemote = Value{Kind: value.Error, Str: "#ERROR!"}5051// remoteEval evaluates a JEV function by asking the Book, which looks52// the answer up in the workbook's RemoteSource, converting the answer53// with result. Errors in the inputs come back as-is so they can be54// fixed; they're never sent.55func remoteEval(build func([]Node, lookup) (RemoteCall, error), result func(RemoteAnswer) Value) func([]Node, lookup) Value {56 return func(args []Node, get lookup) Value {57 call, err := build(args, get)58 if err != nil {59 return err.(inputError).v60 }61 ans, v := get.book.Ask(call)62 switch {63 case v.Kind == value.Error:64 return v65 case ans.Failed != "":66 return ErrRemote67 }68 return result(ans)69 }70}7172// jevWhole is the largest range a JEV function's value sees whole.73const jevWhole = 40967475// inputError carries a spreadsheet error found in a JEV function's inputs.76type inputError struct{ v Value }7778func (e inputError) Error() string { return e.v.Str }7980// jevState turns the value argument into what the model sees: text,81// numbers and booleans as themselves, a range as rows of them. A range82// of more than jevWhole cells leaves off the blank rows and columns past83// its data, so a whole column is its data.84func jevState(n Node, get lookup) (any, error) {85 if rn, ok := get.refOf(n).(formula.Range); ok {86 m := rectMatrix(rn.Sheet, rn.Rect, get)87 if m.blank.Kind == value.Error {88 return nil, inputError{m.blank}89 }90 rows, cols := m.rows, m.cols91 if m.size() > jevWhole {92 rows, cols = min(max(m.dataRows, 1), m.rows), min(max(m.dataCols, 1), m.cols)93 }94 var out [][]any95 for r := range rows {96 var row []any97 for c := range cols {98 v := m.cell(r, c)99 if v.Kind == value.Error {100 return nil, inputError{v}101 }102 row = append(row, plain(v))103 }104 out = append(out, row)105 }106 return out, nil107 }108 v := eval(n, get)109 if v.Kind == value.Error {110 return nil, inputError{v}111 }112 return plain(v), nil113}114115// plain converts a value to a JSON-friendly Go value.116func plain(v Value) any {117 switch v.Kind {118 case value.Number:119 return v.Num120 case value.Bool:121 return v.Num != 0122 case value.Text:123 return v.Str124 }125 return ""126}127128// jevText evaluates an argument that must be text, such as the question.129func jevText(n Node, get lookup) (string, error) {130 v := eval(n, get)131 if v.Kind == value.Error {132 return "", inputError{v}133 }134 return strings.TrimSpace(text(v)), nil135}136137// jevQuestion reads the question, which must not be empty.138func jevQuestion(n Node, get lookup) (string, error) {139 q, err := jevText(n, get)140 if err == nil && q == "" {141 err = inputError{value.ErrValue}142 }143 return q, err144}145146// jevList reads a list argument: a range of cells, or one text of147// comma-separated items like "positive, negative, neutral". Blanks are148// skipped.149func jevList(n Node, get lookup) ([]string, error) {150 var out []string151 if rn, ok := get.refOf(n).(formula.Range); ok {152 var err error153 get.cells(rn.Sheet, rn.Rect, func(_ Addr, v Value) bool {154 if v.Kind == value.Error {155 err = inputError{v}156 return false157 }158 if s := strings.TrimSpace(text(v)); s != "" {159 out = append(out, s)160 }161 return true162 })163 if err != nil {164 return nil, err165 }166 return out, nil167 }168 s, err := jevText(n, get)169 if err != nil {170 return nil, err171 }172 for _, item := range strings.Split(s, ",") {173 if item = strings.TrimSpace(item); item != "" {174 out = append(out, item)175 }176 }177 return out, nil178}179180// yesNoCall is shared by JEV.TEST and JEV.PROB, so asking both about the181// same thing is one request.182func yesNoCall(args []Node, get lookup) (RemoteCall, error) {183 state, err := jevState(args[0], get)184 if err != nil {185 return RemoteCall{}, err186 }187 q, err := jevQuestion(args[1], get)188 if err != nil {189 return RemoteCall{}, err190 }191 var means [2]string192 for i := range 2 {193 if len(args) > 2+i {194 if means[i], err = jevText(args[2+i], get); err != nil {195 return RemoteCall{}, err196 }197 }198 }199 return RemoteCall{Kind: "noul", State: state, Instructions: q, Criteria: means}, nil200}201202func choiceCall(args []Node, get lookup) (RemoteCall, error) {203 state, err := jevState(args[0], get)204 if err != nil {205 return RemoteCall{}, err206 }207 q, err := jevQuestion(args[1], get)208 if err != nil {209 return RemoteCall{}, err210 }211 labels, err := jevList(args[2], get)212 if err != nil {213 return RemoteCall{}, err214 }215 var descs []string216 if len(args) > 3 {217 if descs, err = jevList(args[3], get); err != nil {218 return RemoteCall{}, err219 }220 }221 if len(labels) < 2 || len(labels) > maxChoiceLabels {222 return RemoteCall{}, inputError{value.ErrValue} // nothing to choose between, or too many223 }224 criteria := make(map[string]string, len(labels))225 for i, l := range labels {226 criteria[l] = l227 if i < len(descs) {228 criteria[l] = descs[i]229 }230 }231 return RemoteCall{Kind: "choice", State: state, Instructions: q, Criteria: criteria}, nil232}233234func scoreCall(args []Node, get lookup) (RemoteCall, error) {235 state, err := jevState(args[0], get)236 if err != nil {237 return RemoteCall{}, err238 }239 q, err := jevQuestion(args[1], get)240 if err != nil {241 return RemoteCall{}, err242 }243 levels, err := jevList(args[2], get)244 if err != nil {245 return RemoteCall{}, err246 }247 if len(levels) < 2 || len(levels) > maxScoreLevels {248 return RemoteCall{}, inputError{value.ErrValue}249 }250 return RemoteCall{Kind: "score", State: state, Instructions: q, Criteria: levels}, nil251}