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