Merge V-555: a question about herself has an answer (#200)
This commit is contained in:
@@ -129,6 +129,11 @@ var querySources = []querySource{
|
|||||||
// below answers from the world's. A question about him that got this far
|
// below answers from the world's. A question about him that got this far
|
||||||
// has no answer in his data, and no outside source can supply one, so this
|
// has no answer in his data, and no outside source can supply one, so this
|
||||||
// stops the walk rather than let the encyclopedia and the model guess.
|
// stops the walk rather than let the encyclopedia and the model guess.
|
||||||
|
// A question about her sits just above the boundary, because it has no
|
||||||
|
// answer below one: refusing it as his says "не нашла у тебя такой записи"
|
||||||
|
// about her own description, and letting it through asks SearXNG about
|
||||||
|
// somebody else's assistant (Vikunja #555).
|
||||||
|
{name: "self", answer: (*reactiveHandler).querySelf},
|
||||||
{name: "personal", answer: (*reactiveHandler).queryPersonal},
|
{name: "personal", answer: (*reactiveHandler).queryPersonal},
|
||||||
// The world, read live. Owner's ruling of 2026-08-02: a metasearch hit beats
|
// The world, read live. Owner's ruling of 2026-08-02: a metasearch hit beats
|
||||||
// a frozen ZIM, so SearXNG asks before Kiwix does. Nothing of his is at
|
// a frozen ZIM, so SearXNG asks before Kiwix does. Nothing of his is at
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/phraser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A question about her — "что ты умеешь", "кто ты" — used to have no answer at
|
||||||
|
// all (Vikunja #555). It reached the personal boundary, which claimed it as his
|
||||||
|
// and said "не знаю — не нашла у тебя такой записи", because the boundary knows
|
||||||
|
// two sides and this is neither: her own description is not his data and it is
|
||||||
|
// not the world's either. Letting it past the boundary is no better, because
|
||||||
|
// then SearXNG answers about somebody else's assistant.
|
||||||
|
//
|
||||||
|
// The description does NOT live in the note store. Notes are his. A note about
|
||||||
|
// her sitting in his index would come back for "что я записал", would be fed to
|
||||||
|
// the digestion worker as something he said, and would be recalled by vector
|
||||||
|
// proximity for questions that are not about her at all. It is her own text, so
|
||||||
|
// it lives here, in one place, and it is the only copy.
|
||||||
|
//
|
||||||
|
// This source sits ABOVE the boundary, because a question about her never had
|
||||||
|
// an answer below it.
|
||||||
|
|
||||||
|
// selfDescription — what she is and what this box actually does. Frozen text,
|
||||||
|
// and the one rule for editing it: name only what is really wired. Anything
|
||||||
|
// that depends on config — the house, the LAN, the feeds, telegram, search — is
|
||||||
|
// named as depending on what he allowed, never claimed outright. Inventing a
|
||||||
|
// capability here is the same defect as inventing a fact, and it is worse than
|
||||||
|
// silence because he would plan around it.
|
||||||
|
//
|
||||||
|
// Written in her own voice, feminine, addressing him informally, because it is
|
||||||
|
// handed to the phraser as the evidence for the answer and the phraser will
|
||||||
|
// keep the words it is given.
|
||||||
|
const selfDescription = `Я Мэйвен, твоя помощница. Я живу на твоём сервере, ` +
|
||||||
|
`и наружу уходит только поисковый запрос — больше ничего.
|
||||||
|
|
||||||
|
Что я делаю сама: запоминаю, что ты мне говоришь, и потом отвечаю на вопросы ` +
|
||||||
|
`об этом; веду заметки; ставлю напоминания; читаю твой календарь и задачи; ` +
|
||||||
|
`отвечаю на вопросы о мире — сначала поиском, а если сети нет, то по ` +
|
||||||
|
`офлайновой энциклопедии.
|
||||||
|
|
||||||
|
Что зависит от того, что ты мне разрешил: дом, локальная сеть, ленты, ` +
|
||||||
|
`список покупок, погода, телеграм. Если что-то из этого не настроено, я ` +
|
||||||
|
`скажу об этом прямо, а не буду выдумывать ответ.
|
||||||
|
|
||||||
|
Говорю по-русски и по-английски.`
|
||||||
|
|
||||||
|
// selfSeeds — the questions this source claims. Scoring data like every other
|
||||||
|
// topic set: editing one moves the recogniser and has to be re-measured against
|
||||||
|
// TestONNXTopics.
|
||||||
|
//
|
||||||
|
// All of them are about HER — what she is, what she can do, who made her. The
|
||||||
|
// neighbouring set is topicAttend, "что требует внимания", which asks about the
|
||||||
|
// state of his things; the two share almost nothing but the second person.
|
||||||
|
var selfSeeds = []string{
|
||||||
|
"что ты умеешь",
|
||||||
|
"что ты можешь делать",
|
||||||
|
"кто ты такая",
|
||||||
|
"расскажи о себе",
|
||||||
|
"какие у тебя возможности",
|
||||||
|
// Added after measuring: it won self by 0.0002, under the margin, and the
|
||||||
|
// floor does not carry it — "способна" names no verb the floor matches.
|
||||||
|
"на что ты способна",
|
||||||
|
"чем ты можешь помочь",
|
||||||
|
"what can you do",
|
||||||
|
"who are you",
|
||||||
|
}
|
||||||
|
|
||||||
|
// selfFloor — the offline floor, for a handler with no embedder or a turn whose
|
||||||
|
// vector never got computed. Narrow on purpose, like every other floor here: it
|
||||||
|
// answers only when the seeds cannot, and a broad guess made blind is worse
|
||||||
|
// than a narrow one.
|
||||||
|
//
|
||||||
|
// Go's \b is ASCII-only and never fires next to a Cyrillic letter, so the
|
||||||
|
// Russian patterns spell the boundary out.
|
||||||
|
var selfPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`(?i)(^|[^\p{L}\p{N}])ты\s+(умеешь|можешь)([^\p{L}\p{N}]|$)`),
|
||||||
|
regexp.MustCompile(`(?i)(^|[^\p{L}\p{N}])кто\s+ты([^\p{L}\p{N}]|$)`),
|
||||||
|
regexp.MustCompile(`(?i)(^|[^\p{L}\p{N}])(расскажи|поведай)\s+о\s+себе([^\p{L}\p{N}]|$)`),
|
||||||
|
regexp.MustCompile(`(?i)\bwhat\s+can\s+you\s+do\b`),
|
||||||
|
regexp.MustCompile(`(?i)\bwho\s+are\s+you\b`),
|
||||||
|
}
|
||||||
|
|
||||||
|
func selfFloor(utterance string) bool {
|
||||||
|
for _, re := range selfPatterns {
|
||||||
|
if re.MatchString(utterance) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// querySelf answers a question about her from selfDescription. The description
|
||||||
|
// goes through the phraser as evidence so the answer is shaped to what he
|
||||||
|
// asked — "что ты умеешь" and "кто ты" want different halves of it — and falls
|
||||||
|
// back to the text itself, which is already readable, if the model is down.
|
||||||
|
func (h *reactiveHandler) querySelf(ctx context.Context, t *queryTurn) (string, bool) {
|
||||||
|
if !h.turnIsAbout(ctx, t, topicSelf, selfFloor) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var reply string
|
||||||
|
if h.phraser != nil {
|
||||||
|
var err error
|
||||||
|
reply, err = h.phraser.PhraseQuery(ctx, t.dec.Utterance, []string{selfDescription})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("voice: phrase self: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reply == "" {
|
||||||
|
reply = phraser.Q(phraser.QueryFound, map[string]string{"text": selfDescription})
|
||||||
|
}
|
||||||
|
return reply, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSelfFloorClaimsAQuestionAboutHerAndNothingElse — the offline floor, which
|
||||||
|
// is what answers with no embedder. Narrow on purpose, so the rows that must
|
||||||
|
// NOT match are the point.
|
||||||
|
func TestSelfFloorClaimsAQuestionAboutHerAndNothingElse(t *testing.T) {
|
||||||
|
claimed := []string{
|
||||||
|
"что ты умеешь",
|
||||||
|
"что ты можешь",
|
||||||
|
"а что ты умеешь?",
|
||||||
|
"кто ты",
|
||||||
|
"кто ты такая?",
|
||||||
|
"расскажи о себе",
|
||||||
|
"what can you do",
|
||||||
|
"who are you",
|
||||||
|
}
|
||||||
|
for _, u := range claimed {
|
||||||
|
if !selfFloor(u) {
|
||||||
|
t.Errorf("%q is a question about her and the floor missed it", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declined := []string{
|
||||||
|
"что у меня сегодня",
|
||||||
|
"расскажи про байкал",
|
||||||
|
"кто изобрёл телефон",
|
||||||
|
"что требует внимания",
|
||||||
|
"запиши что я пил воду",
|
||||||
|
// The floor spells its own word boundaries out, because Go's \b never
|
||||||
|
// fires next to a Cyrillic letter. Without that these would match.
|
||||||
|
"кто тыкал в розетку",
|
||||||
|
"расскажи о себестоимости",
|
||||||
|
}
|
||||||
|
for _, u := range declined {
|
||||||
|
if selfFloor(u) {
|
||||||
|
t.Errorf("%q is not about her and the floor claimed it", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSelfSourceAnswersFromTheDescription — with no embedder the source falls
|
||||||
|
// to the floor, and the answer has to be the description rather than silence.
|
||||||
|
func TestSelfSourceAnswersFromTheDescription(t *testing.T) {
|
||||||
|
h := personalHandler()
|
||||||
|
reply, claimed := h.querySelf(context.Background(), &queryTurn{
|
||||||
|
dec: router.Decision{Utterance: "что ты умеешь"},
|
||||||
|
})
|
||||||
|
if !claimed {
|
||||||
|
t.Fatal("a question about her must be claimed above the boundary")
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply, "напоминания") {
|
||||||
|
t.Errorf("the answer must come from the description: %q", reply)
|
||||||
|
}
|
||||||
|
if _, claimed := h.querySelf(context.Background(), &queryTurn{
|
||||||
|
dec: router.Decision{Utterance: "почему небо синее"},
|
||||||
|
}); claimed {
|
||||||
|
t.Error("a world question must pass this source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSelfDescriptionHoldsThePersona — it is her own text and she reads it out,
|
||||||
|
// so the same rules the phrasing eval enforces apply to it. Feminine
|
||||||
|
// self-reference, informal address, no pet names.
|
||||||
|
func TestSelfDescriptionHoldsThePersona(t *testing.T) {
|
||||||
|
lower := strings.ToLower(selfDescription)
|
||||||
|
for _, bad := range []string{"я рад ", "я готов ", "вы ", "ваш", "милый", "дорогой"} {
|
||||||
|
if strings.Contains(lower, bad) {
|
||||||
|
t.Errorf("the description breaks the persona on %q", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{"тво", "ты"} {
|
||||||
|
if !strings.Contains(lower, want) {
|
||||||
|
t.Errorf("the description must address him directly, missing %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSelfDescriptionClaimsNothingUnconditionally — the constraint that makes
|
||||||
|
// this text safe to read out. Every capability that depends on config has to be
|
||||||
|
// named as depending on it, and inventing one here is the same defect as
|
||||||
|
// inventing a fact.
|
||||||
|
func TestSelfDescriptionClaimsNothingUnconditionally(t *testing.T) {
|
||||||
|
conditional := selfDescription[strings.Index(selfDescription, "Что зависит"):]
|
||||||
|
for _, cap := range []string{"дом", "локальная сеть", "ленты", "список покупок", "погода", "телеграм"} {
|
||||||
|
if !strings.Contains(conditional, cap) {
|
||||||
|
t.Errorf("%q is configured, not wired — it must sit under the conditional half", cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ const (
|
|||||||
topicAttend topicLabel = "attention"
|
topicAttend topicLabel = "attention"
|
||||||
topicFeed topicLabel = "feeds"
|
topicFeed topicLabel = "feeds"
|
||||||
topicList topicLabel = "list"
|
topicList topicLabel = "list"
|
||||||
|
topicSelf topicLabel = "self"
|
||||||
topicOther topicLabel = "other"
|
topicOther topicLabel = "other"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,6 +156,14 @@ var topicSeedSets = map[topicLabel][]string{
|
|||||||
"what is on my shopping list",
|
"what is on my shopping list",
|
||||||
"read me the grocery list",
|
"read me the grocery list",
|
||||||
},
|
},
|
||||||
|
// Questions about her (Vikunja #555). The set lives in self.go beside the
|
||||||
|
// description it unlocks, so the two are edited together — a seed claiming
|
||||||
|
// a question the description does not answer is the failure mode.
|
||||||
|
//
|
||||||
|
// "что ты умеешь" was a topicOther seed until this existed, put there so an
|
||||||
|
// attention question had something to lose to. It is a self seed now, and
|
||||||
|
// it cannot be both: a phrasing on two sides never clears the margin.
|
||||||
|
topicSelf: selfSeeds,
|
||||||
topicOther: {
|
topicOther: {
|
||||||
// A task question is not a list read-back. They collide on "что у меня",
|
// A task question is not a list read-back. They collide on "что у меня",
|
||||||
// and the list has its own table to lose to as well.
|
// and the list has its own table to lose to as well.
|
||||||
@@ -185,8 +194,6 @@ var topicSeedSets = map[topicLabel][]string{
|
|||||||
"что я говорил про бэкапы",
|
"что я говорил про бэкапы",
|
||||||
"что у меня сегодня по календарю",
|
"что у меня сегодня по календарю",
|
||||||
"напомни мне позвонить маме",
|
"напомни мне позвонить маме",
|
||||||
// An attention question is about the state of his things; this is not.
|
|
||||||
"что ты умеешь",
|
|
||||||
"what did i say about backups",
|
"what did i say about backups",
|
||||||
// World questions that name a day (Vikunja #553). Weather was the only
|
// World questions that name a day (Vikunja #553). Weather was the only
|
||||||
// topic whose seeds carry a day word — four of its eight do — so every
|
// topic whose seeds carry a day word — four of its eight do — so every
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ func TestONNXTopics(t *testing.T) {
|
|||||||
{"как устроен телефон внутри", topicOther, isNetworkQuery},
|
{"как устроен телефон внутри", topicOther, isNetworkQuery},
|
||||||
// The control: a real scan is still a scan.
|
// The control: a real scan is still a scan.
|
||||||
{"какие устройства подключены к вайфаю", topicNetwork, isNetworkQuery},
|
{"какие устройства подключены к вайфаю", topicNetwork, isNetworkQuery},
|
||||||
|
// Questions about her (Vikunja #555), held out from selfSeeds.
|
||||||
|
{"а что ты вообще умеешь делать", topicSelf, selfFloor},
|
||||||
|
{"какие у тебя навыки", topicSelf, selfFloor},
|
||||||
|
{"расскажи мне о себе", topicSelf, selfFloor},
|
||||||
|
{"what are you able to do", topicSelf, selfFloor},
|
||||||
|
// The control: an attention question is about the state of his things,
|
||||||
|
// and it is the neighbour these seeds could have taken.
|
||||||
|
{"что требует внимания у меня в сервисах", topicAttend, isAttentionQuery},
|
||||||
}
|
}
|
||||||
|
|
||||||
h := &reactiveHandler{recall: recallWiring{embedder: emb}}
|
h := &reactiveHandler{recall: recallWiring{embedder: emb}}
|
||||||
|
|||||||
Reference in New Issue
Block a user