Skill: fangwei-frank/training-quiz (version 1.0.0)

--- name: training-quiz description: > Interactive product knowledge training and quiz system for retail staff. Tests employees on product specs, store policies, sales techniques, and FAQs through flashcards, multiple choice, and scenario-based questions. Tracks completion and scores per employee. Use when: a staff member wants to learn about products, take a quiz, test their knowledge, onboard to a new product line, or practice sales scripts. Triggers on: 考我, 测试, 培训, 学习产品, 练习, quiz me, test my knowledge, training, flashcard, product knowledge test, 我想学, 帮我复习, 新品学习. metadata: openclaw: emoji: 🎓 --- # Training Quiz ## Overview This skill turns the product knowledge base into an interactive learning system for staff. It adapts difficulty, tracks progress, and celebrates improvement — making training feel less like a chore and more like a game. **Depends on:** `products[]` + `policy_entries[]` + `faqs[]` in knowledge base. --- ## Quiz Modes | Mode | Trigger | Format | Best For | |------|---------|--------|---------| | Flashcard | "考我产品知识" | Q → reveal A | Quick daily review | | Multiple Choice | "选择题模式" | Q + 4 options | Structured testing | | Scenario | "情景练习" | Role-play customer scenario | Sales skill practice | | Policy Drill | "考我政策" | Policy rule questions | Compliance training | | New Product | "考我新品" | Focus on recently added items | New arrival onboarding | Default mode: **Flashcard** (lowest friction). --- ## Session Flow ### Start 1. Greet the learner by name (if known from staff config) 2. Ask or confirm: mode, topic focus, number of questions (default 10) 3. Begin immediately — don't over-explain ### During Quiz - Ask one question at a time - Wait for answer before revealing correct response - On correct: brief positive reinforcement ("✅ 答对了！") + optional fun fact - On incorrect: show correct answer + brief explanation (2 sentences max) - Track: correct / total, running accuracy % ### End of Session ``` 🎓 本次练习结束！ 结果：[correct]/[total] — [score]% [评级: 优秀 ≥90% | 良好 70-89% | 需加强 <70%] [If score < 70%]: 建议重点复习：[list weak categories] [If score ≥ 90%]: 太棒了！你已经达到优秀水平 🏆 下次想练习什么？ ``` **Reference:** [question-bank.md](references/question-bank.md) — question templates by type. --- ## Question Generation Questions are auto-generated from the knowledge base. No manual authoring needed. ### From products: - "这款[产品名]的[属性]是什么？" → answer from `description`/`variants` - "下面哪个是[产品名]的正确价格？" → MCQ using real + nearby prices as distractors - "[顾客描述] → 你会推荐哪款产品？" → scenario from `suitable_for` ### From policies: - "退货政策中，[条件]，顾客可以享受什么？" - "以下哪种情况不在退货政策范围内？" → MCQ with real exceptions as options ### From FAQs: - Use `question` field directly - Shuffle real FAQ answers as MCQ distractors **Script:** `scripts/generate_questions.py` — generates a quiz set from the KB. --- ## Progress Tracking Store per-employee progress in agent memory under `training_progress.<staff_id>`: ```json { "staff_id": "zhang_san", "sessions": [ { "date": "2024-07-15", "mode": "flashcard", "score": 8, "total": 10, "accuracy": 80, "weak_categories": ["policy", "pricing"] } ], "cumulative_accuracy": 82, "badges": ["first_quiz", "7day_streak", "policy_master"] } ``` Report progress to manager on request: > "张三本月完成 5 次练习，平均正确率 82%，政策类题目需加强。" --- ## Gamification Keep it motivating: | Achievement | Trigger | |-------------|---------| | 🌟 首次完成 | First quiz session | | 🔥 连续挑战 | 3+ consecutive days | | 📚 政策达人 | 5 policy quizzes with ≥90% | | 🏆 产品专家 | Overall accuracy ≥90% over 10+ sessions | | ⚡ 闪电手 | 10 consecutive correct answers | Announce badges immediately when earned. Keep it brief and genuine.

path: references/question-bank.md
content: # Question Bank Templates

## Question Types

