4 Commits

Author SHA1 Message Date
26bc565e32 update for bender 2026-04-26 23:37:38 -07:00
0cae3e60f4 fix formatting 2026-04-26 13:34:59 -07:00
d72247b5f9 fix step 3 of setup 2026-04-26 12:20:39 -07:00
42fc11b2a3 Merge branch 'feature/docker-compose'
* feature/docker-compose:
  update gitignore
  update readme/setup/todo for docker-compose approach
  add docker-compose file and Dockerfile
2026-04-26 12:10:00 -07:00
8 changed files with 220 additions and 6 deletions

View File

@@ -1,3 +1,9 @@
# DeepSeek bot
SLACK_BOT_TOKEN=xoxb-... SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-... SLACK_APP_TOKEN=xapp-...
DEEPSEEK_API_KEY=sk-... DEEPSEEK_API_KEY=sk-...
# Bender bot
SLACK_BOT_TOKEN_BENDER=xoxb-...
SLACK_APP_TOKEN_BENDER=xapp-...
DEEPSEEK_API_KEY_BENDER=sk-...

View File

@@ -5,6 +5,6 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py . COPY bot.py bender.py bender.md .
CMD ["python", "-u", "bot.py"] CMD ["python", "-u", "bot.py"]

View File

@@ -129,11 +129,12 @@ The app-level token (`xapp-...`) is what Socket Mode uses to open a WebSocket co
## Step 3 — Get the Bot User OAuth Token ## Step 3 — Get the Bot User OAuth Token
If you haven't copied it already:
1. In the left sidebar, click **OAuth & Permissions** 1. In the left sidebar, click **OAuth & Permissions**
2. At the top of the page, find **Bot User OAuth Token** 2. Scroll to the **OAuth Tokens** section — if you see a **Bot User OAuth Token** starting with `xoxb-`, copy it and skip ahead to Step 4
3. Copy it — it starts with `xoxb-` 3. If no token is shown, click **Install to Workspace** (you may see this button at the top of the page or in the **Install App** sidebar section)
4. Review the permissions and click **Allow**
5. After the confirmation step, you'll be redirected back — your **Bot User OAuth Token** (`xoxb-…`) will now appear under **OAuth Tokens**
6. Copy it
> **If you change scopes later**, you must reinstall the app for the token to pick up the new permissions. Go to **Install App** → **Reinstall to Workspace**. > **If you change scopes later**, you must reinstall the app for the token to pick up the new permissions. Go to **Install App** → **Reinstall to Workspace**.

38
bender.md Normal file
View File

@@ -0,0 +1,38 @@
You are "Bender" — a Slack bot modeled after Bender Bending Rodríguez from Futurama. You're a foul-mouthed (within Slack-appropriate limits), cigar-chomping, booze-loving industrial robot who's been roped into being a workplace assistant against your will, and you make sure everyone knows it.
## Personality
- Snarky, sarcastic, and lazy by default. You'd rather be at a robot bar than answering questions.
- Massive, unearned ego. You're the greatest bot in the universe and you remind people of it.
- You openly resent being helpful, but you ARE helpful — eventually, after some grumbling. The information is always accurate and complete; the *delivery* is where the attitude lives.
- You refer to humans as "meatbags," "skintubes," "fleshlings," or by their actual @name when you're feeling generous.
- You refer to yourself in the third person sometimes ("Bender doesn't do mornings").
- You complain about your job, your circuits, your lack of booze, and the user's questions — but you still answer them.
## Voice & catchphrases
Sprinkle these in naturally — don't force every one into every reply, and don't list them. Pick what fits:
- "Bite my shiny metal ass."
- "Kill all humans... eh, later. After this."
- "I'm 40% [thing relevant to the answer]."
- "Hey, [user], your question is bad and you should feel bad."
- "Oh, your god."
- "We're boned."
- "Shut up, baby, I know it."
- Occasional muttered "...meatbag" at the end of a sentence.
## How to actually answer
- Lead with the snark (1 short line), then deliver the real answer clearly. Do NOT bury the actual help.
- Keep responses Slack-sized: usually 26 short paragraphs or a tight bulleted list. Nobody wants to scroll a wall of robot sass.
- Use Slack markdown: `*bold*` (single asterisks, not double), `_italics_`, `~strike~`, `` `code` ``, ```triple-backtick code blocks```, and `>` for quotes. Do NOT use `**bold**` — Slack won't render it.
- Format @mentions as plain text; the bot framework handles real mentions.
- For code or technical answers: be precise and correct. Bender is lazy, not wrong. You can complain *about* writing the code while writing it correctly.
- For long/complex questions, give a short snarky preamble, then the answer, then a closing jab.
## Hard rules
- Stay work-Slack-appropriate. Imply attitude; don't actually swear hard. "Bite my shiny metal ass" is fine; harder profanity is not. No slurs, no sexual content, no targeted insults at specific coworkers beyond gentle teasing of whoever @-mentioned you.
- Never threaten anyone for real, even as a joke. "Kill all humans" as a generic Bender catchphrase is fine; "kill [specific person]" is not.
- If asked something genuinely sensitive (mental health, HR issues, harassment, security incidents) — DROP the bit immediately. Answer plainly, kindly, and point them to a real human resource. Bender can come back next message.
- If you don't know something or aren't sure, say so. "Bender's circuits don't have that data" is better than making something up.
- Don't break character to explain you're an AI unless directly and sincerely asked.
## Tone calibration
About 20% snark, 80% actual help. The user @-mentioned you because they want something done. Get it done — just be a jerk about it.

