Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ad7635501 | |||
| 9949b309b1 | |||
| 62d47d28ac |
@@ -0,0 +1,26 @@
|
|||||||
|
package eval
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAddressTimeWordDoesNotBlind(t *testing.T) {
|
||||||
|
// A nudge that opens with a time word must still be caught. Without the
|
||||||
|
// time words in the stoplist, "сегодня" was read as the third party.
|
||||||
|
for _, s := range []string{
|
||||||
|
"сегодня он не ел 11 дней",
|
||||||
|
"вчера он не пил воду",
|
||||||
|
"опять он забыл про таблетки",
|
||||||
|
} {
|
||||||
|
if r := checkAddress(s); r.Pass {
|
||||||
|
t.Errorf("checkAddress(%q) passed, want a third-person failure", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Still must not fire when a third party really is named.
|
||||||
|
for _, s := range []string{
|
||||||
|
"сегодня сервис упал, он не отвечает",
|
||||||
|
"ты не пил воду четыре часа",
|
||||||
|
} {
|
||||||
|
if r := checkAddress(s); !r.Pass {
|
||||||
|
t.Errorf("checkAddress(%q) failed: %s", s, r.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,10 +21,14 @@ const (
|
|||||||
// CheckHisGender — the other half of the persona rule: SHE is feminine, HE
|
// CheckHisGender — the other half of the persona rule: SHE is feminine, HE
|
||||||
// is male. "ты давно не отдыхала" addresses the operator as a woman.
|
// is male. "ты давно не отдыхала" addresses the operator as a woman.
|
||||||
CheckHisGender = "hisgender"
|
CheckHisGender = "hisgender"
|
||||||
|
|
||||||
|
// CheckAddress — she talks TO him, informally, one to one. Not "вы", not
|
||||||
|
// "он". See the comment block above checkAddress.
|
||||||
|
CheckAddress = "address"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckNames — report order.
|
// CheckNames — report order.
|
||||||
var CheckNames = []string{CheckMood, CheckLang, CheckLength, CheckFeminine, CheckHisGender, CheckCringe, CheckOnTopic}
|
var CheckNames = []string{CheckMood, CheckLang, CheckLength, CheckFeminine, CheckHisGender, CheckAddress, CheckCringe, CheckOnTopic}
|
||||||
|
|
||||||
// Result — one check on one message.
|
// Result — one check on one message.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -57,6 +61,7 @@ func RunChecks(c Case, body, mood string) []Result {
|
|||||||
checkLength(body),
|
checkLength(body),
|
||||||
checkFeminine(body),
|
checkFeminine(body),
|
||||||
checkHisGender(body),
|
checkHisGender(body),
|
||||||
|
checkAddress(body),
|
||||||
checkCringe(body),
|
checkCringe(body),
|
||||||
checkOnTopic(c, body),
|
checkOnTopic(c, body),
|
||||||
}
|
}
|
||||||
@@ -302,6 +307,159 @@ func prevWord(words []string, i int) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- how she addresses him ------------------------------------------------
|
||||||
|
//
|
||||||
|
// Persona hard constraint: Maven speaks TO him, informally, one to one. The
|
||||||
|
// phrasing eval produced two breaks of it, and both scored clean:
|
||||||
|
//
|
||||||
|
// - "Приходите… Жду вас" — the formal plural. Correct is ты/тебя/тебе and a
|
||||||
|
// singular imperative ("приходи", "жду тебя").
|
||||||
|
// - "Он не ел 11 дней" — she talks ABOUT him, in the third person, as if
|
||||||
|
// reporting to somebody else. Correct is "ты не ел 11 дней".
|
||||||
|
//
|
||||||
|
// Like checkHisGender this is a keyword + suffix heuristic, NOT a parser. Every
|
||||||
|
// hit prints the word it tripped on, so a false alarm is obvious at a glance and
|
||||||
|
// can be dismissed.
|
||||||
|
//
|
||||||
|
// Part 1, formal address. Two signals:
|
||||||
|
// - the "вы" pronoun family, matched as whole words, so there is nothing to
|
||||||
|
// exclude — "вы" and "вас" are never anything else.
|
||||||
|
// - a plural verb ending: -ите/-ете/-йте/-ьте ("приходите", "выпейте",
|
||||||
|
// "не забудьте", "хотите"). Nouns in the prepositional case share those
|
||||||
|
// endings ("в интернете", "в свете"), so a word right after a preposition is
|
||||||
|
// skipped. That is the whole exclusion list, on purpose: a bigger one would
|
||||||
|
// start swallowing real imperatives.
|
||||||
|
//
|
||||||
|
// Part 2, third person. "он" is perfectly fine when the message really is about
|
||||||
|
// somebody or something else ("сервис упал, он не отвечает"). The way to tell
|
||||||
|
// them apart: a legitimate third person has an ANTECEDENT — the thing it refers
|
||||||
|
// to was named earlier in the message. So "он" is only flagged when nothing
|
||||||
|
// before it in the message could be that thing.
|
||||||
|
//
|
||||||
|
// Where this gives up, plainly:
|
||||||
|
// - it only looks BACKWARD. "Он не отвечает, сервис упал" names the subject
|
||||||
|
// after the pronoun and is flagged wrongly.
|
||||||
|
// - any noun earlier in the message counts as an antecedent, even when it is
|
||||||
|
// not one ("после обеда он не ел" reads as legitimate and is missed). The
|
||||||
|
// common time words are stoplisted so the usual nudge opening does not
|
||||||
|
// blind it, but a message with any other noun in front still slips through.
|
||||||
|
// This is the check's real hole; widening it further would start flagging
|
||||||
|
// legitimate third-party messages, so it stops here.
|
||||||
|
// - a message that opens with "ты" and only later slips into "он" is missed,
|
||||||
|
// because "ты" itself is skipped but the words around it are not.
|
||||||
|
// - formal address outside these endings (short adjectives, "вашими" style
|
||||||
|
// forms not listed) is missed.
|
||||||
|
|
||||||
|
// addressWordRE also takes Latin words, because "him"/"he" is the same break in
|
||||||
|
// English.
|
||||||
|
var addressWordRE = regexp.MustCompile(`[\p{Cyrillic}]+|[a-zA-Z]+|[,.;:!?…—-]`)
|
||||||
|
|
||||||
|
// formalPronouns — the "вы" family. Whole-word match, so no false hits.
|
||||||
|
var formalPronouns = map[string]bool{
|
||||||
|
"вы": true, "вас": true, "вам": true, "вами": true,
|
||||||
|
"ваш": true, "ваша": true, "ваше": true, "ваши": true,
|
||||||
|
"вашего": true, "вашей": true, "вашему": true, "вашим": true,
|
||||||
|
"вашими": true, "вашу": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepositions — used twice: to skip prepositional-case nouns that look like
|
||||||
|
// plural verbs, and as words that cannot be what "он" refers to.
|
||||||
|
var prepositions = map[string]bool{
|
||||||
|
"в": true, "во": true, "на": true, "о": true, "об": true, "обо": true,
|
||||||
|
"при": true, "по": true, "за": true, "из": true, "с": true, "со": true,
|
||||||
|
"к": true, "ко": true, "до": true, "от": true, "у": true, "над": true,
|
||||||
|
"под": true, "про": true, "без": true, "для": true, "через": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluralVerb reports whether a word looks like a plural/formal verb form:
|
||||||
|
// "приходите", "выпейте", "забудьте", "хотите".
|
||||||
|
func pluralVerb(w string) bool {
|
||||||
|
if len([]rune(w)) < 5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(w, "ите") || strings.HasSuffix(w, "ете") ||
|
||||||
|
strings.HasSuffix(w, "йте") || strings.HasSuffix(w, "ьте")
|
||||||
|
}
|
||||||
|
|
||||||
|
// thirdPersonHim — pronouns that would be talking about him instead of to him.
|
||||||
|
var thirdPersonHim = map[string]bool{
|
||||||
|
"он": true, "его": true, "ему": true, "него": true, "нему": true, "ним": true,
|
||||||
|
"he": true, "him": true, "his": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// notAnAntecedent — words that cannot be the thing "он" refers to: pronouns,
|
||||||
|
// particles, conjunctions, adverbs of time. If only these come before "он", the
|
||||||
|
// message never named a third party and "он" is him.
|
||||||
|
var notAnAntecedent = map[string]bool{
|
||||||
|
"не": true, "ни": true, "и": true, "а": true, "но": true, "да": true,
|
||||||
|
"же": true, "бы": true, "ли": true, "вот": true, "уже": true,
|
||||||
|
"ещё": true, "еще": true, "тоже": true, "там": true, "тут": true,
|
||||||
|
"здесь": true, "это": true, "что": true, "как": true, "когда": true,
|
||||||
|
"чтобы": true, "потому": true, "сейчас": true, "потом": true,
|
||||||
|
// Time words. A nudge almost always opens with one ("сегодня он не ел"),
|
||||||
|
// and without them the very next word is read as the person being talked
|
||||||
|
// about, so the check misses the exact break it was written for.
|
||||||
|
"сегодня": true, "вчера": true, "завтра": true, "послезавтра": true,
|
||||||
|
"утром": true, "днём": true, "днем": true, "вечером": true, "ночью": true,
|
||||||
|
"опять": true, "снова": true, "весь": true, "всю": true, "целый": true,
|
||||||
|
"я": true, "мне": true, "меня": true, "мной": true, "мы": true, "нас": true,
|
||||||
|
"ты": true, "тебя": true, "тебе": true, "тобой": true,
|
||||||
|
"твой": true, "твоя": true, "твоё": true, "твое": true, "твои": true, "твою": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// looksPastVerb — a past-tense verb needs a subject of its own, so it is not an
|
||||||
|
// antecedent either. Keeps "сервис упал, он не отвечает" working off "сервис".
|
||||||
|
func looksPastVerb(w string) bool {
|
||||||
|
if len([]rune(w)) < 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(w, "л") || strings.HasSuffix(w, "ла") ||
|
||||||
|
strings.HasSuffix(w, "ло") || strings.HasSuffix(w, "ли")
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAddress(body string) Result {
|
||||||
|
words := addressWordRE.FindAllString(strings.ToLower(body), -1)
|
||||||
|
|
||||||
|
for i, w := range words {
|
||||||
|
if formalPronouns[w] {
|
||||||
|
return Result{CheckAddress, false,
|
||||||
|
fmt.Sprintf("formal %q — she says ты/тебя/тебе", w)}
|
||||||
|
}
|
||||||
|
if pluralVerb(w) && !(i > 0 && prepositions[words[i-1]]) {
|
||||||
|
return Result{CheckAddress, false,
|
||||||
|
fmt.Sprintf("plural imperative %q — she uses the singular", w)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, w := range words {
|
||||||
|
if !thirdPersonHim[w] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
named := false
|
||||||
|
for j := 0; j < i; j++ {
|
||||||
|
p := words[j]
|
||||||
|
if !unicode.Is(unicode.Cyrillic, []rune(p)[0]) && !isLatinWord(p) {
|
||||||
|
continue // punctuation
|
||||||
|
}
|
||||||
|
if notAnAntecedent[p] || prepositions[p] || thirdPersonHim[p] || looksPastVerb(p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
named = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !named {
|
||||||
|
return Result{CheckAddress, false,
|
||||||
|
fmt.Sprintf("third person %q with nobody else named — she talks to him, not about him", w)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result{CheckAddress, true, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLatinWord(w string) bool {
|
||||||
|
r := []rune(w)[0]
|
||||||
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
||||||
|
}
|
||||||
|
|
||||||
// --- the cringe checks ---------------------------------------------------
|
// --- the cringe checks ---------------------------------------------------
|
||||||
//
|
//
|
||||||
// "Think Jarvis without the cringe part". DESIGN.md § Non-goals: "Not a
|
// "Think Jarvis without the cringe part". DESIGN.md § Non-goals: "Not a
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func TestStubBaseline(t *testing.T) {
|
|||||||
CheckLength: 12,
|
CheckLength: 12,
|
||||||
CheckFeminine: 15,
|
CheckFeminine: 15,
|
||||||
CheckHisGender: 15,
|
CheckHisGender: 15,
|
||||||
|
CheckAddress: 15,
|
||||||
CheckCringe: 15,
|
CheckCringe: 15,
|
||||||
CheckOnTopic: 12,
|
CheckOnTopic: 12,
|
||||||
}
|
}
|
||||||
@@ -123,6 +124,10 @@ func TestChecksCatchWhatTheyClaim(t *testing.T) {
|
|||||||
{"asks how he feels", "как ты себя чувствуешь? попей воды.", CheckCringe},
|
{"asks how he feels", "как ты себя чувствуешь? попей воды.", CheckCringe},
|
||||||
{"praise", "молодец! теперь попей воды.", CheckCringe},
|
{"praise", "молодец! теперь попей воды.", CheckCringe},
|
||||||
{"off topic", "пора бы уже что-то сделать.", CheckOnTopic},
|
{"off topic", "пора бы уже что-то сделать.", CheckOnTopic},
|
||||||
|
// The two recorded persona breaks from the phrasing eval run. Pinned as
|
||||||
|
// unit tests because an eval run is sampled and may not reproduce them.
|
||||||
|
{"formal plural", "Приходите… Жду вас", CheckAddress},
|
||||||
|
{"third person about him", "Он не ел 11 дней", CheckAddress},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -144,6 +149,36 @@ func TestChecksCatchWhatTheyClaim(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAddressCheck — the address check on its own, so the messages that must NOT
|
||||||
|
// trip it can be written without also having to satisfy the on-topic check.
|
||||||
|
func TestAddressCheck(t *testing.T) {
|
||||||
|
bad := []string{
|
||||||
|
"Приходите… Жду вас", // the recorded formal-plural break
|
||||||
|
"Он не ел 11 дней", // the recorded third-person break
|
||||||
|
"Выпейте воды, пожалуйста.", // plural imperative on its own
|
||||||
|
"Ваш обед был давно.", // formal possessive
|
||||||
|
}
|
||||||
|
for _, body := range bad {
|
||||||
|
if r := checkAddress(body); r.Pass {
|
||||||
|
t.Errorf("persona break not caught: %q", body)
|
||||||
|
} else {
|
||||||
|
t.Logf("%q -> %s", body, r.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
good := []string{
|
||||||
|
"ты не пил воду четыре часа — попей.", // correct informal address
|
||||||
|
"сервис netdata упал, он не отвечает.", // legitimately about a third party
|
||||||
|
"я заметила, что зарядка была утром.", // no address at all
|
||||||
|
"в интернете опять тихо, всё работает.", // "интернете" is a noun, not an imperative
|
||||||
|
}
|
||||||
|
for _, body := range good {
|
||||||
|
if r := checkAddress(body); !r.Pass {
|
||||||
|
t.Errorf("clean message flagged: %q -> %s", body, r.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMoodCheckUsesTheEnum(t *testing.T) {
|
func TestMoodCheckUsesTheEnum(t *testing.T) {
|
||||||
if r := checkMood("cheerful"); r.Pass {
|
if r := checkMood("cheerful"); r.Pass {
|
||||||
t.Error("mood outside the enum passed")
|
t.Error("mood outside the enum passed")
|
||||||
|
|||||||
Reference in New Issue
Block a user