5.2 KiB
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 thedevdependency 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 liveruns 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 ofget_conversation_contextat 1k and 5k rows - Container:
./build.sh(podman). CI (.gitea/workflows/build-push.yml, Gitea) runs atestjob (all four checks + hermetic pytest) on pushes/PRs/releases, andbuild-and-push(image build/push) only on main/release, gated ontest
Setup requirements
- A repo-root
.envis required even to run tests:config.pycallsload_dotenv()and raisesRuntimeErrorat 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 importsvibe_bot.app→vibe_bot.tts→sounddevice). On this host:sudo dnf install portaudio(the Containerfile installsportaudio19-dev). - TTS needs
kokoro-v1.0.onnxandvoices-v1.0.binin the repo root (baked into the container image). The bot runs without them; only!speakdegrades.
Test suite gotchas
- The network-dependent tests are marked
live(intest_llm_client.py): unmocked calls to the realCHAT_ENDPOINTfrom.env. They are deselected by default and only run viauv run pytest -m live, which needs network access to that API. - If
uv run <tool>suddenly fails withModuleNotFoundErroror "bad interpreter", the.venvshebangs are stale from a repo move:rm -rf .venv && uv sync.
Code map
main.py— entrypoint:validate_config(),configure_logging(), thenbuild_bot(app).run(token).app.py— composition root:configure_logging()(the onlybasicConfig),@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 inon_messageagainstapp.bot_cache, not registered withbot.command.commands/— command groups registered byregister_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 throughon_message);_state.pyholds 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 thenormcolumn on existing DBs),messages(ChatDatabase: embeddings as float32 blobs with a stored L2norm, 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.pyis a thin facade re-exportingChatDatabase/CustomBotManager.llm/—chat.py(completion clients: instruct / with-history / with-tools),images.py(gen + edit clients),registry.py(tool registry: schema + dispatch).llm_client.pyis the public facade overllm/plus the embedding HTTP plumbing. (llama_wrapper.pyis 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_membersis a no-op LangChain@toolstub used only for name/description/schema; the real implementation isget_channel_members_impl(channel), wired into the tool executor inapp.py.tts.py—TTSEngine(Kokoro wrapper;AudioResult.partialsignals failed chunks); voice/speed defaults are aliases ofconfig.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 inpyproject.toml; it runs with the default ruleset of the locked version.