Merge the feed topic seeds (#178)
This commit is contained in:
@@ -292,6 +292,14 @@ const (
|
|||||||
feedReadOut = 3
|
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 — "что нового в лентах?", "что нового по технологиям?"
|
// queryFeeds — "что нового в лентах?", "что нового по технологиям?"
|
||||||
// (Vikunja #258).
|
// (Vikunja #258).
|
||||||
//
|
//
|
||||||
@@ -299,10 +307,13 @@ const (
|
|||||||
// never speaks; asking is the trigger. If that ever changes, the thing that
|
// 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.
|
// changed is "Maven is not a nag", not a detail of this file.
|
||||||
func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string, bool) {
|
func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string, bool) {
|
||||||
q, ok := router.ParseFeedQuery(t.dec.Utterance)
|
// The seeds decide the subject; router.ParseFeedQuery is the floor behind
|
||||||
if !ok {
|
// 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
|
return "", false
|
||||||
}
|
}
|
||||||
|
category := router.FeedCategoryOf(t.dec.Utterance)
|
||||||
if !h.feedsOn {
|
if !h.feedsOn {
|
||||||
// Claim only when nothing below can read the world. The reason this
|
// Claim only when nothing below can read the world. The reason this
|
||||||
// source used to claim unconditionally was that general knowledge would
|
// 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
|
var picked []string
|
||||||
for _, n := range notes {
|
for _, n := range notes {
|
||||||
if !router.CategoryMatches(rss.NoteCategory(n.Text), q.Category) {
|
if !router.CategoryMatches(rss.NoteCategory(n.Text), category) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// The note carries title, summary, category tag and link; she reads the
|
// 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 len(picked) == 0 {
|
||||||
if q.Category != "" {
|
if category != "" {
|
||||||
return phraser.Q(phraser.QueryFeedsTopic, nil), true
|
return phraser.Q(phraser.QueryFeedsTopic, nil), true
|
||||||
}
|
}
|
||||||
return phraser.Q(phraser.QueryFeedsEmpty, nil), true
|
return phraser.Q(phraser.QueryFeedsEmpty, nil), true
|
||||||
|
|||||||
+30
-1
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Which subject is this question about — the weather, the house, the LAN, what
|
// 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
|
// 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;
|
// 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
|
// 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
|
// внимания", which is Praxis's operational state and reached the web search
|
||||||
// before the source existed (Vikunja #475).
|
// 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
|
// 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
|
// 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.
|
// narrow now precisely because they are no longer the only answer.
|
||||||
@@ -52,6 +57,7 @@ const (
|
|||||||
topicHome topicLabel = "home"
|
topicHome topicLabel = "home"
|
||||||
topicNetwork topicLabel = "network"
|
topicNetwork topicLabel = "network"
|
||||||
topicAttend topicLabel = "attention"
|
topicAttend topicLabel = "attention"
|
||||||
|
topicFeed topicLabel = "feeds"
|
||||||
topicOther topicLabel = "other"
|
topicOther topicLabel = "other"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +123,30 @@ var topicSeedSets = map[topicLabel][]string{
|
|||||||
"what needs attention",
|
"what needs attention",
|
||||||
"what needs looking at right now",
|
"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: {
|
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.
|
// Complaints, which are not requests to scan or to read the house.
|
||||||
// isNetworkQuery's comment names this one: a scan she runs unasked is
|
// isNetworkQuery's comment names this one: a scan she runs unasked is
|
||||||
// the noisy behaviour the bounds exist to prevent.
|
// the noisy behaviour the bounds exist to prevent.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func TestTopicFloorAnswersWithoutSeeds(t *testing.T) {
|
|||||||
{"что включено в доме?", topicHome, isHomeQuery, true},
|
{"что включено в доме?", topicHome, isHomeQuery, true},
|
||||||
{"какие устройства в сети?", topicNetwork, isNetworkQuery, true},
|
{"какие устройства в сети?", topicNetwork, isNetworkQuery, true},
|
||||||
{"что требует внимания?", topicAttend, isAttentionQuery, true},
|
{"что требует внимания?", topicAttend, isAttentionQuery, true},
|
||||||
|
{"что нового в лентах?", topicFeed, feedFloor, true},
|
||||||
{"почему небо синее", topicWeather, isWeatherQuery, false},
|
{"почему небо синее", topicWeather, isWeatherQuery, false},
|
||||||
{"я дома", topicHome, isHomeQuery, false},
|
{"я дома", topicHome, isHomeQuery, false},
|
||||||
{"интернет не работает", topicNetwork, isNetworkQuery, false},
|
{"интернет не работает", topicNetwork, isNetworkQuery, false},
|
||||||
@@ -86,6 +87,12 @@ func TestONNXTopics(t *testing.T) {
|
|||||||
{"что требует моего внимания сейчас", topicAttend, isAttentionQuery},
|
{"что требует моего внимания сейчас", topicAttend, isAttentionQuery},
|
||||||
{"что не так с базой данных", topicAttend, isAttentionQuery},
|
{"что не так с базой данных", 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}}
|
h := &reactiveHandler{recall: recallWiring{embedder: emb}}
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import "strings"
|
|||||||
// Feed queries — "что нового в лентах?", "что нового по технологиям?"
|
// Feed queries — "что нового в лентах?", "что нового по технологиям?"
|
||||||
// (Vikunja #258).
|
// (Vikunja #258).
|
||||||
//
|
//
|
||||||
// Deterministic matching, like the calendar, plan and habit matchers above it:
|
// This file is the OFFLINE FLOOR as of 05-08-2026 (V-522). Whether a turn is
|
||||||
// the LLM router says this is a query, and this decides whether it is a question
|
// about the feeds is a question about meaning, so the frozen seeds decide it —
|
||||||
// about the feeds. A model deciding that would occasionally answer "что нового?"
|
// topicFeed in cmd/mavend/topics.go, through turnIsAbout. The word lists below
|
||||||
// out of world knowledge, which is the one thing a feed reader exists to avoid.
|
// 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
|
// FeedQuery — a parsed "what's new" question. Category is the topic he named
|
||||||
// ("технологии"), empty when he asked about the feeds in general.
|
// ("технологии"), 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.
|
// cannot be mistaken for anything else.
|
||||||
var categoryPreps = map[string]bool{"по": true, "об": true, "про": true, "about": true, "on": true}
|
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 {
|
func feedCategory(toks []string) string {
|
||||||
for i, t := range toks {
|
for i, t := range toks {
|
||||||
if categoryPreps[t] && i+1 < len(toks) {
|
if categoryPreps[t] && i+1 < len(toks) {
|
||||||
|
|||||||
Reference in New Issue
Block a user