Preserve context across conversation intents (V-542)

Owner explicitly requested direct commits to master; bypass the branch-only hook.
This commit is contained in:
2026-08-13 02:14:46 +04:00
parent a0e6643465
commit da9114b623
15 changed files with 499 additions and 56 deletions
+16 -5
View File
@@ -55,11 +55,22 @@ type Candidate struct {
}
type Session struct {
Intent Intent
Slots Slots
Timestamp time.Time
TTL time.Duration
History []Turn // most recent turns, newest last; used for anaphora + cross-intent
Intent Intent
Slots Slots
// Utterance is what he actually said on this turn. It is deliberately
// independent of Slots.Text: Text is an intent payload and several valid
// routes leave it empty or replace it with a normalized subject. Dialogue
// history needs the original words so a later pronoun can refer across
// fact, query and chat boundaries without pretending a storage key is a
// transcript.
Utterance string
// Conversational keeps an explicitly opened or chat-routed exchange alive
// across later fact/query routes. It does not change what those turns do;
// it only says their shared transcript uses the longer dialogue TTL.
Conversational bool
Timestamp time.Time
TTL time.Duration
History []Turn // prior turns in speaking order, oldest first; used for anaphora + cross-intent
// Candidates — the list she just offered, in the order she said it. Empty
// on every turn that offered no choice, which is most of them.
Candidates []Candidate
+16 -4
View File
@@ -29,10 +29,13 @@ func TestSessionSurvivesRestart(t *testing.T) {
first := openStore(t, path)
before := NewPersistentSessionStore(2*time.Minute, first)
before.Put("voice", &Session{
Intent: IntentReminder,
Slots: Slots{Text: "полить цветы", Time: now.Add(time.Hour), HasTime: true},
Timestamp: now,
TTL: 2 * time.Minute,
Intent: IntentReminder,
Slots: Slots{Text: "полить цветы", Time: now.Add(time.Hour), HasTime: true},
Utterance: "напомни полить цветы",
Conversational: true,
Timestamp: now,
TTL: 2 * time.Minute,
History: []Turn{{Intent: IntentChat, Text: "мы говорили о цветах"}},
})
if err := first.Close(); err != nil {
t.Fatalf("close: %v", err)
@@ -51,6 +54,15 @@ func TestSessionSurvivesRestart(t *testing.T) {
if sess.Intent != IntentReminder {
t.Fatalf("intent = %q, want reminder", sess.Intent)
}
if sess.Utterance != "напомни полить цветы" {
t.Fatalf("utterance = %q after restart", sess.Utterance)
}
if !sess.Conversational {
t.Fatal("conversation mode was lost across restart")
}
if len(sess.History) != 1 || sess.History[0].Text != "мы говорили о цветах" {
t.Fatalf("history after restart = %+v", sess.History)
}
// the follow-up carries no text of its own; it must inherit the old one
merged := InheritSlots(sess.Slots, Slots{Time: now.Add(2 * time.Hour), HasTime: true})
if merged.Text != "полить цветы" {