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() locativeQuestions = lexicon.LocativeInterrogatives() 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 } return hasOpenQuestionTokens(toks) } // IsOpenQuestionShaped reports whether the words themselves ask for // information: an interrogative ("где", "как", "which") or a narrative // request ("расскажи", "explain"). A trailing question mark alone does not // qualify. That distinction matters to recall: punctuation can turn an // ordinary first-person proposition into a polar question, but it is not // evidence that an unrelated stored note answers it. // // Capture verbs keep the same precedence as IsQuestionShaped, so "запиши что // я пил" remains a write request even though it contains an interrogative. func IsOpenQuestionShaped(text string) bool { t := strings.TrimSpace(text) if t == "" { return false } toks := planTokens(t) for _, v := range captureVerbs { if hasTok(toks, v) { return false } } return hasOpenQuestionTokens(toks) } // IsLocativeQuestionShaped reports the open-question frame whose answer must // locate the object or event named by the user. Recall treats that named target // as evidence: a high cosine to an unrelated single stored note is not enough // to answer "where is my passport?" with the location of a spare key (V-719). // // The words are the complete locative subset of the interrogative lexicon, // and capture verbs retain precedence exactly as in IsQuestionShaped. func IsLocativeQuestionShaped(text string) bool { t := strings.TrimSpace(text) if t == "" { return false } toks := planTokens(t) for _, v := range captureVerbs { if hasTok(toks, v) { return false } } for _, w := range locativeQuestions { if hasTok(toks, w) { return true } } return false } func hasOpenQuestionTokens(toks []string) bool { for _, w := range interrogatives { if hasTok(toks, w) { return true } } for _, w := range narrativeRequests { if hasTok(toks, w) { return true } } return false }