/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]>
69 lines
1.6 KiB
Go
69 lines
1.6 KiB
Go
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()
|
|
}
|