Merge V-555 tail: she does not look herself up (#201)

This commit is contained in:
2026-08-05 23:05:09 +04:00
4 changed files with 47 additions and 6 deletions
+6 -5
View File
@@ -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
+1 -1
View File
@@ -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)
}
+30
View File
@@ -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) {
+10
View File
@@ -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 }