A live, paying-users Telegram bot that turns OpenAI's Responses API into a complete assistant for a market where most AI products simply aren't localized: Uzbekistan.
It is not a wrapper around one API call. It streams answers token by token into Telegram's native "thinking" UI, decides on its own when to search the web, write and run Python, draw an image, set a reminder or remember a fact about you β and it stays inside a hard cost budget while doing it.
Everything the user sees is in Uzbek β including error messages, limit notices and paywalls. So is every comment in the source: this codebase is maintained by Uzbek speakers, for Uzbek speakers.
|
Streaming replies rendered through Telegram's Rich Message API, with animated status, premium custom emoji and a live elapsed-time counter. Falls back gracefully three levels deep so an answer is never lost. Multi-query DuckDuckGo search with full-page fetching, source comparison and a mandatory citation format. Fires automatically when a question needs today's data. Send a photo with or without a caption and get a real analysis β receipts, screenshots, homework, documents, medicine labels. Speech-to-text and a natural spoken reply. Language is auto-detected per message, so an Uzbek question gets an Uzbek voice, a Russian one gets a Russian voice. |
PDF, Word, Excel, PowerPoint and plain text are parsed and analysed. Send a file with no caption and the bot waits for your follow-up instruction instead of guessing. The model writes Python, the bot runs it in a sandbox and sends back the result: presentations, reports, spreadsheets, charts, format conversions β with a design guide that keeps the output looking professional. The model chooses what is worth keeping β name, profession, city, preferences β and it survives A Telegram-native advantage: the bot can start the conversation. One-off or recurring reminders, plus a daily digest on topics you pick. |
flowchart TB
TG["Telegram Bot API"] --> DP["aiogram Dispatcher<br/>ordered handler chain"]
DP --> PAY["Payments<br/>registered first"]
DP --> FSM["FSM flows<br/>gift Β· promo Β· digest"]
DP --> GATE["Maintenance gate"]
DP --> BUSY["Spam guard<br/>GeneratingState"]
DP --> H["Content handlers<br/>text Β· photo Β· doc Β· voice"]
H --> Q{"Quota check"}
Q -->|"denied"| UP["Localized limit notice<br/>+ upgrade button"]
Q -->|"allowed"| AI["services/ai.py<br/>streaming tool loop"]
AI <--> OAI["OpenAI Responses API"]
AI --> T1["internet_search"]
AI --> T2["run_python_sandbox"]
AI --> T3["generate_image"]
AI --> T4["update_memory"]
AI --> T5["manage_reminder"]
T2 --> SB["Isolated subprocess<br/>scrubbed env Β· rlimits Β· timeout"]
AI --> ST["Rich streaming UI"]
ST --> TG
AI <--> PG[("PostgreSQL<br/>history Β· memory Β· quotas Β· payments")]
WATCH["Background watchers<br/>reminders Β· digests Β· expiry Β· win-back"] --> TG
WATCH <--> PG
sequenceDiagram
autonumber
participant U as User
participant B as Bot
participant M as OpenAI
participant T as Tools
U->>B: "Make me a slide deck on the EV market"
B->>B: Charge points Β· pick reasoning effort
B-->>U: Animated status starts
B->>M: Stream request with active tools
M-->>B: internet_search(...)
B->>T: Multi-query search + page fetch
T-->>B: Sources
B-->>U: Status switches to "searching"
B->>M: Feed results back
M-->>B: run_python_sandbox(python code)
B->>T: Execute in sandbox
T-->>B: output/deck.pptx
B-->>U: Status switches to "building file"
B->>M: Feed result back
M-->>B: Final answer, streamed
B-->>U: Rich message + attached file
B->>B: Save history Β· refund unused quota
Each tool has its own round budget. When a budget runs out that tool is dropped from the request, which forces the model to answer instead of looping. A separate total-round ceiling guards against everything else.
πΈ Two independent quota systems β because one was a support nightmare
Points cover ordinary messages and scale with reasoning effort β "salom" is cheap, an integral is not. But file generation, image drawing and deep research are billed on separate daily counters.
The reason is concrete: when file generation came out of the points budget, three presentations drained a user's entire day and they experienced it as "the bot broke". Now the expensive operations have their own ceilings and ordinary chat keeps working after they run out.
Each expensive operation is charged once per user request, no matter how many times the model calls the tool β and refunded automatically if nothing was produced.
π The model's output is an untrusted boundary
Anything the model writes into a tool call reaches the database, so validation lives in the data layer β never in the tool description. An instruction is not a guarantee.
- Memory entries are stripped of newlines, so a multi-line "fact" cannot render as fake instructions in a later prompt.
- Card, passport and account number patterns are rejected outright.
- Every
UPDATE/DELETEdriven by a model-supplied index carriesAND user_id = $N, and the index is bounds-checked against the list actually shown to the model before the database is touched. - Reminder times are rejected if they are in the past, unparseable, or beyond the maximum horizon.
π§ͺ Running model-written Python without losing the host
The sandbox runs each snippet in a fresh temp directory as a subprocess with a scrubbed environment β no bot token, no API key, no database URL β plus CPU, memory, file-size and process-count rlimits, a hard timeout and process-group kill.
The honest caveat, documented in the source rather than hidden: network is not blocked, because the host offers no container isolation. The mitigation is that there is nothing to steal in that environment and the timeout caps abuse.
β‘ Prompt caching shapes where text is allowed to live
The system prompt is built to day precision so the prefix is byte-identical all day and prompt caching actually hits. Anything per-user β long-term memory, the user's name, the current time β goes into the message list as a developer message, never into the system prompt.
Putting one user-specific string in the wrong place silently destroys the cache for every user, and nothing warns you.
π― Handler registration order is a safety constraint
Payment handlers are registered directly on the dispatcher, before every router. The spam guard that answers "please wait, generatingβ¦" has no content filter β if a payment confirmation arrived while a reply was streaming, that guard would swallow it: money taken, subscription not granted.
By the same logic the maintenance gate sits before the AI handlers but after /start, while /pro and /promo sit after it β selling a subscription for a disabled bot is a refund waiting to happen.
π©Ί Failure modes are designed, not discovered
- Model unavailable or rate-limited β automatic fallback down a model list.
- Rich Message rejected β edit an existing message β plain message without formatting. Long answers are split at paragraph boundaries with code fences reopened across parts.
- Transcription fails β second engine.
- Voice synthesis fails β multilingual fallback voice.
- Payment cannot be granted β the user is told their money is safe and the admin gets everything needed to fix it by hand.
- User blocked the bot β marked inactive instead of retried forever.
| Layer | Choice | Why |
|---|---|---|
| Bot framework | aiogram 3.29 | Native async, FSM, and Guest Mode support |
| AI | OpenAI Responses API | Streaming + parallel tool calling in one loop |
| Database | PostgreSQL via asyncpg | Connection pooling, FOR UPDATE row locks for quota races |
| Sandbox | subprocess + rlimits | No Docker-in-Docker available on the host |
| Docs | python-pptx Β· reportlab Β· openpyxl Β· python-docx | Model writes the code, these do the work |
| Speech | OpenAI audio models, with a free-tier fallback path | Quality where it is paid for, availability everywhere |
| Payments | Telegram Stars (XTR) | No payment provider, no KYC, works instantly |
| Hosting | Railway | Postgres included, deploys by commit SHA |
βββ main.py # entrypoint β handler order is documented and load-bearing
βββ core/
β βββ config.py # model, prompts, plans, limits, costs β one source of truth
β βββ loader.py # bot, dispatcher, OpenAI client singletons
β βββ memory.py # in-RAM buffers with TTL cleanup
βββ handlers/
β βββ messages.py # text Β· photo Β· document Β· voice + streaming UI
β βββ pro.py # payments, gifts, promo codes, referrals
β βββ admin.py # broadcasts, statistics, user management, maintenance
β βββ guest.py # Guest Mode β one identity in groups and DMs
β βββ digest.py # daily digest scheduling
β βββ helpers.py # background watchers: reminders, expiry, win-back
βββ services/
β βββ ai.py # the tool loop, vision, speech, search
β βββ sandbox.py # isolated execution of model-written Python
β βββ file_task_quota.py # charge once, refund if nothing was produced
βββ db/
β βββ database.py # quotas, plans, payments, memory, reminders
β βββ history.py # conversation history in Postgres + RAM cache
βββ tests/ # 30 standalone assert-based suites
No framework, no fixtures, no mocking library. Every suite is a standalone script that fails loudly with a message explaining why the check exists.
python tests/test_pro_security.py # payment attack scenarios
python tests/test_tool_status.py # tool loop status and prompt routing
python tests/test_long_reply.py # long answers must never disappear
python tests/test_plan_limits.py # quota and refund invariantsThey exist to lock down behaviour that is invisible until it breaks in production β a tool call silently routed to web search, a refund handed to a user who was never charged, a status animation lying about what the bot is doing.
π Running it yourself
git clone https://github.com/JumayevOU/ChatGPT-AI.git
cd ChatGPT-AI
pip install -r requirements.txtCreate a .env file:
BOT_TOKEN=... # @BotFather
OPENAI_API_KEY=... # platform.openai.com
DATABASE_URL=... # postgres://user:pass@host:port/dbpython main.pyTables are created on first run. ffmpeg on PATH is optional and only used by the free-tier voice path.