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

73
backend/app/config.py Normal file
View File

@@ -0,0 +1,73 @@
"""Configuration from environment / .env."""
from __future__ import annotations
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from backend/ (project root for this app)
_BACKEND_DIR = Path(__file__).resolve().parent.parent
load_dotenv(_BACKEND_DIR / ".env", override=True)
def _get_bool(name: str, default: bool = False) -> bool:
raw = os.environ.get(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def resolve_llm() -> tuple[str, str, str, str | None]:
"""Return (base_url, model, api_key, provider_name)."""
provider = os.environ.get("LLM_PROVIDER", "").strip()
explicit_base = os.environ.get("LLM_BASE_URL", "").strip()
explicit_model = os.environ.get("LLM_MODEL_NAME", "").strip()
api_key = os.environ.get("LLM_API_KEY", "").strip() or None
presets = {
"deepseek": ("https://api.deepseek.com/v1", "deepseek-chat"),
"openai": ("https://api.openai.com/v1", "gpt-4o-mini"),
"custom": ("", ""),
}
if provider and provider in presets:
base_url, model = presets[provider]
if explicit_base:
base_url = explicit_base
if explicit_model:
model = explicit_model
return base_url, model, api_key or "", provider
# No/unknown provider: fall back to explicit config
return (
explicit_base or "https://api.openai.com/v1",
explicit_model or "gpt-4o-mini",
api_key or "",
provider or None,
)
class Config:
APP_NAME = "Sales Trainer"
SECRET_KEY = os.environ.get("JWT_SECRET", "dev-secret-change-me")
JWT_ALGO = "HS256"
JWT_EXPIRES_HOURS = int(os.environ.get("JWT_EXPIRES_HOURS", "24"))
DATA_DIR = Path(
os.environ.get("DATA_DIR", str(_BACKEND_DIR / "data"))
).resolve()
FLASK_HOST = os.environ.get("FLASK_HOST", "0.0.0.0")
FLASK_PORT = int(os.environ.get("FLASK_PORT", "5001"))
FLASK_DEBUG = _get_bool("FLASK_DEBUG", True)
UPLOAD_MAX_MB = int(os.environ.get("UPLOAD_MAX_MB", "15"))
ALLOWED_UPLOAD_EXTS = {"pdf", "md", "txt"}
# LLM
LLM_BASE_URL, LLM_MODEL, LLM_API_KEY, LLM_PROVIDER = resolve_llm()
ROLES = ("super_admin", "admin", "user")
@classmethod
def ensure_dirs(cls) -> None:
for name in ("users", "orgs", "groups", "sessions"):
(cls.DATA_DIR / name).mkdir(parents=True, exist_ok=True)