Every counseling bot I could find was a funnel: a menu, a form, a "book now" button. That works until the visitor has to describe a problem in their own words, which is exactly the step a menu cannot do.
So I built the opposite: a messenger bot that runs a real clinical intake in a conversation, screens the person, and then ranks the counselors that fit — with the ranking engine inside the bot, not behind a vendor API.
The platform choice, and why it is a Telegram-compatible one
The bot runs on Bale, an Iranian messenger, and is implemented with python-telegram-bot==22.8 pointed at https://tapi.bale.ai/bot. That is deliberate: Bale exposes a Telegram-compatible HTTP API, so the whole ecosystem around python-telegram-bot — handlers, ConversationHandler, filters, the scheduler — applies unchanged. The product is a Bale bot; the library is the Telegram library.
This matters more than it sounds. The ConversationHandler is the piece that makes a multi-step intake possible, and it is a mature, well-understood component rather than something hand-rolled. It also means the code is portable: the same handlers would run against Telegram's API with a changed base URL and token.
Twelve states, one conversation
The intake is a ConversationHandler with twelve states: name, phone, gender, age, topic, previous therapy, details of previous therapy, expectation, preferred counselor gender, branch, the GHQ-28 offer, and the GHQ-28 question itself. Each state has its own handler and its own validation.
The design detail worth noting is the cancellation path. /cancel and a message matching شروع مجدد both abort the conversation cleanly, and the fallback handler returns the user to the start rather than leaving them in a half-finished state — the thing that makes messenger bots feel hostile when it goes wrong.
The screen, offered and interpreted server-side
The GHQ-28 stage is the same instrument the widget uses: 28 questions across four subscales — somatic, anxiety, social and depression — scored server-side and interpreted into bands. The main warning fires at a total of 43 or above, or a depression subscale of 15 or above; a milder warning starts at a total of 22.
What the bot will not do is diagnose. The output is presented as a screening signal, and the recommendation is advisory — the user makes the final choice. That distinction is in the code, not just in the disclaimer.
The matching engine, and how it learns
SpiralMatchEngine runs two passes. The first is hard filters: gender, branch, age bounds. The second is weighted scoring across topic fit, extracted clinical concepts, and live free-slot count. It returns a shortlist, and every entry comes with a "why this match" line — topic fit, age group, and a slot the person can actually book.
Concept extraction maps free text to eleven clinical concepts — anxiety, depression, obsessive-compulsive symptoms, relationships and conflict, couple and marriage, parent and child, child, adolescent and young adult, personal development, legal, and medical/psychiatric — using a curated synonym dictionary that includes Persian and Arabic variants. Name matching is fuzzy, so a visitor's spelling of a counselor's name does not have to be exact — spacing, Arabic/Persian letter variants and nicknames are all handled. Age ranges are parsed out of the free-text topic string, so "برای کودک ۷ ساله" and "نوجوان" land on the right roster rows.
The learning loop is the part that turns a ranking function into a product. Every recommendation, and every admin rating through /feedback, writes to ai_knowledge.db as a positive or negative signal. Those signals re-weight future matches. /learning prints the current weights, so the training is inspectable rather than a black box.
Availability without scraping a page by hand
A scheduled crawler refreshes open-slot counts every two hours by reading the centre's public team page, with proxy fallback, retry logic, and a lock file that prevents overlapping runs. The counts feed the ranking, so the engine prefers counselors the user can actually book. /crawl triggers it on demand.
The admin panel lives inside the chat
This is the part I like most, because it removes an entire web application from the deployment. The admin enters /admin with a password and gets a menu: /stats for live statistics, /export for a two-sheet Excel export of users and request history, /upload to replace the counselors spreadsheet — which hot-reloads the engine without a restart — /broadcast to message every user with a confirmation step, /crawl to run the crawler now, /feedback and /learning to train and inspect the engine, /myid to get the Bale user ID, and /logout to end the session.
Because the admin surface is a bot, the whole operational footprint is one process, one SQLite file set and one environment variable set. Access is restricted by ADMIN_USER_IDS — a comma-separated list of Bale user IDs — with a separate ADMIN_PANEL_PASSWORD and BOT_TOKEN. No secret is stored in the code; all three are read from the environment.
Storage, honestly
Six tables across three SQLite databases: users, user_requests, consultants and upload_history in the user database, appointments for slot availability, and concept_feedback for the learning weights. The roster is loaded from a spreadsheet, converted to JSON, and can be rebuilt from Excel at any time.
What I would build next
The honest next step is evaluation again, and it applies here too. The engine can be argued about; what it needs is a labelled set of intake cases with the ranking a good counsellor would have produced, so a change to the weights can be measured instead of assumed. A second useful addition would be a reminder path for people who were screened and then went quiet — the data to support it is already there.
The full technical documentation is published as a wiki: the architecture, the matching engine, the GHQ screening, the admin panel, setup and deployment, contributing, and troubleshooting. The source is on GitHub under the MIT license.
Related: the same engine ships as an embeddable web widget with a full multi-tenant admin panel — that article is on this site too.