A greeting is not a failed answer (V-663)

classifyTurnRole read "спасибо" and "привет" as answers to whatever was
parked, so she re-asked "В какой день?" at a man saying thank you and
spent one of three attempts doing it. That attempt is a bound meant to end
the ride, so the pleasantry both produced the worst reply in the corpus and
paid for the privilege.

They are asides now: answered as themselves, the question resumed on the
tail, no attempt spent, one ride counted.

The set is a new closed lexicon entry, matched as WHOLE utterances. Every
token rule tried was wrong on something. "вечер" answers "это утра или
вечера?" and "нет" answers a confirm, so anything that could fill a slot
stays out. The control words stay out too, because isCancel owns them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
This commit is contained in:
2026-08-08 21:43:15 +04:00
parent de9884e063
commit a886217223
4 changed files with 64 additions and 13 deletions
+32
View File
@@ -186,6 +186,28 @@ func carriesReminderVerb(text string) bool {
return false
}
// isPleasantry matches the WHOLE utterance against lexicon.Pleasantries, after
// lowercasing and dropping the punctuation a greeting carries.
//
// Whole utterance and not tokens. Every token rule tried here was wrong on
// something: "вечер" answers "это утра или вечера?", "нет" answers a confirm,
// and "спокойной" alone is not an utterance at all. A greeting is a fixed
// phrase, so matching it as one costs nothing and claims nothing else.
func isPleasantry(text string) bool {
t := strings.ToLower(strings.TrimSpace(text))
t = strings.Trim(t, " .,!?…")
t = strings.Join(strings.Fields(t), " ")
if t == "" {
return false
}
for _, p := range lexicon.Pleasantries() {
if t == p {
return true
}
}
return false
}
// offlineOwnRequest is the shape half of the evidence: the offline token tests,
// which cost nothing and never depend on the model that produced the routing.
// It is also the whole answer when there is no route to read — the classifier
@@ -225,6 +247,16 @@ func classifyTurnRole(q *dialogue.PendingQuestion, text string, answer dialogue.
// hour, and no route saying "question" changes that. It works because the
// extractor no longer reads a day word as the current clock, so a sentence
// that names no hour now fills nothing to weigh.
// A pleasantry is neither (V-663). "спасибо" and "привет" fell through to
// roleAnswer, so a question about a reminder's DAY was re-asked at a man
// saying thank you, and the retry it spent was one of the three bounds
// meant to end the ride. It is an aside: answered as itself, the question
// resumed on the tail, no attempt spent, one ride counted. Placed above the
// content gate because "доброе утро" has content and states nothing, so
// neither half of the evidence below can reach it.
if q != nil && isPleasantry(text) {
return roleAside
}
own := false
if len(ownContent(text)) > 0 {
own = offlineOwnRequest(text) || (ok && carriesOwnRequest(routed, text))
+14 -13
View File
@@ -411,22 +411,23 @@ func TestTwoBoundsCannotRearmEachOther(t *testing.T) {
t.Fatalf("after two asides: %+v", q)
}
// A failed answer. It spends an attempt and puts Suspends back to zero, so
// the old bound starts over. Rides does not.
h.handleText(ctx, "web", "спасибо")
if q = h.clarifyStore.Get(id, h.now()); q == nil || q.Suspends != 0 {
t.Fatalf("the failed answer did not rearm the old bound: %+v", q)
// A pleasantry. It used to read as a failed answer, so she re-asked the
// question at a man saying thank you and spent an attempt doing it. Now it
// is an aside: answered as itself, question on the tail, one more ride.
reply := h.handleText(ctx, "web", "спасибо")
if !strings.HasSuffix(reply, resumed) {
t.Fatalf("a pleasantry lost the parked question: %q", reply)
}
if q.Rides != 2 {
t.Fatalf("the rides it already took were forgotten: %+v", q)
q = h.clarifyStore.Get(id, h.now())
if q == nil || q.Attempts != 1 {
t.Fatalf("a pleasantry spent an attempt: %+v", q)
}
if q.Rides != 3 {
t.Fatalf("a pleasantry rode free: %+v", q)
}
// Three more asides. Suspends only reaches three, one short of firing, and
// would go back to zero on the next pleasantry. Rides passes MaxRides and
// the request goes, out loud.
h.handleText(ctx, "web", "какие у меня напоминания?")
h.handleText(ctx, "web", "какие у меня напоминания?")
reply := h.handleText(ctx, "web", "какие у меня напоминания?")
// One more ride of any kind and the request goes, out loud.
reply = h.handleText(ctx, "web", "какие у меня напоминания?")
if !strings.Contains(reply, clarifyDropped) {
t.Fatalf("the question rode four asides and was let go in silence: %q", reply)
}