27bb9119fb
A parked question consumed whatever came next. One act she could not fulfil ate three turns: "выключи свет в спальне" asked "Что сделать?", and "кто изобрёл телефон" was scored as an answer to it, then "как дела" after that. Nothing tested whether the words could be an answer. The test is two offline token checks that already existed for other callers: a question shape, or a capture verb. It fires only where the answer filled nothing, so an answer that closes the gap still lands whatever shape it has, and the retry budget is untouched — the count was never the problem.
81 lines
2.8 KiB
Go
81 lines
2.8 KiB
Go
package router
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/kami/maven/internal/lexicon"
|
|
)
|
|
|
|
// The three word sets this file tests against are closed classes, so they live
|
|
// complete in internal/lexicon rather than inline here (Vikunja #525). The
|
|
// inline lists were short: no "чем", no "чём", no "кем", no declined "какой",
|
|
// so "чем ты занята" carried no interrogative at all and read as a statement.
|
|
//
|
|
// interrogatives mark an utterance as asking rather than telling.
|
|
// narrativeRequests are "tell me about X", which asks for knowledge Maven does
|
|
// not hold about him and carries neither a question mark nor an interrogative —
|
|
// that is how "расскажи про битву при Ватерлоо" reached the fact store (#470).
|
|
// captureVerbs win over both, because "запиши что я пил воду" contains an
|
|
// interrogative and is still a capture: the word he said is "запиши".
|
|
//
|
|
// All three are matched over tokens, never as substrings: "что" inside "чтобы"
|
|
// and "как" inside "какао" are not questions.
|
|
var (
|
|
interrogatives = lexicon.Interrogatives()
|
|
narrativeRequests = lexicon.NarrativeRequests()
|
|
captureVerbs = lexicon.CaptureVerbs()
|
|
)
|
|
|
|
// CarriesCaptureVerb reports whether text tells Maven to write something down.
|
|
// Sibling of IsQuestionShaped and matched over the same tokens, and the two do
|
|
// not overlap: IsQuestionShaped returns false for anything this returns true
|
|
// for, because "запиши что я пил воду" is a capture and not a question.
|
|
//
|
|
// Both exist together so a caller can ask "is this its own request?" — a
|
|
// clarify answer that asks a question or orders a capture is not an answer
|
|
// (Vikunja #554).
|
|
func CarriesCaptureVerb(text string) bool {
|
|
toks := planTokens(strings.TrimSpace(text))
|
|
for _, v := range captureVerbs {
|
|
if hasTok(toks, v) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
}
|