Add /tldr: a private overview of what you missed in a channel
/tldr [since] summarizes the messages in the channel it is run in that the caller has not been summarized yet, using a small local model on Ollama, and replies ephemerally. The answer streams into the reply as it is written. - Per-user, per-channel cursor (tldr_cursors) so each run picks up where the last left off; `since:` (e.g. 6h, 2d, max 7d) peeks without moving it. First use covers the last 24h, and a stale cursor never reaches back more than 7 days. - Only summarizes channels both the caller and the bot can read. - The transcript drops bots, system messages, reactions and filler, turns links, GIFs, images and files into short tags ([link: site], [image], [file: name], preview titles), marks replies, and writes a date marker where the chat paused for 3h or more so the model can keep topics apart. - The prompt asks for one bullet per conversation, oldest first, with a worked example; it was tuned against a synthetic chat and a real export with qwen3:4b-instruct-2507 (the default model). - One model call at a time across the bot, one /tldr per user. - docker-compose: Ollama service (CPU only, 8g memory cap: 6g killed the runner mid-request), model pull on startup. Needs the tldr_cursors migration (velox-dashboard-api 000016). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b1ea5f17e3
commit
7ae1fa5747
21 files changed
+2423
-2
No files matched your search
@@ -0,0 +1,55 @@
|
||||
package tldr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const discordEpochMs = 1420070400000
|
||||
|
||||
var sinceRe = regexp.MustCompile(`^(\d+)([mhd])$`)
|
||||
|
||||
// ParseSince turns "30m", "6h" or "2d" into a duration, clamped to
|
||||
// [1m, maxWindow]. time.ParseDuration has no day unit, hence the custom parser.
|
||||
func ParseSince(s string) (time.Duration, error) {
|
||||
m := sinceRe.FindStringSubmatch(strings.ToLower(strings.TrimSpace(s)))
|
||||
if m == nil {
|
||||
return 0, fmt.Errorf("couldn't read %q, use something like 30m, 6h or 2d", s)
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("couldn't read %q, use something like 30m, 6h or 2d", s)
|
||||
}
|
||||
|
||||
unit := map[string]time.Duration{"m": time.Minute, "h": time.Hour, "d": 24 * time.Hour}[m[2]]
|
||||
if n > int(maxWindow/unit) {
|
||||
return maxWindow, nil
|
||||
}
|
||||
return max(time.Duration(n)*unit, time.Minute), nil
|
||||
}
|
||||
|
||||
// SnowflakeFromTime returns the smallest Discord snowflake for t, usable as
|
||||
// an "after" cursor to mean "messages posted after this moment".
|
||||
func SnowflakeFromTime(t time.Time) string {
|
||||
ms := max(t.UnixMilli()-discordEpochMs, 0)
|
||||
return strconv.FormatInt(ms<<22, 10)
|
||||
}
|
||||
|
||||
// CompareIDs compares two snowflakes numerically (-1, 0, 1).
|
||||
func CompareIDs(a, b string) int {
|
||||
ai, aerr := strconv.ParseUint(a, 10, 64)
|
||||
bi, berr := strconv.ParseUint(b, 10, 64)
|
||||
if aerr != nil || berr != nil {
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
switch {
|
||||
case ai < bi:
|
||||
return -1
|
||||
case ai > bi:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in new issue
Block a user