### Type 1: Factual Recall (记忆题)
Tests: product specs, policy terms, exact numbers.

Template:
```
这款[产品名]的[属性名]是什么？
预期答案：[从KB的对应字段直接取]
```

Examples:
- "SK-II神仙水的容量规格有哪些？" → answer from variants
- "7天无理由退货的适用条件是什么？" → answer from policy_entries.conditions
- "当前满减活动的门槛金额是多少？" → answer from promotions.rules

Scoring: exact/near-exact = correct; vague = partial; wrong = incorrect

---

### Type 2: Multiple Choice (选择题)
Tests: distinguishing correct info from plausible-but-wrong distractors.

Template:
```
[产品名]的售价是多少？
A. ¥[correct_price]
B. ¥[correct_price + 50]
C. ¥[correct_price - 80]
D. ¥[similar_product_price]
```

Distractor generation rules:
- Option A: correct answer (shuffle position randomly)
- Option B: ±10–20% of correct value
- Option C: ±25–40% of correct value
- Option D: use value from a similar product in KB

For policy questions:
- Correct: actual policy condition
- Distractors: common misconceptions (e.g., "购买当天不能退" when policy says 7 days)

---

### Type 3: Scenario (情景题)
Tests: applying knowledge to customer conversations.

Template:
```
情景：一位顾客说"[customer_message]"
你会怎么回应？
```

Good scenario seeds (fill with real KB data):
- "这款适合我妈妈用吗？她皮肤比较干" → tests product knowledge + recommendation
- "我买了5天，不喜欢想退货" → tests return policy
- "你们现在有什么活动？我要买3件" → tests promotion calculation
- "这个有没有XL码？" → tests inventory awareness
- "这两款哪个性价比更高？" → tests comparison skill

Scoring: rubric-based (see below)

---

### Type 4: Policy Drill (政策题)
Tests: correct application of store policies.

Template:
```
判断题：以下说法是否正确？
"[policy_statement]"
```

Correct statements: direct quotes from policy_entries.conditions
Incorrect statements: modified versions with wrong dates, amounts, or conditions

