continuation: an ellipsis names the day, not the topic

Deployed check: "какой сегодня день" then "а завтра?" answered "пока не
умею". The intent was inherited correctly, but replySystem keyword-matches
the utterance, and "а завтра?" contains no topic word — that is the whole
nature of an ellipsis.

So the topic travels with the session. rememberTurn keeps the raw utterance
in Slots.Text for system and query turns that have no Text slot of their
own (a stage-0 grammar fills none), and replySystem matches keywords
against utterance + Slots.Text. Dates keep parsing from the utterance
alone, which is the part the ellipsis actually restates.

Only system and query: everywhere else Text is a payload and must stay
exactly what the router put in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
kami
2026-08-01 22:16:58 +04:00
parent cf40f13573
commit 53616836db
3 changed files with 38 additions and 3 deletions
+10 -1
View File
@@ -348,9 +348,18 @@ func (h *reactiveHandler) rememberTurn(prev *dialogue.Session, dec router.Decisi
if dec.Intent == router.IntentChat {
ttl = 15 * time.Minute // conversational turns should last longer
}
// A system or query turn often carries no Text slot at all — a stage-0
// grammar fills none. The next turn may be an ellipsis ("а завтра?"),
// which knows the day but not what was asked ABOUT, so keep the raw
// utterance where continuation.go can find it. Only these two intents:
// everywhere else Text is a payload and must stay what the router put in.
slots := toDialogueSlots(dec.Slots)
if slots.Text == "" && (dec.Intent == router.IntentSystem || dec.Intent == router.IntentQuery) {
slots.Text = dec.Utterance
}
h.dialogueSessions.Put(voiceDialogueID, &dialogue.Session{
Intent: dialogue.Intent(dec.Intent),
Slots: toDialogueSlots(dec.Slots),
Slots: slots,
Timestamp: now,
TTL: ttl,
History: history,
+15
View File
@@ -101,3 +101,18 @@ func TestContinuationNeverCarriesAnFn(t *testing.T) {
t.Fatalf("carried fn %q into a continuation", dec.Slots.Fn)
}
}
// TestContinuationCarriesTheTopic — the ellipsis names the day; what he is
// asking ABOUT has to come from the previous turn, or replySystem keyword-
// matches "а завтра?" and finds nothing. Caught on the deployed daemon.
func TestContinuationCarriesTheTopic(t *testing.T) {
prev := contSession(dialogue.IntentSystem, "")
prev.Slots.Text = "какой сегодня день"
dec, ok := continuationDecision(prev, "а завтра?", contNow)
if !ok {
t.Fatal("want a decision")
}
if dec.Slots.Text != "какой сегодня день" {
t.Fatalf("Slots.Text = %q, want the previous turn's topic", dec.Slots.Text)
}
}
+13 -2
View File
@@ -369,6 +369,17 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision)
u := strings.ToLower(dec.Utterance)
now := h.now()
// The topic and the day come from different places on a continuation.
// "а завтра?" names the day and nothing else; what he is asking ABOUT
// lives in the previous turn, which continuation.go copied into
// Slots.Text. Dates keep parsing from the utterance — that is the part
// the ellipsis actually restates — and only the keyword match widens.
// On an ordinary turn Slots.Text is the utterance, so nothing changes.
topic := u
if t := strings.ToLower(dec.Slots.Text); t != "" && t != u {
topic = u + " " + t
}
// stage-0 grammars catch the exact time/date patterns, but duration
// queries ("сколько времени прошло") bypass the grammar's build filter
// and can reach replySystem via the classifier path. Guard against them.
@@ -377,14 +388,14 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision)
}
switch {
case strings.Contains(u, "час") || strings.Contains(u, "врем"):
case strings.Contains(topic, "час") || strings.Contains(topic, "врем"):
// "который час в киеве" — she keeps one clock, so any named place gets
// the honest answer. Never local time dressed up as the city's.
if mentionsUnknownPlace(u) {
return onlyLocalTimeReply
}
return "сейчас " + ruClock(now)
case strings.Contains(u, "день") || strings.Contains(u, "числ"):
case strings.Contains(topic, "день") || strings.Contains(topic, "числ"):
// "какое число завтра" — answer for the day the user asked about,
// not today. Reuses the router's calendar day-word parser.
day := now