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
|
||||
// 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.
|
||||
// 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},
|
||||
// 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
|
||||
|
||||
@@ -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"
|
||||
topicFeed topicLabel = "feeds"
|
||||
topicList topicLabel = "list"
|
||||
topicSelf topicLabel = "self"
|
||||
topicOther topicLabel = "other"
|
||||
)
|
||||
|
||||
@@ -155,6 +156,14 @@ var topicSeedSets = map[topicLabel][]string{
|
||||
"what is on my shopping 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: {
|
||||
// A task question is not a list read-back. They collide on "что у меня",
|
||||
// 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",
|
||||
// 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
|
||||
|
||||
@@ -124,6 +124,14 @@ func TestONNXTopics(t *testing.T) {
|
||||
{"как устроен телефон внутри", topicOther, isNetworkQuery},
|
||||
// The control: a real scan is still a scan.
|
||||
{"какие устройства подключены к вайфаю", 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}}
|
||||
|
||||
Reference in New Issue
Block a user