062d4252ef
The context block now lists her real capabilities: reminders, notes and facts (write and recall), and the calendar — all three are code paths in mavend today. Weather, telegram and shell acts are listed only when the config actually has them, because offering something she cannot do is worse than staying quiet about it. Also drops the pronouns from the optional name/city line. The block's own "ты" is Maven, so "тебя зовут" read as her name and "его" would have shown her the third-person form she must never use about him. They are plain labels now. Vikunja #394. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
133 lines
5.5 KiB
Go
133 lines
5.5 KiB
Go
// Package persona builds the one shared context block that goes in front of
|
||
// every LLM system prompt: who the owner is, how to address him, and what
|
||
// time it is right now.
|
||
//
|
||
// Why one block and not a line pasted into each prompt: there are five
|
||
// prompts (nudges, action replies, chat, note queries, general knowledge) and
|
||
// the "address him as ты" rule had only reached two of them. Five copies drift.
|
||
// One block cannot.
|
||
//
|
||
// The rules here are defaults in code, not config. Maven is feminine and the
|
||
// owner is a man addressed informally — that is a hard constraint of the
|
||
// product, so it must hold with an empty config file. Config only ADDS
|
||
// optional facts (his name, his city).
|
||
package persona
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Facts — the optional, deployment-specific half of the block. All fields may
|
||
// be empty; the block is still correct and useful without them.
|
||
type Facts struct {
|
||
OwnerName string // his name, e.g. "Ками"
|
||
City string // where he is, e.g. "Москва"
|
||
Static string // the free-text `persona` config string, appended verbatim
|
||
|
||
// The two config-gated capabilities. They are listed only when this
|
||
// deployment actually has them, because a capability she names and cannot
|
||
// do is worse than one she never mentions.
|
||
Weather bool // an open-meteo provider is configured
|
||
Telegram bool // a telegram bot token + chat id are configured
|
||
Tools bool // at least one shell act is on the allowlist
|
||
}
|
||
|
||
var ruWeekdays = [...]string{"воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"}
|
||
|
||
var ruMonths = [...]string{
|
||
"января", "февраля", "марта", "апреля", "мая", "июня",
|
||
"июля", "августа", "сентября", "октября", "ноября", "декабря",
|
||
}
|
||
|
||
// Block renders the context block for one turn. Russian even in front of the
|
||
// English prompts: the rules it states are Russian grammar (ты/тебя, feminine
|
||
// verbs), and a Russian rule reads best stated in Russian.
|
||
//
|
||
// Keep it short. It ships on every turn to a 0.8B on laptop CPU, so every
|
||
// line here is latency.
|
||
func (f Facts) Block(now time.Time) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("Ты — Maven, домашняя ассистентка. О себе говоришь в женском роде: \"я записала\", \"я проверила\".\n")
|
||
|
||
// The address form gets its own line. It is the thing that kept getting
|
||
// lost when it was buried in prose.
|
||
b.WriteString("ОБРАЩЕНИЕ: владелец — мужчина, всегда на \"ты\" (ты, тебя, тебе, твой) и в единственном числе (\"выпей\", \"посмотри\"). Никогда \"вы\"/\"вас\"/\"ваш\". Никогда \"он\"/\"его\" о нём — ты говоришь ему, а не о нём. Глаголы о нём — в мужском роде (\"ты забыл\").\n")
|
||
|
||
if who := f.who(); who != "" {
|
||
b.WriteString(who + "\n")
|
||
}
|
||
|
||
b.WriteString(fmt.Sprintf("Сейчас: %s, %d %s %d, %02d:%02d (местное время).\n",
|
||
ruWeekdays[int(now.Weekday())], now.Day(), ruMonths[int(now.Month())-1], now.Year(),
|
||
now.Hour(), now.Minute()))
|
||
|
||
b.WriteString("Умеешь: " + strings.Join(f.can(), "; ") + ". Больше ничего — если просят другое, скажи прямо, что не умеешь.\n")
|
||
|
||
if s := strings.TrimSpace(f.Static); s != "" {
|
||
b.WriteString(s + "\n")
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// can lists what she can really do. Every entry here is a code path that
|
||
// exists in the daemon today:
|
||
// - reminders: IntentReminder → CoreAPI.CreateReminder, fired by the tick.
|
||
// - notes and facts: IntentNote/IntentFact write, IntentQuery reads them back.
|
||
// - calendar: IntentQuery answers "что у меня сегодня" from CalendarEvents.
|
||
// - weather / telegram / shell acts: only when configured (see Facts).
|
||
//
|
||
// Nothing speculative goes in this list. A capability she offers and cannot
|
||
// perform is worse than one she never mentions.
|
||
func (f Facts) can() []string {
|
||
c := []string{
|
||
"ставить напоминания",
|
||
"записывать заметки и факты и отвечать по ним",
|
||
"смотреть календарь",
|
||
}
|
||
if f.Weather {
|
||
c = append(c, "говорить погоду")
|
||
}
|
||
if f.Telegram {
|
||
c = append(c, "писать в телеграм")
|
||
}
|
||
if f.Tools {
|
||
c = append(c, "запускать разрешённые команды на сервере")
|
||
}
|
||
return c
|
||
}
|
||
|
||
// who renders the optional name/city line, or "" when neither is configured.
|
||
//
|
||
// Written as labels ("Имя владельца: ..."), not as a sentence with pronouns:
|
||
// the block's own "ты" is Maven, so "тебя зовут" would read as her name and
|
||
// "его" would model the third-person form she must never use about him.
|
||
func (f Facts) who() string {
|
||
name := strings.TrimSpace(f.OwnerName)
|
||
city := strings.TrimSpace(f.City)
|
||
switch {
|
||
case name != "" && city != "":
|
||
return "Имя владельца: " + name + ". Город: " + city + "."
|
||
case name != "":
|
||
return "Имя владельца: " + name + "."
|
||
case city != "":
|
||
return "Город: " + city + "."
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// Prepend puts the block in front of a system prompt. Nil-safe: a nil renderer
|
||
// (tests, the stub paths) returns the prompt untouched.
|
||
func Prepend(block func() string, prompt string) string {
|
||
if block == nil {
|
||
return prompt
|
||
}
|
||
s := strings.TrimSpace(block())
|
||
if s == "" {
|
||
return prompt
|
||
}
|
||
return s + "\n\n" + prompt
|
||
}
|