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,68 @@
|
||||
package tldrrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewRepo(db *sql.DB) *Repo {
|
||||
return &Repo{db: db}
|
||||
}
|
||||
|
||||
// GetCursors returns channel_id -> last_message_id for one user in one guild.
|
||||
func (r *Repo) GetCursors(ctx context.Context, userID, guildID int64) (map[int64]int64, error) {
|
||||
const q = `
|
||||
SELECT channel_id, last_message_id
|
||||
FROM tldr_cursors
|
||||
WHERE user_id = $1 AND guild_id = $2
|
||||
`
|
||||
rows, err := r.db.QueryContext(ctx, q, userID, guildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[int64]int64)
|
||||
for rows.Next() {
|
||||
var channelID, messageID int64
|
||||
if err := rows.Scan(&channelID, &messageID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[channelID] = messageID
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// SetCursors upserts the given channel cursors in one transaction. GREATEST
|
||||
// means a cursor only ever moves forward, so two overlapping runs can't drag
|
||||
// it back and re-summarize messages.
|
||||
func (r *Repo) SetCursors(ctx context.Context, userID, guildID int64, cursors map[int64]int64) error {
|
||||
if len(cursors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
const q = `
|
||||
INSERT INTO tldr_cursors (user_id, channel_id, guild_id, last_message_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, channel_id)
|
||||
DO UPDATE SET
|
||||
last_message_id = GREATEST(tldr_cursors.last_message_id, EXCLUDED.last_message_id),
|
||||
updated_at = NOW()
|
||||
`
|
||||
for channelID, messageID := range cursors {
|
||||
if _, err := tx.ExecContext(ctx, q, userID, channelID, guildID, messageID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"velox-bot/internal/db/services/projects"
|
||||
"velox-bot/internal/db/services/rps"
|
||||
"velox-bot/internal/db/services/schedule"
|
||||
"velox-bot/internal/db/services/tldr"
|
||||
"velox-bot/internal/db/services/twitch"
|
||||
"velox-bot/internal/db/services/usersettings"
|
||||
"velox-bot/internal/db/services/welcome"
|
||||
@@ -28,11 +29,12 @@ type Services struct {
|
||||
DefaultRole *defaultrole.Service
|
||||
LogSettings *logsettings.Service
|
||||
MusicSettings *musicsettings.Service
|
||||
TLDR *tldr.Service
|
||||
}
|
||||
|
||||
var Global *Services
|
||||
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service, logSettingsSvc *logsettings.Service, musicSvc *musicsettings.Service) *Services {
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service, logSettingsSvc *logsettings.Service, musicSvc *musicsettings.Service, tldrSvc *tldr.Service) *Services {
|
||||
s := &Services{
|
||||
Level: level,
|
||||
LevelSettings: levelSettings,
|
||||
@@ -46,6 +48,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
|
||||
DefaultRole: defaultRoleSvc,
|
||||
LogSettings: logSettingsSvc,
|
||||
MusicSettings: musicSvc,
|
||||
TLDR: tldrSvc,
|
||||
}
|
||||
Global = s
|
||||
return s
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package tldr
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"velox-bot/internal/db/repos/tldrrepo"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *tldrrepo.Repo
|
||||
}
|
||||
|
||||
func New(repo *tldrrepo.Repo) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) GetCursors(ctx context.Context, userID, guildID int64) (map[int64]int64, error) {
|
||||
return s.repo.GetCursors(ctx, userID, guildID)
|
||||
}
|
||||
|
||||
func (s *Service) SetCursors(ctx context.Context, userID, guildID int64, cursors map[int64]int64) error {
|
||||
return s.repo.SetCursors(ctx, userID, guildID, cursors)
|
||||
}
|
||||
Reference in new issue
Block a user