Merge branch 'fix/g07' into fix/integrated
# Conflicts: # internal/ipc/api.go # internal/ipc/client.go # internal/llm/client.go
This commit is contained in:
+47
-14
@@ -16,14 +16,25 @@ type FeedQuery struct {
|
||||
Category string
|
||||
}
|
||||
|
||||
// feedNouns — the words that make a question about the feeds themselves.
|
||||
// feedNouns — the words that name the feeds themselves. One of these is enough,
|
||||
// with an ask, to make the turn a feed question.
|
||||
var feedNouns = []string{
|
||||
"лента", "ленте", "ленты", "лентах", "лентам",
|
||||
"новости", "новостей", "новостях", "новостям",
|
||||
"новое", "нового", "новенького",
|
||||
"feed", "feeds", "news", "headlines",
|
||||
}
|
||||
|
||||
// vagueNouns — the newness words that are NOT about the feeds by themselves.
|
||||
//
|
||||
// "что нового?" is the most common opener in the language and it is a greeting,
|
||||
// not a request for headlines. It used to match here, so the shipping daemon —
|
||||
// which has no feeds block — answered "я пока не читаю ленты, они не настроены",
|
||||
// a configuration status in reply to hello. With feeds on it answered "в лентах
|
||||
// пока ничего нового", which is no better. A vague noun claims the turn only
|
||||
// when the utterance narrows it: a named topic ("что нового по технологиям"), or
|
||||
// a feed noun somewhere in it ("что нового в лентах").
|
||||
var vagueNouns = []string{"новое", "нового", "новенького", "new"}
|
||||
|
||||
// newnessMarkers — the "что нового" half. "нового" alone is in feedNouns
|
||||
// because it carries the question on its own ("что нового?"); a bare "лента"
|
||||
// needs the ask, which is what askMarkers below is for.
|
||||
@@ -33,13 +44,15 @@ var askMarkers = []string{
|
||||
}
|
||||
|
||||
// ParseFeedQuery reports whether an utterance asks what is new in the feeds, and
|
||||
// which topic if it names one after "по"/"о"/"про"/"about".
|
||||
// which topic if it names one after "по"/"об"/"про"/"about".
|
||||
//
|
||||
// Both a feed noun and an ask are required. "у меня новая лента в инстаграме" is
|
||||
// a statement and must not be read as a request to recite headlines.
|
||||
// A feed noun and an ask are required. "у меня новая лента в инстаграме" is a
|
||||
// statement and must not be read as a request to recite headlines. A vague
|
||||
// newness word counts as the noun only when a topic is named — see vagueNouns
|
||||
// for why the bare "что нового?" must fall through.
|
||||
func ParseFeedQuery(text string) (FeedQuery, bool) {
|
||||
toks := planTokens(text)
|
||||
noun, ask := false, false
|
||||
noun, vague, ask := false, false, false
|
||||
for _, t := range toks {
|
||||
for _, n := range feedNouns {
|
||||
if t == n {
|
||||
@@ -47,6 +60,12 @@ func ParseFeedQuery(text string) (FeedQuery, bool) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, n := range vagueNouns {
|
||||
if t == n {
|
||||
vague = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, a := range askMarkers {
|
||||
if t == a {
|
||||
ask = true
|
||||
@@ -54,24 +73,34 @@ func ParseFeedQuery(text string) (FeedQuery, bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !noun || !ask {
|
||||
if !ask {
|
||||
return FeedQuery{}, false
|
||||
}
|
||||
return FeedQuery{Category: feedCategory(toks)}, true
|
||||
cat := feedCategory(toks)
|
||||
if !noun && !(vague && cat != "") {
|
||||
return FeedQuery{}, false
|
||||
}
|
||||
return FeedQuery{Category: cat}, true
|
||||
}
|
||||
|
||||
// categoryPreps — the prepositions a topic follows. Russian marks the topic with
|
||||
// a preposition ("по технологиям", "про политику"), so the word after one is the
|
||||
// category; there is no stemming here, and the match against the configured
|
||||
// category is a prefix comparison for exactly that reason.
|
||||
var categoryPreps = map[string]bool{"по": true, "о": true, "об": true, "про": true, "about": true, "on": true}
|
||||
//
|
||||
// "о" is not in the list. It is one rune and it turns up as filler, a typo and
|
||||
// half of "о'кей", so any utterance carrying a stray "о" produced a category of
|
||||
// whatever word came next and she answered "по этой теме в лентах пока ничего"
|
||||
// to a question that named no theme. "об" and "про" carry the same meaning and
|
||||
// cannot be mistaken for anything else.
|
||||
var categoryPreps = map[string]bool{"по": true, "об": true, "про": true, "about": true, "on": true}
|
||||
|
||||
func feedCategory(toks []string) string {
|
||||
for i, t := range toks {
|
||||
if categoryPreps[t] && i+1 < len(toks) {
|
||||
next := toks[i+1]
|
||||
// "по новостям" names no topic, it repeats the noun.
|
||||
for _, n := range feedNouns {
|
||||
for _, n := range append(append([]string{}, feedNouns...), vagueNouns...) {
|
||||
if next == n {
|
||||
return ""
|
||||
}
|
||||
@@ -82,12 +111,16 @@ func feedCategory(toks []string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CategoryMatches reports whether a note's text plausibly belongs to the
|
||||
// category he named. Russian inflects the topic ("технологиям" vs the configured
|
||||
// CategoryMatches reports whether a feed note's own category tag is the one he
|
||||
// named. Russian inflects the topic ("технологиям" vs the configured
|
||||
// "технологии"), and there is no stemmer in this repo, so the comparison is on a
|
||||
// common prefix — long enough that "полит" and "погод" stay apart, short enough
|
||||
// to survive a case ending.
|
||||
func CategoryMatches(text, category string) bool {
|
||||
//
|
||||
// tag is the note's stored category (rss.NoteCategory), NOT the whole note. It
|
||||
// used to be the whole note, which meant "что нового про погоду" matched any
|
||||
// tech headline whose link happened to contain "pogod".
|
||||
func CategoryMatches(tag, category string) bool {
|
||||
if category == "" {
|
||||
return true
|
||||
}
|
||||
@@ -95,7 +128,7 @@ func CategoryMatches(text, category string) bool {
|
||||
if stem == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.ToLower(text), stem)
|
||||
return strings.Contains(strings.ToLower(tag), stem)
|
||||
}
|
||||
|
||||
// categoryStem cuts a word down to the part inflection leaves alone. 5 runes is
|
||||
|
||||
@@ -9,13 +9,19 @@ func TestParseFeedQuery(t *testing.T) {
|
||||
category string
|
||||
}{
|
||||
{"что нового в лентах?", true, ""},
|
||||
{"что нового?", true, ""},
|
||||
{"что нового по технологиям", true, "технологиям"},
|
||||
{"какие новости?", true, ""},
|
||||
{"что нового по технологиям?", true, "технологиям"},
|
||||
{"расскажи новости про политику", true, "политику"},
|
||||
{"что нового по новостям", true, ""},
|
||||
{"what's new in the feeds?", true, ""},
|
||||
{"any news about kubernetes", true, "kubernetes"},
|
||||
// "что нового?" is a greeting. Claiming it made the shipping daemon
|
||||
// answer hello with "я пока не читаю ленты — они не настроены".
|
||||
{"что нового?", false, ""},
|
||||
{"ну что нового", false, ""},
|
||||
// A stray "о" is not a topic marker.
|
||||
{"что нового в лентах, о боже", true, ""},
|
||||
// Statements, not requests.
|
||||
{"у меня новая лента в инстаграме", false, ""},
|
||||
{"новости меня утомили", false, ""},
|
||||
@@ -36,12 +42,16 @@ func TestParseFeedQuery(t *testing.T) {
|
||||
|
||||
func TestCategoryMatches(t *testing.T) {
|
||||
// The inflected form he says must match the form the config spells.
|
||||
if !CategoryMatches("Новый релиз [технологии]", "технологиям") {
|
||||
if !CategoryMatches("технологии", "технологиям") {
|
||||
t.Error("inflected category did not match")
|
||||
}
|
||||
if CategoryMatches("Новый релиз [технологии]", "политику") {
|
||||
if CategoryMatches("технологии", "политику") {
|
||||
t.Error("unrelated category matched")
|
||||
}
|
||||
// The tag, not the note. The link in a tech headline is not a weather report.
|
||||
if CategoryMatches("технологии", "погоду") {
|
||||
t.Error("a tech note matched a weather question")
|
||||
}
|
||||
if !CategoryMatches("anything", "") {
|
||||
t.Error("an empty category must match everything")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user