mavend: wire the correction into the turn, before routing (V-455)
Read next to the confirm and clarify turns, because a correction routed as a fresh utterance files the correction itself. Only turns she acted on are remembered: a clarify asked instead of acting.
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRepairReadsTheCorrectedIntent(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
utterance string
|
||||||
|
want router.Intent
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"нет, ты не поняла, это заметка", router.IntentNote, true},
|
||||||
|
{"нет, это заметка", router.IntentNote, true},
|
||||||
|
{"это не напоминание, а заметка", router.IntentNote, true},
|
||||||
|
{"это заметка, а не напоминание", router.IntentNote, true},
|
||||||
|
{"ты не так поняла — это факт", router.IntentFact, true},
|
||||||
|
{"неправильно поняла, это был вопрос", router.IntentQuery, true},
|
||||||
|
{"you got it wrong, that was a note", router.IntentNote, true},
|
||||||
|
// No marker: an ordinary request that happens to name an intent.
|
||||||
|
{"запиши заметку купить хлеб", "", false},
|
||||||
|
{"напомни мне про заметку", "", false},
|
||||||
|
// A marker with no intent named: nothing to correct to.
|
||||||
|
{"ты не так поняла", "", false},
|
||||||
|
{"", "", false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got, _, ok := parseRepair(c.utterance)
|
||||||
|
if ok != c.ok || (ok && got != c.want) {
|
||||||
|
t.Errorf("parseRepair(%q) = %q,%v; want %q,%v", c.utterance, got, ok, c.want, c.ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRepairTeachesTheClassifierAndRedoesTheTurn is the whole feature: the
|
||||||
|
// previous utterance is filed under the intent he named, the classifier keeps
|
||||||
|
// it as an example, and he hears that it landed.
|
||||||
|
func TestRepairTeachesTheClassifierAndRedoesTheTurn(t *testing.T) {
|
||||||
|
h, st, now := newClarifyHandler(t)
|
||||||
|
emb := router.NewHashEmbedder(256)
|
||||||
|
cls := router.NewClassifier(emb)
|
||||||
|
h.embedder = emb
|
||||||
|
h.router = router.New(router.Config{Classifier: cls, Extractor: h.extractor})
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
h.recordTurn("купить хлеб", router.IntentFact)
|
||||||
|
reply, handled := h.resolveRepair(ctx, "нет, ты не поняла, это заметка")
|
||||||
|
if !handled {
|
||||||
|
t.Fatal("a spoken correction was not handled")
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply, "заметка") {
|
||||||
|
t.Errorf("the correction is not named out loud: %q", reply)
|
||||||
|
}
|
||||||
|
if strings.Contains(reply, "не вышло") {
|
||||||
|
t.Errorf("learning failed unexpectedly: %q", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
ex := cls.Examples(router.IntentNote)
|
||||||
|
if len(ex) != 1 || ex[0].Text != "купить хлеб" {
|
||||||
|
t.Fatalf("the classifier did not learn the correction: %+v", ex)
|
||||||
|
}
|
||||||
|
notes, err := st.RecentNotes(ctx, 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("recent notes: %v", err)
|
||||||
|
}
|
||||||
|
if len(notes) != 1 || !strings.Contains(notes[0].Text, "купить хлеб") {
|
||||||
|
t.Fatalf("the request was not redone as a note: %+v", notes)
|
||||||
|
}
|
||||||
|
_ = now
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepairNeedsARecentTurnToPointAt(t *testing.T) {
|
||||||
|
h, _, now := newClarifyHandler(t)
|
||||||
|
h.router = router.New(router.Config{Classifier: router.NewClassifier(router.NewHashEmbedder(256))})
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Nothing said yet.
|
||||||
|
if _, handled := h.resolveRepair(ctx, "нет, это заметка"); handled {
|
||||||
|
t.Error("a correction with no previous turn was handled")
|
||||||
|
}
|
||||||
|
// Said, but long ago.
|
||||||
|
h.recordTurn("купить хлеб", router.IntentFact)
|
||||||
|
*now = now.Add(repairWindow + time.Minute)
|
||||||
|
if _, handled := h.resolveRepair(ctx, "нет, это заметка"); handled {
|
||||||
|
t.Error("a correction outside the window was handled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepairIsSpentOnce(t *testing.T) {
|
||||||
|
h, _, _ := newClarifyHandler(t)
|
||||||
|
emb := router.NewHashEmbedder(256)
|
||||||
|
h.embedder = emb
|
||||||
|
h.router = router.New(router.Config{Classifier: router.NewClassifier(emb), Extractor: h.extractor})
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
h.recordTurn("купить хлеб", router.IntentFact)
|
||||||
|
if _, handled := h.resolveRepair(ctx, "нет, это заметка"); !handled {
|
||||||
|
t.Fatal("the first correction was not handled")
|
||||||
|
}
|
||||||
|
if _, handled := h.resolveRepair(ctx, "нет, это заметка"); handled {
|
||||||
|
t.Error("the same turn was corrected twice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRepairPassesWhenSheAlreadyDidThat — he names the intent she used. There
|
||||||
|
// is nothing to teach and redoing it would file the request a second time.
|
||||||
|
func TestRepairPassesWhenSheAlreadyDidThat(t *testing.T) {
|
||||||
|
h, _, _ := newClarifyHandler(t)
|
||||||
|
h.router = router.New(router.Config{Classifier: router.NewClassifier(router.NewHashEmbedder(256))})
|
||||||
|
h.recordTurn("купить хлеб", router.IntentNote)
|
||||||
|
if _, handled := h.resolveRepair(context.Background(), "нет, это заметка"); handled {
|
||||||
|
t.Error("a correction to the intent she already used was handled")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -161,6 +161,7 @@ type reactiveHandler struct {
|
|||||||
// the y/n answer. ponytail: single slot, single-user box — a second act
|
// the y/n answer. ponytail: single slot, single-user box — a second act
|
||||||
// while one waits overwrites it (last-asked wins); expires after confirmTTL.
|
// while one waits overwrites it (last-asked wins); expires after confirmTTL.
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
lastRouted *routedTurn // the previous acted turn, for a spoken correction (repair.go)
|
||||||
pending *pendingAct
|
pending *pendingAct
|
||||||
pendingRoutine *pendingRoutineConfirm // routine proposal awaiting y/n
|
pendingRoutine *pendingRoutineConfirm // routine proposal awaiting y/n
|
||||||
pendingHexis *pendingHexisExec // mutating Hexis capability awaiting y/n
|
pendingHexis *pendingHexisExec // mutating Hexis capability awaiting y/n
|
||||||
@@ -299,6 +300,14 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
return withNotice(expiredNotice, reply)
|
return withNotice(expiredNotice, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4d. spoken correction — "нет, это была заметка" points at the previous
|
||||||
|
// turn and names what it should have been (repair.go). Before routing,
|
||||||
|
// like the confirm and clarify turns: routing the correction as a fresh
|
||||||
|
// utterance files the correction itself instead of fixing anything.
|
||||||
|
if reply, handled := h.resolveRepair(ctx, text); handled {
|
||||||
|
return withNotice(expiredNotice, reply)
|
||||||
|
}
|
||||||
|
|
||||||
// 5. route. An elliptical follow-up — "а завтра?" — is answered from the
|
// 5. route. An elliptical follow-up — "а завтра?" — is answered from the
|
||||||
// previous turn instead (continuation.go): the intent is the part it is
|
// previous turn instead (continuation.go): the intent is the part it is
|
||||||
// missing, so no amount of routing recovers it, and the model's guess
|
// missing, so no amount of routing recovers it, and the model's guess
|
||||||
@@ -359,6 +368,13 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remember what this turn was routed as, so the next utterance can correct
|
||||||
|
// it. Only turns she acts on: a clarify asked instead of acting, so there
|
||||||
|
// is nothing yet to be wrong about.
|
||||||
|
if !dec.Clarify {
|
||||||
|
h.recordTurn(text, dec.Intent)
|
||||||
|
}
|
||||||
|
|
||||||
// 8. action — execute the decision's intent. errors here surface as
|
// 8. action — execute the decision's intent. errors here surface as
|
||||||
// short reply text (the user wants to know the action didn't land);
|
// short reply text (the user wants to know the action didn't land);
|
||||||
// the round-trip stays alive.
|
// the round-trip stays alive.
|
||||||
|
|||||||
Reference in New Issue
Block a user