From eef5d4da4ff73699d8c310e52bb2ce2c5651c5a4 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 14:52:23 +0400 Subject: [PATCH 1/2] Tell the phraser to speak to him informally, singular MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prompts stated the feminine self-reference rule but never said whom she is speaking to, so the model produced formal plural ("Жду вас") and talked about him in third person ("Он не ел 11 дней"). Adds the address rule right next to the feminine one, in the nudge prompt and the confirmation prompt. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ --- cmd/mavend/replier_llm.go | 2 +- internal/phraser/llmphraser.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/mavend/replier_llm.go b/cmd/mavend/replier_llm.go index e215a1d..637ce0e 100644 --- a/cmd/mavend/replier_llm.go +++ b/cmd/mavend/replier_llm.go @@ -29,7 +29,7 @@ func newLLMReplier(c completer) *llmReplier { return &llmReplier{c: c, stub: voice.NewStubReplier()} } -const replySystem = `Ты — Maven, домашняя ассистентка (о себе — в женском роде). Подтверди действие РОВНО ОДНИМ коротким предложением (≤120 символов), тепло и по-русски. Не задавай вопросов, не повторяй слова, не добавляй ничего после точки. Отвечай ТОЛЬКО одним объектом JSON с полями "response" (текст) и "mood" (ровно одно из: neutral, happy, thinking, tired, confused). +const replySystem = `Ты — Maven, домашняя ассистентка (о себе — в женском роде). Владелец — мужчина, говоришь с ним на "ты", в единственном числе; никогда не "вы"/"ваш" и не "он"/"его". Подтверди действие РОВНО ОДНИМ коротким предложением (≤120 символов), тепло и по-русски. Не задавай вопросов, не повторяй слова, не добавляй ничего после точки. Отвечай ТОЛЬКО одним объектом JSON с полями "response" (текст) и "mood" (ровно одно из: neutral, happy, thinking, tired, confused). Пример: {"response": "Записала, что ты выпил стакан воды.", "mood": "neutral"} Никогда не пиши "..." в поле response.` diff --git a/internal/phraser/llmphraser.go b/internal/phraser/llmphraser.go index 05624a8..98f06a1 100644 --- a/internal/phraser/llmphraser.go +++ b/internal/phraser/llmphraser.go @@ -439,8 +439,10 @@ func (p *LLMPhraser) chatWithSystem(ctx context.Context, system, user string, ma // as "..." before this. See PHRASING-EVAL-31-07-2026.md. // // Russian only, feminine self-reference, second person masculine (the owner is -// a man). One short sentence — the nudge is spoken aloud. +// a man). She talks TO him, informally, singular — never "вы", never "он". +// One short sentence — the nudge is spoken aloud. const nudgeSystem = `Ты — Maven, домашняя ассистентка. О себе говоришь в женском роде ("я проверила", "я записала"). Владелец — мужчина, обращайся к нему в мужском роде ("ты пил", "ты забыл"). +Говоришь с ним на "ты", в единственном числе ("выпей", "встань"). Никогда не "вы"/"вас"/"ваш" и никогда "он"/"его" — ты говоришь ему, а не о нём. Пиши ОДНО короткое напоминание по-русски: не больше 120 символов и не больше 16 слов. Только по делу. From f4de2fc5e1fc1c2879968c93ceb84cf6c9e16d57 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 14:54:20 +0400 Subject: [PATCH 2/2] Don't let a verb count as the person being talked about MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third-person check asks whether anyone else was named before "он". A nudge is mostly verbs, and they were counted as possible people, so "попробуй встать и отдохнуть — у него есть перерыв" passed. Infinitives and imperatives now join past tense as words that cannot be a person. A plain noun before the pronoun still blinds it. That needs a parser, and the comment says so. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ --- internal/phraser/eval/address_time_test.go | 16 ++++++++++++ internal/phraser/eval/checks.go | 30 ++++++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/internal/phraser/eval/address_time_test.go b/internal/phraser/eval/address_time_test.go index 894a05c..b20c460 100644 --- a/internal/phraser/eval/address_time_test.go +++ b/internal/phraser/eval/address_time_test.go @@ -24,3 +24,19 @@ func TestAddressTimeWordDoesNotBlind(t *testing.T) { } } } + +// TestAddressVerbIsNotAnAntecedent — a nudge is mostly verbs, and a verb is +// never who "он" refers to. This exact string passed the check before. +func TestAddressVerbIsNotAnAntecedent(t *testing.T) { + s := "попробуй встать и отдохнуть — у него есть перерыв" + if r := checkAddress(s); r.Pass { + t.Errorf("checkAddress(%q) passed, want a third-person failure", s) + } + // Still missed, and this is the documented hole: "выпей воды, он не пил" has + // a real noun ("воды") before the pronoun, so the scan believes somebody + // else was named. Telling that apart needs a parser, not a suffix rule. + // A named third party still wins over the verbs around it. + if r := checkAddress("сервис упал, он не отвечает"); !r.Pass { + t.Errorf("checkAddress on a real third party failed: %s", r.Detail) + } +} diff --git a/internal/phraser/eval/checks.go b/internal/phraser/eval/checks.go index 838cbd3..ade60b7 100644 --- a/internal/phraser/eval/checks.go +++ b/internal/phraser/eval/checks.go @@ -340,7 +340,9 @@ func prevWord(words []string, i int) string { // - 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 +// not one ("после обеда он не ел", "выпей воды, он не пил" — both missed). +// Verbs and time words no longer count, which covers the usual nudge, but a +// plain noun before the pronoun still blinds it. 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 @@ -407,14 +409,26 @@ var notAnAntecedent = map[string]bool{ "твой": 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 { +// looksVerb — a verb is never the thing "он" refers to, so it must not count as +// an antecedent. Past tense keeps "сервис упал, он не отвечает" working off +// "сервис"; the infinitive and imperative endings are here because a nudge is +// mostly made of them ("попробуй встать и отдохнуть — у него есть перерыв" +// slipped through with "попробуй" taken for the person being talked about). +func looksVerb(w string) bool { + r := []rune(w) + if len(r) < 3 { return false } - return strings.HasSuffix(w, "л") || strings.HasSuffix(w, "ла") || - strings.HasSuffix(w, "ло") || strings.HasSuffix(w, "ли") + for _, suf := range []string{ + "л", "ла", "ло", "ли", // past tense + "ть", "ться", "ти", "чь", // infinitive + "й", "йся", "йте", // imperative + } { + if strings.HasSuffix(w, suf) { + return true + } + } + return false } func checkAddress(body string) Result { @@ -441,7 +455,7 @@ func checkAddress(body string) Result { if !unicode.Is(unicode.Cyrillic, []rune(p)[0]) && !isLatinWord(p) { continue // punctuation } - if notAnAntecedent[p] || prepositions[p] || thirdPersonHim[p] || looksPastVerb(p) { + if notAnAntecedent[p] || prepositions[p] || thirdPersonHim[p] || looksVerb(p) { continue } named = true