|
| 1 | +# Architecture Guide for AI Assistants |
| 2 | + |
| 3 | +This file is designed for AI tools (ChatGPT, Claude, Cursor, Copilot, etc.) to quickly understand the codebase and help users modify or extend the bot. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Project Overview |
| 8 | + |
| 9 | +**Purpose:** Autonomous Twitter bot that generates original content and responds to mentions using LLM. |
| 10 | + |
| 11 | +**Core behavior:** |
| 12 | +- Posts original tweets on a schedule (autoposting) |
| 13 | +- Monitors mentions and replies to selected ones (mention handling) |
| 14 | +- Optionally generates images for posts and replies |
| 15 | +- All content generation is powered by LLM with structured JSON output |
| 16 | + |
| 17 | +**Tech stack:** |
| 18 | +- Python 3.10+ with async/await |
| 19 | +- FastAPI for HTTP server |
| 20 | +- APScheduler for cron-like job scheduling |
| 21 | +- PostgreSQL for persistence (asyncpg driver) |
| 22 | +- OpenRouter API for LLM inference (Claude Sonnet 4.5) |
| 23 | +- OpenRouter API for image generation (Gemini 3 Pro) |
| 24 | +- Twitter API v2 via tweepy |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## File Structure |
| 29 | + |
| 30 | +``` |
| 31 | +main.py # Application entry point |
| 32 | +config/ |
| 33 | + __init__.py |
| 34 | + settings.py # Environment configuration |
| 35 | + personality.py # Bot personality definition |
| 36 | +services/ |
| 37 | + __init__.py |
| 38 | + autopost.py # Autoposting service |
| 39 | + mentions.py # Mention handling service |
| 40 | + llm.py # LLM client |
| 41 | + twitter.py # Twitter API client |
| 42 | + image_gen.py # Image generation service |
| 43 | + database.py # Database operations |
| 44 | +tools/ |
| 45 | + __init__.py |
| 46 | + registry.py # Tool registry for function calling |
| 47 | +assets/ # Reference images for generation |
| 48 | +.env.example # Environment template |
| 49 | +requirements.txt # Python dependencies |
| 50 | +``` |
| 51 | + |
| 52 | +--- |
| 53 | + |
| 54 | +## Detailed File Descriptions |
| 55 | + |
| 56 | +### main.py |
| 57 | +FastAPI application with lifespan management. On startup: |
| 58 | +1. Connects to PostgreSQL database |
| 59 | +2. Initializes AutoPostService and MentionHandler |
| 60 | +3. Schedules two recurring jobs via APScheduler: |
| 61 | + - `autopost_job` — runs every POST_INTERVAL_MINUTES |
| 62 | + - `mentions_job` — runs every MENTIONS_INTERVAL_MINUTES |
| 63 | +4. Starts the scheduler |
| 64 | + |
| 65 | +HTTP endpoints: |
| 66 | +- `GET /health` — health check with scheduler status |
| 67 | +- `GET /callback` — OAuth callback for Twitter authentication |
| 68 | +- `POST /trigger-post` — manually trigger autopost |
| 69 | +- `POST /trigger-mentions` — manually trigger mention processing |
| 70 | + |
| 71 | +### config/settings.py |
| 72 | +Pydantic Settings class that loads configuration from environment variables and `.env` file. All settings are typed and validated on startup. |
| 73 | + |
| 74 | +Key settings: |
| 75 | +- `openrouter_api_key` — API key for OpenRouter (LLM + image gen) |
| 76 | +- `twitter_*` — Twitter API credentials (5 values) |
| 77 | +- `database_url` — PostgreSQL connection string |
| 78 | +- `post_interval_minutes` — autopost frequency (default: 30) |
| 79 | +- `mentions_interval_minutes` — mention check frequency (default: 20) |
| 80 | +- `enable_image_generation` — toggle for image generation (default: true) |
| 81 | + |
| 82 | +### config/personality.py |
| 83 | +Contains `SYSTEM_PROMPT` constant — the complete personality definition for the bot. This prompt is prepended to all LLM calls and defines: |
| 84 | +- Personality traits and character |
| 85 | +- Communication style (tone, grammar, punctuation) |
| 86 | +- Topics to discuss and avoid |
| 87 | +- Behavioral rules (no hashtags, no engagement bait, etc.) |
| 88 | +- Example tweets that capture the vibe |
| 89 | + |
| 90 | +**To change the bot's personality, edit this file.** |
| 91 | + |
| 92 | +### services/autopost.py |
| 93 | +`AutoPostService` class handles scheduled tweet generation. |
| 94 | + |
| 95 | +Flow: |
| 96 | +1. Fetches last 50 posts from database for context |
| 97 | +2. Calls LLM with structured output format requesting `{text, include_picture}` |
| 98 | +3. If `include_picture=true` and image generation enabled, generates image |
| 99 | +4. Posts tweet to Twitter (with optional media) |
| 100 | +5. Saves post to database |
| 101 | + |
| 102 | +Structured output schema: |
| 103 | +```json |
| 104 | +{ |
| 105 | + "text": "string (max 280 chars)", |
| 106 | + "include_picture": "boolean" |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +### services/mentions.py |
| 111 | +`MentionHandler` class processes Twitter mentions. |
| 112 | + |
| 113 | +Flow: |
| 114 | +1. Fetches recent mentions from Twitter API |
| 115 | +2. Filters out already-processed mentions using database |
| 116 | +3. Sends all unprocessed mentions to LLM in single request |
| 117 | +4. LLM selects ONE mention to reply to (or none if all low quality) |
| 118 | +5. LLM returns selection + reply text + image decision |
| 119 | +6. If mention selected, optionally generates image |
| 120 | +7. Posts reply to Twitter |
| 121 | +8. Saves interaction to database |
| 122 | + |
| 123 | +Structured output schema: |
| 124 | +```json |
| 125 | +{ |
| 126 | + "selected_tweet_id": "string (tweet ID to reply to, empty if none)", |
| 127 | + "text": "string (reply text, max 280 chars)", |
| 128 | + "include_picture": "boolean", |
| 129 | + "reasoning": "string (why this mention was selected)" |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**Design decision:** LLM evaluates ALL pending mentions and picks the best one, rather than replying to every mention. This creates more authentic engagement. |
| 134 | + |
| 135 | +### services/llm.py |
| 136 | +`LLMClient` class — async client for OpenRouter API. |
| 137 | + |
| 138 | +Methods: |
| 139 | +- `generate(system, user)` — simple text completion |
| 140 | +- `generate_structured(system, user, response_format)` — JSON structured output |
| 141 | +- `chat(messages, tools)` — chat completion with optional tool calling |
| 142 | + |
| 143 | +Uses `anthropic/claude-sonnet-4.5` model by default (configurable via TEXT_MODEL constant). |
| 144 | + |
| 145 | +### services/twitter.py |
| 146 | +`TwitterClient` class — Twitter API integration using tweepy. |
| 147 | + |
| 148 | +Methods: |
| 149 | +- `post(text, media_ids)` — post new tweet (API v2) |
| 150 | +- `reply(text, reply_to_tweet_id, media_ids)` — reply to tweet (API v2) |
| 151 | +- `upload_media(image_bytes)` — upload image (API v1.1, required for media) |
| 152 | +- `get_me()` — get authenticated user info |
| 153 | +- `get_mentions(since_id)` — fetch mentions (API v2) |
| 154 | + |
| 155 | +Note: Media upload uses API v1.1 because v2 doesn't support it yet. |
| 156 | + |
| 157 | +### services/image_gen.py |
| 158 | +`ImageGenerator` class — image generation via OpenRouter. |
| 159 | + |
| 160 | +Uses `google/gemini-3-pro-image-preview` model. Supports reference images from `assets/` folder for consistent character appearance. |
| 161 | + |
| 162 | +Flow: |
| 163 | +1. Loads reference images from `assets/` folder (up to 2 randomly selected) |
| 164 | +2. Sends reference images + text prompt to model |
| 165 | +3. Receives base64-encoded image in response |
| 166 | +4. Returns raw image bytes |
| 167 | + |
| 168 | +### services/database.py |
| 169 | +`Database` class — async PostgreSQL client using asyncpg. |
| 170 | + |
| 171 | +Tables (auto-created on startup): |
| 172 | +- `posts` — stores all posted tweets |
| 173 | +- `mentions` — stores mention interactions |
| 174 | +- `bot_state` — key-value store for state (e.g., last_mention_id) |
| 175 | + |
| 176 | +Key methods: |
| 177 | +- `get_recent_posts_formatted(limit)` — posts as formatted string for LLM context |
| 178 | +- `save_post(text, tweet_id, include_picture)` — save new post |
| 179 | +- `save_mention(...)` — save mention interaction |
| 180 | +- `mention_exists(tweet_id)` — check if mention already processed |
| 181 | +- `get_state(key)` / `set_state(key, value)` — state management |
| 182 | + |
| 183 | +### tools/registry.py |
| 184 | +Tool registry for LLM function calling. Contains: |
| 185 | +- `TOOLS` — dict mapping tool names to async functions |
| 186 | +- `TOOLS_SCHEMA` — list of JSON schemas in OpenAI function calling format |
| 187 | + |
| 188 | +Currently empty, ready for extension. To add a tool: |
| 189 | +1. Create tool function in `tools/` directory |
| 190 | +2. Import and add to `TOOLS` dict |
| 191 | +3. Add JSON schema to `TOOLS_SCHEMA` list |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## Database Schema |
| 196 | + |
| 197 | +```sql |
| 198 | +CREATE TABLE posts ( |
| 199 | + id SERIAL PRIMARY KEY, |
| 200 | + text TEXT NOT NULL, |
| 201 | + tweet_id VARCHAR(50), |
| 202 | + include_picture BOOLEAN DEFAULT FALSE, |
| 203 | + created_at TIMESTAMP DEFAULT NOW() |
| 204 | +); |
| 205 | + |
| 206 | +CREATE TABLE mentions ( |
| 207 | + id SERIAL PRIMARY KEY, |
| 208 | + tweet_id VARCHAR(50) UNIQUE, |
| 209 | + author_handle VARCHAR(50), |
| 210 | + author_text TEXT, |
| 211 | + our_reply TEXT, |
| 212 | + action VARCHAR(20), -- 'replied', 'ignored', 'tool_used' |
| 213 | + created_at TIMESTAMP DEFAULT NOW() |
| 214 | +); |
| 215 | + |
| 216 | +CREATE TABLE bot_state ( |
| 217 | + key VARCHAR(50) PRIMARY KEY, |
| 218 | + value TEXT, |
| 219 | + updated_at TIMESTAMP DEFAULT NOW() |
| 220 | +); |
| 221 | +``` |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +## Data Flow Diagrams |
| 226 | + |
| 227 | +### Autoposting |
| 228 | +``` |
| 229 | +[Scheduler] |
| 230 | + → AutoPostService.run() |
| 231 | + → Database.get_recent_posts_formatted(50) |
| 232 | + → LLMClient.generate_structured() |
| 233 | + → returns {text, include_picture} |
| 234 | + → [if include_picture] ImageGenerator.generate() |
| 235 | + → [if include_picture] TwitterClient.upload_media() |
| 236 | + → TwitterClient.post() |
| 237 | + → Database.save_post() |
| 238 | +``` |
| 239 | + |
| 240 | +### Mention Handling |
| 241 | +``` |
| 242 | +[Scheduler] |
| 243 | + → MentionHandler.process_mentions_batch() |
| 244 | + → TwitterClient.get_mentions() |
| 245 | + → Database.mention_exists() [filter processed] |
| 246 | + → LLMClient.generate_structured() |
| 247 | + → returns {selected_tweet_id, text, include_picture, reasoning} |
| 248 | + → [if selected] ImageGenerator.generate() [optional] |
| 249 | + → [if selected] TwitterClient.upload_media() [optional] |
| 250 | + → [if selected] TwitterClient.reply() |
| 251 | + → Database.save_mention() |
| 252 | +``` |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## Extension Points |
| 257 | + |
| 258 | +### Adding a new tool |
| 259 | +1. Create `tools/my_tool.py`: |
| 260 | +```python |
| 261 | +async def my_tool(query: str) -> str: |
| 262 | + # Implementation |
| 263 | + return result |
| 264 | +``` |
| 265 | + |
| 266 | +2. Update `tools/registry.py`: |
| 267 | +```python |
| 268 | +from tools.my_tool import my_tool |
| 269 | + |
| 270 | +TOOLS = { |
| 271 | + "my_tool": my_tool |
| 272 | +} |
| 273 | + |
| 274 | +TOOLS_SCHEMA = [ |
| 275 | + { |
| 276 | + "type": "function", |
| 277 | + "function": { |
| 278 | + "name": "my_tool", |
| 279 | + "description": "What this tool does", |
| 280 | + "parameters": { |
| 281 | + "type": "object", |
| 282 | + "properties": { |
| 283 | + "query": {"type": "string", "description": "..."} |
| 284 | + }, |
| 285 | + "required": ["query"] |
| 286 | + } |
| 287 | + } |
| 288 | + } |
| 289 | +] |
| 290 | +``` |
| 291 | + |
| 292 | +3. Update `services/mentions.py` to handle tool execution in the response flow. |
| 293 | + |
| 294 | +### Changing LLM model |
| 295 | +Edit `services/llm.py`, change `TEXT_MODEL` constant to any OpenRouter model ID. |
| 296 | + |
| 297 | +### Changing image model |
| 298 | +Edit `services/image_gen.py`, change `IMAGE_MODEL` constant. |
| 299 | + |
| 300 | +### Adding new scheduled job |
| 301 | +In `main.py` lifespan function: |
| 302 | +```python |
| 303 | +scheduler.add_job( |
| 304 | + my_function, |
| 305 | + "interval", |
| 306 | + minutes=N, |
| 307 | + id="my_job", |
| 308 | + replace_existing=True |
| 309 | +) |
| 310 | +``` |
| 311 | + |
| 312 | +--- |
| 313 | + |
| 314 | +## API Reference |
| 315 | + |
| 316 | +### OpenRouter |
| 317 | +- Base URL: `https://openrouter.ai/api/v1/chat/completions` |
| 318 | +- Auth: Bearer token in Authorization header |
| 319 | +- Models used: |
| 320 | + - `anthropic/claude-sonnet-4.5` — text generation |
| 321 | + - `google/gemini-3-pro-image-preview` — image generation |
| 322 | + |
| 323 | +### Twitter API |
| 324 | +- API v2 for tweets, mentions, user info |
| 325 | +- API v1.1 for media uploads only |
| 326 | +- Auth: OAuth 1.0a (consumer key/secret + access token/secret) |
| 327 | +- Bearer token for read-only operations |
| 328 | + |
| 329 | +### PostgreSQL |
| 330 | +- Connection via asyncpg |
| 331 | +- Connection pooling enabled |
| 332 | +- Tables auto-created on startup if not exist |
0 commit comments