Files
vibe-bot/AGENTS.md
T
2026-08-19 13:16:43 -04:00

5.2 KiB

AGENTS.md

Single Python package vibe_bot: a Discord bot (discord.py, ! prefix) with SQLite RAG chat history, Kokoro TTS, and image gen/edit via OpenAI-compatible APIs. Python 3.13, managed with uv. Everything lives in vibe_bot/ (plus the standalone benchmark scripts/bench_rag.py, which is never imported by the package).

Commands

  • Setup: uv sync — installs the project plus the dev dependency group (ruff, mypy, pyright, black, pytest); there are no extras
  • Run bot: uv run python -m vibe_bot.main — this logs the real bot into Discord with the token from .env; don't run it as a smoke test
  • Tests: uv run pytest vibe_bot/tests/ -v — hermetic by default (addopts = "-m 'not live'"); uv run pytest -m live runs the network-dependent live tests; single test: uv run pytest vibe_bot/tests/test_app.py::test_name
  • Checks: uv run ruff check vibe_bot/, uv run mypy vibe_bot/ (strict), uv run pyright vibe_bot/ (strict), uv run black --check vibe_bot/
  • RAG benchmark: uv run python scripts/bench_rag.py — hermetic (embeddings monkeypatched, temp DBs); prints p95 latency of get_conversation_context at 1k and 5k rows
  • Container: ./build.sh (podman). CI (.gitea/workflows/build-push.yml, Gitea) runs a test job (all four checks + hermetic pytest) on pushes/PRs/releases, and build-and-push (image build/push) only on main/release, gated on test

Setup requirements

  • A repo-root .env is required even to run tests: config.py calls load_dotenv() and raises RuntimeError at import time if any required var is missing. Placeholder values suffice for the mocked suite. Never commit it.
  • PortAudio is a required system library (kokoro-tts → sounddevice). Without it, the test suite errors at import time (every module loads conftest.py, which imports vibe_bot.app → vibe_bot.tts → sounddevice). On this host: sudo dnf install portaudio (the Containerfile installs portaudio19-dev).
  • TTS needs kokoro-v1.0.onnx and voices-v1.0.bin in the repo root (baked into the container image). The bot runs without them; only !speak degrades.

Test suite gotchas

  • The network-dependent tests are marked live (in test_llm_client.py): unmocked calls to the real CHAT_ENDPOINT from .env. They are deselected by default and only run via uv run pytest -m live, which needs network access to that API.
  • If uv run <tool> suddenly fails with ModuleNotFoundError or "bad interpreter", the .venv shebangs are stale from a repo move: rm -rf .venv && uv sync.

Code map

  • main.py — entrypoint: validate_config(), configure_logging(), then build_bot(app).run(token).
  • app.py — composition root: configure_logging() (the only basicConfig), @dataclass App (flat fields: db/manager/registry/tts/chat/image/speech/conversation/bot_cache), create_app(), build_bot(app) (intents; event handlers; commands.register_all(bot, app)). Custom-bot "commands" (!<bot_name> ...) are matched in on_message against app.bot_cache, not registered with bot.command.
  • commands/ — command groups registered by register_all: custom_bots (custom-bot, list-custom-bots, delete-custom-bot), speech (speak, voices), images (doodlebob, retcon), conversation (talkforme), admin (lobotomize, debug, history), chat (docstring only — custom-bot chat flows through on_message); _state.py holds the shared App.
  • services/ — the LLM-backed logic: ChatService, ImageService, SpeechService, ConversationService; the Discord command handlers are thin wrappers.
  • db/ — connection (shared connect), schema (init + migrations log at INFO; backfills the norm column on existing DBs), messages (ChatDatabase: embeddings as float32 blobs with a stored L2 norm, cleanup, RAG context), bots (CustomBotManager), search (single-JOIN RAG retrieval scored by one matrix multiply over the stored norms), timing (image-gen ETA stats), vectors (vector math). database.py is a thin facade re-exporting ChatDatabase/CustomBotManager.
  • llm/ — chat.py (completion clients: instruct / with-history / with-tools), images.py (gen + edit clients), registry.py (tool registry: schema + dispatch). llm_client.py is the public facade over llm/ plus the embedding HTTP plumbing. (llama_wrapper.py is gone.)
  • config.py — env loading + import-time validation, voice catalog.
  • prompts.py — LLM prompt constants (image layout/prompt/verify, length hint, build_system_prompt).
  • textutil.py — split_message, get_user_info.
  • tools.py — get_channel_members is a no-op LangChain @tool stub used only for name/description/schema; the real implementation is get_channel_members_impl(channel), wired into the tool executor in app.py.
  • tts.py — TTSEngine (Kokoro wrapper; AudioResult.partial signals failed chunks); voice/speed defaults are aliases of config.TTS_VOICE/TTS_SPEED (single source: config).

Style

  • All four checks (ruff, mypy strict, pyright strict, black) are declared gates and currently pass; keep new code clean under all of them.
  • ruff is a dev dependency (installed via uv add --dev) with config in pyproject.toml; it runs with the default ruleset of the locked version.