Repository navigation
Development
Everything a contributor needs to set up the project, understand the code, and make a change that fits the codebase.
git clone https://github.com/Mohammad-Hasan-Kaman/ai-counseling-widget.git
cd ai-counseling-widget
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Windows: copy .env.example .env
uvicorn app.main:app --host 127.0.0.1 --port 8000The SQLite databases in data/ are created and seeded automatically on the first
boot. There is no migration step. To start from a clean state, delete the data/
folder and restart.
| URL | What to check |
|---|---|
/ |
The landing page renders. |
/widget?key=nk_… |
The widget opens and the flow starts. |
/demo/{api_key} |
The tenant demo loads. |
/admin |
The panel login works. |
/api/chat/health |
Returns {"status":"ok"}. |
ai-counseling-widget/
├── app/
│ ├── main.py 4.6K FastAPI app, lifespan, middleware, static mount, landing
│ ├── config.py 1.5K Every path and environment variable
│ ├── db.py 4.7K get_conn, init_all_dbs, gc_expired_sessions
│ ├── auth.py 3.6K PBKDF2 hashing, signed cookies, role guards
│ ├── tenants.py 10K Tenant CRUD, seeding, the default flow
│ ├── session_store.py 24K The state machine and the conversation engine
│ ├── internal_ai_engine.py 36K SpiralMatchEngine, concept extraction, learning weights
│ ├── ghq_analyzer.py 8.4K The 28 questions, sub-scales, scoring
│ ├── user_db.py 24K Users, requests, consultants, conflicts
│ ├── crawler.py 10K The availability crawler
│ ├── admin_tools.py 8.2K The Excel export
│ ├── excel_to_json.py 5.1K Excel → JSON for the roster
│ └── routers/
│ ├── chat.py 5.6K The public API — three routes
│ └── admin.py 47K The panel — 27 routes
├── static/
│ ├── widget.js 4.5K The loader that gets embedded on a client site
│ ├── widget-app.js 10K The chat client inside the iframe
│ ├── widget.css 4.0K The widget styles
│ ├── widget.html 1.1K The iframe document
│ ├── landing.html 10K The product landing page
│ └── demo.html 3.0K The per-tenant demo page
├── app/templates/ 9 Jinja2 templates for the panel
├── docs/ Screenshots for the README
├── data/ Runtime state — git-ignored
├── .env.example The environment template
├── requirements.txt Pinned dependencies
├── Dockerfile
├── docker-compose.yml
└── DEPLOY.md
Where to look for what:
| You want to change | File |
|---|---|
| A new API endpoint | app/routers/chat.py |
| A new panel page |
app/routers/admin.py + app/templates/
|
| The intake questions |
app/tenants.py → DEFAULT_NIKRAVAN_FLOW
|
| The matching rules | app/internal_ai_engine.py |
| The GHQ questions | app/ghq_analyzer.py |
| The widget's look | static/widget.css |
| The embed code | static/widget.js |
internal_ai_engine.py (36 KB) and session_store.py (24 KB) are the two files that
hold the behaviour. Everything else is plumbing around them.
The public surface is small:
def create_session(tenant_id: int) -> str
def get_session(token: str) -> dict | None
def log_message(token: str, role: str, text: str)
def handle_message(token: str, text: str) -> Replyhandle_message is the entry point. It reads the session, dispatches on the state,
and returns a Reply object with text, html, quick_replies, input_type and
done. Everything else — _advance_state, _handle_step, _handle_ghq,
_lead_capture, _finish, _restart — is private to the module.
md_to_html() and md_to_text() are the two formatting helpers. A message can
carry a small Markdown subset — **bold**, *italic*, `code`, and
\n\n paragraph breaks. md_to_html produces the HTML that goes into the iframe;
md_to_text produces the plain version stored in the database.
The public surface:
def match(user_info: dict, tenant_id: int) -> list[dict]
def names_match(a: str, b: str) -> bool
def record_feedback_direct(...)
def get_learning_weight(...)
def get_learning_stats(...)match() is pure rule-based scoring — read Matching Engine for
the full breakdown. The profiles are cached per tenant and invalidated when the
roster changes.
The flow is data, not code. Add a step to DEFAULT_NIKRAVAN_FLOW in
app/tenants.py:
{
"prompt": "سؤالتان را بنویسید…",
"key": "my_answer",
"type": "text", # text | phone | number | choice
"optional": False,
}Steps are numbered from 0, and the prompt supports the same Markdown subset as
every other message. The admin panel's flow editor writes the same JSON, so a step
added this way is immediately editable in the UI.
app/routers/admin.py uses Jinja2 templates:
@router.get("/something", response_class=HTMLResponse)
async def something(request: Request):
user = _current_user(request)
if not user:
return RedirectResponse("/admin/login", status_code=303)
return _render(request, "admin_something.html", {...})The four helpers to know:
| Helper | Purpose |
|---|---|
_current_user(request) |
The logged-in user, or None. |
_render(request, template, ctx, status_code=200) |
Renders a Jinja2 template. |
_tenant_of(user) |
The user's tenant row; 404 if missing. |
auth.require_user / require_admin / require_super
|
The role guards. |
Every panel query must filter by tenant_id. That is the isolation boundary, and
it is the rule the project cares about most.
static/widget.css is plain CSS with the chat-* / hd-* / btn-* prefixes. The
widget's own styles live in that file; the loader's injected styles live in the css
array in static/widget.js — they are separate because the loader has to be
self-contained.
app/routers/chat.py uses Pydantic models:
class MessageReq(BaseModel):
token: str = Field(max_length=64)
text: str = Field(max_length=MAX_MESSAGE_LENGTH + 500)The max_length on the token is a real guard — a token is 32 hex characters, so
anything longer is rejected before it reaches the database.
These come from CONTRIBUTING.md and they are not stylistic preferences — breaking
them breaks the product.
English everywhere in the repository. README, docs, commit messages and code comments are written in English. Strings shown to end users (widget UI, panel labels, GHQ questions) stay in Persian — that is the product language.
So: a docstring is English, a quick-reply button label is Persian.
.env, data/*.db and *.xlsx are git-ignored. Never add a real API key, a
password, or customer data.
Anything you add must be scoped by tenant (API key, cache key or database query) — never introduce global mutable state shared across tenants.
This is the one that matters most. A new cache, a new table or a new query that
isn't filtered by tenant_id leaks one clinic's data to another.
Matching runs locally by design.
No visitor data leaves the server. There is no LLM API, no third-party analytics.
-
Fork and branch —
git checkout -b feature/short-description -
Keep the change small and reviewable.
-
Verify the app still boots and the core flow works:
curl http://127.0.0.1:8000/api/chat/health # {"status":"ok"}Then walk through
/widget?key=nk_…and/adminby hand. -
Commit with a clear English message.
-
Open a pull request describing what changed and why.
There is no automated test suite in the repository. The verification is manual:
| Check | How |
|---|---|
| The app boots | curl /api/chat/health |
| The flow works | Walk /widget?key=nk_… end to end |
| The panel works | Log in, open each page, submit each form |
| The matching works | Answer the flow and check the recommendation in the panel |
| The export works | Export the leads and open the .xlsx
|
| The crawl works | Trigger a manual run and watch the progress |
| A change is isolated | Log in as two different tenants and check the data does not cross |
This is a deliberate trade-off of a small project, and it means a change that touches the state machine has to be walked by hand, step by step.
Open an issue with:
- What you expected and what actually happened
- The steps to reproduce
- The Python version and the OS
CHANGELOG.md follows Keep a Changelog and
Semantic Versioning. Every notable change
gets an entry under Unreleased — Added, Changed, Fixed, Removed,
Security.
MIT. By contributing you agree that your contributions are licensed under the MIT License.
- Architecture — the layers and the request path.
- Data & Storage — the schema.
- Matching Engine — the scoring rules.
- Intake Flow — the flow format.
- Deployment — running it in production.