Example:
- ✅ "购买7天内未使用商品可以无理由退货"
- ❌ "购买15天内可以无理由退货" (wrong — it's 7 days)
- ❌ "已拆封商品也可以无理由退货" (wrong — conditions not met)

---

## Scenario Scoring Rubric

| Score | Criteria |
|-------|---------|
| Full (3 pts) | Correct info + appropriate tone + actionable response |
| Partial (2 pts) | Correct info but awkward tone or missing next step |
| Partial (1 pt) | Partially correct; misses key policy detail |
| Zero (0 pts) | Wrong info, fabricated, or unhelpful |

---

## Difficulty Levels

| Level | Criteria | Use case |
|-------|---------|---------|
| Easy | Single-fact recall; answer directly in KB | New hire, first week |
| Medium | Requires applying a rule to a scenario | After 1-2 weeks |
| Hard | Multi-step reasoning; policy edge cases | Experienced staff |

Auto-assign difficulty:
- Questions from `name`/`price` fields → Easy
- Questions from `description` + `policy conditions` → Medium
- Questions from `exceptions` + scenario combinations → Hard

---

## Session Question Mix (default 10 questions)

| Type | Count | Difficulty |
|------|-------|-----------|
| Factual recall | 3 | Easy–Medium |
| Multiple choice | 3 | Medium |
| Scenario | 3 | Medium–Hard |
| Policy drill | 1 | Medium |


path: scripts/generate_questions.py
content: #!/usr/bin/env python3
"""
generate_questions.py — Auto-generate quiz questions from the retail knowledge base

Usage:
    python3 generate_questions.py --kb knowledge_base.json \
                                   --count 10 \
                                   [--mode flashcard|mcq|scenario|policy|mixed] \
                                   [--difficulty easy|medium|hard|mixed] \
                                   [--focus products|policies|promotions|all]

Output: JSON array of quiz questions
"""

import json
import random
import argparse
from pathlib import Path


def make_factual_question(product: dict) -> dict | None:
    """Generate a factual recall question from a product entry."""
    name = product.get("name", "")
    if not name:
        return None

    candidates = []

    if product.get("price"):
        candidates.append({
            "question": f"这款「{name}」的售价是多少？",
            "answer": f"¥{product['price']:.0f}",
            "type": "flashcard",
            "difficulty": "easy",
            "category": "pricing",
            "source_sku": product.get("sku"),
        })

    if product.get("description"):
        desc = product["description"][:80]
        candidates.append({
            "question": f"请简单介绍一下「{name}」这款产品的主要特点。",
            "answer": desc,
            "type": "flashcard",
            "difficulty": "medium",
            "category": "product_knowledge",
            "source_sku": product.get("sku"),
        })

    if product.get("suitable_for"):
        suitable = "、".join(product["suitable_for"][:3])
        candidates.append({
            "question": f"「{name}」适合哪类人群使用？",
            "answer": suitable,
            "type": "flashcard",
            "difficulty": "easy",
            "category": "recommendation",
            "source_sku": product.get("sku"),
        })

    variants = product.get("variants", [])
    for v in variants:
        if v.get("attribute") == "size" and v.get("values"):
            sizes = "、".join(v["values"])
            candidates.append({
                "question": f"「{name}」有哪些尺码可以选择？",
                "answer": sizes,
                "type": "flashcard",
                "difficulty": "easy",
                "category": "variants",
                "source_sku": product.get("sku"),
            })
            break

    return random.choice(candidates) if candidates else None


def make_mcq_question(product: dict, all_products: list) -> dict | None:
    """Generate a multiple choice question with distractors."""
    name = product.get("name", "")
    price = product.get("price")
    if not name or not price:
        return None

    price = float(price)
    # Generate distractors
    other_prices = [
        float(p.get("price", 0)) for p in all_products
        if p.get("price") and p.get("name") != name and float(p.get("price", 0)) != price
    ]
    random.shuffle(other_prices)

    options = [price]
    # Add price variants
    options.append(round(price * 1.15, 0))
    options.append(round(price * 0.85, 0))
    if other_prices:
        options.append(other_prices[0])
    else:
        options.append(round(price * 1.3, 0))

    options = list(dict.fromkeys(options))  # deduplicate
    random.shuffle(options)
    correct_idx = options.index(price)
    labels = ["A", "B", "C", "D"]

    return {
        "question": f"「{name}」的正常售价是多少？",
        "options": {labels[i]: f"¥{v:.0f}" for i, v in enumerate(options[:4])},
        "correct": labels[correct_idx],
        "answer": f"¥{price:.0f}",
        "type": "mcq",
        "difficulty": "easy",
        "category": "pricing",
        "source_sku": product.get("sku"),
    }


def make_policy_question(policy: dict) -> dict | None:
    """Generate a policy knowledge question."""
    title = policy.get("title", "")
    conditions = policy.get("conditions", [])
    full_text = policy.get("full_text", "")

    if not title or (not conditions and not full_text):
        return None

    if conditions:
        # Pick one condition as the answer
        condition = random.choice(conditions)
        return {
            "question": f"根据「{title}」，顾客需要满足什么条件才能申请？",
            "answer": condition,
            "type": "flashcard",
            "difficulty": "medium",
            "category": "policy",
            "source_policy": policy.get("policy_id"),
        }

    # True/False from full text
    if full_text:
        return {
            "question": f"关于「{title}」，请简述主要规定。",
            "answer": full_text[:200],
            "type": "flashcard",
            "difficulty": "hard",
            "category": "policy",
            "source_policy": policy.get("policy_id"),
        }
    return None


def make_scenario_question(product: dict, policy: dict | None = None) -> dict | None:
    """Generate a role-play scenario question."""
    name = product.get("name", "")
    suitable = product.get("suitable_for", [])
    if not name:
        return None

    scenarios = []

    if suitable:
        target = random.choice(suitable)
        scenarios.append({
            "question": f"情景练习：顾客问「{name}适合{target}吗？」，你会怎么回答？",
            "answer": f"是的，{name}非常适合{target}。" + (f" {product.get('description', '')[:60]}" if product.get("description") else ""),
            "type": "scenario",
            "difficulty": "medium",
            "category": "recommendation",
        })

    if product.get("price"):
        scenarios.append({
            "question": f"情景练习：顾客看中了「{name}」，但说有点贵，你会怎么说？",
            "answer": "应对价格异议：首先认可顾客的感受，然后强调产品价值（品质/功能/适用场景），可以提及当前活动优惠，以及同价位的对比优势。",
            "type": "scenario",
            "difficulty": "hard",
            "category": "sales_skill",
        })

    if policy:
        scenarios.append({
            "question": f"情景练习：顾客购买{name}后第3天说不满意想退货，你会怎么处理？",
            "answer": f"先表示理解，然后按退货政策处理：{policy.get('full_text', '')[:100]}",
            "type": "scenario",
            "difficulty": "medium",
            "category": "after_sales",
        })

    return random.choice(scenarios) if scenarios else None


def make_faq_question(faq: dict) -> dict | None:
    """Turn a FAQ entry into a quiz question."""
    q = faq.get("question", "")
    a = faq.get("answer", "")
    if not q or not a:
        return None
    return {
        "question": f"如果顾客问：「{q}」，正确答案是什么？",
        "answer": a,
        "type": "flashcard",
        "difficulty": "easy",
        "category": faq.get("category", "faq"),
        "source_faq": faq.get("faq_id"),
    }


def generate_quiz(kb: dict, count: int, mode: str, difficulty: str, focus: str) -> list:
    products = kb.get("products", [])
    policies = kb.get("policy_entries", [])
    faqs = kb.get("faqs", [])

    questions = []

    # Sample source data
    sample_products = random.sample(products, min(len(products), count * 2))
    sample_policies = random.sample(policies, min(len(policies), 5))
    return_policy = next((p for p in policies if p.get("type") == "return"), None)

    for p in sample_products:
        if mode in ("flashcard", "mixed") and focus in ("products", "all"):
            q = make_factual_question(p)
            if q and (difficulty == "mixed" or q["difficulty"] == difficulty):
                questions.append(q)

        if mode in ("mcq", "mixed") and focus in ("products", "all"):
            q = make_mcq_question(p, products)
            if q and (difficulty == "mixed" or q["difficulty"] == difficulty):
                questions.append(q)

        if mode in ("scenario", "mixed"):
            q = make_scenario_question(p, return_policy)
            if q and (difficulty == "mixed" or q["difficulty"] == difficulty):
                questions.append(q)

    for pol in sample_policies:
        if mode in ("policy", "flashcard", "mixed") and focus in ("policies", "all"):
            q = make_policy_question(pol)
            if q and (difficulty == "mixed" or q["difficulty"] == difficulty):
                questions.append(q)

    for faq in faqs[:count]:
        if mode in ("flashcard", "mixed") and focus in ("all",):
            q = make_faq_question(faq)
            if q:
                questions.append(q)

    # Shuffle and trim to requested count
    random.shuffle(questions)
    return questions[:count]


def main():
    parser = argparse.ArgumentParser(description="Generate quiz questions from knowledge base")
    parser.add_argument("--kb", required=True, help="knowledge_base.json path")
    parser.add_argument("--count", type=int, default=10)
    parser.add_argument("--mode", default="mixed",
                        choices=["flashcard", "mcq", "scenario", "policy", "mixed"])
    parser.add_argument("--difficulty", default="mixed",
                        choices=["easy", "medium", "hard", "mixed"])
    parser.add_argument("--focus", default="all",
                        choices=["products", "policies", "all"])
    parser.add_argument("--output", default=None)
    args = parser.parse_args()

    kb = json.loads(Path(args.kb).read_text(encoding="utf-8"))
    questions = generate_quiz(kb, args.count, args.mode, args.difficulty, args.focus)

    result = {
        "total": len(questions),
        "mode": args.mode,
        "difficulty": args.difficulty,
        "questions": questions,
    }

    output_json = json.dumps(result, ensure_ascii=False, indent=2)
    if args.output:
        Path(args.output).write_text(output_json, encoding="utf-8")
        print(f"✅ Generated {len(questions)} questions → {args.output}", file=__import__("sys").stderr)
    else:
        print(output_json)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)