Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ OLLAMA_EMBEDDING_MODEL=nomic-embed-text
EMBEDDING_DIM=768
TOP_K=3
TEST_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5445/cs_assistant_test
DISCORD_BOT_TOKEN=
DISCORD_GUILD_ID=
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ setup:
cli:
uv run python -m src.apps.dev_cli

discord:
uv run python -m src.apps.discord_bot

migrate:
uv run alembic -c src/infrastructure/db/alembic.ini upgrade head

Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,41 @@ ask> what electives should I take for a software engineering focus?

You should see an answer followed by a `Sources:` block listing the URLs used.

### 10. (Optional) Run the Discord bot

The bot exposes the same Q&A flow as `make cli` through a `/ask` slash command,
so it needs the full backend already working: Docker up, `make migrate` and
`make ingest` run, and Ollama serving. The CLI is enough for most development —
this step is only needed if you're working on the bot itself.

You'll need your own throwaway Discord server and bot to develop against:

1. In the [Discord Developer Portal](https://discord.com/developers/applications),
create an application, then under **Bot** click **Reset Token** and copy the
token. No privileged intents are required — `/ask` uses default intents.
2. Under **OAuth2 → URL Generator**, select the `bot` and `applications.commands`
scopes, open the generated URL, and invite the bot to a server you own.
3. Enable **Settings → Advanced → Developer Mode**, then right-click your server
icon → **Copy Server ID**.
4. Add both values to your `.env`:

```
DISCORD_BOT_TOKEN=your_token_here
DISCORD_GUILD_ID=your_server_id_here
```

5. Start the bot:

```bash
make discord
```

`/ask` is synced to your server on startup, so it appears immediately. Invoking
it defers the reply (Discord's 3-second ack), then sends the answer and its
sources as a follow-up once `ask()` finishes — expect the same 1–3 minute local
latency as the CLI. If the token or server ID is missing, the bot exits at
startup with a message.

---

## Using it
Expand Down Expand Up @@ -188,6 +223,7 @@ Every command runs through `uv run`, so you never need to activate the venv.
| `make migrate` | Apply Alembic migrations (enable pgvector, create tables) |
| `make ingest` | Scrape, chunk, embed, and store every URL in `list.json` |
| `make cli` | Open the question REPL against the ingested data |
| `make discord` | Run the Discord bot (`/ask` slash command) — see setup step 10 |
| `make lint` | Run ruff (lint only, no changes) |
| `make format` | Run black (rewrites files in place) |
| `make test` | Run the pytest suite (requires Docker running) |
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"langchain-text-splitters",
"celery",
"redis",
"discord-py>=2.7.1",
]

[dependency-groups]
Expand Down
57 changes: 57 additions & 0 deletions src/apps/discord_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import sys

import discord
from discord import app_commands

from src.completions.services.completion_service import ask
from src.config import settings
from src.config.logger import get_logger

log = get_logger(__name__)


class CsAssistantBot(discord.Client):
def __init__(self):
intents = discord.Intents.default()
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)

async def on_ready(self) -> None:
guild = discord.Object(id=settings.discord_guild_id)
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
log.info("discord_ready", self_user=self.user, guild=settings.discord_guild_id)


client = CsAssistantBot()


@client.tree.command(name="ask", description="Receive advice for Carleton CS")
@app_commands.describe(question="Your question")
async def ask_command(interaction: discord.Interaction, question: str) -> None:
await interaction.response.defer()
log.info("discord_ask_received", interaction_id=interaction.id, question=question)

try:
answer = await ask(question=question)
except Exception as e:
await interaction.followup.send(
"❌ I couldn't process this request, please try again later."
)
log.error("discord_ask_failed", interaction_id=interaction.id, error=str(e))
return

content = answer.text
if answer.sources:
content += "\n\n**Sources:**\n" + "\n".join(f"• {source.url}" for source in answer.sources)

await interaction.followup.send(content)
log.info("discord_ask_completed", interaction_id=interaction.id)


if __name__ == "__main__":
if settings.discord_bot_token is None or settings.discord_guild_id is None:
print("Discord config is incomplete or nonexistent", file=sys.stderr)
sys.exit(1)

client.run(settings.discord_bot_token)
2 changes: 2 additions & 0 deletions src/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Settings(BaseSettings):
embedding_dim: int
top_k: int = 3
test_database_url: str | None = None
discord_bot_token: str | None = None
discord_guild_id: int | None = None


settings = Settings()
Loading
Loading