Training Data Methodology
The dataset contains 170 synthetic phone conversations between a caller and a plumbing receptionist AI. These conversations are generated by a teacher model (Claude) and used to fine-tune smaller student models (Phi4-mini 3.8B, Mistral 7B, Gemma4 E2B 7B) via LoRA on Apple MLX.
Overview
The goal: compress Claude-quality phone conversation ability into models that run locally for free.
Data Format
Conversation Level
Each conversation is stored as a JSON object:
{
"id": "train_001",
"scenario": {
"problem": "clogged_drain",
"personality": "default",
"path": "happy_path"
},
"turns": [
{"role": "agent", "text": "Hi, Smith Plumbing...", "phase": "greeting"},
{"role": "caller", "text": "Yeah, my sink is backed up.", "phase": "problem_determination"}
],
"booking_details": { "service": "...", "date": "...", "time": "...", "caller_name": "...", "phone": "..." },
"metadata": { "turn_count": 14, "has_booking": true, "phases_covered": ["greeting", ...] }
}
Turn Level (What the model actually trains on)
Each conversation is decomposed into multiple turn-level prompt/completion pairs. For each agent reply in a conversation, one training example is created:
- Prompt: System prompt + all prior turns (user and assistant)
- Completion: The single agent reply
This means a 14-turn conversation with 7 agent replies produces 7 training examples. The 170 conversations yield 922 training examples in the train split alone.
Why turn-level instead of whole-conversation? LLMs learn to predict the next token. One completion per example gives a cleaner learning signal. The opening greeting (agent speaks first) would otherwise violate the user→assistant turn order that Mistral and Gemma templates expect. More examples from the same data = stronger training signal without generating more conversations.
The Greeting Turn Problem
In phone calls, the agent speaks first. But training templates expect user→assistant ordering. We solve this with a synthetic [phone rings] user turn before the greeting, so every training example follows the user→assistant pattern.
Scenario Matrix (3 Axes)
Each conversation is defined by a combination of three independent axes:
Problem Types (8)
| Problem | Urgency | Notes |
|---|---|---|
| Clogged drain | Normal | Most common call |
| Leak repair | High | Water damage risk |
| Water heater | Normal | No hot water, pilot light |
| Toilet repair | Normal | Running, clogged, leaking |
| Faucet install | Low | New or replacement |
| Sewer line | High | Main line backup |
| Garbage disposal | Low | Jammed or leaking |
| Emergency | Emergency | Burst pipe, gas smell, flooding |
Caller Personalities (7)
| Personality | Behavior Pattern |
|---|---|
| Default | Cooperative, clear communication |
| Rushed | Terse answers, wants fastest slot |
| Chatty | Tangential stories, slow to the point |
| Confused | Vague descriptions, needs help identifying the problem |
| Angry | Frustrated, complains, needs to be heard |
| Elderly | Slow, repeats, appreciates patience |
| Non-native English | Wrong terms ("water hot machine"), simpler grammar |
Conversation Paths (8)
| Path | Ends with booking? | Description |
|---|---|---|
| Happy path | Yes | Smooth booking |
| No slots | Yes (usually) | Preferred time unavailable, agent offers alternatives |
| Cancellation | No | Caller cancels existing appointment |
| Question only | No | Just asking about pricing/services |
| Emergency escalation | Yes | Urgent — safety first, fast-track dispatch |
| Caller hangup | No | Call ends before completion |
| DIY recommendation | No (usually) | Simple fix the agent can explain |
| Changes mind | Yes | Initially declines, then reconsiders |
Full matrix: 8 x 7 x 8 = 448 possible combinations. We sample ~170 to cover the most important scenarios, weighted toward difficult cases.
Splitting Strategy
Why Not 50/50
A 50/50 train/test split wastes training signal. LoRA fine-tuning updates only ~1.5% of model parameters and works well with 500–1000 examples. The test set needs to be large enough to measure quality reliably across all phases, but no larger.
Stratified Split (75 / 12.5 / 12.5)
| Set | Conversations | Turn-level examples | Purpose |
|---|---|---|---|
| Train | 128 (75%) | 922 | LoRA fine-tuning — the model learns from these |
| Val | 21 (12.5%) | 159 | Overfitting detection during training |
| Test | 21 (12.5%) | 145 | Final tournament evaluation — never seen during training |
Stratification: The split is not random. Conversations are grouped by path (the most behaviorally distinct axis), and each path is proportionally distributed across all three sets. This guarantees:
- Every conversation path appears in train, val, AND test
- All 8 problem types are covered in each set
- All 7 personalities are covered in each set
Why stratify by path? A random split with 170 conversations can easily leave caller_hangup (13 total) entirely absent from the test set. Path determines the conversation structure more than problem or personality — a happy_path booking and a caller_hangup are fundamentally different evaluation targets.
What Each Set Proves
| Set | Question It Answers |
|---|---|
| Train | "Can the model learn to imitate these conversations?" |
| Val | "Is it memorizing or generalizing?" (overfitting check) |
| Test | "How good is it on conversations it's never seen?" |
The val set is checked automatically during training. If val loss diverges from train loss after epoch 2, the model is overfitting — reduce LoRA rank from 8 to 4, or increase dropout.
The test set is used ONLY during tournament evaluation. It's the "exam" — touching it during training would invalidate the results.
Phase Balancing
The dataset is intentionally weighted to cover all 11 canonical phases, with targeted generation for underrepresented phases:
| Phase | Before Balancing | After Balancing | Target |
|---|---|---|---|
greeting | 100 | 128 | ≥50 |
problem_determination | 182 | 232 | ≥50 |
solution_proposal | 90 | 98 | ≥50 |
time_preference | 77 | 79 | ≥50 |
match_propose | 21 | 52 | ≥50 ✓ |
confirmation | 57 | 76 | ≥50 |
summarize_book | 23 | 50 | ≥50 ✓ |
goodbye | 98 | 127 | ≥50 |
emergency_escalation | 34 | 40 | ≥30 ✓ |
cancellation | 17 | 27 | ≥20 ✓ |
caller_hangup | 5 | 13 | ≥10 ✓ |
Bold rows are the phases that were specifically targeted for additional generation.
Why these targets? The model needs enough examples of each phase to learn the distinct behavior pattern. Greeting and goodbye are structurally simple — the model picks them up fast. Match & propose, emergency escalation, and cancellation require nuanced multi-turn behavior that needs more examples.
Validation Pipeline
Every conversation passes through two validation stages:
Structural Validation (hard gates — must pass)
- All required fields present (id, scenario, turns, metadata)
- First turn is agent (agent answers the phone)
- Minimum turn count: 6 for bookings, 4 for non-bookings
- No forbidden AI patterns ("As an AI", "as a language model")
- All phase labels are canonical (after normalization)
Business Validation (soft warnings — flagged for review)
- Reply length: Agent turns must be ≤50 words (it's a phone call)
- Emergency triage: Emergency calls must reach escalation within 6 turns
- Confirmation completeness: Booking confirmations must use the caller's name
- Booking details: All booking fields (service, date, time, name, phone) must be present
- DIY safety: DIY recommendations for dangerous problems (sewer, water heater, emergency) must also recommend a professional
- Pricing policy: Agent-quoted prices must be within reasonable range
- No premature promises: Agent shouldn't promise specific availability before checking calendar
37 business warnings are mostly incomplete booking_details in edge-case conversations.
File Structure
data/
├── scenario_matrix.json # The 8x7x8 scenario definitions
├── conversations/
│ ├── batch_01.json # Original generation (20 conversations)
│ ├── batch_02.json # ...
│ ├── batch_03.json
│ ├── batch_04.json
│ ├── batch_05.json
│ ├── batch_06_hard.json # Hard cases: hangups, cancellations, angry callers
│ ├── batch_07_hard.json # Contrastive examples: corrections, interruptions
│ ├── batch_08_match_propose.json # Phase-targeted: slot negotiation
│ ├── batch_09_hangup.json # Phase-targeted: caller exits
│ ├── batch_10_cancellation.json # Phase-targeted: retention and cancel
│ ├── batch_11_summarize.json # Phase-targeted: booking read-back
│ └── batch_12_emergency.json # Phase-targeted: safety triage
├── all_conversations.json # Merged raw (before normalization)
├── all_conversations_normalized.json # Merged with canonical phase labels
└── splits/
├── train.json / train.jsonl # 128 conversations
├── val.json / val.jsonl # 21 conversations
├── test.json / test.jsonl # 21 conversations
├── train_mlx.jsonl # 922 turn-level examples (MLX format)
├── val_mlx.jsonl # 159 turn-level examples
└── test_mlx.jsonl # 145 turn-level examples
Scripts
| Script | Purpose |
|---|---|
scripts/normalize_and_split.py | Phase normalization, structural + business validation, stratified splitting |
scripts/convert_to_chat_templates.py | Turn-level decomposition, MLX/Phi4/Mistral/Gemma format conversion |
Regeneration
To regenerate or extend the dataset:
- Add new conversation files to
data/conversations/batch_NN_description.json - Run
python3 scripts/normalize_and_split.py— normalizes phases, validates, creates stratified splits - Run
python3 scripts/convert_to_chat_templates.py -i data/splits/train.json -f mlx -o data/splits/train_mlx.jsonl(repeat for val/test) - Check phase distribution — if any phase has <30 training examples, generate more targeted conversations
The pipeline is designed to be additive. New conversations are added as new batch files; the normalizer and splitter always re-process everything from scratch.