Skip to content

Create a minimal Discord /ask bot #5

@AJaccP

Description

@AJaccP

NOTE: Confirm you have a working environment first. For this ticket you create your own throwaway Discord server + bot to develop against (steps in the setup note below) and invite Jacc. The dev token + server ID get swapped for the real server later, once the bot works.

🧠 Context

The Discord bot is the planned (and only) deployment surface for cs-assistant, but src/apps/discord_bot.py is currently an empty stub. The core Q&A flow already works end-to-end via completion_service.ask(question) -> Answer (the dev CLI uses exactly this). This ticket stands up the simplest possible bot: one /ask slash command that runs that flow and replies. Polish — markdown formatting, long-message splitting, abstain styling — is intentionally a later ticket; keep this one minimal.

One Discord-specific constraint drives the design: a slash command must be acknowledged within ~3 seconds, but ask() takes 1–3 minutes locally. So the command must defer() immediately, then send the real answer as a followup (Discord allows up to ~15 minutes after a defer).


🛠 Implementation Plan

  1. Add the dependency: uv add discord.py, and commit the updated uv.lock — CI runs uv sync --frozen, so a stale lock fails the build.
  2. Add the bot config the project way: add discord_bot_token: str | None = None and discord_guild_id: int | None = None to Settings in src/config/__init__.py, plus matching lines in .env.example. Keep them optional (None defaults) so the CLI/tests/CI that don't set them still construct Settings fine. The bot should exit with a clear message if the token is unset at startup. Because both are read from .env, swapping to the real server later is just an .env change — no code edit.
  3. In src/apps/discord_bot.py:
    • Create a Discord client with a slash-command tree (discord.Client + app_commands.CommandTree, or commands.Bot — either is fine for one command). Default intents are enough; the message_content intent is not needed for slash commands.
    • Define an /ask command taking a single string question argument.
    • In the callback: await interaction.response.defer() first (the 3-second ack), then answer = await completion_service.ask(question), then await interaction.followup.send(...) with answer.text and its sources.
    • On startup (on_ready), sync the command tree to your dev guild (using settings.discord_guild_id) — guild-scoped sync is instant, whereas global sync can take up to an hour to show up.
    • Run via client.run(settings.discord_bot_token).
  4. Add a discord target to the Makefile mirroring the cli one, so others can start it the same way.

Notes

  • Render answer.text, then list answer.sources. Do not parse or strip sources in the bot — source handling belongs in the completion layer, not the renderer; the bot just displays what Answer gives it. (You may currently see sources appear both in the answer text and in the list — that's a known issue being fixed in the completion layer, not something to work around here.)
  • Out of scope (deferred to the polish ticket): markdown formatting, splitting replies over Discord's 2000-char limit, and special formatting for the abstain answer. A long answer can exceed 2000 chars and error on send — that's a known limitation we're accepting for now, not something to solve in this ticket.
  • Dev Discord setup (you can do this yourself): create your own throwaway server; in the Discord Developer Portal create an application + bot and copy its token; invite the bot to your server with the bot + applications.commands scopes; put the token and your server's ID (enable Developer Mode, then right-click the server → Copy Server ID) into your .env. Invite Jacc to the server for review. These are throwaway dev values — the production server/token are swapped in later via .env.

✅ Acceptance Criteria

  • /ask appears as a slash command in your dev server and accepts a question argument.
  • Invoking it defers immediately and then replies with the generated answer (no "application did not respond" timeout), including its sources.
  • discord.py is in pyproject.toml and the updated uv.lock is committed (CI's --frozen sync passes).
  • The bot token is read from Settings/.env, never hardcoded; the bot exits with a clear message if the token is missing.
  • make lint passes. (No automated tests required — this is verified manually in your dev server.)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

Status
Ready

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions