Skip to content
Mohammad-Hasan-Kaman edited this page Oct 20, 2026 · 3 revisions

AI Counseling Widget — Wiki

A drop-in Persian/RTL chat widget that triages a visitor, screens them with GHQ-28, matches them to the right counselor — and gives every clinic its own API key, admin panel and configurable intake flow. One <script> tag on a clinic's website. No SDK, no build step, no third-party AI service.


What this is, and who it is for

This project is an AI-assisted intake system for counseling centers, shipped as an embeddable chat widget. A visitor who lands on a clinic's website gets a floating "دستیار انتخاب مشاور" bubble; clicking it opens a chat panel that runs a structured intake (name, contact, reason for the visit, …), optionally administers the GHQ-28 wellbeing screen, and ends with either a ranked shortlist of two counselors or a captured lead for the staff to call back.

It serves two audiences at once:

  • The visitor — a private, structured way to find the right counselor instead of phone-tag, in a language and layout that feel native (full RTL, Persian numerals, Persian date-free formatting).
  • The clinic's staff — a web admin panel where they read every conversation, export leads to Persian Excel, edit the intake flow live, upload a new counselor roster, broadcast an announcement, trigger the availability crawler, and rate the engine's recommendations so the matcher improves.

The part worth understanding before reading further: the "AI" is not a hosted language model. It is a local, deterministic engine — clinical concept extraction from free text, fuzzy counselor-name matching, age-bound parsing, weighted scoring over a counselor roster, and a feedback table that re-weights future matches. Nothing a visitor types leaves the server, there is no API key for an LLM vendor, and there is no per-message cost.


Quick navigation

Page What you will find there
Architecture The four layers (browser → HTTP API → flow engine → storage), the tenant-isolation model, and the request lifecycle of a single widget session.
Embedding the Widget The one-line <script> embed, the loader's DOM contract, mobile behaviour, and the demo page.
API Reference Every HTTP endpoint, request and response shape, status codes, rate limits, and the session lifecycle.
Multi-Tenancy How tenants, API keys, domain allow-lists, per-tenant flows and per-tenant data isolation work.
Matching Engine SpiralMatchEngine in depth: hard filters, the three fallback passes, the full scoring table, concept extraction, and the self-learning weights.
GHQ-28 Screening The 28 questions, the four subscales, the scoring function, the interpretation bands, and how the score feeds ranking.
Admin Panel Every panel page and form as reference tables: dashboard, inbox, leads, conflicts, flow editor, consultants, feedback, crawl, super-admin.
Setup and Deployment Clone → venv → pip install → .env → run. Every environment variable, Docker, Nginx, and the production checklist.
Troubleshooting Symptom → cause → fix table for the problems that actually happen: invalid keys, 403/404/429, missing tables, failed crawls, lost sessions.
Contributing Dev setup, the code conventions the repo observes, the data pipeline, and how to add a synonym or a flow step safely.

The feature set, and where each piece lives

Capability Implementation What it actually does
Persian RTL widget static/widget.html, static/widget.css, static/widget-app.js A 380×580 panel that becomes fullscreen under 480px, with quick-reply chips, a typing indicator, and a keyboard-aware layout on Android.
One-line embed static/widget.js A loader that injects its own CSS, creates the bubble and the iframe, and persists the open/closed state in localStorage.
Configurable intake flow app/session_store.py, app/tenants.py A per-tenant flow of numbered steps — text / phone / number / choice — with conditional steps, an optional GHQ stage, and two endings. Edited live from the panel, no redeploy.
GHQ-28 screening app/ghq_analyzer.py 28 questions across four subscales, scored server-side, interpreted into bands, and used as a ranking signal.
Concept extraction app/internal_ai_engine.py A curated Persian/Arabic synonym lexicon that maps free text to 11 clinical concepts (اضطراب, افسردگی, وسواس, زوج_ازدواج, والد_فرزند, …).
Counselor matching SpiralMatchEngine.match() Hard filters (gender, branch, age, legal-specialty guard) followed by weighted scoring over topic fit, clinical concepts, ability, live free slots, and learning weights. Returns the top 2.
Self-learning weights concept_feedback table, get_learning_weight() Every panel rating (and every successful recommendation) updates a per-(tenant, counselor, concept) weight in the range [0.6, 1.6].
Lead capture that never drops save_user_consultation() The request is persisted before matching runs, so a visitor who needs a counselor is recorded even when no counselor matches.
Availability crawler app/crawler.py Scrapes the center's public team pages every 2 hours for open appointment slots, with retry, backoff and a direct-connection fallback.
Multi-tenant by design app/tenants.py Per-tenant API key, admin accounts, allowed domains, flow configuration, consultant roster, and learning weights.
Admin panel app/routers/admin.py, app/templates/ Dashboard, full conversation inbox, lead table with Persian Excel export, conflict resolution, announcement broadcast, flow editor, consultant roster manager, feedback, crawl control, super-admin tenant management.
Signed sessions & hardened cookies app/auth.py PBKDF2-SHA256 password hashing (100k iterations), itsdangerous signed cookies, httponly + samesite=lax, optional secure flag.
Security headers & CORS app/main.py nosniff, X-Frame-Options: SAMEORIGIN (except /widget, which must be embeddable), strict-origin-when-cross-origin, and an explicit origin allow-list.
Rate limiting app/routers/chat.py, app/routers/admin.py 30 requests/minute per IP+session on the chat API; 10 failed logins per IP per 5 minutes on the panel.

