85a3397bf4
reminder_cancel.go is a stateful pre-route resolver ahead of a parked clarification and the statistical cascade. It accepts only an addressed command-position imperative plus the reminder or alarm noun, so questions, reported speech, past-tense reports and prohibitions establish no mutation authority. Subject terms keep negation and quantity, and a parsed time passes the same resolved-hour gate as capture. One match cancels through the typed IPC method. Several are stored as session candidates in the spoken order, capped at five, and only a whole affirmative ordinal consumes that list: re-querying on the follow-up would let a state change move the ordinal underneath him. No match, an unread time, a spent ordinal and an ambiguous delivery result are all explicit no-ops. command_prohibition.go is the first mutation boundary in a turn. A direct prohibition clears the three confirmation slots under their shared mutex, so a later bare "да" cannot revive authority he has just revoked. A parked clarify question is not authority and survives, suspended and repeated. refusesCommand is the same belt at the executor entry points, checked against the original utterance so a model rewriting Slots.Text cannot get around it. The rung is named in preRouteLadder, so /trace records whether it won or declined on every surface. --no-verify: master is the working branch this session by the owner's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
6.3 KiB
Go
128 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"regexp"
|
|
|
|
"github.com/kami/maven/internal/phraser"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// 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) {
|
|
// Product help is self knowledge too (Vikunja V-720), but unlike the prose description it
|
|
// must be exact: these examples name the grammar Maven actually accepts.
|
|
// Answer them before topic scoring so a phrasing such as "как отменить
|
|
// задачу" cannot leak to SearXNG as generic third-party instructions.
|
|
switch router.LocalHelpTopic(t.dec.Utterance) {
|
|
case router.HelpReminderCancel:
|
|
return "Скажи, например: «отмени напоминание про молоко». Если совпадений несколько, я попрошу выбрать одно.", true
|
|
case router.HelpTaskDrop:
|
|
return "Скажи, например: «убери из задач настроить бэкапы». Я уберу задачу из активного списка, не отмечая её выполненной.", true
|
|
}
|
|
if !h.turnIsAbout(ctx, t, topicSelf, selfFloor) {
|
|
return "", false
|
|
}
|
|
var reply string
|
|
if h.phraser != nil {
|
|
var err error
|
|
reply, err = h.phraser.PhraseSelf(ctx, t.dec.Utterance, 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
|
|
}
|