Tools
Tools: How I reduced medical no-shows from 21% to 8.1% with an AI Telegram bot
2026-03-01
0 views
admin
The actual business problem ## Architecture ## The reminder system ## AI triage ## 1C integration headaches ## Compliance (152-FZ) ## Results after 5 weeks ## What I'd do differently ## Stack summary A Moscow medical clinic was losing money every day. 21% of appointments ended in no-shows — patients who booked but never came. Staff spent hours on manual reminder calls. The booking process was 100% phone-based. They hired me to fix it. Here's what I built and what happened in the first 5 weeks. No-shows in medical clinics aren't just annoying — they're expensive. An empty slot that could have been filled is pure revenue loss. At this clinic, 21% no-show rate translated to roughly $7,200/month in lost revenue. The root causes were straightforward: GigaChat instead of OpenAI was a hard requirement — Russian medical regulations prohibit sending patient data to foreign servers. The core of the no-show reduction was a multi-touch reminder sequence: Each message has inline keyboard buttons: Confirm / Reschedule / Cancel. Cancellations automatically trigger waitlist fulfillment. Patients can describe symptoms in free text. The bot uses a RAG pipeline (ChromaDB + rubert-tiny2 + GigaChat) to match symptoms against a curated medical knowledge base and suggest the appropriate specialist. Accuracy after 5 weeks: 89.7% correct specialist routing (validated against actual doctor assignments). 1C:Медицина has a REST API that's... not great. Endpoints time out randomly. The solution was a circuit breaker pattern: Without this, a 1C outage would have taken down the entire booking flow. Russian medical data law is strict: This added roughly 40 hours to the project but was non-negotiable. The clinic recovered the development cost in under 3 weeks. Build the admin panel earlier. Clinic staff needed dashboards from day one — I delivered them in week 3. Earlier delivery would have accelerated adoption. Stress test the APScheduler jobs. With 12 scheduled jobs running simultaneously at peak hours, there were occasional delays. Moving to Celery Beat for the high-priority reminder jobs would be cleaner at scale. More granular no-show prediction. The current system sends reminders to everyone equally. A simple ML model scoring no-show probability per patient (based on history, appointment type, day of week) would let you prioritize aggressive reminders on high-risk slots. Questions about the architecture, GigaChat integration, or 1C circuit breaker pattern — ask in the comments. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
T-48h: "You have an appointment in 2 days. Confirm or reschedule?"
T-24h: "Reminder: tomorrow at 14:00 with Dr. Ivanova. Reply to confirm."
T-2h: "Your appointment is in 2 hours. We're expecting you."
T+30m: If no-show detected → automatic slot release + waitlist notification Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
T-48h: "You have an appointment in 2 days. Confirm or reschedule?"
T-24h: "Reminder: tomorrow at 14:00 with Dr. Ivanova. Reply to confirm."
T-2h: "Your appointment is in 2 hours. We're expecting you."
T+30m: If no-show detected → automatic slot release + waitlist notification CODE_BLOCK:
T-48h: "You have an appointment in 2 days. Confirm or reschedule?"
T-24h: "Reminder: tomorrow at 14:00 with Dr. Ivanova. Reply to confirm."
T-2h: "Your appointment is in 2 hours. We're expecting you."
T+30m: If no-show detected → automatic slot release + waitlist notification COMMAND_BLOCK:
async def triage_patient(symptom_text: str) -> TriageResult: embeddings = await encoder.encode(symptom_text) similar_cases = await chroma.query(embeddings, n_results=5) prompt = build_triage_prompt(symptom_text, similar_cases) response = await gigachat.complete(prompt) return parse_triage_result(response) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
async def triage_patient(symptom_text: str) -> TriageResult: embeddings = await encoder.encode(symptom_text) similar_cases = await chroma.query(embeddings, n_results=5) prompt = build_triage_prompt(symptom_text, similar_cases) response = await gigachat.complete(prompt) return parse_triage_result(response) COMMAND_BLOCK:
async def triage_patient(symptom_text: str) -> TriageResult: embeddings = await encoder.encode(symptom_text) similar_cases = await chroma.query(embeddings, n_results=5) prompt = build_triage_prompt(symptom_text, similar_cases) response = await gigachat.complete(prompt) return parse_triage_result(response) - Patients forgot appointments (no automated reminders)
- Rebooking required a phone call during working hours
- Staff had no visibility into who was likely to no-show - Bot: Python 3.12 + aiogram 3 (Telegram)
- Backend: FastAPI + PostgreSQL 15 + Redis
- AI: GigaChat API (Russian language, data stays in Russia — required for medical data under 152-FZ)
- Vector search: ChromaDB + rubert-tiny2 embeddings
- EHR integration: 1C:Медицина REST API with circuit breaker
- Billing integration: 1C:Бухгалтерия OData
- Scheduling: APScheduler with 12 jobs
- Notifications: React PWA + Web Push
- Compliance: 152-FZ (Russian medical data law), AES-256 encryption via pgcrypto, LUKS disk encryption - 3 consecutive failures → circuit opens, fallback to cached data
- 30-second cooldown → circuit half-opens, single probe request
- Success → circuit closes, normal operation resumes - All patient data must be stored on Russian servers (Selectel VPS)
- Encryption at rest required (pgcrypto AES-256 + LUKS)
- Audit log for every data access
- Data retention and deletion procedures
how-totutorialguidedev.toaimlopenaillmserverroutingpostgresqlpython