A Discord bot powered by Google Gemini that answers questions in multi-turn reply threads. Genie uses a lightweight triage model to decide whether to fetch web pages, extract video captions from video links, use a search engine, or answer directly — then routes to a stronger model for the final response.
- Features
- Agent
- Requirements
- Environment Variables
- Configuration
- Discord Setup
- Deploy with Docker
- Observability
- Development
- Caveats & Shortcomings
- Q&A
- Responds to explicit @mentions and
!ai/!aisearchcommand prefixes in guilds and DMs. - Uses Discord reply-chains as conversation context, allows branching at any point; Persisted in PostgreSQL.
- Automatically fetches and reads web pages linked in messages (static only, HTML & text).
- Extracts captions and transcripts from YouTube and other video URLs via
yt-dlpfor video content summaries and questions. - Searches the web using Google Search grounding or Tavily, and cites sources.
- Processes ANY Discord file attachments that Gemini accepts (images, videos, PDFs, etc.) passed directly in messages.
- Paginates long responses with a Next Page button; Renders math and tables as images or HTML.
- Retry button on failed or degraded responses, with definable fallback models.
- Right-click context menu commands: Summarize, Export as HTML, Export as Image.
- Fully configurable via a single YAML file — models, timeouts, search backend.
- Usable with one paid Google API key or one or more free API keys used in rotation.
- Uploads all files to Gemini File API for extremely fast LLM responses despite large attachments thanks to 48 hour file caching.
...and more
| Trigger | Intent | Graph entry point |
|---|---|---|
@mention (no prefix) |
UNKNOWN |
→ Triage (model decides) |
!ai |
GENERAL |
→ General directly (triage skipped) |
!aisearch |
SEARCH |
→ Search directly (triage skipped, or via Triage in Tavily mode) |
| Context menu: Summarize | SUMMARY |
→ Triage (model decides), with an ephemeral "Summarize this in English" instruction |
flowchart TD
START(["START"])
TRIAGE["Triage\n(fast model, thinking)"]
FETCH["Fetch Content\n(get_website / get_video_captions)"]
GENERAL["General\n(answer model)"]
COMPUTATION["Computation\n(code execution model)"]
SEARCH["Search\n(search model + grounding)"]
END(["END"])
START -- "UNKNOWN / SUMMARY intent" --> TRIAGE
START -- "!ai (GENERAL intent)" --> GENERAL
START -- "!aisearch, Google mode (SEARCH intent)" --> SEARCH
START -- "!aisearch, Tavily mode (SEARCH intent)" --> TRIAGE
TRIAGE -- "route_to_general" --> GENERAL
TRIAGE -- "route_to_python" --> COMPUTATION
TRIAGE -- "route_to_search (Google)" --> SEARCH
TRIAGE -- "web_search tool call (Tavily)" --> SEARCH
TRIAGE -- "get_website / get_video_captions" --> FETCH
FETCH --> GENERAL
GENERAL --> END
COMPUTATION --> END
SEARCH --> END
Triage is a lightweight model. It inspects only the latest turn and chooses one of five actions:
- Call
get_websiteorget_video_captions→ Fetch Content, then General - Call
route_to_search→ Search directly - Call
route_to_python→ Computation directly (Python code execution) - Call
route_to_general→ General directly - No tool call (fallback) → General directly
Routing sentinel calls (route_to_search, route_to_python, route_to_general) are consumed by the triage node and are never written to message state, keeping conversation history clean. Real tool calls (get_website, get_video_captions, web_search) are added to state so their ToolMessage responses have valid tool_call_id pairings.
- Docker — for the full stack (DB + app); or Bun ≥ 1.3 + PostgreSQL ≥ 18 + yt-dlp, Deno, and Chromium (for Playwright) installed on the host
- A Discord application with a bot token (Discord Developer Portal)
- A Google AI API key (Google AI Studio)
| Setup | vCPU | RAM |
|---|---|---|
| Bot + DB (full stack) | 2 | 512 MB |
| Bot only (external DB) | 1 | 512 MB |
The bot embeds Playwright (for HTML-to-image rendering) and bundles yt-dlp, both of which contribute to the memory footprint.
Copy .env.example to .env and fill in the required values.
| Variable | Required | Description |
|---|---|---|
DISCORD_TOKEN |
✅ | Bot token from the Discord Developer Portal |
DISCORD_CLIENT_ID |
✅ | Application ID from the Discord Developer Portal |
DATABASE_URL |
✅ | PostgreSQL connection string |
GOOGLE_FREE_API_KEYS |
✅* | Comma-separated free-tier Google AI API keys (required when any agent node uses apiKeyType: "free") |
GOOGLE_PAID_API_KEY |
✅* | Single paid Google AI API key (required when any agent node uses apiKeyType: "paid") |
TAVILY_API_KEY |
Tavily API key — required only when agent.nodes.search.mode is set to "tavily" |
|
LOG_LEVEL |
Pino log level: trace, debug, info, warn, error (default: info) |
|
FILE_LOG |
Write structured JSON logs to ./logs/ alongside console output (default: false) |
|
NODE_ENV |
Set to production to disable pino-pretty and output raw JSON |
|
CONFIG_PATH |
Path to config YAML file (default: config.local.yaml in the working directory) |
|
SENTRY_URL |
Sentry DSN — enables error and performance monitoring when set |
All bot behaviour is controlled by a YAML config file. config.default.yaml contains every supported option with inline comments explaining what each one does. To customise:
cp config.default.yaml config.local.yaml
# edit config.local.yamlconfig.local.yaml is loaded automatically and is gitignored. In Docker, mount your file and point CONFIG_PATH at it (see Deploy with Docker).
Key things to configure:
- Models — set the Gemini model and
apiKeyType("free"/"paid") for each of the four agent nodes (triage,general,computation,search) - Search backend —
agent.nodes.search.mode:"google"(Gemini grounding) or"tavily" - Attachment mode —
agent.uploadAttachmentMode:"upload"(Gemini Files API) or"inline"(base64) - DMs —
discord.enableInDMs: trueto allow the bot to respond in Direct Messages - System prompt —
prompts.basePromptto customise the bot's persona and instructions
- In the Discord Developer Portal, enable the Message Content privileged intent for your application.
- Under Installation, enable Guild Install (required) and optionally User Install (allows users to add the bot to their account and use the commands in DMs with the bot).
- Guild Install scopes:
applications.commands,bot - Guild Install permissions: Attach Files, Manage Messages, Read Message History, Send Messages (required), Send Messages in Threads (optional — required only if you want the bot to respond inside threads)
- User Install scope:
applications.commands
- Guild Install scopes:
- Mention the bot in any channel to start a conversation:
@Genie what is the capital of France?
Genie only responds to explicit @mentions or !ai / !aisearch prefixes — replying to a message without including @Genie will not trigger it.
The included docker-compose.local.yml starts a PostgreSQL database and the bot together, building the image from source. If you don't want to or can't build the image yourself, docker-compose/docker-compose.local-prebuilt.yml uses the prebuilt image from Docker Hub (vjancik/genieai:latest) instead — note it is currently only available for linux/amd64 and linux/arm64 architectures.
1. Prepare your files
cp .env.example .env # fill in DISCORD_TOKEN, DISCORD_CLIENT_ID, GOOGLE_*_API_KEY(S)
cp config.default.yaml config.local.yaml # customise models, search backend, etc.DATABASE_URL is set automatically by the compose file — do not add it to .env.
2. Start the stack
Migrations run automatically before the app starts.
docker compose -f docker-compose.local.yml up -d --build
# or equivalently:
bun local:upTo stop:
docker compose -f docker-compose.local.yml down
# or equivalently:
bun local:downAny platform that can run a Docker container or Docker Compose stack is supported:
- Render — deploy via Dockerfile
- Railway — deploy via Dockerfile
- Dokploy (self-hosted, VPC) — via Docker Compose or Dockerfile + managed DB
- Coolify (self-hosted, VPC) — via Dockerfile or Docker Compose
DATABASE_URL can point to an external PostgreSQL provider such as Neon, Supabase, or Prisma Postgres (all untested) if you prefer not to manage the database yourself.
LangSmith provides real-time tracing of every agent execution — node transitions, tool calls, model inputs/outputs, latency, and token usage — without any code changes. LangChain picks it up automatically from environment variables:
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=<your-api-key>
LANGSMITH_PROJECT=genie-aiAdd these to your .env (or the container environment) and all agent runs will appear in the LangSmith dashboard. A free tier is available.
Sentry provides exception monitoring, performance tracing, and alerting. Set SENTRY_URL to your project DSN to enable it:
SENTRY_URL=https://<key>@sentry.io/<project>Alerts can be routed to email, a Discord webhook, Slack, and many others via Sentry's alert rules. The LangChain callback handler is also wired in, so Sentry traces include agent span data alongside application errors.
Don't run
bun testdirectly — it loads.envby default, which may polute the environment variables. (Although right now there are no side effects from this) Always usebun run test(uses.env.test) or the filter patterns below.
# Unit tests (no database required)
bun run test tests/unit/
# Integration tests (requires test database)
bun db:test:up && bun db:test:migrate
bun run test tests/integration/
# Filter by test name pattern
bun run test -t "pattern"
# Full suite
bun run testbun typecheck
bun codecheck:fixbun db:up # Start dev database (Docker)
bun db:down # Stop dev database
bun db:generate # Generate migrations from schema changes
bun db:migrate # Apply pending migrations
bun db:studio # Runs Drizzle Kit Database Web UI (default port 4983, works with VS Code port forwarding on Remotes)- docs/features.md — Full (arguably) user-facing feature reference
- docs/file-tree.md — Annotated source file tree
- docs/code-statistics.md — Line count breakdown
- Not suitable for large public servers. There are no per-user or per-channel access controls, allowlists, or token budgets. Anyone who can mention the bot can use it without restriction.
- Gemini API reliability. Since early 2026, Gemini models have been notably flaky — HTTP 503 responses and response times of up to a minute during peak hours are common. The resilient invoker retries on 503s and rotates free keys on 429s, but sustained outages will still surface as failures.
- Inline attachment mode is second-class.
inlinemode re-downloads all attachments in the conversation on every turn (no disk cache), and encodes them as base64 in the request. It was added as a cross-provider fallback;uploadmode (Gemini Files API with 48-hour caching) is strongly preferred. - Gemini 3 tool call hallucinations. Gemini 3 models occasionally hallucinate tool calls in Tavily search mode in nodes where tool use is not expected (general and search nodes). This causes a hard failure that requires the user to hit Retry.
Q: How do I make video captions with yt-dlp work on a cloud server?
A: You need a rotating residential proxy, otherwise YouTube will flag the server as a bot and caption downloading will fail. Webshare starts at around $1/month for 1 GB of traffic, which is more than enough for caption text files. Once you have a proxy URL, set it in config.local.yaml:
ytDlp:
httpProxy: "http://user-US-rotate:pass@p.webshare.io"Q: How can I run this bot with only Free Tier Google AI keys?
A: In the YAML file config, change the search: node model to gemini-2.5-flash, or use searchMode: "tavily" with a free Tavily account (500 searches/month). gemini-3... models don't work with free keys when Google Search grounding is enabled.
Q: Can this bot be hosted with a serverless provider?
A: Any host that supports running Docker containers can run this bot. DATABASE_URL can point to an external PostgreSQL provider like Neon, Supabase, or Prisma Postgres (all untested), removing the need to run the DB container yourself. A fully serverless setup (e.g. Lambda, Cloud Run) is not viable — the bot requires a persistent process for the Discord gateway connection, and the bundled yt-dlp, Deno, and Playwright binaries are not compatible with ephemeral function environments. Cloudflare Workers also won't work because it doesn't support the Bun runtime.