Files
LocalAI/core/services/studio/triggerreply.go
T
Ettore Di Giacinto 69b89ac049 feat(studio): add one-off timers and plain-word answers to chat triggers
"Do it in 10 minutes" and "at 18:00" now work from a connector chat. The
agent proposes a timer with when {"in": ...} or {"at": ...}; it runs
the model once, reports to the chat it was set in, and is removed, also
when the run failed. A timer is at least one minute and at most a year
away. Recurring triggers keep their one-hour floor. Every proposal and
confirmation text states the cost: one model run.

A proposal is answered with a plain word in the same thread: yes, ok,
no, cancel. The code stays as a fallback when several proposals wait.
Only the whole message of an allowed sender counts, and only when the
capability gate allows it, so nothing the agent read can approve.

The approval setting of a binding now takes effect. In auto mode the
trigger is made at once and the chat is told how to cancel it; in
auto_readonly mode that holds only for a proposal that says its runs
only read. The owner's limits bound it, and a proposal over a limit is
asked instead, with the reason. Recurring triggers made this way end
after the lifetime.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 14:59:30 +00:00

107 lines
3.2 KiB
Go

package studio
import (
"errors"
"regexp"
"strconv"
"strings"
"time"
)
// The words a person answers a proposal with. The lists are short on purpose:
// a message is an answer only when all of it is one of these (with an
// optional code), so a longer message always goes to the agent. Words of other
// languages are optional; these cover the common ones.
var (
yesWords = wordSet("yes", "y", "yep", "yeah", "ok", "okay", "sure", "confirm", "approve", "approved", "go ahead", "do it",
"👍", "✅", "si", "sì", "ja", "oui", "vale", "ok grazie")
noWords = wordSet("no", "n", "nope", "cancel", "stop", "drop it", "no thanks", "no thank you", "never mind", "nevermind", "undo",
"👎", "❌", "nein", "non", "annulla")
)
func wordSet(w ...string) map[string]bool {
m := make(map[string]bool, len(w))
for _, x := range w {
m[x] = true
}
return m
}
var replyCodeRE = regexp.MustCompile(`^(.+?)\s+([a-z0-9]{4})$`)
// normalizeReply lowercases a message and removes the space and the sentence
// marks around it.
func normalizeReply(text string) string {
text = strings.ToLower(strings.TrimSpace(text))
text = strings.Trim(text, " \t.!?,;:")
return strings.Join(strings.Fields(text), " ")
}
// ParseReply reads a short answer to a proposal: a word of agreement or
// refusal, alone ("yes", "ok", "cancel") or followed by the code of the
// proposal ("yes 7K2Q"). The verb is "confirm" or "cancel"; the code is in upper
// case, or empty for a bare word. Any other text is not an answer.
func ParseReply(text string) (verb, code string, ok bool) {
t := normalizeReply(text)
if t == "" || len(t) > 40 {
return "", "", false
}
if v, found := replyVerb(t); found {
return v, "", true
}
if m := replyCodeRE.FindStringSubmatch(t); m != nil {
if v, found := replyVerb(m[1]); found {
return v, strings.ToUpper(m[2]), true
}
}
return "", "", false
}
func replyVerb(t string) (string, bool) {
switch {
case yesWords[t]:
return "confirm", true
case noWords[t]:
return "cancel", true
}
return "", false
}
// ---- Times that a chat gives ----
var (
delayPartRE = regexp.MustCompile(`(\d+)\s*(days?|d|hours?|hrs?|h|minutes?|mins?|m)`)
delayWholeRE = regexp.MustCompile(`^(\d+\s*(days?|d|hours?|hrs?|h|minutes?|mins?|m)\s*)+$`)
)
// ParseDelay reads how long to wait: "10m", "10 minutes", "an hour", "1h30m",
// "2 days", "half an hour". The unit is needed, and the result is positive.
func ParseDelay(text string) (time.Duration, error) {
t := normalizeReply(text)
t = strings.NewReplacer("half an hour", "30 minutes", "an hour", "1 hour", "a minute", "1 minute", "a day", "1 day").Replace(t)
if t == "" || !delayWholeRE.MatchString(t) {
return 0, errors.New(`the delay must look like "10 minutes", "1h30m" or "2 days"`)
}
var total time.Duration
for _, m := range delayPartRE.FindAllStringSubmatch(t, -1) {
n, err := strconv.Atoi(m[1])
if err != nil || n > 10000 {
return 0, errors.New("the delay is too large")
}
var unit time.Duration
switch m[2][0] {
case 'd':
unit = 24 * time.Hour
case 'h':
unit = time.Hour
default:
unit = time.Minute
}
total += time.Duration(n) * unit
}
if total <= 0 {
return 0, errors.New("the delay must be more than zero")
}
return total, nil
}