Initial commit: Discord TLDR - automated server summaries
This commit is contained in:
commit
2378565a79
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Environment
|
||||
.env
|
||||
*.env.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.Python
|
||||
venv/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
88
README.md
Normal file
88
README.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Discord TLDR
|
||||
|
||||
Automatically summarize Discord server conversations and post digests to a dedicated channel. Never miss important discussions again.
|
||||
|
||||
## Two Ways to Use
|
||||
|
||||
### 🤖 Option 1: Clawdbot Skill (Recommended if you use Clawdbot)
|
||||
|
||||
Zero code required. Just configure cron jobs and let Clawdbot handle the rest.
|
||||
|
||||
**Pros:**
|
||||
- No separate bot to host
|
||||
- Uses your existing Clawdbot setup
|
||||
- AI summarization built-in
|
||||
- Easy to customize prompts
|
||||
|
||||
**Setup:** See [`skill/SKILL.md`](skill/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
### 🐍 Option 2: Standalone Bot
|
||||
|
||||
A self-contained Python Discord bot. Deploy anywhere.
|
||||
|
||||
**Pros:**
|
||||
- Works without Clawdbot
|
||||
- Full control over deployment
|
||||
- Docker-ready
|
||||
- Manual trigger commands
|
||||
|
||||
**Setup:** See [`standalone/README.md`](standalone/README.md)
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Collect** - Reads messages from all accessible channels since last summary
|
||||
2. **Summarize** - Uses Claude/GPT to create a concise digest
|
||||
3. **Post** - Sends the summary to your #tldr channel
|
||||
4. **Repeat** - Runs on schedule (default: 6 AM, 1 PM, 10 PM)
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
📋 **TLDR Summary** (Jan 25, 2026 - 1:00 PM)
|
||||
|
||||
Busy morning! Major progress on the API, some hiring discussions,
|
||||
and the eternal tabs-vs-spaces debate resurfaces.
|
||||
|
||||
**#general**
|
||||
• Welcomed @newbie to the team
|
||||
• Q1 roadmap discussion - focusing on mobile first
|
||||
• Friday team lunch @ 12:30
|
||||
|
||||
**#engineering**
|
||||
• Shipped v2.1.0 to staging
|
||||
• Found and fixed the auth token bug
|
||||
• Code review backlog cleared
|
||||
|
||||
**#hiring**
|
||||
• 3 new senior eng candidates in pipeline
|
||||
• Technical interview scheduled for Thursday
|
||||
|
||||
**Action Items:**
|
||||
- [ ] @alice: Finalize API docs by Wednesday
|
||||
- [ ] @bob: Set up staging environment monitoring
|
||||
- [ ] Everyone: Submit Q1 goals by EOW
|
||||
```
|
||||
|
||||
## Why?
|
||||
|
||||
- **Async-friendly**: Not everyone can keep up with Discord in real-time
|
||||
- **Context preservation**: Important decisions don't get buried
|
||||
- **Onboarding**: New members can quickly catch up on team dynamics
|
||||
- **Documentation**: Creates a lightweight log of team activity
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs welcome! Ideas for improvement:
|
||||
- Slack/Teams support
|
||||
- Weekly digest mode
|
||||
- Thread summarization
|
||||
- Sentiment analysis
|
||||
- Custom summary templates
|
||||
|
||||
## License
|
||||
|
||||
MIT - do whatever you want with it
|
||||
111
skill/SKILL.md
Normal file
111
skill/SKILL.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Discord TLDR Skill
|
||||
|
||||
Automatically summarize Discord server conversations and post digests to a dedicated #tldr channel.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill configures scheduled summaries of your Discord server's activity. Perfect for:
|
||||
- Teams who want async-friendly catch-up
|
||||
- Servers with multiple active channels
|
||||
- Anyone who doesn't want to scroll through hundreds of messages
|
||||
|
||||
## Requirements
|
||||
|
||||
- Clawdbot with Discord channel configured
|
||||
- Bot must have read access to channels you want summarized
|
||||
- A dedicated #tldr channel for posting summaries
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create your #tldr channel
|
||||
|
||||
Create a text channel in your Discord server (e.g., `#tldr` or `#daily-digest`). Note the channel ID.
|
||||
|
||||
### 2. Get your Guild ID
|
||||
|
||||
Your Discord server's guild ID. You can get this by enabling Developer Mode in Discord, then right-clicking your server name.
|
||||
|
||||
### 3. Add the cron jobs
|
||||
|
||||
Run these commands in Clawdbot or add to your config:
|
||||
|
||||
```bash
|
||||
# Morning summary (6 AM)
|
||||
clawdbot cron add \
|
||||
--name "tldr-morning" \
|
||||
--schedule "0 6 * * *" \
|
||||
--tz "America/New_York" \
|
||||
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||
--channel discord
|
||||
|
||||
# Afternoon summary (1 PM)
|
||||
clawdbot cron add \
|
||||
--name "tldr-afternoon" \
|
||||
--schedule "0 13 * * *" \
|
||||
--tz "America/New_York" \
|
||||
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||
--channel discord
|
||||
|
||||
# Night summary (10 PM)
|
||||
clawdbot cron add \
|
||||
--name "tldr-night" \
|
||||
--schedule "0 22 * * *" \
|
||||
--tz "America/New_York" \
|
||||
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||
--channel discord
|
||||
```
|
||||
|
||||
### 4. Customize (optional)
|
||||
|
||||
**Change frequency:** Modify the cron expressions:
|
||||
- `0 9 * * *` = 9 AM daily
|
||||
- `0 */6 * * *` = every 6 hours
|
||||
- `0 9 * * 1-5` = 9 AM weekdays only
|
||||
|
||||
**Change timezone:** Replace `America/New_York` with your timezone.
|
||||
|
||||
**Customize the prompt:** Edit the message to:
|
||||
- Exclude certain channels
|
||||
- Focus on specific topics
|
||||
- Change summary format (bullets, paragraphs, etc.)
|
||||
- Add action item extraction
|
||||
- Tag specific roles for important updates
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
📋 **TLDR Summary** (Jan 25, 2026 - Morning)
|
||||
|
||||
**#general**
|
||||
• Team discussed jacket-making skills and the legend of Nicholai's sibling
|
||||
• Buba wrote an epic poem about said jacket (instant classic)
|
||||
|
||||
**#dev**
|
||||
• Merged PR #42 for the new API endpoint
|
||||
• Blocked on auth token refresh issue - @jake investigating
|
||||
|
||||
**#random**
|
||||
• Heated debate about best pizza toppings (pineapple discourse continues)
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Jake: Fix token refresh bug
|
||||
- [ ] Team: Review Q1 roadmap by Friday
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Summaries not posting?**
|
||||
- Check that Clawdbot has message read permissions in all channels
|
||||
- Verify the tldr channel ID is correct
|
||||
- Run `clawdbot cron list` to confirm jobs are enabled
|
||||
|
||||
**Summaries missing channels?**
|
||||
- Bot needs explicit read access to private channels
|
||||
- Some channels may be excluded by Discord permissions
|
||||
|
||||
## License
|
||||
|
||||
MIT - do whatever you want with it
|
||||
52
skill/cron-config.json
Normal file
52
skill/cron-config.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"name": "tldr-morning",
|
||||
"schedule": {
|
||||
"kind": "cron",
|
||||
"expr": "0 6 * * *",
|
||||
"tz": "America/New_York"
|
||||
},
|
||||
"sessionTarget": "isolated",
|
||||
"wakeMode": "next-heartbeat",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||
"to": "channel:TLDR_CHANNEL_ID",
|
||||
"channel": "discord"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tldr-afternoon",
|
||||
"schedule": {
|
||||
"kind": "cron",
|
||||
"expr": "0 13 * * *",
|
||||
"tz": "America/New_York"
|
||||
},
|
||||
"sessionTarget": "isolated",
|
||||
"wakeMode": "next-heartbeat",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||
"to": "channel:TLDR_CHANNEL_ID",
|
||||
"channel": "discord"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tldr-night",
|
||||
"schedule": {
|
||||
"kind": "cron",
|
||||
"expr": "0 22 * * *",
|
||||
"tz": "America/New_York"
|
||||
},
|
||||
"sessionTarget": "isolated",
|
||||
"wakeMode": "next-heartbeat",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||
"to": "channel:TLDR_CHANNEL_ID",
|
||||
"channel": "discord"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
standalone/.env.example
Normal file
18
standalone/.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
# Discord Configuration
|
||||
DISCORD_TOKEN=your_discord_bot_token_here
|
||||
DISCORD_GUILD_ID=123456789012345678
|
||||
DISCORD_TLDR_CHANNEL_ID=123456789012345678
|
||||
|
||||
# LLM Provider (anthropic or openai)
|
||||
LLM_PROVIDER=anthropic
|
||||
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||
# OPENAI_API_KEY=sk-xxxxx
|
||||
|
||||
# Schedule (24h format, comma-separated)
|
||||
SUMMARY_HOURS=6,13,22
|
||||
|
||||
# How far back to look for messages (hours)
|
||||
LOOKBACK_HOURS=8
|
||||
|
||||
# Channels to exclude (comma-separated channel IDs)
|
||||
# EXCLUDE_CHANNELS=123456789,987654321
|
||||
10
standalone/Dockerfile
Normal file
10
standalone/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY discord_tldr.py .
|
||||
|
||||
CMD ["python", "discord_tldr.py"]
|
||||
126
standalone/README.md
Normal file
126
standalone/README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Discord TLDR Bot (Standalone)
|
||||
|
||||
A standalone Discord bot that automatically summarizes server conversations and posts digests to a dedicated channel.
|
||||
|
||||
## Features
|
||||
|
||||
- 📋 Automatic scheduled summaries (configurable times)
|
||||
- 🤖 AI-powered summarization (Claude or GPT)
|
||||
- 📊 Channel-by-channel breakdown
|
||||
- ⚡ Manual trigger via `!tldr` command
|
||||
- 🐳 Docker-ready for easy deployment
|
||||
- 🔒 Respects channel permissions
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Discord Bot
|
||||
|
||||
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Create a new application
|
||||
3. Go to Bot → Add Bot
|
||||
4. Enable these Privileged Intents:
|
||||
- Message Content Intent
|
||||
- Server Members Intent (optional)
|
||||
5. Copy the bot token
|
||||
|
||||
### 2. Invite the Bot
|
||||
|
||||
Generate an invite URL with these permissions:
|
||||
- Read Messages/View Channels
|
||||
- Send Messages
|
||||
- Read Message History
|
||||
|
||||
```
|
||||
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=68608&scope=bot
|
||||
```
|
||||
|
||||
### 3. Configure Environment
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your values
|
||||
```
|
||||
|
||||
### 4. Run
|
||||
|
||||
**Direct:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python discord_tldr.py
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker build -t discord-tldr .
|
||||
docker run --env-file .env discord-tldr
|
||||
```
|
||||
|
||||
**Docker Compose:**
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
tldr:
|
||||
build: .
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!tldr` | Generate summary now (uses default lookback) |
|
||||
| `!tldr 24` | Generate summary for last 24 hours |
|
||||
| `!tldr-status` | Check bot status and schedule |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DISCORD_TOKEN` | required | Bot token |
|
||||
| `DISCORD_GUILD_ID` | required | Server ID |
|
||||
| `DISCORD_TLDR_CHANNEL_ID` | required | Channel for summaries |
|
||||
| `LLM_PROVIDER` | `anthropic` | `anthropic` or `openai` |
|
||||
| `ANTHROPIC_API_KEY` | - | Claude API key |
|
||||
| `OPENAI_API_KEY` | - | GPT API key |
|
||||
| `SUMMARY_HOURS` | `6,13,22` | Hours to post (24h format) |
|
||||
| `LOOKBACK_HOURS` | `8` | Hours to look back |
|
||||
| `EXCLUDE_CHANNELS` | - | Channel IDs to skip |
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
📋 **TLDR Summary** (Jan 25, 2026 - 6:00 AM)
|
||||
|
||||
Quick catch-up: Active discussions about the new API launch and some
|
||||
spirited debate about code review practices.
|
||||
|
||||
**#general**
|
||||
• Team welcomed new member @sarah
|
||||
• Discussion about Q1 roadmap priorities
|
||||
• Decided to postpone the demo to next week
|
||||
|
||||
**#dev**
|
||||
• Merged the authentication refactor (PR #142)
|
||||
• Debugging session for the webhook timeout issue
|
||||
• @jake will investigate the memory leak
|
||||
|
||||
**#random**
|
||||
• Friday lunch plans: Thai food won the vote
|
||||
• Shared some quality memes about merge conflicts
|
||||
|
||||
**Action Items:**
|
||||
- @jake: Investigate memory leak by EOD Monday
|
||||
- @team: Review roadmap doc before Wednesday standup
|
||||
```
|
||||
|
||||
## Hosting Options
|
||||
|
||||
- **VPS**: Any cheap VPS (DigitalOcean, Linode, Vultr)
|
||||
- **Railway/Render**: Easy container deployment
|
||||
- **Home server**: Run in Docker with auto-restart
|
||||
- **Raspberry Pi**: Works great for small servers
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
299
standalone/discord_tldr.py
Normal file
299
standalone/discord_tldr.py
Normal file
@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Discord TLDR Bot
|
||||
Summarizes Discord server conversations and posts to a #tldr channel.
|
||||
|
||||
Standalone version - no Clawdbot required.
|
||||
Uses Discord.py for Discord API and Anthropic/OpenAI for summarization.
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
# Optional: use anthropic or openai for summarization
|
||||
try:
|
||||
import anthropic
|
||||
HAS_ANTHROPIC = True
|
||||
except ImportError:
|
||||
HAS_ANTHROPIC = False
|
||||
|
||||
try:
|
||||
import openai
|
||||
HAS_OPENAI = True
|
||||
except ImportError:
|
||||
HAS_OPENAI = False
|
||||
|
||||
# Configuration (override with environment variables)
|
||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID", "0"))
|
||||
TLDR_CHANNEL_ID = int(os.getenv("DISCORD_TLDR_CHANNEL_ID", "0"))
|
||||
|
||||
# LLM Configuration
|
||||
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "anthropic") # "anthropic" or "openai"
|
||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
# Summary schedule (hours in 24h format, comma-separated)
|
||||
SUMMARY_HOURS = [int(h) for h in os.getenv("SUMMARY_HOURS", "6,13,22").split(",")]
|
||||
|
||||
# Channels to exclude (comma-separated channel IDs)
|
||||
EXCLUDE_CHANNELS = [int(c) for c in os.getenv("EXCLUDE_CHANNELS", "").split(",") if c]
|
||||
|
||||
# How far back to look for messages (hours)
|
||||
LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS", "8"))
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("discord-tldr")
|
||||
|
||||
# Bot setup
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.guilds = True
|
||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||
|
||||
|
||||
class MessageCollector:
|
||||
"""Collects messages from Discord channels."""
|
||||
|
||||
def __init__(self, guild: discord.Guild, exclude_channels: list[int]):
|
||||
self.guild = guild
|
||||
self.exclude_channels = exclude_channels
|
||||
|
||||
async def collect_messages(self, since: datetime) -> dict[str, list[dict]]:
|
||||
"""Collect messages from all text channels since the given time."""
|
||||
messages_by_channel = {}
|
||||
|
||||
for channel in self.guild.text_channels:
|
||||
if channel.id in self.exclude_channels:
|
||||
continue
|
||||
if channel.id == TLDR_CHANNEL_ID:
|
||||
continue # Don't summarize the tldr channel itself
|
||||
|
||||
try:
|
||||
messages = []
|
||||
async for msg in channel.history(after=since, limit=500):
|
||||
if msg.author.bot:
|
||||
continue
|
||||
messages.append({
|
||||
"author": msg.author.display_name,
|
||||
"content": msg.content,
|
||||
"timestamp": msg.created_at.isoformat(),
|
||||
"attachments": len(msg.attachments),
|
||||
"reactions": sum(r.count for r in msg.reactions) if msg.reactions else 0
|
||||
})
|
||||
|
||||
if messages:
|
||||
messages_by_channel[channel.name] = list(reversed(messages))
|
||||
logger.info(f"Collected {len(messages)} messages from #{channel.name}")
|
||||
|
||||
except discord.Forbidden:
|
||||
logger.warning(f"No access to #{channel.name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting from #{channel.name}: {e}")
|
||||
|
||||
return messages_by_channel
|
||||
|
||||
|
||||
class Summarizer:
|
||||
"""Summarizes messages using an LLM."""
|
||||
|
||||
def __init__(self, provider: str = "anthropic"):
|
||||
self.provider = provider
|
||||
|
||||
if provider == "anthropic" and HAS_ANTHROPIC:
|
||||
self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
|
||||
elif provider == "openai" and HAS_OPENAI:
|
||||
self.client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
||||
else:
|
||||
self.client = None
|
||||
logger.warning(f"LLM provider '{provider}' not available, using basic summarization")
|
||||
|
||||
def _format_messages_for_prompt(self, messages_by_channel: dict) -> str:
|
||||
"""Format collected messages into a prompt-friendly string."""
|
||||
parts = []
|
||||
for channel, messages in messages_by_channel.items():
|
||||
parts.append(f"\n## #{channel}")
|
||||
for msg in messages:
|
||||
parts.append(f"[{msg['author']}]: {msg['content']}")
|
||||
return "\n".join(parts)
|
||||
|
||||
async def summarize(self, messages_by_channel: dict) -> str:
|
||||
"""Generate a summary of the messages."""
|
||||
if not messages_by_channel:
|
||||
return "No new messages to summarize."
|
||||
|
||||
formatted = self._format_messages_for_prompt(messages_by_channel)
|
||||
|
||||
prompt = f"""Summarize the following Discord server conversations into a concise TLDR digest.
|
||||
|
||||
Format your response as:
|
||||
1. A brief overview (1-2 sentences)
|
||||
2. Bullet points organized by channel, highlighting:
|
||||
- Key discussions and decisions
|
||||
- Important ideas or proposals
|
||||
- Action items or next steps
|
||||
- Notable moments or highlights
|
||||
|
||||
Keep it scannable and useful for someone catching up.
|
||||
|
||||
---
|
||||
MESSAGES:
|
||||
{formatted}
|
||||
---
|
||||
|
||||
Generate the TLDR summary:"""
|
||||
|
||||
if self.provider == "anthropic" and self.client:
|
||||
response = self.client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=1500,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
return response.content[0].text
|
||||
|
||||
elif self.provider == "openai" and self.client:
|
||||
response = self.client.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
max_tokens=1500,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
else:
|
||||
# Basic fallback: just list message counts
|
||||
lines = ["📋 **Activity Summary**\n"]
|
||||
for channel, messages in messages_by_channel.items():
|
||||
lines.append(f"• **#{channel}**: {len(messages)} messages")
|
||||
lines.append("\n*Install anthropic or openai package for AI summaries*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class TLDRBot:
|
||||
"""Main bot class that orchestrates collection and summarization."""
|
||||
|
||||
def __init__(self):
|
||||
self.collector = None
|
||||
self.summarizer = Summarizer(LLM_PROVIDER)
|
||||
self.last_summary_time = None
|
||||
|
||||
async def generate_and_post_summary(self):
|
||||
"""Generate a summary and post it to the TLDR channel."""
|
||||
guild = bot.get_guild(GUILD_ID)
|
||||
if not guild:
|
||||
logger.error(f"Guild {GUILD_ID} not found")
|
||||
return
|
||||
|
||||
tldr_channel = guild.get_channel(TLDR_CHANNEL_ID)
|
||||
if not tldr_channel:
|
||||
logger.error(f"TLDR channel {TLDR_CHANNEL_ID} not found")
|
||||
return
|
||||
|
||||
# Determine lookback time
|
||||
if self.last_summary_time:
|
||||
since = self.last_summary_time
|
||||
else:
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS)
|
||||
|
||||
# Collect messages
|
||||
self.collector = MessageCollector(guild, EXCLUDE_CHANNELS)
|
||||
messages = await self.collector.collect_messages(since)
|
||||
|
||||
if not messages:
|
||||
logger.info("No new messages to summarize")
|
||||
return
|
||||
|
||||
# Generate summary
|
||||
summary = await self.summarizer.summarize(messages)
|
||||
|
||||
# Format and post
|
||||
now = datetime.now()
|
||||
header = f"📋 **TLDR Summary** ({now.strftime('%b %d, %Y - %I:%M %p')})\n\n"
|
||||
|
||||
# Discord has a 2000 char limit
|
||||
full_message = header + summary
|
||||
if len(full_message) > 2000:
|
||||
# Split into multiple messages
|
||||
chunks = [full_message[i:i+1990] for i in range(0, len(full_message), 1990)]
|
||||
for chunk in chunks:
|
||||
await tldr_channel.send(chunk)
|
||||
else:
|
||||
await tldr_channel.send(full_message)
|
||||
|
||||
self.last_summary_time = datetime.now(timezone.utc)
|
||||
logger.info("Posted TLDR summary")
|
||||
|
||||
|
||||
tldr_bot = TLDRBot()
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
logger.info(f"Logged in as {bot.user}")
|
||||
check_summary_time.start()
|
||||
|
||||
|
||||
@tasks.loop(minutes=1)
|
||||
async def check_summary_time():
|
||||
"""Check if it's time to post a summary."""
|
||||
now = datetime.now()
|
||||
if now.hour in SUMMARY_HOURS and now.minute == 0:
|
||||
await tldr_bot.generate_and_post_summary()
|
||||
|
||||
|
||||
@bot.command(name="tldr")
|
||||
async def manual_tldr(ctx, hours: int = None):
|
||||
"""Manually trigger a TLDR summary. Usage: !tldr [hours]"""
|
||||
if hours:
|
||||
tldr_bot.last_summary_time = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
await ctx.send("Generating TLDR summary...")
|
||||
await tldr_bot.generate_and_post_summary()
|
||||
|
||||
|
||||
@bot.command(name="tldr-status")
|
||||
async def tldr_status(ctx):
|
||||
"""Check TLDR bot status."""
|
||||
last = tldr_bot.last_summary_time
|
||||
last_str = last.strftime('%Y-%m-%d %H:%M UTC') if last else "Never"
|
||||
schedule = ", ".join(f"{h}:00" for h in SUMMARY_HOURS)
|
||||
|
||||
await ctx.send(
|
||||
f"**TLDR Bot Status**\n"
|
||||
f"• Last summary: {last_str}\n"
|
||||
f"• Schedule: {schedule}\n"
|
||||
f"• Lookback: {LOOKBACK_HOURS} hours\n"
|
||||
f"• LLM Provider: {LLM_PROVIDER}\n"
|
||||
f"• Excluded channels: {len(EXCLUDE_CHANNELS)}"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if not DISCORD_TOKEN:
|
||||
print("Error: DISCORD_TOKEN environment variable not set")
|
||||
print("\nRequired environment variables:")
|
||||
print(" DISCORD_TOKEN - Your Discord bot token")
|
||||
print(" DISCORD_GUILD_ID - Your server's guild ID")
|
||||
print(" DISCORD_TLDR_CHANNEL_ID - Channel ID for posting summaries")
|
||||
print("\nOptional:")
|
||||
print(" LLM_PROVIDER - 'anthropic' or 'openai' (default: anthropic)")
|
||||
print(" ANTHROPIC_API_KEY - Anthropic API key (if using Claude)")
|
||||
print(" OPENAI_API_KEY - OpenAI API key (if using GPT)")
|
||||
print(" SUMMARY_HOURS - Comma-separated hours (default: 6,13,22)")
|
||||
print(" LOOKBACK_HOURS - Hours to look back (default: 8)")
|
||||
print(" EXCLUDE_CHANNELS - Comma-separated channel IDs to skip")
|
||||
return
|
||||
|
||||
if not GUILD_ID or not TLDR_CHANNEL_ID:
|
||||
print("Error: DISCORD_GUILD_ID and DISCORD_TLDR_CHANNEL_ID must be set")
|
||||
return
|
||||
|
||||
bot.run(DISCORD_TOKEN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
standalone/requirements.txt
Normal file
9
standalone/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
||||
# Core
|
||||
discord.py>=2.3.0
|
||||
|
||||
# LLM providers (install at least one)
|
||||
anthropic>=0.18.0
|
||||
openai>=1.12.0
|
||||
|
||||
# Optional: for better async handling
|
||||
aiohttp>=3.9.0
|
||||
Loading…
x
Reference in New Issue
Block a user