diff --git a/internal/phraser/llmphraser.go b/internal/phraser/llmphraser.go index 790e065..6717e8f 100644 --- a/internal/phraser/llmphraser.go +++ b/internal/phraser/llmphraser.go @@ -597,124 +597,3 @@ func nonEmpty(sources []string) []string { } return out } - -// ruleTopics — Russian gloss for each built-in rule name. The rule names are -// English identifiers; a 0.8B asked to nudge about "netdata_critical" writes -// about nothing. The daemon knows what its own rules mean, so it says so. -var ruleTopics = map[string]string{ - "water": "он давно не пил воду", - "meal": "он давно не ел", - "break": "он давно без перерыва, пора встать и размяться", - "service_down": "сервис не отвечает, лежит", - "netdata_critical": "критический алярм в netdata, проблема с диском или местом", -} - -// ruleKeywords — the word the message must contain. The 0.8B drifts to -// whatever topic it saw last unless the required word is named outright. -var ruleKeywords = map[string]string{ - "water": "воду", - "meal": "поешь", - "break": "перерыв", - "service_down": "сервис", - "netdata_critical": "диск", -} - -// ruleTopic turns a rule name into a Russian description of the situation. -// "routine:зарядка" and "morning:утро" carry their own Russian suffix. -func ruleTopic(rule string) string { - if t, ok := ruleTopics[rule]; ok { - return t - } - if i := strings.IndexByte(rule, ':'); i > 0 && i+1 < len(rule) { - switch rule[:i] { - case "morning": - return "утро, пора начать день: " + rule[i+1:] - default: - return "пора сделать по распорядку: " + rule[i+1:] - } - } - return rule -} - -// ruleKeyword — the word the nudge must contain, or "" when the rule name's -// own Russian suffix already is that word. -func ruleKeyword(rule string) string { - if k, ok := ruleKeywords[rule]; ok { - return k - } - if i := strings.IndexByte(rule, ':'); i > 0 && i+1 < len(rule) { - return rule[i+1:] - } - return "" -} - -// ruDur — duration in Russian. humanDur is English and its output was landing -// verbatim in the message. -func ruDur(d time.Duration) string { - if d < 0 { - d = 0 - } - h, m := int(d.Hours()), int(d.Minutes())%60 - switch { - case h >= 2: - return fmt.Sprintf("%d ч", h) - case h == 1 && m >= 30: - return "полтора часа" - case h == 1: - return "час" - default: - return fmt.Sprintf("%d мин", m) - } -} - -// fallbackNudge — plain Russian for when the model returns nothing parseable. -var fallbackNudges = map[string]string{ - "water": "Ты давно не пил воду.", - "meal": "Ты давно не ел, поешь.", - "break": "Пора сделать перерыв.", - "service_down": "Сервис не отвечает.", - "netdata_critical": "Критический алярм: проверь диск.", -} - -func fallbackNudge(c loop.Candidate) string { - if down := loop.DownServices(c.State); len(down) > 0 { - return "Не отвечает: " + strings.Join(down, ", ") + "." - } - if s, ok := fallbackNudges[c.Rule.Name]; ok { - return s - } - if kw := ruleKeyword(c.Rule.Name); kw != "" { - return "Напоминаю: " + kw + "." - } - return "Напоминаю о деле." -} - -func buildNudgePrompt(c loop.Candidate) string { - var ctxParts []string - ctxParts = append(ctxParts, "Ситуация: "+ruleTopic(c.Rule.Name)) - if f, ok := c.State.Facts[c.Rule.Name]; ok && f.Key != "" && f.Key != c.Rule.Name { - ctxParts = append(ctxParts, "Что именно: "+f.Key) - } - if down := loop.DownServices(c.State); len(down) > 0 { - // The names come from the same helper the rule fired on, so the model - // is never handed a service that is actually up. - ctxParts = append(ctxParts, "Какие сервисы лежат: "+strings.Join(down, ", ")) - } - if d, ok := c.State.Since(c.Rule.Name); ok { - ctxParts = append(ctxParts, "Прошло: "+ruDur(d)) - } - switch sevLabel(c.Severity) { - case "alarm": - ctxParts = append(ctxParts, "Срочно, скажи прямо.") - case "ops": - ctxParts = append(ctxParts, "Это про сервер, не про здоровье.") - } - tail := "Напиши напоминание про эту ситуацию. Одно предложение, по-русски, в JSON." - if kw := ruleKeyword(c.Rule.Name); kw != "" { - // Last line on purpose: a 0.8B weights the end of the prompt hardest, - // and without the required word it drifts back to the examples. - tail += " Ответ ДОЛЖЕН содержать слово «" + kw + "»." - } - - return strings.Join(ctxParts, "\n") + "\n\n" + tail -} diff --git a/internal/phraser/nudge_llm.go b/internal/phraser/nudge_llm.go new file mode 100644 index 0000000..875dc69 --- /dev/null +++ b/internal/phraser/nudge_llm.go @@ -0,0 +1,136 @@ +// phraser/nudge_llm.go — what the model is told about one nudge, and what she +// says when it gives back nothing usable. +// +// The default nudge path is not this one: hand-written templates word every +// nudge unless Config.LLMNudges is on. See nudge_templates.go, and the note on +// that field for why. +package phraser + +import ( + "fmt" + "strings" + "time" + + "github.com/kami/maven/internal/loop" +) + +// ruleTopics — Russian gloss for each built-in rule name. The rule names are +// English identifiers; a 0.8B asked to nudge about "netdata_critical" writes +// about nothing. The daemon knows what its own rules mean, so it says so. +var ruleTopics = map[string]string{ + "water": "он давно не пил воду", + "meal": "он давно не ел", + "break": "он давно без перерыва, пора встать и размяться", + "service_down": "сервис не отвечает, лежит", + "netdata_critical": "критический алярм в netdata, проблема с диском или местом", +} + +// ruleKeywords — the word the message must contain. The 0.8B drifts to +// whatever topic it saw last unless the required word is named outright. +var ruleKeywords = map[string]string{ + "water": "воду", + "meal": "поешь", + "break": "перерыв", + "service_down": "сервис", + "netdata_critical": "диск", +} + +// ruleTopic turns a rule name into a Russian description of the situation. +// "routine:зарядка" and "morning:утро" carry their own Russian suffix. +func ruleTopic(rule string) string { + if t, ok := ruleTopics[rule]; ok { + return t + } + if i := strings.IndexByte(rule, ':'); i > 0 && i+1 < len(rule) { + switch rule[:i] { + case "morning": + return "утро, пора начать день: " + rule[i+1:] + default: + return "пора сделать по распорядку: " + rule[i+1:] + } + } + return rule +} + +// ruleKeyword — the word the nudge must contain, or "" when the rule name's +// own Russian suffix already is that word. +func ruleKeyword(rule string) string { + if k, ok := ruleKeywords[rule]; ok { + return k + } + if i := strings.IndexByte(rule, ':'); i > 0 && i+1 < len(rule) { + return rule[i+1:] + } + return "" +} + +// ruDur — duration in Russian. humanDur is English and its output was landing +// verbatim in the message. +func ruDur(d time.Duration) string { + if d < 0 { + d = 0 + } + h, m := int(d.Hours()), int(d.Minutes())%60 + switch { + case h >= 2: + return fmt.Sprintf("%d ч", h) + case h == 1 && m >= 30: + return "полтора часа" + case h == 1: + return "час" + default: + return fmt.Sprintf("%d мин", m) + } +} + +// fallbackNudge — plain Russian for when the model returns nothing parseable. +var fallbackNudges = map[string]string{ + "water": "Ты давно не пил воду.", + "meal": "Ты давно не ел, поешь.", + "break": "Пора сделать перерыв.", + "service_down": "Сервис не отвечает.", + "netdata_critical": "Критический алярм: проверь диск.", +} + +func fallbackNudge(c loop.Candidate) string { + if down := loop.DownServices(c.State); len(down) > 0 { + return "Не отвечает: " + strings.Join(down, ", ") + "." + } + if s, ok := fallbackNudges[c.Rule.Name]; ok { + return s + } + if kw := ruleKeyword(c.Rule.Name); kw != "" { + return "Напоминаю: " + kw + "." + } + return "Напоминаю о деле." +} + +func buildNudgePrompt(c loop.Candidate) string { + var ctxParts []string + ctxParts = append(ctxParts, "Ситуация: "+ruleTopic(c.Rule.Name)) + if f, ok := c.State.Facts[c.Rule.Name]; ok && f.Key != "" && f.Key != c.Rule.Name { + ctxParts = append(ctxParts, "Что именно: "+f.Key) + } + if down := loop.DownServices(c.State); len(down) > 0 { + // The names come from the same helper the rule fired on, so the model + // is never handed a service that is actually up. + ctxParts = append(ctxParts, "Какие сервисы лежат: "+strings.Join(down, ", ")) + } + if d, ok := c.State.Since(c.Rule.Name); ok { + ctxParts = append(ctxParts, "Прошло: "+ruDur(d)) + } + switch sevLabel(c.Severity) { + case "alarm": + ctxParts = append(ctxParts, "Срочно, скажи прямо.") + case "ops": + ctxParts = append(ctxParts, "Это про сервер, не про здоровье.") + } + tail := "Напиши напоминание про эту ситуацию. Одно предложение, по-русски, в JSON." + if kw := ruleKeyword(c.Rule.Name); kw != "" { + // Last line on purpose: a 0.8B weights the end of the prompt hardest, + // and without the required word it drifts back to the examples. + tail += " Ответ ДОЛЖЕН содержать слово «" + kw + "»." + } + + return strings.Join(ctxParts, "\n") + "\n\n" + tail +}