d52f60c54e
- Add CalendarEvents method to recordingAPI in auth_test.go - Add CalendarEvents method to fakeCore in handlers_test.go Co-Authored-By: opencode <opencode@anthropic.com>
85 lines
3.6 KiB
Go
85 lines
3.6 KiB
Go
// voice/replier.go — reactive reply phrasing.
|
|
//
|
|
// A SEPARATE seam from internal/phraser.Phraser:
|
|
//
|
|
// - Phraser phrases DELIVERIES — the loop's nudges + reminders. Its output
|
|
// (Body + Summary) is consumed by the dispatcher and shipped to ALL the
|
|
// routed channels (voice gets Body, away channels get Summary). The
|
|
// Phraser owns the multiple-of-many-channel output contract.
|
|
//
|
|
// - Replier phrases REPLIES — the one text a voice round-trip says back to
|
|
// the user who just spoke. Reactive, single-channel (voice), no
|
|
// dispatcher involvement. The reply text goes through TTS to a single
|
|
// audio clip played on the originating client; nothing routes elsewhere.
|
|
//
|
|
// Both seams feed TTS in production but at different times: Phraser for
|
|
// proactive nudges (loop tick → voicesink → tts → push), Replier for
|
|
// reactive round-trips (client audio → stt → router → action → replier →
|
|
// tts → response). Splitting the seam keeps the Phraser interface stable
|
|
// (the project's tests mock it; adding a method would break them) and
|
|
// keeps the LLM-backed impl cleanly focused: production phraser =
|
|
// personality-prompted "nudge tone", production replier = "chat tone",
|
|
// different prompts in the same model server.
|
|
//
|
|
// The Stub is the deterministic floor; production swaps in an LLM impl at
|
|
// the daemon seam (config wiring, no CoreAPI or voice-package change).
|
|
package voice
|
|
|
|
import "github.com/kami/maven/internal/router"
|
|
|
|
// Replier — the reactive reply phrasing seam. The daemon's reactive handler
|
|
// calls Reply with the router's Decision; the impl produces a terse reply
|
|
// the handler passes to TTS and ships back to the originating client.
|
|
//
|
|
// The reply is per-Decision (one per reactive turn); the impl sees the
|
|
// decision's Intent + Slots + Clarify. The Intent largely names the reply
|
|
// shape (act/reminder/fact/note/query/clarify); the Slots carry the
|
|
// specifics that personalise it ("got it: water at 14:00").
|
|
type Replier interface {
|
|
Reply(d router.Decision) string
|
|
}
|
|
|
|
// StubReplier — the deterministic, no-model floor. Canned per intent;
|
|
// slots incorporated as plain strings (the LLM impl will natural-language
|
|
// them; the Stub keeps it readable). The same instinct as the phraser
|
|
// Stub: a SCAFFOLD GROUND TRUTH so tests + the daemon end-to-end have
|
|
// deterministic replies; the LLM impl swaps in at the daemon wiring.
|
|
type StubReplier struct{}
|
|
|
|
// NewStubReplier builds the floor replier.
|
|
func NewStubReplier() *StubReplier { return &StubReplier{} }
|
|
|
|
// Reply dispatches on Intent + Clarify. Each branch is short; the LLM impl
|
|
// will replace this with prompted text and the same dispatch shape.
|
|
func (s *StubReplier) Reply(d router.Decision) string {
|
|
if d.Clarify {
|
|
return "не совсем поняла — можешь переформулировать?"
|
|
}
|
|
switch d.Intent {
|
|
case router.IntentAct:
|
|
if !d.Slots.HasFn {
|
|
return "не могу это сделать — не разобрала действие."
|
|
}
|
|
return "ок, записала действие: " + d.Slots.Fn
|
|
case router.IntentReminder:
|
|
if d.Slots.HasTime {
|
|
return "напомню."
|
|
}
|
|
return "напомню."
|
|
case router.IntentFact:
|
|
if d.Slots.HasKey {
|
|
if d.Slots.Value != "" {
|
|
return "отметила: " + d.Slots.Key + " = " + d.Slots.Value
|
|
}
|
|
return "отметила: " + d.Slots.Key
|
|
}
|
|
return "записала факт."
|
|
case router.IntentNote:
|
|
return "сохранила заметку."
|
|
case router.IntentQuery:
|
|
return "поискала в заметках — ничего не нашла."
|
|
default:
|
|
return "приняла."
|
|
}
|
|
}
|