# AI Chatbot Integration — Master Doc (single source of truth)

**Status:** Active · **Last updated:** 2026-07-01 · **Owners:** App (Avi), Backend/AI (Neel), Product (Ram)

> **This is the one canonical doc — everyone edits this file.** Design, the full Q&A with the
> backend, the verified API contract, and the implementation plan all live here. When something
> changes, update the relevant section **and** add a line to the Changelog (§9).
>
> **Accuracy convention:** ✅ = verified against code (ref given) · 🟡 = agreed but not yet built /
> to confirm · 🔴 = open, owner named. Don't state anything as fact without a ref or an owner.

## Contents

1. Overview & agreed direction
2. Options considered (Web vs Native) — pros/cons
3. Architecture
4. API contract (verified against backend code)
5. Coach handoff via Freshchat (verified against app code)
6. Identity & program gating
7. Full Q&A with backend (all 18, checked)
8. Implementation plan (phased)
9. Open action items + Changelog

---

## 1. Overview & agreed direction

Add an in-app **AI coaching assistant**: a stateless LLM support bot that answers QuitSure
program / subscription / quit-journey questions from an **approved knowledge base** (the "Chat
Shortcuts" sheets), and **hands off to a human coach via the existing Freshchat integration** when
it should escalate. The bot is grounded only in approved content (no open-internet knowledge) and
never touches Freshchat itself.

**Agreed direction (both teams):** native app-owned chat surface calling the backend `POST /chat`,
bridging to Freshchat on escalation. Backend confirmed acceptance of this ("Option A") in their
answers doc. This is the current agreement, not a unilateral lock — §2 keeps the alternative on
record.

**At a glance (verified from backend):** model `gemini-2.5-flash`; KB ~488 entries; single
non-streaming JSON; p50 ~2.5s / p95 ~4s; 20s server timeout. Prod target
`https://tweb.quitsure.app/support-ai/` (pending deploy).

## 2. Options considered (Web vs Native)

### Option A — Native screen + `POST /chat` (agreed)

**Pros:** feels 100% native; coach handoff is a direct verified call (app already owns Freshchat +
the transcript); live program/day/subscription context is already native; clean auth via header;
pure-JS screen → OTA-updatable via hot-updater.
**Cons:** rebuild chat UX natively (bubbles, input, markdown, states); new dep
`react-native-markdown-display`; if a web version of _this_ bot is ever needed too, that's a second
build.

### Option B — Embed a web chat in a WebView

**Pros:** reuses the mature WebView infra (auth handoff, token refresh, capture protection); instant
web iteration.
**Cons:** the production AI is a **headless API with no web UI to embed** — so there's nothing to
reuse (the `tweb/discovery` MVP is a _separate_ scripted funnel, not this bot); context must cross a
JS↔native bridge; coach handoff needs a `postMessage` bridge; WebView UX quirks; weaker URL-based
auth.

**Why A:** the two things the bot leans on most (live program context + the Freshchat handoff) are
already native, and there is no shared web bot to reuse. OTA covers the iteration-speed argument.

## 3. Architecture

```
┌──────────────────────── QSApp2022 (React Native) ───────────────────────┐
│ AIChat screen (native)                                                    │
│  • FlatList of message bubbles + input + typing indicator                 │
│  • Sends: message, history (last 6-10), program, platform, +context       │
│  • Auth header (see §4)                                                    │
│        │  POST /chat                                                       │
│        ▼                                                                   │
│  Redux thunk over existing apiHelper ───────►  QuitSure AI Support (Python)│
│                                                 stateless, RAG over KB,     │
│  on escalate:                                   returns answer+escalation   │
│   Freshchat.setUserProperties({ai_summary, ai_thread_url})  ✅             │
│   Freshchat.sendMessage(transcript)  (optional)             ✅             │
│   openFreshchatConversation(Coaching tag)                   ✅             │
└───────────────────────────────────────────────────────────────────────────┘
Backend never touches Freshchat (Ram's rule) — quitsure-support-ai/app.py:8 (separate repo)
```

## 4. API contract (verified against `quitsure-support-ai/app.py` + `docs/API.md`)

> Separate repo: `github.com/neel-qs/quitsure-support-ai`. Refs below are to that repo.

**Base URL:** local `http://localhost:8082`; prod target `https://tweb.quitsure.app/support-ai/`
(Apache strips the `/support-ai` prefix; app mounts at `/` — `app.py:270-273`). 🟡 final deploy +
staging URL pending Ram.

**Auth:** shared secret **`X-API-Key`** header on `/chat`, `/feedback`, `/stats`; `/health` open;
open locally when unset (`app.py:280-282,392`). 🟡 Target for prod is app's **`accessToken`** as
`Authorization: Bearer` — **not built yet; backend needs our token-verification method** (see §9).

**Transport:** single **non-streaming** JSON response (no SSE in v1) — `chat()` returns a dict.
**Timeouts:** 20s server-side model timeout (`app.py:72-73`); use **30s client timeout**; any error
degrades to a graceful `service_error` escalation (`app.py:484-493`).

### `POST /chat` — request

```jsonc
{
  "message": "how do I cancel my subscription?", // required
  "user_id": "12345", // optional, logging only (send iUserId)
  "history": [
    // optional, oldest-first, last 6 used (app.py:435)
    {"role": "user", "text": "hi"},
    {"role": "assistant", "text": "Hi, how can I help?"}
  ],
  "program": "P3", // P3=Original, P9=Relaxed, P11=LeanSure(gate off, see §6)
  "platform": "ios", // ios | android
  "locale": "hi", // optional reply-language hint (auto-detected otherwise)
  "program_day": 3, // optional int — see §6 to-confirm on the app's day value
  "subscription_status": "active" // optional — active | expired | trial
}
```

Request model: `app.py:285-298`.

### `POST /chat` — response

```jsonc
{
  "answer": "You can cancel by going to My Account ...", // always present, show this
  "escalate": false,
  "escalate_reason": null, // see table; null when answered normally
  "coach_summary": null, // one line, ≤300 chars, only when escalating (app.py:381-382)
  "suggested_channel": null, // "coaching" | "technical" | null (app.py:378-380) — hint only
  "message_id": "14063d1feba5", // 12-char hex (app.py:375); echo in /feedback
  "sources": ["..."], // internal debug — ignore
  "used_ids": [] // internal — ignore
}
```

Finalizer that adds `message_id`/`suggested_channel`/caps summary: `app.py:373-383`.

### `escalate_reason` values (`app.py` SYSTEM_RULES + `docs/API.md`)

| reason                | escalate | app action                                                                   |
| --------------------- | -------- | ---------------------------------------------------------------------------- |
| `null`                | false    | show answer                                                                  |
| `greeting`            | false    | show answer (no human)                                                       |
| `out_of_scope`        | false    | show answer, **do NOT route to human**                                       |
| `service_error`       | true     | route → **technical**                                                        |
| `account_action`      | true     | route → **technical** (paid-no-access, refund status)                        |
| `no_confident_answer` | true     | route → coaching                                                             |
| `medical_topic`       | true     | route → coaching                                                             |
| `crisis`              | true     | route → coaching; `answer` already includes helpline text (`app.py:394-407`) |
| `user_requested`      | true     | route → coaching                                                             |
| `user_frustrated`     | true     | route → coaching                                                             |

⚠️ `escalate_reason` is an LLM free-string (not enum-locked in the schema, `app.py:261`) — treat any
unrecognised value defensively (show `answer`; if `escalate` route to coaching).

### Other endpoints

- `POST /feedback` → `{ user_id, message_id, message, answer, rating:"up"|"down", note? }`
  (`app.py:301-307,496-499`). Optional; powers their improvement loop.
- `GET /health` → `{ status, kb_entries, model, retrieval_floor }` (open).

## 5. Coach handoff via Freshchat (verified against app code)

All refs: `src/utils/freshchatUtils.js`, `src/services/notification.js`.

- Coach chat opens via `Freshchat.showConversations(options?)` — `freshchatUtils.js:281`.
- Context push (verified in use): `Freshchat.setUserProperties(flatStringObject, cb)` —
  `freshchatUtils.js:162`. **Values are short strings** → carry `ai_summary` + `ai_thread_url`,
  never a full transcript.
- Full transcript (if wanted in-thread): `Freshchat.sendMessage(new FreshchatMessage({tag, message}))`
  — verified in use at `notification.js:501`.
- SDK: `react-native-freshchat-sdk@4.8.1`; `react-native-webview@13.16.1` installed; **no** SSE or
  markdown lib installed yet.

**Handoff sequence on `escalate:true`:**

1. `sendUserPropertiesToFreshchat({..., properties:{ai_summary: coach_summary, ai_thread_url}})`.
2. (optional) post transcript via `Freshchat.sendMessage`.
3. `openFreshchatConversation(appConfigState, coachingOptions, dispatch)` with the Coaching tag,
   respecting LeanSure gating (§6). Backend deliberately does **not** post into Freshchat — the app
   makes the thread continuous (backend answer §7-#18).

## 6. Identity & program gating

- **Identity:** Freshchat `externalId` = `getFreshchatUserId(iUserId)` — raw `iUserId` in prod,
  `test_<iUserId>` in non-prod (`freshchatUtils.js:36-39`). Send the same **`iUserId`** as the
  backend `user_id` (backend uses it for logging only; it is not authenticated).
- **Program codes (verified `app.py:48-65`):** `P3`=Original, `P9`=Relaxed, `P11`=LeanSure. Any
  other code (P1/P2/P4/P5/P7…) is treated as Original for retrieval.
- **P11 / LeanSure gating:** this KB is smoking-cessation only and does **NOT** cover LeanSure →
  **gate P11 users off the AI bot entirely** (in addition to the existing
  `Constant.P11_VISIBLE_CHAT_TAGS` Coaching-channel restriction, `freshchatUtils.js:314-321`).
- 🟡 **`program_day` to-confirm:** backend wants an integer day. The app has `iActiveDayId` (used as
  `"ID - X"` in `freshchatUtils.js:197`) — confirm whether that is the literal day number or needs
  mapping before sending as `program_day`.

## 7. Full Q&A with backend (all 18 — checked against code)

Source: our questions → `quitsure-support-ai/ai-chatbot-integration-answers.md`, verified in `app.py`.

**Blockers**

1. **API.md** ✅ delivered (`docs/API.md`, matches code).
2. **Auth** 🔴 (owner: our backend) — X-API-Key today; accessToken-Bearer agreed for prod but not
   built; backend needs our token-verification method (JWT secret vs introspection).
3. **Hosting** 🔴 (owner: Ram) — target `tweb.quitsure.app/support-ai/`; deploy + staging URL
   pending.
4. **Transport** ✅ single non-streaming JSON; SSE not in v1 → no `react-native-sse`.
5. **Timeout/latency** ✅ p50 2.5s/p95 4s; 20s server; 30s client; graceful `service_error`.

**Contract** 6. **history** ✅ `[{role,text}]` oldest-first, last 6 used; send 6–10. 7. **escalate_reason enum** ✅ (10 values; `greeting`/`out_of_scope` = escalate=false); free-string caveat. 8. **suggested_channel** ✅ coaching/technical/null; account_action+service_error→technical. 9. **coach_summary** ✅ ≤300 chars, only on escalate. 10. **/feedback + message_id** ✅ 12-char hex id echoed back.

**Context & language** 11. **Program codes** ✅ P3/P9/P11 confirmed; P11 not covered → gate off bot. 12. **Richer context** ✅ `program_day`, `subscription_status` accepted; send what we have. 13. **Language** ✅ auto-detected (incl. Hinglish); optional `locale`; German `ge` or `de`.

**Handoff, privacy, content** 14. **Transcript→Freshchat** ✅ our approach accepted; backend never touches Freshchat. 15. **P11 gating** ✅ app enforces. 16. **PII** ✅ backend logs message+answer now (`app.py:404,478`); retention/hashing planned before prod. 17. **KB updates** ✅ Chat Shortcuts sheets; manual rebuild now; versioned later; no app change. 18. **Seamless thread** ✅ app-side; backend hands summary+answer, app carries into Freshchat.

## 8. Implementation plan (phased)

Every task follows `.claude/rules/` (performance, auto-test, auto-format, coding-style). Package
manager: **yarn**. Tests under `__tests__/…`; next global Test Number ≥ 4093.

### Phase 0 — Unblock (no app code)

- [ ] 🔴 Our backend gives Neel the **accessToken verification** method (else ship staging on X-API-Key).
- [ ] 🔴 Ram confirm **hosting** + staging/prod URLs.
- [ ] Get the **X-API-Key** for staging to start integration now.

### Phase 1 — App foundation (contract-independent, START NOW)

- [ ] **T1 Scaffold:** `yarn add react-native-markdown-display`; add `NavigationKeys.AIChat`;
      create `src/screens/AIChat/index.js` (+ `styles.js`), register route; test
      `__tests__/screens/AIChat/AIChat.test.js`.
- [ ] **T2 State:** `src/store/features/aiChatSlice/` — `messages[]`, `status`, `escalation`;
      `Message={id,role,text,ts}`; specific selectors; register reducer.
- [ ] **T3 UI:** `src/components/ChatBubble/` (memoized; assistant text via
      `react-native-markdown-display`, user plain) + `src/components/ChatInputBar/`; compose FlatList in
      AIChat (perf props, memoized `renderItem`/`keyExtractor`). Local echo only.
- [ ] **T4 Handoff module:** `src/utils/aiChatHandoff.js` — `handoffToCoach({dispatch,
appConfigState, profileState, coachSummary, threadUrl, transcript?})` → setUserProperties →
      optional sendMessage → openFreshchatConversation(Coaching), with P11 gating. Reuse verified
      helpers (§5); test with mocked SDK.

### Phase 2 — Backend wiring (needs Phase 0 auth/URL; contract is otherwise final)

- [ ] **T5 `/chat` thunk:** `src/store/features/aiChatSlice/thunks/chatThunks.js` via existing
      `apiHelper`; add `Config.aiChatBaseUrl` + `X-API-Key` header (swap to Bearer when ready). Request
      per §4; map `program`/`platform` (`Platform.OS`), plus `program_day`/`subscription_status`/`locale`
      when available. Append `answer`; set `escalation` from `escalate`+`suggested_channel`.
- [ ] **T6 Escalation → handoff:** when `escalate`, render "Talk to your coach", build transcript,
      call `handoffToCoach` with `coach_summary`; enforce P11-off-bot and channel gating; treat unknown
      `escalate_reason` defensively.
- [ ] **T7 Feedback:** thumbs up/down on assistant bubbles → `POST /feedback` with `message_id`.
- [ ] **T8 Gating:** hide/disable the AI entry for **P11 (LeanSure)** users entirely.

### Phase 3 — Polish

- [ ] History cache (AsyncStorage keyed by `iUserId`), reset on logout.
- [ ] i18n all strings (7 langs, German key `ge`); empty/error/retry states.
- [ ] `createAppStateAwareTimeout` (30s) so backgrounding doesn't false-error.
- [ ] Accessibility pass; snapshot + interaction tests; update both TSV catalogs.
- [ ] QA on release build incl. escalation→Freshchat, crisis copy, P11 gating.

**Out of scope:** the AI backend/RAG/KB (separate repo & team); the `tweb/discovery` funnel bot.

## 9. Open action items + Changelog

### Open action items

| #   | Item                                                                         | Owner       | Blocks         |
| --- | ---------------------------------------------------------------------------- | ----------- | -------------- |
| A1  | Provide accessToken verification method to Neel (JWT secret / introspection) | Our backend | Prod auth      |
| A2  | Confirm hosting + staging/prod URLs                                          | Ram         | Deploy         |
| A3  | Share X-API-Key for staging                                                  | Neel        | Start Phase 2  |
| A4  | Confirm the app can supply integer `program_day` (map from `iActiveDayId`?)  | App (Avi)   | Richer context |

### Changelog

- 2026-07-01 — Consolidated design + questions + plan into this master doc. All 18 Q&A verified
  against `quitsure-support-ai/app.py` + `docs/API.md`. Corrections vs earlier drafts: transport is
  non-streaming (SSE dropped), auth is X-API-Key first (accessToken later), P11 gates off the bot
  entirely, added `suggested_channel`/`message_id`/`program_day`/`subscription_status`.
