Merge the feed topic seeds (#178)

This commit is contained in:
2026-08-05 18:37:46 +04:00
4 changed files with 69 additions and 9 deletions
+15 -4
View File
@@ -292,6 +292,14 @@ const (
feedReadOut = 3
)
// feedFloor — the keyword test behind topicFeed, in the one-string shape
// turnIsAbout takes. router.ParseFeedQuery returns the category too, which the
// gate has no use for; the caller reads it separately.
func feedFloor(u string) bool {
_, ok := router.ParseFeedQuery(u)
return ok
}
// queryFeeds — "что нового в лентах?", "что нового по технологиям?"
// (Vikunja #258).
//
@@ -299,10 +307,13 @@ const (
// never speaks; asking is the trigger. If that ever changes, the thing that
// changed is "Maven is not a nag", not a detail of this file.
func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string, bool) {
q, ok := router.ParseFeedQuery(t.dec.Utterance)
if !ok {
// The seeds decide the subject; router.ParseFeedQuery is the floor behind
// them (V-522). The category still comes from the utterance either way,
// because a topic is marked by a preposition and needs no recogniser.
if !h.turnIsAbout(ctx, t, topicFeed, feedFloor) {
return "", false
}
category := router.FeedCategoryOf(t.dec.Utterance)
if !h.feedsOn {
// Claim only when nothing below can read the world. The reason this
// source used to claim unconditionally was that general knowledge would
@@ -327,7 +338,7 @@ func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string,
}
var picked []string
for _, n := range notes {
if !router.CategoryMatches(rss.NoteCategory(n.Text), q.Category) {
if !router.CategoryMatches(rss.NoteCategory(n.Text), category) {
continue
}
// The note carries title, summary, category tag and link; she reads the
@@ -339,7 +350,7 @@ func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string,
}
}
if len(picked) == 0 {
if q.Category != "" {
if category != "" {
return phraser.Q(phraser.QueryFeedsTopic, nil), true
}
return phraser.Q(phraser.QueryFeedsEmpty, nil), true
+30 -1
View File
@@ -9,7 +9,7 @@ import (
)
// Which subject is this question about — the weather, the house, the LAN, what
// needs looking at, or none of them. Third of the three mechanisms replacing hand-written Russian
// needs looking at, his feeds, or none of them. Third of the three mechanisms replacing hand-written Russian
// patterns (Vikunja #522, owner's call 2026-08-04). internal/lexicon holds the
// sets that can be finished and internal/morph answers the grammar questions;
// this is for the sets that can never be finished, because "is this about the
@@ -37,6 +37,11 @@ import (
// внимания", which is Praxis's operational state and reached the web search
// before the source existed (Vikunja #475).
//
// A fifth joined on 05-08-2026: the feeds, "что нового в лентах". Its word lists
// were the last pair of hand-written Russian stem lists in the router (V-522),
// and they carried the same admission in their own comments — vagueNouns exists
// because "что нового?" is a greeting that matched a feed noun.
//
// The regexes stay as the offline floor, unchanged, for a handler with no
// embedder or a turn whose vector never got computed. They are allowed to remain
// narrow now precisely because they are no longer the only answer.
@@ -52,6 +57,7 @@ const (
topicHome topicLabel = "home"
topicNetwork topicLabel = "network"
topicAttend topicLabel = "attention"
topicFeed topicLabel = "feeds"
topicOther topicLabel = "other"
)
@@ -117,7 +123,30 @@ var topicSeedSets = map[topicLabel][]string{
"what needs attention",
"what needs looking at right now",
},
topicFeed: {
"что нового в лентах",
"какие новости",
"что нового по технологиям",
"почитай заголовки",
// Two seeds carrying a day word beside the headlines. Without them
// "какие сегодня заголовки" read as weather, because "какая сегодня
// погода" is the nearest thing in the whole set with "сегодня" in it.
"заголовки за сегодня",
"какие главные новости за день",
"покажи новости за сегодня",
"что пишут в новостях",
"что нового про политику",
"расскажи что нового в ленте",
"what is new in the feeds",
"any news headlines today",
},
topicOther: {
// 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
// seeds that it will not clear topicMargin, and a thin call goes to
// ParseFeedQuery, which declines a vague noun with no topic beside it.
"что нового",
"как дела",
// Complaints, which are not requests to scan or to read the house.
// isNetworkQuery's comment names this one: a scan she runs unasked is
// the noisy behaviour the bounds exist to prevent.
+7
View File
@@ -25,6 +25,7 @@ func TestTopicFloorAnswersWithoutSeeds(t *testing.T) {
{"что включено в доме?", topicHome, isHomeQuery, true},
{"какие устройства в сети?", topicNetwork, isNetworkQuery, true},
{"что требует внимания?", topicAttend, isAttentionQuery, true},
{"что нового в лентах?", topicFeed, feedFloor, true},
{"почему небо синее", topicWeather, isWeatherQuery, false},
{"я дома", topicHome, isHomeQuery, false},
{"интернет не работает", topicNetwork, isNetworkQuery, false},
@@ -86,6 +87,12 @@ func TestONNXTopics(t *testing.T) {
{"что требует моего внимания сейчас", topicAttend, isAttentionQuery},
{"что не так с базой данных", topicAttend, isAttentionQuery},
{"есть что-то срочное на сегодня", topicAttend, isAttentionQuery},
{"что нового в ленте за сегодня", topicFeed, feedFloor},
{"какие сегодня заголовки", topicFeed, feedFloor},
{"что нового про искусственный интеллект", topicFeed, feedFloor},
// The greeting. It has to lose to topicOther, or fall thin enough that
// ParseFeedQuery — which declines a vague noun with no topic — answers.
{"что нового?", topicOther, feedFloor},
}
h := &reactiveHandler{recall: recallWiring{embedder: emb}}
+17 -4
View File
@@ -5,10 +5,16 @@ import "strings"
// Feed queries — "что нового в лентах?", "что нового по технологиям?"
// (Vikunja #258).
//
// Deterministic matching, like the calendar, plan and habit matchers above it:
// the LLM router says this is a query, and this decides whether it is a question
// about the feeds. A model deciding that would occasionally answer "что нового?"
// out of world knowledge, which is the one thing a feed reader exists to avoid.
// This file is the OFFLINE FLOOR as of 05-08-2026 (V-522). Whether a turn is
// about the feeds is a question about meaning, so the frozen seeds decide it —
// topicFeed in cmd/mavend/topics.go, through turnIsAbout. The word lists below
// stay because they always answer: a box with no embedder, a turn whose vector
// never got computed, and any call that does not clear topicMargin. They are
// allowed to stay narrow now that they are not the only answer.
//
// What has not changed is that no generative model decides this. One would
// occasionally answer "что нового?" out of world knowledge, which is the one
// thing a feed reader exists to avoid.
// FeedQuery — a parsed "what's new" question. Category is the topic he named
// ("технологии"), empty when he asked about the feeds in general.
@@ -95,6 +101,13 @@ func ParseFeedQuery(text string) (FeedQuery, bool) {
// cannot be mistaken for anything else.
var categoryPreps = map[string]bool{"по": true, "об": true, "про": true, "about": true, "on": true}
// FeedCategoryOf reads the topic out of an utterance without deciding whether the
// turn is a feed question at all. The seeds answer that now (topicFeed in
// cmd/mavend/topics.go, V-522), and they answer it for phrasings the word lists
// here never held — but a claimed turn still needs its category, and a category
// is marked by a preposition rather than recognised.
func FeedCategoryOf(text string) string { return feedCategory(planTokens(text)) }
func feedCategory(toks []string) string {
for i, t := range toks {
if categoryPreps[t] && i+1 < len(toks) {