Closed-source project. This document covers architecture and design decisions only.
Echo is a Discord chatbot designed to simulate human-like conversation behavior. It tracks social relationships, acts autonomously when bored, and operates on a four-layer memory system. All text and personality are in Turkish.
Discord Message
│
▼
┌─────────────────────┐
│ 3-Layer Filter │
│ Hard → Interest → Rate │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ Message Aggregator │ ← 2s debounce, per user
│ + Processing Queue │ ← FIFO, 0.5s delay
└────────┬────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Dual-Brain Architecture │
│ │
│ Brain 1 (Orchestrator) — Decides │
│ ↓ │
│ ActionRouter — Routes the decision │
│ ↓ │
│ Brain 2 (BrainCore) — Generates reply │
└────────┬────────────────────────────────┘
│
▼
┌─────────────────────┐
│ EmotionTagger │ ← Extract emotion tags
│ + MediaManager │ ← Select GIF
│ + ResponseScheduler│ ← Human typing simulation
└────────┬────────────┘
│
▼
Discord Channel
Independent background services also run continuously:
AnalystAgent · BoredomEngine · GossipPipeline · Dreamer
Every incoming message passes through three stages:
Layer 1 — Hard Filters
Bot messages, DMs, empty content, command prefixes (. / $), silence prefixes (, // #), link-only messages, and spam (last 2 messages per user check) are all eliminated here. Minimum character length is also enforced.
Note: Even messages that fail the hard filter are still stored to STM and LTM. This is required so the AnalystAgent can continuously build user profiles.
Layer 2 — Interest Scoring (InterestScorer) A component-based scoring system. If the total score crosses a threshold, the message proceeds to processing:
| Component | Score |
|---|---|
| Bot mention / reply / name | 100 (guaranteed response) |
| Interest keyword match | +30 (per keyword) |
| Question mark / emotional expression | +10 / +5 |
| Long message (>100 chars) | +15 |
| VIP user | +20 |
| Focus mode bonus | +30 |
| Previously talked to this user | +10 |
| Chaos factor | +0-15 (random) |
Layer 3 — Rate Limiter Normal users: 12/min · VIP users: 20/min
A small, fast model with no personality — cold and analytical. It responds to each incoming message with a structured decision:
- What's the intent? (intent + urgency + confidence)
- What should happen? →
CHAT·IGNORE·SEARCH·TOOL_USE - How should it be written? → Mood, temperature, token limit, system injection
The Orchestrator also dynamically injects context into Brain Core's system prompt: who sent the message, their relationship status with the bot, and which conversation mode to use.
Prompt injection protection is in place: dangerous patterns and non-whitelisted prefixes are blocked.
Takes the Orchestrator's decision and routes it to the appropriate handler:
SEARCH→ Keyword search in LTM, injects results into Brain CoreTOOL_USE→ Web search (Serper.dev), writes result to both Brain Core and memoryCHAT→ Passes directly to Brain Core- Falls back to
CHATon any failure
When a web search is performed, results are framed as the bot's "absolute knowledge" before being sent to Brain Core.
A larger, higher-quality model. The system prompt is dynamically assembled from these blocks:
[Identity] + [Mode] + [Date/Time] + [User Profile]
+ [Social Graph Context] + [Chaos Memory] + [Rules]
+ [Emotion Tag Instruction]
Mode System: default · gaming · roast · sleepy
Manual override takes priority, then content-based detection, then automatic time-of-day selection.
The emotion tag is embedded inside the response and extracted before cleanup. The method returns a (text, gif_url) pair.
┌────────────────────────────────────────────────────┐
│ Memory System │
│ │
│ STM → immediate context (15 msg / 5 min) │
│ LTM → SQLite + FTS5 (keyword search) │
│ VectorDB → ChromaDB + MiniLM (semantic search) │
│ GraphDB → Neo4j (social relationship graph) │
└────────────────────────────────────────────────────┘
STM (Short-Term Memory) Last 15 messages per channel, 5-minute TTL. Provides immediate conversational context.
LTM (Long-Term Memory) SQLite with FTS5 full-text search. Turkish unicode tokenizer. Entries below a confidence threshold (<0.3) are not stored. An Echo filter prevents memory from bloating with repeated content.
Vector Memory
ChromaDB with all-MiniLM-L6-v2 embeddings (384 dimensions). Finds results by semantic similarity — catches connections that keyword search misses. Cosine similarity, threshold 0.4.
Graph Memory (Neo4j) Stores social relationships in a structured graph.
Node types: User · Echo · Topic · Trait
Edge types:
| Edge | Description |
|---|---|
RELATIONSHIP (User→Echo) |
Affinity score (-100 to +100) |
INTERACTED_WITH (User↔User) |
User-to-user sentiment (EMA blended) |
INTERESTED_IN (User→Topic) |
Interest area and intensity |
HAS_TRAIT (User→Trait) |
Personality trait and confidence score |
CHAOS_LINK (Topic↔Topic) |
Unexpected connections discovered by Dreamer |
LTM can operate on its own but hallucinates without support. All four sources together ensure consistency and suppress hallucination.
A periodic background analysis runs for each user:
- Trigger: 50 new messages from a user
- Last 50 messages from LTM are read (30-day window)
- An LLM analyzes them and extracts:
- Profile summary and general mood
- Affinity delta (shift in the user's attitude toward the bot)
- Topics and personality traits
- Results are written to both the SQLite
user_profilestable and Neo4j - The Orchestrator reads this profile as needed to enrich the system prompt
Drift limits: -10/+5 per analysis · ±15 per day. This prevents the bot from suddenly becoming too close or hostile toward someone.
The bot gets bored when there's no conversation. Each channel has a boredom meter from 0 to 100.
Meter Mechanics:
| Event | Change |
|---|---|
| 60s of silence | +2.0 |
| Short boring reply ("ok", "lol") | +5.0 |
| Bot gets mentioned | -25.0 |
| Normal message | -3.0 |
| Long message (>50 chars) | -2.0 (additional) |
| Chaotic action completed | -15.0 (dopamine hit) |
3 Mood Tiers & 9 Action Types:
| Mood | Range | Actions |
|---|---|---|
| Observer | 0-30% | Emoji reaction, Discord status change |
| Poking | 30-70% | Old memory dig, baiting, ghost ping |
| Chaos | 70-100% | Topic hijack, self-dialogue, hallucinated reply, channel rename |
Channel renames are automatically reverted after a set duration. All names are restored when the bot shuts down.
VictimSelector decides which user to target:
- Bot owner (always first if online)
- Most recently active user
- Users with negative sentiment (social tension from graph)
- User with the lowest affinity score
- Random online user not recently active in the channel
Automatically detects interactions between users and writes them to the Neo4j graph.
Triggers: replies, @mentions, back-to-back messages within 5 seconds (co-presence).
Messages are accumulated in batches (6 messages or 120 seconds, whichever comes first). An LLM analyzes the batch and extracts who interacted with whom, and with what sentiment (-1 to +1). The result is written to Neo4j as INTERACTED_WITH edges; old sentiment is EMA-blended (old×0.7 + new×0.3).
This is how the bot learns patterns like "these two always argue" or "those two get along."
A background service that runs every 6 hours. Its purpose: discover topics that are semantically distant from each other but connected in unexpectedly interesting ways.
Algorithm (Semantic Drift):
- All
Topicnodes in the graph are embedded usingall-MiniLM-L6-v2 - KNN search is performed via the Neo4j vector index
- Directly related topics (same user's interests) are filtered out
- The "sweet spot" is captured: 0.65-0.85 similarity (homonyms, conceptual accidents)
- A
CHAOS_LINKedge is created between matched topic pairs
These links are injected into Brain Core's prompt as a [CHAOS MEMORY] block. The bot occasionally slips these unexpected associations into conversation.
EmotionTagger: Extracts the hidden emotion tag embedded in Brain Core's response. Captures both primary and secondary emotions, along with intensity.
MediaManager: Sends GIFs based on emotion intensity. Recently sent GIFs are not repeated (FIFO cache).
| Intensity | GIF Probability |
|---|---|
| High | 80-100% |
| Medium | 30% |
| Low | 0% |
ResponseScheduler: Simulates human typing speed. Base delay per character plus random variance. The GIF is always sent after the last sentence, as a separate "punchline" moment (0.5s typing indicator).
| Parameter | Value |
|---|---|
| STM size | 15 messages / channel |
| STM TTL | 5 minutes |
| LTM retrieval limit | 5 results |
| Vector similarity threshold | 0.4 |
| Analyst trigger | 50 messages |
| Analyst refresh | 7 days (stale profile) |
| Focus mode duration | 180 seconds |
| Boredom heartbeat | 60 seconds |
| Boredom action cooldown | 180 seconds / channel |
| Channel rename duration | 300 seconds |
| GIF cooldown | Last 5 URLs tracked |
| Dreamer interval | 6 hours |
| Dreamer KNN similarity range | 0.65 - 0.85 |
| Web search results | 3 organic results |
| Orchestrator timeout | 10s (3 attempts) |
| Layer | Technology |
|---|---|
| Platform | Discord (discord.py) |
| LLM — Orchestrator & Boredom | Llama 3.1 8B Instruct |
| LLM — BrainCore | Grok 4.1 Fast |
| LLM — AnalystAgent | DeepSeek V3.2 |
| Embedding Model | all-MiniLM-L6-v2 (384 dim) |
| Relational DB | SQLite (WAL mode, FTS5) |
| Vector DB | ChromaDB |
| Graph DB | Neo4j |
| Web Search | Serper.dev |
| Language | Python (async/await) |
| Configuration | Pydantic + .env |
This document serves as a general architectural reference. Implementation details remain closed source.