Architecture Overview
System Diagram
Single-Process Architecture
The bot runs as a single process orchestrated by main.py. The async main() function starts components in a specific order:
- Database initialization
- Survey engine loading
- Telegram bot (polling mode)
- APScheduler for timed delivery
- FastAPI HTTP server
Shutdown reverses this order to ensure clean teardown.
Key Components
Survey Engine (src/survey_engine.py)
Loads YAML survey definitions from src/surveys/, resolves composite surveys (which reference other surveys via includes), and supports per-section randomization of question order.
Database (src/database.py)
Uses aiosqlite with WAL mode for concurrent read access. Key tables:
participants-- enrolled study participantssurveys-- survey metadataschedules-- cron-based delivery schedulessurvey_sessions-- active/completed/expired session trackingresponses-- individual question responsescognitive_results-- results from cognitive assessment tasks
Scheduler (src/scheduler.py)
APScheduler handles periodic survey delivery using cron-based schedules. Surveys are pushed to participants at configured times.
API (src/api.py)
FastAPI REST API with all endpoints under /api/v1/. Authentication uses Authorization: Bearer {API_KEY} header (except /health and /cognitive/complete which uses one-time tokens).
Telegram Bot (src/bot.py)
Built on python-telegram-bot with handler groups for priority-based message routing:
- Group 0: ConversationHandler (signup flow), commands, callback query handlers
- Group 1: Survey message handler (separate group to avoid interfering with ConversationHandler)
- Group 2: Fallback handler
WebSocket API (src/api_webapp.py)
WebSocket transport for the PWA frontend. Handles participant registration, WebSocket connections, and survey session management using the same survey logic as the Telegram handler.
Survey Delivery Model
Surveys are pushed to participants by the scheduler or API -- they are not requested by users. Session state is tracked in the survey_sessions table using a state machine:
active → completed
active → expired
Dependency Injection
Multiple modules use a set_globals() pattern for dependency injection. These globals must be set before the module is used, which is why main.py enforces a strict startup order.
Callback Data Encoding
Telegram callback data uses the format s{8-char-session-id}_{payload} (e.g., s1a2b3c4d_3). The 8-character prefix is a truncated uuid4().hex[:8]. This keeps callback data within Telegram's 64-byte limit.