GemAI Bot v2 is an open-source Telegram AI assistant framework focused on resilient real-world bot operations: multi-provider AI routing, streaming recovery, agentic web research, multimodal processing, long-term graph memory, Telegram Mini App surfaces, Docker deployment, and operator tooling.
- Primary maintainer: Le-Who
- Repository: https://github.com/Le-Who/gemaibotv2
- Production branch (checked-in workflows):
vps_testai - License: MIT, see LICENSE.
- Security policy: see SECURITY.md.
- Contributing guide: see CONTRIBUTING.md.
- Maintainer roadmap: see ROADMAP.md.
- Maintainer queue: see docs/MAINTAINER_QUEUE.md.
This is a production-oriented Telegram bot codebase with VPS deployment automation. It includes provider failover, user-facing recovery from model/API failures, authenticated Mini App surfaces, persistent memory and background-task ownership. Current deployment health must be verified separately.
Reviewed against checkout f44541a7 on 2026-09-20. This is a maintained
application with deployment automation; a checkout alone does not establish
current production health, traffic, provider availability or an open-PR count.
Start with the documentation index, agent agreements, architecture and audit findings. The changelog records history, not a current runtime specification.
- Chat with direct OpenAI, Gemini, Opencode, OpenRouter and FreeTheAI routing, model selection, key rotation and recovery from partial responses. Interactive Gemini chat can start a second model after three seconds without text; at most two model routes run at once, and the first to produce text wins. Only the primary attempt races keys. A reserve uses one model and one key; further reserves run sequentially. A slow primary pair may overlap one reserve, with at most three provider calls per routed request. Independent requests and background Daily preparation retain their own execution budgets. One first-text deadline covers all keys and models: 12 seconds for Flash at low/off thinking, 20 at auto/medium, and 30 at high. Inline uses 6 seconds, or 10 with Search, and returns a ready standby immediately. See the latency policy and evidence limits. Router deadlines do not suspend otherwise healthy keys.
- The OpenAI live-answer profile uses Luna Flex without reasoning for ordinary
chat and quick search, and Luna Standard for inline and Crocodile judgement.
A local classifier routes text tasks with multiple constraints/dependencies to
the separate
chat.complexprocess, initially Terra Flex with low reasoning. The dashboard selects the model, reasoning override and Standard/Flex separately. OpenAI reasoning is controlled by the administrator; an absent override uses the model default, and saved user thinking cannot change OpenAI requests. User preferences remain available for Gemini. Flex waits up to 15 seconds for model text, including native GPT-6 web search. Telegram delivery has a separate budget. A Flex capacity error offers a manual retry without switching to Standard. - Quick search (
?), bounded agentic research (??), URL/document/photo understanding and deterministic weather/currency/crypto shortcuts. - Document Q&A preserves the file explicitly selected in
/documents; an unavailable selected file is not silently replaced with the latest upload. PDF/DOCX uploads have a 10 MB processing limit, checked before download when Telegram provides the size. Extracted text is capped at 100,000 characters; a truncated document is explicitly identified in the upload result. - Typed streamed/completed response delivery, preserving final actions; long responses use Redis Reader, optional public Telegraph, or Telegram splitting.
- Inline answers, response tabs, collaborative boards and image generation. Inline text and image paths have their own routing; they are not a fixed promise that a particular model will win a race or meet a latency target.
/drawand natural-language image requests with canvas controls. Gemini native image generation accepts legacy Imagen selections as aliases. Pollinations requires a server-side key and Pollen budget; FreeTheAI has a separate image path.- Voice transcription, intent-aware chat/search routing, queued TTS and Mini App Live Audio. Confirmation depends on the voice flow; it is not universal.
- If a chat response has no first text after a minute, its placeholder offers a safe stop-and-retry action. Optional memory query expansion is limited to eight seconds so slow recall cannot hold up the main response indefinitely.
- Consent-gated private long-term memory, hybrid vector/text recall, provenance-aware graph writes and account-data controls. Group messages are not implicitly saved into private LTM.
/memoryoffers full-text private reading with pagination, return to the list and individual deletion. Expired/deleted facts cannot be reopened; reading does not publish the fact through Reader or Telegraph.- Daily Crocodile, 2048 and trivia. Players can change their daily game from
any of the three Mini App screens or the bot's daily messages; the choice
controls
/dailycrocand their daily subscription. Reopening a completed game in chat shows the player's current result and leaderboard. The admin choice remains the default and is prepared seven days ahead, while the other games are prepared for today and tomorrow. Trivia saves answers in order and calculates ranked scores on the server from the game's pinned question revision. Reopening an unfinished trivia game restores the saved question and score; repeated submissions do not add points. - Scheduled briefings, reminders, tarot and horoscopes. Horoscope settings provide on-demand today/tomorrow readings.
- Feature-gated natal reports with local city lookup, PyEphem planet calculations, local equal-house/angle math and optional LLM interpretation.
- Admin dashboard and daily-content controls, metrics, dependency validation and exact-SHA VPS deployment automation.
The public command/help catalog is app/bot_commands.py. Model defaults below describe code configuration, not independently verified provider entitlements. See Crocodile operations and natal readiness for their boundaries.
- Direct OpenAI uses the official SDK and a separate credential pool. OpenAI-compatible gateways retain their own routing and credentials.
- No ORM: persistence uses asyncpg and numbered SQL.
- Redis absence selects local fallbacks only where implemented; it does not make Reader, deferred delivery or every multi-replica feature equivalent.
- Provider quotas, response time, quality and model access are deployment-specific.
- Private-data erasure cannot retract copies already exported, shared or published to third-party services. Telegraph is disabled by default.
- Natal calculations support equal houses, not Placidus/Koch or a claim of Swiss Ephemeris parity; rollout still needs environment-specific verification.
- Bot Container (
tg-bot): A single async event loop runs both the Telegram webhook updater and the Quart web server via Hypercorn. Webhook mode: Quart receives updates at a derived-hash webhook path, validates the optional secret header, deduplicates updates and enqueues them forUserScopedUpdateProcessor(50). Rejected or backpressured requests do not receive an unconditional success response. Falls back to long-polling ifWEBHOOK_URLis unset. - Local Bot API Server (
tg-api): Self-hostedtelegram-bot-apicontainer (aiogram/telegram-bot-api:latest) communicates with Telegram via MTProto. The bot sends HTTP requests tohttp://tg-api:8081/botinstead ofapi.telegram.org. Shared Docker volume (tg-api-data) enables zero-copy file access for voice/photo/document processing. File limit: 2 GB (vs 50 MB cloud API). Timezone:Europe/Kyiv. - Media Cleanup Cron (
tg-media-cleanup): Alpine-based sidecar container that runs a 60s-tick loop: (1)chmod -R g+rXon the shared volume to fix permission conflicts betweentelegram-bot-api(UID 101) and the bot container (GID 101), and (2) deletes cached media files older than 7 days every 24 hours to prevent disk exhaustion. - Database (PostgreSQL): Source of truth for users, chats, messages, metrics, roles, and pgvector embeddings.
- Cache (Redis): Optional high-speed layer for caching rate limits and transient states.
- Third-Party APIs: Google Gemini (native SDK), Opencode Go (HTTPX), OpenRouter (HTTPX), JINA AI (HTTPX), Tavily (HTTPX).
Docker networking: All three containers share a
tg-netbridge network. The shared volumetg-api-datais mounted at/var/lib/telegram-bot-apiin bothtg-apiandtg-bot. The bot's web server binds to127.0.0.1:$PORTon the host (not0.0.0.0) — Caddy/Nginx reverse proxy is expected in front.
graph TD;
User-->TelegramCloud[Telegram Cloud];
TelegramCloud-->LocalAPI["tg-api<br/>Local Bot API Server<br/>MTProto ↔ REST"];
LocalAPI-->BotHandler["tg-bot<br/>Python Bot + Quart"];
Admin-->QuartServer["Quart Web Server<br/>/metrics, /health, /webapp"];
BotHandler-->ProviderRouter;
ProviderRouter-->Gemini[Google Gemini];
ProviderRouter-->OpencodeGo[Opencode Go];
ProviderRouter-->OpenRouter[OpenRouter];
BotHandler-->Tavily[Tavily Search];
BotHandler-->Cache[(Redis)];
BotHandler-->DB[(PostgreSQL/pgvector)];
QuartServer-->DB;
LocalAPI-.->SharedVolume["tg-api-data<br/>/var/lib/telegram-bot-api<br/>Shared Docker Volume"];
BotHandler-.->SharedVolume;
Cleanup["tg-media-cleanup<br/>Alpine cron<br/>chmod + 7d prune"]-.->SharedVolume;
| Path | Purpose |
|---|---|
app/ |
Core application logic (bot, web server, DB layer, handlers). |
app/handlers/ |
Telegram command and message processors (ai_chat, ai_search, commands, inline). |
app/repos/ |
Database repository pattern implementations (queries for chats, memory, keys). |
app/providers/ |
AI provider abstraction layer (base, Gemini, Opencode, OpenRouter, FreeTheAI, Pollinations, Imagen, TTS, router). |
app/response_delivery/ |
Typed provider-event coordination and the single owner of Telegram response rendering/finalization. |
app/core/ |
Agentic research engine — multi-step query decomposition and tool use. |
app/context/ |
Context assembly subsystem (assembler, summarizer, compression, token budget). |
app/documents/ |
Document processing: chunking strategies, parsers, document repository. |
app/middleware/ |
Request pipeline middleware (debounce aggregation, dedup prevention). |
app/adapters/ |
Distributed/local concurrency primitives. |
app/observability/ |
Bounded log pipeline, correlation, redaction, operational events and incident export. |
ops/observability/ |
Independent private Grafana/Loki/Alloy/Docker-proxy deployment and runbook. |
app/db/ |
Database bootstrap: schema validation, migrations runner, RLS, seed. |
app/utils/ |
Shared utilities (formatting, keyboards, background tasks, image utils, reader SSR, etc.). |
app/templates/ |
HTML Jinja2 templates for admin dashboard and Telegram Mini App. |
app/games/ |
Crocodile, daily 2048/trivia, shared AI authoring and Telegram delivery. |
app/bot_commands.py |
Canonical public command catalog used by /help and Telegram's slash-command menu. |
app/bot_instance.py |
PTB Bot singleton — allows non-PTB code (WebSocket handlers) to call Bot API methods. |
app/web_miniapp.py |
Quart Blueprint for Telegram Mini App (initData auth, memory/settings/game API). |
app/deferred_response.py |
Redis-backed deferred AI generation worker for background retry after total outage. |
app/intent_router.py |
Lightweight LLM-bypass for weather/currency/crypto queries via WeatherAPI.com, ExchangeRate-API & CoinGecko. |
app/state.py |
Process-local user state, local locks and debounced PostgreSQL persistence. |
docs/ |
Extended architectural documentation. |
scripts/migrations/ |
Numbered SQL migration files — single source of truth for all DDL. |
tests/ |
Comprehensive test suite (Unit and Integration). |
bot.py |
Main application entry point uniting Quart and the Telegram updater. |
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Python 3.14-slim | Execution environment |
| Bot Framework | python-telegram-bot |
Async interaction with Telegram APIs |
| Web Server | Quart + Hypercorn | Lightweight dashboard & Prometheus /metrics |
| Database | asyncpg |
High-performance Async PostgreSQL driver |
| Vector DB | pgvector |
Storing and querying semantic memories |
| Data Validation | pydantic |
Configuration and strictly-typed object models |
| Observability | structlog |
Structured JSON logging & context correlation |
- Clone the repository.
- Use Python
>=3.14,<3.15; the Docker image uses the slim variant. Supply PostgreSQL with pgvector available. CI uses PostgreSQL 17 with pgvector. - Install the pinned package manager and synchronize the committed dependency graph:
python -m pip install "uv==0.13.5" uv sync --locked - Create an untracked UTF-8
.envusing the configuration reference below; this checkout does not ship.env.example. The application reads process environment; localuv run --env-file .envor Docker--env-fileloads the file explicitly. - Create PostgreSQL database with
pgvectorextension. Pending numbered SQL migrations fromscripts/migrations/are applied automatically on startup; migration files are kept idempotent so fresh deploys and repeated bootstrap runs remain safe.
Configuration is explicitly read from environment variables in app/config.py and infrastructure modules. The following is an operator reference; load_settings() defines effective defaults. Deployment Secrets and persisted admin overrides can change them. The source inventory tracks literal readers and explicit Docker forwarding; python scripts/check_env_registry.py checks for drift without reading live values.
Important
Variables marked ✅ are required — the application will refuse to start if they are absent. Variables marked ⚙️ are optional and will use the listed defaults.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
TELEGRAM_BOT_TOKEN |
✅ | 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 |
— | Obtained from @BotFather. Must match the running bot. |
ADMIN_ID |
⚙️ | 123456789 |
0 |
Set a real administrator ID for admin features, seeding and natal smoke. The loader permits omission; this does not configure a usable administrator. |
ADMIN_SECRET |
⚙️ | Any secure random string, e.g. openssl rand -hex 24 |
— | Password for the admin web dashboard login form. Also used as encryption seed for stored API keys in the DB. Keep stable across restarts — changing it breaks decryption of stored keys. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
DATABASE_URL |
✅ | postgresql://user:pass@localhost:5432/gemaibotv2 |
— | Standard asyncpg/libpq DSN. Must have the pgvector extension available — the bot won't start without it. Use a DSN reachable from the bot container; localhost inside a container is that container, not the VPS host. |
DB_POOL_MIN_SIZE |
⚙️ | 2—10 |
2 |
Minimum open asyncpg connections. Increase on high-traffic deployments to avoid connection storms. |
DB_POOL_MAX_SIZE |
⚙️ | 10—50 |
10 |
Maximum open asyncpg connections. Keep below the PostgreSQL max_connections limit (default 100). Tune from observed connection pressure and the database connection budget. |
TEST_DATABASE_URL |
⚙️ | Same DSN format, pointing to a test DB | — | Used only during integration test execution (pytest -m integration). Completely isolated from production data. |
RLS deployment boundary: the current startup path uses
DATABASE_URLfor both schema migrations and runtime queries. A PostgreSQL superuser,BYPASSRLSrole, or table owner can bypass ordinaryENABLE ROW LEVEL SECURITYpolicies, so the built-in RLS layer is defense-in-depth rather than a hard tenant boundary when such a DSN is used. Application queries still set a transaction-local tenant context and scope byuser_id. For hard database isolation, first split the current single-DSN bootstrap/runtime path: run migrations and RLS provisioning through a separate owner/migrator, then run the bot through aNOSUPERUSER NOBYPASSRLSnon-owner role with only required DML, sequence, and function privileges. Validate cross-tenant denial against that role before enablingFORCE ROW LEVEL SECURITY; the current code does not yet provide that two-role bootstrap automatically.
Supabase Data API: the bot accesses PostgreSQL directly. The game, natal report and tarot tables protected by migration 079, and the roles catalog, do not grant access to the
anonorauthenticatedAPI roles. A future Data API client requires deliberately reviewed grants and RLS policies; the bot's transaction-local settings are not Supabase Auth IDs. Migration 081 enables RLS on every expected application table before runtime policy setup.
Migrating from Supabase to a local DB:
# 1. Dump from Supabase pg_dump "postgresql://postgres.xxx:PASS@aws-1-eu-north-1.pooler.supabase.com:5432/postgres" \ --no-owner --no-acl -Fc -f backup.dump # 2. Create local DB with pgvector createdb -U postgres gemaibotv2 psql -U postgres gemaibotv2 -c "CREATE EXTENSION IF NOT EXISTS vector;" # 3. Restore data pg_restore -h localhost -U postgres -d gemaibotv2 --no-owner --no-acl -Fc backup.dumpAfter restore, update
DATABASE_URLtopostgresql://postgres:pass@localhost:5432/gemaibotv2and redeploy. Validate restore completeness, extensions, grants and application behavior before switching traffic; keep the backup for recovery.
The VPS deployment uses the isolated Redis stack documented in
Redis operations. Its connection comes from the root-owned
/opt/gemaibot-redis/bot.env, not the legacy GitHub REDIS_URL secret. Redis data,
credentials and local bounded backups survive bot deployments. Local/development
runs still read REDIS_URL normally.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
REDIS_URL |
⚙️ | redis://localhost:6379/0 or rediss://user:pass@host:6379 |
— | Used for: distributed LLM semaphores (multi-replica safety), Telegram Mini App Reader page cache (24h TTL), Gemini image per-key counters. Some locks/counters fall back locally if absent; Redis-backed Reader/deferred features and cross-process coordination need separate validation. Use rediss:// scheme for TLS connections. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
PORT |
⚙️ | 10000 |
10000 |
Port the Quart web server and admin dashboard binds to. On VPS, expose via Caddy/Nginx reverse proxy — do not bind directly to 0.0.0.0 in production. |
ENABLE_WEB_SERVER |
⚙️ | true / false |
true |
Disabling this skips starting the Quart server entirely. Set false only for local dev without the dashboard. |
WEBHOOK_URL |
⚙️ | https://bot.example.com |
— | If set, the bot registers itself as a Telegram Webhook at this URL and stops long-polling. Must be HTTPS. Required for production webhook deployments. If absent, the bot uses long-polling (simpler for single-server setups). |
WEBHOOK_SECRET_TOKEN |
⚙️ | 1–256 ASCII letters, digits, _ or - |
"" |
Optional Telegram secret header. Invalid configured tokens fail validation; when enabled, missing/malformed/mismatched inbound headers are rejected with constant-time comparison for valid strings. |
WEBHOOK_MAX_CONNECTIONS |
⚙️ | Integer | 40 |
Telegram webhook registration clamps the value to 1–100. |
UPDATE_QUEUE_MAXSIZE |
⚙️ | Integer | 1000 |
Bound on all admitted unfinished PTB updates, including fetched updates waiting on processing; released at task_done(). Construction enforces a minimum of 100. |
WEBAPP_BASE_URL |
⚙️ | https://bot.example.com |
"" |
Public URL from which the Telegram Mini App settings panel and reader are served. Must equal WEBHOOK_URL in most deployments. If empty, oversized responses use the optional Telegraph fallback or safe Telegram splitting. |
TELEGRAPH_PUBLICATION_ENABLED |
⚙️ | true / false |
false |
Explicit privacy opt-in for publishing long responses and natal mirrors as public telegra.ph pages. Keep disabled unless users are informed that anyone with the link can read the page. |
MINIAPP_SHORT_NAME |
No | gemaibotv2 |
"" |
Short name for the Telegram Mini App deep links (t.me/<bot>/<short_name>). Configure the matching Mini App short name in BotFather; do not confuse it with a menu button URL. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
NATAL_REPORTS_ENABLED |
⚙️ | true / false |
false |
Enables natal input and hosted report generation. Enable explicitly for a controlled candidate rollout; the deploy readiness and maintenance gates must pass before public release. |
NATAL_REPORT_TTL_DAYS |
⚙️ | 365 |
365 |
Retention window for hosted natal reports. PostgreSQL storage keeps shared links stable by default. |
NATAL_GEOCODER_PROVIDER |
⚙️ | local / nominatim |
local |
Birth-place resolution uses the local GeoNames-backed city catalog. Set nominatim explicitly to allow network fallback for unresolved free text. |
NATAL_CITY_OVERRIDES_PATH |
⚙️ | /srv/bot/natal-city-overrides.json |
"" |
Optional UTF-8 JSON file for admin-reviewed city records missing from GeoNames. Records are loaded into the local autocomplete/geocoding path; see docs/natal-city-overrides.example.json. |
NATAL_SEND_RAW_BIRTH_DATA_TO_LLM |
⚙️ | false |
false |
Privacy guard. Keep false: LLM prompts should receive derived chart data, not raw birth date/place. |
Natal city autocomplete uses GeoNames data via the geonamescache Python package. GeoNames data is licensed under CC BY 4.0 and should be credited in public product materials.
When NATAL_REPORTS_ENABLED=true, the VPS deploy workflow runs configuration,
local city catalog, external offline fixture accuracy, storage and report smoke
checks before maintenance and removal of the preserved previous container. The
same readiness command, from the candidate image with its environment, is:
python scripts/natal_readiness.py \
--check-config --check-storage --require-external \
--reference-fixtures docs/natal-reference-fixture.moira-jpl.json \
--smoke --webhook-url "$WEBHOOK_URL" \
--min-city-count 30000 --max-city-warmup-ms 3000 --max-city-search-ms 300Smoke uses an existing registered ADMIN_ID (or explicit --user-id), invokes
report generation and writes a synthetic report; run it only in an authorized
rollout window. It checks retrieval and hosted HTML rendering from storage. A
manual Telegram/mobile/desktop check must still verify the public HTTPS link.
Live NASA/JPL comparisons use the separate --check-horizons option and are not
a dependency of every deploy. See the natal release contract
for privacy settings, failure recovery and checks the smoke does not cover.
When configured, the bot communicates with a self-hosted Local Bot API Server instead of api.telegram.org. This supports shared-volume media access and larger-file workflows; it does not remove all network or processing latency.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
TELEGRAM_API_ID |
12345678 |
— | From my.telegram.org. Required only when running the Local Bot API Server container. | |
TELEGRAM_API_HASH |
0123456789abcdef... |
— | From my.telegram.org. Required only when running the Local Bot API Server container. | |
TELEGRAM_LOCAL_SERVER_URL |
⚙️ | http://tg-api:8081/bot |
"" |
URL of the Local Bot API Server. When set, enables local_mode=True in PTB. When empty (default), the bot uses the official Telegram cloud API. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
GEMINI_API_KEYS |
✅ | key1,key2,key3 |
— | Comma-separated Google AI Studio API keys. Fresh selection spreads recent traffic across eligible keys before comparing daily usage. Each key has a local daily budget tracked in the DB; Google's actual RPM/TPM/RPD limits are shared per project. Minimum 1 key required. Live Audio no longer consumes this pool. |
GEMINI_AVAILABLE_MODELS |
⚙️ | gemini-3.7-flash,gemini-3.6-flash,gemini-3.5-flash-lite |
gemini-3.6-flash,gemini-3.5-flash-lite |
Exact, ordered Gemini list shown in /model. Future syntactically valid gemini-* IDs are accepted without a code release. Role models such as DEFAULT_MODEL are not added to this selector automatically. Use the single token none for an intentionally empty list. |
DEFAULT_MODEL |
⚙️ | gemini-3.6-flash |
gemini-3.6-flash |
Internal model used for standard conversational messages. It may be intentionally hidden from the user-selectable list. |
QNA_MODEL |
⚙️ | gemini-3.5-flash-lite |
gemini-3.5-flash-lite |
Internal model used for quick Q&A web search queries (? prefix). |
INLINE_MODEL |
⚙️ | gemini-3.5-flash-lite |
gemini-3.5-flash-lite |
Gemini inline role, loaded separately from the selectable chat catalog. |
RESEARCH_MODEL |
⚙️ | gemini-3.6-flash |
gemini-3.6-flash |
Internal model used for synthesizing Tavily search results into a final research answer. |
URL_SELECTION_MODEL |
⚙️ | gemini-3.5-flash-lite |
gemini-3.5-flash-lite |
Lightweight internal model that scores and filters candidate URLs during agentic web research before full content extraction. |
TAXONOMY_MODEL |
⚙️ | gemini-3.5-flash-lite |
gemini-3.5-flash-lite |
Internal model used by MemPalace to classify memories into the Wing/Room taxonomy and to judge temporal contradictions (LLM-as-Judge). |
When VERTEX_AI_KEY is set, Vertex AI Express is used by specialized inline/game paths and eligible chat races; the key carries its own project metadata. Without that key, ordinary chat does not use the service-account credentials mounted for Vertex Live. Explicit Crocodile text-model selection follows the shared Gemini setting described in the operations notes. Live Audio remains separate by default: the default Mini App voice path runs through the Gemini GenAI Live API (gemini-3.8-live), with Rasalgethi first in its 30-voice catalog. Continuous conversation uses automatic speech detection with a 1.5-second silence window and does not automatically interrupt AI replies. A manual turn-ending mode remains available in Live settings on each device. See the Live conversation guide. The experimental Vertex Live route on gemini-live-2.5-flash-native-audio with Google Search grounding is available only when VERTEX_LIVE_ENABLED=true; otherwise it is hidden from settings and its WebSocket rejects connections.
For that experimental live route, VERTEX_AI_PROJECT and VERTEX_AI_LOCATION are not enough by themselves. The bot container must also receive readable ADC credentials, typically by mounting a service-account JSON and exporting GOOGLE_APPLICATION_CREDENTIALS to that in-container path. The included GitHub Actions deploy flow supports this via the VERTEX_LIVE_SERVICE_ACCOUNT_JSON secret and mounts it read-only into the bot container.
A readable service-account file can still refer to an account that no longer
exists. If token refresh reports invalid_grant / account not found, replace
VERTEX_LIVE_SERVICE_ACCOUNT_JSON with a valid account before using the opt-in
Vertex Live route. Ordinary chat uses only VERTEX_AI_KEY for Vertex Express
and does not consume this ADC file.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
VERTEX_AI_KEY |
No | GCP API key string | "" |
Used by configured Vertex Express paths; selection order depends on the caller. This API key is distinct from ADC service-account credentials for the optional Live route. |
VERTEX_AI_PROJECT |
No | my-gcp-project-123 |
"" |
GCP project ID for the regional Vertex Live route; not required by the Express client. |
VERTEX_AI_LOCATION |
No | us-central1 |
us-central1 |
Vertex AI region. Must match where your models are available. |
VERTEX_LIVE_ENABLED |
No | true or false |
false |
Enables the experimental Vertex Live route only after its service-account credentials work. Forwarded from the GitHub Actions repository variable. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
OPENCODE_API_KEYS |
⚙️ | sk-abc,sk-xyz |
[] |
Comma-separated Opencode Go API keys. When set and PRIMARY_PROVIDER=opencode, routes standard chat/search/inline through opencode.ai/zen/go/v1 using Bearer auth. Key rotation works identically to Gemini keys. |
OPENCODE_AVAILABLE_MODELS |
⚙️ | opencode-go/minimax-m2.7,opencode-go/qwen3.5-plus |
Role-model defaults | Exact, ordered Opencode list shown in /model. Use none for an intentionally empty list. |
PRIMARY_PROVIDER |
⚙️ | opencode / gemini / openrouter / freetheai |
opencode |
Legacy default provider selection; persisted /set_provider overrides take precedence. The live-answer profile in /controls pins all covered processes, including existing users, independently of their saved model. Use that profile for a complete OpenAI/Gemini switch. |
OPENAI_API_KEYS |
⚙️ | synthetic-key-one,synthetic-key-two |
[] |
Direct OpenAI credentials, separate from OpenRouter; comma-separated pool with rotation. OPENAI_API_KEY is the local single-key fallback when the pool is empty. Deployment forwards OPENAI_API_KEYS. |
OPENAI_AVAILABLE_MODELS |
⚙️ | openai-direct/gpt-6-luna,openai-direct/gpt-6.1-terra |
Luna + Terra | Exact ordered direct-OpenAI catalog. none keeps an intentionally empty selector catalog without disabling internal process routes. openai/… retains its existing OpenRouter meaning. |
OPENAI_DEFAULT_MODEL |
⚙️ | openai-direct/gpt-6.1-terra |
openai-direct/gpt-6.1-terra |
Initial OpenAI profile for document/image and other live answers. Ordinary chat/search use Luna Flex, complex chat uses Terra Flex, and Crocodile judgement uses Luna Standard. Role overrides also accept bare IDs; saved dashboard plans take precedence. |
OPENAI_QNA_MODEL |
⚙️ | openai-direct/gpt-6.1-terra |
openai-direct/gpt-6.1-terra |
Legacy OpenAI baseline for search. The active OpenAI profile starts with Luna Flex and native web search; saved dashboard plans take precedence. Gateways and legacy models keep Jina retrieval. |
OPENAI_INLINE_MODEL |
⚙️ | openai-direct/gpt-6-luna |
openai-direct/gpt-6-luna |
Initial fast OpenAI profile for inline and short live classifiers. Crocodile answer checking has its own Luna Standard profile default. Daily preparation keeps its existing Gemini routes. |
OPENCODE_DEFAULT_MODEL |
⚙️ | opencode-go/qwen3.5-plus |
opencode-go/qwen3.5-plus |
Default Opencode chat role. |
OPENCODE_QNA_MODEL |
⚙️ | opencode-go/qwen3.6-plus |
opencode-go/qwen3.6-plus |
Model for quick Q&A search synthesis (? prefix). |
OPENCODE_RESEARCH_MODEL |
⚙️ | opencode-go/glm-5.1 |
opencode-go/glm-5.1 |
Configured research-role model included in Opencode selector defaults. It does not switch the specialized ?? Gemini agent loop to Opencode. |
OPENCODE_URL_SELECTION_MODEL |
⚙️ | opencode-go/big-pickle |
opencode-go/big-pickle |
Internal URL-selection role; not automatically added to the selectable model defaults. |
OPENCODE_VISION_MODEL |
⚙️ | opencode-go/mimo-v2-omni |
opencode-go/mimo-v2-omni |
Vision-capable model — supports image_url natively. Exempt from Gemini vision redirect. |
OPENCODE_INLINE_MODEL |
⚙️ | opencode-go/minimax-m2.5 |
opencode-go/minimax-m2.5 |
Lighter model for inline mode generation. |
JINA_API_KEY |
⚙️ | jina_xxx... |
"" |
API key for JINA AI Search. Used as grounding backend for ? quick search when Opencode is active. Also used by agentic research engine for page reading via r.jina.ai. Without this key, Opencode search queries run without web context. |
Model IDs are configurable; this table is not a provider-side availability catalog.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
OPENROUTER_API_KEYS |
⚙️ | sk-or-v1-abc,sk-or-v1-xyz |
[] |
Comma-separated OpenRouter API keys. Required for OpenRouter generation. Rotated same as Gemini keys. Note: OpenRouter is disabled for multimodal (image) requests — Gemini is always used for vision. |
OPENROUTER_AVAILABLE_MODELS |
⚙️ | stepfun/step-3.5-flash:free,qwen/qwen3-4b:free |
OPENROUTER_DEFAULT_MODEL |
Exact, ordered OpenRouter list shown in /model when at least one OpenRouter key is configured. Use none for an intentionally empty list. |
OPENROUTER_DEFAULT_MODEL |
⚙️ | stepfun/step-3.5-flash:free |
stepfun/step-3.5-flash:free |
Default model for standard chat on OpenRouter. |
OPENROUTER_QNA_MODEL |
⚙️ | stepfun/step-3.5-flash:free |
stepfun/step-3.5-flash:free |
OpenRouter model for quick Q&A search synthesis. |
OPENROUTER_RESEARCH_MODEL |
⚙️ | stepfun/step-3.5-flash:free |
stepfun/step-3.5-flash:free |
Research-role mapping in provider-routed use cases (app/agent_use_cases.py); not a replacement for the specialized Gemini agent loop. |
OPENROUTER_URL_SELECTION_MODEL |
⚙️ | stepfun/step-3.5-flash:free |
stepfun/step-3.5-flash:free |
OpenRouter model for URL scoring during agentic research. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
FREETHEAI_API_KEYS |
⚙️ | sk-freetheai-1,sk-freetheai-2 |
[] |
Comma-separated API keys for FreeTheAI. Provides advanced multimodal image generation and Lyria-based audio music models. |
FREETHEAI_AVAILABLE_MODELS |
⚙️ | cat/claude-4-6-sonnet,yng/deepseek-r1 |
FREETHEAI_DEFAULT_MODEL |
Exact, ordered FreeTheAI chat-model list shown in /model. Image/audio IDs are not selectable here. Use none for an intentionally empty list. |
FREETHEAI_DEFAULT_MODEL |
⚙️ | cat/claude-4-6-sonnet |
cat/claude-4-6-sonnet |
Default chat model via FreeTheAI. |
For every *_AVAILABLE_MODELS variable, an unset or whitespace-only value uses
the provider defaults; a space is therefore not a way to clear the list.
Set the value to the single case-insensitive token none to make that
provider's user-selectable list empty. /models changes create an explicit
database override, which takes precedence over env/Secrets across restarts.
The provider view shows the active source. Reset to .env deletes that
override and immediately reloads the current env/Secret value. Legacy
automatically seeded DB lists are discarded so they cannot mask a newly
deployed Secret.
Note:
JINA_API_KEYis documented in the Opencode Go Models section above — it serves dual purpose as both Opencode search grounding and agentic page reader.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
TAVILY_API_KEYS |
✅ | tvly-key1,tvly-key2 |
— | Comma-separated Tavily Search API keys. Used by search/research services; quick-search grounding depends on the active provider. Credit usage is tracked by the application. |
WEATHER_API_KEY |
No | abc123... |
"" |
API key for WeatherAPI.com. Powers intent-direct weather query handler — weather queries intercepted before reaching the LLM. Without this key they fall through to the LLM. |
EXCHANGE_RATE_API_KEY |
No | abc123... |
"" |
API key for ExchangeRate-API.com. Powers intent-direct currency conversion queries. Without this key currency queries fall through to the LLM. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
AGENTIC_MODEL |
⚙️ | Gemini model ID | "" (falls back to RESEARCH_MODEL) |
Used when neither a handler model override nor the current chat model is set. It does not override an explicit chat selection. |
AGENTIC_MAX_ITERATIONS |
⚙️ | 5—15 |
5 |
Maximum research loop cycles before the agent is forced to synthesize an answer. Each iteration = one round of query → search → read → reflect. Higher = deeper research, higher API cost. |
AGENTIC_MAX_PAGES |
⚙️ | 3—10 |
3 |
Shared admission limit on read_page across the research fallback-chain. Slots are reserved before a batch executes. |
AGENTIC_MAX_TOKENS |
⚙️ | 100000—500000 |
100000 |
Known usage plus reservations for missing usage gate the next iteration/fallback. An admitted call and final synthesis may overshoot; not a billing ceiling. |
AGENTIC_TIMEOUT_SECONDS |
⚙️ | 90—300 |
90 |
Shared cooperative async deadline for research attempts/tools/fallback, with min(15 seconds, timeout / 6) reserved for synthesis. Cleanup may add time; initial key lookup and final Telegram delivery are outside the interval. |
AGENTIC_PAGE_CONTENT_LIMIT |
⚙️ | 4096—16384 |
8192 |
Maximum characters extracted from each web page before truncation. Higher = more context per page, more LLM tokens consumed. |
ADAPTIVE_THINKING_ENABLED |
⚙️ | true / false |
true |
Enables the Gemini chat thinking resolver: explicit user level/off, then model-specific defaults, then the heuristic classifier. OpenAI uses administrator reasoning or the model default; automatic complex-chat routing is separate. |
The agentic loop in app/core/agentic.py uses the Gemini SDK directly. Its handler
chooses model_override → chat_state.model → AGENTIC_MODEL → RESEARCH_MODEL;
selection alone does not guarantee that a model has a usable Gemini key/capability.
The handler shares one budget across at most three agent runs. Missing usage
reserves 4,000 tokens per call for admission and is reported as uncertain, never
as confirmed spend. A synthesized answer after a cutoff carries a visible limit
notice. Ordinary
provider-routed chat/search and specialized research execution have distinct paths.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
DAILY_LIMITS |
⚙️ | JSON model-to-limit map or comma-separated model:limit entries |
Model-specific map in app/config.py |
Local Gemini RPD budgets per key and model, reserved atomically in PostgreSQL before dispatch, including TTS. Reset at midnight Pacific Time. Admin model-limit overrides take precedence; models without a configured cap still accumulate usage. |
MAX_CONCURRENT_HEAVY_REQUESTS |
⚙️ | 4—32 |
4 |
Global asyncio semaphore limiting simultaneously active LLM/TTS requests. Backed by Redis for multi-replica safety. Tune against measured load, provider quota and memory limits. |
MAX_CONCURRENT_ULTRA_HEAVY_REQUESTS |
⚙️ | 1—8 |
1 |
Separate semaphore for agentic research (??) sessions. These are memory-intensive due to iterative context accumulation. Tune against measured resource use. |
LRU_STATE_CACHE_SIZE |
⚙️ | 1000—50000 |
1000 |
Maximum number of UserState objects held in the in-process LRU cache. Tune from measured state size and cache misses. |
DB_POOL_MIN_SIZE |
⚙️ | 2—10 |
2 |
See Database section. |
DB_POOL_MAX_SIZE |
⚙️ | 10—50 |
10 |
See Database section. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
IMAGE_MODELS |
⚙️ | Comma-separated IDs/aliases | flux,zimage,gptimage-1-5,gptimage,qwen-image,wan-image,klein |
Configured canvas/inline selections. Daily admin additionally loads the live image catalog; saved selections are retained if unavailable. |
DEFAULT_IMAGE_MODEL |
⚙️ | zimage |
zimage |
Which Pollinations model is pre-selected by default in the Canvas keyboard. |
POLLINATIONS_API_KEY |
⚙️ | sk_... |
— | Required for all Pollinations generation. Keep server-side; manage keys and Pollen budget at enter.pollinations.ai. Runtime /keys overrides remain supported. |
IMAGE_GEN_DAILY_LIMIT |
⚙️ | 10—100 |
10 |
Per-user daily cap for Gemini native image generations via Google API. Counted separately from Pollinations. |
IMAGE_GEN_RPD_PER_KEY |
⚙️ | 25 |
25 |
Requests-per-day budget per Gemini API key for native image generation. This is a local budget, not a provider entitlement. Tracked in Redis with in-memory fallback. Prevents image quota from consuming keys needed for LLM chat. |
IMAGE_GEN_TIMEOUT |
⚙️ | 30.0—120.0 |
60.0 |
Max seconds to wait for a Gemini native image API response before timing out and rotating to the next key. |
IMAGE_GEN_MAX_RETRIES |
⚙️ | 1—5 |
3 |
Number of Gemini key rotation attempts on quota/error before failing image generation entirely. |
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
ELEVENLABS_API_KEYS |
⚙️ | sk_abc,sk_xyz |
[] |
Comma-separated outbound TTS keys, tried in order; failed/exhausted keys are skipped for remaining chunks of that synthesis. If the pipeline cannot finish, the voice engine can fall back to Gemini TTS for the entire response. Provider-side quotas are not defined by this repository. |
ELEVENLABS_VOICE_ID |
⚙️ | XB0fDUnXU5powFXDhCwa |
XB0fDUnXU5powFXDhCwa |
Baseline ElevenLabs voice; the admin catalog and an active user preference take precedence. Default is Charlotte. Browse voices at elevenlabs.io/voice-library. |
ELEVENLABS_MODEL |
⚙️ | eleven_multilingual_v2 |
eleven_multilingual_v2 |
Model passed to the ElevenLabs synthesis path. |
GEMINI_TTS_RPM_PER_KEY |
⚙️ | Positive integer | 3 |
Local rolling 60-second TTS request budget per key and model. Redis coordinates replicas; without Redis the window is process-local. Set against the independent projects' AI Studio quotas. Forwarded by the VPS workflow; restart required. |
The admin dashboard's Voices and TTS section (/controls#audio) manages voice
IDs, names, ordering, defaults, Gemini voice prompts and synthesis parameters.
The Mini App and voice engine consume the same catalogs. Model chains and provider
order remain in Model routes, and shared pronunciation/style/pacing prompts
remain in Prompts. The default Gemini TTS chain is
gemini-3.8-flash-lite-tts → gemini-3.8-flash-tts; admin policies can
override it. See voice controls and model inventory
for parameters, precedence and the separate ASR and Live boundaries.
Explicit recitation such as Озвучь: ваш текст or Read aloud: your text
goes straight to the speech queue without a text-model call or a text echo.
Replying Озвучь это сообщение selects the replied-to text/caption. A forwarded
message plus an authored recitation request in the same batch selects its raw
text. Missing or competing sources produce a clarification. Requests to generate
an answer and speak it retain the chat route. See the
speech request guide for examples and limits.
Long direct recitation retains each original Telegram message: full sources get separate audio replies in source order. A small final continuation can join the preceding source when their combined input fits one request; the tail receives an explanatory reply. Collection waits visibly for possible continuations; repeated text in different message IDs remains part of the source. See the speech guide for the bounded collection window and ambiguous final parts.
Gemini TTS counts the transcript and speech metadata with the selected TTS model's
countTokens endpoint. Fitting input stays whole; verified input overflow or
reported output truncation triggers necessary pieces, synthesized sequentially
into one file for that source. A matching scoped count receipt prevents counting
a merged input again in the worker. The old 2621-byte/duration estimate is removed.
Key rotation is sequential; duplicate key races are disabled.
Explicitly incomplete or token-limited output is discarded and retried with
smaller pieces within bounded limits. A failed source never delivers partial audio.
ElevenLabs also sends a whole transcript when it fits the selected model, splitting
only at known model character limits (10,000 for Multilingual v2, 40,000 for Flash
v2.5). Separate requests can differ in prosody; those necessary long-form joins
retain neighbouring text context in ElevenLabs. The queue's 64 KiB UTF-8 source
limit is separate from Gemini's per-call token budget. Audio validation detects
reported truncation and malformed data; it does not verify every spoken word.
RPM admission precedes atomic RPD admission. An unsent minute reservation is refunded; dispatched requests retain RPM/RPD usage on errors and cancellation. SDK retries are disabled for TTS so each reservation covers one HTTP attempt. Keys are chosen from fresh least-used ordering, with exhausted keys skipped across the whole pool. If all usable keys are minute-limited, the job waits under its deadline. RPD values come from configured model budgets, not automatic discovery of provider account limits; missing caps rely on provider quota errors and rotation.
These tables cover supported operator settings, not every environment read in the
repository. A runtime setting is not automatically forwarded by the VPS workflow:
for example, INLINE_MODEL, OPENCODE_URL_SELECTION_MODEL, ELEVENLABS_MODEL,
WEBHOOK_MAX_CONNECTIONS and UPDATE_QUEUE_MAXSIZE are loaded by code but are not
currently passed through that workflow. Adding a GitHub Secret alone does not
activate them. Conversely, legacy USE_OPENROUTER and
MAX_CONCURRENT_HEAVY_CALLBACKS are forwarded but not loaded by load_settings();
the latter currently uses the handler's fallback of 4. Provider selection uses
PRIMARY_PROVIDER and its persisted override. See the audit.
The default is one correlated UTF-8 NDJSON stream with structured exceptions,
request/trace/job IDs, release metadata and bounded non-blocking output. Every
actually selected provider key is represented by its last four characters plus
a fingerprint on both request and terminal/error events; full credentials are
always scrubbed. Protected operational logs default to bounded scrubbed message
text (LOG_CONTENT_MODE=full, up to 2048 characters) plus size/fingerprint
metadata. Operators can explicitly switch to metadata, while preview remains
a selector-scoped diagnostic mode. Message-bearing rows are restricted evidence,
not CI/public/alert payloads; DEBUG never relaxes credential rules.
Use canonical LOG_FORMAT=json|text; STRUCTURED_LOGGING and LOG_PRETTY are
compatibility aliases. Production deploys private Grafana/Loki/Alloy search with
100-row/15-minute browser defaults and a 500-row server ceiling; access is through
an SSH tunnel. See the operator/agent runbook,
log viewer runbook, event catalog,
and dated audit.
| Variable | Required | Format / Example | Default | Notes |
|---|---|---|---|---|
TEST_DATABASE_URL |
⚙️ | postgresql://user:pass@localhost:5432/test_db |
— | Postgres connection string used exclusively by pytest -m integration. Must point to a clean, separate database — integration tests run destructive DDL and DML. Never set this to the production DB. |
Local Python (dev, long-polling):
uv run --locked --env-file .env python bot.pyDocker Compose (legacy local, single container):
docker compose -f docker-compose.yml up -dProduction VPS (application containers plus private log-search stack via GitHub Actions CI/CD):
The canonical deployment is automated by .github/workflows/deploy.yml. It starts
after a successful completed CI run for a push to this repository's vps_testai
branch. Before checkout, and again before preparing SSH secrets, it verifies that
workflow_run.head_sha is still the current branch tip. PR runs, foreign source
repositories and reruns of superseded commits cannot authorize deployment. It
builds that exact SHA-tagged image in GHCR and then SSH-deploys the application
containers.
CI requires lint/format and documentation checks, Mypy, unit/E2E tests, isolated
PostgreSQL/Redis migration and integration tests, Chromium regressions, and a
production-image build/smoke. These jobs run independently; Python dependencies
use a lockfile-keyed uv cache, and image builds share a BuildKit cache with deploy.
The separate CI Advisory workflow runs on the same pushes and PRs. It retains
the production dependency audit, SBOM/license artifacts and disposable private
log-stack checks. Its failures remain visible and require investigation, but its
completion and result do not block application deployment. Dependency inventories
are generated before vulnerability auditing and uploaded even when that audit
fails. Deploy-time migration, readiness, authentication and health gates remain
mandatory.
The application deployment contains:
tg-api— Local Telegram Bot API Server (aiogram/telegram-bot-api:latest). RequiresTELEGRAM_API_IDandTELEGRAM_API_HASHfrom my.telegram.org. Before starting or reusing the local API server, each deployment runsscripts/release_cloud_bot_api.py; the script safely releases any cloud Bot API session without relying on a stale host marker.tg-bot— The Python bot container. Connected totg-apiviaTELEGRAM_LOCAL_SERVER_URL=http://tg-api:8081/bot. Mounts the shared volumetg-api-dataat/var/lib/telegram-bot-apifor zero-copy file access.tg-media-cleanup— Alpine cron sidecar. Runschmod -R g+rXevery 60s (fixes UID 101 permission conflicts) andfind -mtime +7 -deleteevery 24h (prevents disk exhaustion from cached media).
The application containers share the tg-net Docker bridge network and
TZ=Europe/Kyiv. The same successful deployment starts an independent,
resource-bounded Grafana/Loki/Alloy/Docker-proxy project before replacing
tg-bot; Grafana listens only on VPS 127.0.0.1:3000.
If VERTEX_LIVE_SERVICE_ACCOUNT_JSON is configured in GitHub Actions secrets, the deploy script also writes that service-account JSON to the VPS, mounts it into tg-bot as /run/secrets/vertex-live-sa.json, and exports GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/vertex-live-sa.json. The mounted file must remain readable by the non-root bot process; the workflow now prepares it with read permissions suitable for the container user instead of root-only mode.
For a manual deployment, adapt the checked-in workflow for the target host and
verified image SHA. The workflow is the complete reference for credentials,
migration ordering, health gates, media retention and eligible rollback; a short
docker run example is not equivalent.
All database DDL is managed via numbered SQL migration files in scripts/migrations/.
| Component | Role |
|---|---|
scripts/migrations/000_init_schema.sql |
Initial bootstrap only; later migrations are required for the current schema |
scripts/migrations/001-019_*.sql |
Incremental schema changes (ALTER, indexes, RLS, triggers, cleanup) |
scripts/migrations/020_add_trgm_hybrid_search.sql |
Enables pg_trgm extension + GIN index for hybrid keyword+semantic memory search |
scripts/migrations/024_upgrade_gemini_v2_768.sql |
Migrates embeddings to halfvec(768) (gemini-embedding-2-preview) |
scripts/migrations/025_add_temporal_graph_edges.sql |
Adds updated_at + unique constraint to memory_edges for temporal upserts |
scripts/migrations/026_add_core_persona_edges.sql |
Adds is_core BOOLEAN + partial index to memory_edges for Core Persona Protection |
scripts/migrations/026b_add_predicate_embedding.sql |
Adds predicate_embedding halfvec(768) + HNSW index for Semantic Edge Deduplication |
scripts/migrations/027_add_edge_provenance.sql |
Adds source_memory_ids BIGINT[] + GIN index to memory_edges for HippoRAG 2 Dual-Node provenance |
scripts/migrations/032_add_wing_room_taxonomy.sql |
Adds wing, room, hall_type to long_term_memory and wing, room to memory_nodes with B-tree + partial HNSW indexes |
scripts/migrations/033_add_role_diaries.sql |
Adds role_diaries JSONB DEFAULT '{}' to user_state for MemPalace persistent role diaries |
scripts/migrations/034_global_settings.sql |
Creates global_settings key-value table for runtime configuration; seeds inline_thinking_level default |
scripts/migrations/018_add_missing_table_definitions.sql |
Backfill migration for databases that applied 000 without all tables |
scripts/migrate.py |
Standalone runner with strict manifest validation; deployment applies pending versions before starting the candidate. |
app/db/migrations.py |
Internal migration framework. Stops before legacy DDL when any numbered migration fails. |
app/db/schema.py |
Fail-closed startup validation — verifies all expected tables and critical runtime columns after migrations |
app/db/rls.py |
Row Level Security policy management |
app/db/seed.py |
Initial data seeding (admin user, API keys, indexes) |
Workflow:
- Deploy-time:
scripts/migrate.pyruns in an ephemeral container. If any migration fails, deployment aborts. - CI: apply the complete migration chain to clean pgvector PostgreSQL, rerun it as a no-op, then require
--checkto report no pending migration versions. This is not a checksum or full-schema drift audit. - Startup-time:
init_db()->run_migrations()->validate_schema()->setup_row_level_security()-> seed. Any numbered migration, schema, or RLS failure aborts startup.
Diagnostic caveat: even scripts/migrate.py --check and --status may create the migration-tracking table. They use DATABASE_URL, not TEST_DATABASE_URL; never run them against production as casual read-only checks.
Adding new tables: Create a new numbered .sql file in scripts/migrations/, add the table name to EXPECTED_TABLES in app/db/schema.py, and add RLS configuration to app/db/rls.py if needed.
LTM is implemented in app/repos/memory*.py, not app/memory_manager.py.
It combines pgvector halfvec(768), hybrid text/vector retrieval, graph traversal
and tiered context. Current model names, thresholds and budgets are defined in
app/repos/memory_config.py and the relevant retrieval/handler code, not fixed
latency or recall-quality guarantees.
Private-memory work carries a durable consent epoch and renewable provider lease. Disabling LTM or erasing an account invalidates stale work. Group messages are not implicitly captured into private LTM. Retrieval treats remembered content as data, not trusted instructions.
Extraction and consolidation prepare provider results outside the write transaction,
then share a caller-owned, tenant-bound graph writer. Durable source memories,
nodes/edges, normalized memory_edge_sources and compatibility snapshots are
committed consistently. Cleanup follows live provenance so unsupported edges do
not remain usable. See ADR 0002.
Use /memory, /clearmemory, /settings, /mydata and the confirmed private-chat
/deleteme flow for user controls. Do not substitute ad-hoc SQL deletion for the
repository workflows or claim these controls establish legal compliance.
Exports and third-party published copies are outside automatic local erasure.
| Command | Purpose |
|---|---|
uv run --locked ruff check . |
Run the repository-wide lint rules. |
uv run --locked ruff format --check . |
Enforce the same formatting gate used by CI. |
uv run --locked mypy app bot.py |
Type-check the production Python surface. |
uv run --locked python scripts/check_encoding.py |
Detect UTF-8 corruption in protected documentation. |
uv export --locked --no-dev --output-file production-requirements.txt |
Export the exact production graph for external audit tools. |
pyproject.toml is the only manually edited dependency manifest and uv.lock is the committed exact graph. Use the pinned tool version for every lock operation:
python -m pip install "uv==0.13.5"
uv lock --check
uv sync --locked
uv run --locked python scripts/dependency_frontier.py auditThe Dependency Frontier Audit workflow wakes weekly but a fixed UTC epoch gate permits scheduled resolution only every 14 days; a manual dispatch runs immediately. It resolves the policy-constrained and unconstrained stable frontiers twice for Linux production and Windows development with a seven-day release cooldown. Its report is discovery evidence, not proof of compatibility. Dependabot groups post-1.0 patch/minor updates, while major and current 0.x updates remain separate and are never auto-merged.
The manual Dependency Live Canary workflow accepts a same-repository dependency PR that changes only pyproject.toml and uv.lock. Before the protected job is entered, an unprivileged job rejects non-PyPI/direct sources, disables source builds, installs both locked graphs, and verifies exact environment parity. Both checkouts discard persisted Git credentials. The protected job repeats the wheel-only locked installation without secrets; dedicated credentials exist only in the probe step. The dependency-canary GitHub Environment must require a reviewer and provide dedicated, low-quota credentials:
| Environment entry | Purpose |
|---|---|
CANARY_TELEGRAM_BOT_TOKEN |
Dedicated bot that can access only the canary chat. |
CANARY_TELEGRAM_CHAT_ID |
Dedicated chat where one silent tagged message is sent and deleted. |
CANARY_GEMINI_API_KEY |
Revocable, low-quota Gemini canary key. |
CANARY_TAVILY_API_KEY |
Revocable, low-quota Tavily canary key. |
CANARY_GEMINI_MODEL |
Optional environment variable; defaults to gemini-2.5-flash. |
Baseline and candidate probes run in one approved window. Missing credentials, cleanup failure, baseline failure, transient provider ambiguity, cancellation, or any skipped comparison cannot publish a passing candidate status.
The dedicated canary is optional infrastructure, not a substitute for production verification. If no separate canary bot exists, do not treat that gate as passed or reuse the production bot token in it. For a deliberate production dependency trial, keep the dependency changes in one reviewed PR so CI and the running bot exercise the exact combined graph. The deploy workflow then:
- serializes production transitions without cancelling an in-flight deploy;
- proves that the successful CI commit belongs to exactly one merged same-repository PR;
- allows automatic rollback only when both dependency manifests and an explicit set of non-runtime review files changed;
- preserves the current
tg-botcontainer and independently verifies that its image SHA equals the PR base SHA; - starts the candidate, checks
/health, runs enabled natal configuration/city/external-fixture/storage/report-smoke gates and maintenance, and only then deletes the preserved container; - restores and health-checks the previous container after an eligible candidate failure while leaving the deployment failed for investigation.
This rollback protects startup and health-gated failures. It cannot detect a semantically wrong provider response after /health succeeds, and it deliberately refuses automatic rollback when code or migrations changed. After deploying a frontier trial, exercise the real bot during a controlled low-traffic window:
- ordinary text in both streaming and non-streaming paths;
- provider fallback plus agentic/web search;
- photo and document understanding;
- voice transcription and TTS;
/drawimage generation and/liveaudio when enabled;- memory write/read, a Redis-backed callback or game action, and an admin/background test send;
- structured logs, provider error rates, duplicate replies, stuck streams, and dropped updates.
Any functional regression, schema/type error from a provider, unexplained error-rate increase, duplicate delivery, or stalled response rejects the candidate even when all automated checks passed. Keep the PR under observation for 24–48 hours; a post-health functional failure requires an explicit redeploy of the recorded base SHA because the workflow has no reliable signal for automatic application-level rollback.
Telegram Commands:
- Public command catalog (the same catalog powers Telegram's menu and
/help):- Start and chat:
/start,/help,/newchat. - Personalization:
/model,/roles,/setprompt,/thinking,/settings. - Search and briefings:
/restoggles web search;/subscribeand/unsubscribemanage the daily intelligence briefing checked by the hourly scheduler. - Documents, images, and voice:
/documents,/draw,/live. Image-generation aliases/img,/image, and/generateremain accepted but are not duplicated in the public menu. - Conversation history:
/save,/conversations,/switch,/rename,/delete,/export,/stats. - Memory and privacy:
/memory,/clearmemory,/mydata,/deleteme. Private-data commands require a direct chat; irreversible deletion requires explicit confirmation. - Games and reminders:
/games,/dailycroc,/daily2048,/trivia,/remind. - Tarot and astrology:
/tarot,/natal,/horoscope_settings,/horoscope_stop.
- Start and chat:
Administrative and developer commands are deliberately excluded from app/bot_commands.py and Telegram's public slash-command menu.
- Admin Commands (Requires
ADMIN_ID):/admin— Central administration hub./listmodels,/listusers— List configured models and registered users./adduser,/deluser— Manual user management./metrics,/rolemetrics— Detailed system and role-based usage telemetry./cachestats,/queuestats,/docstats,/groupstats— Performance monitoring for different subsystems./clearcache,/clearoldmetrics,/clearolddocs— System maintenance and cleanup./clearcacheremoves search results from local memory and theqna/searchRedis namespaces; it preserves queues, locks and Reader content. Backend failure is reported instead of confirming a successful clear./updatetavilykeys,/checktavilykeys— Hot-swap and verify search API keys./checkgeminikeys— Async parallel health check of all Gemini API keys against the Google API./set_inline_thinking <level>— Set inline generationthinking_levelat runtime (stored inglobal_settingsDB table, no restart required). Valid:minimal,low,medium,high./set_provider <name>— Switch the primary LLM provider at runtime without restart. Valid:opencode,gemini,openrouter,freetheai. Stored inglobal_settings; individual generation paths still apply their capability and key checks./models— Runtime model management wizard for Gemini, Opencode, OpenRouter, and FreeTheAI. Add or remove individual selectable models, inspect whether env or an admin override is active, and reset by deleting the override and restoring the current env/Secret list. New Gemini additions are checked forgenerateContentsupport before persistence./registergroup— Authorize the bot for use in a specific Telegram group./reloadconfig— Trigger an immediate hot-reload of the environment configuration.
Web Dashboard (Quart HTTP Routes):
GET /,GET /login,POST /login,GET /logout— UI interface (requiresADMIN_SECRETauthentication and uses Cookie Sessions).GET /health— Unauthenticated process/database status: 200 when the database connection flag is healthy, 503 when it is not, 500 on an internal check error. Redis status is reported but does not determine the overall status. This does not probe Telegram or AI providers.GET /ready— The same bounded status probe, additionally requiring configured, reachable Redis. VPS deployment uses this endpoint so unavailable queue/quota coordination cannot pass candidate readiness.GET /metrics— Unauthenticated, rate-limited Prometheus telemetry text. Restrict exposure at the reverse proxy when required by the deployment.GET /api/dashboard— Aggregated batch endpoint (replaces 8 individual fetches with 1 RTT). Auth required.GET /api/overview,/api/keys,/api/errors,/api/cache,/api/queue,/api/database,/api/circuit-breakers,/api/memory— Individual JSON data endpoints for dashboard charts (requires auth cookie orX-Auth-Tokenheader).GET /api/key-health— Per-key health diagnostics: status, failure count, suspension info. Auth required.GET /api/events— Authenticated Server-Sent Events stream (5s interval) for real-time CPU, memory, queue, and request metrics. Service-health probe results come from/api/dashboard./controls#queues— Shared job status, bounded pagination, revision-checked cancellation/retry and separate admission/execution pause controls. Unsupported PTB, TaskManager and TTS actions remain read-only. See the queue lifecycle and cutover runbook./admin_daily#jobs— Read-only eight-day readiness calendar and tracked Crocodile, Trivia and Tarot operations. Job admission uses an operation ID; lost responses are reconciled through status lookup without automatically submitting another generation. Model/prompt edits use revision checks and retain conflicting drafts./admin_daily#broadcast— Recipient preview, tracked offer batches and delivery history. Confirmed delivery, known failure and unknown acknowledgment remain distinct; retry excludes sent and uncertain recipients. Offers and actual daily deliveries have separate counters and an explicit history start. See the broadcast runbook.
The dashboard separates completed user operations from recorded outbound API attempts. Select today, the last 7 or the last 30 calendar days in Europe/Kyiv; the panel shows exact boundaries and marks API periods preceding instrumentation as partial. Gemini model groups start collapsed, support model/masked-key search and sorting, and show per-key local RPD usage when expanded. These quotas retain Pacific Time day boundaries. See metrics and dashboard for counter ownership, historical limits, recovery and performance metric scopes. Stage percentiles use a bounded 15-minute process window and show sample counts; source cards distinguish fresh, stale, partial and unavailable data. Dashboard HTTP and SSE readers share source refreshes. The bot lifecycle comes from the supervisor; a live web connection alone does not mean Telegram processing is ready.
- Start with
/helpfor the current categorized RU/EN public catalog. - In private chat, standalone
натальнаяopens the natal menu,тароorраскладopens Tarot, andгороскопopens the horoscope menu without a slash. Case, surrounding whitespace and final?!.are accepted. These exact menu words also switch out of an active input form; ordinary phrases retain normal chat routing. - Standard chat combines selected model, history and consent-gated memory.
- An idle Tarot session offers a choice to continue Tarot or send the text as
an ordinary request, including the first message after state restoration.
A successful model selection,
/newchat, normal/start, or the new-chat/ new-topic buttons end the Tarot session and remove its reply keyboard. ?requests quick search;??requests bounded agentic research.- Enable inline mode in BotFather and use the deployed bot's username. Deferred AI answers also require chosen-result feedback; immediate compatibility results work without it.
- Inline compatibility accepts
@<bot_username> совместимость мужчина скорпион женщина водолей,совм скорпион водолейandcompatibility man scorpio woman aquarius. The result has adjacent «Подробнее о паре» and Tarot buttons. The first opens private chat with one «Заполнить данные пары» button for the compatibility Mini App when an HTTPSWEBAPP_BASE_URL(or webhook base) is configured. The form collects two dates, optional exact/approximate/range times and local-catalog cities, then provides editable confirmation and one calculation button. Known time requires a city; each partner may independently choose unknown time and skip birthplace. With no HTTPS Mini App base, the existing private two-date chat flow remains. In private chat,/compatibility,/совместимость,совместимостьandсовмopen a two-sign picker with the same instant brief reading and a detailed-reading entry./compatibilityis also listed in Telegram's command menu and/help. Dates are supported from 1900 through today. Local synastry reuses natal astronomy for inter-chart aspects, Ascendant/MC and equal-house overlays; unknown-time aspects and placements are shown only when stable across the sampled interval. Missing time for one partner preserves the other's houses. Detailed results from both the Mini App and the chat date flow pass these local facts to AI for a connected relationship interpretation. The model receives only derived positions, aspects, symbolic matrix data and accuracy limits, without raw dates, times or birthplaces. Calculation-owned limits are appended separately; an unavailable model produces a retry notice rather than a template reading. Processcompatibilityand promptcompatibility.interpretationuse the existing runtime model and prompt controls; the default model follows Tarot chat (gemini-3.1-flash-lite). Brief sign compatibility remains entirely local. Signed and currently authorized Mini App identity determines private delivery; results are not publicly hosted. Birth input is not persisted or sent to an interpretation provider. The chat fallback clears raw dates on completion, cancellation, mode changes or after 30 minutes; contextual Tarot stores derived positions only in its existing bounded 30-minute context. Readings are symbolic. Standalone private date messages inDD.MM.YYYYorYYYY-MM-DDform, including invalid calendar dates and edited messages after a restart or eviction of the bounded tracking markers, are intercepted before ordinary AI processing and message logging. The reply directs the user to «Подробнее о паре» or/natal; active compatibility and natal forms keep their own input handlers. This guard covers date-only messages, with no general scrubbing promise for dates embedded in arbitrary prose. Contextual tarot loads the savedUserStatebefore applying the pair context. A button expired after 30 minutes explains the expiry and preserves active natal input. The tarot handler applies its new session and input cleanup only after successful welcome delivery. A failed contextual callback keeps the previous session, input and compatibility context available for a retry. A new command still cancels compatibility date input independently, as described above. - The natal Mini App offers natal, destiny matrix and combined reports. Matrix-only input asks for a date; natal and combined input retain the time-precision and birthplace steps, including the option to omit an unknown birth time.
/horoscope_settingssupports subscriptions and on-demand readings;/horoscope_stopstops scheduled delivery.- The admin web dashboard requires configured authentication;
/admin_daily#crocmanages shared/per-process Crocodile text models, per-puzzle images, the hourly automatic image quota and background preparation of both difficulties for an arbitrary day. Readiness checks do not invoke providers; explicit preparation can invoke paid providers and bypasses the automatic image quota. See the Daily Croc runbook.
- Conflict Error on Startup: Usually signifies another bot instance is currently polling the Telegram API using the same Token. Requires closing duplicate instances if not using Webhooks.
decryption_errortraces: Usually caused by attempting to load the database on a new host without providing the exact priorADMIN_SECRET(the code derives the Fernet key; the secret itself need not be base64).- Search features hanging: Check
TAVILY_API_KEYSexhaustively or verify thecircuit_breakerstate at/api/circuit-breakers.
- The Dockerfile health start period is 40 seconds. Legacy Compose overrides it to 120 seconds only when that Compose file is used; it does not control the VPS workflow.
- The legacy Compose file exposes its published port differently from the loopback-only VPS workflow. Review network exposure before using it outside local development.
- The VPS deployment state, configured credentials and public provider catalogs must be verified separately; the repository does not prove current health.
Use the architecture reference for current boundaries, ADR 0001 for single-owner delivery, and ADR 0002 for transactional graph writes. ROADMAP.md is future work, not implemented behavior; dated specs and plans retain their original design context.
See CONTRIBUTING.md for exact locked commands, test isolation and CI gates. Unit/E2E checks exclude both the integration directory and integration marker; integration tests also exist outside that directory. Use an explicitly disposable PostgreSQL/Redis target for DB tests. Rollback fixtures do not make arbitrary migration tests safe for production.
pytest.ini enables async auto mode, xdist and per-test timeout by default.
Historical test counts are not fresh validation evidence. For documentation-only
changes, check encoding, links, source accuracy and git diff --check.
See CONTRIBUTING.md for setup, validation, pull-request, and secret-handling guidance.
Minimum maintainer checks for behavior changes:
- Create a descriptive PR.
- Verify the relevant
pytestchecks pass. - Run
python -m ruff check .andpython -m ruff format --check .. - Run
python -m mypy app bot.pyfor production-code changes. - Update user-facing docs when public behavior or deployment changes.
MIT, see LICENSE.