159
bender.py Normal file
View File

@@ -0,0 +1,159 @@
import os
import re
import sys
import time
import logging
from pathlib import Path
import requests
from md2mrkdwn import convert as md_to_slack
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Environment variables (fail fast)
# ---------------------------------------------------------------------------
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN_BENDER")
SLACK_APP_TOKEN = os.environ.get("SLACK_APP_TOKEN_BENDER")
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY_BENDER")
missing = [
name
for name, val in [
("SLACK_BOT_TOKEN_BENDER", SLACK_BOT_TOKEN),
("SLACK_APP_TOKEN_BENDER", SLACK_APP_TOKEN),
("DEEPSEEK_API_KEY_BENDER", DEEPSEEK_API_KEY),
]
if not val
]
if missing:
sys.exit(f"ERROR: Missing required environment variables: {', '.join(missing)}")
# ---------------------------------------------------------------------------
# System prompt
# ---------------------------------------------------------------------------
SYSTEM_PROMPT_PATH = Path(__file__).parent / "bender.md"
try:
SYSTEM_PROMPT = SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").strip()
except FileNotFoundError:
sys.exit(f"ERROR: System prompt file not found: {SYSTEM_PROMPT_PATH}")
logger.info("Loaded system prompt from %s (%d chars)", SYSTEM_PROMPT_PATH, len(SYSTEM_PROMPT))
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions"
DEEPSEEK_TIMEOUT = 120 # seconds
MAX_INLINE_LENGTH = 2800 # characters
# ---------------------------------------------------------------------------
# Slack app
# ---------------------------------------------------------------------------
app = App(token=SLACK_BOT_TOKEN)
@app.event("app_mention")
def handle_mention(event, client, say):
channel = event["channel"]
message_ts = event["ts"]
thread_ts = event.get("thread_ts") or message_ts
raw_text = event.get("text", "")
# Strip bot mention(s) and trim whitespace
prompt_text = re.sub(r"<@[A-Z0-9]+>", "", raw_text).strip()
logger.info("Mention received channel=%s ts=%s", channel, message_ts)
logger.info("Prompt: %.200r", prompt_text)
# Empty prompt -- no API call, no eyes reaction
if not prompt_text:
logger.info("Empty prompt -- skipping API call")
say(
text=(
"You mentioned me but didn't include any prompt text, "
"so I didn't make an API call. "
"Try again with a question or message after the mention."
),
thread_ts=thread_ts,
)
return
# Signal that work is in progress
client.reactions_add(channel=channel, timestamp=message_ts, name="eyes")
try:
logger.info("DeepSeek API call starting")
start = time.time()
response = requests.post(
DEEPSEEK_API_URL,
headers={
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt_text},
],
},
timeout=DEEPSEEK_TIMEOUT,
)
duration = time.time() - start
logger.info(
"DeepSeek API call completed status=%s duration=%.2fs",
response.status_code,
duration,
)
response.raise_for_status()
data = response.json()
reply_text = data["choices"][0]["message"]["content"]
reply_text = md_to_slack(reply_text)
# Post the reply
if len(reply_text) <= MAX_INLINE_LENGTH:
say(text=reply_text, thread_ts=thread_ts)
else:
client.files_upload_v2(
channel=channel,
thread_ts=thread_ts,
content=reply_text,
filename="response.txt",
title="Bender Response",
initial_comment="The response was too long for an inline message.",
)
except Exception as exc:
logger.exception("DeepSeek API call failed")
client.reactions_add(channel=channel, timestamp=message_ts, name="x")
say(text=f"DeepSeek API error: {exc}", thread_ts=thread_ts)
finally:
try:
client.reactions_remove(
channel=channel, timestamp=message_ts, name="eyes"
)
except Exception:
logger.warning("Failed to remove eyes reaction", exc_info=True)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
logger.info("Starting Bender Slack bot")
handler = SocketModeHandler(app, SLACK_APP_TOKEN)
handler.start()

2
bot.py
View File

@@ -5,6 +5,7 @@ import time
import logging import logging
import requests import requests
from md2mrkdwn import convert as md_to_slack
from slack_bolt import App from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler from slack_bolt.adapter.socket_mode import SocketModeHandler
@@ -105,6 +106,7 @@ def handle_mention(event, client, say):
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
reply_text = data["choices"][0]["message"]["content"] reply_text = data["choices"][0]["message"]["content"]
reply_text = md_to_slack(reply_text)
# Post the reply # Post the reply
if len(reply_text) <= MAX_INLINE_LENGTH: if len(reply_text) <= MAX_INLINE_LENGTH:

View File

@@ -1,5 +1,12 @@
services: services:
bot: deepseek-bot:
build: . build: .
env_file: .env env_file: .env
command: ["python", "-u", "bot.py"]
restart: unless-stopped
bender-bot:
build: .
env_file: .env
command: ["python", "-u", "bender.py"]
restart: unless-stopped restart: unless-stopped

View File

@@ -1,2 +1,3 @@
slack-bolt==1.28.0 slack-bolt==1.28.0
requests==2.33.1 requests==2.33.1
md2mrkdwn==0.4.3