Sales Trainer v0.1: corporate sales-training simulator (Flask+Vue, 15 personas, chat simulator, judge, analytics)

- Auth/roles (no self-reg), admin user provision, JWT
- Analyze: sales kit + initial pain-fit from form/upload
- Persona generator: 15 personas (5/tier) w/ pain variety, negotiation, init mode, channel, latent/revealable, wrong_text special
- Chat simulator: per-mode initiation, one-shot, hidden signals, judge-LLM debrief+coaching
- Trainee loop: win/lose board, weak-areas, user-generated personas
- Admin analytics; EN+TH Vue SPA served by Flask
- Deploy: Dockerfile, docker-compose, README, eng-log + HANDOFF
- Tests (mock LLM): m0/m1/routes/e2e all pass
This commit is contained in:
Macky
2026-08-07 15:31:06 +07:00
commit c3d31c06e2
70 changed files with 6135 additions and 0 deletions

62
docs/HANDOFF.md Normal file
View File

@@ -0,0 +1,62 @@
# HANDOFF — Sales Trainer
> Another AI should be able to resume without chat history.
## Branch / repo
- Repo: `~/Gitea/Sales Trainer/` (local git initialized; **no remote yet**).
- Branch: `main` (default).
## What this is
Corporate multi-user sales-training simulator. Vue SPA + Flask API + filesystem JSON storage.
Admins build persona groups from a product (form + upload); trainees chat one-shot against
generated customer personas to practice closing; judge-LLM scores + coaches.
## Current state — COMPLETE (M0M7), prototype verified with mock LLM
All backend + frontend built. All 4 backend test suites pass. Frontend builds. Live HTTP smoke
test passes (SPA served, login, group create, register->404).
## Verified commands
```bash
# Backend tests (mock LLM, no key needed)
cd backend
uv run python scripts/test_m0.py # auth/roles/no-self-reg
uv run python scripts/test_m1.py # group create + role visibility
uv run python scripts/test_routes.py # 21 routes registered
uv run python scripts/test_e2e.py # full flow (analyze->personas->chat->debrief->one-shot->board->analytics)
# Run backend
cd backend && uv run python run.py # Flask :5001 (serves built frontend from frontend/dist)
# Frontend dev
cd frontend && npm install && npm run dev # Vite :3000 proxying /api -> :5001
# Frontend build
cd frontend && npm run build # outputs frontend/dist
```
## Default account
- super_admin: `admin@salestrainer.local` / `admin123` (bootstrap; change in prod).
## Key gotchas
1. **Do NOT invoke `.venv/bin/python <script>` directly** — the tool lifecycle guard crashes
("embedded null byte"). Always: `uv run python scripts/<name>.py`.
2. LLM creds in `.env` (backend/.env for local; root `.env` for compose). `LLM_API_KEY=replace_me`
is a placeholder → LLM is None → analyze/chat return 500 "LLM not configured".
3. SPA fallback in `app/factory._register_frontend` accepts all HTTP methods and 404s `/api/*`
so no-self-registration holds.
## Blockers / open items
- **Real-LLM E2E not yet run** (needs a live API key). This is the #1 item.
- Docker image not built locally (no Docker on this Mac). Validate on EasyPanel.
- No git remote set (Gitea).
## Exact next actions
1. Set real `LLM_PROVIDER` + `LLM_API_KEY` (and optionally base/model) in `backend/.env`.
2. Run a live smoke test: login → create group → analyze → pick persona → chat a few turns → finish → read debrief; confirm judge produces sane output (this exercises real analyzer/persona/chat/judge).
3. Fix any real-model issues surfaced (prompt drift, JSON parsing).
4. Add Gitea remote + push. Optionally wire Gitea Actions / EasyPanel deploy.
5. If EasyPanel: build from root `Dockerfile`, set env vars, map port 5001.
## Docs
- `docs/PLAN.md` — full design + all confirmed decisions & open questions.
- `docs/engineering-log.md` + `docs/engineering-log/2026-08-07-build-out.md` — milestone record.
- `README.md` — quick start, accounts, tests, LLM config.

483
docs/PLAN.md Normal file
View File

