mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Expand sentiment to 10 categories, add DB maintenance job, update README
- Expand LLM sentiment classification from 3 to 10 categories: positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive — with emoji reactions for each - Add daily DB maintenance job at 03:00 UTC to purge stale caches, expired rate limits, old logs, and run SQLite optimize - Add migrate.sql for upgrading existing databases - Update README: add !sentiment command, maintenance job, rate limit config, in-memory message buffer docs, SDK migration history, remove references to previous private repo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@@ -110,6 +111,74 @@ func CacheSet(key, data string) {
|
||||
}
|
||||
}
|
||||
|
||||
// RunMaintenance purges stale data from cache tables, old rate limits,
|
||||
// expired logs, and runs SQLite optimization. Intended to run daily.
|
||||
func RunMaintenance() {
|
||||
d := Get()
|
||||
now := time.Now().UTC()
|
||||
today := now.Format("2006-01-02")
|
||||
cutoff7d := now.AddDate(0, 0, -7).Unix()
|
||||
cutoff30d := now.AddDate(0, 0, -30).Unix()
|
||||
date30d := now.AddDate(0, 0, -30).Format("2006-01-02")
|
||||
date90d := now.AddDate(0, 0, -90).Format("2006-01-02")
|
||||
|
||||
queries := []struct {
|
||||
label string
|
||||
sql string
|
||||
args []interface{}
|
||||
}{
|
||||
// Cache tables — purge entries older than their effective TTL
|
||||
{"api_cache", `DELETE FROM api_cache WHERE cached_at < ?`, []interface{}{cutoff7d}},
|
||||
{"releases_cache", `DELETE FROM releases_cache WHERE cached_at < ?`, []interface{}{cutoff7d}},
|
||||
{"hltb_cache", `DELETE FROM hltb_cache WHERE cached_at < ?`, []interface{}{cutoff7d}},
|
||||
{"stocks_cache", `DELETE FROM stocks_cache WHERE cached_at < ?`, []interface{}{cutoff7d}},
|
||||
{"concerts_cache", `DELETE FROM concerts_cache WHERE cached_at < ?`, []interface{}{cutoff7d}},
|
||||
{"anime_cache", `DELETE FROM anime_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
|
||||
{"movie_cache", `DELETE FROM movie_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
|
||||
{"retro_cache", `DELETE FROM retro_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
|
||||
{"urban_cache", `DELETE FROM urban_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
|
||||
{"url_cache", `DELETE FROM url_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
|
||||
|
||||
// Rate limits — purge entries older than today
|
||||
{"rate_limits", `DELETE FROM rate_limits WHERE date < ?`, []interface{}{today}},
|
||||
|
||||
// Daily prefetch log — keep 30 days
|
||||
{"daily_prefetch", `DELETE FROM daily_prefetch WHERE date < ?`, []interface{}{date30d}},
|
||||
|
||||
// Holiday and WOTD logs — keep 90 days
|
||||
{"holidays_log", `DELETE FROM holidays_log WHERE date < ?`, []interface{}{date90d}},
|
||||
{"wotd_log", `DELETE FROM wotd_log WHERE date < ?`, []interface{}{date90d}},
|
||||
{"wotd_usage", `DELETE FROM wotd_usage WHERE date < ?`, []interface{}{date90d}},
|
||||
|
||||
// LLM classifications — keep 30 days
|
||||
{"llm_classifications", `DELETE FROM llm_classifications WHERE timestamp < ?`, []interface{}{cutoff30d}},
|
||||
|
||||
// Daily activity older than 1 year
|
||||
{"daily_activity", `DELETE FROM daily_activity WHERE date < ?`, []interface{}{now.AddDate(-1, 0, 0).Format("2006-01-02")}},
|
||||
}
|
||||
|
||||
totalDeleted := int64(0)
|
||||
for _, q := range queries {
|
||||
res, err := d.Exec(q.sql, q.args...)
|
||||
if err != nil {
|
||||
slog.Error("maintenance: "+q.label, "err", err)
|
||||
continue
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n > 0 {
|
||||
slog.Info("maintenance: purged", "table", q.label, "rows", n)
|
||||
totalDeleted += n
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite optimization
|
||||
if _, err := d.Exec(`PRAGMA optimize`); err != nil {
|
||||
slog.Error("maintenance: pragma optimize", "err", err)
|
||||
}
|
||||
|
||||
slog.Info("maintenance: complete", "total_purged", totalDeleted)
|
||||
}
|
||||
|
||||
const schema = `
|
||||
-- Users & XP
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -508,6 +577,13 @@ CREATE TABLE IF NOT EXISTS sentiment_stats (
|
||||
positive INTEGER DEFAULT 0,
|
||||
negative INTEGER DEFAULT 0,
|
||||
neutral INTEGER DEFAULT 0,
|
||||
excited INTEGER DEFAULT 0,
|
||||
sarcastic INTEGER DEFAULT 0,
|
||||
frustrated INTEGER DEFAULT 0,
|
||||
curious INTEGER DEFAULT 0,
|
||||
grateful INTEGER DEFAULT 0,
|
||||
humorous INTEGER DEFAULT 0,
|
||||
supportive INTEGER DEFAULT 0,
|
||||
total_score REAL DEFAULT 0
|
||||
);
|
||||
|
||||
|
||||
@@ -120,11 +120,13 @@ func (p *HowAmIPlugin) gatherProfile(userID id.UserID) string {
|
||||
sb.WriteString(fmt.Sprintf("Achievements unlocked: %d\n", achievementCount))
|
||||
|
||||
// Sentiment stats
|
||||
var positive, negative, neutral int
|
||||
var positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive int
|
||||
if err := d.QueryRow(
|
||||
`SELECT positive, negative, neutral FROM sentiment_stats WHERE user_id = ?`, uid,
|
||||
).Scan(&positive, &negative, &neutral); err == nil {
|
||||
sb.WriteString(fmt.Sprintf("Sentiment: %d positive, %d negative, %d neutral\n", positive, negative, neutral))
|
||||
`SELECT positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive
|
||||
FROM sentiment_stats WHERE user_id = ?`, uid,
|
||||
).Scan(&positive, &negative, &neutral, &excited, &sarcastic, &frustrated, &curious, &grateful, &humorous, &supportive); err == nil {
|
||||
sb.WriteString(fmt.Sprintf("Sentiment: %d positive, %d excited, %d supportive, %d grateful, %d humorous, %d curious, %d neutral, %d sarcastic, %d frustrated, %d negative\n",
|
||||
positive, excited, supportive, grateful, humorous, curious, neutral, sarcastic, frustrated, negative))
|
||||
}
|
||||
|
||||
// Profanity count
|
||||
|
||||
@@ -287,26 +287,22 @@ func (p *LLMPassivePlugin) classifyAndProcess(item queueItem) error {
|
||||
}
|
||||
|
||||
// Aggregate sentiment stats
|
||||
switch result.Sentiment {
|
||||
case "positive":
|
||||
_, _ = d.Exec(
|
||||
`INSERT INTO sentiment_stats (user_id, positive, total_score) VALUES (?, 1, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET positive = positive + 1, total_score = total_score + ?`,
|
||||
string(item.UserID), result.SentimentScore, result.SentimentScore,
|
||||
)
|
||||
case "negative":
|
||||
_, _ = d.Exec(
|
||||
`INSERT INTO sentiment_stats (user_id, negative, total_score) VALUES (?, 1, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET negative = negative + 1, total_score = total_score + ?`,
|
||||
string(item.UserID), result.SentimentScore, result.SentimentScore,
|
||||
)
|
||||
default:
|
||||
_, _ = d.Exec(
|
||||
`INSERT INTO sentiment_stats (user_id, neutral, total_score) VALUES (?, 1, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET neutral = neutral + 1, total_score = total_score + ?`,
|
||||
string(item.UserID), result.SentimentScore, result.SentimentScore,
|
||||
)
|
||||
sentimentCol := "neutral"
|
||||
validSentiments := map[string]bool{
|
||||
"positive": true, "negative": true, "neutral": true,
|
||||
"excited": true, "sarcastic": true, "frustrated": true,
|
||||
"curious": true, "grateful": true, "humorous": true, "supportive": true,
|
||||
}
|
||||
if validSentiments[result.Sentiment] {
|
||||
sentimentCol = result.Sentiment
|
||||
}
|
||||
_, _ = d.Exec(
|
||||
fmt.Sprintf(
|
||||
`INSERT INTO sentiment_stats (user_id, %s, total_score) VALUES (?, 1, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET %s = %s + 1, total_score = total_score + ?`,
|
||||
sentimentCol, sentimentCol, sentimentCol),
|
||||
string(item.UserID), result.SentimentScore, result.SentimentScore,
|
||||
)
|
||||
|
||||
// Track profanity with severity
|
||||
if result.Profanity {
|
||||
@@ -360,12 +356,24 @@ func (p *LLMPassivePlugin) classifyAndProcess(item queueItem) error {
|
||||
)
|
||||
}
|
||||
|
||||
// React with emojis based on classification
|
||||
if result.Sentiment == "positive" && result.SentimentScore > 0.7 {
|
||||
_ = p.SendReact(item.RoomID, item.EventID, "\U0001f44d") // thumbsup
|
||||
// React with emojis based on sentiment
|
||||
sentimentEmojis := map[string]string{
|
||||
"positive": "\U0001f44d", // 👍
|
||||
"negative": "\U0001f44e", // 👎
|
||||
"excited": "\U0001f525", // 🔥
|
||||
"sarcastic": "\U0001f928", // 🤨
|
||||
"frustrated": "\U0001f62e\u200d\U0001f4a8", // 😮💨
|
||||
"curious": "\U0001f9d0", // 🧐
|
||||
"grateful": "\U0001f49c", // 💜
|
||||
"humorous": "\U0001f602", // 😂
|
||||
"supportive": "\U0001f917", // 🤗
|
||||
}
|
||||
if result.Sentiment == "negative" && result.SentimentScore < -0.7 {
|
||||
_ = p.SendReact(item.RoomID, item.EventID, "\U0001f44e") // thumbsdown
|
||||
if emoji, ok := sentimentEmojis[result.Sentiment]; ok && result.Sentiment != "neutral" {
|
||||
// Only react to strong sentiments (|score| > 0.5)
|
||||
score := result.SentimentScore
|
||||
if score > 0.5 || score < -0.5 {
|
||||
_ = p.SendReact(item.RoomID, item.EventID, emoji)
|
||||
}
|
||||
}
|
||||
if result.Profanity {
|
||||
switch result.ProfanitySeverity {
|
||||
@@ -413,7 +421,7 @@ func (p *LLMPassivePlugin) callOllama(messageText string) (*classificationResult
|
||||
|
||||
JSON schema:
|
||||
{
|
||||
"sentiment": "positive" | "negative" | "neutral",
|
||||
"sentiment": "positive" | "negative" | "neutral" | "excited" | "sarcastic" | "frustrated" | "curious" | "grateful" | "humorous" | "supportive",
|
||||
"sentiment_score": number between -1.0 and 1.0,
|
||||
"topics": ["topic1", "topic2"],
|
||||
"profanity": true | false,
|
||||
@@ -644,18 +652,22 @@ func (p *LLMPassivePlugin) handleSentiment(ctx MessageContext) error {
|
||||
}
|
||||
|
||||
d := db.Get()
|
||||
var positive, negative, neutral int
|
||||
var positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive int
|
||||
var totalScore float64
|
||||
err := d.QueryRow(
|
||||
`SELECT COALESCE(positive, 0), COALESCE(negative, 0), COALESCE(neutral, 0), COALESCE(total_score, 0)
|
||||
`SELECT COALESCE(positive, 0), COALESCE(negative, 0), COALESCE(neutral, 0),
|
||||
COALESCE(excited, 0), COALESCE(sarcastic, 0), COALESCE(frustrated, 0),
|
||||
COALESCE(curious, 0), COALESCE(grateful, 0), COALESCE(humorous, 0),
|
||||
COALESCE(supportive, 0), COALESCE(total_score, 0)
|
||||
FROM sentiment_stats WHERE user_id = ?`,
|
||||
string(target),
|
||||
).Scan(&positive, &negative, &neutral, &totalScore)
|
||||
).Scan(&positive, &negative, &neutral, &excited, &sarcastic, &frustrated,
|
||||
&curious, &grateful, &humorous, &supportive, &totalScore)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No sentiment data for %s yet.", string(target)))
|
||||
}
|
||||
|
||||
total := positive + negative + neutral
|
||||
total := positive + negative + neutral + excited + sarcastic + frustrated + curious + grateful + humorous + supportive
|
||||
avgScore := 0.0
|
||||
if total > 0 {
|
||||
avgScore = totalScore / float64(total)
|
||||
@@ -672,7 +684,33 @@ func (p *LLMPassivePlugin) handleSentiment(ctx MessageContext) error {
|
||||
mood = "leaning negative"
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Sentiment for %s:\n😊 Positive: %s 😐 Neutral: %s 😠 Negative: %s\nAverage mood: %.2f (%s)",
|
||||
string(target), formatNumber(positive), formatNumber(neutral), formatNumber(negative), avgScore, mood)
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||
// Build sentiment breakdown, only showing non-zero counts
|
||||
type sentEntry struct {
|
||||
emoji string
|
||||
label string
|
||||
count int
|
||||
}
|
||||
entries := []sentEntry{
|
||||
{"👍", "Positive", positive},
|
||||
{"🔥", "Excited", excited},
|
||||
{"🤗", "Supportive", supportive},
|
||||
{"💜", "Grateful", grateful},
|
||||
{"😂", "Humorous", humorous},
|
||||
{"🧐", "Curious", curious},
|
||||
{"😐", "Neutral", neutral},
|
||||
{"🤨", "Sarcastic", sarcastic},
|
||||
{"😮\u200d💨", "Frustrated", frustrated},
|
||||
{"👎", "Negative", negative},
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Sentiment for %s:\n", string(target)))
|
||||
for _, e := range entries {
|
||||
if e.count > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" %s %s: %s\n", e.emoji, e.label, formatNumber(e.count)))
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Average mood: %.2f (%s)", avgScore, mood))
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user