e4f0508a2f
Verbatim move of ruleTopics, ruleKeywords, ruleTopic, ruleKeyword, ruDur, fallbackNudges, fallbackNudge and buildNudgePrompt. Named for the file it sits beside: nudge_templates.go is the default path, this is the model one.
137 lines
4.8 KiB
Go
137 lines
4.8 KiB
Go
// 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
|
|
}
|