Ana is a Discord chatbot that acts less like a helpdesk macro and more like an online person with timing, mood, and occasional restraint.
Under the hood, it is a single Python runtime with:
discord.pyevent handling- a multi-stage AI fallback pipeline
- per-channel short-term memory
- per-user profile extraction and persistence
- a keepalive HTTP endpoint for hosting platforms that distrust silence
- Ana
Ana processes Discord message events and decides whether to:
- ignore,
- react,
- reply,
- run a roast/flirt mode,
- send a random dad joke on non-trigger traffic,
- update a per-user profile in the background.
Core execution traits implemented in code:
- user cooldown: 25 seconds
- channel cooldown: 7 seconds
- low-signal skip: 5 percent
- ghost-typing: 6 percent
- reaction overlay on text reply: 10 percent
- typo injection: 4 percent (70 percent chance of correction follow-up)
- follow-up probabilities: roast 25 percent, flirt 20 percent, normal 8 percent
Ana runs in one process with two execution contexts:
- main async context: Discord gateway, command handling, message pipeline
- daemon thread: Flask keepalive server on port
8080
The AI call path is intentionally offloaded with asyncio.to_thread(...) so HTTP model calls do not block Discord event processing.
End-to-end triggered request flow:
main.py:on_messagereceives message.- mention/roast/flirt/trigger detection runs via precompiled regex patterns.
- cooldown and behavior gates run.
- message text is context-enriched:
- mention tokens resolved to display names
- optional reply-thread context injected
- channel history appended
nlp.process_with_nlp(...)executes in worker thread.- Groq waterfall attempts models in priority order.
- if Groq fails: Gemini Gen1 then Gemini Gen2.
- if all providers fail: static short fallback response list.
- output is normalized and post-processed for style cleanup.
- response is sent in one or multiple chunks.
- optional mode-specific follow-up is sent.
- profile extraction task runs asynchronously and updates JSON profile store.
Responsibilities:
- Discord bot bootstrap and event loop wiring
- command handlers:
!joke,!shutdown - trigger mode selection (normal, roast, flirt)
- cooldown and behavior simulation gates
- message history maintenance (
deque(maxlen=20)per channel) - asynchronous NLP dispatch with typing/read-delay simulation
- optional follow-up line scheduling
- background profile extraction task creation
Responsibilities:
- prompt assembly for normal/roast/flirt modes
- profile-access classifier using backup Groq key path
- Groq model waterfall execution with per-model settings
- Gemini fallback execution
- response normalization (
normalize_response) - artifact stripping and style cleanup (
post_process)
Default model sequence from config:
moonshotai/kimi-k2-instructllama-3.1-8b-instantllama-3.1-8b-instant(deduped at runtime)qwen/qwen3-32b
Fallback chain:
gemini-1.5-flash-latestgemini-2.5-flash-lite- static fallback responses
Responsibilities:
- per-user profile file resolution and caching
- deep-merge of extracted structured facts
- thread-safe file update with atomic replace
- compact profile context formatting for prompt injection
- Gemini-based personal fact extraction (
extract_profile_info)
Persistence model:
- path:
data/profiles/*.json - includes internal fields
_id,_name - stores extracted public facts and preferences
Responsibilities:
- fetch live jokes from configured endpoint
- enforce random chance, cooldown, and daily cap
- wrap send behavior with typing simulation and intro/outro variants
Implemented constraints:
- default chance:
0.15 - default cooldown:
60s - daily cap:
3
Responsibilities:
- load environment variables with
load_dotenv(override=True) - parse typed numeric env values
- expose trigger, roast, flirt word sets
- expose joke settings dataclass
- expose model waterfall overrides and per-model generation settings
Responsibilities:
- Flask app serving
GET /=>Bot is alive! - daemon thread launch for host uptime probes
Runtime dependencies from requirements.txt:
discord.py>=2.3.2flask>=3.0.0python-dotenv>=1.0.0requests>=2.32.0groq>=1.0.0
All configuration is env-driven.
Required for baseline operation:
DISCORD_TOKENGROQ_API_KEY(recommended for primary model path)
Optional but important:
GROQ_BACKUP_API_KEY(reserved for profile-access classifier path)GEN1_API_KEYandGEN2_API_KEY(Gemini fallback and profile extraction)SYSTEM_PROMPT(inline prompt override)CHARACTER_PROFILE_PATH(file-based prompt source override)JOKE_CHANCEJOKE_COOLDOWNJOKE_FETCH_TIMEOUTJOKE_API_URLGROQ_MODEL_PRIMARYGROQ_MODEL_BACKUP1GROQ_MODEL_BACKUP2GROQ_MODEL_BACKUP3
Environment template is provided in .env.example.
!joke: force joke fetch and send!shutdown: owner-only graceful shutdown
on_ready: startup log and cleanup-task activationon_message: full request decision tree
GET /onkeepalive.py: health probe responseBot is alive!
process_with_nlp(...)call_groq(...)call_gemini(...)normalize_response(...)post_process(...)extract_profile_info(...)ProfileStore.update(...)DadJokeService.maybe_send_joke(...)
- Clone repository.
- Create virtual environment.
- Install dependencies.
- Copy
.env.exampleto.envand fill values. - Run
python main.py.
Example:
git clone https://github.com/Kaelith69/Ana.git
cd Ana
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux/macOS
# source .venv/bin/activate
pip install -r requirements.txt
copy .env.example .env # Windows
# cp .env.example .env # Linux/macOS
python main.pyUser: hey ana
Ana: hey what's up
User: ana ur so mid
Ana: imagine saying that and expecting impact
User: ana ur kinda cute
Ana: careful i might start believing u
User: !joke
Ana: okay don't judge me
Ana: <dad joke from API>
Recommended local checks:
python -m compileall .
python smoke_test.py
python -c "import config, jokes, profiles, nlp, keepalive, main; print('imports-ok')"Wiki references:
wiki/Home.mdwiki/Architecture.mdwiki/Installation.mdwiki/Usage.mdwiki/API-Reference.mdwiki/Developer-Guide.mdwiki/Privacy.mdwiki/Troubleshooting.mdwiki/Roadmap.md
- startup succeeds but no reply:
- verify Message Content intent
- verify channel permissions
- verify trigger words
- keys look valid but runtime still fails:
- confirm
.envvalues are current - restart process after edits
- run
python smoke_test.py
- confirm
- keepalive not responding:
- check port
8080availability - verify keepalive thread startup
- check port
- event-loop safety:
- blocking calls are offloaded via
asyncio.to_thread
- blocking calls are offloaded via
- bounded in-memory state:
- per-channel history uses fixed
deque(maxlen=20) - periodic cleanup task prunes stale cooldown keys
- per-channel history uses fixed
- external API resilience:
- waterfall and fallback chain reduce hard failure rates
- output normalization:
- deterministic cleanup reduces LLM artifact leakage without extra API calls
- write-path safety:
- profile writes use temp file + atomic replace
This README is generated against implementation as source of truth. If behavior probabilities or model order changes in code, update this document in the same change set. Future readers will still judge us either way, but at least they will be judging the current version.