How a session actually flows

graph TD
    A["Visitor loads the clinic's page"] --> B["static/widget.js injects<br/>bubble + iframe"]
    B --> C["iframe loads /widget?key=nk_…"]
    C --> D["widget-app.js reads the key<br/>from the query string"]
    D --> E["POST /api/chat/session"]
    E --> F["Server: resolve tenant by key<br/>→ check domain allow-list"]
    F --> G["Create or restore a session row<br/>(state, data, tenant_id, expires_at)"]
    G --> H["Return the welcome message<br/>+ the first prompt"]
    H --> I["Visitor types or taps a chip"]
    I --> J["POST /api/chat/message"]
    J --> K["State machine advances one step"]
    K --> L{"More steps?"}
    L -->|"yes"| H
    L -->|"GHQ enabled"| M["28 questions, scored"]
    L -->|"no"| N["Ending"]
    M --> N
    N --> O{"ending"}
    O -->|"recommend"| P["engine.match() → top 2 counselors<br/>+ reasons + booking links"]
    O -->|"lead_capture"| Q["Persist the request<br/>→ thank-you message"]
    P --> R["Request persisted first,<br/>then the recommendation is shown"]
    R --> S["Panel shows the transcript<br/>and the lead row"]
Loading

The state machine, its negative states, and the conditional-step rules are documented in Architecture and API Reference.


Stack

Layer Choice Note
HTTP framework FastAPI 0.115.6 Pydantic request models, lifespan hooks, APIRouter for the two route groups.
ASGI server uvicorn 0.34.0 app.main:app, one worker — the scheduler and the in-process rate-limit state assume a single worker.
Templates Jinja2 3.1.5 The admin panel and the landing page.
Storage SQLite (3 files) user_records.db, appointments.db, ai_knowledge.db under data/. WAL mode, busy_timeout=5000.
Scheduling APScheduler 3.10.4 AsyncIOScheduler: the crawl every 2 h, the session GC every 60 min.
Scraping requests + beautifulsoup4 The availability crawler.
Excel pandas + openpyxl Consultant roster import, Persian lead export.
Auth itsdangerous 2.2.0 Signed, timed session cookies.
Frontend vanilla JS + CSS No framework, no bundler, no npm. The widget ships as three static files.

Design decisions worth knowing up front

One worker, on purpose. docker-compose.yml starts uvicorn with --workers 1, and the scheduler jobs are registered in lifespan. Multiple workers would start multiple crawlers and split the in-memory rate-limit counters. Scale vertically, not horizontally, unless you move the scheduler out of the process.

The API key is the tenant identity. It is a bearer credential carried in the iframe's query string, mapped to a tenant row. It scopes the flow, the counselor roster, the learning weights and the leads. Rotate it from the super-admin panel; the widget picks up the new key on the next page load.

Domain allow-list is optional but recommended. A tenant may list the domains its widget may be embedded on. The check is an exact host match or a subdomain match — deliberately not a substring check, so notnikravan.org cannot impersonate nikravan.org.

The lead is written before the match runs. This was a deliberate fix (see CHANGELOG.md): if the engine returns nothing, the request still exists and the staff can call the person.

The engine never states a diagnosis. It extracts concepts, scores, ranks and explains ("تخصص در درمان تخصصی اضطراب و استرس"). It does not tell a visitor what condition they have. The GHQ screen is a screening instrument, and the widget presents it as such.


Repository map

ai-counseling-widget/
├── app/
│   ├── main.py                  FastAPI app, security headers, scheduler, page routes
│   ├── config.py                Env-based configuration (.env)
│   ├── db.py                    SQLite init, tenant-column migration, session GC
│   ├── auth.py                  PBKDF2 hashing, signed session cookies, role checks
│   ├── tenants.py               Tenants, API keys, seed data, default flows
│   ├── session_store.py         The per-tenant flow engine (steps, GHQ, endings)
│   ├── internal_ai_engine.py    SpiralMatchEngine — concept extraction + learning weights
│   ├── ghq_analyzer.py          GHQ-28 instrument, scoring and interpretation
│   ├── crawler.py               Availability crawler
│   ├── user_db.py               Users, requests, consultant roster, conflict handling
│   ├── admin_tools.py           Persian Excel export, broadcast helpers
│   ├── excel_to_json.py         Spreadsheet → roster JSON converter
│   ├── routers/
│   │   ├── chat.py              /api/chat/* — the widget's HTTP surface
│   │   └── admin.py             /admin/* — the panel
│   └── templates/               Jinja2 templates for the panel
├── static/
│   ├── widget.js                The embed loader (the <script> tag)
│   ├── widget-app.js            The in-iframe application
│   ├── widget.html              The iframe document
│   ├── widget.css               Widget styling
│   ├── landing.html             The product landing page at /
│   └── demo.html                The per-tenant demo page
├── data/                        SQLite files + roster JSON (git-ignored)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env.example

Where to go next


License and project links

MIT license — see LICENSE in the repository. The source lives at github.com/Mohammad-Hasan-Kaman/ai-counseling-widget, and the changelog is in CHANGELOG.md.

Clone this wiki locally