@@ -0,0 +1,483 @@
# Sales Trainer — Plan & Architecture
App platform for Sales Training. Users develop **customer personas with pain points**
from uploaded files + a natural-language description, then **chat with simulated
customers** to practice closing a sale. Informed by two codebases:
- **MiroFish** (`~/Gitea/MiroFish`) — original full-stack CrowdSight engine (Flask + Vue, OASIS, Zep, persona/report/chat).
- **hermes-brain-and-tools** (`~/Gitea/hermes-brain-and-tools`) — clean-room CrowdSight plugin (seed → ontology → graph → environment; profile generator; durable workflow; report contract; social simulation).
This app is a **standalone web app** (not a Hermes plugin) with its own **login/auth**,
deployable via Dockerfile/EasyPanel (the user's established pattern).
---
## 1. Product goals
1. **Input**: setup form (product / customer segment / description) AND/OR upload files
(`.pdf/.md/.txt`). Product info is used **primarily to extract pains** as the raw material
for persona generation — it is just initial grounding.
2. **Analyze**: extract value propositions, features, pricing anchors, target profile, and an
**initial pain-fit**. A generated persona is **reusable across products in the same/similar
category** — it need not be locked to the exact uploaded product.
3. **Create persona pool**: generate 15 personas (5 × intent tier A/B/C), each marked with a
buying tier, a set of pains (varied, not all product-solvable), negotiation levers, and an
initiation mode (customer-initiated vs seller-initiated). Channel: Facebook / LINE.
4. **Standard report**: human-readable analysis report (per persona: background, pains,
objections, price sensitivity, buying signals, revealable/latent fields) — downloadable.
5. **Sales Simulation (chat)**: trainee chats 1:1 with a persona (one-shot). The persona
reacts realistically: negotiates, stalls, and **refuses to buy** unless the trainee actually
resolves the persona's specific pain(s). On a result, the app reveals the pain and
**why the close succeeded/failed** (short debrief + coaching).
6. **Training loop**: track each user's wins/losses; analyze the personas a user tends to lose
against and **generate new harder/variant personas** (or manual form) to keep training.
---
## 2. Persona design (core requirement)
### 2.1 Intent tiers (23 levels) — REQUIRED
| Tier | Name | Behavior |
|------|------|----------|
| **A** | **Ready-to-buy** (ตั้งใจซื้อ) | Has budget + authority + urgency. Short close window, but still expects the seller to confirm fit & handle 12 objections. Won't buy if the offer clearly misses their need. |
| **B** | **Unsure / educating** (ไม่แน่ใจ) | Researching/considering. Needs discovery, trust-building, proof, comparison, and a clear reason to act now. High chance to stall or go silent. |
| **C** | **Not interested but has pain** (ไม่สนใจแต่มี pain) | Unaware of the category/value, budget-constrained, or skeptical. Strong resistance, but has a real, unresolved pain — the ONLY path to a close is surfacing and resolving that pain. |
### 2.1b Tier counts
- **5 personas per tier** → **15 personas minimum** per project (3 tiers × 5).
- Each persona is a distinct, realistic individual — no two share the same background/income/personality combination.
### 2.2 Persona variety (REQUIRED) — every persona gets:
- **Background** (life story / situation that justifies their behavior)
- **Income & occupation** (varied across personas: salary, e.g. employee / freelancer / SME owner / executive / student / homemaker)
- **Age group & lifestyle** (varied: age ranges, family stage, living situation, habits)
- **Personality & temperament** (e.g. skeptical, analytical, impulsive, cautious, price-haggler, relationship-driven, impatient, distrustful)
- **Communication style** (tone, vocabulary, formality, emoji usage, sentence length)
- **Goal** when entering the chat + **objections** + **budget** + **decision timeline**
- **Pains** (14), each with: description, root need, and **the specific condition(s)** the seller must satisfy to resolve it.
**Consistency rule:** persona demographics (age band, occupation, lifestyle, income) must be
**consistent with the product's target user** (from the sales kit / product data), and the
**additional description / scenario field acts as a framing constraint** for persona creation
(e.g. "product targets SME restaurants in Bangkok" → all 15 personas fit that world, with
sensible income/lifestyle spread).
### 2.3 Realism rules (from user)
- Customers **never buy easily**. Every close requires "earning" it.
- **All tiers can lose — including Ready-to-buy (A).** A customer walks away (no sale) if the
conversation is genuinely bad — e.g. rude/aggressive language, ignoring their needs, pushy
pitching. Even a customer who desperately wants the product will refuse if the seller
violates basic decency or trust. "Wanting it" never overrides "bad experience."
- They **negotiate** (price, timeline, scope, add-ons).
- They **refuse / end the chat** if the seller fails to address their pain.
- Each persona has hidden pains the seller must **discover** (ask questions), not just pitch.
### 2.3a Negotiation — ALL tiers (REQUIRED)
Every persona bargains over **concessions/benefits**, at every tier:
- **Price reduction** (discount ask),
- **Freebies / add-ons / bundling**,
- **Delivery / fulfillment timeline** (especially for made-to-order / production goods),
- Scope adjustments, payment terms, guarantees, etc.
The seller must respond to negotiation realistically — give value in exchange, not just give in.
The specific negotiable leverage each persona will push on is defined in the persona card.
### 2.3b Pain variety (REQUIRED)
- **NOT every pain aligns with the product.** Persona pains may be:
- **Directly solvable** by the product (the clear win),
- **Partially solvable / nearby** (the product helps but doesn't fully close it — seller must
manage expectation / bundle / reframe), or
- **Unrelated/unsolvable** by this product (a genuine red herring — seller must recognize it
and redirect rather than force a fit).
- This mirrors reality: real customers rarely have exactly the pain a product solves. The
seller must **diagnose which pain is which** and only claim what the product truly delivers.
### 2.3c Chat initiation — TWO modes (REQUIRED)
The simulation supports **two initiation modes**, set per persona (a persona is either one or the other):
- **Customer-initiated** (ลูกค้าทักก่อน): for products/brands marketed to drive engagement
(e.g. cars, food, retail). The customer opens the chat; the seller responds.
- **Seller-initiated** (ฝ่ายเราเปิดการขายก่อน): for products/services sold proactively /
outbound (e.g. insurance, B2B services). **The customer does NOT message first — the seller
must open the sale.** The simulator gives the seller an opening task to start the conversation
with a cold/warm lead, and the persona reacts accordingly.
Each persona card declares which mode applies (and, for customer-initiated, includes the
persona's opener; for seller-initiated, the persona's initial mood/receptiveness).
> Note: this supersedes any earlier "customer always messages first" rule. Initiation is
> per-case: some personas are customer-initiated, some are seller-initiated.
### 2.3d Special tier-3 case — "wrong text / lost interest" (REQUIRED, ≥1 in tier C)
- At least **one tier-C persona** is designed as a **false lead**: their opening message makes
them *appear* ready to buy (e.g. "I need this, tell me the price"). But once the seller
responds, the persona immediately reveals lost interest and wants to **end the conversation**
("never mind, forget it") — yet **deep down the pain still exists**.
- To close: the seller must not take the "forget it" at face value; they must gently re-engage,
rebuild a moment of connection, and surface the still-live pain without being pushy. High
difficulty, frequent refusal — it tests resilience + empathy + non-pushy discovery.
### 2.3e Channel context (REQUIRED)
- The simulated chat is presented as a real messaging thread. Primary channels: **Facebook** and
**LINE**. A persona/case declares which channel it happens on (affects the look, and can color
tone — e.g. LINE more casual, FB page vs Messenger).
- Other channels optional later; v1 ships Facebook + LINE.
### 2.4 Persona lifecycle & one-shot rule (REQUIRED)
- **1 persona ↔ many users**: a persona is a shared playable asset; any user who has not yet
chatted it may practice on it.
- **1 user ↔ 1 chat per persona max (one-shot)**: a user can chat a given persona **only once**
the outcome is final and that persona is "used" for that user. It cannot be re-chatted/replayed
by the same user. (Reels/retry against it is not allowed.)
- To practice again on a similar customer, the user must **generate a new persona** (2.5).
### 2.5 Persona sources & generation (REQUIRED)
- **Admin-created baseline pool**: admin/upload-generated personas form the shared pool that
**new users** can pick from and practice on.
- **User-generated personas**: after practicing, a user can generate **their own persona** to
train on, in two ways:
1. **Weak-area generation** (4 in feature list): auto-analyze the personas this user tends to
lose against → generate a new, harder/variant persona targeting that weakness, as a "lock"
to overcome it; **or**
2. **Manual form**: the user describes the persona they want to practice (target profile,
situation, difficulty level) and the system generates it.
- User-generated personas are private to that user (unlike the admin pool).
### 2.6 Data exposure by view (REQUIRED)
- **Approve/edit view (admin)**: full persona data — pains, income, personality, negotiation
levers, hidden details. Admin sees everything to review/edit/approve.
- **Select/chat view (trainee)**: **only** the persona's name + basic info one would plausibly
know up front (profession, age group, channel, initiation mode, product context). **Hidden**:
pain, income, personality, budget, negotiation levers — anything you couldn't know without
talking. Revealed only **after** the conversation ends (win/lose + debrief).
- **Win/lose status**: the user can always see which personas they've **won** vs **lost** vs
**not yet tried**.
---
## 3. App architecture
```
┌────────────────────────────────────────────────────────────┐
│ Vue 3 Frontend (port 3000) │
│ Login · Dashboard · Project setup · Report · Chat view │
└───────────────▲────────────────────────────┬───────────────┘
│ HTTP/JSON (JWT) │
┌───────────────┴────────────────────────────▼───────────────┐
│ Flask Backend (port 5001) │
│ │
│ auth/ → register, login, JWT, per-user isolation, ROLES │
│ api/ → projects, uploads, personas, report, chat, groups │
│ services/ → analyzer · persona generator │
│ → sales_kit (product facts + initial pain-fit) │
│ → report builder │
│ → sales_simulator (chat engine) │
│ storage/ → filesystem JSON per user (no SQL DB) │
│ llm_client/ → OpenAI/DeepSeek/custom OpenAI-compatible calls │
└────────────────────────────────────────────────────────────────────┘
```
### 3.0 Roles & permissions (multi-user corporate)
| Role | Can do |
|------|--------|
| **Super Admin** | Manage all users, assign roles, manage org-level persona groups, view all data + analytics. |
| **Admin** (Manager) | Create/edit/delete **persona groups** (define a product/offer + its personas), **manually edit any persona**, manage users in their scope, view analytics. |
| **User** (trainee) | **Cannot create groups.** Only **selects an existing persona group** and practices (chat) on its personas; sees own results. |
- Org model: `organization``users``persona_groups``projects`/`sessions`.
- **Persona group** = a reusable packaged scenario: product definition (sales kit) + the 15
personas generated for it. Admins build groups **and may edit/re-analyze them later**
(groups are editable; regenerate is allowed; **admin can hand-edit any persona field**).
- A trainee's "project" is a **training session** bound to a group + a chosen persona.
### 3.0b Registration (confirmed)
- **No self-registration.** Admin creates users and sends invites (email/account-creation).
Only Super Admin and Admin can provision accounts. Roles: Super Admin / Admin / User (no Trainer).
- **Repo root**: `~/Gitea/Sales Trainer/`
- **Stack**: Flask 3 + Vue 3 (Vite) + JWT auth + filesystem JSON persistence + OpenAI-compatible LLM.
- **LLM**: reuse the provider-agnostic pattern from MiroFish (`llm_client.py`): configurable
`LLM_PROVIDER` / `LLM_BASE_URL` / `LLM_MODEL_NAME` / `LLM_API_KEY` via `.env`. Supports
**OpenAI, DeepSeek, or any OpenAI-compatible custom model** (base URL + model name overridable).
- **Persistence**: per-user `data/<user_id>/projects/<project_id>/…` JSON (mirrors
MiroFish/Hermes durable file approach). No external DB needed for v1.
### 3.1 Data flow
1. **Setup (admin)** → create a persona group: form (product/segment/description) + upload files
(+ channel & initiation-mode preferences).
2. **Analyze** (background, async) →
- **Sales Kit** extracted from input (what we sell, features, pricing, value prop, target, use cases)
**mainly for pain extraction** (reusable across same/similar category);
- **Personas** generated (15 personas w/ tier, pains, background, negotiation levers, initiation mode, channel, latent/revealable fields).
3. **Approve/Edit (admin)** → review full persona data; edit any field; approve pool.
4. **Report** (background, async) → structured report assembled from personas (downloadable; full data).
5. **Chat** → a trainee picks a persona (one-shot) → real-time conversational close attempt,
per the persona's initiation mode.
- Each message: persona responds; the simulator also returns an internal **state update**
(tier, pain-resolution progress, trust, buying-signal flags) — kept hidden.
- On **close** or **refusal**, the chat terminates with a debrief that reveals latent fields.
6. **Training loop** → record win/lose; weak-area analysis; generate-new-persona (lock or manual).
---
## 4. Services detail
### 4.1 `analyzer` (Sales Kit extraction + initial pain-fit)
Input: **setup form** with:
- **Product** (what it is) — required,
- **Initial customer segment** (optional),
- **Additional description / scenario** (optional framing for persona creation),
- **OR upload files** (`.pdf/.md/.txt`) carrying all of this — user may skip the form entirely.
**Decision logic (clear/simple):** if files are uploaded, parse them for product + target + pains;
if the form is also filled, the **form's explicit fields win** and file text fills the gaps
(and is still analyzed for extra context / pain-fit). If only files → derive everything from files.
If only the form → use the form.
Output (JSON): `productName`, `category`, `valueProps[]`, `features[]`, `pricing`
(budget anchors), `targetAudience` (incl. segment from form), `useCases[]`, `competitors[]`,
`objectionHandlers[]`, and — critically — **`initialPainFit[]`**: the analyzer's first-pass
judgement of **which pains the product can plausibly solve** (with evidence/claims from the
inputs), so persona pains can be built partly against and partly away from this baseline.
`scenario`/description is preserved as a **framing constraint** passed to persona generation.
This grounds all persona + chat generation so the simulation stays on-product.
### 4.2 `persona_generator`
Builds the **15 personas (5 × tier A/B/C)** for a given persona group. Prompt engineered to enforce:
- exactly 5 personas per tier;
- the required variety fields (background, income/occupation, personality,
communication style, goals, budget, timeline, objections);
- **pain variety** (2.3b): not every pain is product-solvable — include directly-solvable,
partially-solvable, and unrelated pains; use `initialPainFit` as the baseline;
- **the tier-C "wrong text / lost interest" special persona** (2.3d) — at least 1;
- **chat opener** for every persona + whether they initiate friendly/blunt/indifferent;
- **initiation mode** (customer-initiated vs seller-initiated) and **channel** (facebook/line);
- **latent vs revealable fields** (2.6) so the UI can hide what a real seller wouldn't know;
- JSON output schema (strict), language follows project `language` (en/th).
### 4.2b `weak_area_analyzer` (win/loss insight → new persona)
- Tracks each user's per-persona outcomes (won/lost) and their scores.
- On request, analyzes the user's **losses**: which tier / initiation mode / channel / pain-type
/ objection-type / negotiation-style they tend to lose against.
- Produces (a) a **summary insight** ("you lose most against seller-initiated, price-hardball
persona; you rarely handle discount + delivery-time pressure together"), and (b) a **spec** to
**generate a new persona** targeting that weakness (as a 'lock' to overcome) — or the user can
use the manual persona form instead.
- A user can also see their **win/lose status board** (which personas won/lost/not-tried).
### 4.3 `sales_simulator` (chat engine) — the heart
State machine per chat session:
```
READY → (customer or seller opens per mode) TALKING ⇄ (negotiating/objecting/thinking) → CLOSED | REFUSED | TIMEOUT
```
- **Initiation depends on the persona's mode (2.3c)**:
- **customer-initiated**: the customer sends the first message; the seller responds.
- **seller-initiated**: the customer does NOT message first — the simulator gives the seller
an **opening task** ("open the sale"), and the seller must start the conversation; the
persona then reacts as a cold/warm lead.
- **Channels**: Facebook or LINE (2.3e) — affects presentation + some tone.
- **One-shot (2.4)**: a user may not start a second session on a persona they've already
finished (won or lost). Backend enforces it.
- **Hidden/latent data (2.6)**: the trainee only ever sees revealable fields during the chat;
pain / income / personality / budget / negotiation levers / opener are latent and hidden until
the end.
- **Hidden pain state**: each persona has hidden unresolved pains. The simulator tracks
per-pain resolution. Pitching without discovery does NOT resolve pain.
- **Pain-fit realism (2.3b)**: since not all pains are product-solvable, the simulator must
let sellers mis-diagnose — claiming to solve an unrelated pain must backfire (trust down)
or lead down a dead end, while the real (product-solvable) pain stays unresolved.
- **Per message**: LLM plays the persona in-character (using persona card + sales kit +
chat history + current internal state). Returns:
- `reply` (the persona's in-character message),
- `internal`: updated `{ trust, painProgress, buyingSignals, tier, mayRefuse }`.
- **Close trigger**: the judge-LM decides if the seller has satisfied the pain-resolution
conditions (visible+hidden) AND the persona verbally accepts the offer/price. Only then `CLOSED`.
- **Refuse trigger**: if trust collapses, the seller pushes a hard pitch without
addressing pain, or after N failed attempts → `REFUSED`.
- **Negotiation**: personas actively counter (price, scope, timeline, freebies, delivery). Seller
must handle these on top of resolving pain.
- **Timeout/abandon**: persona goes silent if seller is repetitive/low-value.
**Internal signals are NEVER shown live.** Trust/pain/buying meters stay hidden during the
conversation (default) — the trainee reads the customer's words only.
**Debrief (on CLOSED or REFUSED) — REQUIRED, shown only at conversation end:** reveal the
latent fields (pain, income, personality, budget, negotiation levers, hidden opener), then:
- Keep the summary **short**, then give **coaching**: for each message that hurt the score,
suggest **how the seller should have responded** so the trainee understands and can improve.
- **CLOSED**: brief note on the persona's `pain` (what it was) + why it closed.
- **REFUSED**: the unaddressed pain(s), where trust was lost, which pain was mis-diagnosed (if
any) — each with **a concrete "better reply" suggestion**.
- A judge-LM (separate from the persona LLM) produces the score + coaching.
- Toggle: admin can choose to show live meters **per persona group** if desired; default hidden.
---
## 5. API surface (v1)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/auth/login` | JWT login |
| GET | `/api/auth/me` | current user + role |
| POST/PUT | `/api/admin/users` | admin: create users + invite, assign roles (super-admin/admin/user) |
| GET | `/api/admin/users` | admin: list users |
| POST | `/api/groups` | admin: create persona group (product files/description) |
| GET | `/api/groups` | list persona groups visible to role (user: selectable only) |
| GET | `/api/groups/<id>` | get group + personas (admin: full; user: practice view) |
| POST | `/api/groups/<id>/analyze` | admin: trigger analyze (sales kit + 15 personas) |
| GET | `/api/groups/<id>/report` | get/download report |
| GET | `/api/groups/<id>/personas` | list personas (grouped by tier; user sees revealable fields only) |
| GET | `/api/groups/<id>/personas/<pid>` | persona card (admin: full; user: revealable only) |
| PUT | `/api/groups/<id>/personas/<pid>` | admin: hand-edit a persona |
| POST | `/api/groups/<id>/reanalyze` | admin: re-run analyze / regenerate personas |
| POST | `/api/groups/<id>/personas/<pid>/chat` | user: start/send message → persona reply + hidden state (one-shot enforced) |
| GET | `/api/groups/<id>/personas/<pid>/session` | user: session state/history |
| GET/POST | `/api/sessions` | user: my training sessions + debriefs (own results) |
| GET | `/api/sessions/<id>/debrief` | user: end-of-chat debrief (pain + reason + coaching + latent reveal) |
| GET | `/api/me/board` | user: win/lose status board per persona (won/lost/not-tried) |
| GET | `/api/me/weak-areas` | user: analyze which personas I tend to lose against (insight) |
| POST | `/api/me/personas/generate` | user: generate own persona — body: {mode: "weak-area" \| "manual", ...spec} |
| GET | `/api/me/personas` | user: list my generated (private) personas + status |
| GET | `/api/analytics` | admin: aggregate trainee analytics (close rate, avg score, hardest personas) |
All `/api/*` except login require `Authorization: Bearer <jwt>`; data scoped by role + org.
Admins call analyze/persona-edit endpoints; trainees read revealable fields + run one-shot sessions +
generate their own personas.
---
## 6. Frontend views (Vue 3 + Vite)
1. **Login** (no self-registration — accounts created by admin)
2. **Dashboard** — (admin) persona groups + user mgmt; (user) selectable groups + my sessions
3. **Admin: Group Builder** — setup form (product/segment/description) + upload files; pick
channel (Facebook/LINE) + initiation-mode mix; trigger analyze → approve/edit the 15 personas
4. **Admin: User Management** — no self-registration; create + invite users, set roles
5. **Personas (approve/edit, admin only)****full data** for all 15 personas; edit any field, approve
6. **Personas (select, user)** — win/lose status board + list of available personas; each card shows
**only revealable info** (name, profession, age group, channel, initiation mode, product context)
7. **Report** — rendered analysis report + download (admin; full persona data)
8. **Simulation (chat)** — Facebook/LINE-style thread. **Internal signals + latent fields hidden**
during chat. Initiation per mode: customer opens OR seller gets an "open the sale" task.
**One-shot enforced.** On end → **debrief overlay**: reveal latent fields + short summary +
coaching (how to improve weak-score replies) + pain + reason + score. Result saved; persona
marked won/lost for this user.
9. **Gen persona (user)** — generate own persona: **weak-area** (from my loss analysis) or
**manual form** (describe target persona). Private to the user.
10. **Weak-areas (user)** — insight: which personas I tend to lose against + generate-a-lock CTA
11. **Admin Analytics** — aggregate trainee results (close rate, avg score, hardest personas).
i18n: en + th (mirrors MiroFish pattern). Role-based navigation (admin vs trainee).
---
## 7. Security, config, deploy
- **Auth**: JWT (HS256) with password hashing (werkzeug `generate_password_hash`).
Secrets in `.env`. No raw tokens/keys in UI. **No self-registration** — only admin-provisioned accounts.
- **LLM credentials**: `.env` only, never shipped/logged.
- **File safety**: upload allowed types + size caps; parse text server-side; strip anything
executable; keep raw uploads out of any served path.
- **LLM route discipline**: the chat + persona gen + judge are the only LLM-touching callers.
- **Deploy**: single `Dockerfile` (python:3.11 + Node 18, build Vue → serve static via
Flask or nginx) + `docker-compose.yml` with `.env`, per the user's EasyPanel pattern.
Local tests via `http.server` / Flask dev (no Docker on local Mac).
---
## 8. Milestones (build order) — ALL COMPLETE ✅
- **M0 — Scaffold & auth**: repo, Flask app factory, JWT auth, roles (super-admin/admin/user),
user + org store, **admin-user creation/invite (no self-registration)**. ✅
- **M1 — Input & analyze**: setup form (product/segment/description) + file upload/parse with
precedence rule, sales-kit extraction + **initial pain-fit**, JSON storage. ✅
- **M2 — Persona groups & persona generation**: group model (editable/re-analyzeable); 15 personas
(5 × tier) with variety, pain variety, negotiation levers, chat openers, **initiation mode
(customer/seller)**, **channel (facebook/line)**, **latent vs revealable fields**, and the
tier-C "wrong text" special case; **admin hand-edit persona endpoints**. ✅
- **M3 — Report**: assemble + render + download report. ✅
- **M4 — Chat simulation**: stateful persona chat (per-mode initiation: customer opens OR seller
"open the sale" task), Facebook/LINE thread, negotiation, all-tiers-can-lose, close/refuse logic,
**hidden signals + latent fields**, **one-shot enforcement**, **separate judge LLM** for scoring,
**short debrief with coaching** (latent reveal + pain + reason + how to improve). ✅
- **M5 — Trainee loop**: win/lose status board, weak-area analysis, **user-generated personas**
(weak-area "lock" + manual form), my-sessions. ✅
- **M6 — Frontend polish**: login, role-based dashboard, group builder (+ persona approve/edit UI),
user mgmt, personas (select view), report, chat UI, debrief overlay, gen-persona, weak-areas,
**admin analytics dashboard**, EN+TH i18n. ✅ (SPA served by Flask; verified live)
- **M7 — Deploy & docs**: Dockerfile, docker-compose, README, engineering-handoff docs,
E2E verification with honest status reporting. ✅ (mock-LLM E2E; real-key + Docker pending)
> Status: prototype complete + verified with mock LLM. **Pending: real-LLM live smoke test
> and remote Docker/EasyPanel validation** (see docs/HANDOFF.md).
---
## 9. Acceptance criteria
1. Multi-user corporate, **no self-registration**: super-admin/admin create + invite users; roles
Super Admin / Admin / User (no Trainer). **User (trainee) cannot create** — selects a group + practices.
2. Setup accepts **form (product / segment / description) AND/OR uploaded files**, with a clear
decision rule (form-wins, file-fills-gaps; files-only → derive all).
3. Analyzer produces sales kit **+ initial pain-fit**; persona demographics (age, occupation,
lifestyle, income) are **varied AND consistent with product + scenario framing**.
4. → 15 personas (5 × tier A/B/C); **admin can edit any persona and re-analyze the group**.
5. Persona group has **≥1 tier-C "wrong text / lost interest" special persona**.
6. Report is human-readable + downloadable.
7. **All tiers can lose** — bad conversation (e.g. rude language) = no sale, even for ready-to-buy.
8. **All tiers negotiate** concessions (price / freebies / delivery timeline / scope / payment).
9. Initiation is **per-persona mode**: customer-initiated (customer opens) OR seller-initiated
(seller gets an "open the sale" task). Channels **Facebook + LINE**. Internal signals + latent
fields are **hidden** during chat; only revealable fields shown.
10. **One-shot rule**: a user can chat a persona only once (won/lost = final); the same persona
stays playable for other users.
11. **Two persona views**: admin sees full data (approve/edit); trainee sees only revealable info
(name, profession, age group, channel, initiation mode) — latent fields revealed only after result.
12. **Win/lose status board**; **weak-area analysis**; **user-generated personas** (weak-area "lock"
OR manual form), private to the user.
13. Debrief is **short + coaches**: suggests how to improve on weak-score messages; reveals pain +
reason. Scored by a **separate judge LLM**; **no speed factor** in scoring.
14. Product data is used primarily for **pain extraction**; personas are reusable across the
same/similar product category.
15. **Admin analytics dashboard** aggregates trainee results (close rate, avg score, hardest personas).
16. Single `docker compose up` runs the whole app (or EasyPanel build); LLM pickable as
OpenAI / DeepSeek / any OpenAI-compatible custom model via `.env`.
17. E2E tests pass; honest report of any blocked stages.
---
## 10. Confirmed decisions (all from user)
- Standalone web app (Flask + Vue + JWT), Docker/EasyPanel deploy.
- Multi-user corporate, **no self-registration** — Super Admin / Admin / User (no Trainer).
Admin creates + invites users.
- **Admin** builds/edits persona groups, **hand-edits any persona**, re-analyzes groups, sees analytics.
- LLM: **OpenAI, DeepSeek, or custom OpenAI-compatible** (configurable via `.env`).
- **5 personas per tier → 15 min per group.**
- Persona variety: **age-group, occupation, lifestyle, income** — consistent with product + scenario framing.
- Internal chat signals **hidden**, revealed at end; **short debrief + coaching** (how to answer better on weak-score messages).
- **All tiers can lose**; **all tiers negotiate** concessions.
- **Separate judge LLM** for close/refuse + scoring; **no speed factor** in scoring.
- **Admin analytics dashboard** (aggregate trainee results).
- UI **EN + TH**.
- Product **user-input via form AND/OR uploaded file** with clear precedence; analyzer computes
initial pain-fit. Product data mainly for **pain extraction** — personas reusable across same/similar category.
- Personas have **pain variety** (not only product-solvable), demographically consistent with product.
- **Initiation per persona**: customer-initiated OR seller-initiated (open-the-sale task).
- **Channels**: Facebook + LINE.
- **Two views**: admin full data; trainee revealable-only (latent hidden until result).
- **One-shot rule**: 1 user = 1 chat per persona; persona shared across users.
- **User-generated personas**: weak-area "lock" OR manual form (private).
- **Win/lose board** + **weak-area analysis**.
- **≥1 tier-C "wrong text / lost interest" persona.**
## 11. Remaining open questions (low-risk; defaults noted)
- **Invite delivery**: email link to set password, or admin pre-sets a temporary password?
(Default: admin sets temporary password on account creation; optional email later.)
- **Analytics granularity**: just group-level aggregates, or drill-down per persona/trainee?
(Default: group + per-persona close rate + avg score; per-trainee detail on request.)
- **Scoring weights** (beyond dropping speed): pain 40 / trust 30 / objection-handling 30 ok?
- **Persona edit UI**: full form for all fields, or JSON editor for power users?
(Default: structured form for common fields + JSON for advanced.)
_Plan is ready for build. Confirm the low-risk defaults above if you disagree, otherwise I begin M0._

35
docs/engineering-log.md Normal file
View File

@@ -0,0 +1,35 @@
# Engineering Log — Sales Trainer
Program status table + dated entries. Append-only entries under `docs/engineering-log/`.
## What this is
A corporate, multi-user **sales-training simulator**. Admins upload/describe a product → app
analyzes it + generates 15 realistic customer personas (5 per intent tier A/B/C) with varied,
partially product-aligned pains, negotiation levers, initiation modes (customer/seller),
channels (Facebook/LINE), and latent-vs-revealable data. Trainees chat 1:1 (one-shot) to close
a sale; customers resist/negotiate/refuse; a separate judge-LLM scores + coaches the result.
Informed by MiroFish (CrowdSight engine) + the hermes-brain-and-tools CrowdSight plugin.
## Status table
| Milestone | Status | Last verified | Evidence | Next action |
|-----------|--------|---------------|----------|-------------|
| M0 Scaffold + auth/roles | complete | 2026-08-07 | `test_m0.py` | — |
| M1 Input & analyze (+pain-fit) | complete | 2026-08-07 | `test_e2e.py` | — |
| M2 Persona groups + generation (15, wrong_text) | complete | 2026-08-07 | `test_e2e.py` | — |
| M3 Report | complete | 2026-08-07 | `test_e2e.py` | — |
| M4 Chat simulator (init modes, one-shot, judge, debrief) | complete | 2026-08-07 | `test_e2e.py` | — |
| M5 Trainee loop (board, weak-areas, gen-persona) | complete | 2026-08-07 | `test_e2e.py` | — |
| M6 Frontend (Vue SPA) + static serving fix | complete | 2026-08-07 | build + live HTTP 200 | — |
| M7 Docker/deploy/docs | complete | 2026-08-07 | Dockerfile/compose/README | live-key E2E |
## Guardrails
- No self-registration; admin provisions users. (Verified: register => 404.)
- One persona = one chat per user (one-shot). Enforced in SessionStore + chat start.
- Latent persona fields never leak to trainees pre-result.
- LLM credentials live in `.env` only; never logged.
## Entry index
- `2026-08-07-build-out.md` — M0M7 build-out, decisions, verification, current state.

View File

@@ -0,0 +1,69 @@
# 2026-08-07 — Sales Trainer build-out (M0M7)
## Summary
Built the first complete version of the Sales Trainer app — corporate multi-user sales-training
simulator — in `~/Gitea/Sales Trainer` from the detailed plan in `docs/PLAN.md` (which records all
user decisions from the planning discussion).
## Plan status
- M0M7 all complete (see `engineering-log.md` status table).
## What was built
Backend (Flask + JWT + filesystem JSON):
- `app/config.py` (env/.env, LLM resolve), `app/llm.py` (OpenAI-compatible client + JSON/conv helpers)
- `app/storage/store.py` (durable JSON store, lock + atomic write)
- `app/auth/users.py` (UserStore: password hash, JWT, roles, no self-reg bootstrap)
- `app/services/`: `file_parser` (pdf/txt/md), `analyzer` (sales kit + initial pain-fit),
`persona_prompts` + `persona_generator` (15 personas, 5/tier, wrong_text special),
`store` (persona shape + revealable_view), `report`, `groups`, `sessions`, `simulator`
(chat + judge), `trainee` (weak-areas, MyPersonaStore), `own_persona`
- `app/api/`: auth, admin, groups, chat, me, analytics routes + JWT/RBAC helpers
- `app/factory.py`: app factory, bootstrap admin, serves built Vue frontend (SPA fallback)
- `run.py` entry
Frontend (Vue 3 + Vite): login, dashboard (role-based), group builder, group edit (admin) ,
personas (trainee revealable-only + win/lose), chat (FB/LINE thread + seller-task + debrief
overlay), my-sessions, weak-areas, gen-persona, admin users, analytics. EN+TH i18n.
## Verified commands / results
```
backend: uv venv --python 3.11 .venv
uv pip install -r requirements.txt --python .venv/bin/python
uv run python scripts/test_m0.py -> ALL M0 TESTS PASSED
uv run python scripts/test_m1.py -> ALL M1/M2-IMPORT TESTS PASSED
uv run python scripts/test_routes.py -> ALL ROUTE REGISTRATION TESTS PASSED
uv run python scripts/test_e2e.py -> ALL E2E TESTS PASSED
frontend: npm install && npm run build -> builds 11 route-split chunks (423ms)
live HTTP (Flask dev on :5001):
GET / -> 200 (SPA)
POST /api/auth/register -> 404 (no self-reg)
POST /api/auth/login -> 200 (JWT)
POST /api/groups -> 201 (draft group)
```
Mock-LLM E2E covers: analyze→15 personas (+wrong_text)→revealable-only→customer/seller-initiated
sessions→debrief(latent reveal+coaching)→one-shot→board→weak-areas→gen-persona→analytics.
## Engineering notes / issues
1. **Tooling**: the Hermes terminal life-cycle guard crashes ("embedded null byte") on direct
`.venv/bin/python <script>` invocation. Workaround: run scripts via `uv run python scripts/x.py`.
2. **Static serving path**: initially pointed at `backend/frontend/dist` (wrong) → `GET /` 404.
Fixed to repo-root `frontend/dist`, and made the SPA fallback accept all HTTP methods so
`/api/*` returns 404 (not 405), preserving no-self-registration.
3. **Mock vs real LLM**: tests use `scripts/mock_llm.py` (deterministic). Real model path
requires a live `LLM_API_KEY` in `.env` — NOT yet exercised live.
## Current state / runtime
- Backend runs via `cd backend && uv run python run.py`; frontend dev via `cd frontend && npm run dev` (proxies /api -> :5001).
- Default super-admin: `admin@salestrainer.local` / `admin123` (bootstrap; change in prod).
- Deploy files: root `Dockerfile`, `docker-compose.yml`, `.env.example`; repo-root `frontend/dist` build.
## Risks / remaining
- **Real-LLM end-to-end not verified** (needs a live key). Next: run analyze + persona + chat +
judge against the configured provider (DeepSeek/OpenAI/custom).
- Docker build not run locally (no Docker on this Mac — per environment note). Dockerfile follows
the EasyPanel single-container pattern; remote build+run should be validated on EasyPanel.
## Next action
1. Set a real `LLM_API_KEY` (and provider) in `.env`, run a live smoke test of analyze→personas→chat→debrief.
2. Push to Gitea remote (repo currently local git, no remote yet).
3. Validate Docker image build on EasyPanel.