Merge pull request 'Bug: a transient complaint is stored as a durable fact at confidence 1.00' (#132) from task/481-bug-a-transient-complaint-is-stored-as-a into master
This commit was merged in pull request #132.
This commit is contained in:
@@ -57,6 +57,9 @@ var actionHandlers = map[router.Intent]func(*reactiveHandler, context.Context, r
|
|||||||
func (h *reactiveHandler) actionChat(ctx context.Context, dec router.Decision) string {
|
func (h *reactiveHandler) actionChat(ctx context.Context, dec router.Decision) string {
|
||||||
// Conversational: build history from dialogue session (prior user turns)
|
// Conversational: build history from dialogue session (prior user turns)
|
||||||
// and let the LLM respond from general knowledge + context.
|
// and let the LLM respond from general knowledge + context.
|
||||||
|
if h.phraser == nil {
|
||||||
|
return "поговорили."
|
||||||
|
}
|
||||||
history := h.chatHistory()
|
history := h.chatHistory()
|
||||||
reply, err := h.phraser.PhraseChat(ctx, dec.Utterance, history)
|
reply, err := h.phraser.PhraseChat(ctx, dec.Utterance, history)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ func (h *reactiveHandler) actionFact(ctx context.Context, dec router.Decision) s
|
|||||||
q.Slots.Value = ""
|
q.Slots.Value = ""
|
||||||
return h.actionQuery(ctx, q)
|
return h.actionQuery(ctx, q)
|
||||||
}
|
}
|
||||||
|
// A complaint is not a fact either (#481). "сеть какая-то медленная" and
|
||||||
|
// "интернет не работает" were stored as `self` rows at confidence 1.00, and
|
||||||
|
// recall reads a self row back later as if it were still true — the same
|
||||||
|
// class of row that outranked live search in #470. The sentence describes a
|
||||||
|
// moment, so she answers it and stores nothing. An explicit "запомни ..."
|
||||||
|
// and anything about him are both left alone by the test.
|
||||||
|
if router.IsTransientComplaint(dec.Utterance) {
|
||||||
|
log.Printf("voice: fact write refused, utterance is a passing complaint: %q (key %q) — answering as chat",
|
||||||
|
dec.Utterance, dec.Slots.Key)
|
||||||
|
c := dec
|
||||||
|
c.Intent = router.IntentChat
|
||||||
|
c.Slots.Key, c.Slots.HasKey = "", false
|
||||||
|
c.Slots.Value = ""
|
||||||
|
return h.actionChat(ctx, c)
|
||||||
|
}
|
||||||
now := h.now()
|
now := h.now()
|
||||||
req := ipc.WriteFactReq{
|
req := ipc.WriteFactReq{
|
||||||
Ts: now,
|
Ts: now,
|
||||||
|
|||||||
@@ -123,3 +123,48 @@ func mustEmbedPassage(t *testing.T, h *reactiveHandler, text string) []float32 {
|
|||||||
}
|
}
|
||||||
return vec
|
return vec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The write half of #481: a complaint about a thing is a state of the
|
||||||
|
// afternoon, not a fact about him. Stored as a `self` row at confidence 1.00
|
||||||
|
// it comes back on recall as if the network were still down.
|
||||||
|
func TestActionFact_ComplaintIsNotWritten(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
h, api := newFactGateHandler(t, time.Now())
|
||||||
|
|
||||||
|
reply := h.actionFact(ctx, router.Decision{
|
||||||
|
Intent: router.IntentFact,
|
||||||
|
Utterance: "сеть какая-то медленная",
|
||||||
|
Slots: router.Slots{Key: "network_speed", HasKey: true, Value: "медленная"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := api.LatestFact(ctx, "network_speed"); err == nil {
|
||||||
|
t.Fatal("a passing complaint was stored as a fact about him")
|
||||||
|
}
|
||||||
|
hits, err := h.memStore.Search(ctx, mustEmbedPassage(t, h, "сеть какая-то медленная"), 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("memory search: %v", err)
|
||||||
|
}
|
||||||
|
if len(hits) != 0 {
|
||||||
|
t.Fatalf("the complaint was indexed for recall: %+v", hits)
|
||||||
|
}
|
||||||
|
if reply == "" {
|
||||||
|
t.Fatal("the turn was neither stored nor answered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the complaint he asked her to keep: the capture verb wins, as it does
|
||||||
|
// over the question gate.
|
||||||
|
func TestActionFact_AskedToRememberAComplaintStillWrites(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
h, api := newFactGateHandler(t, time.Now())
|
||||||
|
|
||||||
|
h.actionFact(ctx, router.Decision{
|
||||||
|
Intent: router.IntentFact,
|
||||||
|
Utterance: "запомни что интернет не работает",
|
||||||
|
Slots: router.Slots{Key: "internet", HasKey: true, Value: "не работает"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := api.LatestFact(ctx, "internet"); err != nil {
|
||||||
|
t.Fatalf("an explicit capture was refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// transientStems — the states a thing is in for an afternoon. Compared as
|
||||||
|
// prefixes because Russian inflects the ending: "медленн" covers "медленная",
|
||||||
|
// "медленный" and "медленно" without listing them.
|
||||||
|
var transientStems = []string{
|
||||||
|
"медленн", "тормоз", "лаг", "завис", "виснет", "глюч", "барахл",
|
||||||
|
"отвал", "падает", "упал", "сдох", "греется", "перегре",
|
||||||
|
"slow", "laggy", "stuck", "frozen", "flaky", "broken", "down",
|
||||||
|
}
|
||||||
|
|
||||||
|
// brokenVerbs — what "не ..." is denying when the sentence is a complaint.
|
||||||
|
// "не работает", "не грузит", "не открывается". Prefixes again.
|
||||||
|
var brokenVerbs = []string{
|
||||||
|
"работ", "пашет", "груз", "открыва", "включа", "коннект", "подключ",
|
||||||
|
"work", "load", "connect", "respond",
|
||||||
|
}
|
||||||
|
|
||||||
|
// selfMarkers — the words that make a sentence about him rather than about a
|
||||||
|
// thing. Their presence turns the test off, because losing a fact he meant to
|
||||||
|
// store is worse than keeping a complaint: "я сломал руку" is durable, and
|
||||||
|
// "интернет не работает" is not.
|
||||||
|
var selfMarkers = []string{"я", "мне", "меня", "мной", "i", "me", "my"}
|
||||||
|
|
||||||
|
// IsTransientComplaint reports whether text observes a passing state of some
|
||||||
|
// thing rather than recording a fact.
|
||||||
|
//
|
||||||
|
// It exists because "сеть какая-то медленная" and "интернет не работает" were
|
||||||
|
// written to the fact store as `self` rows at confidence 1.00 (Vikunja #481),
|
||||||
|
// where recall reads them back later as if they were still true. A complaint
|
||||||
|
// describes a moment; the fact store describes him.
|
||||||
|
//
|
||||||
|
// Deterministic, offline, and shaped exactly like IsQuestionShaped: an
|
||||||
|
// explicit capture verb wins over everything, because "запомни что интернет
|
||||||
|
// не работает" is an instruction and not a passing remark. A first-person
|
||||||
|
// marker also turns it off — the test is meant to catch a sentence about a
|
||||||
|
// thing, and it errs toward storing.
|
||||||
|
func IsTransientComplaint(text string) bool {
|
||||||
|
t := strings.TrimSpace(text)
|
||||||
|
if t == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
toks := planTokens(strings.ToLower(t))
|
||||||
|
for _, v := range captureVerbs {
|
||||||
|
if hasTok(toks, v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range selfMarkers {
|
||||||
|
if hasTok(toks, m) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tok := range toks {
|
||||||
|
for _, stem := range transientStems {
|
||||||
|
if strings.HasPrefix(tok, stem) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "не" plus a verb of working, in either order of the two tokens that
|
||||||
|
// follow it — "не работает" and "не очень работает" both deny the same
|
||||||
|
// thing.
|
||||||
|
for i, tok := range toks {
|
||||||
|
if tok != "не" && tok != "not" && tok != "isn" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j := i + 1; j < len(toks) && j <= i+2; j++ {
|
||||||
|
for _, v := range brokenVerbs {
|
||||||
|
if strings.HasPrefix(toks[j], v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsTransientComplaint(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
text string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// The two rows from the QA run that named this bug.
|
||||||
|
{"сеть какая-то медленная", true},
|
||||||
|
{"интернет не работает", true},
|
||||||
|
{"вайфай тормозит", true},
|
||||||
|
{"сервер завис", true},
|
||||||
|
{"the wifi is slow", true},
|
||||||
|
|
||||||
|
// An instruction wins: he asked for it to be written down.
|
||||||
|
{"запомни что интернет не работает", false},
|
||||||
|
{"запиши что сеть медленная", false},
|
||||||
|
|
||||||
|
// About him, so it stays a fact even when it sounds like a complaint.
|
||||||
|
{"я сломал руку", false},
|
||||||
|
{"мне медленно думается", false},
|
||||||
|
|
||||||
|
// Ordinary captures must not be touched.
|
||||||
|
{"поужинал", false},
|
||||||
|
{"выпил воды", false},
|
||||||
|
{"машина на парковке", false},
|
||||||
|
{"", false},
|
||||||
|
} {
|
||||||
|
if got := IsTransientComplaint(tc.text); got != tc.want {
|
||||||
|
t.Errorf("IsTransientComplaint(%q) = %v, want %v", tc.text, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user