// 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 }