Merge the list read-back seeds (#179)

This commit is contained in:
2026-08-05 18:43:19 +04:00
4 changed files with 73 additions and 2 deletions
+13 -2
View File
@@ -95,6 +95,12 @@ func (h *reactiveHandler) removeListItem(ctx context.Context, cap router.ListCap
return "", false return "", false
} }
// listFloor — the keyword test behind topicList, in the shape turnIsAbout takes.
func listFloor(u string) bool {
_, ok := router.ParseListQuery(u)
return ok
}
// queryList — "что в списке покупок?", "что мне купить?". // queryList — "что в списке покупок?", "что мне купить?".
// //
// A query source, so it sits in querySources and either claims the turn or // A query source, so it sits in querySources and either claims the turn or
@@ -102,10 +108,15 @@ func (h *reactiveHandler) removeListItem(ctx context.Context, cap router.ListCap
// source is: the notes pass would otherwise answer a list question with // source is: the notes pass would otherwise answer a list question with
// whatever note is nearest. // whatever note is nearest.
func (h *reactiveHandler) queryList(ctx context.Context, t *queryTurn) (string, bool) { func (h *reactiveHandler) queryList(ctx context.Context, t *queryTurn) (string, bool) {
list, ok := router.ParseListQuery(t.dec.Utterance) if h.dataStore == nil {
if !ok || h.dataStore == nil {
return "", false return "", false
} }
// The seeds decide the subject and listQueryPrefixes is the floor behind
// them (V-522). Which list he named is a noun lookup either way.
if !h.turnIsAbout(ctx, t, topicList, listFloor) {
return "", false
}
list := router.ListNamedIn(t.dec.Utterance)
items, err := h.dataStore.ListItems(ctx, list, "") items, err := h.dataStore.ListItems(ctx, list, "")
if err != nil { if err != nil {
log.Printf("voice: list items: %v", err) log.Printf("voice: list items: %v", err)
+19
View File
@@ -58,6 +58,7 @@ const (
topicNetwork topicLabel = "network" topicNetwork topicLabel = "network"
topicAttend topicLabel = "attention" topicAttend topicLabel = "attention"
topicFeed topicLabel = "feeds" topicFeed topicLabel = "feeds"
topicList topicLabel = "list"
topicOther topicLabel = "other" topicOther topicLabel = "other"
) )
@@ -140,7 +141,25 @@ var topicSeedSets = map[topicLabel][]string{
"what is new in the feeds", "what is new in the feeds",
"any news headlines today", "any news headlines today",
}, },
// Reading a standing list back, and only that. Adding to one and clearing
// one stay on the phrase tables in internal/router/list.go — see its header
// for why a span and a delete are not seed-shaped work.
topicList: {
"что в списке покупок",
"что мне нужно купить",
"прочитай список покупок",
"покажи что в списке",
"что осталось купить в магазине",
"что мне нужно в аптеке",
"какой у меня список покупок",
"what is on my shopping list",
"read me the grocery list",
},
topicOther: { topicOther: {
// A task question is not a list read-back. They collide on "что у меня",
// and the list has its own table to lose to as well.
"какие у меня задачи",
"что у меня в делах",
// The bare newness opener, which is a greeting and not a request for // The bare newness opener, which is a greeting and not a request for
// headlines. It sits here on purpose: it is close enough to the feed // headlines. It sits here on purpose: it is close enough to the feed
// seeds that it will not clear topicMargin, and a thin call goes to // seeds that it will not clear topicMargin, and a thin call goes to
+7
View File
@@ -26,6 +26,7 @@ func TestTopicFloorAnswersWithoutSeeds(t *testing.T) {
{"какие устройства в сети?", topicNetwork, isNetworkQuery, true}, {"какие устройства в сети?", topicNetwork, isNetworkQuery, true},
{"что требует внимания?", topicAttend, isAttentionQuery, true}, {"что требует внимания?", topicAttend, isAttentionQuery, true},
{"что нового в лентах?", topicFeed, feedFloor, true}, {"что нового в лентах?", topicFeed, feedFloor, true},
{"что в списке покупок?", topicList, listFloor, true},
{"почему небо синее", topicWeather, isWeatherQuery, false}, {"почему небо синее", topicWeather, isWeatherQuery, false},
{"я дома", topicHome, isHomeQuery, false}, {"я дома", topicHome, isHomeQuery, false},
{"интернет не работает", topicNetwork, isNetworkQuery, false}, {"интернет не работает", topicNetwork, isNetworkQuery, false},
@@ -93,6 +94,12 @@ func TestONNXTopics(t *testing.T) {
// The greeting. It has to lose to topicOther, or fall thin enough that // The greeting. It has to lose to topicOther, or fall thin enough that
// ParseFeedQuery — which declines a vague noun with no topic — answers. // ParseFeedQuery — which declines a vague noun with no topic — answers.
{"что нового?", topicOther, feedFloor}, {"что нового?", topicOther, feedFloor},
{"что мне надо купить в магазине", topicList, listFloor},
{"прочитай мне список", topicList, listFloor},
{"что там в аптеке нужно взять", topicList, listFloor},
// A task read-back is not a list read-back, and the two collide on
// "что у меня".
{"какие у меня сейчас задачи", topicOther, listFloor},
} }
h := &reactiveHandler{recall: recallWiring{embedder: emb}} h := &reactiveHandler{recall: recallWiring{embedder: emb}}
+34
View File
@@ -17,6 +17,15 @@ import (
// The markers are deliberately explicit. "молоко закончилось" is an // The markers are deliberately explicit. "молоко закончилось" is an
// observation about the world and belongs in a note; only an instruction to // observation about the world and belongs in a note; only an instruction to
// put something on a list puts it there. // put something on a list puts it there.
//
// The four paths split on 05-08-2026, and the split is by what the caller needs
// rather than by language (V-522). Reading a list back needs one bit — is this
// about the list — so the seeds decide it, topicList through turnIsAbout, and
// listQueryPrefixes below is the offline floor. The other three keep the tables
// as the answer. Add and remove need to know WHERE the item starts, and a
// cosine over a whole utterance does not say which byte the milk begins at.
// Clear DELETES the list, so it stays on exact phrases: a false claim there
// loses rows he cannot get back, which is not the trade a margin makes.
// listTags — the lists he can name, as one dictionary form each. Russian // listTags — the lists he can name, as one dictionary form each. Russian
// declines the tag ("список покупок", "в покупки", "в покупках"), and the // declines the tag ("список покупок", "в покупки", "в покупках"), and the
@@ -186,6 +195,31 @@ func ParseListQuery(text string) (string, bool) {
return list, true return list, true
} }
// ListNamedIn reports which standing list an utterance names, anywhere in it,
// defaulting to покупки when it names none.
//
// takeListTag is not enough for a read-back, because it reads the FRONT of a
// remainder a prefix table has already eaten. The seeds claim a read-back
// without eating anything (topicList, cmd/mavend/topics.go, V-522), so "что мне
// нужно в аптеке" has to be scanned rather than trimmed. A list name is a noun
// in the dictionary, so this is a lookup and decides nothing about meaning.
func ListNamedIn(text string) string {
for _, f := range strings.Fields(strings.ToLower(text)) {
head := strings.Trim(f, listTrimCut)
for _, s := range listTags {
if morph.SameWord(head, s.word) {
return s.list
}
}
for _, s := range listTagsEN {
if head == s.word {
return s.list
}
}
}
return "покупки"
}
// ParseListClear reports whether an utterance crosses off a whole list. // ParseListClear reports whether an utterance crosses off a whole list.
func ParseListClear(text string) (string, bool) { func ParseListClear(text string) (string, bool) {
lower := strings.ToLower(strings.Trim(strings.TrimSpace(text), listTrimCut)) lower := strings.ToLower(strings.Trim(strings.TrimSpace(text), listTrimCut))