Smartbot is a working Discord voice bot (wake word, Gemini AI, ElevenLabs TTS). The core pipeline is functional. This plan adds a massive set of fun features to make it a fully-featured group companion. The user's friend group plays League of Legends, Counter-Strike, Rust, and Monster Hunter. Whisper currently struggles with game-specific names, which we'll fix with the hotwords parameter.
Before adding 20+ new tools, two foundational changes prevent brain.py from becoming unmaintainable.
Replace the if/elif chain in _dispatch_tool() with a registry dict. Each tool module registers a handler at import time.
Files: bot/ai/brain.py
# New: registry dict populated by tool modules
_TOOL_REGISTRY: dict[str, Callable] = {}
def register_tool(name: str, handler: Callable):
_TOOL_REGISTRY[name] = handler
# _dispatch_tool becomes:
async def _dispatch_tool(name, inputs, guild_id, session_id, display_name, user_discord_id, bot_client):
handler = _TOOL_REGISTRY.get(name)
if not handler:
return f"Unknown tool: {name}"
return await handler(
inputs, guild_id=guild_id, session_id=session_id,
display_name=display_name, user_discord_id=user_discord_id,
bot_client=bot_client,
)Each existing tool module (web_search, weather, etc.) gets a register() call at the bottom. New tools self-register on import. brain.py imports all tool modules in _dispatch_tool's first call (lazy) or at module level.
Many features (leaderboards, trivia scores, stats) produce output too long for TTS. Add a helper to post embeds to the text channel.
New file: bot/utils/text_channel.py
async def post_embed(bot_client, guild_id: int, embed: discord.Embed) -> None:
"""Post an embed to the configured text channel."""Fix Whisper's poor recognition of game character names by using the hotwords parameter.
File: bot/audio/transcriber.py — _transcribe_sync()
File: bot/config.py — add WHISPER_HOTWORDS optional config
Add a hotwords string with League champions, CS:GO maps/weapons, Rust items, Monster Hunter monsters. Pass to model.transcribe(hotwords=...). Also expand initial_prompt to include common gaming terms.
New file: bot/audio/gaming_vocab.py — Contains the hotwords string and can be expanded over time.
Hotwords string includes: Ahri, Akali, Anivia, Ashe, Blitzcrank, Caitlyn, Darius, Draven, Ezreal, Garen, Jinx, Kai'Sa, Lee Sin, Lux, Miss Fortune, Morgana, Nasus, Pyke, Thresh, Vayne, Yasuo, Yone, Zed, Dust 2, Mirage, Inferno, AWP, AK-47, Fatalis, Rathalos, Zinogre, etc.
These are pure functions — no state, no DB. Fast to implement.
New file: bot/tools/random_fun.py
Functions:
roll_dice(expression: str) -> str— Parses "2d20", "4d6+3", "d100"flip_coin(count: int = 1) -> str— Heads/tails resultsmagic_8ball(question: str) -> str— Classic 20 responses
3 tool definitions in bot/ai/tools.py: roll_dice, flip_coin, magic_8ball
New file: bot/tools/jokes.py
Function: get_dad_joke() -> str — Calls icanhazdadjoke.com API (free, no key, uses httpx)
1 tool definition: tell_joke
New file: bot/tools/calculator.py
Function: calculate(expression: str) -> str — Safe math eval + unit conversions (F/C, miles/km, lbs/kg)
1 tool definition: calculate
New file: bot/tools/gaming.py
Functions:
game_lookup(game: str, query: str) -> str— Uses web_search internally but with game-specific query formatting- LoL: "league of legends {champion} build season 14 mobafire"
- CS: "counter strike {query}"
- Monster Hunter: "monster hunter wilds {query} guide"
- Rust: "rust game {query}"
1 tool definition: game_lookup — Gemini uses this when someone asks about builds, guides, or game info
New file: bot/tools/sports.py
Function: get_sports_scores(query: str) -> str — Uses web_search internally with sports-optimized queries
1 tool definition: get_sports_scores
New file: bot/tools/briefing.py
Function: morning_briefing(guild_id: int, user_discord_id: str) -> str — Compiles weather + birthdays + news headlines using existing tools
1 tool definition: morning_briefing
Phase 1 total: 8 new tools, 3 new files, 0 DB changes
Use Discord's built-in soundboard (server already has sounds uploaded).
New file: bot/tools/soundboard.py
Functions:
list_sounds(guild) -> str—guild.fetch_soundboard_sounds()→ return namesplay_sound(sound_name: str, guild, voice_channel) -> str— Fuzzy-match name →voice_channel.send_sound(sound)
2 tool definitions: play_sound, list_sounds
The dispatch needs guild + voice_channel access. Resolved via bot_client.get_guild(guild_id) and guild.voice_client.channel.
New file: bot/cogs/soundboard_cog.py
Commands:
/soundboard list— Embed with all server sounds/soundboard play <name>— Play a sound from text
File: bot/main.py — load bot.cogs.soundboard_cog
Phase 2 total: 2 new tools, 2 new files, 0 DB changes
New DB table in bot/db/database.py:
CREATE TABLE IF NOT EXISTS birthdays (
discord_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
month INTEGER NOT NULL,
day INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);New file: bot/tools/birthday_tracker.py
Functions:
set_birthday(discord_id, display_name, month, day) -> strget_upcoming(guild_id) -> str
CRUD in bot/db/models.py: set_birthday(), get_birthday(), get_todays_birthdays(), get_upcoming_birthdays()
2 tool definitions: set_birthday, get_upcoming_birthdays
Birthday daily check: Add discord.ext.tasks.loop in fun_cog.py that checks daily and posts/speaks birthday announcements.
New DB table in bot/db/database.py:
CREATE TABLE IF NOT EXISTS greetings (
discord_id TEXT PRIMARY KEY,
greeting_text TEXT NOT NULL,
greeting_sound TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);New file: bot/tools/greetings.py
Functions:
set_greeting(discord_id, text, sound_name=None) -> strget_greeting(discord_id) -> dict | Noneclear_greeting(discord_id) -> str
1 tool definition: set_greeting
Voice cog change: Add on_voice_state_update listener in voice_cog.py — when a non-bot member joins the bot's channel, look up their greeting and TTS it (with a short delay).
New file: bot/tools/horoscope.py
Function: get_horoscope(discord_id=None, sign=None) -> str — Looks up birthday for zodiac sign, returns sign info so Gemini generates creative horoscope
1 tool definition: get_horoscope
File: bot/cogs/voice_cog.py — In _handle_wake_event, load preferred_callout for ALL channel members (not just speaker) when building channel_members roster:
channel_members = []
for m in vc.channel.members:
if not m.bot:
preferred = await models.get_display_name(str(m.id), m.display_name)
channel_members.append({"name": preferred, "id": str(m.id)})New DB table:
CREATE TABLE IF NOT EXISTS voice_stats (
discord_id TEXT NOT NULL,
guild_id TEXT NOT NULL,
display_name TEXT NOT NULL,
wake_count INTEGER DEFAULT 0,
games_played INTEGER DEFAULT 0,
trivia_correct INTEGER DEFAULT 0,
updated_at TEXT,
PRIMARY KEY (discord_id, guild_id)
);New file: bot/tools/stats_tracker.py
Functions:
increment_stat(discord_id, guild_id, stat_name) -> Noneget_leaderboard(guild_id, metric) -> str
1 tool definition: get_leaderboard
Slash command in fun_cog.py: /stats — shows embed leaderboard
Phase 3 total: 5 new tools, 4 new files, 3 new DB tables
New directory: bot/games/
New file: bot/games/__init__.py
New file: bot/games/manager.py
Core design:
@dataclass
class GameState:
game_type: str
guild_id: int
started_by: str
round_number: int = 0
max_rounds: int = 5
scores: dict[str, int] = field(default_factory=dict)
current_data: dict = field(default_factory=dict)
participants: list[str] = field(default_factory=list)
_active_games: dict[int, GameState] = {} # guild_id → GameState
async def start_game(guild_id, game_type, started_by, **kwargs) -> str
async def get_active_game(guild_id) -> GameState | None
async def end_game(guild_id) -> strNew file: bot/games/trivia.py
Uses Open Trivia DB API (free, no key): https://opentdb.com/api.php?amount=1&type=multiple
Functions:
start_trivia(guild_id, started_by, rounds=5, category=None) -> stranswer_trivia(guild_id, player_id, answer) -> str— Fuzzy match answersend_trivia(guild_id) -> str
1 tool definition (single tool with action param): trivia
Slash command: /trivia leaderboard in fun_cog.py
New file: bot/games/would_you_rather.py
New file: bot/games/wyr_questions.py — Bank of ~100 questions
Functions:
start_wyr(guild_id, started_by, rounds=5) -> strvote_wyr(guild_id, player_id, choice) -> strreveal_wyr(guild_id) -> str
1 tool definition: would_you_rather
New file: bot/games/most_likely.py
Gemini generates prompts based on channel members. Everyone votes.
Functions:
start_most_likely(guild_id, members) -> strvote_most_likely(guild_id, voter_id, target_name) -> strreveal_most_likely(guild_id) -> str
1 tool definition: most_likely_to
New file: bot/games/rps.py
Functions:
play_rps(guild_id, player_id, choice, opponent_id=None) -> str
1 tool definition: rock_paper_scissors
New file: bot/games/hot_takes.py
Gemini poses a statement, everyone rates 1-10.
Functions:
start_hot_takes(guild_id) -> strrate_take(guild_id, player_id, rating) -> strreveal_take(guild_id) -> str
1 tool definition: hot_takes
Phase 4 total: 5 tool definitions, 7 new files, game state infrastructure
New file: bot/tools/modes.py
_active_modes: dict[int, set[str]] = {} # guild_id → set of mode names
async def toggle_mode(guild_id, mode) -> str
async def get_active_modes(guild_id) -> set[str]Modes: hype_man, debate_mode
File: bot/ai/prompts.py — build_system_prompt() gains active_modes param, appends mode-specific personality instructions.
File: bot/cogs/voice_cog.py — Pass active modes to build_system_prompt via brain.handle_request.
1 tool definition: toggle_mode
New file: bot/games/polls.py
Functions:
create_poll(guild_id, question, options) -> strcast_vote(guild_id, voter_id, option) -> strclose_poll(guild_id) -> str
1 tool definition: poll
New file: bot/games/story_mode.py
Functions:
start_story(guild_id, genre=None) -> stradd_to_story(guild_id, player_id, contribution) -> strend_story(guild_id) -> str
1 tool definition: story_mode
New DB table:
CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
guild_id TEXT NOT NULL,
created_by TEXT NOT NULL,
songs TEXT DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);New file: bot/tools/playlist.py
Functions:
create_playlist(name, guild_id, created_by) -> stradd_song(playlist_name, guild_id, song) -> strplay_playlist(playlist_name, guild_id, bot_client) -> str— Sends songs to music bot via call_discord_botlist_playlists(guild_id) -> str
1 tool definition: playlist
Phase 5 total: 4 tool definitions, 4 new files, 1 DB table
File: bot/ai/prompts.py
Update the tools section to group by category:
Tools available:
- Utility: web_search, get_weather, calculate, tell_joke, roll_dice, flip_coin, magic_8ball
- Gaming: game_lookup (builds/guides for LoL, CS, Rust, Monster Hunter), get_sports_scores
- Soundboard: play_sound, list_sounds (plays from this server's Discord soundboard)
- Social: set_birthday, get_upcoming_birthdays, set_greeting, get_horoscope, morning_briefing
- Games: trivia, would_you_rather, most_likely_to, rock_paper_scissors, hot_takes, poll, story_mode
- Music: call_discord_bot, playlist
- Memory: remember_fact, recall_facts, set_user_preference
- Other: log_quote, set_timer, toggle_mode, get_leaderboard
Add gaming context:
This friend group plays League of Legends, Counter-Strike, Rust, and Monster Hunter.
When they ask about builds, guides, or game info, use game_lookup with the right game name.
bot/audio/gaming_vocab.py— Whisper hotwords for gaming termsbot/utils/text_channel.py— Embed posting helperbot/tools/random_fun.py— Dice, coin, 8-ballbot/tools/jokes.py— Dad jokes APIbot/tools/calculator.py— Math + unit conversionbot/tools/gaming.py— Game-specific lookupsbot/tools/sports.py— Sports scoresbot/tools/briefing.py— Morning briefingbot/tools/soundboard.py— Discord soundboard integrationbot/tools/birthday_tracker.py— Birthday CRUDbot/tools/greetings.py— Custom voice greetingsbot/tools/horoscope.py— Zodiac/horoscopebot/tools/stats_tracker.py— Voice stats/leaderboardbot/tools/modes.py— Personality mode togglesbot/tools/playlist.py— Playlist managementbot/cogs/soundboard_cog.py— Soundboard slash commandsbot/games/__init__.pybot/games/manager.py— Shared game statebot/games/trivia.pybot/games/would_you_rather.py+bot/games/wyr_questions.pybot/games/most_likely.pybot/games/rps.pybot/games/hot_takes.pybot/games/polls.pybot/games/story_mode.py
bot/ai/brain.py— Tool registry patternbot/ai/tools.py— ~24 new tool definitionsbot/ai/prompts.py— Updated system prompt with all features + modes + gaming contextbot/audio/transcriber.py— Add hotwords parameterbot/db/database.py— 4 new tables (birthdays, greetings, voice_stats, playlists)bot/db/models.py— CRUD for new tablesbot/cogs/voice_cog.py— Greeting announcements, nickname fix, mode passingbot/cogs/fun_cog.py— Birthday daily check, /trivia leaderboard, /statsbot/main.py— Load soundboard_cogbot/config.py— WHISPER_HOTWORDS optional config
birthdays(discord_id PK, display_name, month, day)greetings(discord_id PK, greeting_text, greeting_sound)voice_stats(discord_id + guild_id composite PK, stats counters)playlists(id PK, name, guild_id, created_by, songs JSON)
Pre-req: Tool registry in brain.py + text_channel helper + gaming vocab hotwords✅Phase 1: Simple tools (random_fun, jokes, calculator, gaming, sports, briefing)✅Phase 2: Soundboard (Discord API integration)✅Phase 3: DB tables + social features (birthdays, greetings, horoscope, nickname fix, stats)✅Phase 4: Game engine + all games (trivia, WYR, most likely, RPS, hot takes)✅Phase 5: Advanced features (modes, polls, story mode, playlists)✅Final: System prompt overhaul✅
After each phase:
docker compose up --build— confirm clean builddocker compose logs -f bot— check for import errors, missing config- Join voice channel, test features by voice:
- "Hey smartbot, roll 2d20"
- "Hey smartbot, tell me a joke"
- "Hey smartbot, play the airhorn"
- "Hey smartbot, what should I build on Jinx?"
- "Hey smartbot, let's play trivia"
- Test slash commands in Discord:
/soundboard list,/quotes,/stats