3.2 conversation depth: cross-intent anaphora + fact-by-key query

- session.go: add History []Turn + Turn type for multi-turn context
- slots.go: add AnaphoraResolver with Resolve() for RU pronoun detection
  (это/он/она/оно/тот/мой and inflected forms)
- followup.go: extend followUpMerge with cross-intent inheritance:
  Query/Fact/Reminder after a Fact with anaphora inherits the key.
  Same-intent path unchanged. Anaphora detection from utterance.
- voice.go: add fact-by-key lookup path in applyAction for IntentQuery
  when dialogue resolved an anaphoric reference (calls LatestFact,
  formats with formatTime helper). History tracked in Session.History
  capped at 4 most recent turns.
- followup_test.go: 7 new test cases: anaphora query-after-fact,
  no-inheritance-without-anaphora, three-turn break, anaphora in
  reminder, anaphora in fact, explicit key wins, time inheritance.
  make test green (303+, -race, all 29 packages).
This commit is contained in:
kami
2026-07-06 13:40:16 +04:00
parent b7eb53a3b4
commit 05236ad480
5 changed files with 292 additions and 9 deletions
+10
View File
@@ -27,11 +27,21 @@ type Slots struct {
HasFn bool
}
// Turn represents one utterance in a multi-turn dialogue history.
// Carried by Session.History for cross-intent reference and anaphora
// resolution (a later turn's pronoun points to an earlier turn's entity).
type Turn struct {
Intent Intent
Slots Slots
Text string // raw utterance
}
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
}
func (s *Session) IsExpired(now time.Time) bool {
+35
View File
@@ -328,6 +328,41 @@ func parseDurationValue(s string) (string, bool) {
return strconv.Itoa(n) + unit, true
}
// AnaphoraResolver resolves pronouns like "это", "он", "она" to the prior
// turn's key entity. Returns a (key, value) pair the prior fact carried,
// or ("", "", false) when no pronoun is detected.
type AnaphoraResolver struct{}
// Resolve checks if text contains an anaphoric reference to a prior turn's
// entity. For MVP this handles the common Russian pronouns:
// - "это" / "этого" / "этому" / "этим" / "этом" → "this" (most common)
// - "он" / "его" / "ему" / "ним" → "he/it", masc
// - "она" / "её" / "ей" / "ней" → "she/it", fem
// - "оно" → "it", neuter
//
// Returns the matching pronoun type for cross-referencing with prior slots.
func (AnaphoraResolver) Resolve(text string) (ref string, ok bool) {
s := strings.ToLower(strings.TrimSpace(text))
toks := strings.Fields(s)
for _, tok := range toks {
switch tok {
case "это", "этого", "этому", "этим", "этом", "эти", "эта":
return "this", true
case "он", "его", "ему", "ним":
return "he", true
case "она", "её", "ей", "ней":
return "she", true
case "оно":
return "it", true
case "тот", "та", "то", "те":
return "that", true
case "мой", "моего", "моему", "моим", "моём", "моя", "моей", "моё":
return "mine", true
}
}
return "", false
}
// ParseCalendarDate detects RU calendar date words in text and returns the
// resolved time (midnight UTC+0 for "сегодня"/"today", next day for "завтра"/"tomorrow").
// Returns zero time + false if no match.