5e66aa8f22
dialogue.Slots gained Value in 925ce22, but toDialogueSlots never copied
it, so a clarifying answer carrying a fact payload still landed nowhere:
clarify.go:202 sends the answer through the converter, and the SlotValue
arm reads answer.Value.
Both converters now carry every field. TestSlotsParity compares the two
field sets by name and type; TestSlotsRoundTrip populates every router
field and checks the round trip, and fails the fixture itself when a new
field is left zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QChoBS5qJSrCV98oNUnHNU
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/dialogue"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// voiceDialogueID — the single dialogue-session key. This is a single-user box
|
|
// (ponytail), so one slot suffices; a second speaker would need per-speaker ids,
|
|
// which waits on voice-print attribution (see PROGRESS multi-user deferral).
|
|
const voiceDialogueID = "voice"
|
|
|
|
// toDialogueSlots and applyDialogueSlots are the only bridge between
|
|
// router.Slots and dialogue.Slots. dialogue must not import router (import
|
|
// cycle), so the two structs are hand-kept copies and every field has to be
|
|
// carried by hand here. Adding a field to either struct without adding it to
|
|
// BOTH functions loses a slot silently — nothing fails to build. The tests in
|
|
// slotsparity_test.go fail when the field sets or the converters stop matching;
|
|
// when they do, fix these two functions, not the tests.
|
|
|
|
// toDialogueSlots projects the router's slots onto the dialogue layer's copy.
|
|
func toDialogueSlots(s router.Slots) dialogue.Slots {
|
|
return dialogue.Slots{
|
|
Time: s.Time,
|
|
HasTime: s.HasTime,
|
|
Key: s.Key,
|
|
Value: s.Value,
|
|
HasKey: s.HasKey,
|
|
Text: s.Text,
|
|
Fn: s.Fn,
|
|
Args: s.Args,
|
|
HasFn: s.HasFn,
|
|
}
|
|
}
|
|
|
|
// applyDialogueSlots writes dialogue slots back onto router slots.
|
|
func applyDialogueSlots(base router.Slots, d dialogue.Slots) router.Slots {
|
|
base.Time, base.HasTime = d.Time, d.HasTime
|
|
base.Key, base.Value, base.HasKey = d.Key, d.Value, d.HasKey
|
|
base.Text = d.Text
|
|
base.Fn, base.Args, base.HasFn = d.Fn, d.Args, d.HasFn
|
|
return base
|
|
}
|
|
|
|
// anaphoraResolver is a shared instance for pronoun detection.
|
|
var anaphoraResolver router.AnaphoraResolver
|
|
|
|
// followUpMerge fills the current turn's missing slots from a prior
|
|
// non-expired session — the multi-turn seam. It handles three cases:
|
|
//
|
|
// 1. Same-intent: inherit missing slots via InheritSlots (existing behavior).
|
|
// 2. Cross-intent anaphora: if the current utterance contains a pronoun
|
|
// ("это" / "он" / "она" etc.) AND the prior session has a key, inherit
|
|
// the key for fact-lookup queries and reminder creation.
|
|
// 3. Query after Fact: a query that references the prior fact's subject
|
|
// inherits the key so the handler can do a fact-by-key lookup.
|
|
//
|
|
// A clarify turn resolves nothing, so it never inherits. InheritSlots only
|
|
// fills gaps, so a fully-slotted current turn is unaffected.
|
|
func followUpMerge(prev *dialogue.Session, dec router.Decision, now time.Time) router.Decision {
|
|
if prev == nil || dec.Clarify || prev.IsExpired(now) {
|
|
return dec
|
|
}
|
|
|
|
// Case 1: same-intent inheritance (existing).
|
|
if prev.Intent == dialogue.Intent(dec.Intent) {
|
|
merged := dialogue.InheritSlots(prev.Slots, toDialogueSlots(dec.Slots))
|
|
dec.Slots = applyDialogueSlots(dec.Slots, merged)
|
|
return dec
|
|
}
|
|
|
|
// Cases 2 & 3: cross-intent anaphora + query-after-fact.
|
|
// A query after a fact may reference the fact's subject by pronoun.
|
|
_, isAnaphoric := anaphoraResolver.Resolve(dec.Utterance)
|
|
if !isAnaphoric && !dec.Slots.HasKey {
|
|
// No anaphora and no explicit key — this is a truly new topic.
|
|
return dec
|
|
}
|
|
|
|
// Inherit key from the prior session's key when the current utterance
|
|
// refers to it (anaphora) or when a query follows a fact.
|
|
switch {
|
|
case dec.Intent == router.IntentQuery && prev.Slots.HasKey:
|
|
dec.Slots.Key = prev.Slots.Key
|
|
dec.Slots.HasKey = true
|
|
if prev.Slots.HasTime {
|
|
dec.Slots.Time = prev.Slots.Time
|
|
dec.Slots.HasTime = true
|
|
}
|
|
case dec.Intent == router.IntentReminder && prev.Slots.HasKey && isAnaphoric:
|
|
dec.Slots.Key = prev.Slots.Key
|
|
dec.Slots.HasKey = true
|
|
case dec.Intent == router.IntentFact && !dec.Slots.HasKey && prev.Slots.HasKey && isAnaphoric:
|
|
dec.Slots.Key = prev.Slots.Key
|
|
dec.Slots.HasKey = true
|
|
}
|
|
|
|
return dec
|
|
}
|