From c6b11a6d1d85a9284a190fd1234aff26b5536fa9 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 5 Aug 2026 23:05:09 +0400 Subject: [PATCH] she does not look herself up (V-555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects found probing the new source on the box. "кто ты" was answered from one of his notes. The self source sat below memory and notes, which match by proximity and have no idea the subject is her. It belongs above all three: a question about her has no answer in his data either. And PhraseQuery opens every answer with "вот что я нашла: ", which is deliberate — it marks the answer as a lookup. Her own description is the one subject she did not look up, so this is PhraseSelf instead, same read-only discipline and its own opener. The Stub reads the description out as it stands, which needs no fallback: it is already her voice. --- cmd/mavend/actions_query.go | 11 ++++++----- cmd/mavend/self.go | 2 +- internal/phraser/llmphraser.go | 30 ++++++++++++++++++++++++++++++ internal/phraser/phraser.go | 10 ++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/cmd/mavend/actions_query.go b/cmd/mavend/actions_query.go index b192892..e24651a 100644 --- a/cmd/mavend/actions_query.go +++ b/cmd/mavend/actions_query.go @@ -122,6 +122,12 @@ var querySources = []querySource{ {name: "network", answer: (*reactiveHandler).queryNetwork}, {name: "calendar", answer: (*reactiveHandler).queryCalendar, dateAware: true}, {name: "weather", answer: (*reactiveHandler).queryWeather}, + // A question about her, above the three sources that search his own data + // (Vikunja #555). It has no answer anywhere else: below the boundary + // SearXNG answers about somebody else's assistant, and above it his notes + // answer by proximity — "кто ты" came back from a note of his, measured on + // the box, because the recall index has no idea the subject is her. + {name: "self", answer: (*reactiveHandler).querySelf}, {name: "embed", answer: (*reactiveHandler).queryEmbed}, {name: "memory", answer: (*reactiveHandler).queryMemory}, {name: "notes", answer: (*reactiveHandler).queryNotes}, @@ -129,11 +135,6 @@ 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 diff --git a/cmd/mavend/self.go b/cmd/mavend/self.go index 58afaa9..dbe5dec 100644 --- a/cmd/mavend/self.go +++ b/cmd/mavend/self.go @@ -104,7 +104,7 @@ func (h *reactiveHandler) querySelf(ctx context.Context, t *queryTurn) (string, var reply string if h.phraser != nil { var err error - reply, err = h.phraser.PhraseQuery(ctx, t.dec.Utterance, []string{selfDescription}) + reply, err = h.phraser.PhraseSelf(ctx, t.dec.Utterance, selfDescription) if err != nil { log.Printf("voice: phrase self: %v", err) } diff --git a/internal/phraser/llmphraser.go b/internal/phraser/llmphraser.go index 84d486b..3b364a1 100644 --- a/internal/phraser/llmphraser.go +++ b/internal/phraser/llmphraser.go @@ -941,6 +941,36 @@ func (p *LLMPhraser) knowledgePrompt(utterance string) (sys, user string) { fmt.Sprintf("Пользователь спрашивает: \"%s\".", utterance) } +// PhraseSelf answers a question about her from her own description. Same +// discipline as the evidence branch — say only what the text says — and a +// different opener, because "вот что я нашла: я — твоя помощница" says she +// looked herself up (Vikunja #555). She did not; this is the one subject she +// does not have to read about. +// +// On any error it reads the description out rather than ship a fragment. That +// is already a readable answer, which is why this needs no separate fallback. +func (p *LLMPhraser) PhraseSelf(ctx context.Context, utterance, description string) (string, error) { + sys := persona.Prepend(p.cfg.ContextBlock, + "Он спрашивает о тебе. Отвечай ТОЛЬКО по описанию, которое тебе дали: всё, что ты говоришь о себе, должно быть в нём. "+ + "Не добавляй умений, которых там нет, и не догадывайся. Не начинай с \"вот что я нашла\" — ты говоришь о себе, а не о находке. "+ + "Отвечай по-русски, коротко и своими словами. О себе — в женском роде, глаголы в прошедшем времени с окончанием -ла. "+ + "Он мужчина, обращайся к нему на \"ты\". Отвечай ТОЛЬКО одним объектом JSON: {\"response\": \"...\", \"mood\": \"neutral\"}.") + prompt := fmt.Sprintf("Он спрашивает: %q\n\nТвоё описание:\n%s\n\nОтветь ему на то, что он спросил.", utterance, description) + resp, err := p.chatWithSystem(ctx, sys, prompt, 768) + text, _, perr := parseResponseMood(resp) + if err != nil || perr != nil { + cause := err + if cause == nil { + cause = perr + } + return description, fmt.Errorf("phrase self: %w", cause) + } + if text != "" { + return text, nil + } + return description, nil +} + // evidencePrompt — the sources branch: read these, add nothing. Shared with // PhraseWorld for the same reason as knowledgePrompt. func (p *LLMPhraser) evidencePrompt(utterance string, notes []string) (sys, user string) { diff --git a/internal/phraser/phraser.go b/internal/phraser/phraser.go index 9cdb34b..df184fc 100644 --- a/internal/phraser/phraser.go +++ b/internal/phraser/phraser.go @@ -49,6 +49,9 @@ type Phraser interface { PhraseNudge(ctx context.Context, c loop.Candidate) (delivery.PhrasedNudge, error) PhraseReminder(ctx context.Context, d loop.ReminderDecision) (delivery.PhrasedReminder, error) PhraseQuery(ctx context.Context, utterance string, notes []string) (string, error) + // PhraseSelf answers a question about her from her own description, which + // is not a source she read and must not be phrased as one (Vikunja #555). + PhraseSelf(ctx context.Context, utterance, description string) (string, error) PhraseChat(ctx context.Context, utterance string, history []dialogue.Turn) (string, error) Close() error } @@ -81,6 +84,13 @@ func (s *Stub) PhraseQuery(_ context.Context, _ string, notes []string) (string, return SourcesFallback(strings.Join(notes, "; ")), nil } +// PhraseSelf reads the description out as it stands. There is nothing to fall +// back to and nothing to shorten: the text is already written in her voice, and +// that is the whole reason it is a constant rather than a prompt. +func (s *Stub) PhraseSelf(_ context.Context, _, description string) (string, error) { + return description, nil +} + // Close implements Phraser.Close (no-op for the stub). func (s *Stub) Close() error { return nil }