Tonight's work as one branch: all 46 reviewed PRs, verified green #47
+12
-6
@@ -606,14 +606,20 @@ func run(args []string) error {
|
||||
// city, the free-text persona string) out of the config. Everything here may
|
||||
// be empty — the context block is correct without any of it.
|
||||
func personaFacts(cfg *config.Config) persona.Facts {
|
||||
f := persona.Facts{
|
||||
// Telegram lives outside the voice block, so it counts either way.
|
||||
Telegram: cfg.Telegram != nil && cfg.Telegram.BotToken != "" && cfg.Telegram.ChatID != "",
|
||||
}
|
||||
if cfg.Voice == nil {
|
||||
return persona.Facts{}
|
||||
}
|
||||
return persona.Facts{
|
||||
OwnerName: cfg.Voice.OwnerName,
|
||||
City: cfg.Voice.City,
|
||||
Static: cfg.Voice.Persona,
|
||||
return f
|
||||
}
|
||||
f.OwnerName = cfg.Voice.OwnerName
|
||||
f.City = cfg.Voice.City
|
||||
f.Static = cfg.Voice.Persona
|
||||
// Same test wireVoice uses to pick the real provider over the stub.
|
||||
f.Weather = cfg.Voice.Weather != nil && cfg.Voice.Weather.Provider == "open-meteo"
|
||||
f.Tools = len(cfg.Voice.Tools) > 0
|
||||
return f
|
||||
}
|
||||
|
||||
// contextBlockFn returns the per-turn renderer of the shared context block.
|
||||
|
||||
@@ -25,6 +25,13 @@ 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{"воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"}
|
||||
@@ -57,23 +64,56 @@ func (f Facts) Block(now time.Time) string {
|
||||
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 + "."
|
||||
return "Имя владельца: " + name + ". Город: " + city + "."
|
||||
case name != "":
|
||||
return "Его зовут " + name + "."
|
||||
return "Имя владельца: " + name + "."
|
||||
case city != "":
|
||||
return "Он в городе " + city + "."
|
||||
return "Город: " + city + "."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -37,6 +37,28 @@ func TestBlockRendersTimePerTurn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// She may only offer what this deployment actually has.
|
||||
func TestCapabilitiesAreConfigGated(t *testing.T) {
|
||||
bare := Facts{}.Block(ref)
|
||||
for _, want := range []string{"напоминания", "заметки", "календарь"} {
|
||||
if !strings.Contains(bare, want) {
|
||||
t.Errorf("block missing always-on capability %q:\n%s", want, bare)
|
||||
}
|
||||
}
|
||||
for _, unwanted := range []string{"погоду", "телеграм", "команды"} {
|
||||
if strings.Contains(bare, unwanted) {
|
||||
t.Errorf("block offers unconfigured %q:\n%s", unwanted, bare)
|
||||
}
|
||||
}
|
||||
|
||||
full := Facts{Weather: true, Telegram: true, Tools: true}.Block(ref)
|
||||
for _, want := range []string{"погоду", "телеграм", "команды"} {
|
||||
if !strings.Contains(full, want) {
|
||||
t.Errorf("block missing configured capability %q:\n%s", want, full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrependNilIsSafe(t *testing.T) {
|
||||
if got := Prepend(nil, "PROMPT"); got != "PROMPT" {
|
||||
t.Errorf("Prepend(nil) = %q", got)
|
||||
|
||||
Reference in New Issue
Block a user