package main import ( "log" "strings" "unicode" ) // ungroundedConfidence — what a self fact is worth when its value appears // nowhere in what he said. Below `query_min_score` is not the point (recall // gates on vector distance, not on this number); the point is that // `/history` and every future reader can tell a value he said from a value // the model supplied. const ungroundedConfidence = 0.6 // factConfidence scores a self fact by whether its value is grounded in the // utterance it came from. Grounded stays 1.00, which is what a tapped fact // has always been worth. Ungrounded drops, and says so in the log. // // An empty value is grounded by definition: the key alone carries the fact // ("поужинал"), and there is nothing for the model to have invented. func factConfidence(utterance, value string) float64 { if strings.TrimSpace(value) == "" { return 1.0 } if valueGrounded(utterance, value) { return 1.0 } log.Printf("voice: fact value %q is not in %q — writing at confidence %.2f", value, utterance, ungroundedConfidence) return ungroundedConfidence } // valueGrounded reports whether every word of value traces back to a word he // actually said. The comparison is on a 4-rune prefix, so the model's // normalization survives ("пил воду" → "вода") while an invented value // ("1.20" for a question about Go) does not. func valueGrounded(utterance, value string) bool { said := factTokens(utterance) words := factTokens(value) if len(words) == 0 { return true } for _, w := range words { if !anyTokenMatches(said, w) { return false } } return true } func anyTokenMatches(said []string, w string) bool { for _, s := range said { if s == w || sameStem(s, w) { return true } } return false } // sameStem is inflection tolerance and nothing more: it compares all but the // last rune of the shorter word, and never fewer than three. Russian marks // case on the ending, so "пил воду" and the stored "вода" are the same word he // said, while "1.20" and "версия" are not. A word of three runes or fewer must // match outright, where a shorter prefix would match half the language. func sameStem(a, b string) bool { ar, br := []rune(a), []rune(b) shorter := min(len(ar), len(br)) n := shorter - 1 if n < 3 || len(ar) < n || len(br) < n { return false } return string(ar[:n]) == string(br[:n]) } // factTokens lowercases and splits on everything that is not a letter or a // digit, the same shape planTokens uses in the router. func factTokens(s string) []string { return strings.FieldsFunc(strings.ToLower(s), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) }