router: a deterministic test for question-shaped text (V-470)
The predicate a fact write needs before it trusts a routing decision. Tokenized, not substring: 'что' inside 'чтобы' is not a question. Capture verbs win over every question signal, because 'запиши что я пил воду' contains an interrogative and is still a capture.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// interrogatives — the question words that mark an utterance as asking rather
|
||||||
|
// than telling. Tokenized, never substring: "что" inside "чтобы" and "как"
|
||||||
|
// inside "какао" are not questions.
|
||||||
|
var interrogatives = []string{
|
||||||
|
"что", "чего", "какой", "какая", "какое", "какие", "каких",
|
||||||
|
"кто", "кого", "кому", "чей", "почему", "зачем", "отчего",
|
||||||
|
"где", "куда", "откуда", "когда", "сколько", "как",
|
||||||
|
"what", "who", "whom", "why", "when", "where", "which", "how",
|
||||||
|
}
|
||||||
|
|
||||||
|
// narrativeRequests — "tell me about X" asks for knowledge Maven does not
|
||||||
|
// hold about him. It carries no question mark and no interrogative, which is
|
||||||
|
// how "расскажи про битву при Ватерлоо" reached the fact store (#470).
|
||||||
|
var narrativeRequests = []string{
|
||||||
|
"расскажи", "объясни", "опиши", "перечисли",
|
||||||
|
"tell", "explain", "describe",
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureVerbs — an explicit instruction to record something. These win over
|
||||||
|
// every test below, because "запиши что я пил воду" contains an interrogative
|
||||||
|
// and is still a capture: the word he said is "запиши".
|
||||||
|
var captureVerbs = []string{
|
||||||
|
"запиши", "запомни", "отметь", "заметь", "добавь", "сохрани",
|
||||||
|
"note", "remember", "log", "save",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsQuestionShaped reports whether text asks for something rather than
|
||||||
|
// records it. It is a deterministic offline test over tokens, so it costs
|
||||||
|
// nothing and never depends on the model that produced the routing decision.
|
||||||
|
//
|
||||||
|
// It exists because a mis-routed question used to be persisted as a fact
|
||||||
|
// about the owner, with the model's invented answer as the value (#470). The
|
||||||
|
// predicate is deliberately blunt: refusing to store a question is cheap and
|
||||||
|
// reversible, storing an invented fact about him is neither.
|
||||||
|
func IsQuestionShaped(text string) bool {
|
||||||
|
t := strings.TrimSpace(text)
|
||||||
|
if t == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
toks := planTokens(t)
|
||||||
|
for _, v := range captureVerbs {
|
||||||
|
if hasTok(toks, v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(t, "?") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, w := range interrogatives {
|
||||||
|
if hasTok(toks, w) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, w := range narrativeRequests {
|
||||||
|
if hasTok(toks, w) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsQuestionShaped(t *testing.T) {
|
||||||
|
// The seven utterances #470 recorded, plus the captures that must keep
|
||||||
|
// working. A capture misread as a question loses a fact; a question
|
||||||
|
// misread as a capture poisons recall, so the captures are the ones worth
|
||||||
|
// pinning here.
|
||||||
|
cases := []struct {
|
||||||
|
text string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"какая последняя версия языка Go?", true},
|
||||||
|
{"что дальше?", true},
|
||||||
|
{"расскажи про битву при Ватерлоо", true},
|
||||||
|
{"почему небо синее?", true},
|
||||||
|
{"какая столица Австралии?", true},
|
||||||
|
{"кто такой Никола Тесла?", true},
|
||||||
|
{"сколько стоит доллар", true},
|
||||||
|
{"who is the premier of Japan", true},
|
||||||
|
{"объясни линии Фраунгофера", true},
|
||||||
|
|
||||||
|
{"запиши что я пил воду", false},
|
||||||
|
{"запомни какая у меня машина", false},
|
||||||
|
{"отметь что я поужинал", false},
|
||||||
|
{"поужинал", false},
|
||||||
|
{"я выпил кофе", false},
|
||||||
|
{"вода", false},
|
||||||
|
{"привет", false},
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := IsQuestionShaped(c.text); got != c.want {
|
||||||
|
t.Errorf("IsQuestionShaped(%q) = %v, want %v", c.text, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substring matching is what made the day-plan predicates wrong before, and
|
||||||
|
// this predicate gates a write, so it gets the same guard.
|
||||||
|
func TestIsQuestionShapedIsTokenized(t *testing.T) {
|
||||||
|
for _, text := range []string{"чтобы не забыть, я полил кактус", "какао выпил"} {
|
||||||
|
if IsQuestionShaped(text) {
|
||||||
|
t.Errorf("IsQuestionShaped(%q) = true; a question word inside a longer word is not a question", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user