Skip to content

Development

Mohammad-Hasan-Kaman edited this page Oct 20, 2026 · 2 revisions

Development

Everything a contributor needs to set up the project, understand the code, and make a change that fits the codebase.


Setup

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 8000

The 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"}.

The file map

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

The two big modules

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.

session_store.py — the conversation

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) -> Reply

handle_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.

internal_ai_engine.py — the matching

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.


Adding a feature — the mechanics

A new step in the intake flow

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.

A new panel route

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.

A new widget style

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.

A new API field

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.


Ground rules

These come from CONTRIBUTING.md and they are not stylistic preferences — breaking them breaks the product.

English in the repo, Persian in 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.

Never commit secrets

.env, data/*.db and *.xlsx are git-ignored. Never add a real API key, a password, or customer data.

Keep tenant isolation intact

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.

No external AI service calls

Matching runs locally by design.

No visitor data leaves the server. There is no LLM API, no third-party analytics.


Making a change

  1. Fork and branch — git checkout -b feature/short-description

  2. Keep the change small and reviewable.

  3. 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 /admin by hand.

  4. Commit with a clear English message.

  5. Open a pull request describing what changed and why.


Testing

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.


Reporting a bug

Open an issue with:

  • What you expected and what actually happened
  • The steps to reproduce
  • The Python version and the OS

The changelog

CHANGELOG.md follows Keep a Changelog and Semantic Versioning. Every notable change gets an entry under Unreleased — Added, Changed, Fixed, Removed, Security.


License

MIT. By contributing you agree that your contributions are licensed under the MIT License.


Related pages

Clone this wiki locally